From eca0a9dcdef4968dff4ba44b977e76f0e894f680 Mon Sep 17 00:00:00 2001 From: oleksii-tumanov Date: Fri, 10 Jul 2026 01:50:01 -0500 Subject: [PATCH 1/2] fix(extract): keep json config edges grounded --- graphify/extractors/json_config.py | 7 +++-- tests/test_extract.py | 44 ++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/graphify/extractors/json_config.py b/graphify/extractors/json_config.py index 025ca20a8..90dab918e 100644 --- a/graphify/extractors/json_config.py +++ b/graphify/extractors/json_config.py @@ -90,10 +90,10 @@ def extract_json(path: Path) -> dict: "optionalDependencies", "bundleDependencies", "bundledDependencies", }) - def add_node(nid: str, label: str, line: int) -> None: + def add_node(nid: str, label: str, line: int, file_type: str = "code") -> None: if nid and nid not in seen_ids: seen_ids.add(nid) - nodes.append({"id": nid, "label": label, "file_type": "code", + nodes.append({"id": nid, "label": label, "file_type": file_type, "source_file": str_path, "source_location": f"L{line}"}) def add_edge(src: str, tgt: str, relation: str, line: int, @@ -165,6 +165,7 @@ def walk_object(obj_node, parent_nid: str, parent_key: str | None, if ref: ref_nid = _make_id("ref", ref) if ref_nid: + add_node(ref_nid, ref, line, file_type="concept") add_edge(key_nid, ref_nid, "extends", line, context="import") elif val.type == "string": @@ -175,6 +176,7 @@ def walk_object(obj_node, parent_nid: str, parent_key: str | None, # Namespace external refs to avoid ID collision with file nodes (J-4) ref_nid = _make_id("ref", val_text) if ref_nid: + add_node(ref_nid, val_text, line, file_type="concept") add_edge(file_nid, ref_nid, "extends", line, context="import") elif key == "$ref" and val_text: @@ -186,6 +188,7 @@ def walk_object(obj_node, parent_nid: str, parent_key: str | None, elif parent_key in _DEP_KEYS and val_text: dep_nid = _make_id(key) if dep_nid: + add_node(dep_nid, key, line, file_type="concept") add_edge(key_nid, dep_nid, "imports", line, context="import") # Entry: find root document → object diff --git a/tests/test_extract.py b/tests/test_extract.py index 545a4f5d0..779b4c3ae 100644 --- a/tests/test_extract.py +++ b/tests/test_extract.py @@ -6,6 +6,7 @@ import pytest +from graphify.build import build_from_json from graphify.extract import extract_python, extract, collect_files, _make_id, extract_bash, extract_json, _DISPATCH FIXTURES = Path(__file__).parent / "fixtures" @@ -1468,6 +1469,49 @@ def test_extract_json_extends_resolved(): assert extends_edges[0].get("context") == "import" +def test_extract_json_import_and_extends_targets_are_real_nodes(tmp_path): + package_json = tmp_path / "package.json" + package_json.write_text(json.dumps({ + "name": "demo", + "dependencies": {"left-pad": "^1.3.0"}, + "devDependencies": {"bats": "^1.11.0"}, + })) + tsconfig = tmp_path / "tsconfig.json" + tsconfig.write_text(json.dumps({ + "extends": "./tsconfig.base.json", + "compilerOptions": {"strict": True}, + })) + + results = [extract_json(package_json), extract_json(tsconfig)] + combined = { + "nodes": [node for result in results for node in result["nodes"]], + "edges": [edge for result in results for edge in result["edges"]], + } + node_ids = {node["id"] for node in combined["nodes"]} + dangling = [ + edge for edge in combined["edges"] + if edge["source"] not in node_ids or edge["target"] not in node_ids + ] + assert dangling == [] + assert {"left-pad", "bats", "./tsconfig.base.json"} <= { + node["label"] for node in combined["nodes"] if node["file_type"] == "concept" + } + + graph = build_from_json(combined, directed=True) + import_targets = { + graph.nodes[data["_tgt"]]["label"] + for _, _, data in graph.edges(data=True) + if data.get("relation") == "imports" + } + extends_targets = { + graph.nodes[data["_tgt"]]["label"] + for _, _, data in graph.edges(data=True) + if data.get("relation") == "extends" + } + assert {"left-pad", "bats"} <= import_targets + assert extends_targets == {"./tsconfig.base.json"} + + def test_extract_json_large_file_skipped(tmp_path): big = tmp_path / "big.json" # Write a JSON file just over 1 MiB From cbec4aa0b419519ee7891df8fa17e8822f13325b Mon Sep 17 00:00:00 2001 From: oleksii-tumanov Date: Fri, 10 Jul 2026 02:13:02 -0500 Subject: [PATCH 2/2] test(extract): cover json config built edges --- tests/test_extract.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_extract.py b/tests/test_extract.py index 779b4c3ae..6247ec59c 100644 --- a/tests/test_extract.py +++ b/tests/test_extract.py @@ -1497,7 +1497,8 @@ def test_extract_json_import_and_extends_targets_are_real_nodes(tmp_path): node["label"] for node in combined["nodes"] if node["file_type"] == "concept" } - graph = build_from_json(combined, directed=True) + extracted = extract([package_json, tsconfig], cache_root=tmp_path, parallel=False) + graph = build_from_json(extracted, directed=True) import_targets = { graph.nodes[data["_tgt"]]["label"] for _, _, data in graph.edges(data=True) @@ -1508,6 +1509,11 @@ def test_extract_json_import_and_extends_targets_are_real_nodes(tmp_path): for _, _, data in graph.edges(data=True) if data.get("relation") == "extends" } + self_loops = [ + data for _, _, data in graph.edges(data=True) + if data.get("relation") in {"imports", "extends"} and data["_src"] == data["_tgt"] + ] + assert self_loops == [] assert {"left-pad", "bats"} <= import_targets assert extends_targets == {"./tsconfig.base.json"}