Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-2228Done

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

  1. The shared-schema import path, wherever 11.6.1 Q2 pinned it, and whatever hoist that answer implies. If Q2 chose to move TOKEN_SCOPES too, that move lands here, in the same PR as the direction it fixes.
  2. toolOk becomes schema-aware. Today it takes structuredContent: 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 — mirroring lib/mcp/scopes.ts, where an ungated tool is a compile error rather than a review note.
  3. 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.
  4. The shared-resource set, DERIVED. Compute "resources exposed by BOTH surfaces" from lib/api/v1/openapi/registry.ts rather than a hand-written list, so a resource added later joins the guard's scope without anyone remembering.
  5. Prove it on get_work_item — the tool at the centre of the founding defect. Its structuredContent stops being structured as unknown as Record<string, unknown> and becomes the shared schema's output.
  6. 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 against origin/main rather than assuming either way.
  7. Update docs/mcp.md for anything this changes in what a tool ADVERTISES. If Q1 chose to declare an outputSchema, the shape becomes visible in tools/list and 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

  • toolOk is 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's structuredContent is the shared schema's output, with no as unknown as cast 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, including tests/mcp/dependency-edges.test.ts and tests/mcp/comment-counts.test.ts.
  • listSprints.ts either returns through toolOk or 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.md reflects any change to what a tool advertises, or records that nothing a caller sees moved.
  • No file imports both zod and zod/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 toolOk typing + derived set) and Q5 (the exemption table) are this card's spec.
  • lib/mcp/toolResult.tstoolOk at line ~93, and the header comment to correct.
  • lib/mcp/tools/getWorkItem.ts — the proving tool; note its structured 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 outside toolOk.
  • 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.tsworkItemDetailSchema, the shape get_work_item derives 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 toEqual that fails solely because a v1 field was ADDED beside the existing ones — the added field must be named in the PR body; (2) the key → identifier change and its numericKey companion, which is the ONE non-additive change in Story 11.6 and is named in the ADR. Its only consumer is tests/mcp/search.test.ts (three { key: number } cast sites); @motir/cli never 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.