Files
boshu2__agentops/scripts/check-pmf-evidence.sh
T
Bo 2c263b424c feat(scripts): PMF evidence gate + .agents-to-docs promoter (soc-m6v5.8 #pmf-evidence-gate) (#381)
## Why

The dogfood plan for 3.0
(`.agents/plans/2026-05-20-ultimate-dogfood-3.0.md`) ran a `/pre-mortem`
that surfaced HIGH-1: `.agents/` is gitignored, so any public claim
(PRODUCT.md, README, launch posts) citing only `.agents/<path>` is
unreachable for readers. D5 of the plan addresses this by introducing
the **evidence promotion** path + a CI gate.

## What

Three artifacts implementing the contract at
`docs/contracts/pmf-evidence.md`:

1. **`scripts/export-evidence.sh <bead-id> <src> [<dest-name>]`** —
copies a `.agents/` artifact to `docs/evidence/<bead-id>/<dest-name>`
and appends a provenance footer (source path, SHA-256, UTC promotion
date, bead-id). Idempotent (no-op if hashes match), refuses to overwrite
drift (exit 1 with `DRIFT` message).

2. **`scripts/check-pmf-evidence.sh [<target>...]`** — gate that scans
PRODUCT.md / README.md / `docs/launch/*.md` for lines naming `.agents/`
paths. Refuses (exit 1) any file that cites `.agents/` without also
referencing a `docs/evidence/` promotion. Opt-out per-line with `<!--
internal -->` for commentary about the rule itself. `--json` flag for CI
consumption.

3. **`docs/contracts/pmf-evidence.md`** — the contract doc + round-trip
example + acceptance criteria block + non-goals. Catalogued in
`docs/documentation-index.md` per CLAUDE.md "Contracts must be
catalogued" rule.

## How it dogfoods

The 3.0 release plan's D6 (ablation experiment, soc-vuu6.33) is the
first real consumer — its output lives at
`.agents/research/.../ablation.md` and D8 (PRODUCT.md rewrite,
soc-m6v5.9.4) needs to cite it. Without this PR, D5 vs D6 was a direct
contradiction noted in the pre-mortem.

## Test

21 bats tests, all passing:
- `tests/scripts/export-evidence.bats` (10): promotion, custom
dest-name, idempotency, DRIFT detection, invalid-bead rejection,
missing-source error, arg-count errors, SHA-256 matches content, nested
bead-id (`soc-m6v5.9.4.1`) accepted.
- `tests/scripts/check-pmf-evidence.bats` (11): clean / violation /
promotion-present / `<!-- internal -->` opt-out / line-number reporting
/ multi-violation count / `--json` shape / missing-target exit 3 /
unknown flag / default PRODUCT.md scan / tolerates missing default
targets.

Two parser bugs were caught + regression-tested during dev:
- `printf '\n---\n'` interprets `---` as a flag (fixed via `printf '%s'
'---'`).
- In-function counter doesn't always survive `set -euo pipefail` across
function boundaries (fixed by counting `wc -l` of the appended-to file).

Closes-scenario: soc-m6v5.8#pmf-evidence-gate
Bounded-context: BC1-Corpus
Evidence: scripts/export-evidence.sh
Evidence: scripts/check-pmf-evidence.sh
Evidence: docs/contracts/pmf-evidence.md
Evidence: tests/scripts/export-evidence.bats
Evidence: tests/scripts/check-pmf-evidence.bats

Co-authored-by: Codex <codex@example.invalid>
2026-05-20 23:57:47 +00:00

147 lines
5.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# check-pmf-evidence.sh — gate for PMF / productivity claims in public docs.
#
# Public docs (PRODUCT.md, README.md, docs/launch/*) cannot cite ONLY
# .agents/ paths to back a measurable claim — .agents/ is gitignored so the
# evidence is not reachable by anyone reading the doc. Use
# scripts/export-evidence.sh to promote the artifact to docs/evidence/<bead>/
# first, then cite the tracked path.
#
# This script scans a target file (default PRODUCT.md) for cite-shaped lines
# and flags any that reference only .agents/ paths without also referencing
# a tracked docs/evidence/ promotion.
#
# A "cite-shaped line" is heuristic — any line containing a path of the form
# .agents/<...>.<ext>
# or wrapped in backticks/brackets that names a `.agents/` artifact. We
# tolerate `.agents/` mentions inside fenced code blocks marked
# `<!-- internal -->` so we don't lint our own commentary about the rule.
#
# Usage:
# scripts/check-pmf-evidence.sh # default targets
# scripts/check-pmf-evidence.sh PRODUCT.md # explicit target
# scripts/check-pmf-evidence.sh --json file # machine-readable
# scripts/check-pmf-evidence.sh --list # list violations across defaults
#
# Exit codes:
# 0 — clean (no .agents/-only citations in scanned files)
# 1 — at least one violation found
# 2 — usage error
# 3 — target file not readable
set -euo pipefail
JSON=0
LIST=0
declare -a TARGETS=()
usage() {
sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'
exit "${1:-0}"
}
while [ $# -gt 0 ]; do
case "$1" in
--json) JSON=1 ;;
--list) LIST=1 ;;
-h|--help) usage 0 ;;
--*) echo "check-pmf-evidence: unknown flag: $1" >&2; usage 2 ;;
*) TARGETS+=("$1") ;;
esac
shift || true
done
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
if [ "${#TARGETS[@]}" -eq 0 ]; then
# Default scan set: top-level public docs + launch artifacts.
TARGETS=("$ROOT/PRODUCT.md" "$ROOT/README.md")
while IFS= read -r f; do
TARGETS+=("$f")
done < <(find "$ROOT/docs/launch" -maxdepth 2 -type f -name '*.md' 2>/dev/null || true)
fi
violations_file="$(mktemp)"
trap 'rm -f "$violations_file"' EXIT
violation_count=0
scan_one() {
local f="$1"
if [ ! -r "$f" ]; then
if [ "$LIST" -eq 0 ] && [ "${#TARGETS[@]}" -eq 1 ]; then
# Explicit single-target read failure is a hard error.
echo "check-pmf-evidence: target not readable: $f" >&2
return 3
fi
return 0
fi
# Find lines that mention .agents/ paths.
local lineno=0
while IFS= read -r line; do
lineno=$((lineno + 1))
# Skip lines inside the documented internal-comment shape.
if printf '%s' "$line" | grep -qE '<!--[[:space:]]*internal[[:space:]]*-->'; then
continue
fi
if printf '%s' "$line" | grep -qE '\.agents/[A-Za-z0-9._/-]+'; then
# Does the same file also promote this artifact via docs/evidence/?
# We require a docs/evidence/ mention SOMEWHERE in the file (not
# necessarily the same line) — heuristic but easy to satisfy.
if ! grep -qE 'docs/evidence/[A-Za-z0-9._/-]+' "$f"; then
printf '%s\t%d\t%s\n' "$f" "$lineno" "$line" >> "$violations_file"
violation_count=$((violation_count + 1))
fi
fi
done < "$f"
}
# Hard error: a single explicit target that's not readable should NOT be
# silently skipped. Detect this case before entering the per-target loop
# (the `|| true` inside the loop would otherwise swallow `return 3`).
if [ "${#TARGETS[@]}" -eq 1 ] && [ "$LIST" -eq 0 ] && [ ! -r "${TARGETS[0]}" ]; then
echo "check-pmf-evidence: target not readable: ${TARGETS[0]}" >&2
exit 3
fi
for f in "${TARGETS[@]}"; do
scan_one "$f" || true
done
# Recount from the file — the in-function counter doesn't always survive
# `set -euo pipefail` + the function-call boundary across all shells, and
# counting wc -l of the appended-to file is the authoritative source.
if [ -s "$violations_file" ]; then
violation_count="$(wc -l < "$violations_file" | tr -d ' ')"
else
violation_count=0
fi
if [ "$JSON" -eq 1 ]; then
if [ -s "$violations_file" ]; then
jq -Rn --slurpfile vs <(jq -Rn 'inputs | split("\t") | {file: .[0], line: (.[1]|tonumber), text: .[2]}' < "$violations_file" 2>/dev/null) \
'{violations: ($vs[0] // []), count: ($vs[0] | length)}' 2>/dev/null || \
printf '{"count": %d}\n' "$violation_count"
else
printf '{"violations":[],"count":0}\n'
fi
else
if [ -s "$violations_file" ]; then
echo "check-pmf-evidence: FAIL — $violation_count .agents/-only citation(s) in public docs:"
awk -F'\t' '{ printf " %s:%d\n %s\n", $1, $2, $3 }' "$violations_file"
echo
echo "Fix: promote each .agents/ artifact via scripts/export-evidence.sh,"
echo "then cite the tracked docs/evidence/<bead-id>/<file> path instead."
else
if [ "$LIST" -eq 1 ] || [ "${#TARGETS[@]}" -gt 1 ]; then
echo "check-pmf-evidence: OK — ${#TARGETS[@]} file(s) scanned, 0 violations"
else
echo "check-pmf-evidence: OK — ${TARGETS[0]} clean"
fi
fi
fi
if [ "$violation_count" -gt 0 ]; then
exit 1
fi
exit 0