diff --git a/.orly/audits/spec-template.sh b/.orly/audits/spec-template.sh index aee0e1e..59bb435 100755 --- a/.orly/audits/spec-template.sh +++ b/.orly/audits/spec-template.sh @@ -1,35 +1,17 @@ #!/usr/bin/env bash -# spec-template.sh — enforce docs/TEMPLATE.md across every spec under -# docs/v*/{pending,active,done}/. Two check families: -# -# 1. PROHIBITED patterns (negative space) — TEMPLATE.md "Prohibited" section: -# time/effort estimates, effort/complexity columns, %-complete, owners, dates. -# Always BLOCK. -# 2. REQUIRED-PRESENT + NO-PLACEHOLDER (positive space) — the determinism -# sections the agent-facing template mandates (PR Intent, Applicable Gates, -# Prior-Art, Metrics, Decomposition, tiered Test Spec, Product Clarity, -# Discovery, …) must EXIST and carry content: no known unfilled slots and -# no surviving "tpl:" guidance comments (template fill grammar). A spec -# that omits them forces the executing agent to guess intent. This is the -# half that makes a spec "built for the agent". -# 2b. DECLARED-COMMAND PARITY — a pending/active spec's Acceptance Rubric -# must quote the repository's declared conform + applicable verify.* commands -# (.orly/orly.json) verbatim: the rubric and `orly gate` grade one -# boundary. Staged/file scope only; done/ specs stay historical. -# -# Dispatch façade: dispatch/write_spec.md (SPEC TEMPLATE GATE) -# Fires in: make lint (after audit-logging, before audit-error-codes). +# spec-template.sh — select specs and snapshot staged content for the shared +# validator in spec-template.ts. The native orly spec.gate criterion calls the +# same validator directly; this entry point serves script callers. +# Dispatch façade: dispatch/write_spec.md (SPEC TEMPLATE GATE). # # Modes: # --staged diff-scope: only specs in `git diff --cached` # --all (default) pending+active specs only — current/in-flight work # --include-done adds done/ specs to the scan (one-time sweep tool) # -# Family 2 runs in --staged and --file scope — the spec being authored/edited right -# now, which is exactly the agent's own output. The bulk scans (--all / -# --include-done) run Family 1 only, so they behave identically over the whole -# corpus and never break an existing spec. No legacy carve-out, no heuristics: -# author a spec → full template required; scan the tree → prohibited-only. +# Staged/file checks require complete sections, references, test mappings and +# declared commands. Bulk scans check prohibited patterns only. Bun is required +# for both scopes; bulk checks do not require repository command configuration. # # Done specs are excluded by default: historical artifacts, not work product. # Exits 0 clean, 1 on any blocking finding. @@ -44,7 +26,6 @@ cd "$ROOT" FAIL=0 fail() { printf "FAIL: %s\n" "$*" >&2; FAIL=1; } ok() { printf "OK: %s\n" "$*"; } -note() { printf "NOTE: %s\n" "$*"; } # Discover spec files in scope. case "$MODE" in @@ -62,8 +43,7 @@ case "$MODE" in while IFS= read -r s; do SPECS+=("$s"); done < <(find docs/v[0-9]* -type f -name '*.md' 2>/dev/null | grep -E '/(pending|active|done)/' || true) ;; --file|file) - # Single named spec, both families — the lifecycle engine's spec.gate - # criterion calls this so the engine and pre-commit share one enforcer. + # Single named spec, using the same checks as the native criterion. [[ -n "${2:-}" ]] || { printf "usage: %s --file \n" "$0" >&2; exit 64; } [[ -f "$2" ]] || { printf "FAIL: spec not found: %s\n" "$2" >&2; exit 1; } SPECS=("$2") @@ -79,70 +59,6 @@ if [[ ${#SPECS[@]} -eq 0 ]]; then exit 0 fi -# --------------------------------------------------------------------------- -# Family 1 — Prohibited patterns from docs/TEMPLATE.md "Prohibited" section. -# Each pattern: :: — the regex comes -# LAST because it may contain colons (e.g. the [:|] label-row class); read(1) -# assigns the remainder of the line to the final field, so the regex survives -# intact. Descriptions must stay colon-free. -# Severity: BLOCK = exit 1; INFO = stdout note only. Patterns use ERE (egrep -E) -# only — no PCRE constructs like (?:...), which grep -E rejects. -# -# DESIGN NOTE — patterns are deliberately structural, not prose-level. -# Bare regex like `\b\d+\s*days?\b` false-positives on legitimate prose -# ("Day 50", "≥7 days uptime", "within 3 days of install"). Instead we -# match where time/effort estimates LIVE structurally: -# - Section headings: `## Estimated effort`, `## Effort`, `## Sizing` -# - Label rows: `**Effort:** medium`, `**Owner:** alice` -# - Tilde-bulleted: `- ~2 h survey`, `* ~5 days OAuth setup` -# (the `~` prefix is Indy's idiom for effort estimates; -# legitimate prose doesn't use it) -# This catches the actual drift class without churning real content. -# --------------------------------------------------------------------------- -PATTERNS=( - 'BLOCK:Estimated effort section heading:^#+ .*Estimated [Ee]ffort' - 'BLOCK:Effort/Complexity/Sizing section heading:^#+ .*\b(Effort|Complexity|Sizing|Cost)\b\s*$' - 'BLOCK:Scale estimate section heading:^#+ .*\bScale\b.*estimate' - 'BLOCK:Tilde-bulleted hour estimate (e.g. "- ~2 h"):^[-*]\s*~\s*[0-9]+\s*h\b' - 'BLOCK:Tilde-bulleted time estimate:^[-*]\s*~\s*[0-9]+\s*(hours?|days?|min(utes?)?)\b' - 'BLOCK:Effort/Estimate label row:^\s*\*\*\s*(Effort|Estimate|Sizing|Complexity|Cost)\s*(:\s*\*\*|\*\*\s*[:|])' - 'BLOCK:Owner label row:^\s*\*\*\s*Owner\s*(:\s*\*\*|\*\*\s*[:|])' - 'BLOCK:Assigned-to label row:^\s*\*\*\s*Assigned to\s*(:\s*\*\*|\*\*\s*[:|])' - 'BLOCK:Date-bound deadline label:^\s*\*\*\s*(Due|Deadline)\s*(:\s*\*\*|\*\*\s*[:|])\s*[0-9]' - 'BLOCK:Percentage-complete metric:^[-*\|]?\s*\*?\*?\s*[0-9]+\s*%\s*complete' - 'BLOCK:Hour-range estimate in effort context:\b(effort|estimate|takes?|spend)\b[^.]{0,40}\b[0-9]+\s*[-–]\s*[0-9]+\s*h\b' - 'BLOCK:Effort/complexity rating:\b(low|medium|high|small|large)\s*[/|-]\s*(effort|complexity)\b' -) - -# Carve-outs — lines that look prohibited but are legitimate. Skip any match. -SAFE_LINES_RE='^\*\*Date:\*\*|^\s*```|