diff --git a/graphify/build.py b/graphify/build.py index 2c2c773bb..af7349891 100644 --- a/graphify/build.py +++ b/graphify/build.py @@ -51,7 +51,9 @@ def build_from_json(extraction: dict, *, directed: bool = False) -> nx.Graph: # Canonicalize legacy node/edge schema before validation. for node in extraction.get("nodes", []): - if isinstance(node, dict) and "source" in node and "source_file" not in node: + if not isinstance(node, dict): + continue + if "source" in node and "source_file" not in node: # Count edges that reference this node so the warning is actionable (#479) node_id = node.get("id", "?") affected_edges = sum( @@ -65,6 +67,12 @@ def build_from_json(extraction: dict, *, directed: bool = False) -> nx.Graph: file=sys.stderr, ) node["source_file"] = node.pop("source") + # Default missing/None file_type to "concept" so legacy graph.json + # entries (and stub nodes preserved by `_rebuild_code` from older + # graphify versions that didn't always populate file_type) don't + # trigger spurious "invalid file_type 'None'" validator warnings (#660). + if node.get("file_type") in (None, ""): + node["file_type"] = "concept" errors = validate_extraction(extraction) # Dangling edges (stdlib/external imports) are expected - only warn about real schema errors. diff --git a/tests/test_build.py b/tests/test_build.py index 2b47bc86d..3750b57d7 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -60,3 +60,56 @@ def test_build_merges_multiple_extractions(): G = build([ext1, ext2]) assert G.number_of_nodes() == 2 assert G.number_of_edges() == 1 + + +def test_none_file_type_defaults_to_concept(capsys): + """Legacy nodes with file_type=None (e.g. preserved from older graph.json + by `_rebuild_code`) must not trigger 'invalid file_type None' warnings (#660).""" + ext = { + "nodes": [ + {"id": "n1", "label": "Stub", "file_type": None, "source_file": "a.py"}, + {"id": "n2", "label": "Real", "file_type": "code", "source_file": "b.py"}, + ], + "edges": [], + "input_tokens": 0, + "output_tokens": 0, + } + G = build_from_json(ext) + err = capsys.readouterr().err + assert "invalid file_type" not in err + # The legacy node still exists in the graph and has been canonicalized + assert G.nodes["n1"]["file_type"] == "concept" + assert G.nodes["n2"]["file_type"] == "code" + + +def test_missing_file_type_defaults_to_concept(capsys): + """Nodes missing file_type entirely should also be canonicalized to 'concept'.""" + ext = { + "nodes": [ + {"id": "n1", "label": "Bare", "source_file": "a.py"}, + ], + "edges": [], + "input_tokens": 0, + "output_tokens": 0, + } + G = build_from_json(ext) + err = capsys.readouterr().err + assert "invalid file_type" not in err + assert "missing required field 'file_type'" not in err + assert G.nodes["n1"]["file_type"] == "concept" + + +def test_real_invalid_file_type_still_warns(capsys): + """Truly invalid file_type values (not None, not empty) must still warn.""" + ext = { + "nodes": [ + {"id": "n1", "label": "Bad", "file_type": "weird_type", "source_file": "a.py"}, + ], + "edges": [], + "input_tokens": 0, + "output_tokens": 0, + } + build_from_json(ext) + err = capsys.readouterr().err + assert "invalid file_type" in err + assert "weird_type" in err