@ccoalm/ccl-skills 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/assets/marketplace/plugins/ccl-skills/agent-context/session-start.md +8 -7
  2. package/dist/assets/marketplace/plugins/ccl-skills/hooks/hooks.json +22 -0
  3. package/dist/assets/marketplace/plugins/ccl-skills/hooks/remind-review-covers-head.sh +128 -0
  4. package/dist/assets/marketplace/plugins/ccl-skills/hooks/remind-untracked-background.sh +53 -0
  5. package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_remind_review_covers_head.sh +104 -0
  6. package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_remind_untracked_background.sh +75 -0
  7. package/dist/assets/marketplace/plugins/ccl-skills/packages/opencode-plugin/ccl-skills.ts +8 -0
  8. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/SKILL.md +6 -4
  9. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/development-completion.md +13 -1
  10. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/staged-review-contract.md +78 -126
  11. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/wording-only-review.md +136 -0
  12. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/review_gate.py +521 -32
  13. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_client_compat.py +25 -2
  14. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_client_order.sh +30 -15
  15. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh +439 -19
  16. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_update_review_plan_intent.sh +14 -7
  17. package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/SKILL.md +1 -1
  18. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/SKILL.md +1 -1
  19. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +6 -6
  20. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/attention-budget-ratchet.md +1 -0
  21. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +48 -119
  22. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-quickstart.md +9 -9
  23. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/firing-point-placement.md +22 -0
  24. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +18 -0
  25. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/validation-and-landing.md +2 -2
  26. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check_review_evidence_present.py +122 -0
  27. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/contract-anchors.tsv +4 -1
  28. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/extraction_review_gate.sh +37 -8
  29. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/register-firing-path-resolution.rb +102 -20
  30. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_ai_coding_implementation_gates.sh +16 -27
  31. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +3 -5
  32. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_review_evidence_present.sh +81 -0
  33. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_extraction_review_gate.sh +137 -279
  34. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_register_firing_path_resolution.sh +41 -1
  35. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_register_firing_path_wiring.sh +49 -3
  36. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/SKILL.md +3 -2
  37. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/closeout-reread.md +40 -0
  38. package/dist/assets/release.json +72 -52
  39. package/package.json +1 -1
  40. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/review_ledger_binding.py +0 -1242
  41. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_review_ledger_binding.sh +0 -978
  42. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_extraction_review_state.sh +0 -1477
  43. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py +0 -1183
