From e604dd12ac05ddb66ecb2770430afdd638a7c357 Mon Sep 17 00:00:00 2001 From: JasonBuildAI Date: Sun, 11 Oct 2026 15:12:00 +0800 Subject: [PATCH] fix(turn-contract): hash canonical LF bytes for the provenance digest A Windows checkout with core.autocrlf=true keeps CRLF in the contract JSON, so the generator recorded a checkout-specific digest and the freshness guard reported the committed artifacts as stale; the suggested regeneration then wrote that CRLF digest into the artifacts, which fails on an LF checkout and would break the required Linux checks if committed. Hash the contract's canonical LF bytes, matching the existing pattern in loopx/presentation/chat_bundle.py::source_digest, and pin the behavior with a regression test that regenerates from a CRLF copy of the contract. Signed-off-by: JasonBuildAI --- scripts/generate_turn_contract.py | 6 +++++- tests/architecture/test_turn_contract_generation.py | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/scripts/generate_turn_contract.py b/scripts/generate_turn_contract.py index 1ee2ae77ee..046c19e897 100644 --- a/scripts/generate_turn_contract.py +++ b/scripts/generate_turn_contract.py @@ -217,7 +217,11 @@ def require(condition, message): def build_artifacts(): contract = validate_contract(read_contract()) - digest = hashlib.sha256(CONTRACT_PATH.read_bytes()).hexdigest() + # Git may check the contract out with CRLF on Windows; hash the canonical LF + # bytes so every checkout records the same provenance digest. + digest = hashlib.sha256( + CONTRACT_PATH.read_bytes().replace(b"\r\n", b"\n") + ).hexdigest() header = f"Generated by scripts/generate_turn_contract.py; do not edit.\nSource: loopx/control_plane/turn_loop_controller_contract_v0.json\nSHA256: {digest}" py = [ '"""' + header + '"""', diff --git a/tests/architecture/test_turn_contract_generation.py b/tests/architecture/test_turn_contract_generation.py index 9bd8cf1e28..d2e4c494fe 100644 --- a/tests/architecture/test_turn_contract_generation.py +++ b/tests/architecture/test_turn_contract_generation.py @@ -105,6 +105,18 @@ def test_generator_check_is_deterministic_and_does_not_repair(tmp_path, monkeypa assert path.read_text() == "stale\n" +def test_contract_digest_ignores_checkout_line_endings(tmp_path, monkeypatch): + # A Windows checkout may still carry CRLF when the guard reads the contract. + crlf_contract = tmp_path / generator.CONTRACT_PATH.name + contract_text = generator.CONTRACT_PATH.read_text(encoding="utf-8") + crlf_contract.write_bytes(contract_text.replace("\n", "\r\n").encode("utf-8")) + monkeypatch.setattr(generator, "CONTRACT_PATH", crlf_contract) + assert generator.build_artifacts() == { + generator.PYTHON_PATH: generator.PYTHON_PATH.read_text(encoding="utf-8"), + generator.TYPESCRIPT_PATH: generator.TYPESCRIPT_PATH.read_text(encoding="utf-8"), + } + + def test_typescript_settlement_reexports_generated_result_set(): result = subprocess.run( [