11.6.2 The shared-schema seam + the TOTALITY mechanism in `toolOk` — proven on `get_work_item`, with the exemption registry
The mechanism the rest of the story applies, built and proven on ONE tool. It re-bases nothing else — three later cards do the volume — because a seam is worth reviewing on its own before thirty-odd files move through it.
What to build
- The shared-schema import path, wherever 11.6.1 Q2 pinned it, and whatever hoist that answer implies. If Q2 chose to move
TOKEN_SCOPEStoo, that move lands here, in the same PR as the direction it fixes. toolOkbecomes schema-aware. Today it takesstructuredContent: Record<string, unknown>and 33 of the 37 tools return through it, so it is the one place totality can be structural. Re-type it per Q4 so a tool whose output resource HAS a declared schema cannot hand it an unvalidated object — mirroringlib/mcp/scopes.ts, where an ungated tool is a compile error rather than a review note.- The EXEMPTION registry — the tools with no v1 counterpart, as a typed value with a reason per entry (Q5's table, executable). A tool that is neither derived nor explicitly exempt must not typecheck. This is what makes the guard's silence meaningful: an absent tool is currently indistinguishable from a covered one.
- The shared-resource set, DERIVED. Compute "resources exposed by BOTH surfaces" from
lib/api/v1/openapi/registry.tsrather than a hand-written list, so a resource added later joins the guard's scope without anyone remembering. - Prove it on
get_work_item— the tool at the centre of the founding defect. ItsstructuredContentstops beingstructured as unknown as Record<string, unknown>and becomes the shared schema's output. - Rewrite
lib/mcp/toolResult.ts's header comment to the decision 11.6.1 recorded — if that card has not already, which it should have. Verify againstorigin/mainrather than assuming either way. - Update
docs/mcp.mdfor anything this changes in what a tool ADVERTISES. If Q1 chose to declare anoutputSchema, the shape becomes visible intools/listand that is a documented behaviour change; if it chose the internal-derivation form, say explicitly that nothing a caller sees moved.
listSprints.ts is the one tool that does not use toolOk
Verified on origin/main: it is the sole tool building its result without the helper (readyFilters, sprintRef and workItemRef are helpers, not tools). Handle it per Q4's answer — bring it through toolOk, or give the exemption registry an entry saying why not. Do not leave it as the one tool the mechanism cannot see, which is exactly the shape of the defect this story exists to remove.
Scope BOUNDARY
Ends at the seam, the totality mechanism, the exemption registry and ONE re-based tool. It does NOT re-base search_work_items, list_ready or any other tool (11.6.3 / 11.6.4 / 11.6.5). It does NOT ship the CI drift guard (11.6.6) — the mechanism here makes coverage checkable; the guard is what runs it over the whole set. It does NOT change any tool's name, arguments, scope or description text. It does NOT change /api/v1.
Acceptance criteria
toolOkis typed so a tool whose output resource has a declared schema cannot pass it an unvalidated object — proven by a compile-failure fixture, the way the scope model's totality is proven, not by inspection.- A tool that is neither schema-derived nor listed in the exemption registry does not typecheck — same fixture treatment.
- The exemption registry carries a reason string per entry, and adding an entry is a deliberate edit rather than a default.
- The shared-resource set is computed from the v1 operation registry; a test adds a fake operation and asserts the set grows without any other file changing.
get_work_item'sstructuredContentis the shared schema's output, with noas unknown ascast remaining in that tool.get_work_item's observable payload is UNCHANGED — same keys, same values, for the same row — asserted by the existing MCP suites passing unmodified, includingtests/mcp/dependency-edges.test.tsandtests/mcp/comment-counts.test.ts.listSprints.tseither returns throughtoolOkor has an exemption entry explaining why; it is not silently outside the mechanism.lib/mcp/toolResult.ts's header comment states the current decision, not Story 7.8's.docs/mcp.mdreflects any change to what a tool advertises, or records that nothing a caller sees moved.- No file imports both
zodandzod/v4— asserted, per 11.6.1 Q3. - The per-file coverage floor (≥90%) holds on every new and changed file.
Context refs
- 11.6.1 — Q2 (where the schemas live), Q3 (the zod boundary), Q4 (the
toolOktyping + derived set) and Q5 (the exemption table) are this card's spec. lib/mcp/toolResult.ts—toolOkat line ~93, and the header comment to correct.lib/mcp/tools/getWorkItem.ts— the proving tool; note itsstructured as unknown as Record<string, unknown>and the two batched reads (getDependencyEdgesForItems,getCommentCountsForItems) whose output the schema must accommodate.lib/mcp/tools/listSprints.ts— the one tool outsidetoolOk.lib/mcp/scopes.ts— the totality pattern being copied.lib/api/v1/openapi/registry.ts— the source of the derived resource set.lib/api/v1/workItems/schema.ts—workItemDetailSchema, the shapeget_work_itemderives from.tests/mcp/dependency-edges.test.ts·comment-counts.test.ts·tools.test.ts— the suites that must pass unmodified.- Story: 11.6.
⚠️ Amended 2026-08-06 (MOTIR-2227 / MOTIR-2276) — what the "suites pass unmodified" criterion actually forbids
Building this card surfaced a collision the plan had not compared for: key means the PROD-<n> identifier on /api/v1 and on MCP's ready rows, but the NUMERIC key on MCP's work-item rows (WorkItemSummaryDto.key / WorkItemListItemDto.key). A live get_work_item returns children as {"key":2227,"identifier":"MOTIR-2227"}. A row already carrying key: 2227 cannot additively gain key: "MOTIR-2227", so this card's "observable payload UNCHANGED / suites pass unmodified" criterion and the story's "both surfaces validate against the SAME schema" criterion are jointly unsatisfiable as written.
Decided in ADR Amendment 7 Q6 + its addendum (docs/decisions/public-api-conventions.md), not improvised here. The criterion is REPLACED by a rule about what may change:
- Still a violation, and still the tell the criterion was written to catch: a key REMOVED, a key RENAMED, or a VALUE altered. Fix the code, never the expectation.
- Permitted, and only these two: (1) a whole-payload
toEqualthat fails solely because a v1 field was ADDED beside the existing ones — the added field must be named in the PR body; (2) thekey→ identifier change and itsnumericKeycompanion, which is the ONE non-additive change in Story 11.6 and is named in the ADR. Its only consumer istests/mcp/search.test.ts(three{ key: number }cast sites);@motir/clinever reads a numeric key (grep -rn "key: number" packages/cli/src/→ no matches).
Everything else about the criterion stands: the suites are frozen, and an edit outside those two categories is the defect.