@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.
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/SKILL.md +4 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/staged-review-contract.md +18 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/review_gate.py +178 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh +127 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/defect-diagnosis/SKILL.md +2 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/SKILL.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/SKILL.md +1 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/SKILL.md +11 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/async-lifecycle-and-performance.md +16 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/source-map.md +1 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/SKILL.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/delivery-lifecycle.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/dispatch-owner-skills.md +9 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/problem-resolution-and-learning.md +2 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/SKILL.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/references/tag-and-prod-pipeline-gate.md +9 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +9 -9
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +7 -23
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/external-practice-controls.md +16 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-quickstart.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/rule-consolidation.md +2 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +27 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-to-skill-extraction.md +2 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/validation-and-landing.md +2 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +5 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/contract-anchors.tsv +1 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/impact-chain-gate.rb +35 -6
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/review_ledger_binding.py +476 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_impact_chain_refscripts.sh +81 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +3 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_review_ledger_binding.sh +336 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_routing_pointer_integrity.sh +41 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_extraction_review_state.sh +116 -9
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py +123 -32
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/SKILL.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/SKILL.md +9 -8
- package/dist/assets/release.json +47 -37
- 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=
|
|
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
|
-
|
|
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 -
|
|
176
|
+
remaining = 2 - chain_position
|
|
172
177
|
if status == "findings":
|
|
173
|
-
state = "post_review_budget" if
|
|
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":
|
|
183
|
-
|
|
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
|
-
"
|
|
191
|
-
"
|
|
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")
|