Skip to content

Absolute-path node IDs with no canonical twin in SQL references and JS calls edges (0.9.28) — not covered by the #1961/#1967 fix #2324

Closed

Description

@Kenta-Nakakoshi

Summary

On graphifyy 0.9.28, the AST extractor emits references and calls edges whose target ID carries the absolute scan path, while the source ID from the same file in the same run is correctly repo-relative. The target never matches a node and the edge is dropped at build.

This looks like #1961 (fixed in 0.9.18 via #1967) but is not covered by that fix. #1961 was a duplicate edge: the absolute-path variant always had a canonical repo-relative twin, and the fix drops the duplicate so the correct edge survives. In my corpus there is no twin — I checked all 224 affected refs and 0 resolve after stripping the prefix, because the repo-relative node was never created either. So the relationship isn't doubled, it's lost.

It also isn't limited to JS/TS tsconfig-alias imports: the clearest case here is a SQL foreign key.

(Paths redacted to users_<username>_...; real IDs embed the absolute home directory.)

Environment

  • graphifyy 0.9.28, Python 3.13, macOS 15, tree_sitter_sql installed
  • Corpus: 187 code files (52 .tsx, 39 .js, 14 .ts, 11 .sql)
  • AST extraction: 1330 nodes / 3376 edges → 615 dangling, of which 224 (184 distinct IDs) are this absolute-path shape

Reproduction — SQL foreign key, both endpoints in one file

prisma/migrations/20260723140000_kc_stock_gap_p0/migration.sql declares Tenant and StockCountSession and adds an FK between them. The emitted edge:

relation        : references
source          : prisma_migrations_20260723140000_kc_stock_gap_p0_migration_stockcountsession   [node exists: True]
target          : users_<username>_yys_..._prisma_migrations_20260723140000_kc_stock_gap_p0_migration_tenant   [node exists: False]
source_file     : prisma/migrations/20260723140000_kc_stock_gap_p0/migration.sql
source_location : L92
confidence      : EXTRACTED

Tenant is a node — as prisma_migrations_20260723140000_kc_stock_gap_p0_migration_tenant. But no edge is emitted to that ID; the absolute-path one is the only edge for this FK. Same pattern on L95, L101, L104 (stockgapevent → tenant, → user).

JS/TS calls targets are affected the same way, e.g. users_<username>_yys_..._src_components_planningview_fetchinboxdata.

Dangling edges by relation (all 615):

relation count
imports_from 348
calls 179
imports 43
indirect_call 33
references 12

Why the #1961 fix doesn't help

The #1967 fix drops the absolute-path edge when its canonical twin exists. Measured on this corpus:

absolute-leaked dangling refs recoverable by stripping the prefix : 0   (0 distinct)
absolute-leaked dangling refs with NO relative twin               : 224 (184 distinct)

Examples with no twin (prefix already stripped for readability):

prisma_migrations_20260728132311_add_record_comments_reactions_migration_user   (4 refs)
src_components_planningview_fetchinboxdata                                      (4 refs)
src_components_planningview_handlesaveinlinecontact                             (3 refs)
src_components_pushnotificationsettings_handleenable                            (3 refs)
src_components_settingsview_handlesaveofficelocations                           (3 refs)

So a twin-based dedupe has nothing to fall back on here — the fix needs to be at ID construction, or the referenced entity needs a node.

Verified not the cause

Each measured rather than assumed; all three give byte-identical results (1330 nodes / 3376 edges / 615 dangling):

  1. Omitting root=. SKILL.md Step 3 Part A calls extract(files, cache_root=Path(INPUT_PATH)) with no root=, while Step 4 passes root=INPUT_PATH to build_from_json. Adding root=Path('.') to extract() changes nothing.
  2. Cache pollution. A fresh empty cache_root, so no pre-0.9.18-shaped entry can be replayed, changes nothing.
  3. Parallel workers. parallel=False reproduces exactly.

Could not isolate a single-file repro

Extracting that migration.sql alone gives 0 dangling under every variation tried — relative vs absolute input path, nested vs top-level directory, with and without root=. The trigger seems to need the multi-file corpus resolution context. Happy to run any diagnostic you want against the real corpus.

Incidentally, a single-file extraction of a nested path produces a different file-node ID depending on whether the input path is relative (..._migration) or absolute (..._migration_sql, extension retained) — possibly the same normalization seam.

Diagnostic script

import json
from collections import Counter
from pathlib import Path
from graphify.extract import collect_files, extract
from graphify.detect import detect

det = detect(Path('.'))
files = []
for f in det['files']['code']:
    files.extend(collect_files(Path(f)) if Path(f).is_dir() else [Path(f)])

res = extract(files, cache_root=Path('.'), root=Path('.'), parallel=False)
ids = {n['id'] for n in res['nodes']}
bad = [e for e in res['edges'] if e['source'] not in ids or e['target'] not in ids]
print(f'{len(res["nodes"])} nodes, {len(res["edges"])} edges, dangling {len(bad)}')
print('by relation:', Counter(e.get('relation') for e in bad).most_common())

miss = Counter()
for e in bad:
    for side in ('source', 'target'):
        if e[side] not in ids:
            miss[e[side]] += 1

# how many absolute-path targets have a canonical twin?  (0 here)
PREFIX = 'users_<username>_...._'   # scan root rendered as an id
rec = sum(v for k, v in miss.items() if k.startswith(PREFIX) and k[len(PREFIX):] in ids)
orph = sum(v for k, v in miss.items() if k.startswith(PREFIX) and k[len(PREFIX):] not in ids)
print(f'recoverable by stripping: {rec} | no twin: {orph}')

Note on the other 347

The remaining ref_* external-module danglers (ref_react 64, ref_lucide_react 57, ref_express 40, …) are already tracked in #2191 / #2130 / #2265 / #2143, so I'm not re-filing those. Mentioning them only because together they make the health warning fire on every run, which is what sent me looking.

Thanks for graphify — the AST-only path on a code corpus is genuinely useful. Building a 1500-node graph with zero LLM spend on the code side is exactly what we needed under a token budget.

Activity

  1. safishamsi commented on Jul 30, 2026

    @safishamsi
    Member

    Fixed in v0.9.31. SQL references to a table defined in another file now mint a sourceless stub that collapses onto the real table node (Go #1402 pattern) instead of an absolute-path id, and identifiers are normalized so "public"."users" matches public.users — so cross-file FKs resolve and no scan-root slug leaks. https://github.com/Graphify-Labs/graphify/releases/tag/v0.9.31

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions