Skip to content

json_config extractor emits imports/extends edges to node IDs it never creates (package.json, tsconfig.json) #1764

Description

@Stashub

Summary

extract_json emits imports and extends edges whose target node is never created. build_from_json then drops those edges without a warning, because the "does not match any node id" error is explicitly filtered out of real_errors. The result is silent edge loss on two of the most common files in any JS/TS repo: package.json and tsconfig.json.

Nothing surfaces this except graphify.diagnostics.diagnose_extraction, which reports them as dangling_endpoint_edges — and by then the edges are already gone from graph.json.

Version

Reproduced on 0.9.11. Python 3.14.

Reproduction

Two files in an empty directory:

// package.json
{
  "name": "demo",
  "dependencies":    { "left-pad": "^1.3.0" },
  "devDependencies": { "bats": "^1.11.0" }
}
// tsconfig.json
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": { "strict": true }
}
from pathlib import Path
from graphify.extract import extract

for f in ("package.json", "tsconfig.json"):
    r = extract([Path(f)], cache_root=Path("."))
    ids = {n["id"] for n in r["nodes"]}
    dangling = [e for e in r["edges"] if e["source"] not in ids or e["target"] not in ids]
    print(f, len(r["nodes"]), "nodes", len(r["edges"]), "edges", len(dangling), "dangling")
    for e in dangling:
        print("   ", e["source"], "--" + e["relation"] + "-->", e["target"])

Actual

package.json  6 nodes 7 edges 2 dangling
    package_dependencies_left_pad --imports--> left_pad
    package_devdependencies_bats --imports--> bats
tsconfig.json 4 nodes 4 edges 1 dangling
    tsconfig --extends--> ref_tsconfig_base_json

Nodes left_pad, bats, ref_tsconfig_base_json do not exist. At build time all three edges disappear from the graph and no warning is printed.

Cause

graphify/extractors/json_config.py:

# ~line 176 — `extends`
ref_nid = _make_id("ref", val_text)
if ref_nid:
    add_edge(file_nid, ref_nid, "extends", line, context="import")

# line 187-189 — dependency keys
elif parent_key in _DEP_KEYS and val_text:
    dep_nid = _make_id(key)
    if dep_nid:
        add_edge(key_nid, dep_nid, "imports", line, context="import")

grep -n 'dep_nid' extractors/json_config.py returns only those three lines: the target ID is synthesized and used as an edge endpoint, but add_node is never called for it. Same for ref_nid.

The drop is then made invisible in graphify/build.py:

real_errors = [e for e in errors if "does not match any node id" not in e]

so the endpoint-mismatch never reaches the user.

(The $ref branch a few lines above uses the same _make_id("ref", val_text) pattern and looks like it shares the defect, but I could not exercise it: my minimal JSON-Schema test file produced zero nodes, which is #1666. Flagging it as suspected-by-inspection, not verified.)

Expected

Either of these would be self-consistent; today's behaviour is neither:

  1. Create the node. Emit an explicit external node for the dependency / extended config (e.g. file_type: "concept", label left-pad), so the imports / extends edge lands. This is precisely the resolution already adopted for the JS/TS equivalent in JavaScript namespace re-exports emit dangling import targets #1551 ("Create a real namespace binding … and resolve the consumer import to it"), so it would make json_config consistent with the rest of the pipeline.
  2. Do not emit the edge. If, after Cross-language phantom edges: unresolved imports/references bind by name across language boundaries #1749, the intent is that unresolved external references must never bind to a node, then the extractor should simply not create the edge.

Given the #1551 precedent, option 1 looks right. Dependency and extends edges are genuinely useful — they are what most users would expect a package.json / tsconfig.json to contribute to the graph.

Related issues

Same pattern elsewhere

Two other extractors synthesize an edge endpoint without creating the node:

  • extractors/bash.py:185source <non-path-word>add_edge(file_nid, _make_id(raw), "imports", ...)
  • extractors/objc.py:169add_edge(file_nid, _make_id(module), "imports", ...)

Worth fixing as one family.

Impact

Every repository with a package.json or tsconfig.json silently loses its dependency edges.

Concretely: on a 63-file repo I audited, diagnose_extraction reported 49 dangling-endpoint edges. I was able to eliminate 47 of them by re-extracting the affected documents. The remaining two were these package.json imports edges — unfixable from the corpus side, because no input to graphify can make the extractor create the node it points at.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions