@ccoalm/ccl-skills 0.10.0 → 0.11.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 (38) hide show
  1. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/SKILL.md +4 -3
  2. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/staged-review-contract.md +18 -0
  3. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/review_gate.py +178 -4
  4. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh +127 -0
  5. package/dist/assets/marketplace/plugins/ccl-skills/skills/defect-diagnosis/SKILL.md +2 -1
  6. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/SKILL.md +1 -1
  7. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/SKILL.md +1 -0
  8. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/SKILL.md +11 -1
  9. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/async-lifecycle-and-performance.md +16 -0
  10. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/source-map.md +1 -0
  11. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/SKILL.md +1 -1
  12. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/delivery-lifecycle.md +1 -1
  13. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/dispatch-owner-skills.md +9 -1
  14. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/problem-resolution-and-learning.md +2 -0
  15. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/SKILL.md +1 -1
  16. package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/references/tag-and-prod-pipeline-gate.md +9 -0
  17. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +9 -9
  18. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +7 -23
  19. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/external-practice-controls.md +16 -0
  20. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-quickstart.md +1 -1
  21. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/rule-consolidation.md +2 -0
  22. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +27 -0
  23. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-to-skill-extraction.md +2 -2
  24. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/validation-and-landing.md +2 -0
  25. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +5 -0
  26. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/contract-anchors.tsv +1 -0
  27. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/impact-chain-gate.rb +35 -6
  28. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/review_ledger_binding.py +476 -0
  29. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_impact_chain_refscripts.sh +81 -1
  30. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +3 -0
  31. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_review_ledger_binding.sh +336 -0
  32. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_routing_pointer_integrity.sh +41 -1
  33. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_extraction_review_state.sh +116 -9
  34. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py +123 -32
  35. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/SKILL.md +1 -1
  36. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/SKILL.md +9 -8
  37. package/dist/assets/release.json +47 -37
  38. package/package.json +1 -1
@@ -0,0 +1,336 @@
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
+ # The suite above exercises a synthetic repository. The gate must also run against
311
+ # the real checkout it ships in, or a break in that path passes every test here.
312
+ REAL_ROOT="$(cd "$DIR/../../.." && pwd -P)"
313
+ real_out="$(env -u CCL_SKILL_BASE_REF python3 "$GATE" --repo-root "$REAL_ROOT" --base HEAD~1 --print-candidate 2>&1)"; real_rc=$?
314
+ # Both outputs are legitimate and which one appears depends on what the parent
315
+ # commit happened to touch: a candidate hash when a reviewed path moved, the
316
+ # no-change signal when it did not. Accepting only the first makes this assertion
317
+ # a function of repository state rather than of the gate.
318
+ # Which answer is legitimate depends on the checkout, so decide that FIRST and then
319
+ # assert the one matching branch. Accepting every output would make this assertion a
320
+ # function of repository state; branching on the state keeps it an assertion about
321
+ # the gate. A dirty checkout is a legitimate state for a developer to run in, and
322
+ # the gate must refuse there rather than print a hash nobody else can recompute.
323
+ real_dirty="$(git -C "$REAL_ROOT" status --porcelain)"
324
+ if [ -n "$real_dirty" ]; then
325
+ check "the gate refuses to answer for a real checkout that is not committed" \
326
+ '[ "$real_rc" != 0 ] && case "$real_out" in *"uncommitted changes"*) true;; *) false;; esac'
327
+ else
328
+ check "the gate answers for the clean real checkout it ships in, whichever legitimate answer applies" \
329
+ '[ "$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; }'
330
+ fi
331
+
332
+ if [ "$fails" -gt 0 ]; then
333
+ echo "test_review_ledger_binding: $fails failing case(s)" >&2
334
+ exit 1
335
+ fi
336
+ echo "test_review_ledger_binding: ok"
@@ -133,10 +133,30 @@ check_absent() { # <file> <token> <label> — file must exist AND token absent;
133
133
  fi
134
134
  }
135
135
 
136
+ check_line_all() { # <file> <selector> <label> <req...> — EVERY <req> must sit on
137
+ # ONE line that matches <selector>. Filtering the matched lines through each req in
138
+ # turn is what binds them to the same line: asserting each req against the whole set
139
+ # of matched lines lets two reqs pass on two different lines, so a contract that says
140
+ # "both owners on that line" would be satisfied while no line carries both.
141
+ # -e is required: a selector starting with "-" is otherwise parsed as an option.
142
+ local file="$1" sel="$2" label="$3"; shift 3
143
+ local out req
144
+ out=$(grep -F -e "$sel" "$file" 2>/dev/null)
145
+ if [ -z "$out" ]; then
146
+ echo "FAIL: $label missing ($file :: no line matches '$sel')" >&2; fail=1; return
147
+ fi
148
+ for req in "$@"; do
149
+ out=$(printf '%s\n' "$out" | grep -F -e "$req")
150
+ if [ -z "$out" ]; then
151
+ echo "FAIL: $label missing ($file :: one line matching '$sel' must also contain '$req')" >&2; fail=1; return
152
+ fi
153
+ done
154
+ }
155
+
136
156
  # Self-pin: an emptied or truncated suite must fail, not pass. Update EXPECTED
137
157
  # when adding/removing checks deliberately (same diff). Runs FIRST: a truncated
138
158
  # tail removes real checks and the count goes red.
139
- EXPECTED_CHECKS=21
159
+ EXPECTED_CHECKS=24
140
160
  actual_checks=$(grep -cE '^check([_a-z]+)? ' "$0")
141
161
  if [ "$actual_checks" -ne "$EXPECTED_CHECKS" ]; then
142
162
  echo "FAIL: suite self-pin: expected $EXPECTED_CHECKS checks, found $actual_checks" >&2
@@ -176,6 +196,22 @@ check "$ROOT/skills/product-rd-workflow/SKILL.md" "must go to that language's de
176
196
  # no-owner fallback be deleted while the suite stays green, which is exactly the
177
197
  # routing hole (a CLI in a language with no dev owner) this rule closes.
178
198
  check "$ROOT/skills/product-rd-workflow/SKILL.md" "no such dev owner stays with \`terminal-cli-dev\` as the default CLI owner" "product-rd coordinator CLI carve-out (no-owner fallback branch, incl. destination)"
199
+ # A routing-bank case cannot guard this: a multi-stage Node request selects
200
+ # product-rd-workflow from its own wording and stays green even when the
201
+ # coordinator omits the Node owner from its dispatch map. Pin the map itself —
202
+ # both the stack enumeration and the CLI carve-out list — or the omission that
203
+ # sent Node deliveries to a coordinator with no owner to dispatch to recurs
204
+ # invisibly, with the no-owner fallback above silently reclaiming Node CLIs.
205
+ check_line_all "$ROOT/skills/product-rd-workflow/SKILL.md" "- Development routing:" "product-rd Development-routing line carries the Node owner in BOTH the stack enumeration and the CLI carve-out" "\`nodejs-service-dev\` for Node.js services" "\`python-service-dev\`, \`nodejs-service-dev\`)"
206
+ # Node has no *-architecture sibling by decision, so the technical-design gate
207
+ # must name where its architecture decisions go; without this the gate lists
208
+ # only the Go and Python architecture owners and Node design work has no owner.
209
+ # Naming the vacuum is not naming a destination: both owners must sit on that
210
+ # line, or the gate can declare Node has no architecture sibling and stop there.
211
+ # The positive ownership clause is pinned too: keeping only the two owner tokens
212
+ # while deleting "run this gate here" would leave the architecture decision ownerless,
213
+ # since nodejs-service-dev is named there as the IMPLEMENTATION owner, not the gate.
214
+ check_line_all "$ROOT/skills/product-rd-workflow/references/delivery-lifecycle.md" "For Node.js backend, worker, or CLI/tooling architecture" "product-rd design gate owns the Node architecture decision and names both downstream owners on that line" "run this gate here" "\`platform-*\` owners" "\`nodejs-service-dev\` for the Node-side implementation contract"
179
215
  # Both legs of the skip clause are pinned: an unanchored leg can be deleted while
180
216
  # the reciprocal trigger anchor above stays green, and the eval fixtures assert
181
217
  # terminal-cli-dev must NOT receive either request.
@@ -187,6 +223,10 @@ check_frontmatter_present "$ROOT/skills/terminal-cli-dev/SKILL.md" "Node.js CLI
187
223
  # implementation) — the gap two independent reviewer lanes hit.
188
224
  check_frontmatter_present "$ROOT/skills/terminal-cli-dev/SKILL.md" "command/subcommand/flag/help contract (owned here even when nothing is rendered)" "terminal-cli-dev owns the non-rendered CLI contract"
189
225
  check "$ROOT/skills/product-rd-workflow/SKILL.md" "command/subcommand/flag/help contract, which it owns even when nothing is rendered" "product-rd mirrors CLI-contract ownership"
226
+ # testing-strategy must not re-claim language-stack CLI test IMPLEMENTATION: that
227
+ # claim collided with the CLI carve-out every stack dev owner and the coordinator
228
+ # carry, so the line is scoped to the interface contract it actually owns.
229
+ check_line_all "$ROOT/skills/testing-strategy/SKILL.md" "- Use \`terminal-cli-dev\` for" "testing-strategy scopes terminal-cli-dev to the interface contract, not language-stack CLI implementation" "terminal interface contract"
190
230
  check_frontmatter_present "$ROOT/skills/platform-release-engineering/SKILL.md" "上线范围确认 / 合并 main / 打 tag" "rollout description prod-release skip"
191
231
  check "$ROOT/skills/app-cross-platform-dev/agents/openai.yaml" "React Native" "app-cross-platform-dev openai overlay RN parity"
192
232
 
@@ -145,6 +145,7 @@ def make_fixture(
145
145
  closeout=None,
146
146
  unmatched=0,
147
147
  open_last=False,
148
+ succession=False,
148
149
  ):
149
150
  if receipt_findings is None:
150
151
  receipt_findings = [[finding(1)], []]
