@ccoalm/ccl-skills 0.9.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 (57) 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 +23 -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 +8 -8
  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 +17 -20
  18. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/attention-budget-ratchet.md +37 -0
  19. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/description-authoring.md +9 -0
  20. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +26 -44
  21. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/eval-routing.md +24 -3
  22. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/external-practice-controls.md +16 -0
  23. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-quickstart.md +5 -5
  24. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/rule-consolidation.md +3 -1
  25. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +59 -0
  26. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-to-skill-extraction.md +2 -2
  27. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/validation-and-landing.md +3 -1
  28. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +35 -0
  29. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-contract-anchors.sh +126 -0
  30. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-size-budget.sh +197 -1
  31. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/contract-anchors.tsv +16 -0
  32. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/eval-routing-bank.rb +210 -36
  33. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/extraction_review_gate.sh +3 -3
  34. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/gate_receipt.py +576 -0
  35. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/impact-chain-gate.rb +35 -6
  36. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/review_ledger_binding.py +476 -0
  37. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_antipattern_grep_panel.sh +80 -0
  38. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_body_compliance_grading.sh +99 -0
  39. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_impact_chain_refscripts.sh +81 -1
  40. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +28 -0
  41. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_size_budget.sh +251 -0
  42. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_contract_anchors.sh +196 -0
  43. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_eval_routing_bank_grader_diagnostics.sh +222 -0
  44. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_extraction_review_gate.sh +16 -10
  45. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_frozen_case_sanctity.sh +178 -0
  46. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_frozen_case_sanctity_selfproof.sh +108 -0
  47. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_gate_receipt.sh +431 -0
  48. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_pinned_phrase_mutation_walk.sh +151 -0
  49. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_review_ledger_binding.sh +336 -0
  50. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_routing_bank_integrity.sh +86 -5
  51. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_routing_pointer_integrity.sh +41 -1
  52. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_extraction_review_state.sh +141 -28
  53. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py +139 -38
  54. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/SKILL.md +1 -1
  55. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/SKILL.md +9 -8
  56. package/dist/assets/release.json +110 -45
  57. package/package.json +1 -1
@@ -194,6 +194,86 @@ run_gate
194
194
  assert_not_contains "impact_chain_firing_path_missing" "$out" "a changed shebang owner executable should satisfy the firing-path gate"
195
195
  assert_rc "$rc" 0 "a complete RED row with a changed owner executable must pass"
196
196
 
