@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.
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/SKILL.md +4 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/staged-review-contract.md +23 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/review_gate.py +178 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh +127 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/defect-diagnosis/SKILL.md +2 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/SKILL.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/SKILL.md +1 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/SKILL.md +11 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/async-lifecycle-and-performance.md +16 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/source-map.md +1 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/SKILL.md +8 -8
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/delivery-lifecycle.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/dispatch-owner-skills.md +9 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/problem-resolution-and-learning.md +2 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/SKILL.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/references/tag-and-prod-pipeline-gate.md +9 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +17 -20
- 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 +26 -44
- 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/external-practice-controls.md +16 -0
- 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 +3 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +59 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-to-skill-extraction.md +2 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/validation-and-landing.md +3 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +35 -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 +16 -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/impact-chain-gate.rb +35 -6
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/review_ledger_binding.py +476 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_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_impact_chain_refscripts.sh +81 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +28 -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_review_ledger_binding.sh +336 -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_routing_pointer_integrity.sh +41 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_extraction_review_state.sh +141 -28
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py +139 -38
- package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/SKILL.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/SKILL.md +9 -8
- package/dist/assets/release.json +110 -45
- package/package.json +1 -1
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Require the landing candidate to be the candidate an external round reviewed.
|
|
3
|
+
|
|
4
|
+
The dual-track ledger is caller-built and caller-run. Nothing at merge time read
|
|
5
|
+
it, so which candidate the rounds actually inspected was unchecked: a chain could
|
|
6
|
+
close out on the pre-fix candidate while a different tree merged. This gate closes
|
|
7
|
+
that gap from the merge side.
|
|
8
|
+
|
|
9
|
+
The reviewed identity is the packet the controller froze, so this recomputes that
|
|
10
|
+
packet with the controller's own `freeze_packet` rather than a second
|
|
11
|
+
implementation of the same bytes -- two implementations of one hash drift, and the
|
|
12
|
+
drift would read as a forged ledger. The bound set is every tracked path, minus
|
|
13
|
+
exactly what this round adds under a round's evidence directory. It is a default
|
|
14
|
+
of everything rather than a whitelist because a whitelist binds only the paths
|
|
15
|
+
some round happened to review: a pull request could carry a ledger valid for its
|
|
16
|
+
skill changes while also landing a root build script, a release script, or any
|
|
17
|
+
other executable path, and because that content does not move the candidate the
|
|
18
|
+
existing ledger still passed and the unreviewed content merged. Binding
|
|
19
|
+
everything makes the default fail-closed -- a new top-level path is bound the day
|
|
20
|
+
it appears rather than the day somebody remembers to add it. The cost is stated
|
|
21
|
+
rather than hidden: a change confined to documentation now needs a ledger too,
|
|
22
|
+
which is the direction this repository has already chosen for shared gates, where
|
|
23
|
+
a false positive is cheaper than a false negative.
|
|
24
|
+
|
|
25
|
+
The exclusion is computed per run (`added_evidence_paths`) rather than written
|
|
26
|
+
down as a subtree, and the difference is load-bearing. Something must be outside
|
|
27
|
+
the candidate or no ledger could ever be committed: a receipt inside the bound set
|
|
28
|
+
would move the very hash it records. But excluding all of `specs/` would exclude
|
|
29
|
+
far more than that -- a pull request could delete or rewrite an earlier round's
|
|
30
|
+
plan and receipts, the committed review history itself, and none of it would reach
|
|
31
|
+
the candidate, so the gate would pass while that history was corrupted. Only the
|
|
32
|
+
paths this round ADDS under a round's own `<round>/evidence/` directory are excluded. Every
|
|
33
|
+
modification and deletion under `specs/`, and every added path outside an evidence
|
|
34
|
+
directory, is bound like any other file. What remains outside is narrow and worth
|
|
35
|
+
naming: a file added under an EARLIER round's evidence directory is excluded too,
|
|
36
|
+
because the rule is structural rather than round-aware.
|
|
37
|
+
|
|
38
|
+
The candidate must also be committed (`require_committed_tree`). The frozen packet
|
|
39
|
+
is built from the working tree and includes untracked files, so a scratch file or
|
|
40
|
+
an unstaged edit inside the bound paths would silently produce a hash no clean
|
|
41
|
+
checkout recomputes -- the author records it in the ledger, and the merge-side run
|
|
42
|
+
then reports that nothing binds the landing candidate. Refusing out loud costs a
|
|
43
|
+
commit; the alternative costs a review round nobody can reproduce. The workflow
|
|
44
|
+
directory stays bound, as it was before: with only `skills/` bound, deleting the CI
|
|
45
|
+
step that runs this gate would not move the candidate the evidence has to match.
|
|
46
|
+
|
|
47
|
+
Boundaries this gate does NOT close, stated because a gate that lives inside the
|
|
48
|
+
candidate cannot authenticate itself: it cannot prove the caller retained every
|
|
49
|
+
earlier chain; it runs the candidate's own validator, so a candidate that also
|
|
50
|
+
rewrites that validator is outside what any in-repo check can settle; and a pull
|
|
51
|
+
request may edit the workflow step that runs it. The fail-closed branch is pinned
|
|
52
|
+
as a contract anchor so hollowing it also edits a registry another required check
|
|
53
|
+
verifies; the workflow step itself is NOT pinned, because that registry addresses
|
|
54
|
+
skill files and a cross-tree row breaks the checker against its own fixtures. The terminal authority is the platform's
|
|
55
|
+
required-check configuration plus human review of changes to this gate itself,
|
|
56
|
+
both of which live outside the candidate. What this proves is narrow and worth
|
|
57
|
+
stating plainly: that what merges is the candidate an external round inspected --
|
|
58
|
+
not that the round was honest, and not that it found nothing. Any terminal state
|
|
59
|
+
the validator accepts satisfies this gate, including one that carries unresolved
|
|
60
|
+
findings forward for a human: this binds identity, and the verdict on the findings
|
|
61
|
+
stays with the human who merges.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
from __future__ import annotations
|
|
65
|
+
|
|
66
|
+
import sys
|
|
67
|
+
|
|
68
|
+
# Importing the controller must not perturb the tree this gate hashes: a written
|
|
69
|
+
# __pycache__ lands as an untracked binary file inside the reviewed paths and the
|
|
70
|
+
# packet freeze then fails on it. Set before any import that can write bytecode.
|
|
71
|
+
sys.dont_write_bytecode = True
|
|
72
|
+
|
|
73
|
+
import argparse
|
|
74
|
+
import hashlib
|
|
75
|
+
import importlib.util
|
|
76
|
+
import json
|
|
77
|
+
import os
|
|
78
|
+
import re
|
|
79
|
+
import subprocess
|
|
80
|
+
import time
|
|
81
|
+
import types
|
|
82
|
+
from pathlib import Path
|
|
83
|
+
|
|
84
|
+
VALIDATOR = "validate_extraction_review_state.py"
|
|
85
|
+
CONTROLLER = Path("skills") / "code-review" / "scripts" / "review_gate.py"
|
|
86
|
+
# Every tracked path. See the module docstring: the inversion is what stops an
|
|
87
|
+
# unreviewed path from riding along on a valid ledger. The only exclusion is
|
|
88
|
+
# computed per run by `added_evidence_paths` -- the receipts this round adds --
|
|
89
|
+
# because a written-down subtree would also hide edits to committed history.
|
|
90
|
+
EVIDENCE_ROOT = "specs"
|
|
91
|
+
EVIDENCE_MEMBER = re.compile(r"^specs/[^/]+/evidence/")
|
|
92
|
+
# A receipt is small; anything larger is not one, and reading it is not free.
|
|
93
|
+
MAX_RECEIPT_BYTES = 4_000_000
|
|
94
|
+
DEFAULT_PATHS = (".",)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def emit(message: str) -> None:
|
|
98
|
+
print(message, file=sys.stderr)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def load_controller(repo_root: Path) -> types.ModuleType:
|
|
102
|
+
controller_path = repo_root / CONTROLLER
|
|
103
|
+
if not controller_path.is_file():
|
|
104
|
+
raise SystemExit(
|
|
105
|
+
f"review_ledger_binding_error: controller not found at {controller_path}"
|
|
106
|
+
)
|
|
107
|
+
spec = importlib.util.spec_from_file_location(
|
|
108
|
+
"ccl_review_gate_for_binding", controller_path
|
|
109
|
+
)
|
|
110
|
+
if spec is None or spec.loader is None:
|
|
111
|
+
raise SystemExit("review_ledger_binding_error: controller is not importable")
|
|
112
|
+
module = importlib.util.module_from_spec(spec)
|
|
113
|
+
spec.loader.exec_module(module)
|
|
114
|
+
return module
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def resolve_base(repo_root: Path, base: str) -> str:
|
|
118
|
+
"""Resolve the caller's base to a commit id before it reaches any git command.
|
|
119
|
+
|
|
120
|
+
An option-shaped base (``--quiet``) is read by git as an option rather than a
|
|
121
|
+
revision, and ``git diff`` with no revision compares the index to the working
|
|
122
|
+
tree: in a clean checkout that reports no paths at all, so the gate would pass
|
|
123
|
+
having compared nothing.
|
|
124
|
+
"""
|
|
125
|
+
result = subprocess.run(
|
|
126
|
+
[
|
|
127
|
+
"git",
|
|
128
|
+
"-C",
|
|
129
|
+
str(repo_root),
|
|
130
|
+
"rev-parse",
|
|
131
|
+
"--verify",
|
|
132
|
+
"--quiet",
|
|
133
|
+
"--end-of-options",
|
|
134
|
+
f"{base}^{{commit}}",
|
|
135
|
+
],
|
|
136
|
+
stdout=subprocess.PIPE,
|
|
137
|
+
stderr=subprocess.PIPE,
|
|
138
|
+
text=True,
|
|
139
|
+
check=False,
|
|
140
|
+
)
|
|
141
|
+
resolved = result.stdout.strip()
|
|
142
|
+
if result.returncode != 0 or len(resolved) != 40 or not all(
|
|
143
|
+
character in "0123456789abcdef" for character in resolved
|
|
144
|
+
):
|
|
145
|
+
raise SystemExit(
|
|
146
|
+
f"review_ledger_binding_error: base does not resolve to a commit: {base}"
|
|
147
|
+
)
|
|
148
|
+
return resolved
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def fork_point(repo_root: Path, base: str) -> str:
|
|
152
|
+
"""Compare against where this branch left the base, not the base's tip.
|
|
153
|
+
|
|
154
|
+
A candidate measured against the tip absorbs every unrelated change the base
|
|
155
|
+
branch gained meanwhile, so an advance on the target branch silently restates
|
|
156
|
+
what this branch is and voids evidence that is still correct. The fork point
|
|
157
|
+
is what the branch actually adds, and it does not move when someone else
|
|
158
|
+
merges.
|
|
159
|
+
"""
|
|
160
|
+
result = subprocess.run(
|
|
161
|
+
["git", "-C", str(repo_root), "merge-base", base, "HEAD"],
|
|
162
|
+
stdout=subprocess.PIPE,
|
|
163
|
+
stderr=subprocess.PIPE,
|
|
164
|
+
text=True,
|
|
165
|
+
check=False,
|
|
166
|
+
)
|
|
167
|
+
resolved = result.stdout.strip()
|
|
168
|
+
if result.returncode != 0 or len(resolved) != 40:
|
|
169
|
+
raise SystemExit(
|
|
170
|
+
f"review_ledger_binding_error: no fork point between HEAD and {base}"
|
|
171
|
+
)
|
|
172
|
+
return resolved
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def added_evidence_paths(repo_root: Path, base: str) -> list[str]:
|
|
176
|
+
"""Paths this round ADDS under a round's evidence directory.
|
|
177
|
+
|
|
178
|
+
These are the only paths the candidate may exclude, and the predicate is what
|
|
179
|
+
the file IS, not where it sits. Two earlier shapes of this exclusion were
|
|
180
|
+
each broken by an adversarial round, and both failures were the same one: the
|
|
181
|
+
rule named a location and the location stood in for "this is a receipt".
|
|
182
|
+
Excluding all of `specs/` let a pull request delete or rewrite an earlier
|
|
183
|
+
round's plan and receipts -- the committed review history itself -- with no
|
|
184
|
+
evidence required. Narrowing that to added paths under an evidence directory
|
|
185
|
+
then let an arbitrary added file there, a script included, ride through
|
|
186
|
+
unreviewed for exactly the same reason.
|
|
187
|
+
|
|
188
|
+
So the third shape stops using the path as a proxy. A file is excluded only
|
|
189
|
+
when it is what the exclusion exists for: a committed JSON object carrying
|
|
190
|
+
the 64-hex `candidate_sha256` that makes it a receipt about some candidate.
|
|
191
|
+
Its directory still has to be a round's evidence directory, because that is
|
|
192
|
+
where receipts belong, but the directory alone no longer buys exclusion.
|
|
193
|
+
Anything else added there -- a script, a fixture, a data file, a JSON file
|
|
194
|
+
with no candidate binding -- is bound like any other path, as is every
|
|
195
|
+
modification and deletion under `specs/`.
|
|
196
|
+
"""
|
|
197
|
+
result = subprocess.run(
|
|
198
|
+
[
|
|
199
|
+
"git", "-C", str(repo_root), "diff", "--name-only",
|
|
200
|
+
"--diff-filter=A", base, "HEAD", "--", EVIDENCE_ROOT,
|
|
201
|
+
],
|
|
202
|
+
stdout=subprocess.PIPE,
|
|
203
|
+
stderr=subprocess.PIPE,
|
|
204
|
+
text=True,
|
|
205
|
+
check=False,
|
|
206
|
+
)
|
|
207
|
+
if result.returncode != 0:
|
|
208
|
+
raise SystemExit(
|
|
209
|
+
"review_ledger_binding_error: cannot enumerate added evidence: "
|
|
210
|
+
f"{result.stderr.strip()}"
|
|
211
|
+
)
|
|
212
|
+
excluded: list[str] = []
|
|
213
|
+
for line in result.stdout.splitlines():
|
|
214
|
+
if not EVIDENCE_MEMBER.match(line):
|
|
215
|
+
continue
|
|
216
|
+
if is_candidate_receipt(repo_root, line):
|
|
217
|
+
excluded.append(line)
|
|
218
|
+
return excluded
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def is_candidate_receipt(repo_root: Path, path_value: str) -> bool:
|
|
222
|
+
"""Whether the committed blob at this path is a receipt about a candidate.
|
|
223
|
+
|
|
224
|
+
Read from the object store rather than the working tree: the exclusion has to
|
|
225
|
+
describe what merges. A blob that is not JSON, is not an object, or carries no
|
|
226
|
+
64-hex `candidate_sha256` is not a receipt, whatever it is named or wherever
|
|
227
|
+
it sits, and it stays inside the candidate.
|
|
228
|
+
"""
|
|
229
|
+
result = subprocess.run(
|
|
230
|
+
["git", "-C", str(repo_root), "cat-file", "blob", f"HEAD:{path_value}"],
|
|
231
|
+
stdout=subprocess.PIPE,
|
|
232
|
+
stderr=subprocess.PIPE,
|
|
233
|
+
check=False,
|
|
234
|
+
)
|
|
235
|
+
if result.returncode != 0 or len(result.stdout) > MAX_RECEIPT_BYTES:
|
|
236
|
+
return False
|
|
237
|
+
try:
|
|
238
|
+
payload = json.loads(result.stdout.decode("utf-8"))
|
|
239
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
240
|
+
return False
|
|
241
|
+
if not isinstance(payload, dict):
|
|
242
|
+
return False
|
|
243
|
+
binding = payload.get("candidate_sha256")
|
|
244
|
+
return (
|
|
245
|
+
isinstance(binding, str)
|
|
246
|
+
and len(binding) == 64
|
|
247
|
+
and all(character in "0123456789abcdef" for character in binding)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def require_committed_tree(repo_root: Path, paths: tuple[str, ...]) -> None:
|
|
252
|
+
"""Refuse a candidate the merge cannot reproduce.
|
|
253
|
+
|
|
254
|
+
The frozen packet is built from the working tree and includes untracked
|
|
255
|
+
files, so a scratch file or an unstaged edit inside the bound paths silently
|
|
256
|
+
produces a hash no clean checkout will ever recompute: the author records it
|
|
257
|
+
in the ledger and the merge-side run then reports that no evidence binds the
|
|
258
|
+
landing candidate. Refusing out loud costs a commit; the alternative costs a
|
|
259
|
+
review round nobody can reproduce. This is the same stance the evidence tree
|
|
260
|
+
already takes -- what merges is the committed tree.
|
|
261
|
+
"""
|
|
262
|
+
result = subprocess.run(
|
|
263
|
+
["git", "-C", str(repo_root), "status", "--porcelain", "--", *paths],
|
|
264
|
+
stdout=subprocess.PIPE,
|
|
265
|
+
stderr=subprocess.PIPE,
|
|
266
|
+
text=True,
|
|
267
|
+
check=False,
|
|
268
|
+
)
|
|
269
|
+
if result.returncode != 0:
|
|
270
|
+
raise SystemExit(
|
|
271
|
+
f"review_ledger_binding_error: cannot read tree state: {result.stderr.strip()}"
|
|
272
|
+
)
|
|
273
|
+
dirty = [line for line in result.stdout.splitlines() if line]
|
|
274
|
+
if dirty:
|
|
275
|
+
raise SystemExit(
|
|
276
|
+
"review_ledger_binding_error: the candidate tree carries uncommitted "
|
|
277
|
+
"changes, so its hash is not the one a clean checkout recomputes; "
|
|
278
|
+
"commit them first: " + ", ".join(entry[3:] for entry in dirty[:5])
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def changed_skill_paths(repo_root: Path, base: str, paths: tuple[str, ...]) -> list[str]:
|
|
283
|
+
result = subprocess.run(
|
|
284
|
+
["git", "-C", str(repo_root), "diff", "--name-only", base, "--", *paths],
|
|
285
|
+
stdout=subprocess.PIPE,
|
|
286
|
+
stderr=subprocess.PIPE,
|
|
287
|
+
text=True,
|
|
288
|
+
check=False,
|
|
289
|
+
)
|
|
290
|
+
if result.returncode != 0:
|
|
291
|
+
raise SystemExit(
|
|
292
|
+
f"review_ledger_binding_error: cannot diff against {base}: {result.stderr.strip()}"
|
|
293
|
+
)
|
|
294
|
+
return [line for line in result.stdout.splitlines() if line]
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def candidate_hash(module: types.ModuleType, repo_root: Path, base: str, paths: tuple[str, ...]) -> str:
|
|
298
|
+
args = argparse.Namespace(
|
|
299
|
+
cwd=str(repo_root),
|
|
300
|
+
diff_file=None,
|
|
301
|
+
base=base,
|
|
302
|
+
paths=list(paths),
|
|
303
|
+
wording_only_proof_file=None,
|
|
304
|
+
)
|
|
305
|
+
packet_path, packet_sha256, _paths, _secrets = module.freeze_packet(
|
|
306
|
+
args, time.monotonic() + 120
|
|
307
|
+
)
|
|
308
|
+
try:
|
|
309
|
+
Path(packet_path).unlink(missing_ok=True)
|
|
310
|
+
except OSError:
|
|
311
|
+
pass
|
|
312
|
+
return packet_sha256
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def validator_accepts(validator: Path, ledger_path: Path) -> tuple[bool, str]:
|
|
316
|
+
result = subprocess.run(
|
|
317
|
+
[sys.executable, str(validator), str(ledger_path)],
|
|
318
|
+
stdout=subprocess.PIPE,
|
|
319
|
+
stderr=subprocess.STDOUT,
|
|
320
|
+
text=True,
|
|
321
|
+
check=False,
|
|
322
|
+
timeout=60,
|
|
323
|
+
)
|
|
324
|
+
return result.returncode == 0, result.stdout.strip()
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def scan(repo_root: Path, evidence_root: str) -> list[tuple[Path, dict]]:
|
|
328
|
+
"""Enumerate committed evidence only.
|
|
329
|
+
|
|
330
|
+
What merges is the committed tree, so evidence that is untracked or modified
|
|
331
|
+
in the working tree is not evidence about the landing candidate -- and a gate
|
|
332
|
+
that reads it would accept a ledger nobody can find after the merge. The
|
|
333
|
+
enumeration comes from HEAD, and a dirty evidence tree is refused outright
|
|
334
|
+
rather than silently read from disk.
|
|
335
|
+
"""
|
|
336
|
+
listing = subprocess.run(
|
|
337
|
+
["git", "-C", str(repo_root), "ls-tree", "-r", "--name-only", "HEAD", "--", evidence_root],
|
|
338
|
+
stdout=subprocess.PIPE,
|
|
339
|
+
stderr=subprocess.PIPE,
|
|
340
|
+
text=True,
|
|
341
|
+
check=False,
|
|
342
|
+
)
|
|
343
|
+
if listing.returncode != 0:
|
|
344
|
+
return []
|
|
345
|
+
dirty = subprocess.run(
|
|
346
|
+
["git", "-C", str(repo_root), "status", "--porcelain", "--", evidence_root],
|
|
347
|
+
stdout=subprocess.PIPE,
|
|
348
|
+
stderr=subprocess.PIPE,
|
|
349
|
+
text=True,
|
|
350
|
+
check=False,
|
|
351
|
+
)
|
|
352
|
+
if dirty.returncode != 0 or dirty.stdout.strip():
|
|
353
|
+
raise SystemExit(
|
|
354
|
+
"review_ledger_binding_error: the evidence tree has uncommitted changes; "
|
|
355
|
+
"commit the ledger and its receipts before this gate can read them"
|
|
356
|
+
)
|
|
357
|
+
found: list[tuple[Path, dict]] = []
|
|
358
|
+
for relative in sorted(line for line in listing.stdout.splitlines() if line.endswith(".json")):
|
|
359
|
+
path = repo_root / relative
|
|
360
|
+
try:
|
|
361
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
362
|
+
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
363
|
+
continue
|
|
364
|
+
if isinstance(payload, dict):
|
|
365
|
+
found.append((path, payload))
|
|
366
|
+
return found
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def main() -> int:
|
|
370
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
371
|
+
parser.add_argument("--repo-root", default=".")
|
|
372
|
+
parser.add_argument("--base", default=None)
|
|
373
|
+
parser.add_argument(
|
|
374
|
+
"--allow-unevaluated",
|
|
375
|
+
action="store_true",
|
|
376
|
+
help="permit a run with no resolvable base to exit 0, for events that have none",
|
|
377
|
+
)
|
|
378
|
+
parser.add_argument("--evidence-root", default="specs")
|
|
379
|
+
parser.add_argument("--paths", nargs="*", default=list(DEFAULT_PATHS))
|
|
380
|
+
parser.add_argument(
|
|
381
|
+
"--print-candidate",
|
|
382
|
+
action="store_true",
|
|
383
|
+
help="print the candidate hash the evidence must bind, then exit",
|
|
384
|
+
)
|
|
385
|
+
args = parser.parse_args()
|
|
386
|
+
|
|
387
|
+
repo_root = Path(args.repo_root).resolve()
|
|
388
|
+
base = args.base or os.environ.get("CCL_SKILL_BASE_REF", "")
|
|
389
|
+
if not base:
|
|
390
|
+
# The gate is base-relative by construction, so a run with no base has
|
|
391
|
+
# checked nothing. Exiting 0 there is how a base-relative gate becomes
|
|
392
|
+
# decorative: a base-wiring mistake would read as a passing required
|
|
393
|
+
# check. Fail closed; a caller whose event genuinely has no base must
|
|
394
|
+
# say so out loud with --allow-unevaluated.
|
|
395
|
+
emit(
|
|
396
|
+
"review_ledger_binding_unevaluated: no base ref supplied "
|
|
397
|
+
"(pass --base or set CCL_SKILL_BASE_REF); nothing was checked"
|
|
398
|
+
)
|
|
399
|
+
return 0 if args.allow_unevaluated else 2
|
|
400
|
+
|
|
401
|
+
base = fork_point(repo_root, resolve_base(repo_root, base))
|
|
402
|
+
# The exclusion is derived from this round's own diff, not written down as a
|
|
403
|
+
# subtree, so edits to committed history stay inside the candidate.
|
|
404
|
+
# These names come from the candidate's own tree and are handed back to git as
|
|
405
|
+
# pathspecs, so `literal` stops git reading a filename as a pattern. A review
|
|
406
|
+
# round called this a total bypass -- a receipt named `*` excluding everything
|
|
407
|
+
# -- and that did not reproduce: the exclusion carries the full path, so a glob
|
|
408
|
+
# in the filename expands only within that one evidence directory, whose other
|
|
409
|
+
# members are receipts anyway. The claim is recorded as narrowed rather than
|
|
410
|
+
# confirmed, and no test asserts a bypass this gate does not have. `literal`
|
|
411
|
+
# stays because interpreting these names as patterns is a capability the gate
|
|
412
|
+
# never needed, and removing it costs nothing.
|
|
413
|
+
paths = tuple(args.paths) + tuple(
|
|
414
|
+
f":(exclude,literal){path}" for path in added_evidence_paths(repo_root, base)
|
|
415
|
+
)
|
|
416
|
+
require_committed_tree(repo_root, paths)
|
|
417
|
+
changed = changed_skill_paths(repo_root, base, paths)
|
|
418
|
+
if not changed:
|
|
419
|
+
# No reviewed path moved, so there is no candidate to freeze and nothing to
|
|
420
|
+
# bind. Say which it is rather than letting an empty packet surface as a
|
|
421
|
+
# freeze error, which reads like a broken gate.
|
|
422
|
+
if args.print_candidate:
|
|
423
|
+
emit(f"review_ledger_binding_no_change: no reviewed-path change against {base}")
|
|
424
|
+
else:
|
|
425
|
+
print(f"review_ledger_binding_ok: no reviewed-path change against {base}")
|
|
426
|
+
return 0
|
|
427
|
+
|
|
428
|
+
module = load_controller(repo_root)
|
|
429
|
+
try:
|
|
430
|
+
expected = candidate_hash(module, repo_root, base, paths)
|
|
431
|
+
except Exception as exc: # noqa: BLE001 - surface the controller's own message
|
|
432
|
+
emit(f"review_ledger_binding_error: cannot freeze the candidate packet: {exc}")
|
|
433
|
+
return 1
|
|
434
|
+
|
|
435
|
+
if args.print_candidate:
|
|
436
|
+
print(expected)
|
|
437
|
+
return 0
|
|
438
|
+
|
|
439
|
+
validator = repo_root / "skills" / "skill-extraction-workflow" / "scripts" / VALIDATOR
|
|
440
|
+
ledgers: list[str] = []
|
|
441
|
+
for path, payload in scan(repo_root, args.evidence_root):
|
|
442
|
+
if payload.get("candidate_sha256") != expected:
|
|
443
|
+
continue
|
|
444
|
+
relative = str(path.relative_to(repo_root))
|
|
445
|
+
# Only a validator-accepted ledger counts. A receipt-shaped file proves
|
|
446
|
+
# nothing on its own: this gate cannot authenticate that a controller
|
|
447
|
+
# minted it, so any branch keyed on a self-declared field is a bypass a
|
|
448
|
+
# contributor can hand-write.
|
|
449
|
+
if "closeout_state" in payload and "controller_receipts" in payload:
|
|
450
|
+
accepted, output = validator_accepts(validator, path)
|
|
451
|
+
if accepted:
|
|
452
|
+
print(
|
|
453
|
+
f"review_ledger_binding_ok: {relative} binds the landing candidate "
|
|
454
|
+
f"({expected[:12]}...) -- {output}"
|
|
455
|
+
)
|
|
456
|
+
return 0
|
|
457
|
+
ledgers.append(f"{relative}: {output}")
|
|
458
|
+
|
|
459
|
+
emit(
|
|
460
|
+
"review_ledger_binding_failed: no accepted review evidence binds the landing "
|
|
461
|
+
f"candidate {expected}"
|
|
462
|
+
)
|
|
463
|
+
emit(f" reviewed paths: {' '.join(paths)} against {base}")
|
|
464
|
+
emit(f" changed files: {len(changed)}")
|
|
465
|
+
for row in ledgers:
|
|
466
|
+
emit(f" rejected ledger -> {row}")
|
|
467
|
+
if not ledgers:
|
|
468
|
+
emit(
|
|
469
|
+
" no committed ledger records this candidate; run the extraction review "
|
|
470
|
+
"lane against the final, committed tree"
|
|
471
|
+
)
|
|
472
|
+
return 1
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
if __name__ == "__main__":
|
|
476
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Structural completeness check for the recurring-anti-patterns grep panel.
|
|
3
|
+
#
|
|
4
|
+
# Invariant pinned (074): every `## Anti-pattern N — ...` section in
|
|
5
|
+
# references/recurring-anti-patterns-checklist.md carries a `**Grep**` line —
|
|
6
|
+
# the runnable-or-manual detection recipe the panel's "How to use" contract
|
|
7
|
+
# promises per entry. A new anti-pattern landed without its Grep recipe is the
|
|
8
|
+
# drift this catches; the panel itself stays manual-by-design (its own
|
|
9
|
+
# "Promoting a symptom to a mechanical gate" growth rule), so this test does
|
|
10
|
+
# NOT execute or compile the grep patterns. Pattern-compilation validation was
|
|
11
|
+
# considered and discarded: the recipes mix GNU-BRE commands with prose
|
|
12
|
+
# instructions by design, so a compile check would false-red on platform
|
|
13
|
+
# regex-dialect differences without protecting a real contract.
|
|
14
|
+
#
|
|
15
|
+
# Self-proof (mutant must-hit): a fixture copy with one Grep line removed must
|
|
16
|
+
# turn this check red for exactly that section; a fixture with an extra
|
|
17
|
+
# non-anti-pattern section stays green (benign neighbor).
|
|
18
|
+
set -u
|
|
19
|
+
|
|
20
|
+
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
|
|
21
|
+
checklist="$script_dir/../references/recurring-anti-patterns-checklist.md"
|
|
22
|
+
fail=0
|
|
23
|
+
|
|
24
|
+
check_panel() { # check_panel <file>; prints missing sections, returns 1 if any
|
|
25
|
+
awk '
|
|
26
|
+
/^## Anti-pattern / {
|
|
27
|
+
if (in_section && !seen_grep) { print "missing_grep_line: " section; bad = 1 }
|
|
28
|
+
in_section = 1; seen_grep = 0; section = $0; count += 1; next
|
|
29
|
+
}
|
|
30
|
+
/^## / {
|
|
31
|
+
if (in_section && !seen_grep) { print "missing_grep_line: " section; bad = 1 }
|
|
32
|
+
in_section = 0; next
|
|
33
|
+
}
|
|
34
|
+
/^\*\*Grep\*\*/ { if (in_section) seen_grep = 1 }
|
|
35
|
+
END {
|
|
36
|
+
if (in_section && !seen_grep) { print "missing_grep_line: " section; bad = 1 }
|
|
37
|
+
if (count == 0) { print "no_anti_pattern_sections_found"; bad = 1 }
|
|
38
|
+
exit bad
|
|
39
|
+
}
|
|
40
|
+
' "$1"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if [[ ! -f "$checklist" ]]; then
|
|
44
|
+
echo "test_antipattern_grep_panel: FAIL (checklist missing: $checklist)"
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Real-panel leg
|
|
49
|
+
if ! out=$(check_panel "$checklist"); then
|
|
50
|
+
echo "$out"
|
|
51
|
+
echo "test_antipattern_grep_panel: FAIL (panel section without a Grep recipe)"
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
echo "ok panel ($(grep -c '^## Anti-pattern ' "$checklist") sections, each with a Grep recipe)"
|
|
55
|
+
|
|
56
|
+
tmp=$(mktemp -d)
|
|
57
|
+
trap 'rm -rf "$tmp"' EXIT
|
|
58
|
+
|
|
59
|
+
# Mutant must-hit: strip the Grep line from Anti-pattern 27 -> must red on it
|
|
60
|
+
awk '/^## Anti-pattern 27 /{inap=1} inap && /^\*\*Grep\*\*/{inap=0; next} {print}' \
|
|
61
|
+
"$checklist" > "$tmp/mutant.md"
|
|
62
|
+
if out=$(check_panel "$tmp/mutant.md"); then
|
|
63
|
+
echo "test_antipattern_grep_panel: FAIL (mutant with stripped Grep line passed)"
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|
|
66
|
+
if [[ "$out" != *"Anti-pattern 27"* ]]; then
|
|
67
|
+
echo "test_antipattern_grep_panel: FAIL (mutant red but wrong section: $out)"
|
|
68
|
+
exit 1
|
|
69
|
+
fi
|
|
70
|
+
echo "ok mutant (stripped Grep line detected on the right section)"
|
|
71
|
+
|
|
72
|
+
# Benign neighbor: an extra non-anti-pattern section must stay green
|
|
73
|
+
{ cat "$checklist"; printf '\n## A closing note\n\nProse only.\n'; } > "$tmp/benign.md"
|
|
74
|
+
if ! check_panel "$tmp/benign.md" >/dev/null; then
|
|
75
|
+
echo "test_antipattern_grep_panel: FAIL (benign extra section turned the check red)"
|
|
76
|
+
exit 1
|
|
77
|
+
fi
|
|
78
|
+
echo "ok benign (non-anti-pattern section ignored)"
|
|
79
|
+
|
|
80
|
+
echo "test_antipattern_grep_panel: ok"
|
|
@@ -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"
|