docs(adr): rename ADR-012 — drop Lazar; update title + content + cross-refs
- Rename docs/decisions/adr-012-lazar-conformance.md → adr-012-feature-conventions.md - Strip "Lazar", "Plan 8/9/10/11", "refactor-logs" refs from all ADRs, architecture docs, HTML explainers, and feature/core AGENTS.md files - Update all incoming links in docs/, packages/*/AGENTS.md, HTML explainers Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,14 +2,11 @@
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-05-06
|
||||
**Supersedes:** none — extends ADR-008 (per-feature DI), ADR-011 (TDD foundation), ADR-012 (Lazar conformance)
|
||||
**Spec:** docs/superpowers/specs/2026-05-06-input-output-unification-design.md
|
||||
**Plan:** docs/superpowers/plans/2026-05-06-plan-9-io-unification.md
|
||||
**Refactor log:** docs/superpowers/refactor-logs/2026-05-06-input-output-unification.md
|
||||
**Supersedes:** none — extends ADR-008 (per-feature DI), ADR-011 (TDD foundation), ADR-012 (feature conventions)
|
||||
|
||||
## Context
|
||||
|
||||
Plan 8 (ADR-012) established factory-function use cases and one-controller-
|
||||
ADR-012 established factory-function use cases and one-controller-
|
||||
per-use-case. But the input contract was still defined three times — once
|
||||
in the tRPC procedure's `.input(z.object({...}))`, once in the controller's
|
||||
local `const inputSchema`, and once implicitly in the use case's TypeScript
|
||||
@@ -44,7 +41,7 @@ spec:
|
||||
failure, then call the use case and pass the result through a
|
||||
top-level `function presenter(value: XOutput)` defined in the same
|
||||
file. The controller's return type is `Promise<ReturnType<typeof
|
||||
presenter>>`. Identity presenters are permitted and expected for
|
||||
presenter>>`. Identity presenters are permitted and expected for
|
||||
pass-through cases — the function form must always exist (R11) so
|
||||
adding a transform is a one-line edit. Void-output controllers
|
||||
(e.g., `signOutController`, `deleteMediaController`) skip the
|
||||
@@ -53,7 +50,7 @@ spec:
|
||||
3. **Feature-scoped error→TRPCError middleware.** Each feature's
|
||||
`integrations/api/procedures.ts` exports an `xProcedure` built from
|
||||
`t.procedure.use(defineErrorMiddleware([[ErrorCtor, "TRPC_CODE"],
|
||||
...]))`. The factory `defineErrorMiddleware` lives in
|
||||
...]))`. The factory `defineErrorMiddleware` lives in
|
||||
`core-shared/trpc/`; it discriminates by `instanceof` and preserves
|
||||
the original error as `TRPCError.cause`. **`core-shared` never
|
||||
enumerates feature-specific error classes** — each feature passes its
|
||||
@@ -87,7 +84,7 @@ spec:
|
||||
- **Clean public surface.** Feature root packages no longer pretend to
|
||||
be UI packages; apps make explicit choices about what they need.
|
||||
- **Frontend gets schemas for free.** Forms can `import { signInInputSchema
|
||||
} from "@repo/auth"` and feed it into `react-hook-form` + `zodResolver`
|
||||
} from "@repo/auth"` and feed it into `react-hook-form` + `zodResolver`
|
||||
with the same constraints the backend enforces.
|
||||
|
||||
### Negative
|
||||
@@ -105,18 +102,18 @@ spec:
|
||||
controller is invoked from non-tRPC entry points.
|
||||
- **Apps with existing imports may need updating** — `articleBySlugQuery`,
|
||||
`pageBySlugQuery`, etc. now live behind `@repo/<feature>/ui`.
|
||||
(At Plan 9 land time, no apps consume these yet, so the cost is
|
||||
(At the time of this ADR, no apps consume these yet, so the cost is
|
||||
forward-only.)
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Keep schemas in controllers (Lazar's reference pattern).** Lazar
|
||||
has only one validation layer (server actions skip `.input()`), so
|
||||
one schema is sufficient. Our entry point is tRPC, which insists on
|
||||
a schema for type inference — putting the canonical schema in the
|
||||
controller and exporting it for the router was considered. Rejected
|
||||
because the use case is the contract owner; schemas describe the
|
||||
*operation*, not the *transport*.
|
||||
- **Keep schemas in controllers.** The reference pattern has only one
|
||||
validation layer (server actions skip `.input()`), so one schema is
|
||||
sufficient. Our entry point is tRPC, which insists on a schema for
|
||||
type inference — putting the canonical schema in the controller and
|
||||
exporting it for the router was considered. Rejected because the use
|
||||
case is the contract owner; schemas describe the _operation_, not the
|
||||
_transport_.
|
||||
|
||||
- **Centralized error-name → code map in `core-shared`.** Considered
|
||||
using `error.name` discrimination with a small global registry.
|
||||
@@ -132,34 +129,29 @@ spec:
|
||||
return is trivial and the bug-catching value at runtime is real
|
||||
(Payload integrations have surprised us before).
|
||||
|
||||
- **Presenters only when reshaping.** Considered Lazar's actual rule
|
||||
(presenter only when there's a transform). Rejected (R11) because
|
||||
the discoverable hook for future shaping is worth the trivial
|
||||
identity-function boilerplate.
|
||||
- **Presenters only when reshaping.** Considered limiting presenters to
|
||||
cases with actual transforms. Rejected because the discoverable hook
|
||||
for future shaping is worth the trivial identity-function boilerplate.
|
||||
|
||||
- **Presenters in a separate `presenters/` folder.** Considered as a
|
||||
concession to "controllers = thin orchestration". Rejected because
|
||||
Lazar's reference co-locates the presenter with its consumer — the
|
||||
controller — keeping the contract visible in one file.
|
||||
co-locating the presenter with its controller keeps the contract
|
||||
visible in one file.
|
||||
|
||||
- **Shared `./schemas` subpath.** Considered exposing schemas only via
|
||||
a dedicated subpath instead of the feature root. Rejected because
|
||||
schemas ARE feature contracts — they belong with the other contracts
|
||||
(types, errors). Adding a fourth subpath felt like ceremony.
|
||||
|
||||
## Acceptance verification (Task 8, 2026-05-06)
|
||||
## Acceptance criteria
|
||||
|
||||
- All Plan 9 acceptance criteria from spec §8 met.
|
||||
- Tests: 360 total. Spec coverage: every R1–R28 represented.
|
||||
- Tests: 360 total. Coverage: every acceptance rule represented.
|
||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm turbo boundaries
|
||||
&& pnpm build` green.
|
||||
- Five feature-level R26 router-error-mapping tests demonstrate domain
|
||||
&& pnpm build` green.
|
||||
- Five feature-level router error-mapping tests demonstrate domain
|
||||
error → `TRPCError.code` translation works end-to-end.
|
||||
|
||||
## References
|
||||
|
||||
- Spec: `docs/superpowers/specs/2026-05-06-input-output-unification-design.md`
|
||||
- Plan: `docs/superpowers/plans/2026-05-06-plan-9-io-unification.md`
|
||||
- Refactor log: `docs/superpowers/refactor-logs/2026-05-06-input-output-unification.md`
|
||||
- Reference (Lazar's blog post + repo): https://github.com/nikolovlazar/nextjs-clean-architecture
|
||||
- Prior ADRs: ADR-008 (per-feature DI), ADR-011 (TDD foundation), ADR-012 (Lazar conformance)
|
||||
- Reference repo: https://github.com/nikolovlazar/nextjs-clean-architecture
|
||||
- Prior ADRs: ADR-008 (per-feature DI), ADR-011 (TDD foundation), ADR-012 (feature conventions)
|
||||
|
||||
Reference in New Issue
Block a user