@@ -166,11 +167,15 @@ def make_fixture(
166
167
  receipt_hashes = []
167
168
  receipt_refs = []
168
169
  for index, findings in enumerate(receipt_findings, start=1):
169
- mode = "review" if index == 1 else "challenge"
170
+ # With a succession the LAST round opens a second chain: its own chain
171
+ # arithmetic restarts at one while its lane position keeps counting.
172
+ is_succession = succession and index == rounds
173
+ chain_position = 1 if is_succession else index
174
+ mode = "challenge" if is_succession else "review" if index == 1 else "challenge"
170
175
  status = "findings" if findings else "passed"
171
- remaining = 2 - index
176
+ remaining = 2 - chain_position
172
177
  if status == "findings":
173
- state = "post_review_budget" if index == 2 else "findings_pending"
178
+ state = "post_review_budget" if chain_position == 2 else "findings_pending"
174
179
  else:
175
180
  state = "reviewed"
176
181
  receipt = {
@@ -179,16 +184,27 @@ def make_fixture(
179
184
  "status": status,
180
185
  "findings": copy.deepcopy(findings),
181
186
  "review_chain_tracked": True,
182
- "review_chain_id": "extraction-chain",
183
- "autonomous_review_index": index,
187
+ "review_chain_id": (
188
+ "extraction-succession" if is_succession else "extraction-chain"
189
+ ),
190
+ "autonomous_review_index": chain_position,
184
191
  "autonomous_review_budget": 2,
185
192
  "autonomous_reviews_remaining": remaining,
186
193
  "autonomous_review_allowed": remaining > 0,
187
- "challenge_index": 0 if index == 1 else index - 1,
194
+ "challenge_index": 1 if is_succession else 0 if index == 1 else index - 1,
188
195
  "challenge_budget": budget,
189
- "prior_review_result_sha256": list(receipt_hashes),
190
- "candidate_sha256": CANDIDATE,
191
- "packet_sha256": CANDIDATE,
196
+ "prior_review_result_sha256": [] if is_succession else list(receipt_hashes),
197
+ "predecessor_chain_id": "extraction-chain" if is_succession else None,
198
+ "predecessor_result_sha256": (
199
+ receipt_hashes[-1] if is_succession and receipt_hashes else None
200
+ ),
201
+ "predecessor_candidate_sha256": OTHER_CANDIDATE if is_succession else None,
202
+ "candidate_sha256": (
203
+ OTHER_CANDIDATE if succession and not is_succession else CANDIDATE
204
+ ),
205
+ "packet_sha256": (
206
+ OTHER_CANDIDATE if succession and not is_succession else CANDIDATE
207
+ ),
192
208
  "review_scope": copy.deepcopy(scope),
193
209
  "review_scope_sha256": scope_sha,
194
210
  "wording_only_proof_sha256": None,
@@ -504,6 +520,97 @@ run("review-only", review_only["ledger"], 1, "ready requires at least one tracke
504
520
  over_budget = make_fixture("over-budget", receipt_findings=[[], [], []])
505
521
  run("over-budget", over_budget["ledger"], 1, "exceeds the wrapper budget")
506
522
 
523
+ # Chain succession. A fix that edits the reviewed owner package ends its chain, so
524
+ # the landing candidate can only be challenged in a succeeding chain. The lane then
525
+ # spans two chains and three rounds, and the succession round is the only one that
526
+ # binds what actually lands.
527
+ succession_ready = make_fixture(
528
+ "succession-ready", receipt_findings=[[finding(1)], [], []], succession=True
529
+ )
530
+ run("succession-ready", succession_ready["ledger"], 0, "closeout_state=ready_for_human_decision")
531
+
532
+ # The failure this whole mechanism exists to make visible: the fix batch moved the
533
+ # candidate and no round ever inspected what lands.
534
+ succession_absent = make_fixture(
535
+ "succession-absent",
536
+ receipt_findings=[[finding(1)], []],
537
+ receipt_mutators={
538
+ 1: lambda row: row.update(
539
+ candidate_sha256=OTHER_CANDIDATE, packet_sha256=OTHER_CANDIDATE
540
+ ),
541
+ 2: lambda row: row.update(
542
+ candidate_sha256=OTHER_CANDIDATE, packet_sha256=OTHER_CANDIDATE
543
+ ),
544
+ },
545
+ )
546
+ run("succession-absent", succession_absent["ledger"], 1, "does not bind the ledger candidate")
547
+
548
+ # A succession whose predecessor saw the same candidate reviewed nothing new.
549
+ succession_unmoved = make_fixture(
550
+ "succession-unmoved",
551
+ receipt_findings=[[finding(1)], [], []],
552
+ succession=True,
553
+ receipt_mutators={
554
+ 1: lambda row: row.update(candidate_sha256=CANDIDATE, packet_sha256=CANDIDATE),
555
+ 2: lambda row: row.update(candidate_sha256=CANDIDATE, packet_sha256=CANDIDATE),
556
+ 3: lambda row: row.update(predecessor_candidate_sha256=CANDIDATE),
557
+ },
558
+ )
559
+ run(
560
+ "succession-unmoved",
561
+ succession_unmoved["ledger"],
562
+ 1,
563
+ "must bind a candidate the wrapper chain never saw",
564
+ )
565
+
566
+ succession_foreign_terminal = make_fixture(
567
+ "succession-foreign-terminal",
568
+ receipt_findings=[[finding(1)], [], []],
569
+ succession=True,
570
+ receipt_mutators={3: lambda row: row.update(predecessor_result_sha256="f" * 64)},
571
+ )
572
+ run(
573
+ "succession-foreign-terminal",
574
+ succession_foreign_terminal["ledger"],
575
+ 1,
576
+ "does not bind the succeeded chain's terminal receipt",
577
+ )
578
+
579
+ succession_self = make_fixture(
580
+ "succession-self",
581
+ receipt_findings=[[finding(1)], [], []],
582
+ succession=True,
583
+ receipt_mutators={3: lambda row: row.update(review_chain_id="extraction-chain")},
584
+ )
585
+ run("succession-self", succession_self["ledger"], 1, "succeeds its own chain")
586
+
587
+ succession_misplaced = make_fixture(
588
+ "succession-misplaced",
589
+ receipt_findings=[[finding(1)], [], []],
590
+ succession=True,
591
+ receipt_mutators={
592
+ 2: lambda row: row.update(predecessor_chain_id="extraction-chain"),
593
+ },
594
+ )
595
+ run(
596
+ "succession-misplaced",
597
+ succession_misplaced["ledger"],
598
+ 1,
599
+ "must be the lane's final round",
600
+ )
601
+
602
+ succession_over_lane = make_fixture(
603
+ "succession-over-lane",
604
+ receipt_findings=[[finding(1)], [], [], []],
605
+ succession=True,
606
+ )
607
+ run(
608
+ "succession-over-lane",
609
+ succession_over_lane["ledger"],
610
+ 1,
611
+ "controller_receipts must contain one to 3 ordered Agent rounds",
612
+ )
613
+
507
614
  missing_complete = make_fixture("missing-complete")
508
615
  missing_complete["ledger"]["completion_receipt"] = None
509
616
  run("missing-complete", missing_complete["ledger"], 1, "requires a completion receipt")