@ccoalm/ccl-skills 0.9.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.
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/staged-review-contract.md +5 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/SKILL.md +7 -7
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +8 -11
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/attention-budget-ratchet.md +37 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/description-authoring.md +9 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +29 -31
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/eval-routing.md +24 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-quickstart.md +5 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/rule-consolidation.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +32 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/validation-and-landing.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +30 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-contract-anchors.sh +126 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-size-budget.sh +197 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/contract-anchors.tsv +15 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/eval-routing-bank.rb +210 -36
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/extraction_review_gate.sh +3 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/gate_receipt.py +576 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_antipattern_grep_panel.sh +80 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_body_compliance_grading.sh +99 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +25 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_size_budget.sh +251 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_contract_anchors.sh +196 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_eval_routing_bank_grader_diagnostics.sh +222 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_extraction_review_gate.sh +16 -10
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_frozen_case_sanctity.sh +178 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_frozen_case_sanctity_selfproof.sh +108 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_gate_receipt.sh +431 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_pinned_phrase_mutation_walk.sh +151 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_routing_bank_integrity.sh +86 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_extraction_review_state.sh +27 -21
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py +25 -15
- package/dist/assets/release.json +79 -24
- 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"
|