100%

Section 06: Plan schema — mandatory Intelligence Reconnaissance block + validator

Status: Not Started Goal: Every new plan section carries an unnumbered ## Intelligence Reconnaissance block — placed after the section framing (Goal / Context / Reference / Depends on) and BEFORE ## {NN}.1 — that records the literal graph queries the author ran and a bounded ≤500-char results summary with date. python -m scripts.plan_corpus check enforces it with a WARNING/ERROR outcome model gated by section status. No on-edit escalation — in-progress sections stay at Severity.MEDIUM / Outcome.WARNING regardless of edits. Retrofit of not-started sections across the active corpus is §09’s work.

Context: Plan sections today are the primary unit of compiler work but carry no required reconnaissance. Making recon schema-mandatory turns the graph from “something you remember” into “something the plan corpus enforces.” §06 lands the template, the SSOT cite, and the validator architecture; §09 consumes §06 to backfill not-started sections. TPR codex-029 (high) is the root driver; gemini-005 concurs.

Design decision 1 — scope is FileClass.PLAN_SECTION only. scripts/plan_corpus/schema.py:60’s FileClass enum carries four section-like classes: PLAN_SECTION, ROADMAP_SECTION, BUG_TRACKER_SECTION, FIX_BUG. The recon mandate applies to PLAN_SECTION ONLY:

  • ROADMAP_SECTION files already use ## {NN}.0 for substantive content (e.g., plans/roadmap/section-00-parser.md:67). Overlaying an unnumbered recon block on top is possible, but roadmap sections are curated differently (many are in-flight, some are long-frozen process documents). Excluding them is both cheaper and correct.
  • BUG_TRACKER_SECTION and FIX_BUG use a separate template (1. Root Cause / 2. TDD / 3. Implementation / ... per plan-schema.md:789). The {NN}.X numbering does not apply, and fix-BUG-*.md files already run the full reconnaissance workflow through /fix-bug Phase 1 — duplicating it in a block is redundant.
  • Both the validator (§06.2) and the retrofit tool (§09.1) MUST filter on file_class == FileClass.PLAN_SECTION. Negative-pin tests ensure no false-positive findings fire on the exempt classes.
  • reroute: true and parallel: true are INDEX-level properties, not file classes — the ordinary section files under such indices are still PLAN_SECTION and IN scope.

Design decision 2 — unnumbered block, NOT {NN}.0 subsection. An earlier draft proposed ## {NN}.0 Intelligence Reconnaissance as a numbered subsection. 23 existing sections already use ## {NN}.0 for Prerequisites / Preflight / Goal content — mandating {NN}.0 Intelligence Reconnaissance would collide with those semantics or force mass renumbering. The unnumbered-block design — treating recon like the existing Goal / Context / Reference implementations unnumbered blocks — avoids the collision entirely and matches the structural-not-indexed nature of reconnaissance. sections: frontmatter lists only numbered {NN}.X subsections; the recon block does not appear there.

Design decision 3 — methodology + results, NOT raw SSOT expansion. The block stores: (1) the literal scripts/intel-query.sh commands the author ran, (2) a ≤500-char results summary, (3) the date. It does NOT @-include the SSOT protocol. @-includes are expanded by the harness at skill/command prompt-expansion time but NOT in plan-file markdown — grep -rEn '^@\.' plans/ returns zero hits today. Embedding @.claude/skills/dual-tpr/compose-intel-summary.md in a plan body produces a dead literal; embedding the expanded SSOT protocol would be a LEAK:algorithmic-duplication — the exact pathology §03 just fixed for 18 consumers. The recon block records WHAT was done; the SSOT records HOW — one-way reference, no content copy.

Design decision 4 — Severity and Outcome are INDEPENDENT axes. scripts/plan_corpus/__main__.py:37 currently does return 1 if all_findings else 0, so any finding fails check regardless of severity. And scripts/plan_corpus/types.py:48 defines Severity as LOW / MEDIUM / HIGH / CRITICAL — not WARNING / ERROR. Severity alone is insufficient for a gate because the enforcement context matters independently of impact. §06.2 introduces a distinct Outcome axis:

  • Severity is set by the emitter — LOW / MEDIUM / HIGH / CRITICAL — per the impact classification of the finding. It reflects “how bad is this?”
  • Outcome is set by the emitter — WARNING / ERROR — per the enforcement mode. It reflects “does this gate the check?” Outcome is NOT auto-derived from Severity; both are set explicitly at Finding-construction time.
  • Default mode: status: not-started missing recon → Severity.HIGH + Outcome.WARNING. status: in-progress missing recon → Severity.MEDIUM + Outcome.WARNING.
  • --strict-recon mode: status: not-started missing recon → Severity.HIGH + Outcome.ERROR (Severity unchanged; Outcome rewritten). status: in-progress is unaffected by --strict-recon.
  • Exit policy: exit 1 iff any finding has Outcome == ERROR.

Design decision 5 — status-gated severity, not A/B/C operator menu. Combined with the corpus already carrying a status field, the validator reads each PLAN_SECTION’s data["status"] (where data is ValidatedFile.data, the parsed frontmatter dict) and applies: not-started missing recon → Severity.HIGH, Outcome.WARNING (default) or Outcome.ERROR (--strict-recon); in-progress missing recon → Severity.MEDIUM, Outcome.WARNING; complete → exempt (0 findings). This is the objective, data-driven model that replaces §06’s earlier A/B/C retrofit menu (now gone; retrofit is §09).

Design decision 6 — --strict-recon as a CLI flag, not a corpus-wide frontmatter. An earlier draft proposed a per-plan strict_recon: bool on 00-overview.md. That model requires the PlanSectionSchema / OverviewSchema to accept a new frontmatter field (currently schema.py:264 rejects unknown keys) and creates corpus-wide state that’s hard to preview. Making it a CLI flag keeps policy at the invocation site: CI can pin --strict-recon for not-started sections; local runs default to warnings-only. No schema widening needed for policy — §06 does NOT add any frontmatter field to OverviewSchema.