@@ -1,978 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Regression for the merge-side binding between the landing candidate and the
3
- # review evidence that inspected it.
4
- set -uo pipefail
5
-
6
- DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
7
- GATE="$DIR/review_ledger_binding.py"
8
- CONTROLLER="$DIR/../../code-review/scripts/review_gate.py"
9
- WORK="$(mktemp -d "${TMPDIR:-/tmp}/review-ledger-binding.XXXXXX")"
10
- trap 'rm -rf "$WORK"' EXIT
11
-
12
- fails=0
13
- check() { if eval "$2"; then echo "ok - $1"; else echo "FAIL - $1"; fails=$((fails+1)); fi; }
14
-
15
- [ -f "$GATE" ] || { echo "FAIL: gate missing: $GATE" >&2; exit 1; }
16
- [ -f "$CONTROLLER" ] || { echo "FAIL: controller missing: $CONTROLLER" >&2; exit 1; }
17
-
18
- REPO="$WORK/repo"
19
- mkdir -p "$REPO/skills/code-review/scripts" "$REPO/skills/skill-extraction-workflow/scripts" "$REPO/specs/round/evidence"
20
- cp "$CONTROLLER" "$REPO/skills/code-review/scripts/review_gate.py"
21
- # The gate delegates ledger acceptance to the validator; these stubs isolate the
22
- # gate's own contract (recompute the packet, bind it, delegate) from the
23
- # validator's fixture surface, which its own suite already covers.
24
- cat >"$REPO/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py" <<'PY'
25
- import sys
26
- print("extraction_review_state_ok: stub accepted")
27
- sys.exit(0)
28
- PY
29
- cat >"$WORK/reject-validator.py" <<'PY'
30
- import sys
31
- print("extraction_review_state_invalid: stub rejected")
32
- sys.exit(1)
33
- PY
34
-
35
- git -C "$REPO" init -q .
36
- git -C "$REPO" config user.email t@example.invalid
37
- git -C "$REPO" config user.name tester
38
- printf 'baseline\n' >"$REPO/skills/skill-extraction-workflow/SKILL.md"
39
- git -C "$REPO" add -A
40
- git -C "$REPO" commit -qm baseline
41
- BASE="$(git -C "$REPO" rev-parse HEAD)"
42
-
43
- run_gate() { python3 "$GATE" --repo-root "$REPO" "$@" 2>&1; }
44
-
45
- out="$(run_gate --base "$BASE")"; rc=$?
46
- check "an unchanged reviewed path needs no review evidence" \
47
- '[ "$rc" = 0 ] && case "$out" in *"no reviewed-path change"*) true;; *) false;; esac'
48
-
49
- out="$(CCL_SKILL_BASE_REF= run_gate)"; rc=$?
50
- check "a missing base ref fails closed instead of recording a passing check" \
51
- '[ "$rc" = 2 ] && case "$out" in *"nothing was checked"*) true;; *) false;; esac'
52
-
53
- out="$(CCL_SKILL_BASE_REF= run_gate --allow-unevaluated)"; rc=$?
54
- check "an event with genuinely no base must say so out loud to exit clean" \
55
- '[ "$rc" = 0 ] && case "$out" in *"nothing was checked"*) true;; *) false;; esac'
56
-
57
- out="$(CCL_SKILL_BASE_REF="$BASE" run_gate)"; rc=$?
58
- check "the gate reads the base from the environment variable its diagnostic names" \
59
- '[ "$rc" = 0 ] && case "$out" in *"no reviewed-path change"*) true;; *) false;; esac'
60
-
61
- # The fix batch lands under the reviewed paths.
62
- printf 'baseline\nlanded change\n' >"$REPO/skills/skill-extraction-workflow/SKILL.md"
63
- git -C "$REPO" add -A
64
- git -C "$REPO" commit -qm "landing change"
65
-
66
- out="$(run_gate --base "$BASE")"; rc=$?
67
- check "a reviewed-path change with no evidence fails closed" \
68
- '[ "$rc" = 1 ] && case "$out" in *"no accepted review evidence binds the landing candidate"*) true;; *) false;; esac'
69
- EXPECTED="$(run_gate --base "$BASE" --print-candidate)"
70
- check "the gate can name the candidate hash the evidence must carry" \
71
- '[ -n "$EXPECTED" ] && case "$out" in *"$EXPECTED"*) true;; *) false;; esac'
72
-
73
- # Evidence for a different candidate must not satisfy the gate: this is the exact
74
- # shape the gate exists to catch -- a chain that closed out on the pre-fix tree.
75
- python3 - "$REPO/specs/round/evidence/stale-closeout.json" <<'JSONGEN'
76
- import json, sys
77
- from pathlib import Path
78
- Path(sys.argv[1]).write_text(json.dumps({"schema_version": 3, "closeout_state": "ready_for_human_decision", "controller_receipts": [], "candidate_sha256": "0" * 64}))
79
- JSONGEN
80
- git -C "$REPO" add -A && git -C "$REPO" commit -qm "stale evidence"
81
- out="$(run_gate --base "$BASE")"; rc=$?
82
- check "a ledger bound to another candidate does not satisfy the gate" \
83
- '[ "$rc" = 1 ] && case "$out" in *"no accepted review evidence"*) true;; *) false;; esac'
84
-
85
- # Committing evidence must not move the candidate: evidence lives outside the
86
- # reviewed paths, which is what lets a ledger record the tree that carries it.
87
- EXPECTED_AFTER="$(run_gate --base "$BASE" --print-candidate)"
88
- check "committing evidence outside the reviewed paths leaves the candidate unmoved" \
89
- '[ "$EXPECTED_AFTER" = "$EXPECTED" ]'
90
-
91
- # A ledger that binds the landing candidate but fails validation is reported as a
92
- # rejected ledger, never silently ignored. Swapping the validator edits a reviewed
93
- # path, so the candidate moves and the ledger is written against the moved hash.
94
- cp "$WORK/reject-validator.py" "$REPO/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py"
95
- git -C "$REPO" add -A && git -C "$REPO" commit -qm "rejecting validator"
96
- REJECT_CANDIDATE="$(run_gate --base "$BASE" --print-candidate)"
97
- check "editing a reviewed path moves the candidate the evidence must bind" \
98
- '[ -n "$REJECT_CANDIDATE" ] && [ "$REJECT_CANDIDATE" != "$EXPECTED" ]'
99
- python3 - "$REPO/specs/round/evidence/closeout.json" "$REJECT_CANDIDATE" <<'JSONGEN'
100
- import json, sys
101
- from pathlib import Path
102
- Path(sys.argv[1]).write_text(json.dumps({"schema_version": 3, "closeout_state": "ready_for_human_decision", "controller_receipts": [], "candidate_sha256": sys.argv[2]}))
103
- JSONGEN
104
- git -C "$REPO" add -A && git -C "$REPO" commit -qm "rejected ledger"
105
- out="$(run_gate --base "$BASE")"; rc=$?
106
- check "a bound but invalid ledger is reported as rejected, not ignored" \
107
- '[ "$rc" = 1 ] && case "$out" in *"rejected ledger"*"stub rejected"*) true;; *) false;; esac'
108
-
109
- cat >"$REPO/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py" <<'ACCEPTSTUB'
110
- import sys
111
- print("extraction_review_state_ok: stub accepted")
112
- sys.exit(0)
113
- ACCEPTSTUB
114
- git -C "$REPO" add -A && git -C "$REPO" commit -qm "accepting validator"
115
- ACCEPT_CANDIDATE="$(run_gate --base "$BASE" --print-candidate)"
116
- python3 - "$REPO/specs/round/evidence/closeout.json" "$ACCEPT_CANDIDATE" <<'JSONGEN'
117
- import json, sys
118
- from pathlib import Path
119
- Path(sys.argv[1]).write_text(json.dumps({"schema_version": 3, "closeout_state": "ready_for_human_decision", "controller_receipts": [], "candidate_sha256": sys.argv[2]}))
120
- JSONGEN
121
- git -C "$REPO" add -A && git -C "$REPO" commit -qm "accepted ledger"
122
- out="$(run_gate --base "$BASE")"; rc=$?
123
- check "a validated ledger bound to the landing candidate passes the gate" \
124
- '[ "$rc" = 0 ] && case "$out" in *"binds the landing candidate"*) true;; *) false;; esac'
125
-
126
- # The wording-only lane records a single review receipt rather than a ledger.
127
- rm "$REPO/specs/round/evidence/closeout.json"
128
- python3 - "$REPO/specs/round/evidence/wording-review.json" "$ACCEPT_CANDIDATE" <<'JSONGEN'
129
- import json, sys
130
- from pathlib import Path
131
- Path(sys.argv[1]).write_text(json.dumps({"schema_version": 3, "mode": "review", "candidate_sha256": sys.argv[2], "wording_only_proof_sha256": "b" * 64}))
132
- JSONGEN
133
- git -C "$REPO" add -A && git -C "$REPO" commit -qm "wording-only proof"
134
- out="$(run_gate --base "$BASE")"; rc=$?
135
- check "a hand-writable receipt-shaped file never satisfies the gate" \
136
- '[ "$rc" = 1 ] && case "$out" in *"no accepted review evidence"*) true;; *) false;; esac'
137
-
138
- # Precision: a wording-only receipt for a different candidate must not pass.
139
- python3 - "$REPO/specs/round/evidence/wording-review.json" <<'PY'
140
- import json, sys
141
- from pathlib import Path
142
- Path(sys.argv[1]).write_text(json.dumps({
143
- "schema_version": 3,
144
- "mode": "review",
145
- "candidate_sha256": "1" * 64,
146
- "wording_only_proof_sha256": "b" * 64,
147
- }))
148
- PY
149
- git -C "$REPO" add -A && git -C "$REPO" commit -qm "stale wording-only proof"
150
- out="$(run_gate --base "$BASE")"; rc=$?
151
- check "a receipt-shaped file for another candidate does not satisfy the gate" \
152
- '[ "$rc" = 1 ] && case "$out" in *"no accepted review evidence"*) true;; *) false;; esac'
153
-
154
- # An option-shaped base must not reach git as an option: git diff with no revision
155
- # compares the index to the working tree, which in a clean checkout reports no
156
- # paths and would let the gate pass having compared nothing.
157
- # The environment path is the one that reaches git without an argument parser in
158
- # front of it, which is exactly how such a base arrives in CI.
159
- for bad_base in --quiet --name-only refs/heads/does-not-exist; do
160
- out="$(CCL_SKILL_BASE_REF="$bad_base" run_gate)"; rc=$?
161
- check "an unresolvable base ($bad_base) is refused, not silently treated as no change" \
162
- '[ "$rc" = 1 ] && case "$out" in *"does not resolve to a commit"*) true;; *) false;; esac'
163
- done
164
-
165
- out="$(run_gate --base "$BASE^{commit}")"; rc=$?
166
- check "a revision expression that does resolve is still accepted" \
167
- '[ "$rc" = 1 ] && case "$out" in *"no accepted review evidence"*) true;; *) false;; esac'
168
-
169
- # The gate must not perturb the tree it hashes; asserting only that a hash comes
170
- # back would stay green if bytecode writes or packet cleanup regressed.
171
- before_state="$(git -C "$REPO" status --porcelain --ignored)"
172
- run_gate --base "$BASE" --print-candidate >/dev/null 2>&1
173
- after_state="$(git -C "$REPO" status --porcelain --ignored)"
174
- check "running the gate leaves the hashed tree byte-identical" \
175
- '[ "$before_state" = "$after_state" ]'
176
-
177
- # What merges is the committed tree, so uncommitted evidence is not evidence about
178
- # the landing candidate: reading it would accept a ledger nobody can find later.
179
- printf '{"unfinished": true}\n' >"$REPO/specs/round/evidence/uncommitted.json"
180
- out="$(run_gate --base "$BASE")"; rc=$?
181
- check "an uncommitted evidence tree is refused rather than read from disk" \
182
- '[ "$rc" = 1 ] && case "$out" in *"uncommitted changes"*) true;; *) false;; esac'
183
- rm "$REPO/specs/round/evidence/uncommitted.json"
184
-
185
- # An unrelated advance on the base branch must not restate what this branch is:
186
- # measured against the tip it would, and evidence that is still correct would go
187
- # stale every time somebody else merged.
188
- BEFORE_ADVANCE="$(run_gate --base "$BASE" --print-candidate)"
189
- git -C "$REPO" checkout -q -b other-work "$BASE"
190
- printf 'unrelated\n' >"$REPO/skills/code-review/UNRELATED.md"
191
- git -C "$REPO" add -A && git -C "$REPO" commit -qm "unrelated work on the base branch"
192
- ADVANCED="$(git -C "$REPO" rev-parse HEAD)"
193
- git -C "$REPO" checkout -q -
194
- AFTER_ADVANCE="$(run_gate --base "$ADVANCED" --print-candidate)"
195
- check "an unrelated advance on the base branch leaves the candidate unchanged" \
196
- '[ -n "$BEFORE_ADVANCE" ] && [ "$AFTER_ADVANCE" = "$BEFORE_ADVANCE" ]'
197
-
198
- # Scope: which paths the candidate is computed over. A whitelist of the paths the
199
- # round happened to review leaves every other tracked path unbound, so a pull
200
- # request can carry a ledger valid for its skill changes while also landing a root
201
- # build script nobody reviewed -- the extra content does not move the candidate, so
202
- # the existing ledger still passes. The bound set is therefore every tracked path
203
- # except the evidence tree, which must stay outside it: evidence that moved the
204
- # candidate it records could never be committed.
205
- SCOPE="$WORK/scope-repo"
206
- mkdir -p "$SCOPE/skills/code-review/scripts" "$SCOPE/skills/skill-extraction-workflow/scripts" "$SCOPE/specs/round/evidence" "$SCOPE/scripts"
207
- cp "$CONTROLLER" "$SCOPE/skills/code-review/scripts/review_gate.py"
208
- cat >"$SCOPE/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py" <<'PY'
209
- import sys
210
- print("extraction_review_state_ok: stub accepted")
211
- sys.exit(0)
212
- PY
213
- git -C "$SCOPE" init -q .
214
- git -C "$SCOPE" config user.email t@example.invalid
215
- git -C "$SCOPE" config user.name tester
216
- printf 'baseline\n' >"$SCOPE/skills/skill-extraction-workflow/SKILL.md"
217
- printf 'test:\n\t@echo baseline\n' >"$SCOPE/Makefile"
218
- printf 'baseline\n' >"$SCOPE/README.md"
219
- printf 'baseline\n' >"$SCOPE/scripts/release.sh"
220
- # Committed review history for the earlier round: a plan and a receipt that already
221
- # exist at the base. These are what a later pull request must not be able to rewrite
222
- # unnoticed.
223
- printf 'earlier round plan\n' >"$SCOPE/specs/round/plan.md"
224
- printf '{"schema_version": 3, "closeout_state": "ready_for_human_decision"}\n' \
225
- >"$SCOPE/specs/round/evidence/history.json"
226
- git -C "$SCOPE" add -A
227
- git -C "$SCOPE" commit -qm baseline
228
- SCOPE_BASE="$(git -C "$SCOPE" rev-parse HEAD)"
229
-
230
- run_scope() { python3 "$GATE" --repo-root "$SCOPE" "$@" 2>&1; }
231
-
232
- # Each of these is an executable or consumer-facing tracked path outside the
233
- # reviewed-skill tree. Landing one with no evidence is the failure the whitelist
234
- # admitted: before the bound set was inverted every one of them reported
235
- # "no reviewed-path change" and merged unreviewed.
236
- for unbound in Makefile README.md scripts/release.sh; do
237
- git -C "$SCOPE" checkout -q -B "probe" "$SCOPE_BASE"
238
- printf 'landed unreviewed\n' >>"$SCOPE/$unbound"
239
- git -C "$SCOPE" add -A && git -C "$SCOPE" commit -qm "land $unbound with no evidence"
240
- out="$(run_scope --base "$SCOPE_BASE")"; rc=$?
241
- check "a change to $unbound alone needs review evidence like any other tracked path" \
242
- '[ "$rc" = 1 ] && case "$out" in *"no accepted review evidence binds the landing candidate"*) true;; *) false;; esac'
243
- done
244
-
245
- # The other half of the same decision: a committed receipt stays excluded. If it did
246
- # not, committing a ledger would move the candidate that ledger records and no
247
- # ledger could ever bind its own landing candidate.
248
- git -C "$SCOPE" checkout -q -B "evidence-only" "$SCOPE_BASE"
249
- printf '{"schema_version": 3, "candidate_sha256": "%s"}\n' \
250
- "0000000000000000000000000000000000000000000000000000000000000000" \
251
- >"$SCOPE/specs/round/evidence/note.json"
252
- git -C "$SCOPE" add -A && git -C "$SCOPE" commit -qm "land a receipt only"
253
- out="$(run_scope --base "$SCOPE_BASE")"; rc=$?
254
- check "a change confined to added receipts still reports no reviewed-path change" \
255
- '[ "$rc" = 0 ] && case "$out" in *"no reviewed-path change"*) true;; *) false;; esac'
256
-
257
- # The exclusion covers what this round ADDS, not the whole tree. Committed review
258
- # history is the record the gate exists to protect, so rewriting or deleting an
259
- # earlier round's plan or receipt has to reach the candidate like any other change.
260
- for corruption in rewrite-plan rewrite-receipt delete-receipt; do
261
- git -C "$SCOPE" checkout -q -B "corrupt" "$SCOPE_BASE"
262
- case "$corruption" in
263
- rewrite-plan) printf 'rewritten by a later round\n' >"$SCOPE/specs/round/plan.md";;
264
- rewrite-receipt) printf '{"schema_version": 3, "closeout_state": "forged"}\n' \
265
- >"$SCOPE/specs/round/evidence/history.json";;
266
- delete-receipt) rm "$SCOPE/specs/round/evidence/history.json";;
267
- esac
268
- git -C "$SCOPE" add -A && git -C "$SCOPE" commit -qm "$corruption"
269
- out="$(run_scope --base "$SCOPE_BASE")"; rc=$?
270
- check "a later round cannot $corruption of committed review history without evidence" \
271
- '[ "$rc" = 1 ] && case "$out" in *"no accepted review evidence binds the landing candidate"*) true;; *) false;; esac'
272
- done
273
-
274
- # The exclusion is about what a file IS, not where it sits. An evidence directory is
275
- # where receipts belong, but the directory alone must not buy exclusion: otherwise a
276
- # script, a fixture, or any other artifact committed there lands unreviewed while an
277
- # unchanged ledger still passes.
278
- for smuggled in payload.py fixture.bin notes.json; do
279
- git -C "$SCOPE" checkout -q -B "smuggle" "$SCOPE_BASE"
280
- mkdir -p "$SCOPE/specs/new-round/evidence"
281
- case "$smuggled" in
282
- payload.py) printf 'import os\nos.system("echo landed")\n' >"$SCOPE/specs/new-round/evidence/$smuggled";;
283
- fixture.bin) printf 'not text at all\n' >"$SCOPE/specs/new-round/evidence/$smuggled";;
284
- notes.json) printf '{"note": "json, but binds no candidate"}\n' >"$SCOPE/specs/new-round/evidence/$smuggled";;
285
- esac
286
- git -C "$SCOPE" add -A && git -C "$SCOPE" commit -qm "smuggle $smuggled"
287
- out="$(run_scope --base "$SCOPE_BASE")"; rc=$?
288
- check "an added $smuggled under an evidence directory is bound, not excluded" \
289
- '[ "$rc" = 1 ] && case "$out" in *"no accepted review evidence binds the landing candidate"*) true;; *) false;; esac'
290
- done
291
-
292
- # The candidate has to be the tree a clean checkout recomputes. An uncommitted edit
293
- # or a scratch file inside the bound paths would otherwise produce a hash only this
294
- # working copy can reproduce, which the author then records in the ledger.
295
- git -C "$SCOPE" checkout -q -B "dirty-probe" "$SCOPE_BASE"
296
- printf 'scratch\n' >"$SCOPE/scratch-note.txt"
297
- out="$(run_scope --base "$SCOPE_BASE" --print-candidate)"; rc=$?
298
- check "an untracked file inside the bound paths refuses instead of printing an unreproducible hash" \
299
- '[ "$rc" != 0 ] && case "$out" in *"uncommitted changes"*) true;; *) false;; esac'
300
- rm "$SCOPE/scratch-note.txt"
301
- printf 'unstaged edit\n' >>"$SCOPE/Makefile"
302
- out="$(run_scope --base "$SCOPE_BASE" --print-candidate)"; rc=$?
303
- check "an unstaged edit inside the bound paths refuses instead of printing an unreproducible hash" \
304
- '[ "$rc" != 0 ] && case "$out" in *"uncommitted changes"*) true;; *) false;; esac'
305
- git -C "$SCOPE" checkout -q -- Makefile
306
- out="$(run_scope --base "$SCOPE_BASE" --print-candidate)"; rc=$?
307
- check "the same clean checkout still reaches its normal verdict" \
308
- '[ "$rc" = 0 ] && case "$out" in *"no reviewed-path change"*) true;; *) false;; esac'
309
-
310
- # Partition: the candidate's Git identity is base..HEAD, but the packet a reviewer
311
- # can read is capped at the controller's byte ceiling. A landing candidate larger
312
- # than one packet used to be unlandable as one pull request -- the whole-candidate
313
- # freeze failed and no ledger could ever bind it -- so authors split the pull
314
- # request instead of the review. A committed landing partition manifest names
315
- # path partitions that together cover every changed file exactly once, each bound
316
- # by its own validated ledger; the gate recomputes every partition and refuses any
317
- # manifest whose parts do not add up to the whole.
318
- PART="$WORK/partition-repo"
319
- mkdir -p "$PART/skills/code-review/scripts" "$PART/skills/skill-extraction-workflow/scripts" "$PART/specs/round/evidence" "$PART/lane-a" "$PART/lane-b"
320
- cp "$CONTROLLER" "$PART/skills/code-review/scripts/review_gate.py"
321
- cat >"$PART/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py" <<'PY'
322
- import sys
323
- print("extraction_review_state_ok: stub accepted")
324
- sys.exit(0)
325
- PY
326
- git -C "$PART" init -q .
327
- git -C "$PART" config user.email t@example.invalid
328
- git -C "$PART" config user.name tester
329
- printf 'baseline\n' >"$PART/skills/skill-extraction-workflow/SKILL.md"
330
- printf 'baseline\n' >"$PART/lane-a/big.txt"
331
- printf 'baseline\n' >"$PART/lane-b/big.txt"
332
- git -C "$PART" add -A && git -C "$PART" commit -qm baseline
333
- PART_BASE="$(git -C "$PART" rev-parse HEAD)"
334
-
335
- run_part() { python3 "$GATE" --repo-root "$PART" "$@" 2>&1; }
336
-
337
- # The manifest oracle mirrors the documented canonical form (sorted keys, compact
338
- # separators, UTF-8) independently of the gate, so a drift in either side reds.
339
- cat >"$WORK/write-manifest.py" <<'PY'
340
- import hashlib, json, sys
341
- out, base, aggregate = sys.argv[1], sys.argv[2], sys.argv[3]
342
- partitions = []
343
- for spec in sys.argv[4:]:
344
- paths, digest = spec.rsplit("=", 1)
345
- partitions.append({"paths": paths.split(), "candidate_sha256": digest})
346
- body = {"schema_version": 1, "kind": "landing_partition_manifest", "base": base, "partitions": partitions}
347
- if aggregate == "AUTO":
348
- canonical = json.dumps(body, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
349
- aggregate = hashlib.sha256(canonical).hexdigest()
350
- body["candidate_sha256"] = aggregate
351
- with open(out, "w", encoding="utf-8") as handle:
352
- json.dump(body, handle, indent=2)
353
- handle.write("\n")
354
- PY
355
- write_closeout() { python3 - "$1" "$2" <<'PY'
356
- import json, sys
357
- from pathlib import Path
358
- Path(sys.argv[1]).write_text(json.dumps({"schema_version": 3, "closeout_state": "ready_for_human_decision", "controller_receipts": [], "candidate_sha256": sys.argv[2]}))
359
- PY
360
- }
361
-
362
- # Two lanes each change ~130 KB: either fits one packet, the pair does not.
363
- python3 - "$PART" <<'PY'
364
- import sys
365
- from pathlib import Path
366
- root = Path(sys.argv[1])
367
- for lane in ("lane-a", "lane-b"):
368
- lines = [f"{lane} line {index:06d} " + "x" * 40 for index in range(2200)]
369
- (root / lane / "big.txt").write_text("\n".join(lines) + "\n")
370
- PY
371
- git -C "$PART" add -A && git -C "$PART" commit -qm "oversized landing"
372
-
373
- out="$(run_part --base "$PART_BASE")"; rc=$?
374
- check "a candidate too large for one packet is refused with the partition recipe, not a bare freeze error" \
375
- '[ "$rc" = 1 ] && case "$out" in *"exceeds 200000 bytes"*"landing partition manifest"*) true;; *) false;; esac'
376
-
377
- MANIFEST_JSON="$(run_part --base "$PART_BASE" --print-manifest --partition lane-a --partition lane-b)"; rc=$?
378
- check "the gate renders a manifest for a partition that covers the whole candidate" \
379
- '[ "$rc" = 0 ] && python3 -c "import json,sys; m=json.loads(sys.argv[1]); assert m[\"kind\"]==\"landing_partition_manifest\" and len(m[\"partitions\"])==2 and m[\"base\"]==sys.argv[2]" "$MANIFEST_JSON" "$PART_BASE"'
380
-
381
- out="$(run_part --base "$PART_BASE" --print-manifest --partition lane-a)"; rc=$?
382
- check "rendering refuses a partition that leaves a changed path uncovered" \
383
- '[ "$rc" != 0 ] && case "$out" in *"uncovered"*"lane-b/big.txt"*) true;; *) false;; esac'
384
-
385
- out="$(run_part --base "$PART_BASE" --print-manifest --partition lane-a --partition lane-a/big.txt lane-b)"; rc=$?
386
- check "rendering refuses partitions that overlap on a changed path" \
387
- '[ "$rc" != 0 ] && case "$out" in *"overlap"*"lane-a/big.txt"*) true;; *) false;; esac'
388
-
389
- out="$(run_part --base "$PART_BASE" --print-manifest --partition lane-a --partition lane-a lane-b)"; rc=$?
390
- check "rendering refuses the same path listed in two partitions before touching git" \
391
- '[ "$rc" != 0 ] && case "$out" in *"listed twice"*"lane-a"*) true;; *) false;; esac'
392
-
393
- HASH_A="$(run_part --base "$PART_BASE" --print-candidate --paths lane-a)"
394
- HASH_B="$(run_part --base "$PART_BASE" --print-candidate --paths lane-b)"
395
- check "a partition's hash is exactly what --print-candidate --paths already answers" \
396
- '[ ${#HASH_A} = 64 ] && python3 -c "import json,sys; m=json.loads(sys.argv[1]); assert [p[\"candidate_sha256\"] for p in m[\"partitions\"]]==[sys.argv[2], sys.argv[3]]" "$MANIFEST_JSON" "$HASH_A" "$HASH_B"'
397
-
398
- printf '%s\n' "$MANIFEST_JSON" >"$PART/specs/round/evidence/landing-partitions.json"
399
- git -C "$PART" add -A && git -C "$PART" commit -qm "partition manifest"
400
- out="$(run_part --base "$PART_BASE")"; rc=$?
401
- check "a manifest whose partitions carry no validated ledger is refused, naming the partition" \
402
- '[ "$rc" = 1 ] && case "$out" in *"no accepted ledger"*"lane-a"*) true;; *) false;; esac'
403
- HASH_A_AFTER="$(run_part --base "$PART_BASE" --print-candidate --paths lane-a)"
404
- check "committing the manifest does not move the partition it describes" \
405
- '[ "$HASH_A_AFTER" = "$HASH_A" ]'
406
-
407
- write_closeout "$PART/specs/round/evidence/closeout-a.json" "$HASH_A"
408
- write_closeout "$PART/specs/round/evidence/closeout-b.json" "$HASH_B"
409
- git -C "$PART" add -A && git -C "$PART" commit -qm "partition ledgers"
410
- out="$(run_part --base "$PART_BASE")"; rc=$?
411
- check "a complete manifest with a validated ledger per partition binds a candidate no single packet could" \
412
- '[ "$rc" = 0 ] && case "$out" in *"binds the landing candidate"*"2 partitions"*) true;; *) false;; esac'
413
- PART_GOOD="$(git -C "$PART" rev-parse HEAD)"
414
-
415
- # Every way the parts can fail to add up to the whole is refused for its own
416
- # reason. Each probe branches from the passing state so the cases stay independent.
417
- MANIFEST="$PART/specs/round/evidence/landing-partitions.json"
418
- probe_part() {
419
- git -C "$PART" checkout -q -B "probe" "$PART_GOOD"
420
- }
421
-
422
- probe_part
423
- python3 "$WORK/write-manifest.py" "$MANIFEST" "$PART_BASE" AUTO "lane-a=$HASH_A"
424
- git -C "$PART" add -A && git -C "$PART" commit -qm "manifest drops a lane"
425
- out="$(run_part --base "$PART_BASE")"; rc=$?
426
- check "a manifest that leaves a changed path in no partition is refused" \
427
- '[ "$rc" = 1 ] && case "$out" in *"uncovered"*"lane-b/big.txt"*) true;; *) false;; esac'
428
-
429
- probe_part
430
- python3 "$WORK/write-manifest.py" "$MANIFEST" "$PART_BASE" AUTO "lane-a=$HASH_A" "lane-a/big.txt lane-b=$HASH_B"
431
- git -C "$PART" add -A && git -C "$PART" commit -qm "manifest overlaps"
432
- out="$(run_part --base "$PART_BASE")"; rc=$?
433
- check "a manifest whose partitions overlap is refused before any packet is frozen" \
434
- '[ "$rc" = 1 ] && case "$out" in *"overlap"*"lane-a/big.txt"*) true;; *) false;; esac'
435
-
436
- probe_part
437
- printf 'moved after the manifest was written\n' >>"$PART/lane-a/big.txt"
438
- git -C "$PART" add -A && git -C "$PART" commit -qm "candidate moves after manifest"
439
- out="$(run_part --base "$PART_BASE")"; rc=$?
440
- check "a partition whose recorded hash no longer reproduces is refused" \
441
- '[ "$rc" = 1 ] && case "$out" in *"lane-a"*"does not reproduce"*) true;; *) false;; esac'
442
-
443
- probe_part
444
- python3 "$WORK/write-manifest.py" "$MANIFEST" "0000000000000000000000000000000000000000" AUTO "lane-a=$HASH_A" "lane-b=$HASH_B"
445
- git -C "$PART" add -A && git -C "$PART" commit -qm "manifest names another base"
446
- out="$(run_part --base "$PART_BASE")"; rc=$?
447
- check "a manifest written against another base is refused" \
448
- '[ "$rc" = 1 ] && case "$out" in *"base"*"fork point"*) true;; *) false;; esac'
449
-
450
- probe_part
451
- python3 "$WORK/write-manifest.py" "$MANIFEST" "$PART_BASE" "$(printf 'f%.0s' $(seq 64))" "lane-a=$HASH_A" "lane-b=$HASH_B"
452
- git -C "$PART" add -A && git -C "$PART" commit -qm "manifest aggregate forged"
453
- out="$(run_part --base "$PART_BASE")"; rc=$?
454
- check "a manifest whose aggregate hash does not reproduce its partitions is refused" \
455
- '[ "$rc" = 1 ] && case "$out" in *"aggregate"*) true;; *) false;; esac'
456
-
457
- for bad_path in ":(exclude)lane-b" "-lane-b" "../lane-b" "/lane-b" "lane-*" "lane-?" "lane-[ab]" "lane-\\b"; do
458
- probe_part
459
- python3 "$WORK/write-manifest.py" "$MANIFEST" "$PART_BASE" AUTO "lane-a=$HASH_A" "$bad_path=$HASH_B"
460
- git -C "$PART" add -A && git -C "$PART" commit -qm "manifest smuggles a pathspec"
461
- out="$(run_part --base "$PART_BASE")"; rc=$?
462
- check "a manifest partition path shaped like $bad_path is refused as a path, not handed to git" \
463
- '[ "$rc" = 1 ] && case "$out" in *"partition path"*) true;; *) false;; esac'
464
- done
465
-
466
- out="$(run_part --base "$PART_BASE" --print-manifest --partition 'lane-*')"; rc=$?
467
- check "rendering refuses a wildcard partition instead of letting git expand it" \
468
- '[ "$rc" != 0 ] && case "$out" in *"partition path"*"wildcard"*) true;; *) false;; esac'
469
-
470
- # Under a narrowed --paths scope the partition union must equal the reviewed
471
- # changed set, not merely contain it: a partition naming `.` reaches lane-b even
472
- # though only lane-a is under review, and that surplus is refused before any
473
- # packet is frozen. The ledgers are removed so the single-ledger path cannot
474
- # satisfy the narrowed scope first.
475
- probe_part
476
- git -C "$PART" rm -q "$PART/specs/round/evidence/closeout-a.json" "$PART/specs/round/evidence/closeout-b.json"
477
- python3 "$WORK/write-manifest.py" "$MANIFEST" "$PART_BASE" AUTO ".=$(printf '0%.0s' $(seq 64))"
478
- git -C "$PART" add -A && git -C "$PART" commit -qm "manifest reaches outside the reviewed scope"
479
- out="$(run_part --base "$PART_BASE" --paths lane-a)"; rc=$?
480
- check "a partition reaching changed paths outside a narrowed --paths scope is refused, not accepted as covering" \
481
- '[ "$rc" = 1 ] && case "$out" in *"outside the reviewed scope"*"lane-b/big.txt"*) true;; *) false;; esac'
482
-
483
- probe_part
484
- git -C "$PART" rm -q "$PART/specs/round/evidence/closeout-b.json"
485
- git -C "$PART" commit -qm "one ledger missing"
486
- out="$(run_part --base "$PART_BASE")"; rc=$?
487
- check "a manifest with one partition unbound is refused, naming that partition" \
488
- '[ "$rc" = 1 ] && case "$out" in *"no accepted ledger"*"lane-b"*) true;; *) false;; esac'
489
-
490
- git -C "$PART" checkout -q -B "landing" "$PART_GOOD"
491
- out="$(run_part --base "$PART_BASE")"; rc=$?
492
- check "the passing partition state still passes after the probes" \
493
- '[ "$rc" = 0 ]'
494
-
495
- # Chain: an integration branch accumulates several reviewed rounds, each merged
496
- # with its own ledger bound at its own base, and is then promoted as one pull
497
- # request. No single ledger binds the promotion candidate, and a path partition
498
- # cannot either when two rounds append to the same file: that file's promotion
499
- # diff is the sum of both appends and no round ever froze that sum. Every byte
500
- # of the promotion reached the branch through a first-parent merge whose second
501
- # parent is a reviewed head, so the gate walks that chain and rebinds each round
502
- # in a detached checkout at the round's own base, requiring every merge to be
503
- # exactly the automatic merge of its parents.
504
- CHAIN="$WORK/chain-repo"
505
- mkdir -p "$CHAIN/skills/code-review/scripts" "$CHAIN/skills/skill-extraction-workflow/scripts" "$CHAIN/specs"
506
- cp "$CONTROLLER" "$CHAIN/skills/code-review/scripts/review_gate.py"
507
- # This stub has a rule, unlike the accept-all stubs above, so a round can forge a
508
- # ledger the real validator would reject and the suite can tell whose validator
509
- # judged it.
510
- STRICT_VALIDATOR='import json, sys
511
- payload = json.load(open(sys.argv[1], encoding="utf-8"))
512
- if payload.get("closeout_state") == "ready_for_human_decision":
513
- print("extraction_review_state_ok: stub accepted")
514
- sys.exit(0)
515
- print("extraction_review_state_invalid: stub rejected")
516
- sys.exit(1)
517
- '
518
- printf '%s' "$STRICT_VALIDATOR" >"$CHAIN/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py"
519
- git -C "$CHAIN" init -q -b main .
520
- git -C "$CHAIN" config user.email t@example.invalid
521
- git -C "$CHAIN" config user.name tester
522
- printf 'baseline\n' >"$CHAIN/skills/skill-extraction-workflow/SKILL.md"
523
- printf '| row | owner |\n' >"$CHAIN/skills/skill-extraction-workflow/register.md"
524
- printf 'baseline\n' >"$CHAIN/README.md"
525
- git -C "$CHAIN" add -A && git -C "$CHAIN" commit -qm baseline
526
- CHAIN_BASE="$(git -C "$CHAIN" rev-parse HEAD)"
527
- git -C "$CHAIN" checkout -q -b dev
528
-
529
- run_chain() { python3 "$GATE" --repo-root "$CHAIN" "$@" 2>&1; }
530
-
531
- # Land one reviewed round on the integration branch: branch, edit, ledger bound
532
- # to the round's own candidate (its base is the integration tip it forked from),
533
- # then a non-fast-forward merge so the round is one first-parent step.
534
- land_round() {
535
- local name="$1" ledger="$2"; shift 2
536
- git -C "$CHAIN" checkout -q -b "round-$name" dev
537
- "$@"
538
- git -C "$CHAIN" add -A && git -C "$CHAIN" commit -qm "round $name"
539
- if [ "$ledger" = with-ledger ]; then
540
- local digest
541
- digest="$(run_chain --base dev --print-candidate)"
542
- mkdir -p "$CHAIN/specs/$name/evidence"
543
- write_closeout "$CHAIN/specs/$name/evidence/closeout.json" "$digest"
544
- git -C "$CHAIN" add -A && git -C "$CHAIN" commit -qm "round $name ledger"
545
- fi
546
- git -C "$CHAIN" checkout -q dev
547
- git -C "$CHAIN" merge -q --no-ff -m "merge round $name" "round-$name"
548
- }
549
- edit_round_a() {
550
- printf 'baseline\nround a\n' >"$CHAIN/skills/skill-extraction-workflow/SKILL.md"
551
- printf '| round a | owner |\n' >>"$CHAIN/skills/skill-extraction-workflow/register.md"
552
- }
553
- edit_round_b() {
554
- mkdir -p "$CHAIN/lane"
555
- printf 'round b\n' >"$CHAIN/lane/file.txt"
556
- printf '| round b | owner |\n' >>"$CHAIN/skills/skill-extraction-workflow/register.md"
557
- }
558
- land_round a with-ledger edit_round_a
559
- land_round b with-ledger edit_round_b
560
- CHAIN_TWO="$(git -C "$CHAIN" rev-parse HEAD)"
561
-
562
- worktrees_before="$(git -C "$CHAIN" worktree list --porcelain)"
563
- tree_before="$(git -C "$CHAIN" status --porcelain --ignored)"
564
- out="$(run_chain --base "$CHAIN_BASE")"; rc=$?
565
- check "two rounds appending to one file bind through the first-parent chain where no single ledger or path partition can" \
566
- '[ "$rc" = 0 ] && case "$out" in *"first-parent chain"*"2 rounds"*"specs/b/evidence/closeout.json"*"specs/a/evidence/closeout.json"*) true;; *) false;; esac'
567
- check "walking the chain leaves no detached checkout registered and the bound tree byte-identical" \
568
- '[ "$(git -C "$CHAIN" worktree list --porcelain)" = "$worktrees_before" ] && [ "$(git -C "$CHAIN" status --porcelain --ignored)" = "$tree_before" ]'
569
-
570
- out="$(run_chain --base "$CHAIN_BASE" --paths skills)"; rc=$?
571
- check "a narrowed --paths scope does not evaluate the chain, because each round's ledger binds the whole round" \
572
- '[ "$rc" = 1 ] && case "$out" in *"default path set"*) true;; *) false;; esac'
573
-
574
- # The promotion pull request is brought up to date with its target: the target
575
- # advanced, and the sync merge's second parent is already on the base. That step
576
- # owes no evidence, but it must still be exactly the automatic merge.
577
- git -C "$CHAIN" checkout -q main
578
- printf 'baseline\nfixed on the target\n' >"$CHAIN/README.md"
579
- git -C "$CHAIN" add -A && git -C "$CHAIN" commit -qm "target advances"
580
- CHAIN_MAIN="$(git -C "$CHAIN" rev-parse HEAD)"
581
- git -C "$CHAIN" checkout -q dev
582
- git -C "$CHAIN" merge -q --no-ff -m "sync target into dev" main
583
- out="$(run_chain --base "$CHAIN_MAIN")"; rc=$?
584
- check "a sync merge of the advanced target on top of the chain owes no evidence and still binds" \
585
- '[ "$rc" = 0 ] && case "$out" in *"first-parent chain"*"2 rounds"*"sync"*) true;; *) false;; esac'
586
- CHAIN_GOOD="$(git -C "$CHAIN" rev-parse HEAD)"
587
-
588
- # A round the integration branch advanced past before it merged: its fork point
589
- # is not the merge's first parent, the merge is still the automatic one, and the
590
- # round's ledger was bound at its own fork point.
591
- git -C "$CHAIN" checkout -q -b round-late dev
592
- printf 'round late\n' >"$CHAIN/lane/late.txt"
593
- git -C "$CHAIN" add -A && git -C "$CHAIN" commit -qm "round late"
594
- LATE_DIGEST="$(run_chain --base dev --print-candidate)"
595
- mkdir -p "$CHAIN/specs/late/evidence"
596
- write_closeout "$CHAIN/specs/late/evidence/closeout.json" "$LATE_DIGEST"
597
- git -C "$CHAIN" add -A && git -C "$CHAIN" commit -qm "round late ledger"
598
- git -C "$CHAIN" checkout -q dev
599
- edit_round_c() { printf 'round c\n' >"$CHAIN/lane/c.txt"; }
600
- land_round c with-ledger edit_round_c
601
- git -C "$CHAIN" merge -q --no-ff -m "merge round late" round-late
602
- out="$(run_chain --base "$CHAIN_MAIN")"; rc=$?
603
- check "a round merged after the integration branch advanced still binds at its own fork point" \
604
- '[ "$rc" = 0 ] && case "$out" in *"first-parent chain"*"4 rounds"*) true;; *) false;; esac'
605
- CHAIN_ADVANCED="$(git -C "$CHAIN" rev-parse HEAD)"
606
-
607
- # Every way a chain step can fail to be a reviewed round is refused for its own
608
- # reason. Each probe branches from the passing state so the cases stay independent.
609
- probe_chain() { git -C "$CHAIN" checkout -q -B dev "$CHAIN_GOOD"; }
610
-
611
- probe_chain
612
- edit_round_d() { printf 'round d\n' >"$CHAIN/lane/d.txt"; }
613
- land_round d no-ledger edit_round_d
614
- UNBOUND_MERGE="$(git -C "$CHAIN" rev-parse --short=12 HEAD)"
615
- out="$(run_chain --base "$CHAIN_MAIN")"; rc=$?
616
- check "a round with no accepted ledger at its own base refuses the whole chain, naming that round" \
617
- '[ "$rc" = 1 ] && case "$out" in *"round $UNBOUND_MERGE"*"does not bind at its own base"*) true;; *) false;; esac'
618
-
619
- # Evidence is read from the LANDING tree, not from the round's own checkout. A
620
- # round that merged without its ledger is still a set of bytes some later review
621
- # can freeze and inspect; a validator-accepted closeout for exactly that candidate,
622
- # committed on the integration branch afterwards, binds it. A closeout for any
623
- # other digest does not, wherever it sits — the candidate hash, not the file's
624
- # location, is what makes a ledger evidence. The retro ledger lands as a round of
625
- # its own (a merge whose only change is receipt-shaped evidence), because a
626
- # direct commit on the integration branch is refused for its own reason.
627
- D_BASE="$(git -C "$CHAIN" rev-parse HEAD^1)"
628
- git -C "$CHAIN" checkout -q round-d
629
- D_DIGEST="$(run_chain --base "$D_BASE" --print-candidate)"
630
- git -C "$CHAIN" checkout -q dev
631
- edit_retro_wrong() {
632
- mkdir -p "$CHAIN/specs/d-retro-wrong/evidence"
633
- write_closeout "$CHAIN/specs/d-retro-wrong/evidence/closeout.json" "$(printf '%064d' 7)"
634
- }
635
- land_round d-retro-wrong no-ledger edit_retro_wrong
636
- out="$(run_chain --base "$CHAIN_MAIN")"; rc=$?
637
- check "a later closeout for a different digest does not bind the unbound round" \
638
- '[ "$rc" = 1 ] && case "$out" in *"round $UNBOUND_MERGE"*"does not bind at its own base"*) true;; *) false;; esac'
639
- # The right digest in a closeout the validator rejects is still not evidence.
640
- edit_retro_rejected() {
641
- mkdir -p "$CHAIN/specs/d-retro-rejected/evidence"
642
- python3 - "$CHAIN/specs/d-retro-rejected/evidence/closeout.json" "$D_DIGEST" <<'PY'
643
- import json, sys
644
- from pathlib import Path
645
- Path(sys.argv[1]).write_text(json.dumps({"schema_version": 3, "closeout_state": "forged", "controller_receipts": [], "candidate_sha256": sys.argv[2]}))
646
- PY
647
- }
648
- land_round d-retro-rejected no-ledger edit_retro_rejected
649
- out="$(run_chain --base "$CHAIN_MAIN")"; rc=$?
650
- check "a later closeout for the right digest that the validator rejects does not bind the unbound round" \
651
- '[ "$rc" = 1 ] && case "$out" in *"round $UNBOUND_MERGE"*"does not bind at its own base"*) true;; *) false;; esac'
652
- # Evidence in the landing tree must be COMMITTED there, exactly as before: an
653
- # untracked closeout for the right digest is refused outright, not read from disk.
654
- mkdir -p "$CHAIN/specs/d-retro/evidence"
655
- write_closeout "$CHAIN/specs/d-retro/evidence/closeout.json" "$D_DIGEST"
656
- out="$(run_chain --base "$CHAIN_MAIN")"; rc=$?
657
- check "an uncommitted closeout in the landing tree is refused before any round is bound" \
658
- '[ "$rc" != 0 ] && case "$out" in *"uncommitted changes"*) true;; *) false;; esac'
659
- rm -r "$CHAIN/specs/d-retro"
660
- edit_retro_right() {
661
- mkdir -p "$CHAIN/specs/d-retro/evidence"
662
- write_closeout "$CHAIN/specs/d-retro/evidence/closeout.json" "$D_DIGEST"
663
- }
664
- land_round d-retro no-ledger edit_retro_right
665
- out="$(run_chain --base "$CHAIN_MAIN")"; rc=$?
666
- check "a validator-accepted closeout for the round's own candidate, committed on the integration branch after the merge, binds it through the chain" \
667
- '[ "$rc" = 0 ] && case "$out" in *"first-parent chain"*"specs/d-retro/evidence/closeout.json"*) true;; *) false;; esac'
668
- # Later evidence changes where a ledger may be found, never what the round is:
669
- # the unbound round's candidate at its own base hashes exactly as it did before
670
- # any retro ledger existed, so its packet, base and excludes are untouched.
671
- git -C "$CHAIN" checkout -q round-d
672
- D_DIGEST_AFTER="$(run_chain --base "$D_BASE" --print-candidate)"
673
- git -C "$CHAIN" checkout -q dev
674
- check "the round's candidate hash is unchanged by evidence committed later in the landing tree" \
675
- '[ "$D_DIGEST_AFTER" = "$D_DIGEST" ]'
676
-
677
- probe_chain
678
- printf 'pushed straight to the integration branch\n' >"$CHAIN/lane/direct.txt"
679
- git -C "$CHAIN" add -A && git -C "$CHAIN" commit -qm "direct commit on dev"
680
- DIRECT="$(git -C "$CHAIN" rev-parse --short=12 HEAD)"
681
- out="$(run_chain --base "$CHAIN_MAIN")"; rc=$?
682
- check "a non-merge commit on the integration branch is refused: nothing reviewed binds its content" \
683
- '[ "$rc" = 1 ] && case "$out" in *"$DIRECT is not a merge commit"*) true;; *) false;; esac'
684
-
685
- probe_chain
686
- git -C "$CHAIN" checkout -q -b round-evil dev
687
- printf 'round evil\n' >"$CHAIN/lane/evil.txt"
688
- git -C "$CHAIN" add -A && git -C "$CHAIN" commit -qm "round evil"
689
- EVIL_DIGEST="$(run_chain --base dev --print-candidate)"
690
- mkdir -p "$CHAIN/specs/evil/evidence"
691
- write_closeout "$CHAIN/specs/evil/evidence/closeout.json" "$EVIL_DIGEST"
692
- git -C "$CHAIN" add -A && git -C "$CHAIN" commit -qm "round evil ledger"
693
- git -C "$CHAIN" checkout -q dev
694
- git -C "$CHAIN" merge -q --no-ff --no-commit round-evil
695
- printf 'slipped into the merge commit itself\n' >"$CHAIN/lane/smuggled.txt"
696
- git -C "$CHAIN" add -A && git -C "$CHAIN" commit -qm "merge round evil (with extra content)"
697
- EVIL_MERGE="$(git -C "$CHAIN" rev-parse --short=12 HEAD)"
698
- out="$(run_chain --base "$CHAIN_MAIN")"; rc=$?
699
- check "a merge whose tree is not the automatic merge of its parents is refused, naming the merge" \
700
- '[ "$rc" = 1 ] && case "$out" in *"$EVIL_MERGE"*"automatic merge"*) true;; *) false;; esac'
701
-
702
- probe_chain
703
- git -C "$CHAIN" checkout -q -b round-conflict "$CHAIN_TWO"
704
- printf 'baseline\nconflicting fix\n' >"$CHAIN/README.md"
705
- git -C "$CHAIN" add -A && git -C "$CHAIN" commit -qm "round conflict"
706
- CONFLICT_DIGEST="$(run_chain --base "$CHAIN_TWO" --print-candidate)"
707
- mkdir -p "$CHAIN/specs/conflict/evidence"
708
- write_closeout "$CHAIN/specs/conflict/evidence/closeout.json" "$CONFLICT_DIGEST"
709
- git -C "$CHAIN" add -A && git -C "$CHAIN" commit -qm "round conflict ledger"
710
- git -C "$CHAIN" checkout -q dev
711
- git -C "$CHAIN" merge -q --no-ff --no-commit round-conflict >/dev/null 2>&1 || true
712
- printf 'baseline\nresolved by hand\n' >"$CHAIN/README.md"
713
- git -C "$CHAIN" add -A && git -C "$CHAIN" commit -qm "merge round conflict (resolved by hand)"
714
- CONFLICT_MERGE="$(git -C "$CHAIN" rev-parse --short=12 HEAD)"
715
- out="$(run_chain --base "$CHAIN_MAIN")"; rc=$?
716
- check "a merge that needed hand resolution is refused: the resolution is content no round reviewed" \
717
- '[ "$rc" = 1 ] && case "$out" in *"$CONFLICT_MERGE"*"automatic merge"*) true;; *) false;; esac'
718
-
719
- probe_chain
720
- git -C "$CHAIN" checkout -q -b round-stale dev
721
- printf 'round stale\n' >"$CHAIN/lane/stale.txt"
722
- git -C "$CHAIN" add -A && git -C "$CHAIN" commit -qm "round stale"
723
- STALE_DIGEST="$(run_chain --base dev --print-candidate)"
724
- mkdir -p "$CHAIN/specs/stale/evidence"
725
- write_closeout "$CHAIN/specs/stale/evidence/closeout.json" "$STALE_DIGEST"
726
- git -C "$CHAIN" add -A && git -C "$CHAIN" commit -qm "round stale ledger"
727
- printf 'edited after the review\n' >>"$CHAIN/lane/stale.txt"
728
- git -C "$CHAIN" add -A && git -C "$CHAIN" commit -qm "round stale moves after its ledger"
729
- git -C "$CHAIN" checkout -q dev
730
- git -C "$CHAIN" merge -q --no-ff -m "merge round stale" round-stale
731
- STALE_MERGE="$(git -C "$CHAIN" rev-parse --short=12 HEAD)"
732
- out="$(run_chain --base "$CHAIN_MAIN")"; rc=$?
733
- check "a round whose ledger binds a candidate it later moved away from is refused" \
734
- '[ "$rc" = 1 ] && case "$out" in *"round $STALE_MERGE"*"does not bind at its own base"*) true;; *) false;; esac'
735
-
736
- # A round is judged with the landing tree's controller and validator, never its
737
- # own: a round that installs an accept-all validator in its own branch, forges a
738
- # ledger the real validator rejects, and is later followed by a round restoring
739
- # the real validator must not bind through history's tools.
740
- probe_chain
741
- git -C "$CHAIN" checkout -q -b round-forged dev
742
- printf 'round forged\n' >"$CHAIN/lane/forged.txt"
743
- printf 'import sys\nprint("extraction_review_state_ok: hollow validator accepts everything")\nsys.exit(0)\n' \
744
- >"$CHAIN/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py"
745
- git -C "$CHAIN" add -A && git -C "$CHAIN" commit -qm "round forged"
746
- FORGED_DIGEST="$(run_chain --base dev --print-candidate)"
747
- mkdir -p "$CHAIN/specs/forged/evidence"
748
- python3 - "$CHAIN/specs/forged/evidence/closeout.json" "$FORGED_DIGEST" <<'PY'
749
- import json, sys
750
- from pathlib import Path
751
- Path(sys.argv[1]).write_text(json.dumps({"schema_version": 3, "closeout_state": "forged", "controller_receipts": [], "candidate_sha256": sys.argv[2]}))
752
- PY
753
- git -C "$CHAIN" add -A && git -C "$CHAIN" commit -qm "round forged ledger"
754
- git -C "$CHAIN" checkout -q dev
755
- git -C "$CHAIN" merge -q --no-ff -m "merge round forged" round-forged
756
- FORGED_MERGE="$(git -C "$CHAIN" rev-parse --short=12 HEAD)"
757
- edit_round_restore() { printf '%s' "$STRICT_VALIDATOR" >"$CHAIN/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py"; }
758
- land_round restore with-ledger edit_round_restore
759
- out="$(run_chain --base "$CHAIN_MAIN")"; rc=$?
760
- check "a round that forged a ledger under its own hollow validator is refused even after a later round restored the real one" \
761
- '[ "$rc" = 1 ] && case "$out" in *"round $FORGED_MERGE"*"does not bind at its own base"*) true;; *) false;; esac'
762
-
763
- # The walk is bounded at exactly 64 steps: a chain of 64 passes, 65 is refused.
764
- # Receipt-only rounds keep the fixture fast, since a round whose only change is
765
- # an excluded receipt binds as "no reviewed-path change" without a freeze.
766
- probe_chain
767
- fill_round() {
768
- git -C "$CHAIN" checkout -q -b "round-fill-$1" dev
769
- mkdir -p "$CHAIN/specs/fill-$1/evidence"
770
- printf '{"schema_version": 3, "candidate_sha256": "%s"}\n' "$(printf '0%.0s' $(seq 64))" >"$CHAIN/specs/fill-$1/evidence/note.json"
771
- git -C "$CHAIN" add -A && git -C "$CHAIN" commit -qm "fill $1"
772
- git -C "$CHAIN" checkout -q dev
773
- git -C "$CHAIN" merge -q --no-ff -m "merge fill $1" "round-fill-$1"
774
- }
775
- for index in $(seq 1 61); do fill_round "$index"; done
776
- out="$(run_chain --base "$CHAIN_MAIN")"; rc=$?
777
- check "a chain of exactly 64 steps is still walked and binds" \
778
- '[ "$rc" = 0 ] && case "$out" in *"first-parent chain"*"63 rounds"*) true;; *) false;; esac'
779
- fill_round 62
780
- out="$(run_chain --base "$CHAIN_MAIN")"; rc=$?
781
- check "a chain of 65 steps is refused as longer than the bound" \
782
- '[ "$rc" = 1 ] && case "$out" in *"more than 64 steps"*) true;; *) false;; esac'
783
-
784
- # A checkout this run cannot release is an error, never a pass: the removal's
785
- # result is checked and the registry read back, and nothing prunes registrations
786
- # the run did not create. A git shim refuses only `worktree remove`.
787
- git -C "$CHAIN" checkout -q -B dev "$CHAIN_GOOD"
788
- REAL_GIT="$(command -v git)"
789
- mkdir -p "$WORK/fakegit" "$WORK/shim-tmp"
790
- cat >"$WORK/fakegit/git" <<EOF
791
- #!/usr/bin/env bash
792
- if [ "\$1" = "-C" ] && [ "\$3" = "worktree" ] && [ "\$4" = "remove" ]; then
793
- echo "shim: refusing to remove the worktree" >&2
794
- exit 1
795
- fi
796
- exec "$REAL_GIT" "\$@"
797
- EOF
798
- chmod +x "$WORK/fakegit/git"
799
- out="$(PATH="$WORK/fakegit:$PATH" TMPDIR="$WORK/shim-tmp" run_chain --base "$CHAIN_MAIN")"; rc=$?
800
- check "a detached checkout that cannot be released turns the verdict into an error, never a pass" \
801
- '[ "$rc" != 0 ] && case "$out" in *"cannot release the detached checkout"*) case "$out" in *review_ledger_binding_ok*) false;; *) true;; esac;; *) false;; esac'
802
- git -C "$CHAIN" worktree prune
803
-
804
- # `git worktree add` can register and populate the checkout and still return
805
- # nonzero. The failure is an error either way, but the registration it left
806
- # behind must be released and verified, not merely have its directory deleted.
807
- mkdir -p "$WORK/fakegit-add" "$WORK/shim-add-tmp"
808
- cat >"$WORK/fakegit-add/git" <<EOF
809
- #!/usr/bin/env bash
810
- if [ "\$1" = "-C" ] && [ "\$3" = "worktree" ] && [ "\$4" = "add" ]; then
811
- "$REAL_GIT" "\$@"
812
- echo "shim: worktree add failed after registering" >&2
813
- exit 1
814
- fi
815
- exec "$REAL_GIT" "\$@"
816
- EOF
817
- chmod +x "$WORK/fakegit-add/git"
818
- worktrees_before="$(git -C "$CHAIN" worktree list --porcelain)"
819
- out="$(PATH="$WORK/fakegit-add:$PATH" TMPDIR="$WORK/shim-add-tmp" run_chain --base "$CHAIN_MAIN")"; rc=$?
820
- check "a checkout creation that fails after registering is an error and leaves no registration or directory behind" \
821
- '[ "$rc" != 0 ] && case "$out" in *"cannot check out round head"*) case "$out" in *review_ledger_binding_ok*) false;; *) true;; esac;; *) false;; esac && [ "$(git -C "$CHAIN" worktree list --porcelain)" = "$worktrees_before" ] && [ -z "$(ls -A "$WORK/shim-add-tmp")" ]'
822
- git -C "$CHAIN" worktree prune
823
-
824
- # The benign sibling: an add that fails before registering anything must error
825
- # the same way, leave the registry untouched, and remove the directory this run
826
- # created for it, without reporting a release problem it does not have.
827
- mkdir -p "$WORK/fakegit-noadd" "$WORK/shim-noadd-tmp"
828
- cat >"$WORK/fakegit-noadd/git" <<EOF
829
- #!/usr/bin/env bash
830
- if [ "\$1" = "-C" ] && [ "\$3" = "worktree" ] && [ "\$4" = "add" ]; then
831
- echo "shim: worktree add refused before registering" >&2
832
- exit 1
833
- fi
834
- exec "$REAL_GIT" "\$@"
835
- EOF
836
- chmod +x "$WORK/fakegit-noadd/git"
837
- worktrees_before="$(git -C "$CHAIN" worktree list --porcelain)"
838
- out="$(PATH="$WORK/fakegit-noadd:$PATH" TMPDIR="$WORK/shim-noadd-tmp" run_chain --base "$CHAIN_MAIN")"; rc=$?
839
- check "a checkout creation that fails before registering errors cleanly with no registration, no directory, and no release complaint" \
840
- '[ "$rc" != 0 ] && case "$out" in *"cannot check out round head"*) case "$out" in *"could not be released"*|*review_ledger_binding_ok*) false;; *) true;; esac;; *) false;; esac && [ "$(git -C "$CHAIN" worktree list --porcelain)" = "$worktrees_before" ] && [ -z "$(ls -A "$WORK/shim-noadd-tmp")" ]'
841
-
842
- # The registry is read NUL-terminated: line-oriented porcelain cannot carry a
843
- # path with a newline (the line splits; a tab is printed raw on the git in use,
844
- # and quoting depends on version and core.quotePath), so a registration under
845
- # such a temporary directory would match neither spelling and read as gone.
846
- # Here the add registers and fails, the remove is refused, and the surviving
847
- # registration must be reported.
848
- TAB_TMP="$WORK/shim nl
849
- dir"
850
- mkdir -p "$WORK/fakegit-tab" "$TAB_TMP"
851
- cat >"$WORK/fakegit-tab/git" <<EOF
852
- #!/usr/bin/env bash
853
- if [ "\$1" = "-C" ] && [ "\$3" = "worktree" ] && [ "\$4" = "add" ]; then
854
- "$REAL_GIT" "\$@"
855
- echo "shim: worktree add failed after registering" >&2
856
- exit 1
857
- fi
858
- if [ "\$1" = "-C" ] && [ "\$3" = "worktree" ] && [ "\$4" = "remove" ]; then
859
- echo "shim: refusing to remove the worktree" >&2
860
- exit 1
861
- fi
862
- exec "$REAL_GIT" "\$@"
863
- EOF
864
- chmod +x "$WORK/fakegit-tab/git"
865
- worktrees_before="$(git -C "$CHAIN" worktree list --porcelain)"
866
- out="$(PATH="$WORK/fakegit-tab:$PATH" TMPDIR="$TAB_TMP" run_chain --base "$CHAIN_MAIN")"; rc=$?
867
- check "a surviving registration under a newline-bearing temporary path is still reported, not read as gone through line-oriented porcelain" \
868
- '[ "$rc" != 0 ] && case "$out" in *"cannot check out round head"*"could not be released"*"still registered"*) true;; *) false;; esac'
869
- rm -rf "$TAB_TMP"
870
- git -C "$CHAIN" worktree prune
871
- # This case deliberately leaves a registration behind, so its own cleanup is
872
- # what restores the fixture. Prune only drops registrations whose directory is
873
- # already gone, so the directory must go first; asserting the restoration keeps
874
- # a later reordering from silently handing every following case a mutated
875
- # registry.
876
- check "the newline-path case restores the fixture's worktree registry for the cases that follow" \
877
- '[ "$(git -C "$CHAIN" worktree list --porcelain)" = "$worktrees_before" ]'
878
-
879
- git -C "$CHAIN" checkout -q -B dev "$CHAIN_ADVANCED"
880
- out="$(run_chain --base "$CHAIN_MAIN")"; rc=$?
881
- check "the passing chain state still passes after the probes" \
882
- '[ "$rc" = 0 ]'
883
-
884
- # The suite above exercises a synthetic repository. The gate must also run against
885
- # the real checkout it ships in, or a break in that path passes every test here.
886
- REAL_ROOT="$(cd "$DIR/../../.." && pwd -P)"
887
- real_out="$(env -u CCL_SKILL_BASE_REF python3 "$GATE" --repo-root "$REAL_ROOT" --base HEAD~1 --print-candidate 2>&1)"; real_rc=$?
888
- # Three outputs are legitimate and which one appears depends on what the parent
889
- # commit happened to touch: a candidate hash when a bounded reviewed path moved,
890
- # the no-change signal when it did not, or the packet-ceiling refusal when the
891
- # parent diff is too large for one reviewer packet.
892
- # Which answer is legitimate depends on the checkout, so decide that FIRST and then
893
- # assert the one matching branch. Accepting every output would make this assertion a
894
- # function of repository state; branching on the state keeps it an assertion about
895
- # the gate. A dirty checkout is a legitimate state for a developer to run in, and
896
- # the gate must refuse there rather than print a hash nobody else can recompute.
897
- real_dirty="$(git -C "$REAL_ROOT" status --porcelain)"
898
- if [ -n "$real_dirty" ]; then
899
- check "the gate refuses to answer for a real checkout that is not committed" \
900
- '[ "$real_rc" != 0 ] && case "$real_out" in *"uncommitted changes"*) true;; *) false;; esac'
901
- else
902
- check "the gate answers for the clean real checkout it ships in, whichever legitimate answer applies" \
903
- '{ [ "$real_rc" = 0 ] && { case "$real_out" in [0-9a-f]*) [ ${#real_out} = 64 ];; *) false;; esac || case "$real_out" in *review_ledger_binding_no_change*) true;; *) false;; esac; }; } || { [ "$real_rc" = 1 ] && case "$real_out" in *"cannot freeze the candidate packet"*"review packet exceeds 200000 bytes"*) true;; *) false;; esac; }'
904
- fi
905
-
906
- # The point of splitting the candidate from the packet is that a round which
907
- # widened its packet to answer an evidence-gap finding still produces a receipt
908
- # this gate can accept. That claim spans both sides, so it is asserted across
909
- # both: the identity the controller records for a widened packet must be the
910
- # identity this gate recomputes from the repository. Mutate the gate to return
911
- # the packet hash instead and this goes red while nothing else does.
912
- printf 'widened-candidate\n' >>"$REPO/skills/skill-extraction-workflow/SKILL.md"
913
- git -C "$REPO" add -A
914
- git -C "$REPO" commit -qm widened
915
- WIDENED_BASE="$BASE"
916
- GATE_CANDIDATE="$(run_gate --base "$WIDENED_BASE" --print-candidate)"
917
- controller_candidate="$(
918
- CONTROLLER_DIR="$(dirname "$CONTROLLER")" REPO="$REPO" BASE="$WIDENED_BASE" \
919
- GATE="$GATE" WORK="$WORK" python3 - <<'PY' 2>&1
920
- import importlib.util
921
- import os
922
- import sys
923
- import time
924
- from pathlib import Path
925
- from types import SimpleNamespace
926
-
927
- sys.path.insert(0, os.environ["CONTROLLER_DIR"])
928
- import review_gate
929
-
930
- spec = importlib.util.spec_from_file_location("binder", os.environ["GATE"])
931
- binder = importlib.util.module_from_spec(spec)
932
- spec.loader.exec_module(binder)
933
-
934
- repo = Path(os.environ["REPO"])
935
- # An author cannot hand-build these: the base is a fork point and the paths
936
- # carry the receipt exclusions this round already added. Both come from the gate
937
- # that will judge the receipt, which is what makes it one identity.
938
- base, _excludes, paths, _changed = binder.candidate_scope(
939
- repo, os.environ["BASE"], (".",)
940
- )
941
-
942
-
943
- def freeze(**overrides):
944
- args = SimpleNamespace(
945
- cwd=str(repo),
946
- diff_file=None,
947
- base=base,
948
- paths=list(paths),
949
- wording_only_proof_file=None,
950
- )
951
- for key, value in overrides.items():
952
- setattr(args, key, value)
953
- return review_gate.freeze_packet(args, time.monotonic() + 60)
954
-
955
-
956
- narrow_path, _narrow_packet, narrow_candidate, _n, _p, _s = freeze()
957
- subject = narrow_path.read_bytes()
958
- narrow_path.unlink()
959
-
960
- # Outside the repository: the candidate includes untracked files.
961
- widened = Path(os.environ["WORK"]) / "widened-for-binder.patch"
962
- widened.write_bytes(subject + b"\n--- appended context for the reviewer ---\n")
963
- wide_path, wide_packet, wide_candidate, _wn, _p, _s = freeze(diff_file=str(widened))
964
- wide_path.unlink()
965
-
966
- assert wide_packet != wide_candidate, "the widened packet must not be its own candidate"
967
- assert wide_candidate == narrow_candidate, "widening moved the candidate"
968
- print(wide_candidate)
969
- PY
970
- )"
971
- check "a widened packet records the candidate identity this gate recomputes" \
972
- '[ ${#GATE_CANDIDATE} = 64 ] && [ "$controller_candidate" = "$GATE_CANDIDATE" ]'
973
-
974
- if [ "$fails" -gt 0 ]; then
975
- echo "test_review_ledger_binding: $fails failing case(s)" >&2
976
- exit 1
977
- fi
978
- echo "test_review_ledger_binding: ok"