197
+ # Non-ASCII anchors must be accepted, and the proof must be DIFFERENTIAL rather
198
+ # than one literal passing: a special-cased implementation, or one that handles
199
+ # Han but not a combining mark, would satisfy a single-literal leg. Each row below
200
+ # pairs a non-ASCII anchor with an ASCII anchor matched on everything the gate
201
+ # actually predicates on - uniqueness, the 16-character bar, list-line shape, a
202
+ # whitelisted normative verb - so the two verdicts must agree. This exists because
203
+ # a round concluded from reading the wrong lambda (`raw_blob_at` forces binary;
204
+ # `locator_valid` resolves through `blob_at`, which does not) that a non-ASCII
205
+ # anchor could never validate, and recorded a defect that was not there.
206
+ #
207
+ # Representations covered: Han; a precomposed Latin letter (NFC, two bytes); the
208
+ # same letter decomposed into base + combining mark (NFD, so the anchor carries a
209
+ # codepoint that is not a character on its own).
210
+ anchor_pair_case() {
211
+ # $1 = case label, $2 = non-ASCII anchor, $3 = ASCII anchor of the same shape
212
+ local label="$1" nonascii="$2" ascii="$3" verdict_nonascii verdict_ascii
213
+ new_case "case-ref-anchor-pair-$label-nonascii"
214
+ printf '\n- %s\n' "$nonascii" >> "$REPO/$UPSTREAM_REF"
215
+ printf '| Fixture anchor pair %s non-ASCII | `downstream-executor` | behavioral-evidence: RED-baseline; observed-failure: yes; result-class: failure; bank-evidence: downscoped:REFSCRIPTS-FIXTURE-NO-BANK; firing-path: file:skills/product-rd-workflow/references/adr-convention.md#%s | `updated` | `product-rd-workflow/SKILL.md` reference edit |\n' "$label" "$nonascii" >> "$REGISTER"
216
+ routing_surface_downscope
217
+ commit_case "anchor pair $label non-ASCII"
218
+ run_gate
219
+ verdict_nonascii="$rc"
220
+
221
+ new_case "case-ref-anchor-pair-$label-ascii"
222
+ printf '\n- %s\n' "$ascii" >> "$REPO/$UPSTREAM_REF"
223
+ printf '| Fixture anchor pair %s ASCII | `downstream-executor` | behavioral-evidence: RED-baseline; observed-failure: yes; result-class: failure; bank-evidence: downscoped:REFSCRIPTS-FIXTURE-NO-BANK; firing-path: file:skills/product-rd-workflow/references/adr-convention.md#%s | `updated` | `product-rd-workflow/SKILL.md` reference edit |\n' "$label" "$ascii" >> "$REGISTER"
224
+ routing_surface_downscope
225
+ commit_case "anchor pair $label ASCII"
226
+ run_gate
227
+ verdict_ascii="$rc"
228
+
229
+ assert_rc "$verdict_ascii" 0 "the ASCII control of pair $label must be accepted"
230
+ assert_rc "$verdict_nonascii" "$verdict_ascii" "pair $label: the non-ASCII anchor must get the same verdict as its ASCII control"
231
+ }
232
+
233
+ anchor_pair_case han \
234
+ '下游执行者必须在收到该契约变更时同步更新其验证面。' \
235
+ 'The downstream executor must refresh its verification surface on this change.'
236
+ # Same sentence and the same whitelisted verb in both arms; only the accented
237
+ # codepoint differs, so a verdict difference can only come from byte composition.
238
+ anchor_pair_case nfc \
239
+ 'The owner must refresh its vérification surface on this change.' \
240
+ 'The owner must refresh its verification surface on this change.'
241
+ anchor_pair_case nfd \
242
+ 'The owner must refresh its vérification surface on that change.' \
243
+ 'The owner must refresh its verification surface on that change.'
244
+
245
+ # When one owner carries several RED rows, the owner-level check reddens the whole
246
+ # package for a single bad row — so the diagnostic must name WHICH row failed.
247
+ # Without it the author inspects the row they just wrote (usually the correct one)
248
+ # while an unrelated row is the offender; that cost one round three iterations.
249
+ new_case case-ref-firing-path-names-offending-row
250
+ printf '\n- 下游执行者必须在收到该契约变更时同步更新其验证面。\n' >> "$REPO/$UPSTREAM_REF"
251
+ printf '| Fixture good row beside a bad one | `downstream-executor` | behavioral-evidence: RED-baseline; observed-failure: yes; result-class: failure; bank-evidence: downscoped:REFSCRIPTS-FIXTURE-NO-BANK; firing-path: file:skills/product-rd-workflow/references/adr-convention.md#下游执行者必须在收到该契约变更时同步更新其验证面 | `updated` | `product-rd-workflow/SKILL.md` reference edit |\n' >> "$REGISTER"
252
+ printf '| Fixture offending row with an unresolvable anchor | `downstream-executor` | behavioral-evidence: RED-baseline; observed-failure: yes; result-class: failure; bank-evidence: downscoped:REFSCRIPTS-FIXTURE-NO-BANK; firing-path: file:skills/product-rd-workflow/references/adr-convention.md#this anchor text was never added by this round | `updated` | `product-rd-workflow/SKILL.md` reference edit |\n' >> "$REGISTER"
253
+ routing_surface_downscope
254
+ commit_case "one good row beside one unresolvable row"
255
+ run_gate
256
+ assert_rc "$rc" 1 "an unresolvable anchor on any bound RED row must fail the owner"
257
+ assert_contains "impact_chain_firing_path_missing" "$out" "checker should report the firing-path verdict"
258
+ assert_contains "Fixture offending row with an unresolvable anchor" "$out" "the diagnostic must name the offending row"
259
+ assert_not_contains "offending row: Fixture good row beside a bad one" "$out" "the diagnostic must not name a row whose anchor resolves"
260
+
261
+ # The offending-row line renders repository text a contributor controls onto a
262
+ # terminal/CI channel. Control and invisible characters must be escaped, not merely
263
+ # truncated: raw ANSI or carriage-return bytes can erase or restyle surrounding
264
+ # output, and bidi overrides can reorder it, so a reader sees a diagnostic that does
265
+ # not match what the gate decided. Both reviewer lanes raised this independently.
266
+ new_case case-ref-offending-row-escapes-control-bytes
267
+ printf '\n- 下游执行者必须在收到该契约变更时同步更新其验证面。\n' >> "$REPO/$UPSTREAM_REF"
268
+ printf '| Fixture control-byte row \033[31m\rSPOOFED\u202E | `downstream-executor` | behavioral-evidence: RED-baseline; observed-failure: yes; result-class: failure; bank-evidence: downscoped:REFSCRIPTS-FIXTURE-NO-BANK; firing-path: file:skills/product-rd-workflow/references/adr-convention.md#this anchor text was never added by this round | `updated` | `product-rd-workflow/SKILL.md` reference edit |\n' >> "$REGISTER"
269
+ routing_surface_downscope
270
+ commit_case "offending row carrying control bytes"
271
+ run_gate
272
+ assert_rc "$rc" 1 "an unresolvable anchor must still fail with a control-byte row"
273
+ assert_contains "\\u001B" "$out" "the diagnostic must escape an ANSI introducer rather than emit it"
274
+ assert_contains "\\u000D" "$out" "the diagnostic must escape a carriage return rather than emit it"
275
+ assert_contains "\\u202E" "$out" "the diagnostic must escape a bidi override rather than emit it"
276
+
197
277
  # A malformed impact-chain-looking row (wrong cell count) is warned but stays
198
278
  # advisory: hard-blocking would false-positive on the register's many other
199
279
  # legitimate table shapes, and deliberate mangling is outside the trust model
@@ -1329,6 +1409,6 @@ assert_rc "$rc" 1 "a directory masquerading as SKILL.md must not read as a prese
1329
1409
  assert_contains "platform-observability/SKILL.md" "$out" "the masqueraded owner must be named"
1330
1410
 
1331
1411
  assert_rc "$full_check_runs" 1 "fixture suite must retain exactly one full checker wiring case"
1332
- assert_rc "$gate_runs" 80 "all remaining impact-chain fixtures must run the standalone gate"
1412
+ assert_rc "$gate_runs" 88 "all remaining impact-chain fixtures must run the standalone gate"
1333
1413
 
1334
1414
  echo "test_check_ccl_impact_chain_refscripts: ok"