Design decision 7 — format coupling with §03 and §07 is a CONTRACT, not a convention. 00-overview.md:144 already states “§07 hook output format MUST match §03’s bounded summary template exactly.” §06 extends this into a three-way coupling: §03 helper (source), §06 recon block (plan-body artifact), §07 hook injection (runtime prompt artifact) — all three use .claude/skills/dual-tpr/compose-intel-summary.md as the authoritative SSOT. The §03/§06/§07 shared contract covers three invariants: (1) ≤500-char bound, (2) [ori]/[repo#N]/[repo:path] citation vocabulary, (3) §03 SSOT helper as the source. Exact line-level formatting may vary per consumer’s rendering context (§06 plan-resident blocks are static markdown artifacts; §07 hooks inject a bounded summary into a prompt payload; these rendering contexts differ legitimately). Graceful degradation: when scripts/intel-query.sh status returns unavailable, the §07 hook omits the summary entirely (per compose-intel-summary.md lines 222-227: “entire summary is OMITTED”), and the §06 plan-resident artifact records the graph-unavailable state as freeform prose (e.g. "Graph was unavailable at YYYY-MM-DD when this section was authored") — NOT a sentinel string matched by the validator. Drift in the ≤500-char bound or citation vocabulary among the three surfaces is a DRIFT:scattered-knowledge finding; line-level formatting differences are not. §06.1’s template text names this contract explicitly; §06.2’s anti-stub detector enforces the citation-grammar half; §07’s plan file cites §06’s contract back.

NOTE — §07 skeleton drift: The current section-07-pre-review-intel-hook.md hook skeleton uses a placeholder - [$FILE] $RESULT output format that does NOT yet match this citation-grammar contract. This is a known inconsistency: §07 is not yet implemented, and the skeleton is scaffolding only. §07’s implementation MUST rewrite the hook output to emit the Step D citation grammar (≤500 chars, [ori] / [repo#N] / [repo:path] markers) before §07 close-out. The coupling contract stated here is the target; §07’s current skeleton is the delta that implementation will close.

Reference implementations:

  • Ori .claude/skills/create-plan/plan-schema.md existing Section File Template (lines 236-508) — §06.1 edit target
  • Ori .claude/skills/create-plan/SKILL.md:808 — second SSOT-ish surface that re-asserts subsection structure; §06.1 cites plan-schema.md instead
  • Ori scripts/plan_corpus/ Python package — types.py (Severity enum), parser.py (frontmatter split + body_offset), schema.py (FILE_CLASS_META + validate), schemas.py (PlanSectionSchema strict allowlist), discovery.py (load_and_validate / ValidatedFile), __main__.py (check / discover / docgen subcommands) — §06.2 edit targets
  • Ori .claude/skills/dual-tpr/compose-intel-summary.md — the SSOT helper the template references (no @-include in plan bodies)
  • Ori 23 existing ## {NN}.0 headers in plans/ — motivation for the unnumbered design
  • Ori plans/roadmap/section-00-parser.md:67 — roadmap-side .0 collision source; evidence for the PLAN_SECTION-only scope decision

Depends on: Section 03 (the recon block describes running the §03 SSOT queries).


Intelligence Reconnaissance

Queries run 2026-04-14 (during /review-plan of this section):

  • scripts/intel-query.sh status — graph available (191K Ori symbols indexed, 10 reference compilers with 505K CALLS edges and 298K issues)
  • scripts/intel-query.sh --human search "plan schema validation" --limit 5 — surfaced 5 external-repo schema-notation issues (zig ZON, go JSON schema, lean4 lakefile toml schema); low direct relevance for in-repo meta-tooling
  • scripts/intel-query.sh --human file-symbols "scripts/plan_corpus/schema" --repo ori — zero results (Python code is NOT indexed in the Ori code-symbol graph; the code graph is Rust-only). Walked the 9-module scripts/plan_corpus/ package directly via Read.
  • scripts/intel-query.sh --human callers "validate" --repo ori — surfaced Rust-side callers (compiler/ori_types / ori_arc); disambiguated via manual Read that scripts/plan_corpus/schema.py:487 validate(fc, data, path) takes only frontmatter — body_text is produced in discovery.py:load_and_validate and currently NEVER reaches validators.
  • grep -rEn '^## \d+\.0\s' plans/ — 23 existing plan sections use ## {NN}.0 for Prerequisites / Preflight / Goal (the decisive finding that forced the unnumbered design)
  • grep -rEn '^@\.' plans/ — zero hits; confirms plan files are NOT harness-expanded, so @-includes in plan bodies would be dead literals

Results summary (≤500 chars) [ori]: Graph indexes Rust+reference repos but NOT scripts/plan_corpus/ Python. Direct Read confirmed: validate(fc, data, path) takes only frontmatter; body_text is split in discovery.py:load_and_validate into ValidatedFile but never propagated — body-level validation requires a new phase, not a parameter tweak. Grep surfaced the load-bearing finding: {NN}.0 slot occupied 23x (PLAN_SECTION) and roadmap .0 is substantive content — forcing unnumbered-block design and PLAN_SECTION-only scope. Severity enum is LOW/MEDIUM/HIGH/CRITICAL (not WARNING/ERROR) — outcome model is a new axis.

Subsystem-mapping note: no preset matches meta-tooling (scripts/plan_corpus/, .claude/skills/create-plan/). Used search fallback per .claude/rules/intelligence.md §Subsystem Mapping. The template text in §06.1 explicitly addresses this fallback case for non-compiler plans.

See .claude/skills/dual-tpr/compose-intel-summary.md for the full query protocol (SSOT — do NOT inline; this block records what was done, not the protocol itself).


06.1 Plan-schema + create-plan SKILL.md edits

File(s): .claude/skills/create-plan/plan-schema.md, .claude/skills/create-plan/SKILL.md

Two surfaces describe section-level structural invariants today: plan-schema.md (Section File Template + “MANDATORY SUBSECTION STRUCTURE” HTML comment at lines 315-326) and create-plan/SKILL.md:808 (which independently hardcodes "EVERY subsection ({NN}.1, {NN}.2, ...)"). Updating only one creates DRIFT:scattered-knowledge. §06.1 edits both — plan-schema.md as authoritative SSOT, SKILL.md as pointer.

  • plan-schema.md — insert unnumbered recon block in the Section File Template. After the **Depends on:** Section {NN} ({why}). line (currently line 311) and BEFORE the --- separator preceding ## {NN}.1, add a --- separator and the following unnumbered block example:

    ---
    
    ## Intelligence Reconnaissance
    
    Queries run {YYYY-MM-DD}:
    
    - `scripts/intel-query.sh --human <preset>` — {one-line outcome}. For compiler sections use the matching preset per `.claude/rules/intelligence.md` §Subsystem Mapping (`ori-arc`, `ori-inference`, `ori-codegen`, `ori-patterns`, `ori-diagnostics`). For non-compiler plans (meta-tooling, docs, build scripts) use `search "<key terms>"` — no preset applies.
    - `scripts/intel-query.sh --human file-symbols "<path-fragment>" --repo ori` — {one-line outcome} (skip for non-Rust targets; the Ori code-symbol index is Rust-only today)
    - `scripts/intel-query.sh --human callers "<symbol>" --repo ori` — {one-line outcome} (blast radius for every public API the section changes)
    - `scripts/intel-query.sh --human similar "<symbol>" --repo rust,swift,koka --limit 5` — {one-line outcome} (cross-repo prior art for design decisions)
    
    Results summary (≤500 chars) [ori]: {bounded paragraph citing blast radius, cross-repo prior art, relevant symbols. Use `[ori]` for Ori-repo claims, `[rust#N]` / `[swift#N]` / `[koka#N]` / etc. for cross-repo issue citations, and `[repo:path]` for symbol results — the same grammar used by `compose-intel-summary.md` Step D (lines 64-82) and by §07's hook injection. Maximum 5 bullets, 500 characters. If the graph is unavailable, record the unavailability state as freeform prose (e.g. `"Graph was unavailable at YYYY-MM-DD when this section was authored"`) — do NOT silently omit the block; the block MUST still exist with the date and a note about unavailability so the validator recognizes it as intentional rather than forgotten.}
    
    See `.claude/skills/dual-tpr/compose-intel-summary.md` for the full query protocol (SSOT — do NOT `@`-include in plan files; plan markdown is not harness-expanded, so the include would be a dead literal).

    Placement requirement: AFTER all section framing (Goal, Success Criteria, Context, Reference implementations, Depends on) and BEFORE the first numbered subsection (## {NN}.1). The block is structurally parallel to the framing blocks — not a subsection.

    Format-coupling contract: The shared contract enforced by _check_intel_recon_block covers: (a) presence of citation markers ([ori], [repo#N], [repo:path]), (b) presence of date marker (ISO YYYY-MM-DD), (c) presence of literal scripts/intel-query.sh command line, (d) absence of mixed-placeholder shapes (per Fix 1 — mixed-placeholder-after-citation emits GAP:VALIDATION_BYPASS). The ≤500-char bound and exact Step D output formatting are SOFT contracts — guidance for §07 hook authors and §06.1 template users, NOT enforced by the §06.2 validator and NOT enforced by discover either. §07 hook implementation owns its own length enforcement (the hook injects a bounded summary into prompt payload; plan-resident blocks are author-curated and the cap is aspirational). Graceful degradation: §07 hook omits the summary entirely when graph is unavailable (per compose-intel-summary.md lines 222-227); §06 plan-resident artifact records the graph-unavailable state as freeform prose with a date (e.g. “Graph was unavailable at YYYY-MM-DD when this section was authored”) — the validator recognizes this as RECON_GRAPH_UNAVAILABLE at Severity.LOW / Outcome.WARNING (intentional documentation, NOT a VALIDATION_BYPASS). Drift in the citation vocabulary among the three surfaces is a DRIFT:scattered-knowledge finding (see §06 Design decision 7); the ≤500-char bound is not validator-enforced anywhere, so no enforcement drift is possible.

  • plan-schema.md — replace the “MANDATORY SUBSECTION STRUCTURE” comment (currently lines 315-326) with “MANDATORY SECTION STRUCTURE” covering both load-bearing invariants:

    <!-- == MANDATORY SECTION STRUCTURE ==
    Every PLAN_SECTION file has TWO mandatory structural features that are
    NOT captured by the numbered {NN}.X subsection sequence alone:
    
    1. **Unnumbered `## Intelligence Reconnaissance` block** — placed after
       the section framing (Goal / Success Criteria / Context / Reference
       implementations / Depends on) and BEFORE `## {NN}.1`. Records the
       literal `scripts/intel-query.sh` commands the author ran, a
       ≤500-char results summary (using the same `[ori]` / `[repo#N]`
       citation grammar as `.claude/skills/dual-tpr/compose-intel-summary.md`
       Step D, lines 64-82), and the date. Coexists with §07's runtime hook:
       the hook omits the summary entirely when graph is unavailable; the
       plan-resident block records unavailability as freeform prose. Enforced
       by `python -m scripts.plan_corpus check` — the validator gates
       severity on the section's `status` field:
         - status: not-started → Severity.HIGH (ERROR under --strict-recon)
         - status: in-progress → Severity.MEDIUM (WARNING, no on-edit escalation)
         - status: complete    → exempt
    
    2. **Per-subsection close-out blocks** — EVERY numbered subsection
       ({NN}.1, {NN}.2, ...) MUST end with a `**Subsection close-out**`
       block containing the per-subsection `/improve-tooling`
       retrospective and `/sync-claude` doc sync BEFORE the `---`
       separator. Pain memory decays within hours, so the look-back fires
       while the debugging journey is hot — NOT at section close.
    
    SCOPE: The recon-block mandate applies ONLY to FileClass.PLAN_SECTION
    (files matching `plans/*/section-*.md` excluding `plans/roadmap/` and
    `plans/bug-tracker/`). Roadmap sections already use `## {NN}.0` for
    substantive content; fix-BUG-*.md files use a separate `1. Root Cause
    / 2. TDD / ...` template that runs recon through /fix-bug Phase 1.
    
    Plans that omit either feature will fail `/continue-roadmap`
    validation. This comment is the only authoritative enumeration of
    section-level structural invariants; `create-plan/SKILL.md` cites
    this schema file and does NOT re-assert the invariants
    (per `impl-hygiene.md` §SSOT).
    -->
  • plan-schema.md — sections: frontmatter example stays unchanged. The recon block is UNNUMBERED and does NOT appear in the sections: list. Add a one-line comment near the sections: example:

    # Note: Intelligence Reconnaissance is an UNNUMBERED structural block
    # (like Goal, Context, Reference implementations, Depends on). It does
    # NOT appear in this `sections:` list — only numbered {NN}.X subsections do.
    sections:
      - id: "{NN}.1"
        ...
  • create-plan/SKILL.md:808 — replace re-assertion with citation. Current text: "**Per-subsection close-out blocks** — EVERY subsection ({NN}.1, {NN}.2, ...) MUST end with a 'Subsection close-out' block ...". New text:

    - **Section-level structural invariants** — see `.claude/skills/create-plan/plan-schema.md` "MANDATORY SECTION STRUCTURE" HTML comment for the two authoritative invariants: (1) unnumbered `## Intelligence Reconnaissance` block placed between section framing and `## {NN}.1` (PLAN_SECTION only; roadmap and bug-tracker sections are exempt); (2) per-subsection close-out blocks containing `/improve-tooling` + `/sync-claude` calls. `plan-schema.md` is the SSOT per `impl-hygiene.md` §SSOT; SKILL.md does NOT re-state the invariants — any drift between the two surfaces is a `DRIFT:scattered-knowledge` finding.
  • Verify via grep that no other .claude/ file independently re-asserts subsection structure. Command: grep -rn "EVERY subsection\|{NN}.1, {NN}.2" .claude/. Expected post-edit: only plan-schema.md contains the authoritative assertion; SKILL.md contains only the citation. If additional re-assertion sites exist, update each to cite plan-schema.md. Document findings in the subsection close-out.

  • Subsection close-out (06.1) — MANDATORY before starting 06.2:

    • Template changes land; plan-schema.md renders with the unnumbered ## Intelligence Reconnaissance block in the canonical example AND the scope note (PLAN_SECTION only) in the MANDATORY SECTION STRUCTURE comment
    • create-plan/SKILL.md:808 updated to cite plan-schema.md rather than re-state invariants, including the PLAN_SECTION-only scope note
    • Format-coupling contract text is present in both the template block and the MANDATORY SECTION STRUCTURE comment; the [ori] / [repo#N] / [repo:path] citation grammar and graceful-degradation behavior (block omitted for §07 hook; freeform prose for §06 plan-resident artifact) are named explicitly
    • grep -rn "EVERY subsection\|{NN}.1, {NN}.2" .claude/ shows only plan-schema.md as authoritative site
    • python -m scripts.plan_corpus check plans/query-intel-adoption/section-06-plan-schema-recon.md still returns 0 (this file’s own recon block above is already non-stub; 06.1 changes do not falsely trigger the not-yet-landed 06.2 validation)
    • Update 06.1 status to complete
    • Run /improve-tooling retrospectively on 06.1 — was editing two SSOT surfaces in lockstep painful enough to warrant a small scripts/ helper that diff-greps for subsection-structure re-assertions? If yes, add it. Commit via build(tooling): add X — surfaced by query-intel-adoption/section-06.1 retrospective. If no gaps, document: "Retrospective 06.1: no tooling gaps — plan-schema.md and SKILL.md edits were mechanical."
    • Run /sync-claude on 06.1plan-schema.md is the SSOT for plan shape. Verify CLAUDE.md §Commands “Plan corpus” bullet (line ~167) still matches the invocation form (python -m scripts.plan_corpus check). Verify no .claude/rules/*.md file references a pre-package single-file .py path or the old {NN}.0 proposal.
    • Repo hygiene checkdiagnostics/repo-hygiene.sh --check → clean.

06.2 plan_corpus validator: body_text propagation, warning/error outcome model, status-gated severity, anti-stub detection, matrix tests

File(s): scripts/plan_corpus/types.py, scripts/plan_corpus/__main__.py, scripts/plan_corpus/schema.py, scripts/plan_corpus/discovery.py, tests/plan-audit/test_recon_block.py (new), tests/plan-audit/fixtures/ (new fixtures)

Four implementation gaps block the enforcement contract:

  1. Body text does not reach validators. scripts/plan_corpus/schema.py:487validate(fc, data, path) takes only frontmatter. scripts/plan_corpus/discovery.py:268 load_and_validate(path) splits body text into ValidatedFile.body (real field name per discovery.py:212) but never passes it into the validator dispatch. Body-level recon detection requires plumbing or a parallel phase.
  2. No WARNING/ERROR outcome model. scripts/plan_corpus/__main__.py:37 does return 1 if all_findings else 0. Severity distinctions are not expressible as exit codes.
  3. No status-gated severity. Validators receive frontmatter data but don’t branch on data.get("status") when emitting recon-block findings.
  4. No anti-performative-ritual detection. Nothing today rejects “header-present, body-empty” or “header-present, no citation” stubs.

All four must be fixed together; any one alone leaves the enforcement path broken.

  • Fix CLI entrypoint DRIFT. Grep active/editable surfaces for legacy references to the single-file .py form of the package (with the .py suffix appended directly to scripts/plan_corpus — the single-file form does NOT exist; the package is invoked as python -m scripts.plan_corpus). Command (uses a two-step grep pattern assembled at runtime to avoid matching this instruction itself):

    PAT='scripts/plan_corpus'; PAT="${PAT}\.py"
    grep -rlE "$PAT" CLAUDE.md .claude/ scripts/ plans/*/*.md | grep -v 'plans/completed/'

    Then rewrite each matched file, replacing every occurrence of the legacy single-file path with python -m scripts.plan_corpus. Scope includes: CLAUDE.md §Commands, .claude/rules/.md, .claude/skills/, scripts/*.py, plans/*/section-*.md (all active sections — not just section-*.md — so 00-overview.md and index.md files under active plan directories are included), excluding plans/completed/ AND excluding files whose frontmatter is status: complete. Do NOT edit plans/completed/ files OR status: complete sections inside non-completed plans — they are frozen artifacts and retain historical command strings as-is. Post-fix verification: the runtime-assembled grep pattern above, filtered to exclude plans/completed/ AND any file whose frontmatter includes status: complete, must return zero matches. Capture the count of replaced occurrences in the close-out note.

    NOTE: Scrub scope is intentionally limited to active/editable surfaces only (CLAUDE.md, .claude/rules/.md, .claude/skills/, scripts/.py, plans//.md excluding plans/completed/). Completed sections inside non-completed plans (e.g., status: complete section-.md files) should NOT be edited — they are frozen artifacts. Only skip the plans/completed/ directory wholesale plus any individual section whose frontmatter is status: complete.

  • Add MISSING_RECON_BLOCK, VALIDATION_BYPASS, and RECON_GRAPH_UNAVAILABLE to the FindingSubtype enum in scripts/plan_corpus/types.py (around line 85). Register all three under FindingCategory.GAP in the _CATEGORY_SUBTYPES dict (around line 153, within the FindingCategory.GAP: frozenset({...}) block). The validator then emits:

    • Finding(category=FindingCategory.GAP, subtype=FindingSubtype.MISSING_RECON_BLOCK, ...) for entirely missing blocks
    • Finding(category=FindingCategory.GAP, subtype=FindingSubtype.VALIDATION_BYPASS, ...) for stub/ritual blocks (header present but content fails concrete-content checks)
    • Finding(category=FindingCategory.GAP, subtype=FindingSubtype.RECON_GRAPH_UNAVAILABLE, ...) for graph-unavailable documentation blocks (intentional, NOT a performative stub — see detection rules below) All three are type-safe via the enum; without this registration, Finding construction raises ValueError because FindingSubtype is an enum and _CATEGORY_SUBTYPES validation rejects unregistered members.
  • Add Outcome enum to scripts/plan_corpus/types.py. Distinct axis from Severity:

    class Outcome(enum.Enum):
        """Gate outcome — distinct from Severity. Gate behavior answers
        'does this fail the check?' independently of how severe it is."""
        WARNING = "warning"   # printed, does NOT affect exit code
        ERROR = "error"       # printed AND forces exit 1

    Add outcome: Outcome = Outcome.ERROR as a default field on the Finding dataclass. The default is Outcome.ERROR — this ensures ALL existing Finding(...) callsites (schema violations, parse errors, unknown frontmatter keys, etc.) continue to emit Outcome.ERROR and gate CI without requiring edits to every existing callsite. Only the new recon-block-specific findings explicitly use Outcome.WARNING. Outcome is set EXPLICITLY by each new emitter at Finding-construction time — it is NOT auto-derived from Severity. The two axes are independent: Severity answers “how bad?” and Outcome answers “does this gate?” For recon-block findings specifically: status: not-started missing recon → Severity.HIGH + Outcome.WARNING (default) or Outcome.ERROR (--strict-recon); status: in-progressSeverity.MEDIUM + Outcome.WARNING; RECON_GRAPH_UNAVAILABLESeverity.LOW + Outcome.WARNING. Update to_markdown / to_json to render the outcome channel. Do NOT rename the Severity enum — LOW/MEDIUM/HIGH/CRITICAL is the established taxonomy and is consumed elsewhere in the package.

  • Rewrite the exit-code policy in scripts/plan_corpus/__main__.py. Replace:

    return 1 if all_findings else 0

    with:

    errors = [f for f in all_findings if f.outcome == Outcome.ERROR]
    return 1 if errors else 0

    Keep print/JSON output for ALL findings including warnings. Update check help text: 'Validate a file or directory (exits 1 only on findings with Outcome.ERROR; WARNING findings are printed but non-gating)'. Add a --strict-recon flag to the check subcommand parser. Plumbing path: __main__.py parses args.strict_recon → passes to load_and_validate(path, strict_recon=args.strict_recon)load_and_validate passes to validate(..., strict_recon=strict_recon)validate passes to the body_validator dispatch → _check_intel_recon_block(data, body, path, strict_recon=strict_recon). When strict_recon=True, the function constructs Finding(..., outcome=Outcome.ERROR) directly for status: not-started missing/stub recon — it does NOT mutate an existing Finding (Finding is frozen=True).

  • Refactor FILE_CLASS_META to carry a body-level validator in addition to the frontmatter validator. Two viable refactor shapes — §06.2 picks shape (a) with rationale documented inline:

    • (a) Extend FileClassMeta with a body_validator: Callable[[dict, str, Path, bool], list[Finding]] | None field (None for classes without body-level checks). The bool parameter is strict_recon. Update validate() signature to validate(file_class, data, body, path, *, strict_recon: bool = False) — calls both frontmatter and body validators sequentially and concatenates findings. discovery.load_and_validate(path, *, strict_recon: bool = False) already produces ValidatedFile.body (real field name per discovery.py:212); the call site passes it through along with strict_recon. Rationale: one dispatch mechanism, explicit per-class opt-in, no parallel phase. (Shape (b) — a post-schema body-check phase registered separately — is rejected because it duplicates the dispatch plumbing and would drift from the class-keyed registry that docgen already relies on.)
    • Update the validate() signature and EVERY call site (discovery.py:267, any direct callers). Use rg 'schema\.validate\(' scripts/ tests/ to find all call sites BEFORE editing; list them in the commit message. This is a - [x] item, not a deferral — signature propagation IS the work.
    • Update load_and_validate() signature to load_and_validate(path: Path, *, strict_recon: bool = False) and thread strict_recon through to validate().
    • Thread strict_recon from CLI: scripts/plan_corpus/__main__.py parses the --strict-recon flag and passes it down through load_and_validate(path, strict_recon=args.strict_recon)validate(..., strict_recon=strict_recon) → body_validator. Since Finding is frozen=True, the validator constructs Finding with the correct Outcome directly at creation time — NOT via mutation after construction.
    • For classes with body_validator = None (ROADMAP_SECTION, BUG_TRACKER_SECTION, FIX_BUG, the various overview / index classes), the extended dispatch is a no-op. Negative-pin tests (§06.2 matrix) confirm zero findings fire.
  • Implement _check_intel_recon_block(data: dict, body: str, path: Path, *, strict_recon: bool = False) -> list[Finding] in scripts/plan_corpus/schema.py. Attach it as the body_validator for FileClass.PLAN_SECTION only. Detection rules:

    • Missing block — no ^## Intelligence Reconnaissance\s*$ header found via re.search(..., re.MULTILINE) on body.

      • status: not-startedSeverity.HIGH, Outcome.WARNING by default; Severity.HIGH, Outcome.ERROR under --strict-recon
      • status: in-progressSeverity.MEDIUM, Outcome.WARNING (unaffected by --strict-recon)
      • status: complete → 0 findings (exempt)
      • FindingCategory.GAP, FindingSubtype.MISSING_RECON_BLOCK, message cites .claude/skills/dual-tpr/compose-intel-summary.md as the SSOT protocol
    • Graph-unavailable documentation block (header present AND body contains a date marker AND body contains one of: literal "graph unavailable", "graph was unavailable", "intelligence graph unavailable" — case-insensitive):

      • This is INTENTIONAL documentation, NOT a performative stub — the author ran the availability check and recorded that the graph was down
      • Severity.LOW, Outcome.WARNING (printed, never gates CI)
      • FindingCategory.GAP, FindingSubtype.RECON_GRAPH_UNAVAILABLE, message: "Section records graph-unavailable state at <date>. Fill in full queries if/when graph becomes available."
      • This shape PASSES (does NOT trigger VALIDATION_BYPASS); it is a distinct, lower-severity finding
    • Stub / performative-ritual block (header present but body fails one or more concrete-content checks AND does NOT qualify as a graph-unavailable documentation block):

      • Block body is empty / whitespace-only between the header and the next ^## (or end-of-file), OR
      • Block body contains only placeholder tokens. Tokens (case-insensitive, whole-token match): TBD, none, n/a, todo, (empty), ellipsis-only (..., ), OR
      • Block body fails ANY of the three concrete-content requirements:
        • (a) No literal scripts/intel-query.sh command line (matched via regex \bscripts/intel-query\.sh\b — must appear in the block body)
        • (b) No date marker in ISO format YYYY-MM-DD within the block (matched via regex \d{4}-\d{2}-\d{2})
        • (c) No concrete citation marker: no literal [ori], no cross-repo citation marker matching \[[a-z][a-z0-9-]*[#:][^\]]+\] (generic pattern — matches both [repo#123] issue citations AND [repo:path/to/symbol] symbol citations; avoids DRIFT when reference repos are added or when symbol results use the [repo:path] form permitted by compose-intel-summary.md Step D lines 78-80)
      • Mixed-placeholder-after-citation — block body passes the citation-marker check but the summary line immediately following the citation marker contains a placeholder token (TBD, TODO, n/a, none, (empty), ..., , XXX) within 20 characters. Detection: scan each summary line for the pattern \[[a-z][a-z0-9-]*[#:] (start of citation marker) within 20 characters of a placeholder token (case-insensitive). A line matching both is a ceremonial placeholder — the citation marker provides false cover but the content is not real. Emit GAP:VALIDATION_BYPASS. Example: [ori] TBD — citation marker [ori] is present but TBD follows within 5 characters.
      • Block body pastes the literal @.claude/skills/dual-tpr/compose-intel-summary.md directive verbatim without a condensed summary paragraph following it (the @-include is a SOURCE for Claude’s prompt, NOT a substitute for the plan-resident snapshot)
      • Severity / outcome mapping identical to “missing block” above
      • FindingCategory.GAP, FindingSubtype.VALIDATION_BYPASS, message names which specific check(s) failed (missing query / missing date / missing citation / placeholder-only / empty / mixed-placeholder-after-citation)
    • Complete block — header present AND body passes all concrete-content checks (or qualifies as graph-unavailable) → 0 VALIDATION_BYPASS findings

    Accepted body shapes (PASS / no VALIDATION_BYPASS):

    1. Full recon with scripts/intel-query.sh command + ISO date + citation marker → 0 findings
    2. Graph-unavailable note with ISO date + one of the unavailability phrases → RECON_GRAPH_UNAVAILABLE at Severity.LOW / Outcome.WARNING (distinct, non-gating)

    Rejected body shapes (FAIL / emit VALIDATION_BYPASS): 3. Empty — header present, body whitespace-only 4. Placeholder — body contains only TBD / none / n/a / todo / (empty) / ellipsis 5. Citation-free — has query and date, no [ori] / [repo#N] citation marker 6. Query-free — has date and citation, no scripts/intel-query.sh literal 7. Date-free — has query and citation, no ISO YYYY-MM-DD marker

    Block-body extraction: slurp from the line after the header to the next ^## or end-of-file. Strip whitespace and HTML comments (<!-- ... -->) before token / citation checks. HTML comments are metadata, not content.

  • Wire body through scripts/plan_corpus/discovery.py:load_and_validate. ValidatedFile already carries body (real field name per discovery.py:212); the dispatch to validate(...) at discovery.py:267 currently passes only frontmatter. Update the call site to pass body and strict_recon through per the new validate() signature. Use rg 'schema\.validate\(' scripts/ tests/ to find all call sites before editing.

  • Add discover per-plan recon-coverage reporter. After the existing per-plan summary, print a status-grouped table — §09 consumes this table to measure retrofit completeness:

    Per-plan recon coverage:
      plans/foo/                  — not-started: 3/5 PRESENCE   in-progress: 1/2 PRESENCE   complete: 4/4 exempt
      plans/bar/                  — not-started: 0/4 PRESENCE   in-progress: 0/0            complete: 0/0
      plans/query-intel-adoption/ — not-started: 4/4 PRESENCE   in-progress: 0/0            complete: 5/5 exempt

    Refactor choice (data source): The discover command currently uses discover_corpus() (discovery.py), which walks the tree and classifies files but does NOT parse bodies or run load_and_validate per file. For the recon-coverage reporter, the discover command MUST additionally call load_and_validate(path) on each PLAN_SECTION path in corpus.plan_sections.keys() — this provides body text + recon-block validation findings without a second filesystem walk (paths are already discovered). The discover reporter then READS ValidatedFile.violations (already populated by body_validator during load_and_validate) to count recon-block findings — it does NOT call _check_intel_recon_block directly. Alternative (b) — running a standalone regex or calling _check_intel_recon_block directly from discover — is rejected because load_and_validate / body_validator already ran the detection; calling it again would be duplicate work and would diverge if detection logic changes. There is no --strict-recon flag on discoverdiscover reports block PRESENCE (any shape including stubs counts as present), not block quality gating. Quality findings (stub vs. complete) live in check. Concrete implementation:

    1. In __main__.py discover subcommand handler, after building the Corpus, iterate corpus.plan_sections.keys() and call load_and_validate(path) on each
    2. For each successful LoadResult.ok, read ValidatedFile.violations to determine recon block presence and shape (missing = MISSING_RECON_BLOCK violation; stub = VALIDATION_BYPASS violation; graph-unavailable = RECON_GRAPH_UNAVAILABLE violation; complete = no recon violations)
    3. The coverage metric counts block PRESENCE: a block is “present” if no MISSING_RECON_BLOCK violation exists (stubs, graph-unavailable notes, and complete blocks all count as present). Quality issues are separate findings reported by check.
    4. Group results by plan directory and status; emit the table above
  • Write the representative matrix of body-level recon tests in tests/plan-audit/test_recon_block.py (new file, sibling of existing test_plan_corpus.py). Reuse the existing fixture harness pattern.

    Matrix: (FileClass) × (body-shape) × (severity-mode) — REPRESENTATIVE coverage (not exhaustive permutation). Every body-shape × FileClass combination has at least one default-mode pin and one —strict-recon pin where the modes diverge. Strict-mode pins for individual stub variants (no-query, no-date, no-citation) are tested via shared anti-stub detector dispatch — one strict-mode pin per shape covers all FileClass variants since the detector is FileClass-uniform. The exempt-class section covers representative body shapes; present-no-query, present-no-date, and graph-unavailable body shapes are not pinned for exempt classes (any such content is ignored — the class exemption is total).

    FileClassbody-shapestatus--strict-recon?Expected findings
    PLAN_SECTIONcomplete (header + queries + [ori] citation + summary)not-startedno0
    PLAN_SECTIONcompletein-progressno0
    PLAN_SECTIONcompletecompleteno0
    PLAN_SECTIONabsentnot-startedno1, Severity.HIGH, Outcome.WARNING
    PLAN_SECTIONabsentnot-startedyes1, Severity.HIGH, Outcome.ERROR
    PLAN_SECTIONabsentin-progressno1, Severity.MEDIUM, Outcome.WARNING
    PLAN_SECTIONabsentin-progressyes1, Severity.MEDIUM, Outcome.WARNING (strict only escalates not-started)
    PLAN_SECTIONabsentcompleteno0 (exempt)
    PLAN_SECTIONabsentcompleteyes0 (exempt; strict does not override complete exemption)
    PLAN_SECTIONstub-empty (header, whitespace body)not-startedno1, Severity.HIGH, Outcome.WARNING
    PLAN_SECTIONstub-placeholder (“TBD” / “none” / etc.)not-startedno1 (per token)
    PLAN_SECTIONstub-no-query (prose + date + citation but no scripts/intel-query.sh literal)not-startedno1, GAP:VALIDATION_BYPASS
    PLAN_SECTIONstub-no-date (query + citation but no YYYY-MM-DD date marker)not-startedno1, GAP:VALIDATION_BYPASS
    PLAN_SECTIONstub-no-citation (query + date but no [ori] / [repo#N] / [repo:path])not-startedno1, GAP:VALIDATION_BYPASS
    PLAN_SECTIONmixed-placeholder-after-citation (e.g. [ori] TBD — citation marker present but placeholder within 20 chars)not-startedno1, GAP:VALIDATION_BYPASS
    PLAN_SECTIONmixed-placeholder-after-citation ([ori] TBD)in-progressno1, Severity.MEDIUM, Outcome.WARNING
    PLAN_SECTIONcomplete with [repo:path] citation (e.g. [rust:compiler/rustc_errors/src/lib.rs])not-startedno0 (symbol-path citation is valid per \[[a-z][a-z0-9-]*[#:][^\]]+\])
    PLAN_SECTIONstub-only-@-include (directive pasted, no condensed paragraph)not-startedno1, GAP:VALIDATION_BYPASS
    PLAN_SECTIONgraph-unavailable (date + “graph unavailable” phrase, no query)not-startedno1, GAP:RECON_GRAPH_UNAVAILABLE, Severity.LOW, Outcome.WARNING
    PLAN_SECTIONgraph-unavailable (date + “graph unavailable” phrase, no query)not-startedyes1, GAP:RECON_GRAPH_UNAVAILABLE, Severity.LOW, Outcome.WARNING (—strict-recon does NOT escalate graph-unavailable)
    PLAN_SECTIONgraph-unavailable (date + “intelligence graph unavailable” phrase)in-progressno1, GAP:RECON_GRAPH_UNAVAILABLE, Severity.LOW, Outcome.WARNING
    PLAN_SECTIONstub-empty (header, whitespace body)in-progressno1, Severity.MEDIUM, Outcome.WARNING
    PLAN_SECTIONstub-placeholder (“TBD” / “none” / etc.)in-progressno1, Severity.MEDIUM, Outcome.WARNING
    PLAN_SECTIONstub-no-query (prose + date + citation but no scripts/intel-query.sh literal)in-progressno1, Severity.MEDIUM, Outcome.WARNING
    PLAN_SECTIONstub-no-date (query + citation but no YYYY-MM-DD date marker)in-progressno1, Severity.MEDIUM, Outcome.WARNING
    PLAN_SECTIONstub-no-citation (query + date but no [ori] / [repo#N] / [repo:path])in-progressno1, Severity.MEDIUM, Outcome.WARNING

    | Exempt-class negative pins — ROADMAP_SECTION | | | | | | ROADMAP_SECTION | absent | not-started | no | 0 (exempt — out of scope) | | ROADMAP_SECTION | absent | not-started | yes | 0 (exempt — out of scope) | | ROADMAP_SECTION | present-empty | not-started | no | 0 (exempt) | | ROADMAP_SECTION | present-empty | not-started | yes | 0 (exempt) | | ROADMAP_SECTION | present-placeholder | not-started | no | 0 (exempt) | | ROADMAP_SECTION | present-no-citation | not-started | no | 0 (exempt) | | ROADMAP_SECTION | present-no-query | not-started | no | 0 (exempt) | | ROADMAP_SECTION | present-no-date | not-started | no | 0 (exempt) | | ROADMAP_SECTION | graph-unavailable | not-started | no | 0 (exempt) | | ROADMAP_SECTION | absent | not-started | yes (—strict-recon) | 0 (exempt; strict does not affect exempt classes) | | Exempt-class negative pins — BUG_TRACKER_SECTION | | | | | | BUG_TRACKER_SECTION | absent | not-started | no | 0 (exempt — out of scope) | | BUG_TRACKER_SECTION | absent | not-started | yes | 0 (exempt — out of scope) | | BUG_TRACKER_SECTION | present-empty | not-started | no | 0 (exempt) | | BUG_TRACKER_SECTION | present-empty | not-started | yes | 0 (exempt) | | BUG_TRACKER_SECTION | present-placeholder | not-started | no | 0 (exempt) | | BUG_TRACKER_SECTION | present-no-citation | not-started | no | 0 (exempt) | | BUG_TRACKER_SECTION | present-no-query | not-started | no | 0 (exempt) | | BUG_TRACKER_SECTION | present-no-date | not-started | no | 0 (exempt) | | BUG_TRACKER_SECTION | graph-unavailable | not-started | no | 0 (exempt) | | BUG_TRACKER_SECTION | absent | not-started | yes (—strict-recon) | 0 (exempt; strict does not affect exempt classes) | | Exempt-class negative pins — FIX_BUG | | | | | | FIX_BUG | absent | not-started | no | 0 (exempt — different template) | | FIX_BUG | absent | not-started | yes | 0 (exempt — different template) | | FIX_BUG | present-empty | not-started | no | 0 (exempt) | | FIX_BUG | present-empty | not-started | yes | 0 (exempt) | | FIX_BUG | present-placeholder | not-started | no | 0 (exempt) | | FIX_BUG | present-no-citation | not-started | no | 0 (exempt) | | FIX_BUG | present-no-query | not-started | no | 0 (exempt) | | FIX_BUG | present-no-date | not-started | no | 0 (exempt) | | FIX_BUG | graph-unavailable | not-started | no | 0 (exempt) | | FIX_BUG | absent | not-started | yes (—strict-recon) | 0 (exempt; strict does not affect exempt classes) | | Exit-code tests | | | | | | Exit code — warnings-only corpus, default mode | — | — | no | main() returns 0 | | Exit code — warnings-only corpus, --strict-recon with not-started missing-recon | — | not-started | yes | main() returns 1 | | Exit code — corpus with one Severity.HIGH compound finding | — | — | no | main() returns 1 |

    Each test asserts exact finding counts, severities, outcomes, category / subtype strings, and for exit-code tests, the __main__.py main() return value. Fixtures live under tests/plan-audit/fixtures/recon_block/; use tests/plan-audit/test_plan_corpus.py’s fixture conventions.

  • Subsection close-out (06.2) — MANDATORY before section close:

    • Validator refactor + matrix tests land; all new tests pass via pytest tests/plan-audit/test_recon_block.py
    • ./test-all.sh green (no regressions in existing plan-audit or Rust test suites)
    • python -m scripts.plan_corpus check plans/query-intel-adoption/section-06-plan-schema-recon.md returns exit 0 (this file has a non-stub recon block above)
    • python -m scripts.plan_corpus check plans/query-intel-adoption/section-07-pre-review-intel-hook.md returns exit 1 under --strict-recon with one Severity.HIGH / Outcome.ERROR finding (no recon block yet; status: not-started) — end-to-end demo of the strict path. Without --strict-recon: exit 0 with one WARNING finding printed.
    • python -m scripts.plan_corpus check plans/roadmap/section-00-parser.md returns exit 0 with ZERO recon-related findings — ROADMAP_SECTION is out of scope (negative-pin check).
    • python -m scripts.plan_corpus check plans/bug-tracker/fix-BUG-*.md returns exit 0 with ZERO recon-related findings across the directory — FIX_BUG is out of scope.
    • CLI-entrypoint DRIFT count from the grep step is zero post-edit
    • Update 06.2 status to complete
    • Run /improve-tooling retrospectively on 06.2 — does the validator error message include a pointer to the SSOT and the format-coupling contract? Minimum text: "Section lacks non-stub '## Intelligence Reconnaissance' block. See '.claude/skills/create-plan/plan-schema.md' MANDATORY SECTION STRUCTURE; run queries per '.claude/skills/dual-tpr/compose-intel-summary.md'; summary format must use [ori] / [repo#N] / [repo:path] citation grammar (Step D, lines 64-82). If the graph is unavailable, record the unavailability as freeform prose with the date — do NOT omit the block." Commit via build(tooling): improve plan_corpus recon-block error messages — surfaced by section-06.2 retrospective.
    • Run /sync-claude on 06.2 — CLAUDE.md §Commands “Plan corpus” bullet (line ~167) describes plan_corpus check. Update to mention the WARNING/ERROR outcome model, status-gated severity, and --strict-recon flag. Verify no .claude/rules/*.md file contradicts the new exit-code policy.
    • Repo hygiene checkdiagnostics/repo-hygiene.sh --check → clean.

06.R Third Party Review Findings

Round 1 findings (2026-04-15, codex-only — gemini unavailable: 3 consecutive gemini_api_capacity / 429 rateLimit failures across transport attempts 1–3):

  • [TPR-06-001-codex][high] scripts/plan_corpus/schema.py:505[GAP] Narrow mixed-placeholder detection so natural-language ‘None …’ summaries stay valid. Evidence: _RECON_PLACEHOLDER_PATTERN includes \bNONE\b, and _RECON_MIXED_CITATION_RE applies that placeholder set to any token within 20 characters of a citation marker. A block with real query + ISO date + [ori] None of the callers of validate live outside scripts/plan_corpus. emits VALIDATION_BYPASS instead of accepting the block. Complete body reclassified as stub. Resolved: Fixed on 2026-04-15. Split _RECON_PLACEHOLDER_PATTERN into _RECON_WHOLE_BODY_PLACEHOLDER_PATTERN (all tokens including NONE/N/A — used for whole-body stub detection) and _RECON_MIXED_PLACEHOLDER_PATTERN (strict tokens only: TBD/TODO/XXX/(empty)/…/… — used for mixed-placeholder-after-citation). Also tightened the mixed-citation regex gap from [^\n]{0,20}? to [\s:;—–\-]{0,4} so only standalone stub shapes match. Regression tests TestNaturalNoneAcceptedAfterCitation.test_ori_none_of_callers_prose_passes, test_ori_n_a_in_natural_prose_passes, test_strict_stub_tokens_still_detected pin the correct behavior.

  • [TPR-06-002-codex][medium] scripts/plan_corpus/schema.py:498[DRIFT] Tighten repo citation regex to the exact Step D issue/path grammar. Evidence: _RECON_REPO_CITATION_RE = \[[a-z][a-z0-9-]*[#:][^\]]+\] accepts any non-] payload after #/:, so malformed issue shorthand like [rust#abc] satisfies has_citation. Step D allows [repo#N] (numeric issue) and [repo:path] (symbol), not arbitrary text after #. Resolved: Fixed on 2026-04-15. Split into _RECON_REPO_ISSUE_CITATION_RE = r"\[[a-z][a-z0-9-]*#\d+\]" (numeric issue id required) and _RECON_REPO_PATH_CITATION_RE = r"\[[a-z][a-z0-9-]*:[^\]\s][^\]]*\]" (non-empty path required). Negative pins TestRepoCitationGrammarStrict.test_malformed_issue_citation_rejected ([rust#abc]), test_empty_issue_citation_rejected ([rust#]), test_empty_path_citation_rejected ([rust:]), plus positive pin test_valid_numeric_issue_citation_accepted ([rust#12345]) clamp the correct grammar.

  • [TPR-06-003-codex][medium] scripts/plan_corpus/schema.py:599[GAP] Restrict graph-unavailable classification so full recon blocks are not downgraded. Evidence: Graph-unavailable branch fires before concrete-content checks and only requires has_date plus one of three phrases anywhere in the block. A block with real query + date + valid [ori] citation that also contains the sentence This section discusses how graph unavailable notes should be handled [ori]. emits RECON_GRAPH_UNAVAILABLE instead of zero findings. Phrase mention shadows otherwise complete shape. Resolved: Fixed on 2026-04-15. Added and not _RECON_QUERY_RE.search(block) to the graph-unavailable branch — now the subtype is emitted ONLY when the block is genuinely substituting for full recon (no real query). Positive pin TestGraphUnavailablePrecedenceNarrowed.test_complete_block_mentioning_phrase_still_complete verifies a complete block with the phrase in prose passes; negative pin test_genuine_graph_unavailable_still_detected confirms the legitimate graph-unavailable case still triggers.

  • [TPR-06-004-codex][medium] tests/plan-audit/test_recon_block.py:834[GAP] Replace fixture-list introspection with a real self-verifying matrix count. Evidence: TestMatrixCompleteness does not prove the claimed FileClass × body-shape × status × strict_recon coverage. Only asserts STUB_SHAPES ≥ 7 labels and representative names in EXEMPT_BODY_SHAPES. No expected-cell count, no collected-test count pin, no extractor-edge coverage for HTML-comment stripping, next-## subsection boundaries, or multi-header files (even though _extract_recon_block() is load-bearing). Resolved: Fixed on 2026-04-15. Rewrote TestMatrixCompleteness with explicit _EXPECTED_STUB_SHAPES cell table, added test_stub_shape_set_matches_expected (drift gate) and test_stub_plan_section_cell_count_matches_expected (inspects TestPlanSectionStubBlocks’s parametrize marks, asserts total invocations == len(STUB_SHAPES) * 3). Added new TestExtractorEdgeCases class with three pins: HTML-comment stripping, next-## section truncation, multi-header first-only extraction.

  • [TPR-06-005-codex][low] scripts/plan_corpus/docgen.py:113[DRIFT] Generate Outcome documentation alongside the new finding taxonomy. Evidence: Outcome is now a first-class SSOT enum in types.py, rendered in Finding.to_json()/to_markdown(), drives CLI exit policy. But generate_schema_reference() emits only status enums, file classes, and finding-category subtype lists. Committed docs/internal/plan-schema-reference.md has the three new recon subtypes but no Outcome documentation. Resolved: Fixed on 2026-04-15. Added ## Finding Severity and ## Finding Outcome sections to generate_schema_reference() with the full Outcome enum + the Severity/Outcome orthogonality contract + the Finding.outcome default + the id-hash exclusion rule. Regenerated docs/internal/plan-schema-reference.md; docgen drift gate passes.

  • [TPR-06-006-codex][informational] scripts/plan_corpus/schema.py:549[NOTE] Either enforce the ≤500-char recon bound or remove it from the validator contract. Evidence: _check_intel_recon_block() has no length check; test suite has no over-limit case; discover has no informational reporting path for oversized summaries. Yet the plan frames the §03/§06/§07 contract in terms of the 500-char cap. Resolved: Fixed on 2026-04-15. Chose the “demote to non-validated guidance” option — §06.2 intentionally treats the 500-char bound as a SOFT contract for §07 hook authors and §06.1 template users, not a validator contract (the bound applies to the condensed summary output shape, which discover cannot structurally parse without a precision tax). Updated plan line 133 to remove the incorrect claim that discover flags over-500 blocks informationally, and added explicit “the ≤500-char bound is not validator-enforced anywhere, so no enforcement drift is possible.” The contract surfaces now all agree: §07 hook owns its own length enforcement; §06 plan-resident artifacts are author-curated.

  • [TPR-06-007-codex][low] tests/plan-audit/test_plan_corpus.py:2[DRIFT] Finish the legacy CLI scrub by removing the stale single-file path from active tests. Evidence: §06.2 scrub left tests/plan-audit/test_plan_corpus.py describing the legacy single-file path in its docstring even though the package-only form has been the SSOT since the split. Active test code, not one of the three intentionally preserved frozen status: complete plan sections. Resolved: Fixed on 2026-04-15. Updated the test file’s module docstring to describe scripts/plan_corpus/ as a package. Verified via grep -rlE "scripts/plan_corpus\.py" tests/ — zero hits.

  • [TPR-06-008-codex][informational] scripts/plan_corpus/__main__.py:122[NOTE] Keep print(ref, end="") because plain print(ref) really does self-drift redirected docgen output. Evidence: generate_schema_reference() returns a string ending in \n. print(ref) appends another newline, so redirecting to a file writes \n\n at EOF while docgen --check compares against the single-newline generator output. Real byte-parity bug, confirmed. Resolved: Fixed on 2026-04-15. Added regression test class TestDocgenByteParityWithShellRedirect with two pins: test_shell_redirect_bytes_match_generator_output (compares subprocess stdout to generate_schema_reference() return value) and test_docgen_check_immediately_after_regeneration_passes (end-to-end: regenerate via redirect, run docgen --check, must pass). Both previously would have failed without end="".

Gemini envelope: Not produced. Three consecutive 429 rateLimit failures against gemini-3.1-pro-preview on 2026-04-15 ~09:50-10:12 EDT. Transport exhausted 3/5 infra retries on whole-round gemini failures; attempt 3 was SIGTERM’d mid-flight. Single-source (codex-only) review accepted per /tpr-review §Transport Failure Handling because findings are real, verified against the code, and match the objective. Re-run MUST attempt dual-source after fixes land — codex-only is not a valid terminal clean pass.