fix(commonlisp): derive node ids from the full path stem - #3131
Conversation
`extract_commonlisp` built its id prefix from the bare `path.stem`, so `a/sample.lisp` and `b/sample.lisp` both minted `sample` / `sample_init`. Every other extractor uses `_file_stem(path)`, which preserves the full repo-relative path for exactly this reason (Graphify-Labs#1504); commonlisp never imported it. The in-batch collision rescue hides this when both files land in one extract() call, but incremental `graphify update` extracts in separate batches -- there the ids collide, build() merges them last-writer-wins, and one file's nodes are dropped from the graph. The id-remap post-pass does not cover it either: it rewrites absolute-path-derived ids (Graphify-Labs#502), and a bare stem matches none of its keys. The stem is pre-collapsed through `_make_id` rather than passed to `_cl_id` raw, because `_CL_CHAR_MAP` would map the `/` separators to `_slash` (`tests_slashfixtures_slashsample_init`). Pre-collapsing is idempotent under `_cl_id` and leaves the CL operator-char handling intact (`upi=` -> `upi_eq`). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Fixes Common Lisp node id collisions between same-named .lisp files in different directories by deriving the id prefix from the path-qualified _file_stem (run through _make_id) instead of the bare path.stem, so separate extract batches no longer merge and drop one file's nodes (#1504). Adds a regression test asserting extract_commonlisp mints disjoint ids for a/sample.lisp and b/sample.lisp.
No blocking issues surfaced. 2 lower-confidence candidates did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 547 functions depend on the 547 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract_commonlisp()— 28 callers, 6 callees - new:
_process_form()— 1 callers, 10 callees - new:
_handle_defpackage()— 1 callers, 7 callees - new:
_handle_def_form()— 1 callers, 6 callees
Verification — 547 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: 547 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify extract\_commonlisp.
The verifier did not have enough to check extract\_commonlisp, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
· 4 more finding(s) on lines outside this diff (see the check run).
extract_commonlispbuilds its id prefix from the barepath.stem, soa/sample.lispandb/sample.lispboth mintsample/sample_init. Every other extractor uses_file_stem(path), which preserves the full repo-relative path for exactly this reason (#1504) —commonlisp.pynever imported it. It looks like the line predates the port out ofextract.pyand came along unchanged.Impact
Not cosmetic — nodes are lost. The in-batch collision rescue hides it when both files land in one
extract()call, but incrementalgraphify updateextracts in separate batches:build()merges last-writer-wins andb/sample.lispdisappears. It does warn loudly, which is how I found it.The id-remap post-pass doesn't cover this either: it rewrites absolute-path-derived ids (#502), and a bare stem matches none of its keys.
This also means any graph containing a
.lispfile tripsgraph_has_legacy_ids, soquery/serveprint the "pre-#1504 node-ID scheme" nudge on a freshly built graph. On this repo,tests/fixtures/sample.lispalone was enough.The fix
Pre-collapsed through
_make_idrather than passed to_cl_idraw, because_CL_CHAR_MAPmaps/to_slash— the naivestem = _file_stem(path)that matches the other extractors yieldstests_slashfixtures_slashsample_init. Pre-collapsing is idempotent under_cl_idand leaves the CL operator-char handling intact (upi=->upi_eq, verified viaheader_eq/header_ltin the existing fixture).Testing
Added
test_cl_ids_are_path_qualified_across_directories, modelled on the existingtest_decldef_merge_does_not_merge_across_directories. Red/green verified by reverting the one-line change: fails withoverlap ['sample', 'sample_init'], passes with it.uv run pytest tests/ -q— 5086 passed, 11 skipped, 0 failed.One note for reviewers: the AST cache is keyed by content hash, so after this lands a graph built with an older graphify keeps the old ids until
graphify-out/cache/astis cleared. The cache dir is version-stamped (v0.9.50-s2), so a release bump handles it.🤖 Generated with Claude Code