Repository navigation
build: the repository has no .gitattributes, so a Windows checkout is CRLF and the engine stages every client relation empty #963
Description
Priority: P2 — on a default Windows clone every checked-out text file is CRLF, and the engine's IR-map reader treats the trailing \r as part of a filename, so every relation is staged empty and the run produces an empty graph with exit 0.
Follow-up to #16 (Windows support for the engine pipeline). Measured at b54d2bd1.
Where
The repository ships no .gitattributes. Git's Windows default is core.autocrlf=true, so a stock git clone on Windows materialises every text file with CRLF endings — 4392 files in this tree.
graph/pipeline/run-souffle.sh:209-216 stages client facts by reading a tab-separated map:
while IFS=$'\t' read -r rel csv; do
if [ -f "$CLIENT/$csv.csv" ]; then awk ... "$CLIENT/$csv.csv" > "$FACTS/$rel.facts"
else : > "$FACTS/$rel.facts"; fi
...
done < <(read_map "$TPL/client-ir.map")IFS=$'\t' splits on tabs only, so with a CRLF map the last field keeps its \r: csv becomes all-python-call-sites<CR>. The [ -f "$CLIENT/$csv.csv" ] test then looks for a file whose name contains a carriage return, finds nothing, and takes the else branch.
Why it is silent
The else branch is deliberate and documented — an absent CSV stages an empty relation so the compiled binary stays reusable across projects (run-souffle.sh:196-207). That is correct behaviour for a project that genuinely has no XML. It is indistinguishable from a project whose every CSV was made unreachable by a stray \r.
So the run does not error. It reports staged 43 relations, solves for several minutes, exits 0, and writes a graph:
▶ staged 43 relations (client + full lib signatures from 0 library root(s))
▶ solve (iteration 1)...
reachable_method = 0, forward_call = 0
Elapsed (solve): 445s
sqlite call_sites: 0 rows
sqlite call_edges: 0 rows
✅ reasoning complete: .../graph.sqlite
Measured on both front ends at this HEAD, with a correct non-empty IR on disk (2031 Python call-site rows / 2657 Java METHOD_INVOCATION rows staged from). Normalising the tree to LF and re-running the identical command gives 2182 call sites / 2298 edges for the Python project and 3232 call sites / 4488 edges for the Java one.
Second symptom: the schema gate can never pass
graph/test/tools/bundle-test.sh:226 byte-compares generated output against a committed file:
if ! diff -q <(BUNDLE --print-schema) "$ROOT/graph/bundle/SCHEMA.md" >/dev/null; thenThe generator writes LF; the checked-out SCHEMA.md is CRLF. The contents are identical:
committed: 76604 bytes, CRLF: 930
generated: 75674 bytes, CRLF: 0
equal: False
equal ignoring CRLF: True
76604 − 75674 = 930, exactly the CRLF count. The gate reports SCHEMA.md is stale — run: npm run schema-doc, and running it does not help, because the regenerated file is checked back in as CRLF. This aborts bin/axiomcode test <lang> before any case executes.
Repro
git config --global core.autocrlf true # the Windows default
git clone https://github.com/AxiomCodeAI/axiom-code-graph.git && cd axiom-code-graph
npm install && npm run build
bin/axiomcode parser <any-project> ./ir
bin/axiomcode engine --language python --client-ir ./ir/python --out ./out
# -> exit 0, "staged N relations", 0 call_sites, 0 call_edgesFix
Add a .gitattributes that pins the files the tooling parses or byte-compares to LF. At minimum the machine-read ones:
*.map text eol=lf
*.dl text eol=lf
*.sh text eol=lf
bin/axiomcode text eol=lf
graph/bundle/SCHEMA.md text eol=lf
* text=auto eol=lf for the whole tree is the simpler call if nothing here wants CRLF.
Hardening worth considering independently, since a .gitattributes only protects fresh checkouts: strip trailing \r in read_map, and make "every client relation staged empty" a diagnosable condition rather than a silent one — a run where no client CSV was found is a broken invocation, not a project with no facts.
Acceptance
- a clone with
core.autocrlf=trueproduces a non-empty graph for a project with call sites bundle-test.sh'sSCHEMA.mdcomparison passes on Windows without regenerating
Activity
swapnilpaliwal-sd commentedon Sep 18, 2026
Third symptom, and it defeats the engine packaging that merged in #904 an hour ago: a CRLF checkout changes the engine id, so a published engine is refused on every Windows clone.
The id hashes the rule text. Same rules, different line endings, different hash:
Measured by copying the tree, converting
graph/java/**/*.dlto CRLF, and re-runningrun-souffle.sh --language java --print-engine-id. Nothing else changed.run-souffle.shthen reports:and falls through to a local compile, which on Windows means souffle, which is the dependency the packaging exists to remove. The refusal is correct given its inputs; the inputs are wrong. Same shape as #895, where the id followed the shell's locale, and the same consequence: the error names the rules when the cause is the environment.
Why the earlier Windows validation missed all three
The five-platform run I did for #904 installed from npm tarballs, and a tarball preserves LF. Every one of these symptoms needs a
git cloneon Windows to appear. So "Windows works" was true for the shipped package and false for anyone working in the repo on Windows, which are two different claims and were being treated as one.Worth stating in the issue because it changes who is affected: not just a Windows developer running the suites, but any Windows machine that would otherwise have consumed a published engine.
Fix
Testing
.gitattributeswith* text=auto eol=lfon a Windows VM now, against a stock clone withcore.autocrlf=true. It also needs explicitbinaryfor*.node,*.tgz,*.sqliteand similar: newline conversion applied to an engine binary corrupts it, and the checksum meant to guard it would be computed over the corruption.Will post the before and after from that machine rather than asserting it works.
build: pin the tree to LF so a Windows checkout is not CRLF (#964)
swapnilpaliwal-sd commentedon Sep 18, 2026
Fixed on main in #964 (
38d4203a):.gitattributespins the tree toeol=lf, with explicitbinaryfor*.node,*.tgz,*.sqliteand similar, since newline conversion applied to an engine binary corrupts it and the checksum guarding it would be computed over the corruption.Leaving this issue open, because one of its three symptoms is not yet pinned down.
Fixed and verified: the
SCHEMA.mdgate (930 CR lines, exactly the byte delta it reports) and the engine id shifting on CRLF rules (0a0c96cbtod269ab85), which silently defeated the engine packaging in #904 for every Windows clone.Not reproduced, and worth someone confirming the environment: the first symptom, every client relation staging empty. On a Windows Server 2022 VM with a stock
autocrlf=trueclone,read_mapdoes not yield a CR-terminated field, because it usesgrepand MSYS2's GNU grep 3.0 strips the CR first:staging.confis clean after sourcing too, since its assignments are quoted and the CR falls outside the value.Another session measured the empty-graph outcome at raw HEAD for Java and Python, so the symptom is real. I am not disputing that; I could not reproduce the stated mechanism under Git Bash, which points at the vector being environment-specific, for instance WSL or any shell whose grep preserves CR. That distinction matters for the inference in that report: if the vector is not the map reader, "same code path, same exposure" is not the argument for TypeScript, JavaScript and C#, though the conclusion may still hold since each language has its own templates.
Whoever picks this up: the useful next measurement is which shell and grep produced the empty graph, then whether the CR survives
read_mapthere. The fix removes the class either way.swapnilpaliwal-sd commentedon Sep 18, 2026
Fixed by #964 (
38d4203a), which adds the.gitattributesthe repository never had, pinning the tree toeol=lfwith explicitbinaryfor*.node,*.tgz,*.sqliteand similar.All three symptoms resolved:
Empty client relations. Confirmed in practice on Windows: at
38d4203awith a pristine checkout and zero local changes, four of five languages run parse to engine and produce non-empty, semantically correct graphs. Before the fix that run needed 4396 files normalised by hand.The schema gate.
SCHEMA.mdchecked out with 930 CR lines, exactly the byte difference the gate reported as stale, sobin/axiomcode test <lang>aborted before a case ran. Measured on a Windows Server 2022 VM.The engine id. Not in the original report and the most consequential for the engine packaging: the id hashes the rule text, so CRLF rules hash differently. Converting
graph/java/**/*.dlto CRLF and changing nothing else moved the java id from0a0c96cbtod269ab85. A published engine was therefore refused on every Windows clone and fell back to a local compile, which on Windows means souffle, the dependency the packaging exists to remove.One footnote, not a reason to keep this open
The mechanism in the original report is that
IFS=$'\t' readkeeps the trailing CR fromclient-ir.map. That does not reproduce under Git Bash, becauseread_mapusesgrepand MSYS2's GNU grep 3.0 strips the CR first:staging.confis clean after sourcing too, since its assignments are quoted. So the empty-graph outcome was real and is now fixed, but the CR appears to have reached the pipeline by some route other than the map reader, in an environment whose shell or grep preserves it. Pinning that down is not worth an open bug now that the class is gone; if anyone reproduces an empty graph on a current checkout, reopen with the shell and grep version.Note for anyone with an existing clone
.gitattributeschanges what git checks out, so a clone made before38d4203astill has CRLF and still computes the wrong engine id. Fix in place with:or re-clone.