fix(extract): canonicalize source_file to POSIX on every node and edge - #2627
fix(extract): canonicalize source_file to POSIX on every node and edge#2627rajarshidattapy wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.
Formal verification. 5 change(s) tested, no difference found (not proven).
Graphify review — findings
This pull request appears to revert or roll back a set of changes: the CHANGELOG.md is edited to remove the 0.9.39 and 0.9.40 release note sections and to mark 0.9.38 as "unreleased" again. Correspondingly, graphify/build.py replaces uses of a cross-platform absolute-path helper (is_absolute_any_platform / _is_abs) with os.path.isabs and Path.is_absolute in path normalization, id remapping, and legacy-id detection logic. The surface area spans the build, cache, extraction, resolution, watch, and serve modules along with a large number of associated tests, so the change touches path handling, import/edge resolution, and related test coverage broadly.
Worth a look
- graph_has_legacy_ids no longer detects POSIX-absolute source_files on Windows —
graphify/build.py:688· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 4829 functions depend on the 2969 functions this change touches.
Health — grade B; 10 existing hotspot(s) in the area this change touches (pre-existing, not introduced here):
extract()— 401 callers, 39 callees (high)_rebuild_code()— 93 callers, 51 callees (high)build_from_json()— 151 callers, 17 callees (high)detect()— 82 callers, 13 callees (high)deduplicate_entities()— 53 callers, 19 callees (high)build_merge()— 41 callers, 14 callees (high)save_semantic_cache()— 50 callers, 9 callees (high)_extract_generic()— 18 callers, 22 callees (high)- …and 2 more
Verification — 4829 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: 4548 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify \_abs\_identity.
The verifier did not have enough to check \_abs\_identity, 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: not verifiable: the input domain has 100 values but only 10 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)
Could not verify: Could not verify \_derive\_prune\_root.
The verifier did not have enough to check \_derive\_prune\_root, 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 `prune_sources` is annotated `'list[str]'` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_norm\_source\_file.
The verifier did not have enough to check \_norm\_source\_file, 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: not verifiable: the input domain has 100 values but only 10 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)
No difference found (not proven): No behavior difference found in \_semantic\_id\_remap (not a proof).
The verifier ran both versions of \_semantic\_id\_remap 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.
Could not verify: Could not verify build\_from\_json.
The verifier did not have enough to check build\_from\_json, 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: not verifiable: all 5 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify graph\_has\_legacy\_ids.
The verifier did not have enough to check graph\_has\_legacy\_ids, 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 `root` is annotated `str | Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify cached\_word\_count.
The verifier did not have enough to check cached\_word\_count, 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
Could not verify: Could not verify file\_hash.
The verifier did not have enough to check file\_hash, 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
Could not verify: Could not verify \_llm\_tiebreak.
The verifier did not have enough to check \_llm\_tiebreak, 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: not verifiable: all 105 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify deduplicate\_entities.
The verifier did not have enough to check deduplicate\_entities, 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: not verifiable: all 7 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_is\_ignored.
The verifier did not have enough to check \_is\_ignored, 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
No difference found (not proven): No behavior difference found in \_nfc (not a proof).
The verifier ran both versions of \_nfc 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.
No difference found (not proven): No behavior difference found in \_parse\_gitignore\_line (not a proof).
The verifier ran both versions of \_parse\_gitignore\_line 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.
No difference found (not proven): No behavior difference found in to\_json (not a proof).
The verifier ran both versions of to\_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.
No difference found (not proven): No behavior difference found in to\_html (not a proof).
The verifier ran both versions of to\_html 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.
Could not verify: Could not verify \_rescue\_js\_dynamic\_imports.
The verifier did not have enough to check \_rescue\_js\_dynamic\_imports, 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
Could not verify: Could not verify \_resolve\_objc\_member\_calls.
The verifier did not have enough to check \_resolve\_objc\_member\_calls, 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: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
Could not verify: Could not verify extract.
The verifier did not have enough to check extract, 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 `cache_root` is annotated `Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_extract\_generic.
The verifier did not have enough to check \_extract\_generic, 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
Could not verify: Could not verify \_js\_local\_bound\_names.
The verifier did not have enough to check \_js\_local\_bound\_names, 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: not verifiable: all 12 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_go\_collect\_type\_refs.
The verifier did not have enough to check \_go\_collect\_type\_refs, 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: not verifiable: all 84 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify extract\_go.
The verifier did not have enough to check extract\_go, 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
Could not verify: Could not verify extract\_objc.
The verifier did not have enough to check extract\_objc, 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
Could not verify: Could not verify \_merge\_decl\_def\_classes.
The verifier did not have enough to check \_merge\_decl\_def\_classes, 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: not verifiable: the input domain has 49 values but only 7 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)
Could not verify: Could not verify \_probe\_python\_module\_candidate.
The verifier did not have enough to check \_probe\_python\_module\_candidate, 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 `candidate` is annotated `Path` — outside the synthesizable primitive/collection set
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.
Formal verification. 5 change(s) tested, no difference found (not proven).
Graphify review — findings
This PR reverts changelog entries for versions 0.9.38 (marking it back to "unreleased"), 0.9.39, and 0.9.40, removing their associated notes including the cross-platform absolute-path fix (#2618). Correspondingly, it removes the is_absolute_any_platform helper usage in graphify/build.py, reverting several call sites back to os.path.isabs() / Path.is_absolute(). The touched surface spans build.py, cache.py, and a broad set of test files across build, extract, detect, watch, serve, dedup, exporters, and graphify internals. Note: the diff was truncated, so changes beyond CHANGELOG.md and the start of build.py/cache.py aren't fully visible for summarization.
Worth a look
- Cross-platform absolute-path detection reverted to os.path.isabs, reintroducing #2618 Windows id leak —
graphify/build.py:611· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 4829 functions depend on the 2969 functions this change touches.
Health — grade B; 10 existing hotspot(s) in the area this change touches (pre-existing, not introduced here):
extract()— 401 callers, 39 callees (high)_rebuild_code()— 93 callers, 51 callees (high)build_from_json()— 151 callers, 17 callees (high)detect()— 82 callers, 13 callees (high)deduplicate_entities()— 53 callers, 19 callees (high)build_merge()— 41 callers, 14 callees (high)save_semantic_cache()— 50 callers, 9 callees (high)_extract_generic()— 18 callers, 22 callees (high)- …and 2 more
Verification — 4829 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: 4548 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify \_abs\_identity.
The verifier did not have enough to check \_abs\_identity, 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: not verifiable: the input domain has 100 values but only 10 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)
Could not verify: Could not verify \_derive\_prune\_root.
The verifier did not have enough to check \_derive\_prune\_root, 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 `prune_sources` is annotated `'list[str]'` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_norm\_source\_file.
The verifier did not have enough to check \_norm\_source\_file, 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: not verifiable: the input domain has 100 values but only 10 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)
No difference found (not proven): No behavior difference found in \_semantic\_id\_remap (not a proof).
The verifier ran both versions of \_semantic\_id\_remap 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.
Could not verify: Could not verify build\_from\_json.
The verifier did not have enough to check build\_from\_json, 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: not verifiable: all 5 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify graph\_has\_legacy\_ids.
The verifier did not have enough to check graph\_has\_legacy\_ids, 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 `root` is annotated `str | Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify cached\_word\_count.
The verifier did not have enough to check cached\_word\_count, 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
Could not verify: Could not verify file\_hash.
The verifier did not have enough to check file\_hash, 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
Could not verify: Could not verify \_llm\_tiebreak.
The verifier did not have enough to check \_llm\_tiebreak, 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: not verifiable: all 105 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify deduplicate\_entities.
The verifier did not have enough to check deduplicate\_entities, 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: not verifiable: all 7 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_is\_ignored.
The verifier did not have enough to check \_is\_ignored, 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
No difference found (not proven): No behavior difference found in \_nfc (not a proof).
The verifier ran both versions of \_nfc 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.
No difference found (not proven): No behavior difference found in \_parse\_gitignore\_line (not a proof).
The verifier ran both versions of \_parse\_gitignore\_line 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.
No difference found (not proven): No behavior difference found in to\_json (not a proof).
The verifier ran both versions of to\_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.
No difference found (not proven): No behavior difference found in to\_html (not a proof).
The verifier ran both versions of to\_html 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.
Could not verify: Could not verify \_rescue\_js\_dynamic\_imports.
The verifier did not have enough to check \_rescue\_js\_dynamic\_imports, 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
Could not verify: Could not verify \_resolve\_objc\_member\_calls.
The verifier did not have enough to check \_resolve\_objc\_member\_calls, 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: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
Could not verify: Could not verify extract.
The verifier did not have enough to check extract, 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 `cache_root` is annotated `Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_extract\_generic.
The verifier did not have enough to check \_extract\_generic, 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
Could not verify: Could not verify \_js\_local\_bound\_names.
The verifier did not have enough to check \_js\_local\_bound\_names, 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: not verifiable: all 12 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_go\_collect\_type\_refs.
The verifier did not have enough to check \_go\_collect\_type\_refs, 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: not verifiable: all 84 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify extract\_go.
The verifier did not have enough to check extract\_go, 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
Could not verify: Could not verify extract\_objc.
The verifier did not have enough to check extract\_objc, 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
Could not verify: Could not verify \_merge\_decl\_def\_classes.
The verifier did not have enough to check \_merge\_decl\_def\_classes, 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: not verifiable: the input domain has 49 values but only 7 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)
Could not verify: Could not verify \_probe\_python\_module\_candidate.
The verifier did not have enough to check \_probe\_python\_module\_candidate, 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 `candidate` is annotated `Path` — outside the synthesizable primitive/collection set
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Fixed in v0.9.41 ( |
|
Shipped in v0.9.41 ( |
Fixes #2625.
Problem
source_fileis built from whateverPathan extractor was handed, and only therelativizing branch of
extract()'s remap (_sf_entry) ever callsas_posix(). A run givenrelative inputs therefore keeps the native separator on Windows — and because different
extractors reach that branch differently, a single extraction emits two conventions at once:
source_fileis compared as a string downstream —build._norm_source_filekeying,_derive_prune_root, dedup, the semantic-cache key path, andanalyze.find_import_cycles,which matches an edge's
source_fileagainst a node's by equality. Two spellings are twodifferent files: the fragmentation of #683, resurfaced on a different code path.
The primary
graphify extractCLI passes an explicitrootand takes the already-correctbranch, which is why this stayed invisible. It is reachable from the documented
python -m graphify.extract <file> …entry point and from any library caller.Fix
One pass at the end of
extract(), where all nodes and edges are already being walked:Three deliberate choices, each verified rather than assumed:
source_filewith mixed path separators (src\lib\content.tsandsrc/pages/index.astroin the same output) on Windows #2625 suggests a node-only loop; edges carrysource_filetoo.Without the fix the new test reports 5 node + 3 edge violations, and
find_import_cyclescompares an edge'ssource_fileto a node's by equality — so anode-only fix would have left cycle detection broken on exactly this input. (
target_fileis popped earlier and never reaches the output, so it needs nothing.)
make_id, which collapses every non-wordcharacter —
\and/alike — to_. Confirmed:make_id("src\lib\content.ts") == make_id("src/lib/content.ts") == "src_lib_content_ts".Canonicalizing the separator after IDs exist cannot desync an id from its
source_file.PurePathis the native flavour on purpose. On POSIX a backslash is a legal filenamecharacter and must be left alone, so this only rewrites paths on the platform where
\isactually a separator.
Warm cache entries are healed, no version bump needed
The pass runs after the AST cache merge, so an entry written by a pre-fix version is
corrected on read:
No cache invalidation and no re-extraction cost for existing users.
Changes
graphify/extract.py— the normalization pass.tests/test_extract.py— newtest_extract_emits_posix_source_file_for_relative_inputs,covering nodes and edges. Uses relative inputs deliberately: passing an explicit
roottakes the branch that already normalized and would make the test vacuous.
tests/test_js_import_resolution.py—test_alias_import_does_not_remap_an_owned_symbol_idlookedsource_fileup withstr(Path(...)), the native spelling. It passed on Windows only because the product emittedbackslashes too; with
source_filenow canonical, the four lookups needas_posix(). Thisis the mirror image of Tests hardcode
/in path assertions, so they fail on Windows — and some pass vacuously, hiding the assertion they were meant to make #2620 — a test coupled to a separator convention rather than to theproperty it means to assert.
I checked for siblings:
test_extract.py:84-91keys both sides of its comparison from thesame output, so it is self-consistent and unaffected (not silently vacuous).
Verification
python -m graphify.extract …test_astro_import_ids::test_astro_relative_inputs_keep_canonical_ids(collateral evidence in #2625)graphify extract .(CLI path)The single removed failure is the astro test above. The remaining 40 are the pre-existing
Windows baseline (symlink privilege,
os.geteuid, POSIX mode bits), untouched here.Migration note
Any Windows-built graph currently holding backslash spellings will re-key those nodes on the
next rebuild — the same one-time churn as #683's fix, moving them onto the canonical form the
rest of the pipeline already expects.