From 1ee42f2cb6986de80929528e3cf4b94a8f52320c Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Thu, 27 Aug 2026 14:47:47 -0400 Subject: [PATCH] Fix: Sort the contract manifest in byte order, not the machine's The manifest is written in byte order, but both the runner and the manifest contract discovered contracts with a bare `sort` and compared them with bash's `<` -- and both of those follow LC_COLLATE. Under en_US.UTF-8 the collation folds punctuation away, so `calendar_agenda_bridge_test.py` sorts before `calendar-agenda-helper-contract` instead of after it, and eight pairs that differ only by `-` against `_` come back out of order. The effect was that `tests/setup/contract-manifest-contract` failed on this machine, and `panama test` refused to run at all, with eight identical "paths are not lexicographically sorted" findings and nothing naming which paths. A gate whose answer depends on the machine's LANG is not a gate, so the sort and the comparison are both pinned to byte order. LC_ALL rather than LC_COLLATE, because an exported LC_ALL outranks it and would have put the bug back. Claude-Session: https://claude.ai/code/session_017zzbtfnMLoYrB8WesqANFY --- bin/panama | 9 ++++++++- tests/setup/contract-manifest-contract | 8 +++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/bin/panama b/bin/panama index 6db8338..0779d9f 100755 --- a/bin/panama +++ b/bin/panama @@ -451,11 +451,16 @@ CONTRACT_CAPABILITIES=(hermetic live-host live-compositor live-desktop network p contract_paths() { local candidate + # The manifest is kept in byte order, so both the discovery sort and the + # comparison below have to be byte order too. A UTF-8 collation folds the + # punctuation away -- `calendar_agenda_bridge_test.py` sorts before + # `calendar-agenda-helper-contract` under en_US and after it under C -- and + # a gate that passes or fails on the machine's LANG is not a gate. while IFS= read -r candidate; do [[ -x "$candidate" || "$candidate" == *_test.py ]] || continue printf 'tests/%s\n' "${candidate#"$PANAMA_DIR/tests/"}" done < <(find "$PANAMA_DIR/tests" -type f \ - -not -path '*/fixtures/*' -not -path '*__pycache__*' | sort) + -not -path '*/fixtures/*' -not -path '*__pycache__*' | LC_ALL=C sort) } contract_manifest_entries() { @@ -478,6 +483,8 @@ validate_contract_manifest() { require_contract_manifest || return 1 local manifest="$PANAMA_DIR/$CONTRACT_MANIFEST" + # Byte order, for the same reason contract_paths sorts in it. + local LC_ALL=C local line capabilities path extra previous_comment="" previous_was_comment=0 local previous_path="" capability discovered local -a capability_list=() findings=() diff --git a/tests/setup/contract-manifest-contract b/tests/setup/contract-manifest-contract index eb26af0..a4a5755 100755 --- a/tests/setup/contract-manifest-contract +++ b/tests/setup/contract-manifest-contract @@ -11,16 +11,22 @@ manifest="$repo_dir/tests/contracts.manifest" discover_contracts() { discovered_contracts=() + # Byte order, exactly as the runner discovers them. A UTF-8 collation folds + # the punctuation away and reorders the pairs that differ only by `-` and + # `_`, so a manifest correct here would be wrong on a machine with a + # different LANG. while IFS= read -r path; do [[ -x "$path" || "$path" == *_test.py ]] || continue discovered_contracts+=("tests/${path#"$repo_dir/tests/"}") done < <(find "$repo_dir/tests" -type f \ - -not -path '*/fixtures/*' -not -path '*__pycache__*' | sort) + -not -path '*/fixtures/*' -not -path '*__pycache__*' | LC_ALL=C sort) } validate_manifest() { local candidate="$1" local -n expected_contracts="$2" + # Byte order, for the same reason discover_contracts sorts in it. + local LC_ALL=C local line capabilities path extra previous_comment="" previous_was_comment=0 local -a capability_list=() local -A manifest_paths=() capability_counts=()