test(14-06): check-phase14.sh --evidence refuses an incomplete phase record

- every T-14 threat has one review row copying its plan's severity and
  disposition; a mitigated one names a test --named runs and a removal row
- the validation map is validated, Nyquist-compliant and Wave 0 complete,
  with no open row and only tests --named runs
- each COVERAGE.md INTEGRATE row names a run test, each OPT-OUT a reason
- REQUIREMENTS.md keeps no SDK wording for INTG-02 and no sitemap output
  for API-08; the ROADMAP Phase 14 repos and criteria name the album
  Discogs and recognize routes and both shared plugin repos
- --all runs it after the coverage stage
This commit is contained in:
Jakub Zych
2026-10-04 02:11:39 +02:00
parent 81543524be
commit a1fccd39ad

View File

@@ -49,6 +49,7 @@ usage:
check-phase14.sh --named
check-phase14.sh --removal
check-phase14.sh --coverage
check-phase14.sh --evidence
check-phase14.sh --all
EOF
exit 2
@@ -722,6 +723,162 @@ removal_harness_in() {
)
}
# evidence_check PHASE_DIR REVIEW VALIDATION COVERAGE REQUIREMENTS ROADMAP
# NAMED: every T-14 threat the plans declare has exactly one review row
# copying its strictest severity and disposition; a mitigated threat names
# a test the --named stage runs (or a gate stage) and has a removal row; the
# validation file is validated, Nyquist-compliant, Wave 0 complete, without
# a pending or TBD row, names the seven requirements, and every test it
# names is run by --named; every COVERAGE.md INTEGRATE row names a test the
# --named stage runs and every OPT-OUT row gives a reason; REQUIREMENTS.md
# no longer carries the SDK wording for INTG-02 or the sitemap output for
# API-08 (D-06, D-14); the ROADMAP Phase 14 repos line and success criteria
# name the album Discogs and recognize routes and both shared plugin repos
# (D-07, D-13).
evidence_check() {
python3 - "$@" <<'PY'
import glob, os, re, sys
phase_dir, review_path, validation_path, coverage_path, req_path, roadmap_path, named = sys.argv[1:8]
named = set(named.split())
for p in (review_path, validation_path, coverage_path, req_path, roadmap_path):
if not os.path.isfile(p):
print(f"refuse: {p} is missing", file=sys.stderr)
sys.exit(1)
review = open(review_path).read()
validation = open(validation_path).read()
coverage = open(coverage_path).read()
requirements = open(req_path).read()
roadmap = open(roadmap_path).read()
test_re = re.compile(r"\b(?:Test|Fuzz)[A-Z][A-Za-z0-9_]*")
# Threats.
sev_rank = {"low": 0, "medium": 1, "high": 2}
declared = {}
for plan in sorted(glob.glob(os.path.join(phase_dir, "14-0*-PLAN.md"))):
for line in open(plan):
m = re.match(r"^\| (T-14-(?:\d\d|SC)) \|", line)
if not m:
continue
cells = [c.strip().lower() for c in line.strip().strip("|").split("|")]
prev = declared.get(m.group(1))
if prev is None:
declared[m.group(1)] = cells
continue
sev = max(prev[3], cells[3], key=lambda s: sev_rank.get(s, -1))
disp = "mitigate" if "mitigate" in (prev[4], cells[4]) else prev[4]
declared[m.group(1)] = prev[:3] + [sev, disp] + prev[5:]
if not declared:
print("refuse: no plan declares a T-14 threat", file=sys.stderr)
sys.exit(1)
lines = review.splitlines()
removal = [l for l in lines if re.match(r"^\| RC-\d+ \| T-14-", l)]
for tid, cells in sorted(declared.items()):
rows = [l for l in lines if l.startswith("| " + tid + " |")]
if len(rows) != 1:
print(f"refuse: review has {len(rows)} threat rows for {tid}, want 1", file=sys.stderr)
sys.exit(1)
row = [c.strip().lower() for c in rows[0].strip().strip("|").split("|")]
severity, disposition = cells[3], cells[4]
if severity not in row or disposition not in row:
print(f"refuse: review row {tid} does not copy severity {severity!r} and disposition {disposition!r}", file=sys.stderr)
sys.exit(1)
if disposition == "mitigate":
tests = set(test_re.findall(rows[0]))
if not tests and "check-phase14.sh" not in rows[0]:
print(f"refuse: mitigated threat {tid} names no test or gate stage", file=sys.stderr)
sys.exit(1)
unrun = sorted(t for t in tests if t not in named)
if unrun:
print(f"refuse: threat {tid} names {', '.join(unrun)}, which the --named stage does not run", file=sys.stderr)
sys.exit(1)
if not any(re.match(r"^\| RC-\d+ \| " + re.escape(tid) + r" \|", l) for l in removal):
print(f"refuse: mitigated threat {tid} has no removal check row", file=sys.stderr)
sys.exit(1)
# Validation.
for flag in ("nyquist_compliant: true", "wave_0_complete: true", "status: validated"):
if not re.search(r"^" + re.escape(flag) + r"$", validation, re.M):
print(f"refuse: validation lacks {flag!r}", file=sys.stderr)
sys.exit(1)
status_word = re.compile(r"(?<![A-Za-z])pending(?![A-Za-z])|⬜|\| TBD \|", re.I)
for line in validation.splitlines():
if line.startswith("|") and status_word.search(line):
print("refuse: validation row still pending: " + line, file=sys.stderr)
sys.exit(1)
for req in ["JOBS-02", "JOBS-03", "SRCH-02", "INTG-01", "INTG-02", "API-08", "CLI-05"]:
if req not in validation:
print(f"refuse: validation does not name {req}", file=sys.stderr)
sys.exit(1)
task_rows = "\n".join(l for l in validation.splitlines() if re.match(r"^\| 14-\d\d-T\d", l))
if not task_rows:
print("refuse: validation has no per-task verification rows", file=sys.stderr)
sys.exit(1)
for name in sorted(set(test_re.findall(task_rows))):
if name not in named:
print(f"refuse: validation names {name}, which the --named stage does not run", file=sys.stderr)
sys.exit(1)
# API coverage.
rows = [l for l in coverage.splitlines() if re.match(r"^\| [a-z0-9]+: ", l)]
if not rows:
print("refuse: COVERAGE.md has no capability rows", file=sys.stderr)
sys.exit(1)
for l in rows:
cells = [c.strip() for c in l.strip().strip("|").split("|")]
if len(cells) < 3:
print("refuse: malformed coverage row: " + l, file=sys.stderr)
sys.exit(1)
decision, reason = cells[1], cells[2]
if decision == "INTEGRATE":
tests = set(test_re.findall(reason))
if not tests:
print("refuse: INTEGRATE row names no test: " + l, file=sys.stderr)
sys.exit(1)
unrun = sorted(t for t in tests if t not in named)
if unrun:
print(f"refuse: INTEGRATE row names {', '.join(unrun)}, which the --named stage does not run: " + l, file=sys.stderr)
sys.exit(1)
elif decision == "OPT-OUT":
if len(reason) < 10:
print("refuse: OPT-OUT row without a reason: " + l, file=sys.stderr)
sys.exit(1)
else:
print(f"refuse: coverage row decision {decision!r}: " + l, file=sys.stderr)
sys.exit(1)
# Requirement wording (D-06, D-14).
for line in requirements.splitlines():
if "**INTG-02**" in line and re.search(r"\bSDK\b", line):
print("refuse: REQUIREMENTS.md INTG-02 still names an SDK: " + line, file=sys.stderr)
sys.exit(1)
if "**API-08**" in line and re.search(r"sitemap output", line, re.I):
print("refuse: REQUIREMENTS.md API-08 still names the sitemap output: " + line, file=sys.stderr)
sys.exit(1)
# ROADMAP Phase 14 (D-07, D-13): the repos line and the success criteria.
m = re.search(r"^### Phase 14:.*?(?=^### Phase )", roadmap, re.M | re.S)
if not m:
print("refuse: ROADMAP.md has no Phase 14 section", file=sys.stderr)
sys.exit(1)
section = m.group(0)
repos = "\n".join(l for l in section.splitlines() if l.startswith("**Repos:**"))
crit = re.search(r"\*\*Success Criteria\*\*(.*?)\*\*Plans:\*\*", section, re.S)
if not repos or not crit:
print("refuse: ROADMAP Phase 14 lacks its Repos line or success criteria", file=sys.stderr)
sys.exit(1)
scope = repos + "\n" + crit.group(1)
for needle in ("albums/import/discogs", "recognize", "sm-golem-plugin", "sm-feedback-plugin"):
if needle not in scope:
print(f"refuse: ROADMAP Phase 14 repos and success criteria do not name {needle}", file=sys.stderr)
sys.exit(1)
print("phase14 evidence passed")
PY
}
run_evidence() {
evidence_check "$PHASE_DIR" "$REVIEW" "$VALIDATION" "$PHASE_DIR/COVERAGE.md" "$ROOT/.planning/REQUIREMENTS.md" "$ROOT/.planning/ROADMAP.md" "$(all_named)"
}
run_self_test() {
bash -n "${BASH_SOURCE[0]}"
expect_detect pass 0 '{"Action":"pass","Package":"p","Test":"TestPhase14Threats"}'
@@ -1007,6 +1164,82 @@ for rc, threat, repo, rel, anchor, repl, pkg, run in table:
print(f"refuse: self-test {rc}: anchor occurs {n} times in {rel}", file=sys.stderr)
sys.exit(1)
PY
# The evidence check refuses a missing threat row, a wrong disposition,
# a mitigated threat without a removal row, a test the named stage does
# not run, a pending validation row, a missing Wave 0 flag, an unnamed
# validation test, an INTEGRATE row without a run test, an OPT-OUT row
# without a reason, the SDK and sitemap wording, and a roadmap without
# the shared plugin repos; a threat two plans declare takes the stricter
# severity and disposition.
mkdir -p "$scratch/phase"
printf '| T-14-90 | Spoofing | x | high | mitigate | y |\n| T-14-91 | Tampering | x | low | accept | y |\n' >"$scratch/phase/14-01-PLAN.md"
printf '| T-14-91 | Tampering | x | medium | mitigate | y |\n' >"$scratch/phase/14-02-PLAN.md"
cat >"$scratch/review.md" <<'EOR'
| T-14-90 | Spoofing | x | high | mitigate | y | TestAlpha | pass | none |
| T-14-91 | Tampering | x | medium | mitigate | y | TestAlpha | pass | none |
| RC-90 | T-14-90 | f | a | b | c | fails |
| RC-91 | T-14-91 | f | a | b | c | fails |
EOR
cat >"$scratch/validation.md" <<'EOV'
status: validated
nyquist_compliant: true
wave_0_complete: true
| 14-01-T1 | JOBS-02, JOBS-03, SRCH-02, INTG-01, INTG-02, API-08, CLI-05 | `go test -run '^TestAlpha$'` | ✅ green |
EOV
cat >"$scratch/coverage.md" <<'EOC'
| capability | decision | reason |
|---|---|---|
| vendor: thing one | INTEGRATE | test: TestAlpha (14-02) |
| vendor: thing two | OPT-OUT | not used by the PHP reference — parity port |
EOC
cat >"$scratch/req.md" <<'EOQ'
- [x] **INTG-02**: AI cover recognition through adapters over the guarded client
- [x] **API-08**: Feedback submissions (sitemap dropped for this application, D-14)
EOQ
cat >"$scratch/roadmap.md" <<'EOM'
### Phase 14: Domain jobs
**Repos:** fonoteka.go; sm-golem-plugin and sm-feedback-plugin
**Success Criteria** (what must be TRUE):
4. albums/import/discogs passes.
5. albums/recognize passes.
**Plans:** 6/6
### Phase 15: Next
EOM
local ev=("$scratch/phase" "$scratch/review.case" "$scratch/validation.case" "$scratch/coverage.case" "$scratch/req.case" "$scratch/roadmap.case" "TestAlpha")
local case
for case in complete missing-row disposition removal unrun pending wave0 unnamed integrate optout sdk sitemap roadmap; do
cp "$scratch/review.md" "$scratch/review.case"
cp "$scratch/validation.md" "$scratch/validation.case"
cp "$scratch/coverage.md" "$scratch/coverage.case"
cp "$scratch/req.md" "$scratch/req.case"
cp "$scratch/roadmap.md" "$scratch/roadmap.case"
case "$case" in
missing-row) sed -i '/^| T-14-91 /d' "$scratch/review.case" ;;
disposition) sed -i 's/| medium | mitigate |/| low | accept |/' "$scratch/review.case" ;;
removal) sed -i '/^| RC-91 /d' "$scratch/review.case" ;;
unrun) sed -i 's/| TestAlpha | pass | none |$/| TestGamma | pass | none |/' "$scratch/review.case" ;;
pending) printf '| 14-02-T1 | API-08 | x | ⬜ pending |\n' >>"$scratch/validation.case" ;;
wave0) sed -i '/^wave_0_complete: true$/d' "$scratch/validation.case" ;;
unnamed) printf '| 14-02-T1 | API-08 | `go test -run TestBeta` | ✅ green |\n' >>"$scratch/validation.case" ;;
integrate) sed -i 's/test: TestAlpha (14-02)/covered by the replay/' "$scratch/coverage.case" ;;
optout) sed -i 's/not used by the PHP reference — parity port//' "$scratch/coverage.case" ;;
sdk) sed -i 's/through adapters/through the Anthropic Go SDK/' "$scratch/req.case" ;;
sitemap) sed -i 's/Feedback submissions/Feedback submissions and sitemap output/' "$scratch/req.case" ;;
roadmap) sed -i 's/ and sm-feedback-plugin//' "$scratch/roadmap.case" ;;
esac
if [[ "$case" == complete ]]; then
evidence_check "${ev[@]}" >/dev/null 2>&1 || {
echo "refuse: self-test evidence_check rejected a complete record" >&2
exit 1
}
continue
fi
if evidence_check "${ev[@]}" >/dev/null 2>&1; then
echo "refuse: self-test evidence_check accepted the $case plant" >&2
exit 1
fi
done
echo "phase14 self-test passed"
}
@@ -1017,6 +1250,7 @@ case "${1:-}" in
--named) run_named ;;
--removal) run_removal ;;
--coverage) run_coverage ;;
--evidence) run_evidence ;;
--all)
# --removal edits tracked source while it runs, so it runs on its own.
run_self_test
@@ -1024,6 +1258,7 @@ case "${1:-}" in
run_parity
run_named
run_coverage
run_evidence
echo "phase14 all passed"
;;
*) usage ;;