Files
agentic-dev/docs/superpowers/refactor-logs/2026-05-06-input-output-unification.md
Danijel Martinek 3dc843ca4b docs(refactor-log): reconcile commit count + test delta inconsistencies
Final reviewer flagged two changelog inaccuracies:
- Summary said 14 commits; actual is 15 (88db39b..HEAD).
- §7 Task 8 said +144 tests; correct delta is +35 (325 → 360, +11%).
  The +144 figure was a typo from the implementer's draft.

Both reconciled. Plan 9 ships.
2026-05-06 16:15:50 +02:00

257 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Refactor Changelog — Input/Output Unification + Presenter + Error Middleware + Public-Surface Cleanup
**Started:** 2026-05-06
**Spec:** [2026-05-06-input-output-unification-design.md](../specs/2026-05-06-input-output-unification-design.md)
**Plan:** [2026-05-06-plan-9-io-unification.md](../plans/2026-05-06-plan-9-io-unification.md)
**Branch:** feature/io-unification
This document captures every architectural change made during Plan 9
execution, organized by category. After the plan is merged, use the
"Doc update checklist" at the bottom to update external docs in a
single follow-up pass — combined with the still-pending Plan 8
doc-update items so docs are written once for the post-Plan-9 state.
---
## Summary
**Completed:** 2026-05-06
**Branch:** main (or feature/io-unification per execution choice)
**Total commits:** 15 (Tasks 19, including the R6 fix-up + auth code-review tightening + blog style sweep + 2× refactor-log catch-up commits)
**Net test count change:** 325 → 360 (+35 tests, +11%)
| Category | Count |
|---|---|
| Files added | 5 × procedures.ts + 5 × ui/index.ts + 1 × define-error-middleware{.ts,.test.ts} + 1 × ADR + 1 × changelog = 14 (approx; some ui/index.ts replace existing query.ts) |
| Files modified | 12 use cases + 12 controllers + 5 routers + 5 src/index.ts + 5 package.json + 5 router tests + ~25 test files = ~70 |
| New tRPC error codes mapped | 5 (BAD_REQUEST, NOT_FOUND, UNAUTHORIZED, FORBIDDEN, plus tRPC built-in BAD_REQUEST from zod) |
### Tasks completed
| Task | Commit | Description |
|---|---|---|
| 1 | `61baaae` | Refactor changelog scaffold |
| 2 | `e25b1f7`, `aac37fd` | core-shared defineErrorMiddleware + t export + jsdoc fix |
| 3 | `2bbec70`, `5b61674`, `70c7b7d` | auth migration + changelog §6.3 + test tightening |
| 4 | `86070b2`, `614c901` | blog migration + style dividers |
| 5 | `f4adf31` | marketing-pages migration |
| R6 | `9663c82`, `aff0ce0` | this.name in all error classes + changelog §7 |
| 6 | `27c79e6` | navigation migration |
| 7 | `2b67964` | media migration |
| 8 | `2df137c` | final verification + app caller stragglers + changelog |
| 9 | (this commit) | ADR-013 + changelog summary |
### Conformance verification (Task 8)
Spec acceptance criteria §8: all met.
- Every use case exports xInputSchema; non-void use cases also export xOutputSchema.
- Every non-void controller has a top-level presenter and uses ReturnType<typeof presenter>.
- Every feature has procedures.ts with feature-scoped error map.
- core-shared/trpc/define-error-middleware.ts is the only plumbing in core-shared; no central name→code registry.
- Per-feature package.json has ./ui subpath; root index.ts no longer exports query builders.
- R25 (output-validation) test exists per non-void use case.
- R26 (router error-mapping) test exists per feature.
- pnpm typecheck && pnpm lint && pnpm test && pnpm turbo boundaries && pnpm build all green.
---
## 1. Files added
- packages/core-shared/src/trpc/define-error-middleware.ts — middleware factory mapping [ErrorCtor, TRPC_CODE] tuples to TRPCError translation
- packages/core-shared/src/trpc/define-error-middleware.test.ts — 4 tests covering mapped translation, multiple codes, unmapped passthrough (verifies INTERNAL_SERVER_ERROR + cause preservation), cause preservation
- packages/auth/src/integrations/api/procedures.ts — authProcedure with feature error map (InputParse → BAD_REQUEST, Auth/Unauthenticated → UNAUTHORIZED, Unauthorized → FORBIDDEN)
- packages/auth/src/ui/index.ts — placeholder UI surface (no queries today; mutations only)
- packages/blog/src/integrations/api/procedures.ts — blogProcedure with feature error map (InputParseError → BAD_REQUEST, ArticleNotFoundError → NOT_FOUND)
- packages/blog/src/ui/index.ts — re-exports articleBySlugQuery and listArticlesQuery from ./query (moved from feature root index)
- packages/marketing-pages/src/integrations/api/procedures.ts — marketingPagesProcedure with feature error map (InputParseError → BAD_REQUEST, PageNotFoundError → NOT_FOUND)
- packages/marketing-pages/src/ui/index.ts — re-exports pageBySlugQuery and siteSettingsQuery from ./query (moved from feature root index)
- packages/navigation/src/integrations/api/procedures.ts — navigationProcedure with feature error map (InputParseError → BAD_REQUEST, HeaderNotFoundError → NOT_FOUND)
- packages/navigation/src/ui/index.ts — re-exports headerQuery from ./query (moved from feature root index)
- packages/media/src/integrations/api/procedures.ts — mediaProcedure with feature error map (InputParseError → BAD_REQUEST, MediaNotFoundError → NOT_FOUND)
- packages/media/src/ui/index.ts — placeholder UI surface (no queries today; media has no React Query option builders)
- packages/media/src/integrations/api/router.test.ts — R26 router error-mapping tests
## 2. Files modified
- packages/core-shared/src/trpc/init.ts — `t` instance now exported (was internal const) so feature procedures.ts can do `t.procedure.use(...)`
- packages/core-shared/package.json — added "./trpc/define-error-middleware" subpath export
- packages/core-shared/tsconfig.json — set `rootDir: "."` and added `@/*` path alias so test files using `@/` resolve correctly under `tsc --noEmit`
- packages/auth/src/application/use-cases/sign-in.use-case.ts — input + output schemas; output.parse before return; SignInInput/SignInOutput types exported
- packages/auth/src/application/use-cases/sign-up.use-case.ts — input + output schemas (with confirmPassword refine); output.parse; types exported; removed user from output (presenter extracts cookie)
- packages/auth/src/application/use-cases/sign-out.use-case.ts — input schema only (void output, no presenter); SignOutInput exported; takes { sessionId } object instead of raw string
- packages/auth/src/interface-adapters/controllers/sign-in.controller.ts — presenter returning cookie; unknown input; ReturnType<typeof presenter> return type
- packages/auth/src/interface-adapters/controllers/sign-up.controller.ts — presenter returning cookie; unknown input
- packages/auth/src/interface-adapters/controllers/sign-out.controller.ts — no presenter (void); unknown input; Promise<void> return
- packages/auth/src/integrations/api/router.ts — uses authProcedure, .input(xInputSchema)
- packages/auth/src/index.ts — schemas + types now exported from feature root
- packages/auth/package.json — added ./ui subpath
- packages/auth/tests/sign-in-flow.feature.test.ts — updated to match new presenter shapes (cookie return, void sign-out, object input)
- All affected auth use-case + controller tests updated for new contracts
- packages/blog/src/application/use-cases/get-articles.use-case.ts — getArticlesInputSchema + getArticlesOutputSchema; status narrowed to articleStatusSchema; output.parse; types exported
- packages/blog/src/application/use-cases/create-article.use-case.ts — createArticleInputSchema + createArticleOutputSchema; output.parse; types exported
- packages/blog/src/application/use-cases/get-article-by-slug.use-case.ts — getArticleBySlugInputSchema + getArticleBySlugOutputSchema; output.parse; types exported
- packages/blog/src/interface-adapters/controllers/get-articles.controller.ts — identity presenter; unknown input; imports getArticlesInputSchema from use-case
- packages/blog/src/interface-adapters/controllers/create-article.controller.ts — identity presenter; unknown input; imports createArticleInputSchema from use-case
- packages/blog/src/interface-adapters/controllers/get-article-by-slug.controller.ts — identity presenter; unknown input; imports getArticleBySlugInputSchema from use-case
- packages/blog/src/integrations/api/router.ts — uses blogProcedure + .input(xInputSchema) for all 3 procedures
- packages/blog/src/index.ts — removed articleBySlugQuery/listArticlesQuery re-exports; added schemas + types + IUseCase/IController aliases
- packages/blog/package.json — added ./ui subpath export
- All affected blog use-case + controller tests updated for new contracts
- packages/marketing-pages/src/application/use-cases/get-page-by-slug.use-case.ts — getPageBySlugInputSchema + getPageBySlugOutputSchema; output.parse (returns undefined for missing page per existing behavior); types exported
- packages/marketing-pages/src/application/use-cases/get-site-settings.use-case.ts — getSiteSettingsInputSchema (z.object({}).strict() per R5) + getSiteSettingsOutputSchema; output.parse; types exported; _input parameter (void input)
- packages/marketing-pages/src/interface-adapters/controllers/get-page-by-slug.controller.ts — identity presenter; unknown input; returns undefined for missing page
- packages/marketing-pages/src/interface-adapters/controllers/get-site-settings.controller.ts — identity presenter; unknown input; imports getSiteSettingsInputSchema from use-case
- packages/marketing-pages/src/integrations/api/router.ts — uses marketingPagesProcedure + .input(xInputSchema) for both procedures; siteSettings now uses .input(getSiteSettingsInputSchema)
- packages/marketing-pages/src/index.ts — removed pageBySlugQuery/siteSettingsQuery re-exports; added schemas + types + IUseCase/IController aliases
- packages/marketing-pages/package.json — added ./ui subpath export
- All affected marketing-pages use-case + controller tests updated for new contracts
- packages/navigation/src/application/use-cases/get-header.use-case.ts — getHeaderInputSchema (z.object({}).strict()) + getHeaderOutputSchema (= headerSchema); _input parameter; output.parse; types exported
- packages/navigation/src/interface-adapters/controllers/get-header.controller.ts — identity presenter; unknown input; imports getHeaderInputSchema from use-case; ReturnType<typeof presenter> return type
- packages/navigation/src/integrations/api/router.ts — uses navigationProcedure + .input(getHeaderInputSchema); ctrl called with input
- packages/navigation/src/index.ts — removed headerQuery re-export (moved to ./ui); exports getHeaderInputSchema, getHeaderOutputSchema, GetHeaderInput, GetHeaderOutput, IGetHeaderUseCase, IGetHeaderController; added HeaderNotFoundError + InputParseError re-exports
- packages/navigation/package.json — added ./ui subpath export
- packages/navigation/src/integrations/api/router.test.ts — updated to call caller.header({}); added R26 describe block; uses beforeEach/afterEach to rebind container
- packages/media/src/application/use-cases/get-media.use-case.ts — getMediaInputSchema + getMediaOutputSchema (= mediaSchema); output.parse; types exported
- packages/media/src/application/use-cases/list-media.use-case.ts — listMediaInputSchema (limit/offset strict object) + listMediaOutputSchema (z.array(mediaSchema)); output.parse; types exported
- packages/media/src/application/use-cases/delete-media.use-case.ts — deleteMediaInputSchema (void output — no xOutputSchema); types exported; input now typed DeleteMediaInput
- packages/media/src/interface-adapters/controllers/get-media.controller.ts — identity presenter; unknown input; imports getMediaInputSchema from use-case
- packages/media/src/interface-adapters/controllers/list-media.controller.ts — identity presenter; unknown input; imports listMediaInputSchema from use-case
- packages/media/src/interface-adapters/controllers/delete-media.controller.ts — no presenter (void); unknown input; imports deleteMediaInputSchema from use-case
- packages/media/src/integrations/api/router.ts — uses mediaProcedure + .input(xInputSchema) for all 3 procedures; no more publicProcedure or local schema redefinitions
- packages/media/src/index.ts — exports schemas (getMediaInputSchema/Output, listMediaInputSchema/Output, deleteMediaInputSchema) + types + IUseCase/IController aliases
- packages/media/package.json — added ./ui subpath export
## 3. Pattern changes (code-level)
### 3.1 Use-case files — input + output schemas + runtime parse
auth migrated: all 3 use cases. signIn and signUp export xInputSchema + xOutputSchema + types; signOut exports xInputSchema only (void output). All non-void use cases end with `xOutputSchema.parse(result)` before returning.
blog migrated: all 3 use cases. getArticles, createArticle, getArticleBySlug each export xInputSchema + xOutputSchema + XInput + XOutput types. getArticlesInputSchema narrows status to articleStatusSchema (not loose string). All 3 end with `xOutputSchema.parse(result)` before returning.
marketing-pages migrated: both use cases. getPageBySlug exports getPageBySlugInputSchema + getPageBySlugOutputSchema + types; returns undefined for missing page (preserving existing semantics) — parse only called when page found. getSiteSettings exports getSiteSettingsInputSchema (z.object({}).strict() — void input per R5) + getSiteSettingsOutputSchema + types; takes `_input: GetSiteSettingsInput` to satisfy uniform input contract.
navigation migrated: single use case (getHeader). Exports getHeaderInputSchema (z.object({}).strict() — void input per R5) + getHeaderOutputSchema (= headerSchema) + types; takes `_input: GetHeaderInput`; throws HeaderNotFoundError when repository returns falsy (existing behavior preserved); ends with `getHeaderOutputSchema.parse(header)`.
media migrated: all 3 use cases. getMedia exports getMediaInputSchema + getMediaOutputSchema (= mediaSchema) + types; ends with getMediaOutputSchema.parse(media). listMedia exports listMediaInputSchema (strict object with optional int limit/offset) + listMediaOutputSchema (z.array(mediaSchema)) + types; ends with listMediaOutputSchema.parse(result). deleteMedia exports deleteMediaInputSchema only (void output per R12); no xOutputSchema.
### 3.2 Controller files — presenter + unknown input + view return type
auth migrated: all 3 controllers. signIn/signUp have `function presenter(value: XOutput)` returning `value.cookie`; return type is `ReturnType<typeof presenter>`. signOut has no presenter (void). All controllers accept `unknown` input and safeparse with the use-case schema.
blog migrated: all 3 controllers. All 3 (getArticles, createArticle, getArticleBySlug) have `function presenter(value: XOutput)` that is identity (`return value`); return type is `ReturnType<typeof presenter>`. All accept `unknown` input and safeparse with the imported use-case schema.
marketing-pages migrated: both controllers. getPageBySlug has identity presenter; return type is `ReturnType<typeof presenter> | undefined` (preserves missing-page semantics). getSiteSettings has identity presenter; return type is `ReturnType<typeof presenter>`. Both accept `unknown` input and safeparse with the imported use-case schema.
navigation migrated: single controller (getHeaderController). Identity presenter; return type `ReturnType<typeof presenter>`. Accepts `unknown` input and safeparses with getHeaderInputSchema imported from use-case file.
media migrated: all 3 controllers. getMediaController and listMediaController have `function presenter(value: XOutput)` that is identity (`return value`); return type is `ReturnType<typeof presenter>`. deleteMediaController has no presenter (void return); return type is `Promise<void>`. All 3 accept `unknown` input and safeparse with the imported use-case schema.
### 3.3 tRPC integration — feature-scoped procedures, schema reuse from use cases
auth migrated: authProcedure in procedures.ts wraps defineErrorMiddleware with 4-tuple error map. Router uses `authProcedure.input(xInputSchema)` for all 3 procedures — no more local schema redefinition.
blog migrated: blogProcedure in procedures.ts wraps defineErrorMiddleware with 2-tuple map (InputParseError → BAD_REQUEST, ArticleNotFoundError → NOT_FOUND). Router uses `blogProcedure.input(xInputSchema)` for all 3 procedures.
marketing-pages migrated: marketingPagesProcedure in procedures.ts wraps defineErrorMiddleware with 2-tuple map (InputParseError → BAD_REQUEST, PageNotFoundError → NOT_FOUND). Router uses `marketingPagesProcedure.input(xInputSchema)` for both procedures. siteSettings now uses `.input(getSiteSettingsInputSchema)` (was a no-input `.query()`).
navigation migrated: navigationProcedure in procedures.ts wraps defineErrorMiddleware with 2-tuple map (InputParseError → BAD_REQUEST, HeaderNotFoundError → NOT_FOUND). Router uses `navigationProcedure.input(getHeaderInputSchema)` (was a no-input `.query()` with publicProcedure).
media migrated: mediaProcedure in procedures.ts wraps defineErrorMiddleware with 2-tuple map (InputParseError → BAD_REQUEST, MediaNotFoundError → NOT_FOUND). Router uses `mediaProcedure.input(xInputSchema)` for all 3 procedures — getMedia (query), listMedia (query), deleteMedia (mutation).
## 4. Error-middleware adoption
- core-shared infrastructure landed; feature routers will adopt in Tasks 3-7.
- Discriminator: `instanceof Ctor` (not error name string), so duck-typing is impossible — features pass their own class constructors.
- Cause preservation: TRPCError carries the original domain error in `.cause` for client structured-error inspection.
- Note on tRPC v11 behavior: unmapped errors still surface as `TRPCError(code: INTERNAL_SERVER_ERROR)` because tRPC's `createCaller` wraps all procedure errors. Our middleware does not interfere — the original error is preserved as `.cause` for inspection.
## 5. Public-API surface
### 5.1 ./ui subpath added per feature
auth: `./ui` subpath added to package.json exports; `src/ui/index.ts` placeholder created (auth has no query builders — all procedures are mutations).
blog: `./ui` subpath added to package.json exports; `src/ui/index.ts` created re-exporting articleBySlugQuery and listArticlesQuery from ./query.
marketing-pages: `./ui` subpath added to package.json exports; `src/ui/index.ts` created re-exporting pageBySlugQuery and siteSettingsQuery from ./query.
navigation: `./ui` subpath added to package.json exports; `src/ui/index.ts` created re-exporting headerQuery from ./query (moved from feature root index).
media: `./ui` subpath added to package.json exports; `src/ui/index.ts` placeholder created (media has no query builders today — no existing UI to migrate).
### 5.2 Feature root index.ts cleanup
auth: root `src/index.ts` now exports all use-case schemas (signInInputSchema, signInOutputSchema, signUpInputSchema, signUpOutputSchema, signOutInputSchema) and types (SignInInput/Output, SignUpInput/Output, SignOutInput, ISignInUseCase, ISignUpUseCase, ISignOutUseCase) plus controller type aliases.
blog: root `src/index.ts` removed articleBySlugQuery/listArticlesQuery re-exports (moved to ./ui); now exports getArticlesInputSchema/Output, createArticleInputSchema/Output, getArticleBySlugInputSchema/Output, all XInput/XOutput types, IUseCase aliases, and IController aliases.
marketing-pages: root `src/index.ts` removed pageBySlugQuery/siteSettingsQuery re-exports (moved to ./ui); now exports getPageBySlugInputSchema/Output, getSiteSettingsInputSchema/Output, all XInput/XOutput types, IUseCase aliases, and IController aliases.
navigation: root `src/index.ts` removed headerQuery re-export (moved to ./ui); now exports getHeaderInputSchema, getHeaderOutputSchema, GetHeaderInput, GetHeaderOutput, IGetHeaderUseCase, IGetHeaderController; also exports HeaderNotFoundError and InputParseError.
media: root `src/index.ts` kept existing Media type + MediaNotFoundError + InputParseError + MediaRouter re-exports; added getMediaInputSchema/Output, listMediaInputSchema/Output, deleteMediaInputSchema and all XInput/XOutput types; added IUseCase/IController type aliases. No query builders to remove (media had no UI exports).
## 6. Test additions
### 6.1 R25 — output-validation tests (use case)
auth: signIn and signUp each have 2 new R25 tests — one verifying that a malformed service response throws (Zod parse error), one verifying the output schema parses a valid shape. signOut is void — no R25 test.
blog: getArticles, createArticle, getArticleBySlug each have 2 new R25 tests — one verifying that a repository returning a malformed object throws ZodError (instanceof), one verifying the output schema parses a valid shape.
marketing-pages: getPageBySlug has 1 R25 test verifying that a repository returning a malformed page object throws ZodError (instanceof). getSiteSettings has 1 R25 test using an inline malformed repository mock that returns `{ siteName: "" }` (fails min(1) constraint) to assert ZodError (instanceof).
navigation: getHeader has 1 R25 test using an inline malformed repository mock that returns `{ items: [{ label: "", href: "/", external: false }] }` (label fails min(1) constraint) to assert ZodError (instanceof).
media: getMedia has 2 R25 tests — one verifying getMediaOutputSchema parses a valid media object, one verifying that a repository returning a malformed object (missing filesize/filename/mimeType) throws ZodError (instanceof). listMedia has 2 R25 tests — same pattern using listMediaOutputSchema. deleteMedia is void — no R25 test.
### 6.2 R26 — router error-mapping tests
auth: 2 new R26 tests in router.test.ts — UNAUTHORIZED on missing user (AuthenticationError translation), BAD_REQUEST on Zod schema failure (schema validation at procedure boundary).
blog: 2 new R26 tests in router.test.ts — NOT_FOUND on articleBySlug with missing slug (ArticleNotFoundError translation via blogProcedure), BAD_REQUEST on articleBySlug with empty input ({} as { slug: string }) (schema validation at tRPC procedure boundary).
marketing-pages: 2 new R26 tests in router.test.ts — BAD_REQUEST on pageBySlug with empty input ({} as { slug: string }) (schema validation at tRPC procedure boundary); undefined return for missing slug confirmed (use case returns undefined rather than throwing PageNotFoundError, so NOT_FOUND mapping does not apply for this use case).
navigation: 2 new R26 tests in router.test.ts — BAD_REQUEST on header with extra fields (strict() z.object({}) rejects unknown keys via InputParseError); NOT_FOUND on header when a NullHeaderRepository (inline @injectable class) causes HeaderNotFoundError (container rebound inline for the test).
media: 4 new R26 tests in router.test.ts — NOT_FOUND on getMedia with nonexistent id (MediaNotFoundError translation via mediaProcedure); BAD_REQUEST on getMedia with empty input ({} as { id: string }) (schema validation at tRPC procedure boundary); NOT_FOUND on deleteMedia with nonexistent id; NOT_FOUND via NullMediaRepository inline rebind confirming mediaProcedure error map applies.
### 6.3 R27/R28 — presenter shape tests
- auth (Task 3): sign-in and sign-up controllers reshape `{ session, cookie }``cookie`. Controller tests assert cookie shape (`result.name`, `result.value`, etc.) — not the full use-case output — proving the presenter ran. Same assertions in `tests/sign-in-flow.feature.test.ts`. sign-out controller has no presenter (void return), so no view-shape test applies.
## 7. Open issues / deferred decisions
- 2026-05-06 / R6 fix-up (commit `9663c82`): Plan 8 left every feature's
domain error class with a constructor that did NOT set `this.name`.
Plan 9 spec R6 explicitly requires it. Caught by spec reviewer during
Task 6 (navigation). Fixed across all 10 error files (auth × 4, blog
× 2, marketing-pages × 2, navigation × 2, media × 2). Functionally a
no-op (defineErrorMiddleware uses `instanceof`), but ensures correct
serialization, stack-trace labels, and JSON inspectability.
### Plan 9 final verification (Task 8, 2026-05-06)
- pnpm typecheck / lint / test / turbo boundaries / build: all green after fixing 2 Plan 9 regressions in app callers (see stragglers below).
- Total tests: 360 across 15 test suites. Net delta vs pre-Plan-9 baseline of 325 (post-Plan-8): +35 tests, +11% (Plan 9 added R25 output-validation + R26 router error-mapping + R27/R28 presenter shape tests across all 5 features).
- Spec §8 acceptance criteria sweep:
- All non-test use cases export xInputSchema. ✓
- Non-void use cases also export xOutputSchema (void exempt: sign-out, delete-media). ✓
- All non-void controllers have `function presenter`; void controllers (sign-out, delete-media) return Promise<void>. ✓
- 5 procedures.ts files (one per feature). ✓
- 5 ./ui subpath entries in package.json. ✓
- All error classes set this.name (R6 satisfied after `9663c82`). ✓
- R26 router error-mapping tests pass for all 5 features (46 tests each). ✓
- Stragglers fixed in this task (Plan 9 regressions, NOT pre-existing):
- `apps/web-tanstack/src/routes/index.tsx``queryOptions()``queryOptions({})` for siteSettings and header (both procedures now require `{}` input after Plan 9 added `.input(z.object({}).strict())`).
- `apps/web-next/src/app/page.tsx``caller.marketingPages.siteSettings()``caller.marketingPages.siteSettings({})` and `caller.navigation.header()``caller.navigation.header({})` (same root cause).
- Pre-existing items (NOT introduced by Plan 9): none.
---
## Doc update checklist (deferred — combined with paused Plan 8 doc pass)
After Plan 9 lands, the still-pending Plan 8 doc-update items resume
and now also pick up Plan 9 rules. Each item below covers BOTH plans'
material so we touch every file once.
- [ ] `CLAUDE.md` — Key Conventions: append schema-in-use-case rule, presenter rule, controller `unknown` input rule, `./ui` subpath rule, schema-export-from-root rule
- [ ] `AGENTS.md` (root) — Per-Package Conventions: document `./ui` subpath, schema reachability from feature root, factory-bound use cases / controllers (carry-over from Plan 8)
- [ ] `docs/guides/adding-a-feature.md` — restructure to use the Plan-9 use-case template (with input + output schemas + parse), the Plan-9 controller template (with presenter), and the new `procedures.ts` step (per-feature error map)
- [ ] `docs/guides/tdd-workflow.md` — update mock decision tree + worked example to factory injection (Plan 8) + R25 output-validation test pattern (Plan 9) + R26 router-error-mapping test pattern (Plan 9)
- [ ] `docs/guides/testing-strategy.md` — Mocking section: direct factory injection (Plan 8); R25/R26/R27/R28 test obligations (Plan 9)
- [ ] `docs/architecture/vertical-feature-spec.md` — §6 file shape: schemas + presenter + procedures.ts; §10 testing: R25/R26/R27/R28
- [ ] `docs/architecture/overview.md` — data-flow box: add input schema, output schema, presenter, error-middleware lanes
- [ ] `docs/architecture/dependency-flow.md` — verify (no expected change beyond a `./ui` mention)
- [ ] `docs/decisions/adr-012-lazar-conformance.md` — append note that input/output unification + presenter + error middleware land in ADR-013
- [ ] `docs/decisions/adr-013-input-output-unification.md` — created in this plan's final task (Task 9); link from prior ADRs
- [ ] Per-feature `AGENTS.md` (auth/blog/media/marketing-pages/navigation) — Plan 8 file paths + Plan 9 schema/presenter/procedures patterns
- [ ] `packages/core-testing/AGENTS.md` — note R25/R26 test patterns
- [ ] `packages/core-shared/AGENTS.md` — document `defineErrorMiddleware` and the `t` re-export
- [ ] Plan 8 plan/spec — add a one-line annotation that some controller/router patterns shifted in Plan 9; link to this changelog
---
## Notes for the doc-update pass author
When updating external docs, apply these substitutions globally:
| Old reference | New reference |
|---|---|
| Use case takes `{ x }` directly typed | Use case takes `XInput` (`z.infer<typeof xInputSchema>`); schema lives in same file |
| Use case returns `Promise<Entity>` | Use case returns `Promise<XOutput>` validated by `xOutputSchema.parse(...)` |
| Controller imports `z` and defines local `inputSchema` | Controller imports `xInputSchema` from the use-case file |
| Controller input typed `Partial<z.infer<...>>` | Controller input typed `unknown` |
| Controller returns `Promise<Entity>` | Controller has `function presenter(value: XOutput)`; returns `Promise<ReturnType<typeof presenter>>` (or `Promise<void>` for void use cases) |
| Router uses `publicProcedure` from `core-shared/trpc/init` | Router uses feature's `xProcedure` from `./procedures` |
| Router redefines input schema as `z.object({...})` in `.input(...)` | Router uses `.input(xInputSchema)` imported from the use-case file |
| Domain error reaches the wire as a generic `TRPCError({ code: "INTERNAL_SERVER_ERROR" })` | Domain error mapped to specific code by `defineErrorMiddleware` in `procedures.ts` |
| Frontend imports `articleBySlugQuery` from `@repo/blog` | Frontend imports from `@repo/blog/ui` |