@@ -114,6 +114,12 @@ fast_tests=(
114
114
  test_shared_git_surface_gate.sh
115
115
  test_extraction_review_gate.sh
116
116
  test_validate_extraction_review_state.sh
117
+ # Merge-side binding between the landing candidate and the review evidence that
118
+ # inspected it: own throwaway git repo, no clone, seconds.
119
+ test_review_ledger_binding.sh
120
+ # Candidate-SHA-bound gate receipts (mint/verify): own throwaway git repo,
121
+ # no clone, seconds — belongs in the lane every run exercises.
122
+ test_gate_receipt.sh
117
123
  test_check_ccl_route_drift.sh
118
124
  test_check_sync_pointers.sh
119
125
  test_check_ccl_register_pending_exclusion.sh
@@ -137,6 +143,13 @@ fast_tests=(
137
143
  test_validate_skill_cross_refs.sh
138
144
  test_git_identity_predicate_gate.sh
139
145
  test_liveness_predicate_gate.sh
146
+ # 074: contract-anchor gate self-proof (tmp fixtures + one real-repo leg,
147
+ # seconds) and the anti-patterns panel structural check (awk over one file).
148
+ test_check_contract_anchors.sh
149
+ test_antipattern_grep_panel.sh
150
+ # 075: body-compliance grading contract (grade() walk, fail-closed --ids legs,
151
+ # stub-model denominator/forbidden_hit legs; deterministic, seconds).
152
+ test_body_compliance_grading.sh
140
153
  test_regression_runner_registration.sh
141
154
  # Harness binding for the gate lanes themselves: the impact-chain gate's
142
155
  # round walk needs the branch's own first-parent chain, which the default
@@ -149,6 +162,14 @@ fast_tests=(
149
162
  test_regression_runner_lanes.sh
150
163
  test_routing_pointer_integrity.sh
151
164
  test_routing_bank_integrity.sh
165
+ # 077: frozen-case sanctity (regressions-are-sacred) — base-relative guard
166
+ # that a deleted/re-scoped bank case or golden trace names its adjudication
167
+ # row; degrades to an explicit skip token without CCL_SKILL_BASE_REF. The
168
+ # selfproof lane proves the oracle fails for the right reason (synthetic
169
+ # repo: deletion/re-scope/nested-trace mutants red, forged-credit legs red,
170
+ # adjudicated and control legs green).
171
+ test_frozen_case_sanctity.sh
172
+ test_frozen_case_sanctity_selfproof.sh
152
173
  test_uiux_delivery_contract.sh
153
174
  test_uiux_loading_budget.sh
154
175
  test_governing_chain_diff.sh
@@ -195,6 +216,13 @@ heavy_tests=(
195
216
  # 「跳过义务会不会留下可检出的缺席」——044 连续五轮 challenge 都停在这一点上,负向
196
217
  # 用例(缺证据、非枚举值、降范围无留痕)必须每轮都跑,否则触发器会悄悄退化成声称。
197
218
  test_impact_chain_self_adjudication.sh
219
+ # 074: one applied deletion mutation per pinned-phrase gate family, each leg
220
+ # running the FULL shipped checker against a committed fixture copy of the
221
+ # working tree (6 full-gate runs — far too slow for pre-commit). Proves the
222
+ # ~40 required_phrase pins and the contract-anchor delegation can actually
223
+ # go red for the right reason; the fast lane keeps the checker-level
224
+ # self-proof in test_check_contract_anchors.sh.
225
+ test_pinned_phrase_mutation_walk.sh
198
226
  )
199
227
 
200
228
  # Registration self-audit: every sibling test_*.sh must appear in fast_tests or
@@ -817,6 +817,257 @@ assert_contains "entrypoint_word_budget_block_partial: skills/demo-skill/SKILL.m
817
817
  assert_contains "entrypoint_word_budget_blocking_failed" "$out" "word-budget partial emits failed marker"
818
818
  assert_not_contains "entrypoint_word_budget_blocking_ok" "$out" "unknown baseline never earns a word-budget pass"
819
819
 
820
+ # ---------------------------------------------------------------------------
821
+ # Reference-file line ratchet (fresh repo): a NEW or crossing reference over 500
822
+ # physical lines blocks; a historical over-limit reference may stay level or
823
+ # shrink but not grow; the append-only ledger is excluded by gate design; an
824
+ # unresolvable base fails closed; a long new reference without ## structure
825
+ # draws an advisory, never a block.
826
+ REF_REPO="$TMP/repo-reference-lines"
827
+ mkdir -p "$REF_REPO/skills/demo-skill/references" "$REF_REPO/skills/legacy-skill/references"
828
+ git init -q -b main "$REF_REPO"
829
+ git -C "$REF_REPO" config user.email test@example.invalid
830
+ git -C "$REF_REPO" config user.name "Test User"
831
+
832
+ write_skill_stub() { # <skill-dir>
833
+ {
834
+ printf -- '---\nname: %s\ndescription: reference line-ratchet fixture.\n---\n\n' "$(basename "$1")"
835
+ printf -- 'Body pointer to references.\n'
836
+ } > "$1/SKILL.md"
837
+ }
838
+ write_reference_lines() { # <path> <line-count> [--no-headings]
839
+ local n="$2" i
840
+ : > "$1"
841
+ if [ "${3:-}" != "--no-headings" ]; then
842
+ printf -- '# Fixture reference\n\n## Section one\n' >> "$1"
843
+ i=3
844
+ else
845
+ i=0
846
+ fi
847
+ while [ "$i" -lt "$n" ]; do
848
+ printf -- 'line %s\n' "$i" >> "$1"
849
+ i=$((i + 1))
850
+ done
851
+ }
852
+
853
+ write_skill_stub "$REF_REPO/skills/demo-skill"
854
+ write_skill_stub "$REF_REPO/skills/legacy-skill"
855
+ write_reference_lines "$REF_REPO/skills/demo-skill/references/small.md" 40
856
+ write_reference_lines "$REF_REPO/skills/legacy-skill/references/legacy.md" 620
857
+ git -C "$REF_REPO" add -A
858
+ git -C "$REF_REPO" commit -qm "reference baseline: one small, one historical over-limit"
859
+ git -C "$REF_REPO" checkout -qb feature
860
+
861
+ run_ref() { # runs with a guaranteed-unset base ref
862
+ set +e
863
+ out="$(env -u CCL_SKILL_BASE_REF bash "$SIZE_SCRIPT" "$REF_REPO" 2>&1)"
864
+ rc=$?
865
+ set -e
866
+ }
867
+
868
+ # Case g0: nothing changed => clean counts and the reference verdict token. The
869
+ # historical over-limit file is visible in the counter but reddens nothing.
870
+ run_ref
871
+ assert_rc "$rc" 0 "untouched reference corpus must pass"
872
+ assert_contains "reference_line_over_limit_count_base=1" "$out" "base over-limit count available"
873
+ assert_contains "reference_line_over_limit_count_head=1" "$out" "head over-limit count available"
874
+ assert_contains "reference_line_over_limit_count_delta=+0" "$out" "over-limit delta is signed"
875
+ assert_contains "reference_line_budget_blocking_ok" "$out" "clean reference verdict token"
876
+ assert_not_contains "reference_line_block" "$out" "an untouched over-limit reference must not red an unrelated change"
877
+
878
+ # Case g1: a NEW reference over the line budget blocks with path, actual, and
879
+ # allowed counts.
880
+ write_reference_lines "$REF_REPO/skills/demo-skill/references/oversized.md" 501
881
+ run_ref
882
+ assert_rc "$rc" 1 "new over-budget reference must block"
883
+ assert_contains "reference_line_block: skills/demo-skill/references/oversized.md: new reference over line budget" "$out" "block names the file"
884
+ assert_contains "head_lines=501" "$out" "block cites the actual line count"
885
+ assert_contains "(> 500)" "$out" "block cites the allowed line count"
886
+ assert_contains "reference_line_budget_blocking_failed" "$out" "reference failure marker present"
887
+ assert_not_contains "reference_line_budget_blocking_ok" "$out" "no false ok next to a reference block"
888
+ assert_not_contains "entrypoint_size_budget_advisory_ok" "$out" "no legacy ok next to a reference failure"
889
+
890
+ # Case g1b: exactly at the budget is legal (the boundary is > 500, not >= 500).
891
+ write_reference_lines "$REF_REPO/skills/demo-skill/references/oversized.md" 500
892
+ run_ref
893
+ assert_rc "$rc" 0 "a reference exactly at the budget must land"
894
+ assert_contains "reference_line_budget_blocking_ok" "$out" "boundary case keeps the clean verdict"
895
+ rm -f "$REF_REPO/skills/demo-skill/references/oversized.md"
896
+
897
+ # Case g2: an existing within-limit reference that CROSSES the budget blocks on
898
+ # the same new-or-crossing branch.
899
+ write_reference_lines "$REF_REPO/skills/demo-skill/references/small.md" 640
900
+ run_ref
901
+ assert_rc "$rc" 1 "a reference crossing the budget must block"
902
+ assert_contains "reference_line_block: skills/demo-skill/references/small.md: new reference over line budget" "$out" "crossing block names the file"
903
+ git -C "$REF_REPO" checkout -- skills/demo-skill/references/small.md
904
+
905
+ # Case g3: a historical over-limit reference may SHRINK and may stay LEVEL.
906
+ write_reference_lines "$REF_REPO/skills/legacy-skill/references/legacy.md" 600
907
+ run_ref
908
+ assert_rc "$rc" 0 "shrinking a historical over-limit reference must land"
909
+ assert_contains "reference_line_budget_legacy_ok: skills/legacy-skill/references/legacy.md" "$out" "legacy allowance token emitted"
910
+ assert_contains "allowed_lines=620" "$out" "legacy allowance is frozen at the base measure"
911
+ assert_contains "delta_lines=-" "$out" "per-file reference delta is negative"
912
+
913
+ # Case g3b: a same-line-count content edit of a historical over-limit reference
914
+ # lands. A shrink leg alone cannot see a `>` turning into `>=`, which would block
915
+ # an author who only rewrites text inside an already-over-limit file. The shrink
916
+ # from the previous leg is restored first so this one measures a true level edit.
917
+ git -C "$REF_REPO" checkout -- skills/legacy-skill/references/legacy.md
918
+ python3 - "$REF_REPO/skills/legacy-skill/references/legacy.md" <<'PY'
919
+ import sys
920
+ p = sys.argv[1]
921
+ lines = open(p, encoding="utf-8").read().splitlines(True)
922
+ lines[5] = "rewritten content at the same line count\n"
923
+ open(p, "w", encoding="utf-8").writelines(lines)
924
+ PY
925
+ run_ref
926
+ assert_rc "$rc" 0 "a same-size content edit of a historical over-limit reference must land"
927
+ assert_contains "reference_line_budget_legacy_ok: skills/legacy-skill/references/legacy.md" "$out" "level edit keeps the legacy allowance"
928
+ assert_contains "delta_lines=+0" "$out" "the level edit really is level"
929
+ git -C "$REF_REPO" checkout -- skills/legacy-skill/references/legacy.md
930
+
931
+ # Case g4: the same historical reference may NOT grow.
932
+ write_reference_lines "$REF_REPO/skills/legacy-skill/references/legacy.md" 621
933
+ run_ref
934
+ assert_rc "$rc" 1 "growing a historical over-limit reference must block"
935
+ assert_contains "reference_line_block: skills/legacy-skill/references/legacy.md: over-limit reference grew" "$out" "growth block names the file"
936
+ assert_contains "base_lines=620" "$out" "growth block cites the frozen baseline"
937
+ assert_contains "head_lines=621" "$out" "growth block cites the head count"
938
+ git -C "$REF_REPO" checkout -- skills/legacy-skill/references/legacy.md
939
+
940
+ # Case g5: the append-only ledger is excluded by gate design — it may exceed the
941
+ # budget and may GROW (append-only rows) without blocking, and says so.
942
+ write_reference_lines "$REF_REPO/skills/demo-skill/references/source-register.md" 900
943
+ run_ref
944
+ assert_rc "$rc" 0 "the append-only ledger must not be line-capped"
945
+ assert_contains "reference_line_budget_ledger_excluded: skills/demo-skill/references/source-register.md" "$out" "ledger exclusion is reported, not silent"
946
+ assert_not_contains "reference_line_block: skills/demo-skill/references/source-register.md" "$out" "ledger growth never blocks"
947
+ git -C "$REF_REPO" add -A
948
+ git -C "$REF_REPO" commit -qm "land the ledger fixture"
949
+ write_reference_lines "$REF_REPO/skills/demo-skill/references/source-register.md" 1200
950
+ run_ref
951
+ assert_rc "$rc" 0 "appending rows to the ledger must stay landable"
952
+ assert_not_contains "reference_line_block" "$out" "ledger append is not a growth block"
953
+ git -C "$REF_REPO" checkout -- skills/demo-skill/references/source-register.md
954
+
955
+ # Case g6: a rename of a historical over-limit reference carries ONLY its own
956
+ # allowance, and move-plus-growth blocks.
957
+ git -C "$REF_REPO" mv skills/legacy-skill/references/legacy.md skills/legacy-skill/references/legacy-renamed.md
958
+ run_ref
959
+ assert_rc "$rc" 0 "renaming a historical over-limit reference without growth must pass"
960
+ assert_contains "reference_line_budget_move_ok: skills/legacy-skill/references/legacy-renamed.md" "$out" "rename carries its own historical allowance"
961
+ assert_contains "moved_from=skills/legacy-skill/references/legacy.md" "$out" "move diagnostic proves the rename branch ran"
962
+ write_reference_lines "$REF_REPO/skills/legacy-skill/references/legacy-renamed.md" 621
963
+ run_ref
964
+ assert_rc "$rc" 1 "rename plus growth must block"
965
+ assert_not_contains "reference_line_budget_move_ok" "$out" "move credit must not survive growth"
966
+ git -C "$REF_REPO" reset --hard -q HEAD
967
+
968
+ # Case g7: a changed over-budget reference with an unresolvable base fails
969
+ # closed — losing the baseline must never become a waiver.
970
+ write_reference_lines "$REF_REPO/skills/demo-skill/references/oversized.md" 700
971
+ set +e
972
+ out="$(env CCL_SKILL_BASE_REF=definitely-not-a-ref bash "$SIZE_SCRIPT" "$REF_REPO" 2>&1)"
973
+ rc=$?
974
+ set -e
975
+ assert_rc "$rc" 1 "over-budget reference with unknown base must fail closed"
976
+ assert_contains "reference_line_block_partial: skills/demo-skill/references/oversized.md: base unknown" "$out" "reference partial names the file"
977
+ assert_contains "reference_line_budget_blocking_failed" "$out" "reference partial emits the failed marker"
978
+ assert_not_contains "reference_line_budget_blocking_ok" "$out" "unknown baseline never earns a reference pass"
979
+ rm -f "$REF_REPO/skills/demo-skill/references/oversized.md"
980
+
981
+ # Case g8: committed-only reference growth with an unresolvable base and a clean
982
+ # tree evaluates nothing, so it must say un-evaluated rather than ok.
983
+ write_reference_lines "$REF_REPO/skills/demo-skill/references/oversized.md" 700
984
+ git -C "$REF_REPO" add -A
985
+ git -C "$REF_REPO" commit -qm "commit the reference growth so the tree is clean"
986
+ [ -z "$(git -C "$REF_REPO" status --porcelain)" ] || fail "g8 fixture: tree must be clean"
987
+ set +e
988
+ out="$(env CCL_SKILL_BASE_REF=definitely-not-a-ref bash "$SIZE_SCRIPT" "$REF_REPO" 2>&1)"
989
+ rc=$?
990
+ set -e
991
+ assert_rc "$rc" 0 "base-less clean tree keeps the advisory exit contract"
992
+ assert_contains "reference_line_budget_blocking_unevaluated" "$out" "un-evaluated reference gate says so"
993
+ assert_not_contains "reference_line_budget_blocking_ok" "$out" "a base-less run must never claim a reference pass"
994
+
995
+ # Case g9: a NEW reference over 100 lines with no ## structure draws the
996
+ # navigation advisory and does NOT block; a structured one stays quiet. The g8
997
+ # over-budget fixture is retired first so this leg measures only its own files.
998
+ git -C "$REF_REPO" rm -q skills/demo-skill/references/oversized.md
999
+ git -C "$REF_REPO" commit -qm "retire the over-budget reference fixture"
1000
+ write_reference_lines "$REF_REPO/skills/demo-skill/references/flat.md" 140 --no-headings
1001
+ run_ref
1002
+ assert_rc "$rc" 0 "the navigation advisory must never block"
1003
+ assert_contains "reference_nav_advisory: skills/demo-skill/references/flat.md" "$out" "unstructured long reference draws the advisory"
1004
+ assert_contains "reference_line_budget_blocking_ok" "$out" "advisory leaves the blocking verdict clean"
1005
+ write_reference_lines "$REF_REPO/skills/demo-skill/references/structured.md" 140
1006
+ run_ref
1007
+ assert_rc "$rc" 0 "a structured long reference stays clean"
1008
+ assert_not_contains "reference_nav_advisory: skills/demo-skill/references/structured.md" "$out" "sectioned reference draws no advisory"
1009
+ rm -f "$REF_REPO/skills/demo-skill/references/flat.md" "$REF_REPO/skills/demo-skill/references/structured.md"
1010
+
1011
+ # Case g9b: H3 headings are not section structure for this purpose — a long new
1012
+ # reference whose only headings are `###` still draws the navigation advisory.
1013
+ { printf -- '# Fixture\n\n### Detail only\n'; i=0; while [ "$i" -lt 140 ]; do printf -- 'line %s\n' "$i"; i=$((i + 1)); done; } > "$REF_REPO/skills/demo-skill/references/h3only.md"
1014
+ run_ref
1015
+ assert_rc "$rc" 0 "the H3-only advisory must never block"
1016
+ assert_contains "reference_nav_advisory: skills/demo-skill/references/h3only.md" "$out" "an H3-only long reference still needs ## sections"
1017
+ rm -f "$REF_REPO/skills/demo-skill/references/h3only.md"
1018
+
1019
+ # Case g10: line endings must not change the measure. A CR-only or CRLF file
1020
+ # measures like the LF file a reader sees, so a 501-line CR-delimited reference
1021
+ # cannot slip through as a single line, and its 500-line sibling still lands.
1022
+ python3 - "$REF_REPO/skills/demo-skill/references/cr-only.md" <<'PY'
1023
+ import sys
1024
+ open(sys.argv[1], "wb").write(b"\r".join(b"line %d" % i for i in range(501)) + b"\r")
1025
+ PY
1026
+ run_ref
1027
+ assert_rc "$rc" 1 "a CR-delimited over-budget reference must block"
1028
+ assert_contains "reference_line_block: skills/demo-skill/references/cr-only.md: new reference over line budget" "$out" "CR-only block names the file"
1029
+ assert_contains "head_lines=501" "$out" "CR-only lines are counted like LF lines"
1030
+ python3 - "$REF_REPO/skills/demo-skill/references/cr-only.md" <<'PY'
1031
+ import sys
1032
+ open(sys.argv[1], "wb").write(b"\r\n".join(b"line %d" % i for i in range(501)) + b"\r\n")
1033
+ PY
1034
+ run_ref
1035
+ assert_rc "$rc" 1 "a CRLF over-budget reference must block"
1036
+ assert_contains "head_lines=501" "$out" "CRLF lines are counted once, not twice"
1037
+ python3 - "$REF_REPO/skills/demo-skill/references/cr-only.md" <<'PY'
1038
+ import sys
1039
+ open(sys.argv[1], "wb").write(b"\r".join(b"line %d" % i for i in range(500)) + b"\r")
1040
+ PY
1041
+ run_ref
1042
+ assert_rc "$rc" 0 "a CR-delimited reference at the budget still lands"
1043
+ assert_contains "reference_line_budget_blocking_ok" "$out" "boundary holds under CR line endings"
1044
+ rm -f "$REF_REPO/skills/demo-skill/references/cr-only.md"
1045
+
1046
+ # Case g11: an unreadable reference degrades the CENSUS COUNTER to unknown and
1047
+ # says so, instead of silently counting it as within budget or aborting the whole
1048
+ # program before the per-file verdicts run. Running as root defeats chmod, so the
1049
+ # leg checks that the fixture is actually unreadable and prints an explicit skip
1050
+ # token otherwise — skipped is not passed.
1051
+ UNREADABLE="$REF_REPO/skills/demo-skill/references/unreadable.md"
1052
+ write_reference_lines "$UNREADABLE" 40
1053
+ git -C "$REF_REPO" add -A
1054
+ git -C "$REF_REPO" commit -qm "land a readable reference to make unreadable"
1055
+ chmod 000 "$UNREADABLE"
1056
+ if head -c 1 "$UNREADABLE" >/dev/null 2>&1; then
1057
+ echo "reference_census_unreadable_leg_skipped: this process can still read a chmod-000 file (likely root) — skipped is not passed"
1058
+ else
1059
+ run_ref
1060
+ assert_rc "$rc" 1 "an unreadable CHANGED reference fails closed"
1061
+ assert_contains "reference_line_census_partial" "$out" "the census says it is incomplete"
1062
+ assert_contains "reference_line_over_limit_count_head=unknown" "$out" "an incomplete census reports unknown, never a count"
1063
+ assert_contains "reference_line_over_limit_count_delta=unknown" "$out" "an unknown head count cannot produce a signed delta"
1064
+ assert_contains "reference_line_block_partial: skills/demo-skill/references/unreadable.md" "$out" "the per-file verdict still runs and names the file"
1065
+ assert_not_contains "reference_line_budget_blocking_ok" "$out" "an unreadable changed reference never earns a pass"
1066
+ fi
1067
+ chmod 644 "$UNREADABLE"
1068
+ git -C "$REF_REPO" rm -q "skills/demo-skill/references/unreadable.md"
1069
+ git -C "$REF_REPO" commit -qm "retire the unreadable fixture"
1070
+
820
1071
  # Case e8: gate wiring anchors — the validator must fail the gate on non-zero
821
1072
  # and must not demote the size gate back to advisory.
822
1073
  validator="$SCRIPT_DIR/check-ccl-skills.sh"
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env bash
2
+ # Self-proof suite for check-contract-anchors.sh.
3
+ #
4
+ # Oracle self-proof tables (the gate is trusted only because every row below
5
+ # was APPLIED and observed):
6
+ #
7
+ # MUST-HIT (mutant -> expected red token, expected rc):
8
+ # M1 pinned literal deleted from anchored file -> contract_anchor_missing rc 1
9
+ # M2 pinned literal duplicated in anchored file -> contract_anchor_duplicate rc 1
10
+ # M3 anchored file removed -> contract_anchor_file_missing rc 1
11
+ # M4 table emptied (comments only) -> contract_anchor_table_empty rc 2
12
+ # M5 malformed row (3 fields) -> contract_anchor_row_malformed rc 2
13
+ # M6 literal under 16 chars -> contract_anchor_literal_too_short rc 2
14
+ # M7 duplicate anchor id -> contract_anchor_duplicate_id rc 2
15
+ # M8 table file missing -> contract_anchor_table_missing rc 2
16
+ # M9 malformed row + drifted anchor in one table -> BOTH reported, rc 2
17
+ # (integrity errors are collected, well-formed rows still evaluated,
18
+ # and a broken table yields no verdict — fail-closed over the drift)
19
+ #
20
+ # MUST-NOT-HIT (benign neighbor -> stays green, rc 0):
21
+ # B1 wording adjacent to the pinned literal edited (same line, outside the pin)
22
+ # B2 unrelated file in the tree edited freely
23
+ # B3 anchored file grows new content elsewhere
24
+ #
25
+ # DIFFERENTIAL ATTRIBUTION:
26
+ # D1 two anchors, one broken -> output names exactly the broken id, rc 1,
27
+ # and the healthy id is not reported
28
+ #
29
+ # REAL-REPO LEG:
30
+ # R1 shipped table against the repo root -> rc 0 and pinned output grammar
31
+ # "contract_anchor_gate_ok (<N> anchors)"
32
+ set -u
33
+
34
+ script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
35
+ checker="$script_dir/check-contract-anchors.sh"
36
+ repo_root=$(cd "$script_dir/../../.." && pwd -P)
37
+ fail=0
38
+
39
+ note() { printf '%s\n' "$*"; }
40
+ expect() { # expect <case> <want_rc> <got_rc> <want_token> <output>
41
+ local case_id="$1" want_rc="$2" got_rc="$3" want_token="$4" output="$5"
42
+ if [[ "$got_rc" -ne "$want_rc" ]]; then
43
+ note "FAIL $case_id: rc=$got_rc want=$want_rc"
44
+ note "$output"
45
+ fail=1
46
+ return
47
+ fi
48
+ if [[ -n "$want_token" && "$output" != *"$want_token"* ]]; then
49
+ note "FAIL $case_id: missing token '$want_token' in output"
50
+ note "$output"
51
+ fail=1
52
+ return
53
+ fi
54
+ note "ok $case_id"
55
+ }
56
+
57
+ tmp=$(mktemp -d)
58
+ trap 'rm -rf "$tmp"' EXIT
59
+
60
+ make_fixture() { # rebuild a fresh fixture tree + table under $tmp/fx
61
+ rm -rf "$tmp/fx"
62
+ mkdir -p "$tmp/fx/docs"
63
+ cat > "$tmp/fx/docs/sample-contract.md" <<'MD'
64
+ # Sample contract
65
+
66
+ The sample verdict is decided by exactly one discriminating predicate here.
67
+ Unrelated surrounding prose that may change freely.
68
+ MD
69
+ cat > "$tmp/fx/docs/other.md" <<'MD'
70
+ Free-floating document with no anchors at all.
71
+ MD
72
+ printf '%s\t%s\t%s\t%s\n' \
73
+ "sample-discriminator" "docs/sample-contract.md" \
74
+ "decided by exactly one discriminating predicate" "fixture row" \
75
+ > "$tmp/fx/anchors.tsv"
76
+ }
77
+
78
+ run() { # run <root> <table>; sets got_rc/got_out
79
+ got_out=$(bash "$checker" "$1" --table "$2" 2>&1)
80
+ got_rc=$?
81
+ }
82
+
83
+ # R1: shipped table against the real repo
84
+ run "$repo_root" "$script_dir/contract-anchors.tsv"
85
+ expect R1 0 "$got_rc" "contract_anchor_gate_ok (" "$got_out"
86
+ if ! grep -qE '^contract_anchor_gate_ok \([0-9]+ anchors\)$' <<<"$got_out"; then
87
+ note "FAIL R1-grammar: green output grammar drifted: $got_out"
88
+ fail=1
89
+ else
90
+ note "ok R1-grammar"
91
+ fi
92
+
93
+ # Green fixture control (precondition for every mutant below)
94
+ make_fixture
95
+ run "$tmp/fx" "$tmp/fx/anchors.tsv"
96
+ expect C0 0 "$got_rc" "contract_anchor_gate_ok (1 anchors)" "$got_out"
97
+
98
+ # M1 literal deleted
99
+ make_fixture
100
+ perl -pi -e 's/decided by exactly one discriminating predicate/decided case by case/' "$tmp/fx/docs/sample-contract.md"
101
+ run "$tmp/fx" "$tmp/fx/anchors.tsv"
102
+ expect M1 1 "$got_rc" "contract_anchor_missing: sample-discriminator" "$got_out"
103
+
104
+ # M2 literal duplicated
105
+ make_fixture
106
+ printf '\nA second copy: decided by exactly one discriminating predicate.\n' >> "$tmp/fx/docs/sample-contract.md"
107
+ run "$tmp/fx" "$tmp/fx/anchors.tsv"
108
+ expect M2 1 "$got_rc" "contract_anchor_duplicate: sample-discriminator" "$got_out"
109
+
110
+ # M3 anchored file removed
111
+ make_fixture
112
+ rm "$tmp/fx/docs/sample-contract.md"
113
+ run "$tmp/fx" "$tmp/fx/anchors.tsv"
114
+ expect M3 1 "$got_rc" "contract_anchor_file_missing: sample-discriminator" "$got_out"
115
+
116
+ # M4 table emptied
117
+ make_fixture
118
+ printf '# only comments\n\n' > "$tmp/fx/anchors.tsv"
119
+ run "$tmp/fx" "$tmp/fx/anchors.tsv"
120
+ expect M4 2 "$got_rc" "contract_anchor_table_empty" "$got_out"
121
+
122
+ # M5 malformed row
123
+ make_fixture
124
+ printf 'bad-row\tdocs/sample-contract.md\tonly three fields\n' > "$tmp/fx/anchors.tsv"
125
+ run "$tmp/fx" "$tmp/fx/anchors.tsv"
126
+ expect M5 2 "$got_rc" "contract_anchor_row_malformed" "$got_out"
127
+
128
+ # M6 short literal
129
+ make_fixture
130
+ printf 'short\tdocs/sample-contract.md\ttiny pin\tnote\n' > "$tmp/fx/anchors.tsv"
131
+ run "$tmp/fx" "$tmp/fx/anchors.tsv"
132
+ expect M6 2 "$got_rc" "contract_anchor_literal_too_short" "$got_out"
133
+
134
+ # M7 duplicate id
135
+ make_fixture
136
+ printf '%s\t%s\t%s\t%s\n' "sample-discriminator" "docs/sample-contract.md" \
137
+ "Unrelated surrounding prose that may change" "second row same id" >> "$tmp/fx/anchors.tsv"
138
+ run "$tmp/fx" "$tmp/fx/anchors.tsv"
139
+ expect M7 2 "$got_rc" "contract_anchor_duplicate_id" "$got_out"
140
+
141
+ # M8 table missing
142
+ make_fixture
143
+ run "$tmp/fx" "$tmp/fx/missing.tsv"
144
+ expect M8 2 "$got_rc" "contract_anchor_table_missing" "$got_out"
145
+
146
+ # M9 malformed row + drifted anchor: both reported in one run, rc 2
147
+ make_fixture
148
+ printf 'bad-row\tdocs/sample-contract.md\tonly three fields\n' >> "$tmp/fx/anchors.tsv"
149
+ perl -pi -e 's/decided by exactly one discriminating predicate/decided informally/' "$tmp/fx/docs/sample-contract.md"
150
+ run "$tmp/fx" "$tmp/fx/anchors.tsv"
151
+ expect M9 2 "$got_rc" "contract_anchor_table_invalid" "$got_out"
152
+ if [[ "$got_out" != *"contract_anchor_row_malformed"* || "$got_out" != *"contract_anchor_missing: sample-discriminator"* ]]; then
153
+ note "FAIL M9-completeness: expected both the integrity error and the drift finding in one run"
154
+ note "$got_out"
155
+ fail=1
156
+ else
157
+ note "ok M9-completeness"
158
+ fi
159
+
160
+ # B1 benign neighbor: same line, outside the pinned span
161
+ make_fixture
162
+ perl -pi -e 's/predicate here\./predicate here, with a clarifying tail./' "$tmp/fx/docs/sample-contract.md"
163
+ run "$tmp/fx" "$tmp/fx/anchors.tsv"
164
+ expect B1 0 "$got_rc" "contract_anchor_gate_ok" "$got_out"
165
+
166
+ # B2 unrelated file edited
167
+ make_fixture
168
+ printf 'More free text.\n' >> "$tmp/fx/docs/other.md"
169
+ run "$tmp/fx" "$tmp/fx/anchors.tsv"
170
+ expect B2 0 "$got_rc" "contract_anchor_gate_ok" "$got_out"
171
+
172
+ # B3 anchored file grows elsewhere
173
+ make_fixture
174
+ printf '\n## New unrelated section\n\nNew prose.\n' >> "$tmp/fx/docs/sample-contract.md"
175
+ run "$tmp/fx" "$tmp/fx/anchors.tsv"
176
+ expect B3 0 "$got_rc" "contract_anchor_gate_ok" "$got_out"
177
+
178
+ # D1 differential attribution: two anchors, one broken
179
+ make_fixture
180
+ printf '%s\t%s\t%s\t%s\n' "healthy-anchor" "docs/sample-contract.md" \
181
+ "Unrelated surrounding prose that may change freely." "healthy row" >> "$tmp/fx/anchors.tsv"
182
+ perl -pi -e 's/decided by exactly one discriminating predicate/decided informally/' "$tmp/fx/docs/sample-contract.md"
183
+ run "$tmp/fx" "$tmp/fx/anchors.tsv"
184
+ expect D1 1 "$got_rc" "contract_anchor_missing: sample-discriminator" "$got_out"
185
+ if [[ "$got_out" == *"healthy-anchor"* ]]; then
186
+ note "FAIL D1-attribution: healthy anchor reported in failure output"
187
+ fail=1
188
+ else
189
+ note "ok D1-attribution"
190
+ fi
191
+
192
+ if [[ "$fail" -ne 0 ]]; then
193
+ echo "test_check_contract_anchors: FAIL"
194
+ exit 1
195
+ fi
196
+ echo "test_check_contract_anchors: ok"