Repository navigation
[Bug]: the Turn contract digest covers checkout bytes, so Windows CRLF checkouts fail the guard and the suggested regeneration breaks LF checkouts #6198
Description
Preflight
-
I searched existing issues and discussions for this behavior.
-
This report is not an unpatched security vulnerability.
-
The report and attachments contain no credentials or private/internal material.
-
Issue origin: Observed or reproduced in a real environment
-
LoopX version or commit:
5fb256a4d(latestmainat report time, LoopX 1.4.0) -
Host or runtime surface: Not host-specific; reproduced on native Windows 11 with Git for Windows' default
core.autocrlf=true -
LoopX area: Control plane (goals, todos, quota, scheduler, registry, runtime)
Problem
scripts/generate_turn_contract.py records the contract provenance digest by hashing the physical checkout bytes of loopx/control_plane/turn_loop_controller_contract_v0.json:
digest = hashlib.sha256(CONTRACT_PATH.read_bytes()).hexdigest()Git for Windows checks that JSON out with CRLF by default (core.autocrlf=true), while the committed artifacts carry the digest of the LF bytes Git stores. On a Windows checkout the guard therefore reports a stale artifact even though the revision is unmodified:
examples/semantic-vocabulary-drift-smoke.pyfails withstale generated Turn contract: loopx\control_plane\turn_driver\turn_contract_generated.py; run scripts/generate_turn_contract.py;tests/architecture/test_turn_contract_generation.py(2 rows) andtests/architecture/test_semantic_vocabulary_drift.pyfail on the same message;loopx canary premerge --from-git-diffselects that smoke as a defaultarchitecturecheck for any diff underloopx/, so the pre-push gate documented inCONTRIBUTING.mdfails on Windows.
The printed repair makes the damage worse: running uv run --extra test python scripts/generate_turn_contract.py on Windows rewrites both committed artifacts with the CRLF digest. That content then fails the same freshness guard on an LF checkout, so a Windows contributor who follows the error message commits a generated artifact that breaks the required Linux checks.
Minimal reproduction
git clone https://github.com/loopx-project/loopx
cd loopx
git config --get core.autocrlf # true on a default Git for Windows install
uv sync --extra test
uv run --extra test python examples/semantic-vocabulary-drift-smoke.py
uv run --extra test python -m pytest -q tests/architecture/test_turn_contract_generation.pyExpected behavior
The provenance digest covers the canonical LF content of the contract, so one revision records the same digest on every checkout: the smoke and the architecture tests pass on a CRLF checkout, and running the generator there reports up to date without touching the committed artifacts.
Actual behavior
The smoke fails with the stale-contract message, and the suggested regeneration changes the recorded digest from the LF value to the CRLF value:
-SHA256: ab92e3888d83977b2b4548d924a7ff800f7fd1f6388f347772a8c3d6211fa3d8"""
+SHA256: 20bfb14e763ed0f4f3ce4183c35362fe9727ac436c6be7bad8b126dece5e9010"""Sanitized diagnostics
$ uv run --extra test python examples/semantic-vocabulary-drift-smoke.py
semantic-vocabulary-drift-smoke: FAIL
stale generated Turn contract: loopx\control_plane\turn_driver\turn_contract_generated.py; run scripts/generate_turn_contract.py
$ uv run --extra test python -m pytest -q tests/architecture/test_turn_contract_generation.py
FAILED tests/architecture/test_turn_contract_generation.py::test_controller_and_projection_witnesses_cover_live_dispositions
FAILED tests/architecture/test_turn_contract_generation.py::test_new_independent_twin_cannot_hide_behind_generated_pair
2 failed, 20 passed
Additional context
loopx/presentation/chat_bundle.py::source_digestalready normalizesCRLFtoLFbefore hashing tracked text build inputs for exactly this reason ("Git may check text out with CRLF on Windows; binary assets remain exact"), so this generator is the inconsistent owner rather than the checkout being unsupported.- The
.gitattributesnote for the archived DeepSWE input ("Preserve the immutable archive's bytes even with core.autocrlf=true") shows the same checkout mode is treated as real elsewhere in the repository. - This is a portability defect in the M2 generated Turn contract (PR feat(semantics): generate shared Turn contracts and controller rules (M2) #4499, tracker [Task]: Semantic vocabulary convergence follow-ups after RFC PR #4433 / 语义词表收敛后续任务 #4447), not a new semantic slice: the accepted digest shape and the generated artifacts do not change on LF checkouts.
Activity
chore(coordination): record issue loopx-project#6198 and PR loopx-pro…