Skip to content

build: the repository has no .gitattributes, so a Windows checkout is CRLF and the engine stages every client relation empty #963

Description

@Whua689

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; then

The 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_edges

Fix

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=true produces a non-empty graph for a project with call sites
  • bundle-test.sh's SCHEMA.md comparison passes on Windows without regenerating

Activity

  1. added
    bugSomething isn't working
    platformOS / toolchain portability
    buildBuild, packaging and developer setup
    windowsMicrosoft Windows support
    on Sep 18, 2026
  2. added theissue type on Sep 18, 2026
  3. swapnilpaliwal-sd commented on Sep 18, 2026

    @swapnilpaliwal-sd
    Contributor

    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:

    LF rules:   0a0c96cbe8cb5dfe...
    CRLF rules: d269ab85e1583241...
    

    Measured by copying the tree, converting graph/java/**/*.dl to CRLF, and re-running run-souffle.sh --language java --print-engine-id. Nothing else changed.

    run-souffle.sh then reports:

    ! @axiomcode/engine-win32-x64 holds java at <ci id>, these rules are <crlf id> - not using it
    

    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 clone on 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 .gitattributes with * text=auto eol=lf on a Windows VM now, against a stock clone with core.autocrlf=true. It also needs explicit binary for *.node, *.tgz, *.sqlite and 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.

  4. swapnilpaliwal-sd commented on Sep 18, 2026

    @swapnilpaliwal-sd
    Contributor

    Fixed on main in #964 (38d4203a): .gitattributes pins the tree to eol=lf, with explicit binary for *.node, *.tgz, *.sqlite and 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.md gate (930 CR lines, exactly the byte delta it reports) and the engine id shifting on CRLF rules (0a0c96cb to d269ab85), 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=true clone, read_map does not yield a CR-terminated field, because it uses grep and MSYS2's GNU grep 3.0 strips the CR first:

    rows read: 26   rows whose last field ends in CR: 0
    

    staging.conf is 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_map there. The fix removes the class either way.

  5. swapnilpaliwal-sd commented on Sep 18, 2026

    @swapnilpaliwal-sd
    Contributor

    Fixed by #964 (38d4203a), which adds the .gitattributes the repository never had, pinning the tree to eol=lf with explicit binary for *.node, *.tgz, *.sqlite and similar.

    All three symptoms resolved:

    Empty client relations. Confirmed in practice on Windows: at 38d4203a with 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.md checked out with 930 CR lines, exactly the byte difference the gate reported as stale, so bin/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/**/*.dl to CRLF and changing nothing else moved the java id from 0a0c96cb to d269ab85. 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' read keeps the trailing CR from client-ir.map. That does not reproduce under Git Bash, because read_map uses grep and MSYS2's GNU grep 3.0 strips the CR first:

    rows read: 26   rows whose last field ends in CR: 0
    

    staging.conf is 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

    .gitattributes changes what git checks out, so a clone made before 38d4203a still has CRLF and still computes the wrong engine id. Fix in place with:

    git add --renormalize .
    

    or re-clone.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingbuildBuild, packaging and developer setupplatformOS / toolchain portabilitywindowsMicrosoft Windows support

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions