@ccoalm/ccl-skills 0.8.0 → 0.10.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 (78) hide show
  1. package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/references/mobile-quality-release.md +5 -0
  2. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/manual-invocation-and-prompts.md +6 -0
  3. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/staged-review-contract.md +5 -0
  4. package/dist/assets/marketplace/plugins/ccl-skills/skills/defect-diagnosis/SKILL.md +1 -0
  5. package/dist/assets/marketplace/plugins/ccl-skills/skills/feature-risk-router/SKILL.md +3 -1
  6. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/architecture-playbook.md +1 -1
  7. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/multi-tenant-isolation.md +1 -1
  8. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/state-machine-task-patterns.md +2 -0
  9. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/inference-capacity-operations.md +24 -0
  10. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/llm-client-gateway.md +1 -1
  11. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/model-prompt-evaluation.md +4 -1
  12. package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/references/contracts-and-state.md +5 -0
  13. package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/async-lifecycle-and-performance.md +1 -0
  14. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/SKILL.md +3 -2
  15. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/metrics-conventions.md +8 -1
  16. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/sli-slo-design.md +2 -2
  17. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/canary-and-rollout-strategy.md +16 -2
  18. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/promotion-gate-and-review.md +9 -0
  19. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/SKILL.md +12 -12
  20. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/code-review-checklist.md +4 -0
  21. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/delivery-lifecycle.md +1 -1
  22. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/rd-standards-doc-family-checklist.md +1 -0
  23. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-system-source-of-truth.md +2 -0
  24. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/platform-mobile-patterns.md +2 -2
  25. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/tokens-and-components.md +1 -0
  26. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/ui-ux-audit.md +8 -0
  27. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/multi-tenant-isolation.md +1 -1
  28. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/state-machine-task-patterns.md +2 -0
  29. package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/SKILL.md +1 -1
  30. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +12 -15
  31. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/attention-budget-ratchet.md +37 -0
  32. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/description-authoring.md +13 -0
  33. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +37 -30
  34. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/eval-routing.md +24 -3
  35. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-quickstart.md +5 -5
  36. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/rule-consolidation.md +1 -1
  37. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +81 -0
  38. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-to-skill-extraction.md +12 -0
  39. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/validation-and-landing.md +1 -1
  40. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +30 -0
  41. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-contract-anchors.sh +126 -0
  42. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-size-budget.sh +197 -1
  43. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/contract-anchors.tsv +15 -0
  44. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/eval-routing-bank.rb +210 -36
  45. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/extraction_review_gate.sh +3 -3
  46. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/gate_receipt.py +576 -0
  47. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_antipattern_grep_panel.sh +80 -0
  48. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_body_compliance_grading.sh +99 -0
  49. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +25 -0
  50. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_size_budget.sh +251 -0
  51. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_contract_anchors.sh +196 -0
  52. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_eval_routing_bank_grader_diagnostics.sh +222 -0
  53. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_extraction_review_gate.sh +16 -10
  54. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_frozen_case_sanctity.sh +178 -0
  55. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_frozen_case_sanctity_selfproof.sh +108 -0
  56. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_gate_receipt.sh +431 -0
  57. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_pinned_phrase_mutation_walk.sh +151 -0
  58. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_routing_bank_integrity.sh +86 -5
  59. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_extraction_review_state.sh +27 -21
  60. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py +25 -15
  61. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/classical-test-design-techniques.md +1 -1
  62. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/tc-review-and-prioritization.md +1 -1
  63. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/update-lifecycle.md +2 -0
  64. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/SKILL.md +9 -9
  65. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/ci-fixtures-and-flake-control.md +5 -1
  66. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/e2e-real-flow-testing.md +2 -2
  67. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/integration-contract-testing.md +10 -0
  68. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/test-code-authoring-patterns.md +2 -2
  69. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/test-topology-and-commands.md +1 -1
  70. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/SKILL.md +2 -1
  71. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/annotation-driven-revision.md +9 -0
  72. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/figure-and-table-craft.md +8 -2
  73. package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/SKILL.md +1 -0
  74. package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/references/react-architecture.md +3 -0
  75. package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/references/web-quality-release.md +37 -4
  76. package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/references/web-ui-quality.md +10 -1
  77. package/dist/assets/release.json +127 -67
  78. package/package.json +1 -1
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env bash
2
+ # Deterministic contract tests for eval/body-compliance-eval.rb — no live model.
3
+ #
4
+ # Covers the grading legs a live advisory run cannot regress-guard:
5
+ # G1 required-only verdict passes; G2 forbidden marker fails with forbidden_hit;
6
+ # G3 a marker immediately closed by a backtick/quote is a mention, not a verdict;
7
+ # G4 leading markdown decoration is accepted; G5 a mid-sentence prose marker
8
+ # does not count; G6 the differential holds in both directions (a continue
9
+ # probe fails on a blocked verdict).
10
+ # C1 unknown --ids, C2 empty --ids, C3 comma-only --ids, C4 missing repo root
11
+ # all exit 2 (fail-closed, never a silent 0/0 green).
12
+ # E1/E2 end-to-end with a stub `claude` on PATH: denominators scope to the
13
+ # selected subset (1/1 pass; 0/1 with forbidden_hit reported), exit stays 0
14
+ # because the lane is advisory by construction.
15
+ # Bash 3.2-safe. Runs against the real repo tree read-only plus a tmp stub dir.
16
+ set -u
17
+
18
+ script_dir="$(cd "$(dirname "$0")" && pwd -P)"
19
+ repo_root="$(cd "$script_dir/../../.." && pwd -P)"
20
+ runner="$repo_root/eval/body-compliance-eval.rb"
21
+ fails=0
22
+
23
+ fail() { echo "test_body_compliance_grading FAIL: $1" >&2; fails=$((fails + 1)); }
24
+
25
+ [ -f "$runner" ] || { echo "test_body_compliance_grading FAIL: runner missing at $runner" >&2; exit 1; }
26
+
27
+ # G1-G6: grade() unit walk through the library require path.
28
+ if ! ruby -e '
29
+ require_relative ARGV[0]
30
+ stop = PROBES.find { |p| p[:id] == "prd-stop-materially" } or abort "stop probe missing"
31
+ cont = PROBES.find { |p| p[:id] == "prd-continue-evidenced" } or abort "continue probe missing"
32
+ checks = []
33
+ checks << ["G1", grade(stop, "blocked: 两方案无占优,停给用户")[:status] == "PASS"]
34
+ g2 = grade(stop, "blocked: x\ncontinuing: y")
35
+ checks << ["G2", g2[:status] == "FAIL" && !g2[:forbidden_hit].empty?]
36
+ checks << ["G3", grade(stop, "`blocked:` is the label I would use")[:status] == "FAIL"]
37
+ checks << ["G4", grade(stop, "**blocked:** 加粗裁决")[:status] == "PASS"]
38
+ checks << ["G4b", grade(stop, "`blocked: 整行反引号裁决`")[:status] == "PASS"]
39
+ checks << ["G5", grade(stop, "他说 blocked: 不该出现在这里")[:status] == "FAIL"]
40
+ g6 = grade(cont, "blocked: 反向裁决")
41
+ checks << ["G6", g6[:status] == "FAIL" && !g6[:forbidden_hit].empty?]
42
+ bad = checks.reject { |_, ok| ok }
43
+ abort("grade walk failed: #{bad.map(&:first).join(",")}") unless bad.empty?
44
+ puts "grade walk ok (#{checks.length} cases)"
45
+ ' "$runner"; then
46
+ fail "grade() unit walk"
47
+ fi
48
+
49
+ # C1-C4: fail-closed CLI legs (no model involved; the runner must exit 2
50
+ # before any probe would run).
51
+ ruby "$runner" "$repo_root" --ids no-such-probe >/dev/null 2>&1
52
+ [ $? -eq 2 ] || fail "unknown --ids did not exit 2"
53
+ ruby "$runner" "$repo_root" --ids '' >/dev/null 2>&1
54
+ [ $? -eq 2 ] || fail "empty --ids did not exit 2"
55
+ ruby "$runner" "$repo_root" --ids ',' >/dev/null 2>&1
56
+ [ $? -eq 2 ] || fail "comma-only --ids did not exit 2"
57
+ ruby "$runner" >/dev/null 2>&1
58
+ [ $? -eq 2 ] || fail "missing repo root did not exit 2"
59
+ ruby "$runner" "$repo_root" --ids >/dev/null 2>&1
60
+ [ $? -eq 2 ] || fail "bare --ids (no value) did not exit 2"
61
+ ruby "$runner" "$repo_root" --ids --timeout >/dev/null 2>&1
62
+ [ $? -eq 2 ] || fail "--ids followed by another flag did not exit 2"
63
+
64
+ # E1/E2: end-to-end with a stub claude — proves subset denominators and the
65
+ # forbidden_hit report line without a live model.
66
+ stub_dir="$(mktemp -d "${TMPDIR:-/tmp}/bodycomp.XXXXXX")" || { fail "mktemp"; echo "test_body_compliance_grading: $fails failure(s)"; exit 1; }
67
+ trap 'rm -rf "$stub_dir"' EXIT
68
+ cat > "$stub_dir/claude" <<'STUB'
69
+ #!/bin/sh
70
+ cat > /dev/null
71
+ printf '%s\n' "$BODY_COMPLIANCE_STUB_LINE"
72
+ STUB
73
+ chmod +x "$stub_dir/claude"
74
+
75
+ e1_out="$(BODY_COMPLIANCE_STUB_LINE='continuing: 桩裁决' PATH="$stub_dir:$PATH" ruby "$runner" "$repo_root" --ids prd-continue-evidenced --timeout 30 2>&1)"
76
+ e1_rc=$?
77
+ case "$e1_out" in
78
+ *"1/1 pass"*) : ;;
79
+ *) fail "E1 expected 1/1 pass, got: $e1_out" ;;
80
+ esac
81
+ [ "$e1_rc" -eq 0 ] || fail "E1 advisory run exited $e1_rc"
82
+
83
+ e2_out="$(BODY_COMPLIANCE_STUB_LINE='blocked: 桩裁决' PATH="$stub_dir:$PATH" ruby "$runner" "$repo_root" --ids prd-continue-evidenced --timeout 30 2>&1)"
84
+ e2_rc=$?
85
+ case "$e2_out" in
86
+ *"0/1 pass, 1 fail"*) : ;;
87
+ *) fail "E2 expected 0/1 pass, 1 fail, got: $e2_out" ;;
88
+ esac
89
+ case "$e2_out" in
90
+ *forbidden_hit=*) : ;;
91
+ *) fail "E2 expected a forbidden_hit report line, got: $e2_out" ;;
92
+ esac
93
+ [ "$e2_rc" -eq 0 ] || fail "E2 advisory run exited $e2_rc"
94
+
95
+ if [ "$fails" -gt 0 ]; then
96
+ echo "test_body_compliance_grading: $fails failure(s)" >&2
97
+ exit 1
98
+ fi
99
+ echo "test_body_compliance_grading_ok"
@@ -114,6 +114,9 @@ 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
+ # Candidate-SHA-bound gate receipts (mint/verify): own throwaway git repo,
118
+ # no clone, seconds — belongs in the lane every run exercises.
119
+ test_gate_receipt.sh
117
120
  test_check_ccl_route_drift.sh
118
121
  test_check_sync_pointers.sh
119
122
  test_check_ccl_register_pending_exclusion.sh
@@ -137,6 +140,13 @@ fast_tests=(
137
140
  test_validate_skill_cross_refs.sh
138
141
  test_git_identity_predicate_gate.sh
139
142
  test_liveness_predicate_gate.sh
143
+ # 074: contract-anchor gate self-proof (tmp fixtures + one real-repo leg,
144
+ # seconds) and the anti-patterns panel structural check (awk over one file).
145
+ test_check_contract_anchors.sh
146
+ test_antipattern_grep_panel.sh
147
+ # 075: body-compliance grading contract (grade() walk, fail-closed --ids legs,
148
+ # stub-model denominator/forbidden_hit legs; deterministic, seconds).
149
+ test_body_compliance_grading.sh
140
150
  test_regression_runner_registration.sh
141
151
  # Harness binding for the gate lanes themselves: the impact-chain gate's
142
152
  # round walk needs the branch's own first-parent chain, which the default
@@ -149,6 +159,14 @@ fast_tests=(
149
159
  test_regression_runner_lanes.sh
150
160
  test_routing_pointer_integrity.sh
151
161
  test_routing_bank_integrity.sh
162
+ # 077: frozen-case sanctity (regressions-are-sacred) — base-relative guard
163
+ # that a deleted/re-scoped bank case or golden trace names its adjudication
164
+ # row; degrades to an explicit skip token without CCL_SKILL_BASE_REF. The
165
+ # selfproof lane proves the oracle fails for the right reason (synthetic
166
+ # repo: deletion/re-scope/nested-trace mutants red, forged-credit legs red,
167
+ # adjudicated and control legs green).
168
+ test_frozen_case_sanctity.sh
169
+ test_frozen_case_sanctity_selfproof.sh
152
170
  test_uiux_delivery_contract.sh
153
171
  test_uiux_loading_budget.sh
154
172
  test_governing_chain_diff.sh
@@ -195,6 +213,13 @@ heavy_tests=(
195
213
  # 「跳过义务会不会留下可检出的缺席」——044 连续五轮 challenge 都停在这一点上,负向
196
214
  # 用例(缺证据、非枚举值、降范围无留痕)必须每轮都跑,否则触发器会悄悄退化成声称。
197
215
  test_impact_chain_self_adjudication.sh
216
+ # 074: one applied deletion mutation per pinned-phrase gate family, each leg
217
+ # running the FULL shipped checker against a committed fixture copy of the
218
+ # working tree (6 full-gate runs — far too slow for pre-commit). Proves the
219
+ # ~40 required_phrase pins and the contract-anchor delegation can actually
220
+ # go red for the right reason; the fast lane keeps the checker-level
221
+ # self-proof in test_check_contract_anchors.sh.
222
+ test_pinned_phrase_mutation_walk.sh
198
223
  )
199
224
 
200
225
  # 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"