fix(aprender-train): matmul #[should_panic] expected — unblocks queue#862
Merged
Conversation
The matmul-v1 contract macro now panics with
"Contract [matmul] Pre-condition violated: a.len() == m * k"
BEFORE the function body's assert_eq!("Matrix A size mismatch") can
fire, so test_matmul_size_mismatch_{a,b} with
`expected = "size mismatch"` fail with "panic did not contain expected
string".
Update the expected substring to match the contract macro's message.
Every PR against main has been failing workspace-test on this regression;
this unblocks the queue.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
noahgift
added a commit
that referenced
this pull request
Apr 18, 2026
PR #862 is a matmul test fix, not the MCP M1 skeleton. The correct skeleton PR is #864 (`feat(mcp): apr mcp M1 skeleton — MCP server over stdio`). All three stale citations in the M1 milestone replaced. Five-whys root cause: the spec retrofit (#873) reconstructed PR numbers from memory; future retrofits should verify against `git log --grep=...` before committing. Refs PMAT-037.
noahgift
added a commit
that referenced
this pull request
Apr 18, 2026
PR #862 is a matmul test fix, not the MCP M1 skeleton. The correct skeleton PR is #864 (`feat(mcp): apr mcp M1 skeleton — MCP server over stdio`). All three stale citations in the M1 milestone replaced. Five-whys root cause: the spec retrofit (#873) reconstructed PR numbers from memory; future retrofits should verify against `git log --grep=...` before committing. Refs PMAT-037.
noahgift
added a commit
that referenced
this pull request
Apr 19, 2026
…888) * docs(mcp): spec v1.1.0 — DRAFT → ACTIVE; M1–M3 shipped - Status: DRAFT (pre-implementation) → ACTIVE (M1–M3 shipped; M4 dogfood pending) - Falsification conditions table now annotates ENFORCED / PARTIAL / Deferred per shipped test mappings in contracts/apr-mcp-server-v1.yaml (#886) - Adds FALSIFY-MCP-PROGRESS-001 entry for #887 progress-notification gate - Milestones M1/M2/M3 marked SHIPPED with PR cross-references - M4 acceptance items remain open (real-model gates, dogfood) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp): align spec output shapes with CLI reality (PR #889 falsifications) PR #889 added mock-subprocess e2e tests for FALSIFY-MCP-003/-004 and discovered two spec-vs-CLI mismatches via test failures: 1. apr.run: spec listed `stop_reason` in output — CLI's print_run_output (crates/apr-cli/src/commands/run_entry.rs:279) does NOT emit it. Spec corrected to the actual emitted set (model, text, tokens, ...). 2. apr.qa: spec wrote gates as `{pass, value, threshold}` — CLI's GateResult (crates/apr-cli/src/commands/qa.rs:368) uses `passed` not `pass`. Spec corrected. Also fixes the codegen source reference: FALSIFY-MCP-008 uses contracts/apr-mcp-tool-schemas-v1.yaml (PR #871), not apr-cli-commands-v1.yaml. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(mcp-spec): M1 PR refs #862 → #864 PR #862 is a matmul test fix, not the MCP M1 skeleton. The correct skeleton PR is #864 (`feat(mcp): apr mcp M1 skeleton — MCP server over stdio`). All three stale citations in the M1 milestone replaced. Five-whys root cause: the spec retrofit (#873) reconstructed PR numbers from memory; future retrofits should verify against `git log --grep=...` before committing. Refs PMAT-037. * fix(mcp-spec): demote unmerged contract + M3 PR accuracy Three stale citations corrected in the M3 milestone: - #874 removed from cancellation bullet (#874 is the book-chapter doc commit, not cancellation — that's #883 alone). - `contracts/apr-mcp-server-v1.yaml (ACTIVE) ... (#886)` bullet moved from M3 SHIPPED to M4 IN PROGRESS. PR #886 is still OPEN and its own title says "M4 — apr-mcp-server-v1 contract ACTIVE". The file is not in-tree. Header's "**New**:" label also updated to "Pending (PR #886)" for the same file. - Book-chapter citation expanded to list #874 (M2 creation) + #885 (M3 update) for accurate provenance. Five-whys root cause (false "M3 SHIPPED" on #886): the spec promotion commit (a496ce97c) rolled unmerged M4 work into M3 bullets under the optimistic assumption the PR would land first. Going forward: any bullet citing a PR must verify `gh pr view <N>` is MERGED before promoting a milestone. Refs PMAT-037. * fix(mcp-spec): Architecture — refresh to match built reality The Architecture + Protocol + Out-of-Scope sections carried pre-M1 aspirations that no longer match the shipped crate. Refreshed against actual source tree in crates/aprender-mcp/: - Goal: schema source was `apr-cli-commands-v1.yaml`; lines 11/87/139 correctly cite `apr-mcp-tool-schemas-v1.yaml`. Unified. - Directory diagram: listed absent `schema.rs`; missing `build.rs`, `types.rs`, `tools/subprocess.rs`, `tools/version.rs`. `server.rs` comment said "pmcp::Server wiring" but M1 shipped a hand-rolled JSON-RPC loop (also noted line 149). `Cargo.toml` comment listed pmcp/tokio/clap/apr-cli — none are actual deps (verified: serde, serde_json, anyhow, nix, serde_yaml build, jsonschema dev). `tests/` now lists the four actual `falsify_*.rs` harnesses. - `apr mcp` subcommand: snippet promised `async` with `McpArgs` + transport matching + SSE; actual `run()` is blocking, takes no args, calls `AprMcpServer::new().run_stdio()`. - Protocol/Transport: "SSE optional" was false; flag doesn't exist. Downgraded to stdio-only and added SSE to Out of Scope. Five-whys root cause: the Architecture diagram was authored pre-M1 as a design sketch; later commits (#873 retrofit, v1.1.0 promotion) updated Milestones but never re-diffed the static diagram against `ls crates/aprender-mcp/src/`. Going forward: any spec change touching Milestones must run a diagram-vs-tree check. Follow-up filed: verify Config Precedence (lines 122-126) against implementation — `pub fn run()` consults no env vars today. Refs PMAT-037. * fix(mcp-spec): reconcile 8-vs-9 tool count + Related Work misattribution Two factual errors corrected: - Tool count: spec said "8 Phase-1 tools" (lines 74, 133); `tools/list` actually returns 9 because `apr.version` (M1 scaffold) is also registered. Verified by `crates/aprender-mcp/tests/falsify_m1.rs::falsify_mcp_002_tools_list_schema_shape`, which asserts all 9 names (apr.version + 8 workflow tools). Clarified spec to state "8 Phase-1 workflow tools + apr.version scaffold = 9 total registered" and added test cross-link to the FALSIFY-MCP-002 bullet. - Related Work line 210 claimed `crates/apr-cli/src/tool_commands.rs` is the "planned MCP tool surface (referenced but unimplemented)". That file exists and is the `apr tool` CLI subcommand group (Showcase, Rosetta, …), unrelated to MCP. The actual MCP tool surface lives in `crates/aprender-mcp/src/tools/`. Corrected and noted that rust-mcp-sdk (paiml/rust-mcp-sdk) is currently unused since M1 shipped a hand-rolled JSON-RPC dispatcher. Five-whys root cause (8 vs 9): the original Phase-1 design enumerated 8 workflow tools and `apr.version` was added later as an M1 handshake probe without updating the narrative count. No invariant check cross-references spec tool-count against `tools/list` test assertions. Refs PMAT-037. * fix(mcp-spec): mark config precedence Phase-2 aspirational Lines 122-126 stated a four-level config precedence (`--config`, `$APR_MCP_CONFIG`, `~/.config/apr/mcp.toml`, defaults) as if it were implemented. Actual `crates/apr-cli/src/commands/mcp.rs::run()` takes no arguments and consults no env vars; `AprMcpServer::new()` has no config loader. The `APR_MODEL_DIR` env in the `.mcp.json` snippet is read by the spawned `apr <cmd>` subprocesses, not by the MCP server. Rewrote the section to keep the intended precedence as the Phase-2 contract while making Phase 1's "no config loader" reality explicit. Five-whys root cause: the Configuration section predates the M1 skeleton and was not re-verified against `commands/mcp.rs` during the v1.1.0 promotion. A "spec bullet implies an API — grep for the API" check belongs in the promotion workflow. Refs PMAT-037. * fix(mcp-spec): Success Criteria gate count 8 → 9 Spec's Falsification Conditions section lists 9 entries (FALSIFY-MCP-001 through -008 plus FALSIFY-MCP-PROGRESS-001 added in M3), but the Success Criteria table still said "8 falsification gates". Count corrected and wording clarified to reflect that -003/-004 are currently PARTIAL and must promote to PASS at M4 close. Five-whys root cause: adding PROGRESS-001 in M3 touched the conditions section but didn't update the downstream summary row. Going forward: whenever a new FALSIFY-MCP-* lands, grep the spec for `N falsification` to catch all downstream counts. Refs PMAT-037. * fix(mcp-spec): close residual kaizen items Three dangling claims resolved: - Target version: `v0.32.0 / v0.33.0` stands as the intended release tags but `git tag -l 'v0.3*'` returns only `v0.3.0 / v0.3.1 / v0.30.0`. M1–M3 are merged on `main` but unreleased. Added a clarifier so a reader doesn't assume those tags exist. - Aspirational follow-ons: `apr-mcp-plugin-marketplace-v1.md` and `apr-mcp-hooks-v1.md` are not in `docs/specifications/`. Labelled "(spec files not yet authored)" so readers don't hunt for them. - Risk Register: "pmcp crate API instability" is dormant because M1 shipped a hand-rolled JSON-RPC dispatcher (line 166 already notes pmcp is deferred). Row reworded so the risk's activation condition is explicit. Five-whys root cause (across all three): the spec's non-Milestone sections — Target, Related Work, Risk Register — were not refreshed during v1.1.0 promotion. Every milestone promotion should sweep those sections, not just the milestone table. Refs PMAT-037. * chore(pmcp): bump to 2.3 and drop pforge-runtime (Refs PMAT-037) Five-Whys: - Symptom: dual pmcp versions (1.20 + 2.3) resolved in agents-mcp build. - Why #1: pforge-runtime 0.1.4 (last released 2025-12) still pins pmcp 1.x. - Why #2: pforge-runtime was listed as an optional dep alongside pmcp. - Why #3: it was a forward-compat hedge — but no Rust code imports it (only doc-comment mentions and knowledge-graph string literals). - Why #4: keeping an unused dep doubled the compile footprint and split the pmcp protocol surface across two crates. - Root cause: speculative dep on a framework wrapper for an SDK we already use directly. Fix: - Cargo.toml: bump pmcp 1.10 → 2.3 (PAIML's actively-maintained SDK); remove pforge-runtime dep; agents-mcp feature now just ["agents","pmcp"]. - Doc comments and the mcp_demo example rewritten to name pmcp v2.3 as the SDK instead of pforge. No Rust-level API change — pforge-runtime was never imported, just advertised. - cargo tree -i pmcp now shows a single pmcp v2.3.0 node. Follow-up: spec's pmcp framing (M1 note + Risk Register) still needs rewrite in apr-mcp-server-spec.md. * docs(apr-mcp-spec): v1.2.0 — honest pmcp framing, add M5 migration plan (Refs PMAT-037) Five-Whys: - Symptom: spec framed pmcp as unstable/dormant, treating the SDK as risk rather than planned substrate. - Why #1: Risk Register called out "pmcp crate API instability (dormant...)" — language from before pmcp was actively maintained. - Why #2: M1 note said "pmcp SDK deferred — more deterministic for current scope" without explaining the actual technical rationale. - Why #3: no adoption path existed — M4 stops at dogfood, so readers couldn't tell whether pmcp would ever land. - Why #4: pmcp v2.3 is PAIML's own crate (paiml/rust-mcp-sdk) and already used by aprender-orchestrate; keeping the spec's out-of-date framing forced the /tmp/spec-update session to discover this from crates.io. - Root cause: stale spec language from the early M1 period where the adoption path was genuinely uncertain; never updated after pmcp stabilised. Fix: - Line 15: link now labels pmcp as "PAIML's Rust MCP SDK, actively maintained, v2.3.1 on crates.io (2026-04-16)". - Line 44 / 167: architecture + M1 note explain the three concrete reasons the dispatcher is hand-rolled (minimal request/response shape over `apr <cmd> --json`, build.rs schema codegen keeps tools/list byte-identical to contract YAML, falsification asserts on wire bytes without an SDK layer). - Risk Register row rewritten from "API instability" to "adoption-path coordination" — real risk is workspace version alignment with the pmcp client role in aprender-orchestrate. Mitigation: single workspace-wide bump + `cargo tree -d` CI gate. - New M5 milestone: concrete pmcp migration plan — port dispatcher to pmcp::Server (retain build.rs codegen), add SSE + WebSocket transports, re-run falsification suite post-migration. - Out of Scope: SSE/WebSocket transports reclassified as "scheduled for M5 on top of pmcp v2.3". - Related Work: pmcp-sdk contract row now notes aprender-orchestrate already links pmcp v2.3 as a client; server-side migration is M5. - Version bumped 1.1.0 → 1.2.0. * docs(mcp-spec): reconcile M4 gate count with PR #886; bump pmcp contract v2.3 (Refs PMAT-037) Five-Whys: - Symptom: M4 bullet claimed "9 falsification_conditions" to match the 9 gates listed in Section 145, but PR #886's contract pins exactly 8 (FALSIFY-MCP-001..008) and a Rust test enforces that invariant. - Why #1: the 9th gate (FALSIFY-MCP-PROGRESS-001) was added in M3 AFTER PR #886 was drafted. - Why #2: PR #886's harness (apr_mcp_server_contract_ids_are_falsify_mcp_001_through_008) explicitly rejects anything outside 001..008, so the contract row for PROGRESS-001 cannot land in the same PR without harness changes. - Why #3: the spec's earlier count-reconciliation (2026-04-18 prior kaizen round) missed this because it was looking for text matches, not contract row counts. - Root cause: spec and contract evolved on different PR branches. Fix: - M4 bullet: accurately describes PR #886 as landing 8 falsification rows, names the exact-8 invariant by its test function. - Adds an explicit follow-up bullet: "Extend the contract with a 9th row for PROGRESS-001 after PR #886 merges — relax the exact-8 invariant to 'FALSIFY-MCP-001..008 + PROGRESS-001, no extras'". - Success Criteria table unchanged (line 220 still correctly says "9 falsification gates ... all PASS or PARTIAL→PASS by M4 close") — the 9th gate is already ENFORCED in code via falsify_mcp_progress_001.rs, we just need the contract YAML to catch up. Also: - contracts/pmcp/mcp-protocol-sdk-v1.yaml version 1.0.0 → 1.1.0 with "last_modified: 2026-04-18". - Description updated v2.1 → v2.3, adds consumer-of-record (aprender- orchestrate via agents-mcp feature) + future consumer (aprender-mcp M5 migration) + link to apr-mcp-server-spec.md. * docs(book/mcp): align M3 scope + add M5 pmcp migration row (Refs PMAT-037) Five-Whys: - Symptom: book chapter's M3 row missed FALSIFY-MCP-PROGRESS-001 (shipped via PR #887) and the paragraph called progress streaming "a follow-up slice" for BOTH apr.run and apr.finetune — incorrect for apr.finetune. - Why #1: book chapter was authored before PR #887 landed progressToken-gated notifications for apr.finetune. - Why #2: M5 pmcp migration (added to spec v1.2.0 today) had no corresponding row in the book status table. - Root cause: book lagged spec after the M3 progress slice merged and after the M5 migration plan was formalised today. Fix: - M3 row now mentions the opt-in progress notifications. - Paragraph specifies: FALSIFY-MCP-PROGRESS-001 is enforced for apr.finetune; only per-step structured progress (CLI event channel prereq) and apr.run progress (apr run --stream flag prereq) remain open. - New M5 row in the status table mirrors the spec's M5 milestone. * docs(mcp-spec): tighten streaming claim + M5 transport pointer (Refs PMAT-037) Five-Whys: - Symptom: Section "Protocol" bullet on Streaming claimed "apr.run and apr.finetune send notifications/progress for each decoded token / training step" — but apr.run progress is a deferred M4 item and apr.finetune only emits per-stdout-line progress (not per training step) and only when the client opts in via progressToken. - Why #1: the bullet was authored when both tools were planned to stream per-token. Reality diverged: progress landed for apr.finetune only (opt-in, per-line), apr.run was deferred. - Why #2: the Architecture paragraph pointed to "Phase 2 with SSE" for transport selection without naming the actual M5 milestone that now schedules it. - Root cause: drift between aspirational early-M2 text and the M3/M5 structure formalised today. Fix: - Streaming bullet now names what's actually enforced (FALSIFY-MCP-PROGRESS-001, apr.finetune opt-in, per-stdout-line) and explicitly calls out the apr.run follow-up prereq (apr run --stream flag + per-step CLI event channel). - Architecture paragraph points at M5 as the SSE/WebSocket landing spot rather than the generic "Phase 2". * fix(examples): unblock Chapter Examples Compile on main (Refs PMAT-037) Five-Whys: - Symptom: CI job "Chapter Examples Compile" has been failing on every push to main since PR #701 (2 days), plus on this PR, with RUSTFLAGS= "-D warnings" promoting unused-import warnings to hard errors. - Why #1: ch10_training and ch24_switch_pytorch both import `aprender::nn::Optimizer` but only call `optimizer.step_with_params`, which is an inherent method on `SGD` (not a trait method) — so the trait import is genuinely unused. - Why #2: ch26_switch_ndarray binds `let pred = lr.predict(&x)` but never reads `pred` (score re-computes internally). - Why #3: these examples predate the refactor that moved `step_with_params` from the Optimizer trait to inherent impls; the trait import was never cleaned up. - Why #4: the Book Contract Enforcement and Chapter Examples Compile jobs are non-required checks, so the red status never blocked merges and accumulated as tech debt. - Root cause: main CI andon rule (main must always be green) was waived for non-required checks. Toyota Way: "all defects are your defects" — fix it regardless of whose PR introduced it. Fix: - ch10_training.rs, ch24_switch_pytorch.rs: drop `Optimizer` from the aprender::nn:: import list. - ch26_switch_ndarray.rs: consume `pred` by printing the first prediction — preserves pedagogical intent of showing predict() works, and unblocks -D warnings. - `cargo build -p aprender-core --examples` now warnings-clean. * fix(ci): use contract: pointer, not derived PCU path (Refs PMAT-037) The "Every PCU page has matching contract" gate derived paths from the PCU ID (`apr-page-${ID}-v1.yaml` / `apr-book-${ID}-v1.yaml`) but real page headers already carry an authoritative `contract:` field, and chapter contracts are named `apr-book-ch01-v1.yaml` (chapter-number only) while PCU IDs include a slug (`ch01-why-rust`). The mismatch failed all 27+ book pages on every run. Five whys: 1. Why red? Script can't find `apr-page-tools-apr-cli-v1.yaml` from ID `tools-apr-cli`... wait it can. But for chapters it looks for `apr-book-ch01-why-rust-v1.yaml` which doesn't exist. 2. Why does it derive? The earlier convention stored ID-derived paths before `contract:` was added to headers. 3. Why not updated when `contract:` was added? The workflow was not migrated; the two lookup paths stopped covering all cases. 4. Why silent until now? The gate was not blocking main. 5. Why fix now? Kaizen sweep surfaced 27-page failure. Parse the authoritative `contract:` field. Also add missing PCU header + page contract for book/src/tools/mcp-server.md (now points to contracts/apr-page-tools-mcp-server-v1.yaml). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp): retire stale 'M3 will ship apr.serve lifecycle' (Refs PMAT-037) Three places claimed `apr.serve` cancellation lands in M3: - book/src/tools/mcp-server.md apr.serve paragraph - crates/aprender-mcp/src/tools/serve.rs module/fn docs - serve tool `description` field embedded in tools/list M3 actually shipped `notifications/cancelled` for apr.run only. `server.rs::CancelHandle` doc explicitly states: "Only apr.run currently honours cancellation." apr.serve remains fire-and-forget and the spec M3 bullet list never promised otherwise. Five whys: 1. Why stale? Comments predicted M3 scope before scope narrowed. 2. Why narrowed? Spec M3 scope: FALSIFY-MCP-006 for apr.run, -008 codegen, -PROGRESS-001 for apr.finetune. apr.serve lifecycle was never inside that gate set. 3. Why not updated at M3 close? No acceptance criterion forced a sweep of surface prose when milestone shipped. 4. Why matters now? Readers of book/tools page and users calling apr.serve via MCP get incorrect "lifecycle lands in M3" note that reads as imminent, not aspirational. 5. Why fix now? Kaizen sweep surfaced; retarget to M5 where a daemon registry + pmcp Server port belong together. Edits: book paragraph + serve.rs module header + serve.rs `call` docstring + serve.rs description field + spec M5 new bullet for apr.serve cancel extension. Also spec M5 falsification-suite bullet updated from "71+ tests" to measured "75 tests" with file list. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(book/mcp): clarify apr.finetune progress shipped with limits (Refs PMAT-037) The apr.finetune paragraph said "Per-step notifications/progress streaming is a follow-up M3 slice" — read as "no progress yet" — but FALSIFY-MCP-PROGRESS-001 shipped in PR #887: per-line progress over `params._meta.progressToken` IS live. Five whys: 1. Why stale? Paragraph was written before PR #887 merged. 2. Why not updated at PR #887? PR focused on server.rs + test additions; book paragraph not flagged in review. 3. Why matters? Clients reading the book will assume they cannot stream updates and skip progressToken, losing observability. 4. Why two progress layers? Per-line (shipped, stdout-driven) vs per-step (needs a CLI event channel from `apr finetune` itself) — the former is cheap plumbing over JSON-RPC, the latter is a CLI-side refactor. 5. Why fix now? Kaizen sweep surfaced. Rewrote the paragraph to state (a) what shipped (opt-in per-line), (b) the gate it satisfies (FALSIFY-MCP-PROGRESS-001), (c) the honest limitation (terminal blob today), (d) where per-step lives (M4 follow-up with CLI prereq). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * contract(mcp-schemas): retire 'retrofit-only' header, lock v1.1.0 (Refs PMAT-037) The apr-mcp-tool-schemas-v1.yaml header still read: "This M2 cut is RETROFIT-ONLY" "If this file ever disagrees with the Rust source, the Rust source wins" "In milestone M3 a build.rs at ... will read this YAML" All three are post-M3 stale: 1. M3 shipped (PRs #880, #884) — build.rs is live. 2. Byte-identity is enforced by tests/falsify_mcp_008.rs (5 tests). 3. Rust tool sources contain zero hand-written schemas — they only parse `crate::schemas::APR_<TOOL>_SCHEMA` from $OUT_DIR. 4. Direction is reversed: YAML authoritative, Rust derived. Five whys: 1. Why stale header? Written for M2 retrofit cut. 2. Why not flipped at M3 close? PR #884 focused on codegen, not contract prose. 3. Why matters? Future readers will assume Rust source is the authority and "fix" the wrong side of a drift — inverting FALSIFY-MCP-008's intent. 4. Why now? Kaizen sweep. 5. Why v1.1.0? Semantic bump: authoritativeness change, plus new reference pointer to apr-mcp-server-spec.md. Bumped metadata version 1.0.0 → 1.1.0, added last_modified, rewrote header and description to reflect current state (YAML is SoT, Rust parses codegen constants, falsify_mcp_008.rs enforces byte-identity). Also updated spec M5 falsification-suite file list to include `falsify_mcp_008` and drop nonexistent `codegen_bytes`. Verified: `cargo test -p aprender-mcp --test falsify_mcp_008` — 5/5 pass after YAML comment edits (no functional change, just prose). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp-spec): 57 → 58 CLI commands (mcp added PR #864) (Refs PMAT-037) The spec claimed a 57-command CLI surface three times: - Contracts bullet: "57-command tool surface" - Problem paragraph: "57-subcommand CLI" - Goal paragraph: "subset of the 57 apr CLI commands" PR #864 registered `apr mcp` as the 58th command (contracts/apr-cli-commands-v1.yaml). The 63-line count in the contract is 58 commands + 5 FALSIFY-CLI-00* falsification rules. Five whys: 1. Why stale? The 57 figure dates to #701 contract landing (2026-04-06) — the initial MCP PRs added `apr mcp` but didn't sweep cross-cutting doc claims. 2. Why matters? MCP spec's own subject command is the 58th — a reader comparing counts will mistrust the surface-area claim. 3. Why only fixing here? Scope is `apr-mcp-server-spec.md`; CLAUDE.md and apr-book-spec.md have broader audiences and want their own kaizen passes. 4. Why cite PR #864 inline? Makes the delta auditable by a future reviewer checking `git log --oneline apr-cli-commands-v1.yaml`. 5. Why not reword to "58+ commands" for future-proofing? The contract is the source of truth; stale counts are better caught by an exact-match CI gate than smeared over with imprecise phrasing. (PR #864 added a FALSIFY-CLI gate.) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp-spec): honest release-target footer (M3 shipped same week as M2) (Refs PMAT-037) The footer claimed: v0.32.0 (M1–M2), v0.33.0 (M3–M4) But M3 shipped on 2026-04-18, same week as M2 (2026-04-17/18), and the workspace is still at v0.30.0 on main. The old split-tag plan (M1–M2 in one release, M3–M4 in the next) no longer maps to reality — M3 will publish alongside M1–M2 because there's nothing to publish in between. Five whys: 1. Why stale? Target was written assuming M2 → cut release → M3. 2. Why reality diverged? M3 landed fast because cancellation + codegen + progress + apr.finetune were all independent PRs. 3. Why matters? A reader looking at `git tag` + this footer would expect v0.32.0 to exist; it doesn't. 4. Why not assign firm tags? Release cuts require a separate decision (changelog + publishing); this spec shouldn't preempt it. 5. Why keep historical context? Future reader asking "why is the M3–M4 split collapsed?" deserves a traceable answer instead of silently rewritten history. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(aprender-mcp/README): sync milestones + full gate table (Refs PMAT-037) The crate README was three milestones behind the spec: - M2 bullet: "apr.serve (fire-and-forget; full lifecycle in M3)" — M3 shipped apr.run cancel only; serve registry is M5. - M3 bullet: "in progress" — M3 actually shipped 2026-04-18 (PRs #880, #881, #883, #884, #887). - Gate table: listed 5 gates (001, 002, 005, 007, VALIDATE-001); missed 003, 004, 006, 008, PROGRESS-001 — 4 of 5 are now ENFORCED or PARTIAL, and PROGRESS-001 is net-new since M3. Five whys: 1. Why lag? README is surface-facing, spec/code are the primary targets during milestone closes. 2. Why matters? crates.io readers land here first — inaccurate milestone + gate table = miscalibrated expectations, especially about apr.serve cancellation. 3. Why add status column? Distinguishing ENFORCED vs PARTIAL vs planned is what readers actually want when choosing whether to depend on a given gate. 4. Why spell out M4 + M5 here? Same reason — readers want to know what's next, not dig through the spec. 5. Why fix now? Kaizen sweep; PR #888 already touches this crate. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(README): 57 → 58 commands across 4 sites (Refs PMAT-037) The MCP spec already reconciled 57 → 58 (PR #864 added `apr mcp` as the 58th command in contracts/apr-cli-commands-v1.yaml). The root README still repeated 57 in four places: headline paragraph, stats bullet list, crate-layout tree comment, and smoke-test snippet. Keeping the count exact matters more than soft-pedalling it — PR #864 also added a FALSIFY-CLI gate that enforces `apr --help` listing against the YAML, so drift is caught at CI and the README should track it. Fixing here alongside the spec keeps the docs audit self-consistent within one PR. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(orchestrate/book): pmcp 1.8 → 2.3, drop pforge-runtime (Refs PMAT-037) Two orchestrate book pages carried stale pmcp/pforge references: - part3/pmcp.md — header still claimed pmcp v1.8.6 and showed `pmcp = "1.8"` in Cargo.toml snippet. crates.io has pmcp 2.3.1 as of 2026-04-16 and the crate's Cargo.toml already pins it. - part3/agent-runtime.md L575 — `agents-mcp = ["agents", "pmcp", "pforge-runtime"]` but pforge-runtime was dropped earlier in this PR series (it pinned pmcp 1.20 and was unused outside knowledge-graph cataloguing). Five whys for each: 1. Why stale? Book pages were written against pmcp 1.x, before the 2.x release cleanup. 2. Why not caught? The orchestrate book has no CI gate matching its Cargo.toml snippets to actual crate deps. 3. Why matters? Readers copy-pasting `pmcp = "1.8"` into a new project would land on a yanked / unmaintained line. 4. Why not add a CI gate? Out of PR scope; filed mentally as an M5+ follow-up when `apr-contracts` lints cross-project snippets. 5. Why fix now? Kaizen sweep surfaced during pmcp/pforge audit. Both archived batuta-agent.md references left alone — they live in `docs/specifications/archive/` and document the old design state. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(CLAUDE.md): 57 → 58 commands, add mcp to key-command list (Refs PMAT-037) Three stale 57-command claims in CLAUDE.md — the overview line, the key-files bullet, and the APR CLI section. Brought them in line with contracts/apr-cli-commands-v1.yaml (58 commands including `apr mcp`, added PR #864). Also added `mcp` to the inline key-command list — discovery matters more than alphabetical tradition given the MCP spec is the current top-of-mind work. The 405-contract and 25,300-test counts are out of spec scope and left for a future sweep (workspace tests reportedly 25,391 per the root README, but confirming across the 70 crates needs real `cargo test --workspace --lib` run, not a file read). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp-spec): document FALSIFY-MCP-VALIDATE-001 dispatcher invariant Symptom: spec Falsification Conditions section had 9 entries (MCP-001..008 + PROGRESS-001), but crates/aprender-mcp/README.md and book/src/tools/mcp-server.md both list a 10th enforced gate, FALSIFY-MCP-VALIDATE-001, which was missing from the spec entirely. Five-whys: (1) spec only lists conditions destined for apr-mcp-server-v1.yaml; (2) VALIDATE-001 is a dispatcher-level contract point (how the server shapes tool errors), not a per-tool behavioural promise; (3) it therefore lives *alongside* but *outside* the YAML contract — mirrored in the book under "Additional invariant enforced by the dispatcher"; (4) the spec's own section header ("Falsification Conditions for apr-mcp-server-v1.yaml") excluded it by scope, but the omission reads as "we forgot a gate" to anyone cross-referencing README/book; (5) fix is to add an "Additional dispatcher invariant" subsection pointing at the existing test falsify_m1.rs::falsify_validate_missing_model_path_is_tool_error. Refs PMAT-037 * docs(aprender-mcp): refresh module-level scope docs for M3-shipped state Symptom: `src/lib.rs` crate-level docs titled the scope section "M1 Scope" and claimed "M2 adds the 8 Phase-1 tools"; `src/tools/mod.rs` said "M3 adds `apr.finetune` (synchronous initial slice; streaming is a follow-up)"; and `src/server.rs` had a test doc-comment reading "Full 8-tool set lands when M2 completes." All three predate M3 shipping on 2026-04-18. Five-whys: (1) module docs were written incrementally milestone-by- milestone; (2) each PR updated its own surface but left sibling module docs unchanged; (3) there is no CI gate on module-level Rustdoc matching milestone status; (4) new readers start at `lib.rs` and encounter text that contradicts `apr mcp --help` + README; (5) cheapest fix is to rewrite the three doc-comments to a single authoritative summary keyed off the spec's own "M1–M3 SHIPPED" tags, leaving M4/M5 forward-looking. No behaviour change; no test updates needed. Refs PMAT-037 * docs(mcp): update apr.finetune/apr.run docs for shipped-M3 progress state Symptom: three stale M3 claims, each LLM-visible or reader-visible: (1) `apr.finetune`'s `description` field still read "Progress streaming lands in a follow-up M3 slice" — but PR #887 shipped the streaming slice on 2026-04-18, and the description is returned verbatim in `tools/list` to LLM clients. (2) The same stale sentence is duplicated in the authoritative `contracts/apr-mcp-tool-schemas-v1.yaml`. (3) `src/tools/run.rs` module docs say "Progress notifications (streamed per-token) are a separate M3 slice" — the spec's M3 checklist (line 192) now records that as deferred to M4 pending `apr run --stream`. Five-whys: (1) tool `description` fields are hand-written strings that become part of the MCP wire response; (2) FALSIFY-MCP-008 compares `inputSchema` byte-for-byte but *not* `description`, so description drift is silent; (3) when PR #887 shipped progress streaming, only the crate module docs in finetune.rs were partially updated — the `description` field and the YAML contract were missed; (4) stale LLM- visible strings confuse agents about which call shape actually works today; (5) fix is to (a) promise exactly what ships (opt-in via `params._meta.progressToken`, falsification gate PROGRESS-001), (b) align the YAML contract and Rust source, and (c) rewrite `apr.run`'s module prelude to describe the cancel-token surface that shipped and the per-token progress that didn't. Verified: `cargo test -p aprender-mcp --test falsify_mcp_008` passes (5/5). Description field is not covered by the schema gate, confirming the drift was invisible to CI until now. Refs PMAT-037 * docs(mcp-spec): cross-link M4 checklist items to the PRs carrying them Symptom: M4 checklist items in the milestone section all read "in flight" / "dogfood" without referencing any PR, even though six open PRs (#886, #889, #890, #891, #892, plus our own #888) are carrying this exact work. Readers who arrive from the PR list can't map a PR onto the spec box it's trying to tick, and readers who arrive from the spec can't find the open PR. Also added a `falsify_mcp_progress_001.rs` row to the crate-layout tree (previously omitted) and broadened the `falsify_m1.rs` description to mention all gates it enforces (-001, -002, -005, -007, -VALIDATE-001), not just the first two. Five-whys: (1) M4 work is happening across 4+ PRs in parallel; (2) the spec was last edited when only PR #886 existed; (3) new PRs (#889/#890/#891/#892) introduced new gate IDs (FALSIFY-MCP-E2E-001, FALSIFY-MCP-DOGFOOD-001, FALSIFY-MCP-PROGRESS-002) but the spec never reflected them; (4) without PR cross-links, the spec drifts out of sync within days; (5) fix is to name the branch + PR for each in-flight box so the linkage is obvious and breaks visibly when a PR is closed or renamed. Refs PMAT-037 * docs(contracts): fix stale 57-command count + codegen test path Two small contract-metadata fixes caught by the kaizen sweep: 1. `contracts/apr-cli-commands-v1.yaml` line 24 — `scope` field still claimed "57 commands"; the actual command list has 58 entries as of PR #864 (apr mcp added 2026-04-17). Verified by counting `^ - name:` entries under the `commands:` key (`awk` filter — 58). 2. `contracts/apr-mcp-tool-schemas-v1.yaml` had two sibling errors: (a) Block-comment header line 7 still said "each of its 57 entries" referring to apr-cli-commands-v1.yaml — updated to 58 to stay in sync with the registry. (b) `metadata.description` pointed readers at `tests/codegen_bytes.rs` for FALSIFY-MCP-008 enforcement; the actual file is `crates/aprender-mcp/tests/falsify_mcp_008.rs` (confirmed via `ls crates/aprender-mcp/tests/`). The wrong path is particularly bad because new contributors clone the repo and try to grep for a file that doesn't exist. Five-whys on (2b): (1) an earlier contract rev proposed the filename `codegen_bytes.rs`; (2) the commit that renamed it to `falsify_mcp_008.rs` (conventions: one test file per FALSIFY gate) didn't update the contract metadata; (3) nothing in CI cross-checks prose filename references inside YAML headers; (4) the spec we edited in PR #888 already fixed this in one spot but missed the sibling in this file; (5) the cheapest fix is a literal string replace — adding a lint for "tests/[a-z_]+\.rs" strings that don't resolve is follow-on work, tracked separately. Refs PMAT-037 * docs(contracts): bump 57→58 command count in apr-cli-publish + apr-cli-qa Symptom: the two CLI-level contracts that gate `cargo install` and dogfood QA still asserted "all 57 commands" in their postconditions, falsification predictions, and proof_obligations. The actual `apr --help` surface is 58 commands as of PR #864 (mcp added 2026-04-17), and `contracts/apr-cli-commands-v1.yaml` was already updated to 58 in the previous commit. Affected invariants: - apr-cli-publish-v1.yaml equations.all_commands_compile formula - FALSIFY-PUB-CLI-003 prediction ("apr --help lists all N commands") - apr-cli-qa-v1.yaml postconditions, FALSIFY-QA-001 rule, and proof_obligations[0].property Why this matters: when these prose counts go stale, an engineer reading the contract reasonably concludes either (a) the contract is behind reality and they should doubt it, or (b) the list of commands was shortened and a command got removed — neither is true. Five-whys: (1) the mcp command was added via PR #864 with contract update constrained to apr-cli-commands-v1.yaml; (2) sibling contracts that reference the count (publish + qa) were not updated in the same PR; (3) no CI linter cross-checks "N commands" strings against the authoritative registry count; (4) the drift persisted for ~1 day and would have confused contract reviewers on the next spec pass; (5) fix is bulk text replace plus a mental note to add a numeric cross-check linter in a follow-up (tracked separately). No test iteration count changes (the harnesses iterate the contract YAML entries, not the hardcoded number). The strings are readability only. Refs PMAT-037 * docs: bump 57→58 command count in book + spec prose Surface-prose sweep after bumping the two load-bearing contracts (apr-cli-publish + apr-cli-qa) in the previous commit. Same root cause: PR #864 added `apr mcp` as the 58th command but prose references scattered through the book and spec suite were not updated in lockstep. Touched (one literal "57 commands" → "58 commands" per line): - book/src/architecture/monorepo-layout.md — crate-tree caption - docs/specifications/apr-cli-qa-spec.md — 4 sites (problem framing, structural gate cell, Phase-1 section heading, Phase-8 grid line) - docs/specifications/aprender-monorepo-consolidation.md — the "Users NEVER pass --features" principle (line 414); the historical "DONE" entry at line 618 is left at 57 because it describes the phase as it was completed, not current state - docs/specifications/aprender-readme-book-rewrite.md — book tree caption Not touched (out of scope for this sweep): - docs/hero.svg and docs/specifications/apr-book-spec.md — user-facing graphics + marketing copy; will sweep separately - archive/ and examples/ — either historical or println strings with lower blast radius - .claude/skills/dogfood/SKILL.md — dogfood skill instruction, queued Refs PMAT-037 * docs(book/mcp): add FALSIFY-MCP-PROGRESS-001 row to gates table The book's falsification-gates table in book/src/tools/mcp-server.md listed rows for FALSIFY-MCP-001..008 and then the dispatcher-level FALSIFY-MCP-VALIDATE-001, but skipped the M3 addition FALSIFY-MCP-PROGRESS-001 that the spec already calls out as item 9 of the contract-bound gates (apr-mcp-server-spec.md#L159) and that the success-criteria row counts as part of the "9 falsification gates (FALSIFY-MCP-001..008 + PROGRESS-001)" invariant (L228). Five whys: - Symptom: book table shows 8 contract gates, spec says 9. - Why: PROGRESS-001 row was never added when M3 shipped (#887). - Why: M3 PR #887 landed PROGRESS-001 behaviour + test but did not touch the book's gates table (touched the narrative section only). - Why: the gates table is organized numerically and the PR author added PROGRESS-001 to the prose but not to the table below it. - Root cause: the table is a cross-cutting artifact that any new gate must be added to — no codegen pressure, no CI guard. - Fix: add the row now; future change: fold this into contract-driven codegen when apr-mcp-server-v1.yaml lands (PR #886, tracked for M4). Refs PMAT-037, FALSIFY-MCP-PROGRESS-001 * docs(aprender-mcp/README): fix 8→9 tools count in M3 codegen coverage The M3 entry said build.rs generates schemas for "all 8 tools"; in fact the contract apr-mcp-tool-schemas-v1.yaml has 9 entries (the M1 apr.version scaffold + the 8 Phase-1 workflow tools), and build.rs emits one pub const APR_<TOOL>_SCHEMA per entry for all 9. Five whys: - Symptom: README says "all 8 tools"; contract has 9 tool entries. - Why: the "8 tools" figure was the Phase-1 workflow-tool count. - Why: when FALSIFY-MCP-008 expanded to codegen every tool in M3 it picked up apr.version too, but the README M3 bullet kept the Phase-1-focused "8 tools" wording. - Why: the Phase-1 count and the registered-tool count are both in circulation in docs (spec refers to both as "8 Phase-1 tools plus apr.version") and it's easy to conflate them. - Root cause: no single-sourcing of the tool-count number — any doc can drift from `contracts/apr-mcp-tool-schemas-v1.yaml` (the authoritative list) silently. - Fix now: split the count honestly ("8th Phase-1 workflow tool — 9th registered" and "all 9 registered tools"); deferred fix: when the spec's M4 contract promotion (PR #886) lands, add a FALSIFY-MCP-008-style codegen check that the tool-count numbers in README/spec/book match the YAML row count. Refs PMAT-037 * docs: sweep remaining 57→58 command drift in book + spec prose Five prose sites still carried the stale 57-command count after the earlier commits bumped the contract YAMLs and the monorepo/crate-tree captions: - book/src/introduction.md (2 occurrences — "What is Aprender?" headline + CLI Reference bullet) - docs/specifications/apr-book-spec.md (2 occurrences — Ch 1.5 entry + Appendix A crate-map row for apr-cli) - docs/specifications/aprender-readme-book-rewrite.md (2 occurrences — Problem section intro + "What is aprender?" bullet) Why these were missed earlier: the previous sweep focused on contract YAMLs (apr-cli-commands-v1, apr-cli-publish-v1, apr-cli-qa-v1) + the monorepo layout crate-tree captions. These prose sites live in discursive book/spec text and weren't caught by the YAML-first grep. Scope discipline preserved: left the two intentional historical references alone — aprender-monorepo-consolidation.md#L618 DONE history line and apr-mcp-server-spec.md#L10/#L21 which say "58 commands (57 + mcp added PR #864)" on purpose to explain the jump. Refs PMAT-037 * docs(aprender-mcp/validate): refresh stale 'remaining 7 will follow' doc-comment The module doc-comment for apr.validate still read as if M2 was in progress — "the remaining 7 Phase-1 tools will follow: spawn apr <subcommand> --json...". M2 shipped 2026-04-17/18 (#865, #866, #867, #870, #872) and M3 shipped 2026-04-18 (#881), so all 7 M2 wrappers plus the M3 apr.finetune addition now live on this pattern. Updated to present-tense enumeration: lists each wrapper by name and makes explicit that apr.finetune also inherits the subprocess pattern, so a reader landing on this file first gets the full shape of what ships. Five whys: - Symptom: validate.rs doc-comment describes M2 as future work. - Why: comment was written when apr.validate was the first-shipped wrapper (#865) and the other 6 were still PRs. - Why: subsequent wrapper PRs (#866, #867, #870, #872) and the M3 addition (#881) didn't circle back to retire the "will follow" tense on the earliest module. - Why: no codegen or lint forced doc-comments to reference contract-driven tool counts, so the prose drifted silently. - Root cause: module doc-comments are low-visibility — they don't show up in tools/list output, so FALSIFY-MCP-008 doesn't catch them. - Fix: manual sweep now; longer-term, an apr-mcp doc-invariant contract could codegen "shipped tools" lists from the registry. Refs PMAT-037 * docs(mcp-contract): sync apr.serve description with source truth The YAML contract still said "Full lifecycle (cancel/SIGTERM) lands in M3." — but M3 shipped weeks ago (finetune + opt-in progress) and serve lifecycle was deferred to a post-M3 follow-up. The source-of-truth description in `crates/aprender-mcp/src/tools/serve.rs:44-46` already reads "Cancel-token lifecycle (SIGTERM) is a post-M3 follow-up" — the contract YAML is the one that drifted. Five-whys 1. Why did the YAML description drift from the source? → FALSIFY-MCP-008 only asserts byte-identity on the `inputSchema` (properties/required), not on the tool-level description. 2. Why was FALSIFY-MCP-008 scoped that way? → Descriptions are LLM-visible free-form prose that humans edit in both places during development; byte-comparing them every build would churn CI. 3. Why did the divergence survive post-M3? → No periodic kaizen sweep compares YAML tool descriptions with their source counterparts. 4. Why didn't any kanban/release task catch it? → Release templates don't list the MCP contract YAML among per-milestone artifacts to refresh. 5. Why not? → Contract YAML changes are treated as codegen input, not documentation — so prose rot goes unnoticed until a kaizen pass. Symptom fixed; root-cause follow-up (a byte-compare for descriptions, or a lint that forbids roadmap-tense phrases like "lands in Mx" after that milestone ships) is tracked for a future pass — not a PMAT-037 blocker because descriptions are advisory for LLM clients and the actual tool behaviour is covered by FALSIFY-MCP-005/007/008. Refs PMAT-037 * docs(mcp-contract): drop false stop_reason claim from apr.run description YAML + source both advertised that apr.run "returns tokens + tok/s + stop reason", but the apr CLI does not emit `stop_reason`. Spec line 90 of apr-mcp-server-spec.md records the ground truth: CLI as of 2026-04-18; `stop_reason` not emitted Replaced with an accurate inventory ("generated text, tokens, tok/s, and timing") plus the cancellation note that is genuinely load-bearing for MCP clients (FALSIFY-MCP-005 asserts cancel wiring). Five-whys 1. Why did the description promise a field the CLI doesn't emit? → The description was written speculatively ahead of a planned `apr run --json` enrichment that never landed. 2. Why did the speculative doc survive? → FALSIFY-MCP-008 compares inputSchema byte-for-byte, but does NOT compare the tool description to the actual CLI response keys. 3. Why doesn't any gate detect output-shape drift? → apr.run returns free-form stdout bytes to the MCP client; there is no typed contract on the response shape. 4. Why not? → The MCP tool surface is intentionally a pass-through so the CLI can evolve without churning the MCP spec. 5. Why does that hurt here? → Pass-through evolution needs matching doc-hygiene passes (like this one) to keep the LLM-visible description honest. Same root-cause class as the apr.serve fix one commit back. Same class of drift as 715781df5 (apr.serve "lands in M3"). Tracking a shared follow-up: lint for roadmap-tense phrases and a smoke-test that the description's field enumeration is a subset of the CLI's actual JSON keys. Refs PMAT-037 * docs(mcp-spec): clarify Success Criteria scope — spec ACTIVE, gate is for M4 close The header reads "Acceptance gate for promoting to ACTIVE" — but the spec status at the top already says ACTIVE (promoted at M3 ship on 2026-04-18). The criteria listed (contract-level gates, 9-gate pass including the M4 dogfood session) actually describe **closing M4** — promoting `apr-mcp-server-v1.yaml` from DRAFT to ENFORCED and lifting FALSIFY-MCP-003/-004 from PARTIAL to PASS. Five-whys 1. Why does "promoting to ACTIVE" survive past ACTIVE promotion? → The Success Criteria block was drafted pre-M3 when the spec was still DRAFT, and was never re-scoped after the M3 ship flipped the spec header to ACTIVE. 2. Why did no gate force a re-scope? → The spec's own header was updated in the same commit that set the status, but the mid-doc sections weren't traversed because nothing links them to the header change. 3. Why isn't that traversal automated? → provable-contracts' doc_integrity checker validates cross-links between spec and contract YAML, not internal consistency of roadmap language across sections of the same spec. 4. Why is internal consistency not a contract check? → Roadmap language ("will ship", "pending", "ACTIVE") is prose, not structured data — hard to assert byte-for-byte. 5. Why not structure the status fields? → Longer-term work; this commit is the symptom fix so readers can trust the Success Criteria block against the spec header. Now readers see: - Spec header: ACTIVE - Success Criteria: gate for closing M4 (contract DRAFT→ENFORCED, FALSIFY-MCP-003/-004 PARTIAL→PASS, dogfood done) That's the actual open-work framing. Refs PMAT-037 * docs(book/mcp): fix stale apr.version example payload (0.31.0 → 0.30.0) The book's apr.version example response used "0.31.0", but the tool emits CARGO_PKG_VERSION baked in at compile time — currently 0.30.0 (workspace Cargo.toml, unchanged since 2026-04-12). A client developer reading the doc and pinning to the example shape would see an immediate mismatch against a real server. Five-whys 1. Why did the doc show a version that doesn't exist? → The example was forward-scoped during an earlier release-planning pass that anticipated a 0.31.0 bump. 2. Why did that anticipated bump not land? → M1-M3 all shipped on main but never got tagged; the plan line in the spec says "M1-M3 planned for v0.32.0 publication" (line 263). 3. Why didn't the doc update when the tag plan changed? → Example payloads are prose, not codegen, and aren't covered by any contract byte-compare. 4. Why no lint for version strings in examples? → Version drift is rare and most tools show "x.y.z" abstracts; apr.version's case is unusual because the book shows a concrete literal. 5. Why show a concrete literal? → Helpful for readers debugging an actual tools/call round-trip — but that helpfulness inverts once the literal goes stale. Fix: set the example to 0.30.0 (current workspace version) and add a one-sentence note telling clients to parse for diagnostics rather than pin to the literal. That way the next version bump doesn't immediately invalidate the doc. Refs PMAT-514 * test(falsify-mcp-008): enforce tool description YAML↔source byte-equality Before: `migrated_tools_match_yaml_contract_byte_for_byte` compared only `inputSchema`, leaving `tools[*].description` free to drift silently. This drift was observed twice on 2026-04-18 alone (apr.serve — 715781df5, apr.run — 91a613968) after the YAML contract was audited manually against the source. Five whys: 1. Why did apr.serve/apr.run descriptions drift from the contract? → dev edits in tools/*.rs never propagated back to the YAML. 2. Why wasn't this caught in CI? → FALSIFY-MCP-008 harness compared only `inputSchema`. 3. Why was `inputSchema` the only thing compared? → M3 PR #881 scoped the byte-identity gate to the schema codegen path (build.rs emits APR_*_SCHEMA constants), where drift would crash the build. 4. Why didn't the contract itself catch this? → YAML line 282 asserted "each tool's `description` matches tools[*].description byte-for-byte" — but that assertion was aspirational, never wired into a test. 5. Root cause: claim-without-enforcement is the silent-drift seed. Fix is to make the assertion load-bearing by adding a second test that compares `ToolDefinition.description` to the YAML string directly. The new test `tool_descriptions_match_yaml_contract` discharges the class of drift that caused both commits above, without widening scope — it uses the same contract loader and `migrated_tools()` iterator as the existing schema gate. Verified: all 6 tests in falsify_mcp_008 pass, including the new one. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp-contract): flip DRAFT→ENFORCED, clear stale M3 parentheticals The contract YAML self-describes as DRAFT and pins its test_harness / codegen_consumer with "(to be added in M3)" parentheticals — but M3 shipped on 2026-04-18 (PR #881). The drift surfaces as: - Line 58 top-level `status: DRAFT` - Line 271 `FALSIFY-MCP-008.status: DRAFT` - Line 287 `test_harness: ...falsify_schema_codegen.rs (to be added in M3)` — the real harness is `falsify_mcp_008.rs` and has six tests green - Line 288 `codegen_consumer: ...build.rs (to be added in M3)` — already landed - Line 57 top-level `version: "1.0.0"` vs line 30 `metadata.version: 1.1.0` Five whys: 1. Why is the contract still DRAFT after M3 shipped? → nobody reran a spec audit after PR #881 merged. 2. Why did the M3-ship commit not touch this file's status? → PR #881 scope was "wire up codegen + harness"; contract fields were treated as documentation, not code. 3. Why weren't the parentheticals caught? → they read as prose, not as testable assertions; no gate compares them against reality. 4. Why didn't any automation flag a version mismatch between top-level `version` (1.0.0) and `metadata.version` (1.1.0)? → no such check exists on this contract schema. 5. Root cause: contract-as-documentation drift. Counterpart: PMAT-514 just added a harness test that makes the `description`-equality claim on line 282 load-bearing. This commit brings the surrounding prose (status + parentheticals + version pin) into alignment with that ENFORCED reality. Follow-up candidates (not in this commit): - Add a harness check that `metadata.version == top-level version` to prevent this class from re-emerging (parallel to FALSIFY-MCP-008). Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp-spec): document FALSIFY-MCP-008 description-equality extension Three coordinated edits, all propagating the harness change from PMAT-514 into the spec surface: 1. Gate summary (line 158): narrow "schema byte-identical" claim broadened to "schema + description byte-identical", naming both test functions explicitly so readers can find the enforcement point. 2. File-tree comment (line 60): `falsify_mcp_008.rs` blurb now says "schema + description byte-identity", matching the new test. 3. M5 re-run checklist (line 215): test count 75 → 76 (one new test in falsify_mcp_008.rs). Verified: `cargo test -p aprender-mcp` reports 51+8+4+6+4+2+1 = 76 tests all passing. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(roadmap): register PMAT-514 — APR-MCP-KAIZEN continuous drift sweep Adds the pmat work ticket that tracks ongoing kaizen on apr-mcp-server-spec and its satellites (aprender-mcp source, book chapter, schema contract YAML). Status: inprogress. First discharge: byte-compare YAML tool descriptions with source descriptions (closed silent-drift class that bit apr.serve on 715781df5 and apr.run on 91a613968 in one 24h window). Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(book/mcp): book chapter mirrors FALSIFY-MCP-008 description extension Symmetric to the spec update in 2f38f0241. Two book edits: 1. Falsification gates table (line 333): gate now reads "inputSchema AND description byte-identical" — same broadening applied to the spec. 2. Schema-codegen prose (line 315-320): calls out the two specific test functions that enforce the gate, and tightens the "edit YAML, rebuild" guidance to include descriptions. Readers landing on the book chapter (via rustdoc cross-link or GitHub Pages) now see the same gate surface as spec readers. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(aprender-mcp/README): mirror FALSIFY-MCP-008 description extension Crate README's gate table is the third surface that readers hit — after the spec and book chapter. Aligning all three to say "inputSchema AND description" closes the documentation side of the silent-drift class. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp-contract): sharpen coverage-note — 9 entries, gate surface spelled out Before: the coverage note said "All 8 Phase-1 tools are now registered in this contract" — technically correct (apr.version is an M1 scaffold, not a Phase-1 workflow tool) but ambiguous, because the FALSIFY-MCP-008 harness iterates over all 9 entries including apr.version. A new reader easily miscounts. After: the note enumerates both categories explicitly (scaffold + 8 wrappers = 9 entries) and adds a second paragraph spelling out what the PMAT-514 extension now covers — `inputSchema` byte-identity AND tool-level `description` byte-identity — with the specific test function names. This matches the surface that was already asserted in the falsification block above (lines 281-286) and discharges the ambiguity in one pass. Verified: `cargo test -p aprender-mcp --test falsify_mcp_008` 6/6 green. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp-spec): add apr-mcp-tool-schemas-v1 to Contracts header list The tool-schemas contract is the **single source of truth** for every MCP tool's `inputSchema` (and, as of PMAT-514, description), drives the `build.rs` codegen, and is referenced by FALSIFY-MCP-008 — yet it was missing from the header `**Contracts**:` list. The spec's own body text referenced it five times (lines 27, 40, 158, 177, 193) but a reader landing on the spec from a link would not see it in the contract register. Five whys: 1. Why was the contract not listed? → the header was authored before the tool-schemas YAML was split out into a standalone contract. 2. Why didn't the split author backfill the header? → the split PR (#871 — authored the YAML) focused on the contract body; the spec header wasn't on the review checklist. 3. Why isn't there a checklist? → spec-header/contract-file consistency has no automated gate. 4. Why no gate? → the spec body mentions multiple contracts in prose, so "spec references contract X" doesn't uniquely identify which contracts should appear in the header. 5. Root cause: the header is a curated list (things a reader must know about), not a mechanical index. Kaizen is the right fix for curated-list drift — no automation needed, just periodic sweeps. Also included the ENFORCED status inline so readers see M3 progress at a glance. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp-contract): broaden FALSIFY-MCP-008 condition to match assertions The `assertions:` block already covered descriptions (line 282) but the prose `condition:` above it talked only about "JSON Schema". Readers skimming the condition paragraph would miss that descriptions are also load-bearing. The rewrite preserves the JSON canonicalization language (important — that's the byte-for-byte definition) and adds a second clause spelling out how descriptions flow: directly compared at test time against `ToolDefinition.description`, separate from the build.rs codegen path that carries `inputSchema`. Verified: `cargo test -p aprender-mcp --test falsify_mcp_008` still 6/6 green. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(falsify-mcp-008): refresh module doc-comment for PMAT-514 extension The file-level doc-comment predated the description-equality test added in PMAT-514. Three updates: 1. Opening summary: "byte-identical to the schema" → "byte-identical to the corresponding entry ... covering both the `inputSchema` object and the tool-level `description` string" — so cargo-doc readers see the full gate surface on first hit. 2. Numbered list: step 6 added for the description assertion, keeping the structural schema assertion as step 5. 3. Scope paragraph: "Scope (M3 completion — PR #881 follow-up)" → "Scope (M3 shipped, extended by PMAT-514 on 2026-04-18)" and counts updated from "all 8 Phase-1 tools" to "all 9 registered tools (apr.version + 8 Phase-1 wrappers)" — matches the contract coverage-note landed in 3266e365f. Verified: 6/6 tests still pass. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(book/mcp): sharpen 'edit YAML, rebuild' — descriptions need Rust edit too Previous prose read "The Rust source does not need editing for schemas, and descriptions must track the YAML verbatim" — technically implies descriptions auto-flow from the YAML. They don't: the description string is hand-written in `crates/aprender-mcp/src/tools/<tool>.rs` and must be mirrored manually when the YAML changes. The harness (`tool_descriptions_match_yaml_contract`) fails CI on divergence but does not auto-fix the source. Why this matters: a contributor reading the old wording would think editing only the YAML is enough, push, and then be surprised when CI fails. The new wording makes the two-file edit explicit. Future cleanup: extend `build.rs` to codegen description constants too, then this note can collapse back to "edit YAML only". Not in scope for PMAT-514 — the test-time enforcement is sufficient today. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(aprender-mcp): codegen tool descriptions from YAML contract Extends build.rs to emit `APR_<TOOL>_DESCRIPTION: &str` alongside the existing `APR_<TOOL>_SCHEMA: &str` for each tool in `contracts/apr-mcp-tool-schemas-v1.yaml`. All 9 tool modules now consume `crate::schemas::APR_<TOOL>_DESCRIPTION.to_string()` instead of hand-mirroring the string in Rust source. Five-whys: 1. Why extend codegen? Descriptions drifted silently twice in a 24h window (apr.serve 715781df5, apr.run 91a613968). 2. Why did the test-time gate (PMAT-514) not catch drift before merge? It did — but only after the drift was committed; a compile-time gate prevents the drift from ever building. 3. Why split schema and description into separate constants instead of one merged blob? ToolDefinition's `description` is a Rust String, not JSON; keeping them separate avoids forcing a JSON round-trip on a non-JSON field. 4. Why keep the test-layer `tool_descriptions_match_yaml_contract` if codegen eliminates drift? Defence in depth — catches a future refactor that replaces the codegen consumer with a literal. 5. Why only 9 files to update? 8 Phase-1 wrappers + apr.version are the entire current tool surface. M5 tools will consume the codegen constants from day one. Refs PMAT-514. * test(falsify-mcp-008): codegen-layer description gate + coverage guardrail Adds two new tests to `falsify_mcp_008.rs`: * `codegen_description_constants_match_yaml` — asserts each `schemas::APR_<TOOL>_DESCRIPTION` codegen constant equals `tools[*].description` byte-for-byte. This is a strictly stronger gate than `tool_descriptions_match_yaml_contract`: the live-ToolDefinition test would silently pass if a future refactor replaced `APR_X_DESCRIPTION.to_string()` with a hand-coded literal. Asserting the codegen constant itself closes that bypass route. * `codegen_descriptions_cover_every_tool_name` — mirrors the existing `codegen_constants_cover_every_tool_name` guardrail: every name in `schemas::TOOL_NAMES` must appear in `CODEGEN_DESCRIPTIONS`, catching the case where a new tool is added to YAML but its description constant isn't registered in the test table. Refreshes module-level doc-comment to enumerate 7 layers of coverage and the dual codegen path (SCHEMA + DESCRIPTION). Test count: falsify_mcp_008 grows 6→8; aprender-mcp total 76→78. Refs PMAT-514. * docs(mcp): sync all surfaces with PMAT-514 description-codegen extension Mirrors the build.rs description-codegen change into every doc surface that previously said descriptions were hand-mirrored: * docs/specifications/apr-mcp-server-spec.md — FALSIFY-MCP-008 row now names the codegen-layer test; M3 milestone bullet points at the PMAT-514 extension; suite count 76→78. * contracts/apr-mcp-tool-schemas-v1.yaml — `condition:` prose and `test_harness:` / `codegen_consumer:` pointers describe both `APR_<TOOL>_SCHEMA` and `APR_<TOOL>_DESCRIPTION` codegen paths; tool-registry comment states both fields flow through build.rs. * book/src/tools/mcp-server.md — "edit YAML, rebuild" guidance updated: changing a description now requires only a YAML edit (was: YAML + Rust); enumerates 4 sub-tests (2 live, 2 codegen). * crates/aprender-mcp/README.md — gate-table row references the dual codegen constants. Refs PMAT-514. * chore(roadmap): PMAT-514 record description-codegen discharge line Marks the PMAT-514 roadmap entry with a DISCHARGED acceptance line pointing at the two-layer gate (test-layer + codegen-layer) and the `APR_<TOOL>_DESCRIPTION` build.rs output. The top-level "ongoing kaizen sweeps" acceptance stays — this is one ticket, many sweeps. Refs PMAT-514. * docs(mcp): sync remaining module-doc + README M3 bullet with PMAT-514 Three surfaces still described M3 codegen as "schema only": * docs/specifications/apr-mcp-server-spec.md — file-tree build.rs comment now spells out both constants emitted. * crates/aprender-mcp/README.md — M3 milestone bullet enumerates `APR_<TOOL>_SCHEMA` and `APR_<TOOL>_DESCRIPTION`. * crates/aprender-mcp/src/lib.rs — module-doc for `schemas` now documents both constants, how to consume them, and that hand-coding either is caught by tests/falsify_…
noahgift
added a commit
that referenced
this pull request
Apr 19, 2026
…891/#892) landing (#904) * docs(mcp): spec v1.1.0 — DRAFT → ACTIVE; M1–M3 shipped - Status: DRAFT (pre-implementation) → ACTIVE (M1–M3 shipped; M4 dogfood pending) - Falsification conditions table now annotates ENFORCED / PARTIAL / Deferred per shipped test mappings in contracts/apr-mcp-server-v1.yaml (#886) - Adds FALSIFY-MCP-PROGRESS-001 entry for #887 progress-notification gate - Milestones M1/M2/M3 marked SHIPPED with PR cross-references - M4 acceptance items remain open (real-model gates, dogfood) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp): align spec output shapes with CLI reality (PR #889 falsifications) PR #889 added mock-subprocess e2e tests for FALSIFY-MCP-003/-004 and discovered two spec-vs-CLI mismatches via test failures: 1. apr.run: spec listed `stop_reason` in output — CLI's print_run_output (crates/apr-cli/src/commands/run_entry.rs:279) does NOT emit it. Spec corrected to the actual emitted set (model, text, tokens, ...). 2. apr.qa: spec wrote gates as `{pass, value, threshold}` — CLI's GateResult (crates/apr-cli/src/commands/qa.rs:368) uses `passed` not `pass`. Spec corrected. Also fixes the codegen source reference: FALSIFY-MCP-008 uses contracts/apr-mcp-tool-schemas-v1.yaml (PR #871), not apr-cli-commands-v1.yaml. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(mcp-spec): M1 PR refs #862 → #864 PR #862 is a matmul test fix, not the MCP M1 skeleton. The correct skeleton PR is #864 (`feat(mcp): apr mcp M1 skeleton — MCP server over stdio`). All three stale citations in the M1 milestone replaced. Five-whys root cause: the spec retrofit (#873) reconstructed PR numbers from memory; future retrofits should verify against `git log --grep=...` before committing. Refs PMAT-037. * fix(mcp-spec): demote unmerged contract + M3 PR accuracy Three stale citations corrected in the M3 milestone: - #874 removed from cancellation bullet (#874 is the book-chapter doc commit, not cancellation — that's #883 alone). - `contracts/apr-mcp-server-v1.yaml (ACTIVE) ... (#886)` bullet moved from M3 SHIPPED to M4 IN PROGRESS. PR #886 is still OPEN and its own title says "M4 — apr-mcp-server-v1 contract ACTIVE". The file is not in-tree. Header's "**New**:" label also updated to "Pending (PR #886)" for the same file. - Book-chapter citation expanded to list #874 (M2 creation) + #885 (M3 update) for accurate provenance. Five-whys root cause (false "M3 SHIPPED" on #886): the spec promotion commit (a496ce97c) rolled unmerged M4 work into M3 bullets under the optimistic assumption the PR would land first. Going forward: any bullet citing a PR must verify `gh pr view <N>` is MERGED before promoting a milestone. Refs PMAT-037. * fix(mcp-spec): Architecture — refresh to match built reality The Architecture + Protocol + Out-of-Scope sections carried pre-M1 aspirations that no longer match the shipped crate. Refreshed against actual source tree in crates/aprender-mcp/: - Goal: schema source was `apr-cli-commands-v1.yaml`; lines 11/87/139 correctly cite `apr-mcp-tool-schemas-v1.yaml`. Unified. - Directory diagram: listed absent `schema.rs`; missing `build.rs`, `types.rs`, `tools/subprocess.rs`, `tools/version.rs`. `server.rs` comment said "pmcp::Server wiring" but M1 shipped a hand-rolled JSON-RPC loop (also noted line 149). `Cargo.toml` comment listed pmcp/tokio/clap/apr-cli — none are actual deps (verified: serde, serde_json, anyhow, nix, serde_yaml build, jsonschema dev). `tests/` now lists the four actual `falsify_*.rs` harnesses. - `apr mcp` subcommand: snippet promised `async` with `McpArgs` + transport matching + SSE; actual `run()` is blocking, takes no args, calls `AprMcpServer::new().run_stdio()`. - Protocol/Transport: "SSE optional" was false; flag doesn't exist. Downgraded to stdio-only and added SSE to Out of Scope. Five-whys root cause: the Architecture diagram was authored pre-M1 as a design sketch; later commits (#873 retrofit, v1.1.0 promotion) updated Milestones but never re-diffed the static diagram against `ls crates/aprender-mcp/src/`. Going forward: any spec change touching Milestones must run a diagram-vs-tree check. Follow-up filed: verify Config Precedence (lines 122-126) against implementation — `pub fn run()` consults no env vars today. Refs PMAT-037. * fix(mcp-spec): reconcile 8-vs-9 tool count + Related Work misattribution Two factual errors corrected: - Tool count: spec said "8 Phase-1 tools" (lines 74, 133); `tools/list` actually returns 9 because `apr.version` (M1 scaffold) is also registered. Verified by `crates/aprender-mcp/tests/falsify_m1.rs::falsify_mcp_002_tools_list_schema_shape`, which asserts all 9 names (apr.version + 8 workflow tools). Clarified spec to state "8 Phase-1 workflow tools + apr.version scaffold = 9 total registered" and added test cross-link to the FALSIFY-MCP-002 bullet. - Related Work line 210 claimed `crates/apr-cli/src/tool_commands.rs` is the "planned MCP tool surface (referenced but unimplemented)". That file exists and is the `apr tool` CLI subcommand group (Showcase, Rosetta, …), unrelated to MCP. The actual MCP tool surface lives in `crates/aprender-mcp/src/tools/`. Corrected and noted that rust-mcp-sdk (paiml/rust-mcp-sdk) is currently unused since M1 shipped a hand-rolled JSON-RPC dispatcher. Five-whys root cause (8 vs 9): the original Phase-1 design enumerated 8 workflow tools and `apr.version` was added later as an M1 handshake probe without updating the narrative count. No invariant check cross-references spec tool-count against `tools/list` test assertions. Refs PMAT-037. * fix(mcp-spec): mark config precedence Phase-2 aspirational Lines 122-126 stated a four-level config precedence (`--config`, `$APR_MCP_CONFIG`, `~/.config/apr/mcp.toml`, defaults) as if it were implemented. Actual `crates/apr-cli/src/commands/mcp.rs::run()` takes no arguments and consults no env vars; `AprMcpServer::new()` has no config loader. The `APR_MODEL_DIR` env in the `.mcp.json` snippet is read by the spawned `apr <cmd>` subprocesses, not by the MCP server. Rewrote the section to keep the intended precedence as the Phase-2 contract while making Phase 1's "no config loader" reality explicit. Five-whys root cause: the Configuration section predates the M1 skeleton and was not re-verified against `commands/mcp.rs` during the v1.1.0 promotion. A "spec bullet implies an API — grep for the API" check belongs in the promotion workflow. Refs PMAT-037. * fix(mcp-spec): Success Criteria gate count 8 → 9 Spec's Falsification Conditions section lists 9 entries (FALSIFY-MCP-001 through -008 plus FALSIFY-MCP-PROGRESS-001 added in M3), but the Success Criteria table still said "8 falsification gates". Count corrected and wording clarified to reflect that -003/-004 are currently PARTIAL and must promote to PASS at M4 close. Five-whys root cause: adding PROGRESS-001 in M3 touched the conditions section but didn't update the downstream summary row. Going forward: whenever a new FALSIFY-MCP-* lands, grep the spec for `N falsification` to catch all downstream counts. Refs PMAT-037. * fix(mcp-spec): close residual kaizen items Three dangling claims resolved: - Target version: `v0.32.0 / v0.33.0` stands as the intended release tags but `git tag -l 'v0.3*'` returns only `v0.3.0 / v0.3.1 / v0.30.0`. M1–M3 are merged on `main` but unreleased. Added a clarifier so a reader doesn't assume those tags exist. - Aspirational follow-ons: `apr-mcp-plugin-marketplace-v1.md` and `apr-mcp-hooks-v1.md` are not in `docs/specifications/`. Labelled "(spec files not yet authored)" so readers don't hunt for them. - Risk Register: "pmcp crate API instability" is dormant because M1 shipped a hand-rolled JSON-RPC dispatcher (line 166 already notes pmcp is deferred). Row reworded so the risk's activation condition is explicit. Five-whys root cause (across all three): the spec's non-Milestone sections — Target, Related Work, Risk Register — were not refreshed during v1.1.0 promotion. Every milestone promotion should sweep those sections, not just the milestone table. Refs PMAT-037. * chore(pmcp): bump to 2.3 and drop pforge-runtime (Refs PMAT-037) Five-Whys: - Symptom: dual pmcp versions (1.20 + 2.3) resolved in agents-mcp build. - Why #1: pforge-runtime 0.1.4 (last released 2025-12) still pins pmcp 1.x. - Why #2: pforge-runtime was listed as an optional dep alongside pmcp. - Why #3: it was a forward-compat hedge — but no Rust code imports it (only doc-comment mentions and knowledge-graph string literals). - Why #4: keeping an unused dep doubled the compile footprint and split the pmcp protocol surface across two crates. - Root cause: speculative dep on a framework wrapper for an SDK we already use directly. Fix: - Cargo.toml: bump pmcp 1.10 → 2.3 (PAIML's actively-maintained SDK); remove pforge-runtime dep; agents-mcp feature now just ["agents","pmcp"]. - Doc comments and the mcp_demo example rewritten to name pmcp v2.3 as the SDK instead of pforge. No Rust-level API change — pforge-runtime was never imported, just advertised. - cargo tree -i pmcp now shows a single pmcp v2.3.0 node. Follow-up: spec's pmcp framing (M1 note + Risk Register) still needs rewrite in apr-mcp-server-spec.md. * docs(apr-mcp-spec): v1.2.0 — honest pmcp framing, add M5 migration plan (Refs PMAT-037) Five-Whys: - Symptom: spec framed pmcp as unstable/dormant, treating the SDK as risk rather than planned substrate. - Why #1: Risk Register called out "pmcp crate API instability (dormant...)" — language from before pmcp was actively maintained. - Why #2: M1 note said "pmcp SDK deferred — more deterministic for current scope" without explaining the actual technical rationale. - Why #3: no adoption path existed — M4 stops at dogfood, so readers couldn't tell whether pmcp would ever land. - Why #4: pmcp v2.3 is PAIML's own crate (paiml/rust-mcp-sdk) and already used by aprender-orchestrate; keeping the spec's out-of-date framing forced the /tmp/spec-update session to discover this from crates.io. - Root cause: stale spec language from the early M1 period where the adoption path was genuinely uncertain; never updated after pmcp stabilised. Fix: - Line 15: link now labels pmcp as "PAIML's Rust MCP SDK, actively maintained, v2.3.1 on crates.io (2026-04-16)". - Line 44 / 167: architecture + M1 note explain the three concrete reasons the dispatcher is hand-rolled (minimal request/response shape over `apr <cmd> --json`, build.rs schema codegen keeps tools/list byte-identical to contract YAML, falsification asserts on wire bytes without an SDK layer). - Risk Register row rewritten from "API instability" to "adoption-path coordination" — real risk is workspace version alignment with the pmcp client role in aprender-orchestrate. Mitigation: single workspace-wide bump + `cargo tree -d` CI gate. - New M5 milestone: concrete pmcp migration plan — port dispatcher to pmcp::Server (retain build.rs codegen), add SSE + WebSocket transports, re-run falsification suite post-migration. - Out of Scope: SSE/WebSocket transports reclassified as "scheduled for M5 on top of pmcp v2.3". - Related Work: pmcp-sdk contract row now notes aprender-orchestrate already links pmcp v2.3 as a client; server-side migration is M5. - Version bumped 1.1.0 → 1.2.0. * docs(mcp-spec): reconcile M4 gate count with PR #886; bump pmcp contract v2.3 (Refs PMAT-037) Five-Whys: - Symptom: M4 bullet claimed "9 falsification_conditions" to match the 9 gates listed in Section 145, but PR #886's contract pins exactly 8 (FALSIFY-MCP-001..008) and a Rust test enforces that invariant. - Why #1: the 9th gate (FALSIFY-MCP-PROGRESS-001) was added in M3 AFTER PR #886 was drafted. - Why #2: PR #886's harness (apr_mcp_server_contract_ids_are_falsify_mcp_001_through_008) explicitly rejects anything outside 001..008, so the contract row for PROGRESS-001 cannot land in the same PR without harness changes. - Why #3: the spec's earlier count-reconciliation (2026-04-18 prior kaizen round) missed this because it was looking for text matches, not contract row counts. - Root cause: spec and contract evolved on different PR branches. Fix: - M4 bullet: accurately describes PR #886 as landing 8 falsification rows, names the exact-8 invariant by its test function. - Adds an explicit follow-up bullet: "Extend the contract with a 9th row for PROGRESS-001 after PR #886 merges — relax the exact-8 invariant to 'FALSIFY-MCP-001..008 + PROGRESS-001, no extras'". - Success Criteria table unchanged (line 220 still correctly says "9 falsification gates ... all PASS or PARTIAL→PASS by M4 close") — the 9th gate is already ENFORCED in code via falsify_mcp_progress_001.rs, we just need the contract YAML to catch up. Also: - contracts/pmcp/mcp-protocol-sdk-v1.yaml version 1.0.0 → 1.1.0 with "last_modified: 2026-04-18". - Description updated v2.1 → v2.3, adds consumer-of-record (aprender- orchestrate via agents-mcp feature) + future consumer (aprender-mcp M5 migration) + link to apr-mcp-server-spec.md. * docs(book/mcp): align M3 scope + add M5 pmcp migration row (Refs PMAT-037) Five-Whys: - Symptom: book chapter's M3 row missed FALSIFY-MCP-PROGRESS-001 (shipped via PR #887) and the paragraph called progress streaming "a follow-up slice" for BOTH apr.run and apr.finetune — incorrect for apr.finetune. - Why #1: book chapter was authored before PR #887 landed progressToken-gated notifications for apr.finetune. - Why #2: M5 pmcp migration (added to spec v1.2.0 today) had no corresponding row in the book status table. - Root cause: book lagged spec after the M3 progress slice merged and after the M5 migration plan was formalised today. Fix: - M3 row now mentions the opt-in progress notifications. - Paragraph specifies: FALSIFY-MCP-PROGRESS-001 is enforced for apr.finetune; only per-step structured progress (CLI event channel prereq) and apr.run progress (apr run --stream flag prereq) remain open. - New M5 row in the status table mirrors the spec's M5 milestone. * docs(mcp-spec): tighten streaming claim + M5 transport pointer (Refs PMAT-037) Five-Whys: - Symptom: Section "Protocol" bullet on Streaming claimed "apr.run and apr.finetune send notifications/progress for each decoded token / training step" — but apr.run progress is a deferred M4 item and apr.finetune only emits per-stdout-line progress (not per training step) and only when the client opts in via progressToken. - Why #1: the bullet was authored when both tools were planned to stream per-token. Reality diverged: progress landed for apr.finetune only (opt-in, per-line), apr.run was deferred. - Why #2: the Architecture paragraph pointed to "Phase 2 with SSE" for transport selection without naming the actual M5 milestone that now schedules it. - Root cause: drift between aspirational early-M2 text and the M3/M5 structure formalised today. Fix: - Streaming bullet now names what's actually enforced (FALSIFY-MCP-PROGRESS-001, apr.finetune opt-in, per-stdout-line) and explicitly calls out the apr.run follow-up prereq (apr run --stream flag + per-step CLI event channel). - Architecture paragraph points at M5 as the SSE/WebSocket landing spot rather than the generic "Phase 2". * fix(examples): unblock Chapter Examples Compile on main (Refs PMAT-037) Five-Whys: - Symptom: CI job "Chapter Examples Compile" has been failing on every push to main since PR #701 (2 days), plus on this PR, with RUSTFLAGS= "-D warnings" promoting unused-import warnings to hard errors. - Why #1: ch10_training and ch24_switch_pytorch both import `aprender::nn::Optimizer` but only call `optimizer.step_with_params`, which is an inherent method on `SGD` (not a trait method) — so the trait import is genuinely unused. - Why #2: ch26_switch_ndarray binds `let pred = lr.predict(&x)` but never reads `pred` (score re-computes internally). - Why #3: these examples predate the refactor that moved `step_with_params` from the Optimizer trait to inherent impls; the trait import was never cleaned up. - Why #4: the Book Contract Enforcement and Chapter Examples Compile jobs are non-required checks, so the red status never blocked merges and accumulated as tech debt. - Root cause: main CI andon rule (main must always be green) was waived for non-required checks. Toyota Way: "all defects are your defects" — fix it regardless of whose PR introduced it. Fix: - ch10_training.rs, ch24_switch_pytorch.rs: drop `Optimizer` from the aprender::nn:: import list. - ch26_switch_ndarray.rs: consume `pred` by printing the first prediction — preserves pedagogical intent of showing predict() works, and unblocks -D warnings. - `cargo build -p aprender-core --examples` now warnings-clean. * fix(ci): use contract: pointer, not derived PCU path (Refs PMAT-037) The "Every PCU page has matching contract" gate derived paths from the PCU ID (`apr-page-${ID}-v1.yaml` / `apr-book-${ID}-v1.yaml`) but real page headers already carry an authoritative `contract:` field, and chapter contracts are named `apr-book-ch01-v1.yaml` (chapter-number only) while PCU IDs include a slug (`ch01-why-rust`). The mismatch failed all 27+ book pages on every run. Five whys: 1. Why red? Script can't find `apr-page-tools-apr-cli-v1.yaml` from ID `tools-apr-cli`... wait it can. But for chapters it looks for `apr-book-ch01-why-rust-v1.yaml` which doesn't exist. 2. Why does it derive? The earlier convention stored ID-derived paths before `contract:` was added to headers. 3. Why not updated when `contract:` was added? The workflow was not migrated; the two lookup paths stopped covering all cases. 4. Why silent until now? The gate was not blocking main. 5. Why fix now? Kaizen sweep surfaced 27-page failure. Parse the authoritative `contract:` field. Also add missing PCU header + page contract for book/src/tools/mcp-server.md (now points to contracts/apr-page-tools-mcp-server-v1.yaml). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp): retire stale 'M3 will ship apr.serve lifecycle' (Refs PMAT-037) Three places claimed `apr.serve` cancellation lands in M3: - book/src/tools/mcp-server.md apr.serve paragraph - crates/aprender-mcp/src/tools/serve.rs module/fn docs - serve tool `description` field embedded in tools/list M3 actually shipped `notifications/cancelled` for apr.run only. `server.rs::CancelHandle` doc explicitly states: "Only apr.run currently honours cancellation." apr.serve remains fire-and-forget and the spec M3 bullet list never promised otherwise. Five whys: 1. Why stale? Comments predicted M3 scope before scope narrowed. 2. Why narrowed? Spec M3 scope: FALSIFY-MCP-006 for apr.run, -008 codegen, -PROGRESS-001 for apr.finetune. apr.serve lifecycle was never inside that gate set. 3. Why not updated at M3 close? No acceptance criterion forced a sweep of surface prose when milestone shipped. 4. Why matters now? Readers of book/tools page and users calling apr.serve via MCP get incorrect "lifecycle lands in M3" note that reads as imminent, not aspirational. 5. Why fix now? Kaizen sweep surfaced; retarget to M5 where a daemon registry + pmcp Server port belong together. Edits: book paragraph + serve.rs module header + serve.rs `call` docstring + serve.rs description field + spec M5 new bullet for apr.serve cancel extension. Also spec M5 falsification-suite bullet updated from "71+ tests" to measured "75 tests" with file list. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(book/mcp): clarify apr.finetune progress shipped with limits (Refs PMAT-037) The apr.finetune paragraph said "Per-step notifications/progress streaming is a follow-up M3 slice" — read as "no progress yet" — but FALSIFY-MCP-PROGRESS-001 shipped in PR #887: per-line progress over `params._meta.progressToken` IS live. Five whys: 1. Why stale? Paragraph was written before PR #887 merged. 2. Why not updated at PR #887? PR focused on server.rs + test additions; book paragraph not flagged in review. 3. Why matters? Clients reading the book will assume they cannot stream updates and skip progressToken, losing observability. 4. Why two progress layers? Per-line (shipped, stdout-driven) vs per-step (needs a CLI event channel from `apr finetune` itself) — the former is cheap plumbing over JSON-RPC, the latter is a CLI-side refactor. 5. Why fix now? Kaizen sweep surfaced. Rewrote the paragraph to state (a) what shipped (opt-in per-line), (b) the gate it satisfies (FALSIFY-MCP-PROGRESS-001), (c) the honest limitation (terminal blob today), (d) where per-step lives (M4 follow-up with CLI prereq). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * contract(mcp-schemas): retire 'retrofit-only' header, lock v1.1.0 (Refs PMAT-037) The apr-mcp-tool-schemas-v1.yaml header still read: "This M2 cut is RETROFIT-ONLY" "If this file ever disagrees with the Rust source, the Rust source wins" "In milestone M3 a build.rs at ... will read this YAML" All three are post-M3 stale: 1. M3 shipped (PRs #880, #884) — build.rs is live. 2. Byte-identity is enforced by tests/falsify_mcp_008.rs (5 tests). 3. Rust tool sources contain zero hand-written schemas — they only parse `crate::schemas::APR_<TOOL>_SCHEMA` from $OUT_DIR. 4. Direction is reversed: YAML authoritative, Rust derived. Five whys: 1. Why stale header? Written for M2 retrofit cut. 2. Why not flipped at M3 close? PR #884 focused on codegen, not contract prose. 3. Why matters? Future readers will assume Rust source is the authority and "fix" the wrong side of a drift — inverting FALSIFY-MCP-008's intent. 4. Why now? Kaizen sweep. 5. Why v1.1.0? Semantic bump: authoritativeness change, plus new reference pointer to apr-mcp-server-spec.md. Bumped metadata version 1.0.0 → 1.1.0, added last_modified, rewrote header and description to reflect current state (YAML is SoT, Rust parses codegen constants, falsify_mcp_008.rs enforces byte-identity). Also updated spec M5 falsification-suite file list to include `falsify_mcp_008` and drop nonexistent `codegen_bytes`. Verified: `cargo test -p aprender-mcp --test falsify_mcp_008` — 5/5 pass after YAML comment edits (no functional change, just prose). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp-spec): 57 → 58 CLI commands (mcp added PR #864) (Refs PMAT-037) The spec claimed a 57-command CLI surface three times: - Contracts bullet: "57-command tool surface" - Problem paragraph: "57-subcommand CLI" - Goal paragraph: "subset of the 57 apr CLI commands" PR #864 registered `apr mcp` as the 58th command (contracts/apr-cli-commands-v1.yaml). The 63-line count in the contract is 58 commands + 5 FALSIFY-CLI-00* falsification rules. Five whys: 1. Why stale? The 57 figure dates to #701 contract landing (2026-04-06) — the initial MCP PRs added `apr mcp` but didn't sweep cross-cutting doc claims. 2. Why matters? MCP spec's own subject command is the 58th — a reader comparing counts will mistrust the surface-area claim. 3. Why only fixing here? Scope is `apr-mcp-server-spec.md`; CLAUDE.md and apr-book-spec.md have broader audiences and want their own kaizen passes. 4. Why cite PR #864 inline? Makes the delta auditable by a future reviewer checking `git log --oneline apr-cli-commands-v1.yaml`. 5. Why not reword to "58+ commands" for future-proofing? The contract is the source of truth; stale counts are better caught by an exact-match CI gate than smeared over with imprecise phrasing. (PR #864 added a FALSIFY-CLI gate.) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp-spec): honest release-target footer (M3 shipped same week as M2) (Refs PMAT-037) The footer claimed: v0.32.0 (M1–M2), v0.33.0 (M3–M4) But M3 shipped on 2026-04-18, same week as M2 (2026-04-17/18), and the workspace is still at v0.30.0 on main. The old split-tag plan (M1–M2 in one release, M3–M4 in the next) no longer maps to reality — M3 will publish alongside M1–M2 because there's nothing to publish in between. Five whys: 1. Why stale? Target was written assuming M2 → cut release → M3. 2. Why reality diverged? M3 landed fast because cancellation + codegen + progress + apr.finetune were all independent PRs. 3. Why matters? A reader looking at `git tag` + this footer would expect v0.32.0 to exist; it doesn't. 4. Why not assign firm tags? Release cuts require a separate decision (changelog + publishing); this spec shouldn't preempt it. 5. Why keep historical context? Future reader asking "why is the M3–M4 split collapsed?" deserves a traceable answer instead of silently rewritten history. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(aprender-mcp/README): sync milestones + full gate table (Refs PMAT-037) The crate README was three milestones behind the spec: - M2 bullet: "apr.serve (fire-and-forget; full lifecycle in M3)" — M3 shipped apr.run cancel only; serve registry is M5. - M3 bullet: "in progress" — M3 actually shipped 2026-04-18 (PRs #880, #881, #883, #884, #887). - Gate table: listed 5 gates (001, 002, 005, 007, VALIDATE-001); missed 003, 004, 006, 008, PROGRESS-001 — 4 of 5 are now ENFORCED or PARTIAL, and PROGRESS-001 is net-new since M3. Five whys: 1. Why lag? README is surface-facing, spec/code are the primary targets during milestone closes. 2. Why matters? crates.io readers land here first — inaccurate milestone + gate table = miscalibrated expectations, especially about apr.serve cancellation. 3. Why add status column? Distinguishing ENFORCED vs PARTIAL vs planned is what readers actually want when choosing whether to depend on a given gate. 4. Why spell out M4 + M5 here? Same reason — readers want to know what's next, not dig through the spec. 5. Why fix now? Kaizen sweep; PR #888 already touches this crate. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(README): 57 → 58 commands across 4 sites (Refs PMAT-037) The MCP spec already reconciled 57 → 58 (PR #864 added `apr mcp` as the 58th command in contracts/apr-cli-commands-v1.yaml). The root README still repeated 57 in four places: headline paragraph, stats bullet list, crate-layout tree comment, and smoke-test snippet. Keeping the count exact matters more than soft-pedalling it — PR #864 also added a FALSIFY-CLI gate that enforces `apr --help` listing against the YAML, so drift is caught at CI and the README should track it. Fixing here alongside the spec keeps the docs audit self-consistent within one PR. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(orchestrate/book): pmcp 1.8 → 2.3, drop pforge-runtime (Refs PMAT-037) Two orchestrate book pages carried stale pmcp/pforge references: - part3/pmcp.md — header still claimed pmcp v1.8.6 and showed `pmcp = "1.8"` in Cargo.toml snippet. crates.io has pmcp 2.3.1 as of 2026-04-16 and the crate's Cargo.toml already pins it. - part3/agent-runtime.md L575 — `agents-mcp = ["agents", "pmcp", "pforge-runtime"]` but pforge-runtime was dropped earlier in this PR series (it pinned pmcp 1.20 and was unused outside knowledge-graph cataloguing). Five whys for each: 1. Why stale? Book pages were written against pmcp 1.x, before the 2.x release cleanup. 2. Why not caught? The orchestrate book has no CI gate matching its Cargo.toml snippets to actual crate deps. 3. Why matters? Readers copy-pasting `pmcp = "1.8"` into a new project would land on a yanked / unmaintained line. 4. Why not add a CI gate? Out of PR scope; filed mentally as an M5+ follow-up when `apr-contracts` lints cross-project snippets. 5. Why fix now? Kaizen sweep surfaced during pmcp/pforge audit. Both archived batuta-agent.md references left alone — they live in `docs/specifications/archive/` and document the old design state. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(CLAUDE.md): 57 → 58 commands, add mcp to key-command list (Refs PMAT-037) Three stale 57-command claims in CLAUDE.md — the overview line, the key-files bullet, and the APR CLI section. Brought them in line with contracts/apr-cli-commands-v1.yaml (58 commands including `apr mcp`, added PR #864). Also added `mcp` to the inline key-command list — discovery matters more than alphabetical tradition given the MCP spec is the current top-of-mind work. The 405-contract and 25,300-test counts are out of spec scope and left for a future sweep (workspace tests reportedly 25,391 per the root README, but confirming across the 70 crates needs real `cargo test --workspace --lib` run, not a file read). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp-spec): document FALSIFY-MCP-VALIDATE-001 dispatcher invariant Symptom: spec Falsification Conditions section had 9 entries (MCP-001..008 + PROGRESS-001), but crates/aprender-mcp/README.md and book/src/tools/mcp-server.md both list a 10th enforced gate, FALSIFY-MCP-VALIDATE-001, which was missing from the spec entirely. Five-whys: (1) spec only lists conditions destined for apr-mcp-server-v1.yaml; (2) VALIDATE-001 is a dispatcher-level contract point (how the server shapes tool errors), not a per-tool behavioural promise; (3) it therefore lives *alongside* but *outside* the YAML contract — mirrored in the book under "Additional invariant enforced by the dispatcher"; (4) the spec's own section header ("Falsification Conditions for apr-mcp-server-v1.yaml") excluded it by scope, but the omission reads as "we forgot a gate" to anyone cross-referencing README/book; (5) fix is to add an "Additional dispatcher invariant" subsection pointing at the existing test falsify_m1.rs::falsify_validate_missing_model_path_is_tool_error. Refs PMAT-037 * docs(aprender-mcp): refresh module-level scope docs for M3-shipped state Symptom: `src/lib.rs` crate-level docs titled the scope section "M1 Scope" and claimed "M2 adds the 8 Phase-1 tools"; `src/tools/mod.rs` said "M3 adds `apr.finetune` (synchronous initial slice; streaming is a follow-up)"; and `src/server.rs` had a test doc-comment reading "Full 8-tool set lands when M2 completes." All three predate M3 shipping on 2026-04-18. Five-whys: (1) module docs were written incrementally milestone-by- milestone; (2) each PR updated its own surface but left sibling module docs unchanged; (3) there is no CI gate on module-level Rustdoc matching milestone status; (4) new readers start at `lib.rs` and encounter text that contradicts `apr mcp --help` + README; (5) cheapest fix is to rewrite the three doc-comments to a single authoritative summary keyed off the spec's own "M1–M3 SHIPPED" tags, leaving M4/M5 forward-looking. No behaviour change; no test updates needed. Refs PMAT-037 * docs(mcp): update apr.finetune/apr.run docs for shipped-M3 progress state Symptom: three stale M3 claims, each LLM-visible or reader-visible: (1) `apr.finetune`'s `description` field still read "Progress streaming lands in a follow-up M3 slice" — but PR #887 shipped the streaming slice on 2026-04-18, and the description is returned verbatim in `tools/list` to LLM clients. (2) The same stale sentence is duplicated in the authoritative `contracts/apr-mcp-tool-schemas-v1.yaml`. (3) `src/tools/run.rs` module docs say "Progress notifications (streamed per-token) are a separate M3 slice" — the spec's M3 checklist (line 192) now records that as deferred to M4 pending `apr run --stream`. Five-whys: (1) tool `description` fields are hand-written strings that become part of the MCP wire response; (2) FALSIFY-MCP-008 compares `inputSchema` byte-for-byte but *not* `description`, so description drift is silent; (3) when PR #887 shipped progress streaming, only the crate module docs in finetune.rs were partially updated — the `description` field and the YAML contract were missed; (4) stale LLM- visible strings confuse agents about which call shape actually works today; (5) fix is to (a) promise exactly what ships (opt-in via `params._meta.progressToken`, falsification gate PROGRESS-001), (b) align the YAML contract and Rust source, and (c) rewrite `apr.run`'s module prelude to describe the cancel-token surface that shipped and the per-token progress that didn't. Verified: `cargo test -p aprender-mcp --test falsify_mcp_008` passes (5/5). Description field is not covered by the schema gate, confirming the drift was invisible to CI until now. Refs PMAT-037 * docs(mcp-spec): cross-link M4 checklist items to the PRs carrying them Symptom: M4 checklist items in the milestone section all read "in flight" / "dogfood" without referencing any PR, even though six open PRs (#886, #889, #890, #891, #892, plus our own #888) are carrying this exact work. Readers who arrive from the PR list can't map a PR onto the spec box it's trying to tick, and readers who arrive from the spec can't find the open PR. Also added a `falsify_mcp_progress_001.rs` row to the crate-layout tree (previously omitted) and broadened the `falsify_m1.rs` description to mention all gates it enforces (-001, -002, -005, -007, -VALIDATE-001), not just the first two. Five-whys: (1) M4 work is happening across 4+ PRs in parallel; (2) the spec was last edited when only PR #886 existed; (3) new PRs (#889/#890/#891/#892) introduced new gate IDs (FALSIFY-MCP-E2E-001, FALSIFY-MCP-DOGFOOD-001, FALSIFY-MCP-PROGRESS-002) but the spec never reflected them; (4) without PR cross-links, the spec drifts out of sync within days; (5) fix is to name the branch + PR for each in-flight box so the linkage is obvious and breaks visibly when a PR is closed or renamed. Refs PMAT-037 * docs(contracts): fix stale 57-command count + codegen test path Two small contract-metadata fixes caught by the kaizen sweep: 1. `contracts/apr-cli-commands-v1.yaml` line 24 — `scope` field still claimed "57 commands"; the actual command list has 58 entries as of PR #864 (apr mcp added 2026-04-17). Verified by counting `^ - name:` entries under the `commands:` key (`awk` filter — 58). 2. `contracts/apr-mcp-tool-schemas-v1.yaml` had two sibling errors: (a) Block-comment header line 7 still said "each of its 57 entries" referring to apr-cli-commands-v1.yaml — updated to 58 to stay in sync with the registry. (b) `metadata.description` pointed readers at `tests/codegen_bytes.rs` for FALSIFY-MCP-008 enforcement; the actual file is `crates/aprender-mcp/tests/falsify_mcp_008.rs` (confirmed via `ls crates/aprender-mcp/tests/`). The wrong path is particularly bad because new contributors clone the repo and try to grep for a file that doesn't exist. Five-whys on (2b): (1) an earlier contract rev proposed the filename `codegen_bytes.rs`; (2) the commit that renamed it to `falsify_mcp_008.rs` (conventions: one test file per FALSIFY gate) didn't update the contract metadata; (3) nothing in CI cross-checks prose filename references inside YAML headers; (4) the spec we edited in PR #888 already fixed this in one spot but missed the sibling in this file; (5) the cheapest fix is a literal string replace — adding a lint for "tests/[a-z_]+\.rs" strings that don't resolve is follow-on work, tracked separately. Refs PMAT-037 * docs(contracts): bump 57→58 command count in apr-cli-publish + apr-cli-qa Symptom: the two CLI-level contracts that gate `cargo install` and dogfood QA still asserted "all 57 commands" in their postconditions, falsification predictions, and proof_obligations. The actual `apr --help` surface is 58 commands as of PR #864 (mcp added 2026-04-17), and `contracts/apr-cli-commands-v1.yaml` was already updated to 58 in the previous commit. Affected invariants: - apr-cli-publish-v1.yaml equations.all_commands_compile formula - FALSIFY-PUB-CLI-003 prediction ("apr --help lists all N commands") - apr-cli-qa-v1.yaml postconditions, FALSIFY-QA-001 rule, and proof_obligations[0].property Why this matters: when these prose counts go stale, an engineer reading the contract reasonably concludes either (a) the contract is behind reality and they should doubt it, or (b) the list of commands was shortened and a command got removed — neither is true. Five-whys: (1) the mcp command was added via PR #864 with contract update constrained to apr-cli-commands-v1.yaml; (2) sibling contracts that reference the count (publish + qa) were not updated in the same PR; (3) no CI linter cross-checks "N commands" strings against the authoritative registry count; (4) the drift persisted for ~1 day and would have confused contract reviewers on the next spec pass; (5) fix is bulk text replace plus a mental note to add a numeric cross-check linter in a follow-up (tracked separately). No test iteration count changes (the harnesses iterate the contract YAML entries, not the hardcoded number). The strings are readability only. Refs PMAT-037 * docs: bump 57→58 command count in book + spec prose Surface-prose sweep after bumping the two load-bearing contracts (apr-cli-publish + apr-cli-qa) in the previous commit. Same root cause: PR #864 added `apr mcp` as the 58th command but prose references scattered through the book and spec suite were not updated in lockstep. Touched (one literal "57 commands" → "58 commands" per line): - book/src/architecture/monorepo-layout.md — crate-tree caption - docs/specifications/apr-cli-qa-spec.md — 4 sites (problem framing, structural gate cell, Phase-1 section heading, Phase-8 grid line) - docs/specifications/aprender-monorepo-consolidation.md — the "Users NEVER pass --features" principle (line 414); the historical "DONE" entry at line 618 is left at 57 because it describes the phase as it was completed, not current state - docs/specifications/aprender-readme-book-rewrite.md — book tree caption Not touched (out of scope for this sweep): - docs/hero.svg and docs/specifications/apr-book-spec.md — user-facing graphics + marketing copy; will sweep separately - archive/ and examples/ — either historical or println strings with lower blast radius - .claude/skills/dogfood/SKILL.md — dogfood skill instruction, queued Refs PMAT-037 * docs(book/mcp): add FALSIFY-MCP-PROGRESS-001 row to gates table The book's falsification-gates table in book/src/tools/mcp-server.md listed rows for FALSIFY-MCP-001..008 and then the dispatcher-level FALSIFY-MCP-VALIDATE-001, but skipped the M3 addition FALSIFY-MCP-PROGRESS-001 that the spec already calls out as item 9 of the contract-bound gates (apr-mcp-server-spec.md#L159) and that the success-criteria row counts as part of the "9 falsification gates (FALSIFY-MCP-001..008 + PROGRESS-001)" invariant (L228). Five whys: - Symptom: book table shows 8 contract gates, spec says 9. - Why: PROGRESS-001 row was never added when M3 shipped (#887). - Why: M3 PR #887 landed PROGRESS-001 behaviour + test but did not touch the book's gates table (touched the narrative section only). - Why: the gates table is organized numerically and the PR author added PROGRESS-001 to the prose but not to the table below it. - Root cause: the table is a cross-cutting artifact that any new gate must be added to — no codegen pressure, no CI guard. - Fix: add the row now; future change: fold this into contract-driven codegen when apr-mcp-server-v1.yaml lands (PR #886, tracked for M4). Refs PMAT-037, FALSIFY-MCP-PROGRESS-001 * docs(aprender-mcp/README): fix 8→9 tools count in M3 codegen coverage The M3 entry said build.rs generates schemas for "all 8 tools"; in fact the contract apr-mcp-tool-schemas-v1.yaml has 9 entries (the M1 apr.version scaffold + the 8 Phase-1 workflow tools), and build.rs emits one pub const APR_<TOOL>_SCHEMA per entry for all 9. Five whys: - Symptom: README says "all 8 tools"; contract has 9 tool entries. - Why: the "8 tools" figure was the Phase-1 workflow-tool count. - Why: when FALSIFY-MCP-008 expanded to codegen every tool in M3 it picked up apr.version too, but the README M3 bullet kept the Phase-1-focused "8 tools" wording. - Why: the Phase-1 count and the registered-tool count are both in circulation in docs (spec refers to both as "8 Phase-1 tools plus apr.version") and it's easy to conflate them. - Root cause: no single-sourcing of the tool-count number — any doc can drift from `contracts/apr-mcp-tool-schemas-v1.yaml` (the authoritative list) silently. - Fix now: split the count honestly ("8th Phase-1 workflow tool — 9th registered" and "all 9 registered tools"); deferred fix: when the spec's M4 contract promotion (PR #886) lands, add a FALSIFY-MCP-008-style codegen check that the tool-count numbers in README/spec/book match the YAML row count. Refs PMAT-037 * docs: sweep remaining 57→58 command drift in book + spec prose Five prose sites still carried the stale 57-command count after the earlier commits bumped the contract YAMLs and the monorepo/crate-tree captions: - book/src/introduction.md (2 occurrences — "What is Aprender?" headline + CLI Reference bullet) - docs/specifications/apr-book-spec.md (2 occurrences — Ch 1.5 entry + Appendix A crate-map row for apr-cli) - docs/specifications/aprender-readme-book-rewrite.md (2 occurrences — Problem section intro + "What is aprender?" bullet) Why these were missed earlier: the previous sweep focused on contract YAMLs (apr-cli-commands-v1, apr-cli-publish-v1, apr-cli-qa-v1) + the monorepo layout crate-tree captions. These prose sites live in discursive book/spec text and weren't caught by the YAML-first grep. Scope discipline preserved: left the two intentional historical references alone — aprender-monorepo-consolidation.md#L618 DONE history line and apr-mcp-server-spec.md#L10/#L21 which say "58 commands (57 + mcp added PR #864)" on purpose to explain the jump. Refs PMAT-037 * docs(aprender-mcp/validate): refresh stale 'remaining 7 will follow' doc-comment The module doc-comment for apr.validate still read as if M2 was in progress — "the remaining 7 Phase-1 tools will follow: spawn apr <subcommand> --json...". M2 shipped 2026-04-17/18 (#865, #866, #867, #870, #872) and M3 shipped 2026-04-18 (#881), so all 7 M2 wrappers plus the M3 apr.finetune addition now live on this pattern. Updated to present-tense enumeration: lists each wrapper by name and makes explicit that apr.finetune also inherits the subprocess pattern, so a reader landing on this file first gets the full shape of what ships. Five whys: - Symptom: validate.rs doc-comment describes M2 as future work. - Why: comment was written when apr.validate was the first-shipped wrapper (#865) and the other 6 were still PRs. - Why: subsequent wrapper PRs (#866, #867, #870, #872) and the M3 addition (#881) didn't circle back to retire the "will follow" tense on the earliest module. - Why: no codegen or lint forced doc-comments to reference contract-driven tool counts, so the prose drifted silently. - Root cause: module doc-comments are low-visibility — they don't show up in tools/list output, so FALSIFY-MCP-008 doesn't catch them. - Fix: manual sweep now; longer-term, an apr-mcp doc-invariant contract could codegen "shipped tools" lists from the registry. Refs PMAT-037 * docs(mcp-contract): sync apr.serve description with source truth The YAML contract still said "Full lifecycle (cancel/SIGTERM) lands in M3." — but M3 shipped weeks ago (finetune + opt-in progress) and serve lifecycle was deferred to a post-M3 follow-up. The source-of-truth description in `crates/aprender-mcp/src/tools/serve.rs:44-46` already reads "Cancel-token lifecycle (SIGTERM) is a post-M3 follow-up" — the contract YAML is the one that drifted. Five-whys 1. Why did the YAML description drift from the source? → FALSIFY-MCP-008 only asserts byte-identity on the `inputSchema` (properties/required), not on the tool-level description. 2. Why was FALSIFY-MCP-008 scoped that way? → Descriptions are LLM-visible free-form prose that humans edit in both places during development; byte-comparing them every build would churn CI. 3. Why did the divergence survive post-M3? → No periodic kaizen sweep compares YAML tool descriptions with their source counterparts. 4. Why didn't any kanban/release task catch it? → Release templates don't list the MCP contract YAML among per-milestone artifacts to refresh. 5. Why not? → Contract YAML changes are treated as codegen input, not documentation — so prose rot goes unnoticed until a kaizen pass. Symptom fixed; root-cause follow-up (a byte-compare for descriptions, or a lint that forbids roadmap-tense phrases like "lands in Mx" after that milestone ships) is tracked for a future pass — not a PMAT-037 blocker because descriptions are advisory for LLM clients and the actual tool behaviour is covered by FALSIFY-MCP-005/007/008. Refs PMAT-037 * docs(mcp-contract): drop false stop_reason claim from apr.run description YAML + source both advertised that apr.run "returns tokens + tok/s + stop reason", but the apr CLI does not emit `stop_reason`. Spec line 90 of apr-mcp-server-spec.md records the ground truth: CLI as of 2026-04-18; `stop_reason` not emitted Replaced with an accurate inventory ("generated text, tokens, tok/s, and timing") plus the cancellation note that is genuinely load-bearing for MCP clients (FALSIFY-MCP-005 asserts cancel wiring). Five-whys 1. Why did the description promise a field the CLI doesn't emit? → The description was written speculatively ahead of a planned `apr run --json` enrichment that never landed. 2. Why did the speculative doc survive? → FALSIFY-MCP-008 compares inputSchema byte-for-byte, but does NOT compare the tool description to the actual CLI response keys. 3. Why doesn't any gate detect output-shape drift? → apr.run returns free-form stdout bytes to the MCP client; there is no typed contract on the response shape. 4. Why not? → The MCP tool surface is intentionally a pass-through so the CLI can evolve without churning the MCP spec. 5. Why does that hurt here? → Pass-through evolution needs matching doc-hygiene passes (like this one) to keep the LLM-visible description honest. Same root-cause class as the apr.serve fix one commit back. Same class of drift as 715781df5 (apr.serve "lands in M3"). Tracking a shared follow-up: lint for roadmap-tense phrases and a smoke-test that the description's field enumeration is a subset of the CLI's actual JSON keys. Refs PMAT-037 * docs(mcp-spec): clarify Success Criteria scope — spec ACTIVE, gate is for M4 close The header reads "Acceptance gate for promoting to ACTIVE" — but the spec status at the top already says ACTIVE (promoted at M3 ship on 2026-04-18). The criteria listed (contract-level gates, 9-gate pass including the M4 dogfood session) actually describe **closing M4** — promoting `apr-mcp-server-v1.yaml` from DRAFT to ENFORCED and lifting FALSIFY-MCP-003/-004 from PARTIAL to PASS. Five-whys 1. Why does "promoting to ACTIVE" survive past ACTIVE promotion? → The Success Criteria block was drafted pre-M3 when the spec was still DRAFT, and was never re-scoped after the M3 ship flipped the spec header to ACTIVE. 2. Why did no gate force a re-scope? → The spec's own header was updated in the same commit that set the status, but the mid-doc sections weren't traversed because nothing links them to the header change. 3. Why isn't that traversal automated? → provable-contracts' doc_integrity checker validates cross-links between spec and contract YAML, not internal consistency of roadmap language across sections of the same spec. 4. Why is internal consistency not a contract check? → Roadmap language ("will ship", "pending", "ACTIVE") is prose, not structured data — hard to assert byte-for-byte. 5. Why not structure the status fields? → Longer-term work; this commit is the symptom fix so readers can trust the Success Criteria block against the spec header. Now readers see: - Spec header: ACTIVE - Success Criteria: gate for closing M4 (contract DRAFT→ENFORCED, FALSIFY-MCP-003/-004 PARTIAL→PASS, dogfood done) That's the actual open-work framing. Refs PMAT-037 * docs(book/mcp): fix stale apr.version example payload (0.31.0 → 0.30.0) The book's apr.version example response used "0.31.0", but the tool emits CARGO_PKG_VERSION baked in at compile time — currently 0.30.0 (workspace Cargo.toml, unchanged since 2026-04-12). A client developer reading the doc and pinning to the example shape would see an immediate mismatch against a real server. Five-whys 1. Why did the doc show a version that doesn't exist? → The example was forward-scoped during an earlier release-planning pass that anticipated a 0.31.0 bump. 2. Why did that anticipated bump not land? → M1-M3 all shipped on main but never got tagged; the plan line in the spec says "M1-M3 planned for v0.32.0 publication" (line 263). 3. Why didn't the doc update when the tag plan changed? → Example payloads are prose, not codegen, and aren't covered by any contract byte-compare. 4. Why no lint for version strings in examples? → Version drift is rare and most tools show "x.y.z" abstracts; apr.version's case is unusual because the book shows a concrete literal. 5. Why show a concrete literal? → Helpful for readers debugging an actual tools/call round-trip — but that helpfulness inverts once the literal goes stale. Fix: set the example to 0.30.0 (current workspace version) and add a one-sentence note telling clients to parse for diagnostics rather than pin to the literal. That way the next version bump doesn't immediately invalidate the doc. Refs PMAT-514 * test(falsify-mcp-008): enforce tool description YAML↔source byte-equality Before: `migrated_tools_match_yaml_contract_byte_for_byte` compared only `inputSchema`, leaving `tools[*].description` free to drift silently. This drift was observed twice on 2026-04-18 alone (apr.serve — 715781df5, apr.run — 91a613968) after the YAML contract was audited manually against the source. Five whys: 1. Why did apr.serve/apr.run descriptions drift from the contract? → dev edits in tools/*.rs never propagated back to the YAML. 2. Why wasn't this caught in CI? → FALSIFY-MCP-008 harness compared only `inputSchema`. 3. Why was `inputSchema` the only thing compared? → M3 PR #881 scoped the byte-identity gate to the schema codegen path (build.rs emits APR_*_SCHEMA constants), where drift would crash the build. 4. Why didn't the contract itself catch this? → YAML line 282 asserted "each tool's `description` matches tools[*].description byte-for-byte" — but that assertion was aspirational, never wired into a test. 5. Root cause: claim-without-enforcement is the silent-drift seed. Fix is to make the assertion load-bearing by adding a second test that compares `ToolDefinition.description` to the YAML string directly. The new test `tool_descriptions_match_yaml_contract` discharges the class of drift that caused both commits above, without widening scope — it uses the same contract loader and `migrated_tools()` iterator as the existing schema gate. Verified: all 6 tests in falsify_mcp_008 pass, including the new one. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp-contract): flip DRAFT→ENFORCED, clear stale M3 parentheticals The contract YAML self-describes as DRAFT and pins its test_harness / codegen_consumer with "(to be added in M3)" parentheticals — but M3 shipped on 2026-04-18 (PR #881). The drift surfaces as: - Line 58 top-level `status: DRAFT` - Line 271 `FALSIFY-MCP-008.status: DRAFT` - Line 287 `test_harness: ...falsify_schema_codegen.rs (to be added in M3)` — the real harness is `falsify_mcp_008.rs` and has six tests green - Line 288 `codegen_consumer: ...build.rs (to be added in M3)` — already landed - Line 57 top-level `version: "1.0.0"` vs line 30 `metadata.version: 1.1.0` Five whys: 1. Why is the contract still DRAFT after M3 shipped? → nobody reran a spec audit after PR #881 merged. 2. Why did the M3-ship commit not touch this file's status? → PR #881 scope was "wire up codegen + harness"; contract fields were treated as documentation, not code. 3. Why weren't the parentheticals caught? → they read as prose, not as testable assertions; no gate compares them against reality. 4. Why didn't any automation flag a version mismatch between top-level `version` (1.0.0) and `metadata.version` (1.1.0)? → no such check exists on this contract schema. 5. Root cause: contract-as-documentation drift. Counterpart: PMAT-514 just added a harness test that makes the `description`-equality claim on line 282 load-bearing. This commit brings the surrounding prose (status + parentheticals + version pin) into alignment with that ENFORCED reality. Follow-up candidates (not in this commit): - Add a harness check that `metadata.version == top-level version` to prevent this class from re-emerging (parallel to FALSIFY-MCP-008). Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp-spec): document FALSIFY-MCP-008 description-equality extension Three coordinated edits, all propagating the harness change from PMAT-514 into the spec surface: 1. Gate summary (line 158): narrow "schema byte-identical" claim broadened to "schema + description byte-identical", naming both test functions explicitly so readers can find the enforcement point. 2. File-tree comment (line 60): `falsify_mcp_008.rs` blurb now says "schema + description byte-identity", matching the new test. 3. M5 re-run checklist (line 215): test count 75 → 76 (one new test in falsify_mcp_008.rs). Verified: `cargo test -p aprender-mcp` reports 51+8+4+6+4+2+1 = 76 tests all passing. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(roadmap): register PMAT-514 — APR-MCP-KAIZEN continuous drift sweep Adds the pmat work ticket that tracks ongoing kaizen on apr-mcp-server-spec and its satellites (aprender-mcp source, book chapter, schema contract YAML). Status: inprogress. First discharge: byte-compare YAML tool descriptions with source descriptions (closed silent-drift class that bit apr.serve on 715781df5 and apr.run on 91a613968 in one 24h window). Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(book/mcp): book chapter mirrors FALSIFY-MCP-008 description extension Symmetric to the spec update in 2f38f0241. Two book edits: 1. Falsification gates table (line 333): gate now reads "inputSchema AND description byte-identical" — same broadening applied to the spec. 2. Schema-codegen prose (line 315-320): calls out the two specific test functions that enforce the gate, and tightens the "edit YAML, rebuild" guidance to include descriptions. Readers landing on the book chapter (via rustdoc cross-link or GitHub Pages) now see the same gate surface as spec readers. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(aprender-mcp/README): mirror FALSIFY-MCP-008 description extension Crate README's gate table is the third surface that readers hit — after the spec and book chapter. Aligning all three to say "inputSchema AND description" closes the documentation side of the silent-drift class. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp-contract): sharpen coverage-note — 9 entries, gate surface spelled out Before: the coverage note said "All 8 Phase-1 tools are now registered in this contract" — technically correct (apr.version is an M1 scaffold, not a Phase-1 workflow tool) but ambiguous, because the FALSIFY-MCP-008 harness iterates over all 9 entries including apr.version. A new reader easily miscounts. After: the note enumerates both categories explicitly (scaffold + 8 wrappers = 9 entries) and adds a second paragraph spelling out what the PMAT-514 extension now covers — `inputSchema` byte-identity AND tool-level `description` byte-identity — with the specific test function names. This matches the surface that was already asserted in the falsification block above (lines 281-286) and discharges the ambiguity in one pass. Verified: `cargo test -p aprender-mcp --test falsify_mcp_008` 6/6 green. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp-spec): add apr-mcp-tool-schemas-v1 to Contracts header list The tool-schemas contract is the **single source of truth** for every MCP tool's `inputSchema` (and, as of PMAT-514, description), drives the `build.rs` codegen, and is referenced by FALSIFY-MCP-008 — yet it was missing from the header `**Contracts**:` list. The spec's own body text referenced it five times (lines 27, 40, 158, 177, 193) but a reader landing on the spec from a link would not see it in the contract register. Five whys: 1. Why was the contract not listed? → the header was authored before the tool-schemas YAML was split out into a standalone contract. 2. Why didn't the split author backfill the header? → the split PR (#871 — authored the YAML) focused on the contract body; the spec header wasn't on the review checklist. 3. Why isn't there a checklist? → spec-header/contract-file consistency has no automated gate. 4. Why no gate? → the spec body mentions multiple contracts in prose, so "spec references contract X" doesn't uniquely identify which contracts should appear in the header. 5. Root cause: the header is a curated list (things a reader must know about), not a mechanical index. Kaizen is the right fix for curated-list drift — no automation needed, just periodic sweeps. Also included the ENFORCED status inline so readers see M3 progress at a glance. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp-contract): broaden FALSIFY-MCP-008 condition to match assertions The `assertions:` block already covered descriptions (line 282) but the prose `condition:` above it talked only about "JSON Schema". Readers skimming the condition paragraph would miss that descriptions are also load-bearing. The rewrite preserves the JSON canonicalization language (important — that's the byte-for-byte definition) and adds a second clause spelling out how descriptions flow: directly compared at test time against `ToolDefinition.description`, separate from the build.rs codegen path that carries `inputSchema`. Verified: `cargo test -p aprender-mcp --test falsify_mcp_008` still 6/6 green. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(falsify-mcp-008): refresh module doc-comment for PMAT-514 extension The file-level doc-comment predated the description-equality test added in PMAT-514. Three updates: 1. Opening summary: "byte-identical to the schema" → "byte-identical to the corresponding entry ... covering both the `inputSchema` object and the tool-level `description` string" — so cargo-doc readers see the full gate surface on first hit. 2. Numbered list: step 6 added for the description assertion, keeping the structural schema assertion as step 5. 3. Scope paragraph: "Scope (M3 completion — PR #881 follow-up)" → "Scope (M3 shipped, extended by PMAT-514 on 2026-04-18)" and counts updated from "all 8 Phase-1 tools" to "all 9 registered tools (apr.version + 8 Phase-1 wrappers)" — matches the contract coverage-note landed in 3266e365f. Verified: 6/6 tests still pass. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(book/mcp): sharpen 'edit YAML, rebuild' — descriptions need Rust edit too Previous prose read "The Rust source does not need editing for schemas, and descriptions must track the YAML verbatim" — technically implies descriptions auto-flow from the YAML. They don't: the description string is hand-written in `crates/aprender-mcp/src/tools/<tool>.rs` and must be mirrored manually when the YAML changes. The harness (`tool_descriptions_match_yaml_contract`) fails CI on divergence but does not auto-fix the source. Why this matters: a contributor reading the old wording would think editing only the YAML is enough, push, and then be surprised when CI fails. The new wording makes the two-file edit explicit. Future cleanup: extend `build.rs` to codegen description constants too, then this note can collapse back to "edit YAML only". Not in scope for PMAT-514 — the test-time enforcement is sufficient today. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(aprender-mcp): codegen tool descriptions from YAML contract Extends build.rs to emit `APR_<TOOL>_DESCRIPTION: &str` alongside the existing `APR_<TOOL>_SCHEMA: &str` for each tool in `contracts/apr-mcp-tool-schemas-v1.yaml`. All 9 tool modules now consume `crate::schemas::APR_<TOOL>_DESCRIPTION.to_string()` instead of hand-mirroring the string in Rust source. Five-whys: 1. Why extend codegen? Descriptions drifted silently twice in a 24h window (apr.serve 715781df5, apr.run 91a613968). 2. Why did the test-time gate (PMAT-514) not catch drift before merge? It did — but only after the drift was committed; a compile-time gate prevents the drift from ever building. 3. Why split schema and description into separate constants instead of one merged blob? ToolDefinition's `description` is a Rust String, not JSON; keeping them separate avoids forcing a JSON round-trip on a non-JSON field. 4. Why keep the test-layer `tool_descriptions_match_yaml_contract` if codegen eliminates drift? Defence in depth — catches a future refactor that replaces the codegen consumer with a literal. 5. Why only 9 files to update? 8 Phase-1 wrappers + apr.version are the entire current tool surface. M5 tools will consume the codegen constants from day one. Refs PMAT-514. * test(falsify-mcp-008): codegen-layer description gate + coverage guardrail Adds two new tests to `falsify_mcp_008.rs`: * `codegen_description_constants_match_yaml` — asserts each `schemas::APR_<TOOL>_DESCRIPTION` codegen constant equals `tools[*].description` byte-for-byte. This is a strictly stronger gate than `tool_descriptions_match_yaml_contract`: the live-ToolDefinition test would silently pass if a future refactor replaced `APR_X_DESCRIPTION.to_string()` with a hand-coded literal. Asserting the codegen constant itself closes that bypass route. * `codegen_descriptions_cover_every_tool_name` — mirrors the existing `codegen_constants_cover_every_tool_name` guardrail: every name in `schemas::TOOL_NAMES` must appear in `CODEGEN_DESCRIPTIONS`, catching the case where a new tool is added to YAML but its description constant isn't registered in the test table. Refreshes module-level doc-comment to enumerate 7 layers of coverage and the dual codegen path (SCHEMA + DESCRIPTION). Test count: falsify_mcp_008 grows 6→8; aprender-mcp total 76→78. Refs PMAT-514. * docs(mcp): sync all surfaces with PMAT-514 description-codegen extension Mirrors the build.rs description-codegen change into every doc surface that previously said descriptions were hand-mirrored: * docs/specifications/apr-mcp-server-spec.md — FALSIFY-MCP-008 row now names the codegen-layer test; M3 milestone bullet points at the PMAT-514 extension; suite count 76→78. * contracts/apr-mcp-tool-schemas-v1.yaml — `condition:` prose and `test_harness:` / `codegen_consumer:` pointers describe both `APR_<TOOL>_SCHEMA` and `APR_<TOOL>_DESCRIPTION` codegen paths; tool-registry comment states both fields flow through build.rs. * book/src/tools/mcp-server.md — "edit YAML, rebuild" guidance updated: changing a description now requires only a YAML edit (was: YAML + Rust); enumerates 4 sub-tests (2 live, 2 codegen). * crates/aprender-mcp/README.md — gate-table row references the dual codegen constants. Refs PMAT-514. * chore(roadmap): PMAT-514 record description-codegen discharge line Marks the PMAT-514 roadmap entry with a DISCHARGED acceptance line pointing at the two-layer gate (test-layer + codegen-layer) and the `APR_<TOOL>_DESCRIPTION` build.rs output. The top-level "ongoing kaizen sweeps" acceptance stays — this is one ticket, many sweeps. Refs PMAT-514. * docs(mcp): sync remaining module-doc + README M3 bullet with PMAT-514 Three surfaces still described M3 codegen as "schema only": * docs/specifications/apr-mcp-server-spec.md — file-tree build.rs comment now spells out both constants emitted. * crates/aprender-mcp/README.md — M3 milestone bullet enumerates `APR_<TOOL>_SCHEMA` and `APR_<TOOL>_DESCRIPTION`. * crates/aprender-mcp/src/lib.rs — module-doc for `schemas` now documents both constants, how to consume them, and that hand-coding either is caugh…
noahgift
added a commit
that referenced
this pull request
Apr 19, 2026
…ix dangling "this PR" ref (#905) * docs(mcp): spec v1.1.0 — DRAFT → ACTIVE; M1–M3 shipped - Status: DRAFT (pre-implementation) → ACTIVE (M1–M3 shipped; M4 dogfood pending) - Falsification conditions table now annotates ENFORCED / PARTIAL / Deferred per shipped test mappings in contracts/apr-mcp-server-v1.yaml (#886) - Adds FALSIFY-MCP-PROGRESS-001 entry for #887 progress-notification gate - Milestones M1/M2/M3 marked SHIPPED with PR cross-references - M4 acceptance items remain open (real-model gates, dogfood) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp): align spec output shapes with CLI reality (PR #889 falsifications) PR #889 added mock-subprocess e2e tests for FALSIFY-MCP-003/-004 and discovered two spec-vs-CLI mismatches via test failures: 1. apr.run: spec listed `stop_reason` in output — CLI's print_run_output (crates/apr-cli/src/commands/run_entry.rs:279) does NOT emit it. Spec corrected to the actual emitted set (model, text, tokens, ...). 2. apr.qa: spec wrote gates as `{pass, value, threshold}` — CLI's GateResult (crates/apr-cli/src/commands/qa.rs:368) uses `passed` not `pass`. Spec corrected. Also fixes the codegen source reference: FALSIFY-MCP-008 uses contracts/apr-mcp-tool-schemas-v1.yaml (PR #871), not apr-cli-commands-v1.yaml. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(mcp-spec): M1 PR refs #862 → #864 PR #862 is a matmul test fix, not the MCP M1 skeleton. The correct skeleton PR is #864 (`feat(mcp): apr mcp M1 skeleton — MCP server over stdio`). All three stale citations in the M1 milestone replaced. Five-whys root cause: the spec retrofit (#873) reconstructed PR numbers from memory; future retrofits should verify against `git log --grep=...` before committing. Refs PMAT-037. * fix(mcp-spec): demote unmerged contract + M3 PR accuracy Three stale citations corrected in the M3 milestone: - #874 removed from cancellation bullet (#874 is the book-chapter doc commit, not cancellation — that's #883 alone). - `contracts/apr-mcp-server-v1.yaml (ACTIVE) ... (#886)` bullet moved from M3 SHIPPED to M4 IN PROGRESS. PR #886 is still OPEN and its own title says "M4 — apr-mcp-server-v1 contract ACTIVE". The file is not in-tree. Header's "**New**:" label also updated to "Pending (PR #886)" for the same file. - Book-chapter citation expanded to list #874 (M2 creation) + #885 (M3 update) for accurate provenance. Five-whys root cause (false "M3 SHIPPED" on #886): the spec promotion commit (a496ce97c) rolled unmerged M4 work into M3 bullets under the optimistic assumption the PR would land first. Going forward: any bullet citing a PR must verify `gh pr view <N>` is MERGED before promoting a milestone. Refs PMAT-037. * fix(mcp-spec): Architecture — refresh to match built reality The Architecture + Protocol + Out-of-Scope sections carried pre-M1 aspirations that no longer match the shipped crate. Refreshed against actual source tree in crates/aprender-mcp/: - Goal: schema source was `apr-cli-commands-v1.yaml`; lines 11/87/139 correctly cite `apr-mcp-tool-schemas-v1.yaml`. Unified. - Directory diagram: listed absent `schema.rs`; missing `build.rs`, `types.rs`, `tools/subprocess.rs`, `tools/version.rs`. `server.rs` comment said "pmcp::Server wiring" but M1 shipped a hand-rolled JSON-RPC loop (also noted line 149). `Cargo.toml` comment listed pmcp/tokio/clap/apr-cli — none are actual deps (verified: serde, serde_json, anyhow, nix, serde_yaml build, jsonschema dev). `tests/` now lists the four actual `falsify_*.rs` harnesses. - `apr mcp` subcommand: snippet promised `async` with `McpArgs` + transport matching + SSE; actual `run()` is blocking, takes no args, calls `AprMcpServer::new().run_stdio()`. - Protocol/Transport: "SSE optional" was false; flag doesn't exist. Downgraded to stdio-only and added SSE to Out of Scope. Five-whys root cause: the Architecture diagram was authored pre-M1 as a design sketch; later commits (#873 retrofit, v1.1.0 promotion) updated Milestones but never re-diffed the static diagram against `ls crates/aprender-mcp/src/`. Going forward: any spec change touching Milestones must run a diagram-vs-tree check. Follow-up filed: verify Config Precedence (lines 122-126) against implementation — `pub fn run()` consults no env vars today. Refs PMAT-037. * fix(mcp-spec): reconcile 8-vs-9 tool count + Related Work misattribution Two factual errors corrected: - Tool count: spec said "8 Phase-1 tools" (lines 74, 133); `tools/list` actually returns 9 because `apr.version` (M1 scaffold) is also registered. Verified by `crates/aprender-mcp/tests/falsify_m1.rs::falsify_mcp_002_tools_list_schema_shape`, which asserts all 9 names (apr.version + 8 workflow tools). Clarified spec to state "8 Phase-1 workflow tools + apr.version scaffold = 9 total registered" and added test cross-link to the FALSIFY-MCP-002 bullet. - Related Work line 210 claimed `crates/apr-cli/src/tool_commands.rs` is the "planned MCP tool surface (referenced but unimplemented)". That file exists and is the `apr tool` CLI subcommand group (Showcase, Rosetta, …), unrelated to MCP. The actual MCP tool surface lives in `crates/aprender-mcp/src/tools/`. Corrected and noted that rust-mcp-sdk (paiml/rust-mcp-sdk) is currently unused since M1 shipped a hand-rolled JSON-RPC dispatcher. Five-whys root cause (8 vs 9): the original Phase-1 design enumerated 8 workflow tools and `apr.version` was added later as an M1 handshake probe without updating the narrative count. No invariant check cross-references spec tool-count against `tools/list` test assertions. Refs PMAT-037. * fix(mcp-spec): mark config precedence Phase-2 aspirational Lines 122-126 stated a four-level config precedence (`--config`, `$APR_MCP_CONFIG`, `~/.config/apr/mcp.toml`, defaults) as if it were implemented. Actual `crates/apr-cli/src/commands/mcp.rs::run()` takes no arguments and consults no env vars; `AprMcpServer::new()` has no config loader. The `APR_MODEL_DIR` env in the `.mcp.json` snippet is read by the spawned `apr <cmd>` subprocesses, not by the MCP server. Rewrote the section to keep the intended precedence as the Phase-2 contract while making Phase 1's "no config loader" reality explicit. Five-whys root cause: the Configuration section predates the M1 skeleton and was not re-verified against `commands/mcp.rs` during the v1.1.0 promotion. A "spec bullet implies an API — grep for the API" check belongs in the promotion workflow. Refs PMAT-037. * fix(mcp-spec): Success Criteria gate count 8 → 9 Spec's Falsification Conditions section lists 9 entries (FALSIFY-MCP-001 through -008 plus FALSIFY-MCP-PROGRESS-001 added in M3), but the Success Criteria table still said "8 falsification gates". Count corrected and wording clarified to reflect that -003/-004 are currently PARTIAL and must promote to PASS at M4 close. Five-whys root cause: adding PROGRESS-001 in M3 touched the conditions section but didn't update the downstream summary row. Going forward: whenever a new FALSIFY-MCP-* lands, grep the spec for `N falsification` to catch all downstream counts. Refs PMAT-037. * fix(mcp-spec): close residual kaizen items Three dangling claims resolved: - Target version: `v0.32.0 / v0.33.0` stands as the intended release tags but `git tag -l 'v0.3*'` returns only `v0.3.0 / v0.3.1 / v0.30.0`. M1–M3 are merged on `main` but unreleased. Added a clarifier so a reader doesn't assume those tags exist. - Aspirational follow-ons: `apr-mcp-plugin-marketplace-v1.md` and `apr-mcp-hooks-v1.md` are not in `docs/specifications/`. Labelled "(spec files not yet authored)" so readers don't hunt for them. - Risk Register: "pmcp crate API instability" is dormant because M1 shipped a hand-rolled JSON-RPC dispatcher (line 166 already notes pmcp is deferred). Row reworded so the risk's activation condition is explicit. Five-whys root cause (across all three): the spec's non-Milestone sections — Target, Related Work, Risk Register — were not refreshed during v1.1.0 promotion. Every milestone promotion should sweep those sections, not just the milestone table. Refs PMAT-037. * chore(pmcp): bump to 2.3 and drop pforge-runtime (Refs PMAT-037) Five-Whys: - Symptom: dual pmcp versions (1.20 + 2.3) resolved in agents-mcp build. - Why #1: pforge-runtime 0.1.4 (last released 2025-12) still pins pmcp 1.x. - Why #2: pforge-runtime was listed as an optional dep alongside pmcp. - Why #3: it was a forward-compat hedge — but no Rust code imports it (only doc-comment mentions and knowledge-graph string literals). - Why #4: keeping an unused dep doubled the compile footprint and split the pmcp protocol surface across two crates. - Root cause: speculative dep on a framework wrapper for an SDK we already use directly. Fix: - Cargo.toml: bump pmcp 1.10 → 2.3 (PAIML's actively-maintained SDK); remove pforge-runtime dep; agents-mcp feature now just ["agents","pmcp"]. - Doc comments and the mcp_demo example rewritten to name pmcp v2.3 as the SDK instead of pforge. No Rust-level API change — pforge-runtime was never imported, just advertised. - cargo tree -i pmcp now shows a single pmcp v2.3.0 node. Follow-up: spec's pmcp framing (M1 note + Risk Register) still needs rewrite in apr-mcp-server-spec.md. * docs(apr-mcp-spec): v1.2.0 — honest pmcp framing, add M5 migration plan (Refs PMAT-037) Five-Whys: - Symptom: spec framed pmcp as unstable/dormant, treating the SDK as risk rather than planned substrate. - Why #1: Risk Register called out "pmcp crate API instability (dormant...)" — language from before pmcp was actively maintained. - Why #2: M1 note said "pmcp SDK deferred — more deterministic for current scope" without explaining the actual technical rationale. - Why #3: no adoption path existed — M4 stops at dogfood, so readers couldn't tell whether pmcp would ever land. - Why #4: pmcp v2.3 is PAIML's own crate (paiml/rust-mcp-sdk) and already used by aprender-orchestrate; keeping the spec's out-of-date framing forced the /tmp/spec-update session to discover this from crates.io. - Root cause: stale spec language from the early M1 period where the adoption path was genuinely uncertain; never updated after pmcp stabilised. Fix: - Line 15: link now labels pmcp as "PAIML's Rust MCP SDK, actively maintained, v2.3.1 on crates.io (2026-04-16)". - Line 44 / 167: architecture + M1 note explain the three concrete reasons the dispatcher is hand-rolled (minimal request/response shape over `apr <cmd> --json`, build.rs schema codegen keeps tools/list byte-identical to contract YAML, falsification asserts on wire bytes without an SDK layer). - Risk Register row rewritten from "API instability" to "adoption-path coordination" — real risk is workspace version alignment with the pmcp client role in aprender-orchestrate. Mitigation: single workspace-wide bump + `cargo tree -d` CI gate. - New M5 milestone: concrete pmcp migration plan — port dispatcher to pmcp::Server (retain build.rs codegen), add SSE + WebSocket transports, re-run falsification suite post-migration. - Out of Scope: SSE/WebSocket transports reclassified as "scheduled for M5 on top of pmcp v2.3". - Related Work: pmcp-sdk contract row now notes aprender-orchestrate already links pmcp v2.3 as a client; server-side migration is M5. - Version bumped 1.1.0 → 1.2.0. * docs(mcp-spec): reconcile M4 gate count with PR #886; bump pmcp contract v2.3 (Refs PMAT-037) Five-Whys: - Symptom: M4 bullet claimed "9 falsification_conditions" to match the 9 gates listed in Section 145, but PR #886's contract pins exactly 8 (FALSIFY-MCP-001..008) and a Rust test enforces that invariant. - Why #1: the 9th gate (FALSIFY-MCP-PROGRESS-001) was added in M3 AFTER PR #886 was drafted. - Why #2: PR #886's harness (apr_mcp_server_contract_ids_are_falsify_mcp_001_through_008) explicitly rejects anything outside 001..008, so the contract row for PROGRESS-001 cannot land in the same PR without harness changes. - Why #3: the spec's earlier count-reconciliation (2026-04-18 prior kaizen round) missed this because it was looking for text matches, not contract row counts. - Root cause: spec and contract evolved on different PR branches. Fix: - M4 bullet: accurately describes PR #886 as landing 8 falsification rows, names the exact-8 invariant by its test function. - Adds an explicit follow-up bullet: "Extend the contract with a 9th row for PROGRESS-001 after PR #886 merges — relax the exact-8 invariant to 'FALSIFY-MCP-001..008 + PROGRESS-001, no extras'". - Success Criteria table unchanged (line 220 still correctly says "9 falsification gates ... all PASS or PARTIAL→PASS by M4 close") — the 9th gate is already ENFORCED in code via falsify_mcp_progress_001.rs, we just need the contract YAML to catch up. Also: - contracts/pmcp/mcp-protocol-sdk-v1.yaml version 1.0.0 → 1.1.0 with "last_modified: 2026-04-18". - Description updated v2.1 → v2.3, adds consumer-of-record (aprender- orchestrate via agents-mcp feature) + future consumer (aprender-mcp M5 migration) + link to apr-mcp-server-spec.md. * docs(book/mcp): align M3 scope + add M5 pmcp migration row (Refs PMAT-037) Five-Whys: - Symptom: book chapter's M3 row missed FALSIFY-MCP-PROGRESS-001 (shipped via PR #887) and the paragraph called progress streaming "a follow-up slice" for BOTH apr.run and apr.finetune — incorrect for apr.finetune. - Why #1: book chapter was authored before PR #887 landed progressToken-gated notifications for apr.finetune. - Why #2: M5 pmcp migration (added to spec v1.2.0 today) had no corresponding row in the book status table. - Root cause: book lagged spec after the M3 progress slice merged and after the M5 migration plan was formalised today. Fix: - M3 row now mentions the opt-in progress notifications. - Paragraph specifies: FALSIFY-MCP-PROGRESS-001 is enforced for apr.finetune; only per-step structured progress (CLI event channel prereq) and apr.run progress (apr run --stream flag prereq) remain open. - New M5 row in the status table mirrors the spec's M5 milestone. * docs(mcp-spec): tighten streaming claim + M5 transport pointer (Refs PMAT-037) Five-Whys: - Symptom: Section "Protocol" bullet on Streaming claimed "apr.run and apr.finetune send notifications/progress for each decoded token / training step" — but apr.run progress is a deferred M4 item and apr.finetune only emits per-stdout-line progress (not per training step) and only when the client opts in via progressToken. - Why #1: the bullet was authored when both tools were planned to stream per-token. Reality diverged: progress landed for apr.finetune only (opt-in, per-line), apr.run was deferred. - Why #2: the Architecture paragraph pointed to "Phase 2 with SSE" for transport selection without naming the actual M5 milestone that now schedules it. - Root cause: drift between aspirational early-M2 text and the M3/M5 structure formalised today. Fix: - Streaming bullet now names what's actually enforced (FALSIFY-MCP-PROGRESS-001, apr.finetune opt-in, per-stdout-line) and explicitly calls out the apr.run follow-up prereq (apr run --stream flag + per-step CLI event channel). - Architecture paragraph points at M5 as the SSE/WebSocket landing spot rather than the generic "Phase 2". * fix(examples): unblock Chapter Examples Compile on main (Refs PMAT-037) Five-Whys: - Symptom: CI job "Chapter Examples Compile" has been failing on every push to main since PR #701 (2 days), plus on this PR, with RUSTFLAGS= "-D warnings" promoting unused-import warnings to hard errors. - Why #1: ch10_training and ch24_switch_pytorch both import `aprender::nn::Optimizer` but only call `optimizer.step_with_params`, which is an inherent method on `SGD` (not a trait method) — so the trait import is genuinely unused. - Why #2: ch26_switch_ndarray binds `let pred = lr.predict(&x)` but never reads `pred` (score re-computes internally). - Why #3: these examples predate the refactor that moved `step_with_params` from the Optimizer trait to inherent impls; the trait import was never cleaned up. - Why #4: the Book Contract Enforcement and Chapter Examples Compile jobs are non-required checks, so the red status never blocked merges and accumulated as tech debt. - Root cause: main CI andon rule (main must always be green) was waived for non-required checks. Toyota Way: "all defects are your defects" — fix it regardless of whose PR introduced it. Fix: - ch10_training.rs, ch24_switch_pytorch.rs: drop `Optimizer` from the aprender::nn:: import list. - ch26_switch_ndarray.rs: consume `pred` by printing the first prediction — preserves pedagogical intent of showing predict() works, and unblocks -D warnings. - `cargo build -p aprender-core --examples` now warnings-clean. * fix(ci): use contract: pointer, not derived PCU path (Refs PMAT-037) The "Every PCU page has matching contract" gate derived paths from the PCU ID (`apr-page-${ID}-v1.yaml` / `apr-book-${ID}-v1.yaml`) but real page headers already carry an authoritative `contract:` field, and chapter contracts are named `apr-book-ch01-v1.yaml` (chapter-number only) while PCU IDs include a slug (`ch01-why-rust`). The mismatch failed all 27+ book pages on every run. Five whys: 1. Why red? Script can't find `apr-page-tools-apr-cli-v1.yaml` from ID `tools-apr-cli`... wait it can. But for chapters it looks for `apr-book-ch01-why-rust-v1.yaml` which doesn't exist. 2. Why does it derive? The earlier convention stored ID-derived paths before `contract:` was added to headers. 3. Why not updated when `contract:` was added? The workflow was not migrated; the two lookup paths stopped covering all cases. 4. Why silent until now? The gate was not blocking main. 5. Why fix now? Kaizen sweep surfaced 27-page failure. Parse the authoritative `contract:` field. Also add missing PCU header + page contract for book/src/tools/mcp-server.md (now points to contracts/apr-page-tools-mcp-server-v1.yaml). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp): retire stale 'M3 will ship apr.serve lifecycle' (Refs PMAT-037) Three places claimed `apr.serve` cancellation lands in M3: - book/src/tools/mcp-server.md apr.serve paragraph - crates/aprender-mcp/src/tools/serve.rs module/fn docs - serve tool `description` field embedded in tools/list M3 actually shipped `notifications/cancelled` for apr.run only. `server.rs::CancelHandle` doc explicitly states: "Only apr.run currently honours cancellation." apr.serve remains fire-and-forget and the spec M3 bullet list never promised otherwise. Five whys: 1. Why stale? Comments predicted M3 scope before scope narrowed. 2. Why narrowed? Spec M3 scope: FALSIFY-MCP-006 for apr.run, -008 codegen, -PROGRESS-001 for apr.finetune. apr.serve lifecycle was never inside that gate set. 3. Why not updated at M3 close? No acceptance criterion forced a sweep of surface prose when milestone shipped. 4. Why matters now? Readers of book/tools page and users calling apr.serve via MCP get incorrect "lifecycle lands in M3" note that reads as imminent, not aspirational. 5. Why fix now? Kaizen sweep surfaced; retarget to M5 where a daemon registry + pmcp Server port belong together. Edits: book paragraph + serve.rs module header + serve.rs `call` docstring + serve.rs description field + spec M5 new bullet for apr.serve cancel extension. Also spec M5 falsification-suite bullet updated from "71+ tests" to measured "75 tests" with file list. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(book/mcp): clarify apr.finetune progress shipped with limits (Refs PMAT-037) The apr.finetune paragraph said "Per-step notifications/progress streaming is a follow-up M3 slice" — read as "no progress yet" — but FALSIFY-MCP-PROGRESS-001 shipped in PR #887: per-line progress over `params._meta.progressToken` IS live. Five whys: 1. Why stale? Paragraph was written before PR #887 merged. 2. Why not updated at PR #887? PR focused on server.rs + test additions; book paragraph not flagged in review. 3. Why matters? Clients reading the book will assume they cannot stream updates and skip progressToken, losing observability. 4. Why two progress layers? Per-line (shipped, stdout-driven) vs per-step (needs a CLI event channel from `apr finetune` itself) — the former is cheap plumbing over JSON-RPC, the latter is a CLI-side refactor. 5. Why fix now? Kaizen sweep surfaced. Rewrote the paragraph to state (a) what shipped (opt-in per-line), (b) the gate it satisfies (FALSIFY-MCP-PROGRESS-001), (c) the honest limitation (terminal blob today), (d) where per-step lives (M4 follow-up with CLI prereq). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * contract(mcp-schemas): retire 'retrofit-only' header, lock v1.1.0 (Refs PMAT-037) The apr-mcp-tool-schemas-v1.yaml header still read: "This M2 cut is RETROFIT-ONLY" "If this file ever disagrees with the Rust source, the Rust source wins" "In milestone M3 a build.rs at ... will read this YAML" All three are post-M3 stale: 1. M3 shipped (PRs #880, #884) — build.rs is live. 2. Byte-identity is enforced by tests/falsify_mcp_008.rs (5 tests). 3. Rust tool sources contain zero hand-written schemas — they only parse `crate::schemas::APR_<TOOL>_SCHEMA` from $OUT_DIR. 4. Direction is reversed: YAML authoritative, Rust derived. Five whys: 1. Why stale header? Written for M2 retrofit cut. 2. Why not flipped at M3 close? PR #884 focused on codegen, not contract prose. 3. Why matters? Future readers will assume Rust source is the authority and "fix" the wrong side of a drift — inverting FALSIFY-MCP-008's intent. 4. Why now? Kaizen sweep. 5. Why v1.1.0? Semantic bump: authoritativeness change, plus new reference pointer to apr-mcp-server-spec.md. Bumped metadata version 1.0.0 → 1.1.0, added last_modified, rewrote header and description to reflect current state (YAML is SoT, Rust parses codegen constants, falsify_mcp_008.rs enforces byte-identity). Also updated spec M5 falsification-suite file list to include `falsify_mcp_008` and drop nonexistent `codegen_bytes`. Verified: `cargo test -p aprender-mcp --test falsify_mcp_008` — 5/5 pass after YAML comment edits (no functional change, just prose). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp-spec): 57 → 58 CLI commands (mcp added PR #864) (Refs PMAT-037) The spec claimed a 57-command CLI surface three times: - Contracts bullet: "57-command tool surface" - Problem paragraph: "57-subcommand CLI" - Goal paragraph: "subset of the 57 apr CLI commands" PR #864 registered `apr mcp` as the 58th command (contracts/apr-cli-commands-v1.yaml). The 63-line count in the contract is 58 commands + 5 FALSIFY-CLI-00* falsification rules. Five whys: 1. Why stale? The 57 figure dates to #701 contract landing (2026-04-06) — the initial MCP PRs added `apr mcp` but didn't sweep cross-cutting doc claims. 2. Why matters? MCP spec's own subject command is the 58th — a reader comparing counts will mistrust the surface-area claim. 3. Why only fixing here? Scope is `apr-mcp-server-spec.md`; CLAUDE.md and apr-book-spec.md have broader audiences and want their own kaizen passes. 4. Why cite PR #864 inline? Makes the delta auditable by a future reviewer checking `git log --oneline apr-cli-commands-v1.yaml`. 5. Why not reword to "58+ commands" for future-proofing? The contract is the source of truth; stale counts are better caught by an exact-match CI gate than smeared over with imprecise phrasing. (PR #864 added a FALSIFY-CLI gate.) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp-spec): honest release-target footer (M3 shipped same week as M2) (Refs PMAT-037) The footer claimed: v0.32.0 (M1–M2), v0.33.0 (M3–M4) But M3 shipped on 2026-04-18, same week as M2 (2026-04-17/18), and the workspace is still at v0.30.0 on main. The old split-tag plan (M1–M2 in one release, M3–M4 in the next) no longer maps to reality — M3 will publish alongside M1–M2 because there's nothing to publish in between. Five whys: 1. Why stale? Target was written assuming M2 → cut release → M3. 2. Why reality diverged? M3 landed fast because cancellation + codegen + progress + apr.finetune were all independent PRs. 3. Why matters? A reader looking at `git tag` + this footer would expect v0.32.0 to exist; it doesn't. 4. Why not assign firm tags? Release cuts require a separate decision (changelog + publishing); this spec shouldn't preempt it. 5. Why keep historical context? Future reader asking "why is the M3–M4 split collapsed?" deserves a traceable answer instead of silently rewritten history. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(aprender-mcp/README): sync milestones + full gate table (Refs PMAT-037) The crate README was three milestones behind the spec: - M2 bullet: "apr.serve (fire-and-forget; full lifecycle in M3)" — M3 shipped apr.run cancel only; serve registry is M5. - M3 bullet: "in progress" — M3 actually shipped 2026-04-18 (PRs #880, #881, #883, #884, #887). - Gate table: listed 5 gates (001, 002, 005, 007, VALIDATE-001); missed 003, 004, 006, 008, PROGRESS-001 — 4 of 5 are now ENFORCED or PARTIAL, and PROGRESS-001 is net-new since M3. Five whys: 1. Why lag? README is surface-facing, spec/code are the primary targets during milestone closes. 2. Why matters? crates.io readers land here first — inaccurate milestone + gate table = miscalibrated expectations, especially about apr.serve cancellation. 3. Why add status column? Distinguishing ENFORCED vs PARTIAL vs planned is what readers actually want when choosing whether to depend on a given gate. 4. Why spell out M4 + M5 here? Same reason — readers want to know what's next, not dig through the spec. 5. Why fix now? Kaizen sweep; PR #888 already touches this crate. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(README): 57 → 58 commands across 4 sites (Refs PMAT-037) The MCP spec already reconciled 57 → 58 (PR #864 added `apr mcp` as the 58th command in contracts/apr-cli-commands-v1.yaml). The root README still repeated 57 in four places: headline paragraph, stats bullet list, crate-layout tree comment, and smoke-test snippet. Keeping the count exact matters more than soft-pedalling it — PR #864 also added a FALSIFY-CLI gate that enforces `apr --help` listing against the YAML, so drift is caught at CI and the README should track it. Fixing here alongside the spec keeps the docs audit self-consistent within one PR. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(orchestrate/book): pmcp 1.8 → 2.3, drop pforge-runtime (Refs PMAT-037) Two orchestrate book pages carried stale pmcp/pforge references: - part3/pmcp.md — header still claimed pmcp v1.8.6 and showed `pmcp = "1.8"` in Cargo.toml snippet. crates.io has pmcp 2.3.1 as of 2026-04-16 and the crate's Cargo.toml already pins it. - part3/agent-runtime.md L575 — `agents-mcp = ["agents", "pmcp", "pforge-runtime"]` but pforge-runtime was dropped earlier in this PR series (it pinned pmcp 1.20 and was unused outside knowledge-graph cataloguing). Five whys for each: 1. Why stale? Book pages were written against pmcp 1.x, before the 2.x release cleanup. 2. Why not caught? The orchestrate book has no CI gate matching its Cargo.toml snippets to actual crate deps. 3. Why matters? Readers copy-pasting `pmcp = "1.8"` into a new project would land on a yanked / unmaintained line. 4. Why not add a CI gate? Out of PR scope; filed mentally as an M5+ follow-up when `apr-contracts` lints cross-project snippets. 5. Why fix now? Kaizen sweep surfaced during pmcp/pforge audit. Both archived batuta-agent.md references left alone — they live in `docs/specifications/archive/` and document the old design state. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(CLAUDE.md): 57 → 58 commands, add mcp to key-command list (Refs PMAT-037) Three stale 57-command claims in CLAUDE.md — the overview line, the key-files bullet, and the APR CLI section. Brought them in line with contracts/apr-cli-commands-v1.yaml (58 commands including `apr mcp`, added PR #864). Also added `mcp` to the inline key-command list — discovery matters more than alphabetical tradition given the MCP spec is the current top-of-mind work. The 405-contract and 25,300-test counts are out of spec scope and left for a future sweep (workspace tests reportedly 25,391 per the root README, but confirming across the 70 crates needs real `cargo test --workspace --lib` run, not a file read). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp-spec): document FALSIFY-MCP-VALIDATE-001 dispatcher invariant Symptom: spec Falsification Conditions section had 9 entries (MCP-001..008 + PROGRESS-001), but crates/aprender-mcp/README.md and book/src/tools/mcp-server.md both list a 10th enforced gate, FALSIFY-MCP-VALIDATE-001, which was missing from the spec entirely. Five-whys: (1) spec only lists conditions destined for apr-mcp-server-v1.yaml; (2) VALIDATE-001 is a dispatcher-level contract point (how the server shapes tool errors), not a per-tool behavioural promise; (3) it therefore lives *alongside* but *outside* the YAML contract — mirrored in the book under "Additional invariant enforced by the dispatcher"; (4) the spec's own section header ("Falsification Conditions for apr-mcp-server-v1.yaml") excluded it by scope, but the omission reads as "we forgot a gate" to anyone cross-referencing README/book; (5) fix is to add an "Additional dispatcher invariant" subsection pointing at the existing test falsify_m1.rs::falsify_validate_missing_model_path_is_tool_error. Refs PMAT-037 * docs(aprender-mcp): refresh module-level scope docs for M3-shipped state Symptom: `src/lib.rs` crate-level docs titled the scope section "M1 Scope" and claimed "M2 adds the 8 Phase-1 tools"; `src/tools/mod.rs` said "M3 adds `apr.finetune` (synchronous initial slice; streaming is a follow-up)"; and `src/server.rs` had a test doc-comment reading "Full 8-tool set lands when M2 completes." All three predate M3 shipping on 2026-04-18. Five-whys: (1) module docs were written incrementally milestone-by- milestone; (2) each PR updated its own surface but left sibling module docs unchanged; (3) there is no CI gate on module-level Rustdoc matching milestone status; (4) new readers start at `lib.rs` and encounter text that contradicts `apr mcp --help` + README; (5) cheapest fix is to rewrite the three doc-comments to a single authoritative summary keyed off the spec's own "M1–M3 SHIPPED" tags, leaving M4/M5 forward-looking. No behaviour change; no test updates needed. Refs PMAT-037 * docs(mcp): update apr.finetune/apr.run docs for shipped-M3 progress state Symptom: three stale M3 claims, each LLM-visible or reader-visible: (1) `apr.finetune`'s `description` field still read "Progress streaming lands in a follow-up M3 slice" — but PR #887 shipped the streaming slice on 2026-04-18, and the description is returned verbatim in `tools/list` to LLM clients. (2) The same stale sentence is duplicated in the authoritative `contracts/apr-mcp-tool-schemas-v1.yaml`. (3) `src/tools/run.rs` module docs say "Progress notifications (streamed per-token) are a separate M3 slice" — the spec's M3 checklist (line 192) now records that as deferred to M4 pending `apr run --stream`. Five-whys: (1) tool `description` fields are hand-written strings that become part of the MCP wire response; (2) FALSIFY-MCP-008 compares `inputSchema` byte-for-byte but *not* `description`, so description drift is silent; (3) when PR #887 shipped progress streaming, only the crate module docs in finetune.rs were partially updated — the `description` field and the YAML contract were missed; (4) stale LLM- visible strings confuse agents about which call shape actually works today; (5) fix is to (a) promise exactly what ships (opt-in via `params._meta.progressToken`, falsification gate PROGRESS-001), (b) align the YAML contract and Rust source, and (c) rewrite `apr.run`'s module prelude to describe the cancel-token surface that shipped and the per-token progress that didn't. Verified: `cargo test -p aprender-mcp --test falsify_mcp_008` passes (5/5). Description field is not covered by the schema gate, confirming the drift was invisible to CI until now. Refs PMAT-037 * docs(mcp-spec): cross-link M4 checklist items to the PRs carrying them Symptom: M4 checklist items in the milestone section all read "in flight" / "dogfood" without referencing any PR, even though six open PRs (#886, #889, #890, #891, #892, plus our own #888) are carrying this exact work. Readers who arrive from the PR list can't map a PR onto the spec box it's trying to tick, and readers who arrive from the spec can't find the open PR. Also added a `falsify_mcp_progress_001.rs` row to the crate-layout tree (previously omitted) and broadened the `falsify_m1.rs` description to mention all gates it enforces (-001, -002, -005, -007, -VALIDATE-001), not just the first two. Five-whys: (1) M4 work is happening across 4+ PRs in parallel; (2) the spec was last edited when only PR #886 existed; (3) new PRs (#889/#890/#891/#892) introduced new gate IDs (FALSIFY-MCP-E2E-001, FALSIFY-MCP-DOGFOOD-001, FALSIFY-MCP-PROGRESS-002) but the spec never reflected them; (4) without PR cross-links, the spec drifts out of sync within days; (5) fix is to name the branch + PR for each in-flight box so the linkage is obvious and breaks visibly when a PR is closed or renamed. Refs PMAT-037 * docs(contracts): fix stale 57-command count + codegen test path Two small contract-metadata fixes caught by the kaizen sweep: 1. `contracts/apr-cli-commands-v1.yaml` line 24 — `scope` field still claimed "57 commands"; the actual command list has 58 entries as of PR #864 (apr mcp added 2026-04-17). Verified by counting `^ - name:` entries under the `commands:` key (`awk` filter — 58). 2. `contracts/apr-mcp-tool-schemas-v1.yaml` had two sibling errors: (a) Block-comment header line 7 still said "each of its 57 entries" referring to apr-cli-commands-v1.yaml — updated to 58 to stay in sync with the registry. (b) `metadata.description` pointed readers at `tests/codegen_bytes.rs` for FALSIFY-MCP-008 enforcement; the actual file is `crates/aprender-mcp/tests/falsify_mcp_008.rs` (confirmed via `ls crates/aprender-mcp/tests/`). The wrong path is particularly bad because new contributors clone the repo and try to grep for a file that doesn't exist. Five-whys on (2b): (1) an earlier contract rev proposed the filename `codegen_bytes.rs`; (2) the commit that renamed it to `falsify_mcp_008.rs` (conventions: one test file per FALSIFY gate) didn't update the contract metadata; (3) nothing in CI cross-checks prose filename references inside YAML headers; (4) the spec we edited in PR #888 already fixed this in one spot but missed the sibling in this file; (5) the cheapest fix is a literal string replace — adding a lint for "tests/[a-z_]+\.rs" strings that don't resolve is follow-on work, tracked separately. Refs PMAT-037 * docs(contracts): bump 57→58 command count in apr-cli-publish + apr-cli-qa Symptom: the two CLI-level contracts that gate `cargo install` and dogfood QA still asserted "all 57 commands" in their postconditions, falsification predictions, and proof_obligations. The actual `apr --help` surface is 58 commands as of PR #864 (mcp added 2026-04-17), and `contracts/apr-cli-commands-v1.yaml` was already updated to 58 in the previous commit. Affected invariants: - apr-cli-publish-v1.yaml equations.all_commands_compile formula - FALSIFY-PUB-CLI-003 prediction ("apr --help lists all N commands") - apr-cli-qa-v1.yaml postconditions, FALSIFY-QA-001 rule, and proof_obligations[0].property Why this matters: when these prose counts go stale, an engineer reading the contract reasonably concludes either (a) the contract is behind reality and they should doubt it, or (b) the list of commands was shortened and a command got removed — neither is true. Five-whys: (1) the mcp command was added via PR #864 with contract update constrained to apr-cli-commands-v1.yaml; (2) sibling contracts that reference the count (publish + qa) were not updated in the same PR; (3) no CI linter cross-checks "N commands" strings against the authoritative registry count; (4) the drift persisted for ~1 day and would have confused contract reviewers on the next spec pass; (5) fix is bulk text replace plus a mental note to add a numeric cross-check linter in a follow-up (tracked separately). No test iteration count changes (the harnesses iterate the contract YAML entries, not the hardcoded number). The strings are readability only. Refs PMAT-037 * docs: bump 57→58 command count in book + spec prose Surface-prose sweep after bumping the two load-bearing contracts (apr-cli-publish + apr-cli-qa) in the previous commit. Same root cause: PR #864 added `apr mcp` as the 58th command but prose references scattered through the book and spec suite were not updated in lockstep. Touched (one literal "57 commands" → "58 commands" per line): - book/src/architecture/monorepo-layout.md — crate-tree caption - docs/specifications/apr-cli-qa-spec.md — 4 sites (problem framing, structural gate cell, Phase-1 section heading, Phase-8 grid line) - docs/specifications/aprender-monorepo-consolidation.md — the "Users NEVER pass --features" principle (line 414); the historical "DONE" entry at line 618 is left at 57 because it describes the phase as it was completed, not current state - docs/specifications/aprender-readme-book-rewrite.md — book tree caption Not touched (out of scope for this sweep): - docs/hero.svg and docs/specifications/apr-book-spec.md — user-facing graphics + marketing copy; will sweep separately - archive/ and examples/ — either historical or println strings with lower blast radius - .claude/skills/dogfood/SKILL.md — dogfood skill instruction, queued Refs PMAT-037 * docs(book/mcp): add FALSIFY-MCP-PROGRESS-001 row to gates table The book's falsification-gates table in book/src/tools/mcp-server.md listed rows for FALSIFY-MCP-001..008 and then the dispatcher-level FALSIFY-MCP-VALIDATE-001, but skipped the M3 addition FALSIFY-MCP-PROGRESS-001 that the spec already calls out as item 9 of the contract-bound gates (apr-mcp-server-spec.md#L159) and that the success-criteria row counts as part of the "9 falsification gates (FALSIFY-MCP-001..008 + PROGRESS-001)" invariant (L228). Five whys: - Symptom: book table shows 8 contract gates, spec says 9. - Why: PROGRESS-001 row was never added when M3 shipped (#887). - Why: M3 PR #887 landed PROGRESS-001 behaviour + test but did not touch the book's gates table (touched the narrative section only). - Why: the gates table is organized numerically and the PR author added PROGRESS-001 to the prose but not to the table below it. - Root cause: the table is a cross-cutting artifact that any new gate must be added to — no codegen pressure, no CI guard. - Fix: add the row now; future change: fold this into contract-driven codegen when apr-mcp-server-v1.yaml lands (PR #886, tracked for M4). Refs PMAT-037, FALSIFY-MCP-PROGRESS-001 * docs(aprender-mcp/README): fix 8→9 tools count in M3 codegen coverage The M3 entry said build.rs generates schemas for "all 8 tools"; in fact the contract apr-mcp-tool-schemas-v1.yaml has 9 entries (the M1 apr.version scaffold + the 8 Phase-1 workflow tools), and build.rs emits one pub const APR_<TOOL>_SCHEMA per entry for all 9. Five whys: - Symptom: README says "all 8 tools"; contract has 9 tool entries. - Why: the "8 tools" figure was the Phase-1 workflow-tool count. - Why: when FALSIFY-MCP-008 expanded to codegen every tool in M3 it picked up apr.version too, but the README M3 bullet kept the Phase-1-focused "8 tools" wording. - Why: the Phase-1 count and the registered-tool count are both in circulation in docs (spec refers to both as "8 Phase-1 tools plus apr.version") and it's easy to conflate them. - Root cause: no single-sourcing of the tool-count number — any doc can drift from `contracts/apr-mcp-tool-schemas-v1.yaml` (the authoritative list) silently. - Fix now: split the count honestly ("8th Phase-1 workflow tool — 9th registered" and "all 9 registered tools"); deferred fix: when the spec's M4 contract promotion (PR #886) lands, add a FALSIFY-MCP-008-style codegen check that the tool-count numbers in README/spec/book match the YAML row count. Refs PMAT-037 * docs: sweep remaining 57→58 command drift in book + spec prose Five prose sites still carried the stale 57-command count after the earlier commits bumped the contract YAMLs and the monorepo/crate-tree captions: - book/src/introduction.md (2 occurrences — "What is Aprender?" headline + CLI Reference bullet) - docs/specifications/apr-book-spec.md (2 occurrences — Ch 1.5 entry + Appendix A crate-map row for apr-cli) - docs/specifications/aprender-readme-book-rewrite.md (2 occurrences — Problem section intro + "What is aprender?" bullet) Why these were missed earlier: the previous sweep focused on contract YAMLs (apr-cli-commands-v1, apr-cli-publish-v1, apr-cli-qa-v1) + the monorepo layout crate-tree captions. These prose sites live in discursive book/spec text and weren't caught by the YAML-first grep. Scope discipline preserved: left the two intentional historical references alone — aprender-monorepo-consolidation.md#L618 DONE history line and apr-mcp-server-spec.md#L10/#L21 which say "58 commands (57 + mcp added PR #864)" on purpose to explain the jump. Refs PMAT-037 * docs(aprender-mcp/validate): refresh stale 'remaining 7 will follow' doc-comment The module doc-comment for apr.validate still read as if M2 was in progress — "the remaining 7 Phase-1 tools will follow: spawn apr <subcommand> --json...". M2 shipped 2026-04-17/18 (#865, #866, #867, #870, #872) and M3 shipped 2026-04-18 (#881), so all 7 M2 wrappers plus the M3 apr.finetune addition now live on this pattern. Updated to present-tense enumeration: lists each wrapper by name and makes explicit that apr.finetune also inherits the subprocess pattern, so a reader landing on this file first gets the full shape of what ships. Five whys: - Symptom: validate.rs doc-comment describes M2 as future work. - Why: comment was written when apr.validate was the first-shipped wrapper (#865) and the other 6 were still PRs. - Why: subsequent wrapper PRs (#866, #867, #870, #872) and the M3 addition (#881) didn't circle back to retire the "will follow" tense on the earliest module. - Why: no codegen or lint forced doc-comments to reference contract-driven tool counts, so the prose drifted silently. - Root cause: module doc-comments are low-visibility — they don't show up in tools/list output, so FALSIFY-MCP-008 doesn't catch them. - Fix: manual sweep now; longer-term, an apr-mcp doc-invariant contract could codegen "shipped tools" lists from the registry. Refs PMAT-037 * docs(mcp-contract): sync apr.serve description with source truth The YAML contract still said "Full lifecycle (cancel/SIGTERM) lands in M3." — but M3 shipped weeks ago (finetune + opt-in progress) and serve lifecycle was deferred to a post-M3 follow-up. The source-of-truth description in `crates/aprender-mcp/src/tools/serve.rs:44-46` already reads "Cancel-token lifecycle (SIGTERM) is a post-M3 follow-up" — the contract YAML is the one that drifted. Five-whys 1. Why did the YAML description drift from the source? → FALSIFY-MCP-008 only asserts byte-identity on the `inputSchema` (properties/required), not on the tool-level description. 2. Why was FALSIFY-MCP-008 scoped that way? → Descriptions are LLM-visible free-form prose that humans edit in both places during development; byte-comparing them every build would churn CI. 3. Why did the divergence survive post-M3? → No periodic kaizen sweep compares YAML tool descriptions with their source counterparts. 4. Why didn't any kanban/release task catch it? → Release templates don't list the MCP contract YAML among per-milestone artifacts to refresh. 5. Why not? → Contract YAML changes are treated as codegen input, not documentation — so prose rot goes unnoticed until a kaizen pass. Symptom fixed; root-cause follow-up (a byte-compare for descriptions, or a lint that forbids roadmap-tense phrases like "lands in Mx" after that milestone ships) is tracked for a future pass — not a PMAT-037 blocker because descriptions are advisory for LLM clients and the actual tool behaviour is covered by FALSIFY-MCP-005/007/008. Refs PMAT-037 * docs(mcp-contract): drop false stop_reason claim from apr.run description YAML + source both advertised that apr.run "returns tokens + tok/s + stop reason", but the apr CLI does not emit `stop_reason`. Spec line 90 of apr-mcp-server-spec.md records the ground truth: CLI as of 2026-04-18; `stop_reason` not emitted Replaced with an accurate inventory ("generated text, tokens, tok/s, and timing") plus the cancellation note that is genuinely load-bearing for MCP clients (FALSIFY-MCP-005 asserts cancel wiring). Five-whys 1. Why did the description promise a field the CLI doesn't emit? → The description was written speculatively ahead of a planned `apr run --json` enrichment that never landed. 2. Why did the speculative doc survive? → FALSIFY-MCP-008 compares inputSchema byte-for-byte, but does NOT compare the tool description to the actual CLI response keys. 3. Why doesn't any gate detect output-shape drift? → apr.run returns free-form stdout bytes to the MCP client; there is no typed contract on the response shape. 4. Why not? → The MCP tool surface is intentionally a pass-through so the CLI can evolve without churning the MCP spec. 5. Why does that hurt here? → Pass-through evolution needs matching doc-hygiene passes (like this one) to keep the LLM-visible description honest. Same root-cause class as the apr.serve fix one commit back. Same class of drift as 715781df5 (apr.serve "lands in M3"). Tracking a shared follow-up: lint for roadmap-tense phrases and a smoke-test that the description's field enumeration is a subset of the CLI's actual JSON keys. Refs PMAT-037 * docs(mcp-spec): clarify Success Criteria scope — spec ACTIVE, gate is for M4 close The header reads "Acceptance gate for promoting to ACTIVE" — but the spec status at the top already says ACTIVE (promoted at M3 ship on 2026-04-18). The criteria listed (contract-level gates, 9-gate pass including the M4 dogfood session) actually describe **closing M4** — promoting `apr-mcp-server-v1.yaml` from DRAFT to ENFORCED and lifting FALSIFY-MCP-003/-004 from PARTIAL to PASS. Five-whys 1. Why does "promoting to ACTIVE" survive past ACTIVE promotion? → The Success Criteria block was drafted pre-M3 when the spec was still DRAFT, and was never re-scoped after the M3 ship flipped the spec header to ACTIVE. 2. Why did no gate force a re-scope? → The spec's own header was updated in the same commit that set the status, but the mid-doc sections weren't traversed because nothing links them to the header change. 3. Why isn't that traversal automated? → provable-contracts' doc_integrity checker validates cross-links between spec and contract YAML, not internal consistency of roadmap language across sections of the same spec. 4. Why is internal consistency not a contract check? → Roadmap language ("will ship", "pending", "ACTIVE") is prose, not structured data — hard to assert byte-for-byte. 5. Why not structure the status fields? → Longer-term work; this commit is the symptom fix so readers can trust the Success Criteria block against the spec header. Now readers see: - Spec header: ACTIVE - Success Criteria: gate for closing M4 (contract DRAFT→ENFORCED, FALSIFY-MCP-003/-004 PARTIAL→PASS, dogfood done) That's the actual open-work framing. Refs PMAT-037 * docs(book/mcp): fix stale apr.version example payload (0.31.0 → 0.30.0) The book's apr.version example response used "0.31.0", but the tool emits CARGO_PKG_VERSION baked in at compile time — currently 0.30.0 (workspace Cargo.toml, unchanged since 2026-04-12). A client developer reading the doc and pinning to the example shape would see an immediate mismatch against a real server. Five-whys 1. Why did the doc show a version that doesn't exist? → The example was forward-scoped during an earlier release-planning pass that anticipated a 0.31.0 bump. 2. Why did that anticipated bump not land? → M1-M3 all shipped on main but never got tagged; the plan line in the spec says "M1-M3 planned for v0.32.0 publication" (line 263). 3. Why didn't the doc update when the tag plan changed? → Example payloads are prose, not codegen, and aren't covered by any contract byte-compare. 4. Why no lint for version strings in examples? → Version drift is rare and most tools show "x.y.z" abstracts; apr.version's case is unusual because the book shows a concrete literal. 5. Why show a concrete literal? → Helpful for readers debugging an actual tools/call round-trip — but that helpfulness inverts once the literal goes stale. Fix: set the example to 0.30.0 (current workspace version) and add a one-sentence note telling clients to parse for diagnostics rather than pin to the literal. That way the next version bump doesn't immediately invalidate the doc. Refs PMAT-514 * test(falsify-mcp-008): enforce tool description YAML↔source byte-equality Before: `migrated_tools_match_yaml_contract_byte_for_byte` compared only `inputSchema`, leaving `tools[*].description` free to drift silently. This drift was observed twice on 2026-04-18 alone (apr.serve — 715781df5, apr.run — 91a613968) after the YAML contract was audited manually against the source. Five whys: 1. Why did apr.serve/apr.run descriptions drift from the contract? → dev edits in tools/*.rs never propagated back to the YAML. 2. Why wasn't this caught in CI? → FALSIFY-MCP-008 harness compared only `inputSchema`. 3. Why was `inputSchema` the only thing compared? → M3 PR #881 scoped the byte-identity gate to the schema codegen path (build.rs emits APR_*_SCHEMA constants), where drift would crash the build. 4. Why didn't the contract itself catch this? → YAML line 282 asserted "each tool's `description` matches tools[*].description byte-for-byte" — but that assertion was aspirational, never wired into a test. 5. Root cause: claim-without-enforcement is the silent-drift seed. Fix is to make the assertion load-bearing by adding a second test that compares `ToolDefinition.description` to the YAML string directly. The new test `tool_descriptions_match_yaml_contract` discharges the class of drift that caused both commits above, without widening scope — it uses the same contract loader and `migrated_tools()` iterator as the existing schema gate. Verified: all 6 tests in falsify_mcp_008 pass, including the new one. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp-contract): flip DRAFT→ENFORCED, clear stale M3 parentheticals The contract YAML self-describes as DRAFT and pins its test_harness / codegen_consumer with "(to be added in M3)" parentheticals — but M3 shipped on 2026-04-18 (PR #881). The drift surfaces as: - Line 58 top-level `status: DRAFT` - Line 271 `FALSIFY-MCP-008.status: DRAFT` - Line 287 `test_harness: ...falsify_schema_codegen.rs (to be added in M3)` — the real harness is `falsify_mcp_008.rs` and has six tests green - Line 288 `codegen_consumer: ...build.rs (to be added in M3)` — already landed - Line 57 top-level `version: "1.0.0"` vs line 30 `metadata.version: 1.1.0` Five whys: 1. Why is the contract still DRAFT after M3 shipped? → nobody reran a spec audit after PR #881 merged. 2. Why did the M3-ship commit not touch this file's status? → PR #881 scope was "wire up codegen + harness"; contract fields were treated as documentation, not code. 3. Why weren't the parentheticals caught? → they read as prose, not as testable assertions; no gate compares them against reality. 4. Why didn't any automation flag a version mismatch between top-level `version` (1.0.0) and `metadata.version` (1.1.0)? → no such check exists on this contract schema. 5. Root cause: contract-as-documentation drift. Counterpart: PMAT-514 just added a harness test that makes the `description`-equality claim on line 282 load-bearing. This commit brings the surrounding prose (status + parentheticals + version pin) into alignment with that ENFORCED reality. Follow-up candidates (not in this commit): - Add a harness check that `metadata.version == top-level version` to prevent this class from re-emerging (parallel to FALSIFY-MCP-008). Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp-spec): document FALSIFY-MCP-008 description-equality extension Three coordinated edits, all propagating the harness change from PMAT-514 into the spec surface: 1. Gate summary (line 158): narrow "schema byte-identical" claim broadened to "schema + description byte-identical", naming both test functions explicitly so readers can find the enforcement point. 2. File-tree comment (line 60): `falsify_mcp_008.rs` blurb now says "schema + description byte-identity", matching the new test. 3. M5 re-run checklist (line 215): test count 75 → 76 (one new test in falsify_mcp_008.rs). Verified: `cargo test -p aprender-mcp` reports 51+8+4+6+4+2+1 = 76 tests all passing. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(roadmap): register PMAT-514 — APR-MCP-KAIZEN continuous drift sweep Adds the pmat work ticket that tracks ongoing kaizen on apr-mcp-server-spec and its satellites (aprender-mcp source, book chapter, schema contract YAML). Status: inprogress. First discharge: byte-compare YAML tool descriptions with source descriptions (closed silent-drift class that bit apr.serve on 715781df5 and apr.run on 91a613968 in one 24h window). Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(book/mcp): book chapter mirrors FALSIFY-MCP-008 description extension Symmetric to the spec update in 2f38f0241. Two book edits: 1. Falsification gates table (line 333): gate now reads "inputSchema AND description byte-identical" — same broadening applied to the spec. 2. Schema-codegen prose (line 315-320): calls out the two specific test functions that enforce the gate, and tightens the "edit YAML, rebuild" guidance to include descriptions. Readers landing on the book chapter (via rustdoc cross-link or GitHub Pages) now see the same gate surface as spec readers. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(aprender-mcp/README): mirror FALSIFY-MCP-008 description extension Crate README's gate table is the third surface that readers hit — after the spec and book chapter. Aligning all three to say "inputSchema AND description" closes the documentation side of the silent-drift class. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp-contract): sharpen coverage-note — 9 entries, gate surface spelled out Before: the coverage note said "All 8 Phase-1 tools are now registered in this contract" — technically correct (apr.version is an M1 scaffold, not a Phase-1 workflow tool) but ambiguous, because the FALSIFY-MCP-008 harness iterates over all 9 entries including apr.version. A new reader easily miscounts. After: the note enumerates both categories explicitly (scaffold + 8 wrappers = 9 entries) and adds a second paragraph spelling out what the PMAT-514 extension now covers — `inputSchema` byte-identity AND tool-level `description` byte-identity — with the specific test function names. This matches the surface that was already asserted in the falsification block above (lines 281-286) and discharges the ambiguity in one pass. Verified: `cargo test -p aprender-mcp --test falsify_mcp_008` 6/6 green. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp-spec): add apr-mcp-tool-schemas-v1 to Contracts header list The tool-schemas contract is the **single source of truth** for every MCP tool's `inputSchema` (and, as of PMAT-514, description), drives the `build.rs` codegen, and is referenced by FALSIFY-MCP-008 — yet it was missing from the header `**Contracts**:` list. The spec's own body text referenced it five times (lines 27, 40, 158, 177, 193) but a reader landing on the spec from a link would not see it in the contract register. Five whys: 1. Why was the contract not listed? → the header was authored before the tool-schemas YAML was split out into a standalone contract. 2. Why didn't the split author backfill the header? → the split PR (#871 — authored the YAML) focused on the contract body; the spec header wasn't on the review checklist. 3. Why isn't there a checklist? → spec-header/contract-file consistency has no automated gate. 4. Why no gate? → the spec body mentions multiple contracts in prose, so "spec references contract X" doesn't uniquely identify which contracts should appear in the header. 5. Root cause: the header is a curated list (things a reader must know about), not a mechanical index. Kaizen is the right fix for curated-list drift — no automation needed, just periodic sweeps. Also included the ENFORCED status inline so readers see M3 progress at a glance. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(mcp-contract): broaden FALSIFY-MCP-008 condition to match assertions The `assertions:` block already covered descriptions (line 282) but the prose `condition:` above it talked only about "JSON Schema". Readers skimming the condition paragraph would miss that descriptions are also load-bearing. The rewrite preserves the JSON canonicalization language (important — that's the byte-for-byte definition) and adds a second clause spelling out how descriptions flow: directly compared at test time against `ToolDefinition.description`, separate from the build.rs codegen path that carries `inputSchema`. Verified: `cargo test -p aprender-mcp --test falsify_mcp_008` still 6/6 green. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(falsify-mcp-008): refresh module doc-comment for PMAT-514 extension The file-level doc-comment predated the description-equality test added in PMAT-514. Three updates: 1. Opening summary: "byte-identical to the schema" → "byte-identical to the corresponding entry ... covering both the `inputSchema` object and the tool-level `description` string" — so cargo-doc readers see the full gate surface on first hit. 2. Numbered list: step 6 added for the description assertion, keeping the structural schema assertion as step 5. 3. Scope paragraph: "Scope (M3 completion — PR #881 follow-up)" → "Scope (M3 shipped, extended by PMAT-514 on 2026-04-18)" and counts updated from "all 8 Phase-1 tools" to "all 9 registered tools (apr.version + 8 Phase-1 wrappers)" — matches the contract coverage-note landed in 3266e365f. Verified: 6/6 tests still pass. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(book/mcp): sharpen 'edit YAML, rebuild' — descriptions need Rust edit too Previous prose read "The Rust source does not need editing for schemas, and descriptions must track the YAML verbatim" — technically implies descriptions auto-flow from the YAML. They don't: the description string is hand-written in `crates/aprender-mcp/src/tools/<tool>.rs` and must be mirrored manually when the YAML changes. The harness (`tool_descriptions_match_yaml_contract`) fails CI on divergence but does not auto-fix the source. Why this matters: a contributor reading the old wording would think editing only the YAML is enough, push, and then be surprised when CI fails. The new wording makes the two-file edit explicit. Future cleanup: extend `build.rs` to codegen description constants too, then this note can collapse back to "edit YAML only". Not in scope for PMAT-514 — the test-time enforcement is sufficient today. Refs PMAT-514 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(aprender-mcp): codegen tool descriptions from YAML contract Extends build.rs to emit `APR_<TOOL>_DESCRIPTION: &str` alongside the existing `APR_<TOOL>_SCHEMA: &str` for each tool in `contracts/apr-mcp-tool-schemas-v1.yaml`. All 9 tool modules now consume `crate::schemas::APR_<TOOL>_DESCRIPTION.to_string()` instead of hand-mirroring the string in Rust source. Five-whys: 1. Why extend codegen? Descriptions drifted silently twice in a 24h window (apr.serve 715781df5, apr.run 91a613968). 2. Why did the test-time gate (PMAT-514) not catch drift before merge? It did — but only after the drift was committed; a compile-time gate prevents the drift from ever building. 3. Why split schema and description into separate constants instead of one merged blob? ToolDefinition's `description` is a Rust String, not JSON; keeping them separate avoids forcing a JSON round-trip on a non-JSON field. 4. Why keep the test-layer `tool_descriptions_match_yaml_contract` if codegen eliminates drift? Defence in depth — catches a future refactor that replaces the codegen consumer with a literal. 5. Why only 9 files to update? 8 Phase-1 wrappers + apr.version are the entire current tool surface. M5 tools will consume the codegen constants from day one. Refs PMAT-514. * test(falsify-mcp-008): codegen-layer description gate + coverage guardrail Adds two new tests to `falsify_mcp_008.rs`: * `codegen_description_constants_match_yaml` — asserts each `schemas::APR_<TOOL>_DESCRIPTION` codegen constant equals `tools[*].description` byte-for-byte. This is a strictly stronger gate than `tool_descriptions_match_yaml_contract`: the live-ToolDefinition test would silently pass if a future refactor replaced `APR_X_DESCRIPTION.to_string()` with a hand-coded literal. Asserting the codegen constant itself closes that bypass route. * `codegen_descriptions_cover_every_tool_name` — mirrors the existing `codegen_constants_cover_every_tool_name` guardrail: every name in `schemas::TOOL_NAMES` must appear in `CODEGEN_DESCRIPTIONS`, catching the case where a new tool is added to YAML but its description constant isn't registered in the test table. Refreshes module-level doc-comment to enumerate 7 layers of coverage and the dual codegen path (SCHEMA + DESCRIPTION). Test count: falsify_mcp_008 grows 6→8; aprender-mcp total 76→78. Refs PMAT-514. * docs(mcp): sync all surfaces with PMAT-514 description-codegen extension Mirrors the build.rs description-codegen change into every doc surface that previously said descriptions were hand-mirrored: * docs/specifications/apr-mcp-server-spec.md — FALSIFY-MCP-008 row now names the codegen-layer test; M3 milestone bullet points at the PMAT-514 extension; suite count 76→78. * contracts/apr-mcp-tool-schemas-v1.yaml — `condition:` prose and `test_harness:` / `codegen_consumer:` pointers describe both `APR_<TOOL>_SCHEMA` and `APR_<TOOL>_DESCRIPTION` codegen paths; tool-registry comment states both fields flow through build.rs. * book/src/tools/mcp-server.md — "edit YAML, rebuild" guidance updated: changing a description now requires only a YAML edit (was: YAML + Rust); enumerates 4 sub-tests (2 live, 2 codegen). * crates/aprender-mcp/README.md — gate-table row references the dual codegen constants. Refs PMAT-514. * chore(roadmap): PMAT-514 record description-codegen discharge line Marks the PMAT-514 roadmap entry with a DISCHARGED acceptance line pointing at the two-layer gate (test-layer + codegen-layer) and the `APR_<TOOL>_DESCRIPTION` build.rs output. The top-level "ongoing kaizen sweeps" acceptance stays — this is one ticket, many sweeps. Refs PMAT-514. * docs(mcp): sync remaining module-doc + README M3 bullet with PMAT-514 Three surfaces still described M3 codegen as "schema only": * docs/specifications/apr-mcp-server-spec.md — file-tree build.rs comment now spells out both constants emitted. * crates/aprender-mcp/README.md — M3 milestone bullet enumerates `APR_<TOOL>_SCHEMA` and `APR_<TOOL>_DESCRIPTION`. * crates/aprender-mcp/src/lib.rs — module-doc for `schemas` now documents both constants, how to consume them, and that hand-coding either is …
noahgift
added a commit
that referenced
this pull request
May 13, 2026
The matmul-v1 contract macro now panics with
"Contract [matmul] Pre-condition violated: a.len() == m * k"
BEFORE the function body's assert_eq!("Matrix A size mismatch") can
fire, so test_matmul_size_mismatch_{a,b} with
`expected = "size mismatch"` fail with "panic did not contain expected
string".
Update the expected substring to match the contract macro's message.
Every PR against main has been failing workspace-test on this regression;
this unblocks the queue.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
matmul-v1contract macro panics with `Contract [matmul] Pre-condition violated: a.len() == m * k` before the function body'sassert_eq!("Matrix A size mismatch")firestest_matmul_size_mismatch_{a,b}therefore fail with "panic did not contain expected string" — theirexpected = "size mismatch"no longer matchesexpectedto"Pre-condition violated"(matches the contract macro's message)Test plan
cargo test -p aprender-train --lib test_matmul_size_mismatch— 2 passed ✅ci / gate+workspace-testgreen🤖 Generated with Claude Code