Repository navigation
fix(json_config): link a package.json dependency's manifest entry to its ref node (#4569) - #4572
Deepjyoti-Sarmah wants to merge 2 commits into
Conversation
…its ref node (Graphify-Labs#4569) Each dependency produced two nodes with the same label: the manifest entry (reached through `dependencies --contains-->`) and the `ref_*` node that code imports resolve to. The fix for Graphify-Labs#1764 moved the `imports` edge to the manifest, which removed the only link between them, so the entry and the code that uses the package fell into different communities, and `explain` and `affected` found two nodes for the name. The entry now has a `declares` edge to its ref node, and its label is `name@range` (the range is one line, capped at 40 characters), so the two nodes no longer share a label and nothing reads as `react -> react`. The ref node keeps the bare name that imports resolve to. An empty or blank range links nothing and keeps the bare name. Values that are not strings are extracted as before. A key shared by two sibling blocks is relabelled only for the pair on its own line. The AST cache schema goes from 17 to 18 so cached package.json files are extracted again.
…ed to its ref node (Graphify-Labs#4569)
|
Thanks for the pull request, @Deepjyoti-Sarmah. A maintainer will review it soon. Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions. A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic. |
There was a problem hiding this comment.
Graphify reviewed this change.
No issues found by static checks — no coupling regressions or blocking issues in the code graph. That is not the same as safe to merge: see what was not checked.
Not checked: tests were not run; formal verification proved 0 of 1 changed function(s) (1 sampled, not proven).
Formal verification. PR-changed functions: 1/1 verified (0 proven, 1 may-equivalent, 0 distinguished) · 0 not verified.
Graphify review — findings
Fixes disconnected package.json dependency entries (#4569): each manifest entry now links to its ref node with a declares edge, so the entry and the code that imports the package land in the same community. The entry is relabelled name@range (range collapsed to one line, capped at 40 chars) to avoid a same-label react -> react edge and ambiguous explain/affected lookups, while the ref node keeps the bare name that import "react" resolves to; a blank range links nothing and the entry keeps its bare name. Bumps _AST_CACHE_SCHEMA to 18 so stale cached extractions are not served, and adds tests covering all four dependency blocks, local-module name clashes, and a two-manifest corpus case marked strict xfail whose node lookup passes a path where a label is expected.
No blocking issues surfaced. 4 lower-confidence candidates did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 3021 functions depend on the 684 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
extract()— 976 callers, 59 callees - worse:
save_cached()— 49 callers, 9 callees - worse:
extract_json()— 33 callers, 7 callees - new:
test_a_dev_dependency_nothing_imports_is_not_reported_isolated()— 0 callers, 6 callees
Verification — 3021 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 1798 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
40 of 432 test file(s) selected (9%) via static blast radius.
tests/test_cache.py— impacttests/test_cache_stale_import_target.py— impacttests/test_charmap_encoding.py— impacttests/test_chunking.py— impacttests/test_csharp_partial_classes.py— impacttests/test_definition_file_portability.py— impacttests/test_detect.py— impacttests/test_extract.py— impact, changed-testtests/test_extract_cache_location.py— impacttests/test_extract_cli.py— impacttests/test_grammar_cache_key.py— impacttests/test_ignore_file_encoding.py— impacttests/test_incremental_mtime_collision.py— impacttests/test_java_member_calls.py— impacttests/test_js_member_collisions.py— impacttests/test_json_schema_config.py— impacttests/test_language_override_lifecycle.py— impacttests/test_language_overrides.py— impacttests/test_llm_backends.py— impacttests/test_manifest_ingest.py— impacttests/test_objc_field_table_remap.py— impacttests/test_out_dir_evidence.py— impacttests/test_partial_cache.py— impacttests/test_pipeline.py— impacttests/test_python_inherited_method_calls.py— impacttests/test_python_property_reads.py— impacttests/test_python_pytest_fixture_receivers.py— impacttests/test_rust_self_member_calls.py— impacttests/test_rust_use_trees.py— impacttests/test_semantic_cache_basename_recovery.py— impacttests/test_semantic_cache_out_root.py— impacttests/test_skill_persisted_scan_options.py— impacttests/test_skill_semantic_all_cached.py— impacttests/test_stale_prune.py— impacttests/test_stat_index_husk.py— impacttests/test_stat_index_multi_root.py— impacttests/test_stat_index_portability.py— impacttests/test_watch_manifest_location.py— impacttests/test_word_count_cache.py— impacttests/test_zero_node_no_cache.py— impact
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
Formal verification
No difference found (not proven): No behavior difference found in extract\_json (not a proof).
The verifier ran both versions of extract\_json on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
· 1 grounded finding(s) anchored inline below; 3 more finding(s) on lines outside this diff (see the check run).
| assert resolve_seed(G, "react") is not None # what `graphify affected react` calls | ||
|
|
||
|
|
||
| def test_a_dev_dependency_nothing_imports_is_not_reported_isolated(tmp_path): |
There was a problem hiding this comment.
test_a_dev_dependency_nothing_imports_is_not_reported_isolated()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
What does this PR do?
Fixes #4569. Each package.json dependency produced two nodes labelled
reactwith no edge between them:<stem>_dependencies_react(type code, carries the version-range line), reached throughdependencies --contains-->;ref_react(type concept), reached throughpackage.json --imports-->. Code imports (import React from "react") resolve here.5711e9d (#1764 follow-up) moved the
importsedge fromkey_nidtofile_nidso it would stop reading asreact --imports--> react. That edge was the only link between the two nodes. On v8 the manifest entry is 4 hops from the code that imports the package, the two land in different communities,explain reactexits 3 ("Ambiguous: 'react' matches 2 nodes in package.json"), andaffected reactprints "No unique node match".The manifest entry now has a
declaresedge to the ref node and is labelledname@range:elif parent_key in _DEP_KEYS and val_text.strip(): dep_nid = _make_id("ref", key) add_node(dep_nid, key, line, file_type="concept") add_edge(file_nid, dep_nid, "imports", line, context="import") + add_edge(key_nid, dep_nid, "declares", line) + version = one line of val_text, capped at 40 chars ("...") + relabel key_nid to f"{key}@{version}" # only if its source_location is this lineThe ref node keeps the bare name that imports resolve to, so the two nodes no longer share a label: no
react -> reactedge, andreactresolves to one node. A blank range (" ") now behaves like"": bare label, no ref, no edge._AST_CACHE_SCHEMAgoes from 17 to 18.The relabel checks
source_locationbecause a key node id carries only the immediate parent key. In a v2 lockfile,dependencies.semverunder twonode_modules/*entries mints one node;add_nodekeeps the first, so relabelling every pair would put the later range on the first line (4 of 44 relabelled nodes on a 340-package lockfile).Before (the issue's package.json):
After:
I chose this after writing tests for the wanted behaviour and running four candidates against them:
declaresedgereferencesedgename@range(this PR)explain/affectedresolvereacttest_extract_json_nested_contains)A2 also makes
affectedlist the manifest line as a consumer, sincereferencesis inDEFAULT_AFFECTED_RELATIONS.declaresis not, and nothing in graphify whitelists relation names.Type of change
Verification & Invariants
A package.json dependency's manifest entry is connected to the code that imports the package, and no edge joins two nodes with the same label.
Merge Danger
Door: two-way. One edge and one relabel in the
_DEP_KEYSbranch, plus a cache schema bump. No id changes. Reverting restores v8 output after one re-extract.Blast Radius: moderate. Every manifest entry under a dependency block changes label (
reactbecomesreact@^19.0.0), and anything keyed on that label sees the new text. The ref node keepsreact, so searching for the package still finds it. Bumping_AST_CACHE_SCHEMAinvalidates the whole AST cache, so the first forced run after upgrading re-extracts every code file once.Limitations
package-lock.jsonalready takes this path on v8 (its top-leveldependencieskey makes it config JSON). Its nested entries get the new label and edge too: 99declaresedges on a 340-package lockfile. v3 lockfiles are skipped as data JSON. I did not change_is_config_json.""and" "as*. graphify has never linked"", unchanged here;" "used to mintref_reactand animportsedge and now matches"".left-pad@workspace:*.react@19.0.0.md); the existing numeric-suffix dedup handles collisions.a_package_json_ref_react) while code imports a separateref_react, so in a monorepo the manifest and the code still don't connect. Separate bug, recorded as a strict xfail.@types/*key nodes show as isolated. On v8 they are already exempt (Weakly-connected / "Knowledge Gaps" signal doesn't filter AST-origin declarations without a () suffix — false positives dominate #4205); the flagged node is the ref node, which this PR now connects.How was this tested?
New tests in
tests/test_extract.py, next to the existing json_config tests. The build-level tests runextract+build_from_jsonon a package.json and a.tsxthat importsreact. The manifest entry is found by structure (containsfrom the dependency block), not by label, so a relabel can't pass them on its own.explain/affectedresolve the package nameimports, no self-looputilsdependency stays a namespacedref_The ones that pass on v8 guard #1764, J-4 and existing output.
Graphify-specific checklist
uv run python -m tools.skillgen --bless) when changing their source fragments. (N/A: no skill fragments touched.)