mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-12 02:17:04 +00:00
e2ef4ef3d1
#1631: a malformed LLM chunk (a stray non-dict entry in edges/nodes/hyperedges) crashed the AST+semantic merge and the semantic-cache write with `AttributeError: 'list' object has no attribute 'get'`, discarding every successful chunk and writing no graph.json. `_parse_llm_json` now sanitizes each fragment at the single parse chokepoint (dict entries only; non-list values coerced to []), protecting the cache writer, the adaptive-retry merge, and the CLI merge in one place. #1638: an unresolved bare npm import (`import colors from "tailwindcss/colors"`) emitted an imports_from edge to the bare id `colors`, which build.py's pre-migration alias index then remapped onto an unrelated local file of that stem (backend/utils/colors.py) - a confident EXTRACTED cross-language phantom edge, one per importing file. The external-import fallback now namespaces its target with the `ref` prefix (the J-4 convention), so it can never collapse to a local node id; the ref target has no node, so build drops it as an external reference. #1632: with a parallel LLM backend, extract_corpus_parallel merged chunk results in completion order, so which network call returned first reordered nodes/edges run-to-run even when the model returned identical content - churning graph.json. Chunks are now merged in deterministic submission order after the pool drains (matching the serial path); the progress callback still fires in completion order. The model's own content variance is unchanged (irreducible). Full suite: 2882 passed, 3 skipped. Validated end-to-end via a local wheel build on a mixed TS+Python corpus: `explain colors.py` shows only the real importer, and graph.json is byte-identical across repeated runs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
117 lines
4.3 KiB
Python
117 lines
4.3 KiB
Python
"""#1638 — an unresolved bare npm import must not alias onto an unrelated
|
|
same-named local file, producing a confident cross-language phantom edge.
|
|
|
|
`import colors from "tailwindcss/colors"` in a .tsx file used to emit an
|
|
`imports_from` edge to the bare id ``colors``. build.py's pre-migration alias
|
|
index registers every local file's bare stem (``backend/utils/colors.py`` ->
|
|
alias ``colors``), so the dangling ``colors`` target was remapped onto the
|
|
Python file — an EXTRACTED-confidence edge between two files in different
|
|
languages with no real relationship.
|
|
|
|
The fix namespaces the external-import fallback id with the ``ref`` prefix (the
|
|
J-4 convention), so it can never collide with a local file/symbol node id.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from graphify.build import build_from_json
|
|
from graphify.extract import _make_id, _resolve_js_import_target, extract
|
|
|
|
|
|
def _write(path: Path, text: str) -> Path:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(text, encoding="utf-8")
|
|
return path
|
|
|
|
|
|
# ── unit: the resolver never returns a bare local-shaped id for an external ──
|
|
|
|
|
|
def test_unresolved_bare_import_is_ref_namespaced():
|
|
tgt, resolved_path = _resolve_js_import_target(
|
|
"tailwindcss/colors", "frontend/src/SomeChart.tsx"
|
|
)
|
|
assert resolved_path is None
|
|
# Must not be the bare last-segment id that collides with a local `colors` file.
|
|
assert tgt != _make_id("colors")
|
|
assert tgt != _make_id("colors.py")
|
|
assert tgt.startswith("ref")
|
|
|
|
|
|
def test_scoped_package_import_is_ref_namespaced():
|
|
tgt, resolved_path = _resolve_js_import_target(
|
|
"@scope/utils", "src/thing.ts"
|
|
)
|
|
assert resolved_path is None
|
|
assert tgt != _make_id("utils")
|
|
assert tgt.startswith("ref")
|
|
|
|
|
|
# ── end-to-end: the reporter's exact synthetic monorepo ─────────────────────
|
|
|
|
|
|
def test_no_phantom_edge_from_tsx_to_unrelated_python_file(tmp_path: Path):
|
|
py = _write(
|
|
tmp_path / "backend/utils/colors.py",
|
|
"def hex_to_rgb(value):\n return (0, 0, 0)\n",
|
|
)
|
|
tsx = _write(
|
|
tmp_path / "frontend/src/SomeChart.tsx",
|
|
'import colors from "tailwindcss/colors";\n\n'
|
|
"export const CHART_COLOR = colors.blue[500];\n",
|
|
)
|
|
|
|
result = extract([py, tsx], cache_root=tmp_path / "graphify-out")
|
|
G = build_from_json(result, root=str(tmp_path))
|
|
|
|
# Find the python file node.
|
|
py_ids = [
|
|
n for n, d in G.nodes(data=True)
|
|
if str(d.get("source_file", "")).endswith("colors.py")
|
|
]
|
|
assert py_ids, "colors.py should have produced at least one node"
|
|
|
|
# No edge from the TSX file (or any TS symbol) should land on the python file
|
|
# as an imports_from relationship.
|
|
for u, v, d in G.edges(data=True):
|
|
if d.get("relation") != "imports_from":
|
|
continue
|
|
endpoints = {u, v}
|
|
if endpoints & set(py_ids):
|
|
other = (endpoints - set(py_ids)) or endpoints
|
|
srcfiles = {str(G.nodes[e].get("source_file", "")) for e in other}
|
|
assert not any(sf.endswith((".tsx", ".ts")) for sf in srcfiles), (
|
|
f"phantom cross-language imports_from edge onto colors.py: "
|
|
f"{u} -> {v} ({d})"
|
|
)
|
|
|
|
|
|
def test_multiple_tsx_files_do_not_all_alias_onto_one_python_file(tmp_path: Path):
|
|
# The real-world symptom: N unrelated .tsx files all doing the same bare
|
|
# import showed up as N imports_from sources on one python module.
|
|
_write(
|
|
tmp_path / "backend/utils/colors.py",
|
|
"def hex_to_rgb(value):\n return (0, 0, 0)\n",
|
|
)
|
|
for i in range(3):
|
|
_write(
|
|
tmp_path / f"frontend/src/Chart{i}.tsx",
|
|
'import colors from "tailwindcss/colors";\n'
|
|
f"export const C{i} = colors.blue;\n",
|
|
)
|
|
|
|
paths = list((tmp_path).rglob("*.py")) + list((tmp_path / "frontend").rglob("*.tsx"))
|
|
result = extract(paths, cache_root=tmp_path / "graphify-out")
|
|
G = build_from_json(result, root=str(tmp_path))
|
|
|
|
py_ids = {
|
|
n for n, d in G.nodes(data=True)
|
|
if str(d.get("source_file", "")).endswith("colors.py")
|
|
}
|
|
phantom = [
|
|
(u, v) for u, v, d in G.edges(data=True)
|
|
if d.get("relation") == "imports_from" and ({u, v} & py_ids)
|
|
]
|
|
assert not phantom, f"phantom edges onto colors.py: {phantom}"
|