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_SECTIONfiles already use## {NN}.0for 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_SECTIONandFIX_BUGuse a separate template (1. Root Cause / 2. TDD / 3. Implementation / ...perplan-schema.md:789). The{NN}.Xnumbering does not apply, andfix-BUG-*.mdfiles already run the full reconnaissance workflow through/fix-bugPhase 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: trueandparallel: trueare INDEX-level properties, not file classes — the ordinary section files under such indices are stillPLAN_SECTIONand 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 atFinding-construction time. - Default mode:
status: not-startedmissing recon →Severity.HIGH+Outcome.WARNING.status: in-progressmissing recon →Severity.MEDIUM+Outcome.WARNING. --strict-reconmode:status: not-startedmissing recon →Severity.HIGH+Outcome.ERROR(Severity unchanged; Outcome rewritten).status: in-progressis 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.mdexisting 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(PlanSectionSchemastrict allowlist),discovery.py(load_and_validate/ValidatedFile),__main__.py(check/discover/docgensubcommands) — §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}.0headers inplans/— motivation for the unnumbered design - Ori
plans/roadmap/section-00-parser.md:67— roadmap-side.0collision 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-toolingscripts/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-modulescripts/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 thatscripts/plan_corpus/schema.py:487 validate(fc, data, path)takes only frontmatter — body_text is produced indiscovery.py:load_and_validateand currently NEVER reaches validators.grep -rEn '^## \d+\.0\s' plans/— 23 existing plan sections use## {NN}.0for 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_blockcovers: (a) presence of citation markers ([ori],[repo#N],[repo:path]), (b) presence of date marker (ISO YYYY-MM-DD), (c) presence of literalscripts/intel-query.shcommand line, (d) absence of mixed-placeholder shapes (per Fix 1 — mixed-placeholder-after-citation emitsGAP: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 bydiscovereither. §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 (percompose-intel-summary.mdlines 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 asRECON_GRAPH_UNAVAILABLEatSeverity.LOW/Outcome.WARNING(intentional documentation, NOT a VALIDATION_BYPASS). Drift in the citation vocabulary among the three surfaces is aDRIFT:scattered-knowledgefinding (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 thesections:list. Add a one-line comment near thesections: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
grepthat no other.claude/file independently re-asserts subsection structure. Command:grep -rn "EVERY subsection\|{NN}.1, {NN}.2" .claude/. Expected post-edit: onlyplan-schema.mdcontains 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.mdrenders with the unnumbered## Intelligence Reconnaissanceblock in the canonical example AND the scope note (PLAN_SECTION only) in the MANDATORY SECTION STRUCTURE comment -
create-plan/SKILL.md:808updated 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.mdstill 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.1status tocomplete - Run
/improve-toolingretrospectively on 06.1 — was editing two SSOT surfaces in lockstep painful enough to warrant a smallscripts/helper that diff-greps for subsection-structure re-assertions? If yes, add it. Commit viabuild(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-claudeon 06.1 —plan-schema.mdis 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/*.mdfile references a pre-package single-file.pypath or the old{NN}.0proposal. - Repo hygiene check —
diagnostics/repo-hygiene.sh --check→ clean.
- Template changes land;
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:
- Body text does not reach validators.
scripts/plan_corpus/schema.py:487—validate(fc, data, path)takes only frontmatter.scripts/plan_corpus/discovery.py:268 load_and_validate(path)splits body text intoValidatedFile.body(real field name perdiscovery.py:212) but never passes it into the validator dispatch. Body-level recon detection requires plumbing or a parallel phase. - No WARNING/ERROR outcome model.
scripts/plan_corpus/__main__.py:37doesreturn 1 if all_findings else 0. Severity distinctions are not expressible as exit codes. - No status-gated severity. Validators receive frontmatter
databut don’t branch ondata.get("status")when emitting recon-block findings. - 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
.pyform of the package (with the.pysuffix appended directly toscripts/plan_corpus— the single-file form does NOT exist; the package is invoked aspython -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 justsection-*.md— so00-overview.mdandindex.mdfiles under active plan directories are included), excludingplans/completed/AND excluding files whose frontmatter isstatus: complete. Do NOT editplans/completed/files ORstatus: completesections 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 excludeplans/completed/AND any file whose frontmatter includesstatus: 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: completesection-.md files) should NOT be edited — they are frozen artifacts. Only skip theplans/completed/directory wholesale plus any individual section whose frontmatter isstatus: complete. -
Add
MISSING_RECON_BLOCK,VALIDATION_BYPASS, andRECON_GRAPH_UNAVAILABLEto theFindingSubtypeenum inscripts/plan_corpus/types.py(around line 85). Register all three underFindingCategory.GAPin the_CATEGORY_SUBTYPESdict (around line 153, within theFindingCategory.GAP: frozenset({...})block). The validator then emits:Finding(category=FindingCategory.GAP, subtype=FindingSubtype.MISSING_RECON_BLOCK, ...)for entirely missing blocksFinding(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,Findingconstruction raisesValueErrorbecauseFindingSubtypeis an enum and_CATEGORY_SUBTYPESvalidation rejects unregistered members.
-
Add
Outcomeenum toscripts/plan_corpus/types.py. Distinct axis fromSeverity: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 1Add
outcome: Outcome = Outcome.ERRORas a default field on theFindingdataclass. The default isOutcome.ERROR— this ensures ALL existingFinding(...)callsites (schema violations, parse errors, unknown frontmatter keys, etc.) continue to emitOutcome.ERRORand gate CI without requiring edits to every existing callsite. Only the new recon-block-specific findings explicitly useOutcome.WARNING. Outcome is set EXPLICITLY by each new emitter atFinding-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-startedmissing recon →Severity.HIGH+Outcome.WARNING(default) orOutcome.ERROR(--strict-recon);status: in-progress→Severity.MEDIUM+Outcome.WARNING;RECON_GRAPH_UNAVAILABLE→Severity.LOW+Outcome.WARNING. Updateto_markdown/to_jsonto render the outcome channel. Do NOT rename theSeverityenum —LOW/MEDIUM/HIGH/CRITICALis 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 0with:
errors = [f for f in all_findings if f.outcome == Outcome.ERROR] return 1 if errors else 0Keep print/JSON output for ALL findings including warnings. Update
checkhelp text:'Validate a file or directory (exits 1 only on findings with Outcome.ERROR; WARNING findings are printed but non-gating)'. Add a--strict-reconflag to thechecksubcommand parser. Plumbing path:__main__.pyparsesargs.strict_recon→ passes toload_and_validate(path, strict_recon=args.strict_recon)→load_and_validatepasses tovalidate(..., strict_recon=strict_recon)→validatepasses to thebody_validatordispatch →_check_intel_recon_block(data, body, path, strict_recon=strict_recon). Whenstrict_recon=True, the function constructsFinding(..., outcome=Outcome.ERROR)directly forstatus: not-startedmissing/stub recon — it does NOT mutate an existing Finding (Finding isfrozen=True). -
Refactor
FILE_CLASS_METAto 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
FileClassMetawith abody_validator: Callable[[dict, str, Path, bool], list[Finding]] | Nonefield (None for classes without body-level checks). Theboolparameter isstrict_recon. Updatevalidate()signature tovalidate(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 producesValidatedFile.body(real field name perdiscovery.py:212); the call site passes it through along withstrict_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). Userg '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 toload_and_validate(path: Path, *, strict_recon: bool = False)and threadstrict_reconthrough tovalidate(). - Thread
strict_reconfrom CLI:scripts/plan_corpus/__main__.pyparses the--strict-reconflag and passes it down throughload_and_validate(path, strict_recon=args.strict_recon)→validate(..., strict_recon=strict_recon)→ body_validator. SinceFindingisfrozen=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.
- (a) Extend
-
Implement
_check_intel_recon_block(data: dict, body: str, path: Path, *, strict_recon: bool = False) -> list[Finding]inscripts/plan_corpus/schema.py. Attach it as thebody_validatorforFileClass.PLAN_SECTIONonly. Detection rules:-
Missing block — no
^## Intelligence Reconnaissance\s*$header found viare.search(..., re.MULTILINE)onbody.status: not-started→Severity.HIGH,Outcome.WARNINGby default;Severity.HIGH,Outcome.ERRORunder--strict-reconstatus: in-progress→Severity.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.mdas 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.shcommand line (matched via regex\bscripts/intel-query\.sh\b— must appear in the block body) - (b) No date marker in ISO format
YYYY-MM-DDwithin 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 bycompose-intel-summary.mdStep D lines 78-80)
- (a) No literal
- 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. EmitGAP:VALIDATION_BYPASS. Example:[ori] TBD— citation marker[ori]is present butTBDfollows within 5 characters. - Block body pastes the literal
@.claude/skills/dual-tpr/compose-intel-summary.mddirective 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)
- Block body is empty / whitespace-only between the header and the next
-
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):
- Full recon with
scripts/intel-query.shcommand + ISO date + citation marker → 0 findings - Graph-unavailable note with ISO date + one of the unavailability phrases →
RECON_GRAPH_UNAVAILABLEatSeverity.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, noscripts/intel-query.shliteral 7. Date-free — has query and citation, no ISOYYYY-MM-DDmarkerBlock-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.ValidatedFilealready carriesbody(real field name perdiscovery.py:212); the dispatch tovalidate(...)atdiscovery.py:267currently passes only frontmatter. Update the call site to passbodyandstrict_reconthrough per the newvalidate()signature. Userg 'schema\.validate\(' scripts/ tests/to find all call sites before editing. -
Add
discoverper-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 exemptRefactor choice (data source): The
discovercommand currently usesdiscover_corpus()(discovery.py), which walks the tree and classifies files but does NOT parse bodies or runload_and_validateper file. For the recon-coverage reporter, thediscovercommand MUST additionally callload_and_validate(path)on eachPLAN_SECTIONpath incorpus.plan_sections.keys()— this provides body text + recon-block validation findings without a second filesystem walk (paths are already discovered). Thediscoverreporter then READSValidatedFile.violations(already populated bybody_validatorduringload_and_validate) to count recon-block findings — it does NOT call_check_intel_recon_blockdirectly. Alternative (b) — running a standalone regex or calling_check_intel_recon_blockdirectly fromdiscover— is rejected becauseload_and_validate/body_validatoralready ran the detection; calling it again would be duplicate work and would diverge if detection logic changes. There is no--strict-reconflag ondiscover—discoverreports block PRESENCE (any shape including stubs counts as present), not block quality gating. Quality findings (stub vs. complete) live incheck. Concrete implementation:- In
__main__.pydiscoversubcommand handler, after building theCorpus, iteratecorpus.plan_sections.keys()and callload_and_validate(path)on each - For each successful
LoadResult.ok, readValidatedFile.violationsto determine recon block presence and shape (missing =MISSING_RECON_BLOCKviolation; stub =VALIDATION_BYPASSviolation; graph-unavailable =RECON_GRAPH_UNAVAILABLEviolation; complete = no recon violations) - The coverage metric counts block PRESENCE: a block is “present” if no
MISSING_RECON_BLOCKviolation exists (stubs, graph-unavailable notes, and complete blocks all count as present). Quality issues are separate findings reported bycheck. - Group results by plan directory and status; emit the table above
- In
-
Write the representative matrix of body-level recon tests in
tests/plan-audit/test_recon_block.py(new file, sibling of existingtest_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, andgraph-unavailablebody shapes are not pinned for exempt classes (any such content is ignored — the class exemption is total).FileClass body-shape status --strict-recon?Expected findings PLAN_SECTION complete (header + queries + [ori]citation + summary)not-started no 0 PLAN_SECTION complete in-progress no 0 PLAN_SECTION complete complete no 0 PLAN_SECTION absent not-started no 1, Severity.HIGH, Outcome.WARNING PLAN_SECTION absent not-started yes 1, Severity.HIGH, Outcome.ERROR PLAN_SECTION absent in-progress no 1, Severity.MEDIUM, Outcome.WARNING PLAN_SECTION absent in-progress yes 1, Severity.MEDIUM, Outcome.WARNING (strict only escalates not-started) PLAN_SECTION absent complete no 0 (exempt) PLAN_SECTION absent complete yes 0 (exempt; strict does not override complete exemption) PLAN_SECTION stub-empty (header, whitespace body) not-started no 1, Severity.HIGH, Outcome.WARNING PLAN_SECTION stub-placeholder (“TBD” / “none” / etc.) not-started no 1 (per token) PLAN_SECTION stub-no-query (prose + date + citation but no scripts/intel-query.shliteral)not-started no 1, GAP:VALIDATION_BYPASS PLAN_SECTION stub-no-date (query + citation but no YYYY-MM-DD date marker) not-started no 1, GAP:VALIDATION_BYPASS PLAN_SECTION stub-no-citation (query + date but no [ori]/[repo#N]/[repo:path])not-started no 1, GAP:VALIDATION_BYPASS PLAN_SECTION mixed-placeholder-after-citation (e.g. [ori] TBD— citation marker present but placeholder within 20 chars)not-started no 1, GAP:VALIDATION_BYPASS PLAN_SECTION mixed-placeholder-after-citation ( [ori] TBD)in-progress no 1, Severity.MEDIUM, Outcome.WARNING PLAN_SECTION complete with [repo:path]citation (e.g.[rust:compiler/rustc_errors/src/lib.rs])not-started no 0 (symbol-path citation is valid per \[[a-z][a-z0-9-]*[#:][^\]]+\])PLAN_SECTION stub-only-@-include (directive pasted, no condensed paragraph) not-started no 1, GAP:VALIDATION_BYPASS PLAN_SECTION graph-unavailable (date + “graph unavailable” phrase, no query) not-started no 1, GAP:RECON_GRAPH_UNAVAILABLE, Severity.LOW, Outcome.WARNING PLAN_SECTION graph-unavailable (date + “graph unavailable” phrase, no query) not-started yes 1, GAP:RECON_GRAPH_UNAVAILABLE, Severity.LOW, Outcome.WARNING (—strict-recon does NOT escalate graph-unavailable) PLAN_SECTION graph-unavailable (date + “intelligence graph unavailable” phrase) in-progress no 1, GAP:RECON_GRAPH_UNAVAILABLE, Severity.LOW, Outcome.WARNING PLAN_SECTION stub-empty (header, whitespace body) in-progress no 1, Severity.MEDIUM, Outcome.WARNING PLAN_SECTION stub-placeholder (“TBD” / “none” / etc.) in-progress no 1, Severity.MEDIUM, Outcome.WARNING PLAN_SECTION stub-no-query (prose + date + citation but no scripts/intel-query.shliteral)in-progress no 1, Severity.MEDIUM, Outcome.WARNING PLAN_SECTION stub-no-date (query + citation but no YYYY-MM-DD date marker) in-progress no 1, Severity.MEDIUM, Outcome.WARNING PLAN_SECTION stub-no-citation (query + date but no [ori]/[repo#N]/[repo:path])in-progress no 1, 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-reconwith 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 undertests/plan-audit/fixtures/recon_block/; usetests/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.shgreen (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.mdreturns 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.mdreturns exit 1 under--strict-reconwith 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.mdreturns 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-*.mdreturns 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.2status tocomplete - Run
/improve-toolingretrospectively 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 viabuild(tooling): improve plan_corpus recon-block error messages — surfaced by section-06.2 retrospective. - Run
/sync-claudeon 06.2 — CLAUDE.md §Commands “Plan corpus” bullet (line ~167) describesplan_corpus check. Update to mention the WARNING/ERROR outcome model, status-gated severity, and--strict-reconflag. Verify no.claude/rules/*.mdfile contradicts the new exit-code policy. - Repo hygiene check —
diagnostics/repo-hygiene.sh --check→ clean.
- Validator refactor + matrix tests land; all new tests pass via
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_PATTERNincludes\bNONE\b, and_RECON_MIXED_CITATION_REapplies 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.emitsVALIDATION_BYPASSinstead of accepting the block. Complete body reclassified as stub. Resolved: Fixed on 2026-04-15. Split_RECON_PLACEHOLDER_PATTERNinto_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 testsTestNaturalNoneAcceptedAfterCitation.test_ori_none_of_callers_prose_passes,test_ori_n_a_in_natural_prose_passes,test_strict_stub_tokens_still_detectedpin 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]satisfieshas_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 pinsTestRepoCitationGrammarStrict.test_malformed_issue_citation_rejected([rust#abc]),test_empty_issue_citation_rejected([rust#]),test_empty_path_citation_rejected([rust:]), plus positive pintest_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 requireshas_dateplus one of three phrases anywhere in the block. A block with real query + date + valid[ori]citation that also contains the sentenceThis section discusses how graph unavailable notes should be handled [ori].emitsRECON_GRAPH_UNAVAILABLEinstead of zero findings. Phrase mention shadows otherwise complete shape. Resolved: Fixed on 2026-04-15. Addedand 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 pinTestGraphUnavailablePrecedenceNarrowed.test_complete_block_mentioning_phrase_still_completeverifies a complete block with the phrase in prose passes; negative pintest_genuine_graph_unavailable_still_detectedconfirms 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:TestMatrixCompletenessdoes not prove the claimedFileClass × body-shape × status × strict_reconcoverage. Only assertsSTUB_SHAPES≥ 7 labels and representative names inEXEMPT_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. RewroteTestMatrixCompletenesswith explicit_EXPECTED_STUB_SHAPEScell table, addedtest_stub_shape_set_matches_expected(drift gate) andtest_stub_plan_section_cell_count_matches_expected(inspectsTestPlanSectionStubBlocks’s parametrize marks, asserts total invocations ==len(STUB_SHAPES) * 3). Added newTestExtractorEdgeCasesclass 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:Outcomeis now a first-class SSOT enum intypes.py, rendered inFinding.to_json()/to_markdown(), drives CLI exit policy. Butgenerate_schema_reference()emits only status enums, file classes, and finding-category subtype lists. Committeddocs/internal/plan-schema-reference.mdhas the three new recon subtypes but no Outcome documentation. Resolved: Fixed on 2026-04-15. Added## Finding Severityand## Finding Outcomesections togenerate_schema_reference()with the full Outcome enum + the Severity/Outcome orthogonality contract + theFinding.outcomedefault + the id-hash exclusion rule. Regenerateddocs/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;discoverhas 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, whichdiscovercannot structurally parse without a precision tax). Updated plan line 133 to remove the incorrect claim thatdiscoverflags 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 lefttests/plan-audit/test_plan_corpus.pydescribing 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 frozenstatus: completeplan sections. Resolved: Fixed on 2026-04-15. Updated the test file’s module docstring to describescripts/plan_corpus/as a package. Verified viagrep -rlE "scripts/plan_corpus\.py" tests/— zero hits. -
[TPR-06-008-codex][informational]scripts/plan_corpus/__main__.py:122—[NOTE]Keepprint(ref, end="")because plainprint(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\nat EOF whiledocgen --checkcompares against the single-newline generator output. Real byte-parity bug, confirmed. Resolved: Fixed on 2026-04-15. Added regression test classTestDocgenByteParityWithShellRedirectwith two pins:test_shell_redirect_bytes_match_generator_output(compares subprocess stdout togenerate_schema_reference()return value) andtest_docgen_check_immediately_after_regeneration_passes(end-to-end: regenerate via redirect, rundocgen --check, must pass). Both previously would have failed withoutend="".
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.