chore(template): clean-slate template snapshot from bb4a0c7

Curated, product-agnostic snapshot of the post-story-04 tree: demo
content deleted, auth-only reference feature, web-next shell, all gates
green. Product-specific docs, ADRs 027-029, PRDs/epics/archive, editor
library traces, and product naming are curated out; generic template
repairs (coverage provider devDeps, root test:coverage script, live
lint fixes, root-only release-please) are kept. See TEMPLATE.md for
provenance, curation list, and usage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
This commit is contained in:
2026-07-12 20:40:54 +02:00
commit f77e6ea881
1062 changed files with 105156 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
# AGENTS.md — {{kebabCase name}}
Feature package scaffolded by `turbo gen feature`. Provides domain logic, repositories, use cases, controllers and the tRPC router for `{{pascalCase entity}}`.
## Overview
`@repo/{{kebabCase name}}` owns: the `{{pascalCase entity}}` domain model, feature-scoped errors, the `I{{pascalCase entity}}Repository` interface, one use case (`get{{pascalCase entity}}UseCase`), one controller, a real Payload-backed repository (stub body — fill in once the Payload collection is added), an in-memory mock repository, and the tRPC `{{camelCase name}}Router`.
## Layer responsibilities
| Layer | Key files |
|---|---|
| **entities/models** | `{{kebabCase entity}}.ts` — `{{pascalCase entity}}` Zod schema + type |
| **entities/errors** | `{{kebabCase entity}}.ts` ({{pascalCase entity}}NotFoundError), `common.ts` (InputParseError) |
| **application/use-cases** | `get-{{kebabCase entity}}.use-case.ts` — factory function + exported schemas |
| **application/repositories** | `{{kebabCase entity}}.repository.interface.ts` — `I{{pascalCase entity}}Repository` |
| **infrastructure/repositories** | `{{kebabCase entity}}.repository.ts` (real Payload-backed; stub body), `{{kebabCase entity}}.repository.mock.ts` (in-memory) |
| **interface-adapters/controllers** | `get-{{kebabCase entity}}.controller.ts` — one file per use case |
| **di** | `symbols.ts`, `module.ts`, `container.ts`, `bind-production.ts`, `bind-dev-seed.ts` |
| **integrations/api** | `procedures.ts` ({{camelCase name}}Procedure), `router.ts` ({{camelCase name}}Router) |
## Public exports
| Subpath | Contents |
|---|---|
| `.` | `{{pascalCase entity}}` type; `{{pascalCase entity}}NotFoundError`, `InputParseError`; `get{{pascalCase entity}}InputSchema`, `get{{pascalCase entity}}OutputSchema`, `Get{{pascalCase entity}}Input`, `Get{{pascalCase entity}}Output`, `IGet{{pascalCase entity}}UseCase`; `IGet{{pascalCase entity}}Controller` type alias; `{{pascalCase name}}Router` type |
| `./api` | `{{camelCase name}}Router` (tRPC router) |
| `./di/bind-production` | `bindProduction{{pascalCase name}}(config, tracer, logger)` |
| `./di/bind-dev-seed` | `bindDevSeed{{pascalCase name}}(tracer, logger)` |
| `./ui` | (reserved — empty barrel) |
## Tests
```bash
pnpm test --filter @repo/{{kebabCase name}}
```
## What it must NOT import
- Any other feature package
- Any app package
- `@repo/core-api`, `@repo/core-cms`, `@repo/core-trpc`, `@repo/core-ui` directly; only `@repo/core-shared`

View File

@@ -0,0 +1,9 @@
# Changelog — @repo/{{kebabCase name}}
All notable changes to the `{{kebabCase name}}` feature package. Maintained by [release-please](https://github.com/googleapis/release-please) on merges to `main`. See [ADR-021](../../docs/decisions/adr-021-versioning-and-changelog.md) and [`docs/guides/releasing.md`](../../docs/guides/releasing.md).
## 0.1.0 (Initial)
### Initial baseline
The `{{kebabCase name}}` feature scaffolded via `pnpm turbo gen feature {{kebabCase name}}`. Tracked at v0.1.0 in the release-please manifest; future entries appear above this section as release-please assembles them from conventional commits scoped to `packages/{{kebabCase name}}/**`.

View File

@@ -0,0 +1,3 @@
import baseConfig from "@repo/core-eslint/base";
export default baseConfig;

View File

@@ -0,0 +1,36 @@
{
"name": "@repo/{{kebabCase name}}",
"private": true,
"version": "0.1.0",
"type": "module",
"exports": {
".": "./src/index.ts",
"./ui": "./src/ui/index.ts",
"./cms": "./src/integrations/cms/index.ts",
"./api": "./src/integrations/api/router.ts",
"./di/bind-production": "./src/di/bind-production.ts",
"./di/bind-dev-seed": "./src/di/bind-dev-seed.ts"
},
"scripts": {
"build": "tsc --noEmit",
"lint": "eslint .",
"test": "vitest run --passWithNoTests",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@repo/core-shared": "workspace:*",
"@trpc/server": "^11.0.0",
"inversify": "^6.2.0",
"payload": "^3.14.0",
"reflect-metadata": "^0.2.2",
"zod": "^3.24.0"
},
"devDependencies": {
"@repo/core-eslint": "workspace:*",
"@repo/core-testing": "workspace:*",
"@repo/core-typescript": "workspace:*",
"@types/node": "^22.0.0",
"@vitest/coverage-v8": "^3.2.4",
"vitest": "^3.1.0"
}
}

View File

@@ -0,0 +1,55 @@
import { it, expect, beforeEach, describe } from "vitest";
import { defineContractSuite } from "@repo/core-testing/contract";
import type { I{{pascalCase entity}}Repository } from "../application/repositories/{{kebabCase entity}}.repository.interface";
import type { {{pascalCase entity}} } from "../entities/models/{{kebabCase entity}}";
/**
* Known fixtures every implementation's `buildSubject` must pre-seed.
* Exported so test files can pass them to `Mock{{pascalCase entity}}Repository` or the
* Payload stub without duplicating definitions.
*/
export const CONTRACT_{{constantCase entity}}_SEED: ReadonlyArray<readonly [string, {{pascalCase entity}}]> = [
["seed-1", { id: "seed-1", name: "Seed One" }],
["seed-2", { id: "seed-2", name: "Seed Two" }],
];
/**
* Contract for I{{pascalCase entity}}Repository.
*
* The interface exposes only `get{{pascalCase entity}}(id)`. The contract verifies
* found vs missing behaviour and span emission.
*/
export const {{camelCase entity}}RepositoryContract = defineContractSuite<I{{pascalCase entity}}Repository>(
"I{{pascalCase entity}}Repository",
({ buildSubject, getTracer }) => {
let repo: I{{pascalCase entity}}Repository;
beforeEach(async () => {
repo = await buildSubject();
});
it("get{{pascalCase entity}} returns the seeded {{camelCase entity}} when id exists", async () => {
const result = await repo.get{{pascalCase entity}}("seed-1");
expect(result).not.toBeNull();
expect(result?.id).toBe("seed-1");
expect(typeof result?.name).toBe("string");
});
it("get{{pascalCase entity}} returns null for an unknown id", async () => {
const result = await repo.get{{pascalCase entity}}("does-not-exist");
expect(result).toBeNull();
});
describe("span emission", () => {
it("get{{pascalCase entity}} emits span '{{camelCase entity}}.get{{pascalCase entity}}' with op=repository", async () => {
if (!getTracer) return;
const tracer = getTracer();
tracer.reset();
await repo.get{{pascalCase entity}}("seed-1");
const span = tracer.findSpan("{{camelCase entity}}.get{{pascalCase entity}}");
expect(span).toBeDefined();
expect(span!.op).toBe("repository");
});
});
},
);

View File

@@ -0,0 +1,4 @@
// Phase-1 stub. Replace with `defineFactory<{{pascalCase entity}}>` once the
// entity shape stabilises. See packages/auth/src/__factories__/ for an
// example.
export {};

View File

@@ -0,0 +1,3 @@
// Phase-1 placeholder. Add faker-driven factories here once the entity
// shape stabilises. See packages/blog/src/__factories__/ for the pattern.
export {};

View File

@@ -0,0 +1,16 @@
import type { {{pascalCase entity}} } from "../entities/models/{{kebabCase entity}}";
/**
* Realistic dev seed for `bindDevSeed{{pascalCase name}}`.
*
* Phase-1: returns a small hand-rolled Map. Replace with a faker-driven
* `defineFactory` (see `packages/blog/src/__factories__/`) once the entity
* shape stabilises.
*/
export function buildDev{{pascalCase entity}}Map(): Map<string, {{pascalCase entity}}> {
return new Map<string, {{pascalCase entity}}>([
["dev-1", { id: "dev-1", name: "Dev One" }],
["dev-2", { id: "dev-2", name: "Dev Two" }],
["dev-3", { id: "dev-3", name: "Dev Three" }],
]);
}

View File

@@ -0,0 +1,5 @@
import type { {{pascalCase entity}} } from "../../entities/models/{{kebabCase entity}}";
export interface I{{pascalCase entity}}Repository {
get{{pascalCase entity}}(id: string): Promise<{{pascalCase entity}} | null>;
}

View File

@@ -0,0 +1,31 @@
import { describe, expect, it } from "vitest";
import { ZodError } from "zod";
import { get{{pascalCase entity}}UseCase } from "@/application/use-cases/get-{{kebabCase entity}}.use-case";
import { Mock{{pascalCase entity}}Repository } from "@/infrastructure/repositories/{{kebabCase entity}}.repository.mock";
import { {{pascalCase entity}}NotFoundError } from "@/entities/errors/{{kebabCase entity}}";
describe("get{{pascalCase entity}}UseCase", () => {
it("returns the seeded {{camelCase entity}} by id", async () => {
const repo = new Mock{{pascalCase entity}}Repository();
const useCase = get{{pascalCase entity}}UseCase(repo);
const result = await useCase({ id: "seed-1" });
expect(result.id).toBe("seed-1");
expect(result.name).toBeTypeOf("string");
});
it("throws {{pascalCase entity}}NotFoundError when repository returns null", async () => {
const repo = new Mock{{pascalCase entity}}Repository(new Map());
const useCase = get{{pascalCase entity}}UseCase(repo);
await expect(useCase({ id: "missing" })).rejects.toBeInstanceOf(
{{pascalCase entity}}NotFoundError,
);
});
it("throws ZodError when repository returns malformed data", async () => {
const malformedRepo = {
get{{pascalCase entity}}: async () => ({ id: "", name: "x" }) as never,
};
const useCase = get{{pascalCase entity}}UseCase(malformedRepo);
await expect(useCase({ id: "anything" })).rejects.toBeInstanceOf(ZodError);
});
});

View File

@@ -0,0 +1,30 @@
import { z } from "zod";
import { {{pascalCase entity}}NotFoundError } from "../../entities/errors/{{kebabCase entity}}";
import { {{camelCase entity}}Schema } from "../../entities/models/{{kebabCase entity}}";
import type { I{{pascalCase entity}}Repository } from "../repositories/{{kebabCase entity}}.repository.interface";
// ── Input ────────────────────────────────────────────────────────────────
export const get{{pascalCase entity}}InputSchema = z
.object({
id: z.string().min(1),
})
.strict();
export type Get{{pascalCase entity}}Input = z.infer<typeof get{{pascalCase entity}}InputSchema>;
// ── Output ───────────────────────────────────────────────────────────────
export const get{{pascalCase entity}}OutputSchema = {{camelCase entity}}Schema;
export type Get{{pascalCase entity}}Output = z.infer<typeof get{{pascalCase entity}}OutputSchema>;
// ── Use case ─────────────────────────────────────────────────────────────
export type IGet{{pascalCase entity}}UseCase = ReturnType<typeof get{{pascalCase entity}}UseCase>;
export const get{{pascalCase entity}}UseCase =
({{camelCase entity}}Repository: I{{pascalCase entity}}Repository) =>
async (input: Get{{pascalCase entity}}Input): Promise<Get{{pascalCase entity}}Output> => {
const result = await {{camelCase entity}}Repository.get{{pascalCase entity}}(input.id);
if (!result) {
throw new {{pascalCase entity}}NotFoundError(`{{pascalCase entity}} not found: ${input.id}`);
}
return get{{pascalCase entity}}OutputSchema.parse(result);
};

View File

@@ -0,0 +1,56 @@
import "reflect-metadata";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { NoopTracer, NoopLogger } from "@repo/core-shared/instrumentation";
import { bindDevSeed{{pascalCase name}} } from "@/di/bind-dev-seed";
import { {{camelCase name}}Container } from "@/di/container";
import { {{constantCase name}}_SYMBOLS } from "@/di/symbols";
import { Mock{{pascalCase entity}}Repository } from "@/infrastructure/repositories/{{kebabCase entity}}.repository.mock";
import type { I{{pascalCase entity}}Repository } from "@/application/repositories/{{kebabCase entity}}.repository.interface";
const noop = { tracer: new NoopTracer(), logger: new NoopLogger() };
describe("bindDevSeed{{pascalCase name}}", () => {
beforeEach(() => {
if ({{camelCase name}}Container.isBound({{constantCase name}}_SYMBOLS.I{{pascalCase entity}}Repository)) {
{{camelCase name}}Container.unbind({{constantCase name}}_SYMBOLS.I{{pascalCase entity}}Repository);
}
{{camelCase name}}Container
.bind<I{{pascalCase entity}}Repository>({{constantCase name}}_SYMBOLS.I{{pascalCase entity}}Repository)
.to(Mock{{pascalCase entity}}Repository);
});
afterEach(() => {
if ({{camelCase name}}Container.isBound({{constantCase name}}_SYMBOLS.I{{pascalCase entity}}Repository)) {
{{camelCase name}}Container.unbind({{constantCase name}}_SYMBOLS.I{{pascalCase entity}}Repository);
}
{{camelCase name}}Container
.bind<I{{pascalCase entity}}Repository>({{constantCase name}}_SYMBOLS.I{{pascalCase entity}}Repository)
.to(Mock{{pascalCase entity}}Repository);
});
it("populates the repository with the dev seed", async () => {
await bindDevSeed{{pascalCase name}}(noop);
const repo = {{camelCase name}}Container.get<I{{pascalCase entity}}Repository>(
{{constantCase name}}_SYMBOLS.I{{pascalCase entity}}Repository,
);
const found = await repo.get{{pascalCase entity}}("dev-1");
expect(found).not.toBeNull();
expect(found?.id).toBe("dev-1");
});
it("is idempotent — calling twice rebuilds a fresh populated repo", async () => {
await bindDevSeed{{pascalCase name}}(noop);
const before = {{camelCase name}}Container.get<I{{pascalCase entity}}Repository>(
{{constantCase name}}_SYMBOLS.I{{pascalCase entity}}Repository,
);
await bindDevSeed{{pascalCase name}}(noop);
const after = {{camelCase name}}Container.get<I{{pascalCase entity}}Repository>(
{{constantCase name}}_SYMBOLS.I{{pascalCase entity}}Repository,
);
expect(after).not.toBe(before);
});
});

View File

@@ -0,0 +1,100 @@
import {
withSpan,
withCapture,
INSTRUMENTATION_SYMBOLS,
type ITracer,
type ILogger,
} from "@repo/core-shared/instrumentation";
import type { BindContext } from "@repo/core-shared/di";
import {
assertFeatureConformance,
wireUseCase,
} from "@repo/core-shared/conformance";
import { {{camelCase name}}Manifest } from "../feature.manifest";
import { {{camelCase name}}Container } from "./container";
import { {{constantCase name}}_SYMBOLS } from "./symbols";
import { Mock{{pascalCase entity}}Repository } from "../infrastructure/repositories/{{kebabCase entity}}.repository.mock";
import { buildDev{{pascalCase entity}}Map } from "../__seeds__/dev";
import { get{{pascalCase entity}}UseCase } from "../application/use-cases/get-{{kebabCase entity}}.use-case";
import { get{{pascalCase entity}}Controller } from "../interface-adapters/controllers/get-{{kebabCase entity}}.controller";
import type { I{{pascalCase entity}}Repository } from "../application/repositories/{{kebabCase entity}}.repository.interface";
/**
* Replace the default mock with a populated one for dev mode + storybook.
*
* Mutually exclusive with `bindProduction{{pascalCase name}}(ctx)`.
* Tests must NOT call this — they construct `new Mock{{pascalCase entity}}Repository()`
* directly and seed via factories per-test.
*
* Idempotent: safe to call multiple times; each call rebuilds a fresh
* populated repo and rebinds the symbol.
*/
export async function bindDevSeed{{pascalCase name}}(ctx: BindContext): Promise<void> {
const { tracer, logger, bus, queue, realtime, realtimeRegistry } = ctx;
// Bind shared instrumentation into feature container
if ({{camelCase name}}Container.isBound(INSTRUMENTATION_SYMBOLS.TRACER)) {
{{camelCase name}}Container.unbind(INSTRUMENTATION_SYMBOLS.TRACER);
}
if ({{camelCase name}}Container.isBound(INSTRUMENTATION_SYMBOLS.LOGGER)) {
{{camelCase name}}Container.unbind(INSTRUMENTATION_SYMBOLS.LOGGER);
}
{{camelCase name}}Container.bind<ITracer>(INSTRUMENTATION_SYMBOLS.TRACER).toConstantValue(tracer);
{{camelCase name}}Container.bind<ILogger>(INSTRUMENTATION_SYMBOLS.LOGGER).toConstantValue(logger);
if ({{camelCase name}}Container.isBound({{constantCase name}}_SYMBOLS.I{{pascalCase entity}}Repository)) {
{{camelCase name}}Container.unbind({{constantCase name}}_SYMBOLS.I{{pascalCase entity}}Repository);
}
const repo = new Mock{{pascalCase entity}}Repository(buildDev{{pascalCase entity}}Map(), tracer, logger);
{{camelCase name}}Container
.bind<I{{pascalCase entity}}Repository>({{constantCase name}}_SYMBOLS.I{{pascalCase entity}}Repository)
.toConstantValue(repo);
// Use case
const wrappedGet{{pascalCase entity}} = wireUseCase({
container: {{camelCase name}}Container,
symbol: {{constantCase name}}_SYMBOLS.IGet{{pascalCase entity}}UseCase,
factory: get{{pascalCase entity}}UseCase,
deps: [repo],
feature: "{{kebabCase name}}",
layer: "use-case",
name: "get{{pascalCase entity}}",
tracer,
logger,
});
if ({{camelCase name}}Container.isBound({{constantCase name}}_SYMBOLS.IGet{{pascalCase entity}}Controller)) {
{{camelCase name}}Container.unbind({{constantCase name}}_SYMBOLS.IGet{{pascalCase entity}}Controller);
}
{{camelCase name}}Container
.bind({{constantCase name}}_SYMBOLS.IGet{{pascalCase entity}}Controller)
.toConstantValue(
withSpan(
tracer,
{ name: "{{camelCase name}}.get{{pascalCase entity}}", op: "controller" },
withCapture(
logger,
{ feature: "{{kebabCase name}}", layer: "controller", name: "{{camelCase name}}.get{{pascalCase entity}}" },
get{{pascalCase entity}}Controller(wrappedGet{{pascalCase entity}}),
),
),
);
// bus + queue are passed through; generated handlers consume them at the anchors below.
void bus;
void queue;
void realtime;
void realtimeRegistry;
// <gen:event-handlers>
// <gen:jobs>
// <gen:realtime-handlers>
// Boot-time conformance check (dev-seed mode).
assertFeatureConformance(
{{camelCase name}}Container,
{{camelCase name}}Manifest,
{
get{{pascalCase entity}}: {{constantCase name}}_SYMBOLS.IGet{{pascalCase entity}}UseCase,
},
ctx,
);
}

View File

@@ -0,0 +1,91 @@
import {
withSpan,
withCapture,
INSTRUMENTATION_SYMBOLS,
type ITracer,
type ILogger,
} from "@repo/core-shared/instrumentation";
import type { BindProductionContext } from "@repo/core-shared/di";
import {
assertFeatureConformance,
wireUseCase,
} from "@repo/core-shared/conformance";
import { {{camelCase name}}Container } from "./container";
import { {{constantCase name}}_SYMBOLS } from "./symbols";
import { {{camelCase name}}Manifest } from "../feature.manifest";
import { {{pascalCase entity}}Repository } from "../infrastructure/repositories/{{kebabCase entity}}.repository";
import { get{{pascalCase entity}}UseCase } from "../application/use-cases/get-{{kebabCase entity}}.use-case";
import { get{{pascalCase entity}}Controller } from "../interface-adapters/controllers/get-{{kebabCase entity}}.controller";
export function bindProduction{{pascalCase name}}(ctx: BindProductionContext): void {
const { config, tracer, logger, bus, queue, realtime, realtimeRegistry } = ctx;
// Bind shared instrumentation into feature container
if ({{camelCase name}}Container.isBound(INSTRUMENTATION_SYMBOLS.TRACER)) {
{{camelCase name}}Container.unbind(INSTRUMENTATION_SYMBOLS.TRACER);
}
if ({{camelCase name}}Container.isBound(INSTRUMENTATION_SYMBOLS.LOGGER)) {
{{camelCase name}}Container.unbind(INSTRUMENTATION_SYMBOLS.LOGGER);
}
{{camelCase name}}Container.bind<ITracer>(INSTRUMENTATION_SYMBOLS.TRACER).toConstantValue(tracer);
{{camelCase name}}Container.bind<ILogger>(INSTRUMENTATION_SYMBOLS.LOGGER).toConstantValue(logger);
// Real repository
if ({{camelCase name}}Container.isBound({{constantCase name}}_SYMBOLS.I{{pascalCase entity}}Repository)) {
{{camelCase name}}Container.unbind({{constantCase name}}_SYMBOLS.I{{pascalCase entity}}Repository);
}
const repo = new {{pascalCase entity}}Repository(config, tracer, logger);
{{camelCase name}}Container
.bind({{constantCase name}}_SYMBOLS.I{{pascalCase entity}}Repository)
.toConstantValue(repo);
// Use case
const wrappedGet{{pascalCase entity}} = wireUseCase({
container: {{camelCase name}}Container,
symbol: {{constantCase name}}_SYMBOLS.IGet{{pascalCase entity}}UseCase,
factory: get{{pascalCase entity}}UseCase,
deps: [repo],
feature: "{{kebabCase name}}",
layer: "use-case",
name: "get{{pascalCase entity}}",
tracer,
logger,
});
// Controller — wrapped with span at bind time
if ({{camelCase name}}Container.isBound({{constantCase name}}_SYMBOLS.IGet{{pascalCase entity}}Controller)) {
{{camelCase name}}Container.unbind({{constantCase name}}_SYMBOLS.IGet{{pascalCase entity}}Controller);
}
{{camelCase name}}Container
.bind({{constantCase name}}_SYMBOLS.IGet{{pascalCase entity}}Controller)
.toConstantValue(
withSpan(
tracer,
{ name: "{{camelCase name}}.get{{pascalCase entity}}", op: "controller" },
withCapture(
logger,
{ feature: "{{kebabCase name}}", layer: "controller", name: "{{camelCase name}}.get{{pascalCase entity}}" },
get{{pascalCase entity}}Controller(wrappedGet{{pascalCase entity}}),
),
),
);
// bus + queue are passed through; generated handlers consume them at the anchors below.
void bus;
void queue;
void realtime;
void realtimeRegistry;
// <gen:event-handlers>
// <gen:jobs>
// <gen:realtime-handlers>
// Boot-time conformance check: refuses to start if any use-case binding
// is missing a required brand (withSpan / withCapture / withAudit).
assertFeatureConformance(
{{camelCase name}}Container,
{{camelCase name}}Manifest,
{
get{{pascalCase entity}}: {{constantCase name}}_SYMBOLS.IGet{{pascalCase entity}}UseCase,
},
ctx,
);
}

View File

@@ -0,0 +1,40 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { {{camelCase name}}Container } from "./container";
import { {{constantCase name}}_SYMBOLS } from "./symbols";
import { {{pascalCase name}}Module } from "./module";
import { Mock{{pascalCase entity}}Repository } from "@/infrastructure/repositories/{{kebabCase entity}}.repository.mock";
import type { I{{pascalCase entity}}Repository } from "@/application/repositories/{{kebabCase entity}}.repository.interface";
import type { IGet{{pascalCase entity}}UseCase } from "@/application/use-cases/get-{{kebabCase entity}}.use-case";
import type { IGet{{pascalCase entity}}Controller } from "@/interface-adapters/controllers/get-{{kebabCase entity}}.controller";
describe("{{camelCase name}}Container", () => {
beforeEach(() => {
{{camelCase name}}Container.unbindAll();
{{camelCase name}}Container.load({{pascalCase name}}Module);
});
afterEach(() => {
{{camelCase name}}Container.unbindAll();
});
it("resolves I{{pascalCase entity}}Repository to Mock{{pascalCase entity}}Repository", () => {
const repo = {{camelCase name}}Container.get<I{{pascalCase entity}}Repository>(
{{constantCase name}}_SYMBOLS.I{{pascalCase entity}}Repository,
);
expect(repo).toBeInstanceOf(Mock{{pascalCase entity}}Repository);
});
it("resolves IGet{{pascalCase entity}}UseCase as a function", () => {
const useCase = {{camelCase name}}Container.get<IGet{{pascalCase entity}}UseCase>(
{{constantCase name}}_SYMBOLS.IGet{{pascalCase entity}}UseCase,
);
expect(typeof useCase).toBe("function");
});
it("resolves IGet{{pascalCase entity}}Controller as a function", () => {
const controller = {{camelCase name}}Container.get<IGet{{pascalCase entity}}Controller>(
{{constantCase name}}_SYMBOLS.IGet{{pascalCase entity}}Controller,
);
expect(typeof controller).toBe("function");
});
});

View File

@@ -0,0 +1,6 @@
import "reflect-metadata";
import { Container } from "inversify";
import { {{pascalCase name}}Module } from "./module";
export const {{camelCase name}}Container = new Container({ defaultScope: "Singleton" });
{{camelCase name}}Container.load({{pascalCase name}}Module);

View File

@@ -0,0 +1,39 @@
import { ContainerModule, type interfaces } from "inversify";
import type { I{{pascalCase entity}}Repository } from "../application/repositories/{{kebabCase entity}}.repository.interface";
import { Mock{{pascalCase entity}}Repository } from "../infrastructure/repositories/{{kebabCase entity}}.repository.mock";
import {
get{{pascalCase entity}}UseCase,
type IGet{{pascalCase entity}}UseCase,
} from "../application/use-cases/get-{{kebabCase entity}}.use-case";
import {
get{{pascalCase entity}}Controller,
type IGet{{pascalCase entity}}Controller,
} from "../interface-adapters/controllers/get-{{kebabCase entity}}.controller";
import { {{constantCase name}}_SYMBOLS } from "./symbols";
export const {{pascalCase name}}Module = new ContainerModule((bind: interfaces.Bind) => {
bind<I{{pascalCase entity}}Repository>({{constantCase name}}_SYMBOLS.I{{pascalCase entity}}Repository).to(
Mock{{pascalCase entity}}Repository,
);
bind<IGet{{pascalCase entity}}UseCase>(
{{constantCase name}}_SYMBOLS.IGet{{pascalCase entity}}UseCase,
).toDynamicValue((ctx) =>
get{{pascalCase entity}}UseCase(
ctx.container.get<I{{pascalCase entity}}Repository>(
{{constantCase name}}_SYMBOLS.I{{pascalCase entity}}Repository,
),
),
);
bind<IGet{{pascalCase entity}}Controller>(
{{constantCase name}}_SYMBOLS.IGet{{pascalCase entity}}Controller,
).toDynamicValue((ctx) =>
get{{pascalCase entity}}Controller(
ctx.container.get<IGet{{pascalCase entity}}UseCase>(
{{constantCase name}}_SYMBOLS.IGet{{pascalCase entity}}UseCase,
),
),
);
});

View File

@@ -0,0 +1,10 @@
export const {{constantCase name}}_SYMBOLS = {
I{{pascalCase entity}}Repository: Symbol.for("{{kebabCase name}}:I{{pascalCase entity}}Repository"),
// Use cases
IGet{{pascalCase entity}}UseCase: Symbol.for("{{kebabCase name}}:IGet{{pascalCase entity}}UseCase"),
// Controllers
IGet{{pascalCase entity}}Controller: Symbol.for("{{kebabCase name}}:IGet{{pascalCase entity}}Controller"),
// <gen:event-handler-symbols>
// <gen:job-symbols>
// <gen:realtime-handler-symbols>
} as const;

View File

@@ -0,0 +1,6 @@
export class InputParseError extends Error {
constructor(message: string, options?: ErrorOptions) {
super(message, options);
this.name = "InputParseError";
}
}

View File

@@ -0,0 +1,6 @@
export class {{pascalCase entity}}NotFoundError extends Error {
constructor(message = "{{pascalCase entity}} not found", options?: ErrorOptions) {
super(message, options);
this.name = "{{pascalCase entity}}NotFoundError";
}
}

View File

@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import { {{camelCase entity}}Schema } from "./{{kebabCase entity}}";
describe("{{camelCase entity}}Schema", () => {
it("accepts a valid {{camelCase entity}}", () => {
const result = {{camelCase entity}}Schema.parse({ id: "1", name: "Example" });
expect(result.id).toBe("1");
expect(result.name).toBe("Example");
});
it("rejects an empty id", () => {
expect(() => {{camelCase entity}}Schema.parse({ id: "", name: "ok" })).toThrow();
});
it("rejects an empty name", () => {
expect(() => {{camelCase entity}}Schema.parse({ id: "1", name: "" })).toThrow();
});
it("rejects a name over 128 chars", () => {
expect(() =>
{{camelCase entity}}Schema.parse({ id: "1", name: "x".repeat(129) }),
).toThrow();
});
});

View File

@@ -0,0 +1,8 @@
import { z } from "zod";
export const {{camelCase entity}}Schema = z.object({
id: z.string().min(1),
name: z.string().min(1).max(128),
});
export type {{pascalCase entity}} = z.infer<typeof {{camelCase entity}}Schema>;

View File

@@ -0,0 +1,45 @@
import { defineFeature } from "@repo/core-shared/conformance";
/**
* The {{camelCase name}} feature's conformance manifest. Drives binding-slot
* types in `di/bind-production.ts` and is read by ESLint, the boot
* assertion, and the CI drift gate.
*
* Conventions:
* - `mutates: true` for any use case that creates, updates, or deletes state
* - `audits` lists every audit event the use case emits (must match calls
* to `auditLog.record(...)` in the factory body — ESLint enforces this)
* - `publishes` / `consumes` cover cross-feature events through `IEventBus`
*/
export const {{camelCase name}}Manifest = defineFeature({
name: "{{kebabCase name}}",
requiredCores: [],
useCases: {
get{{pascalCase entity}}: {
mutates: false,
audits: [],
publishes: [],
consumes: [],
analyticsEvents: [],
},
},
realtimeChannels: [],
jobs: [],
// <gen:coverage>
// Coverage bands — single source of truth (ADR-020). Read by:
// - vitest.config.ts (test-time thresholds, L0)
// - assertFeatureConformance (boot-time, fails dev/prod on drift)
// - pnpm coverage:diff (cover-the-diff gate, L1)
// Edit here; the helper in vitest.config picks up the new numbers.
coverage: {
bands: {
baseline: { statements: 80, branches: 75, functions: 80, lines: 80 },
entities: { statements: 100, branches: 100, functions: 100, lines: 100 },
"use-cases": { statements: 100, branches: 95, functions: 100, lines: 100 },
controllers: { statements: 100, branches: 95, functions: 100, lines: 100 },
},
mutationTargets: ["entities", "use-cases"],
},
} as const);
export type {{pascalCase name}}Manifest = typeof {{camelCase name}}Manifest;

View File

@@ -0,0 +1,19 @@
export type { {{pascalCase entity}} } from "./entities/models/{{kebabCase entity}}";
export type { {{pascalCase name}}Router } from "./integrations/api/router";
export { {{pascalCase entity}}NotFoundError } from "./entities/errors/{{kebabCase entity}}";
export { InputParseError } from "./entities/errors/common";
// Use case schemas + types
export {
get{{pascalCase entity}}InputSchema,
get{{pascalCase entity}}OutputSchema,
type Get{{pascalCase entity}}Input,
type Get{{pascalCase entity}}Output,
type IGet{{pascalCase entity}}UseCase,
} from "./application/use-cases/get-{{kebabCase entity}}.use-case";
// Controller type aliases
export type { IGet{{pascalCase entity}}Controller } from "./interface-adapters/controllers/get-{{kebabCase entity}}.controller";
// <gen:events>
// <gen:realtime-channels>

View File

@@ -0,0 +1,15 @@
import { describe } from "vitest";
import { RecordingTracer } from "@repo/core-testing/instrumentation";
import { Mock{{pascalCase entity}}Repository } from "@/infrastructure/repositories/{{kebabCase entity}}.repository.mock";
import {
{{camelCase entity}}RepositoryContract,
CONTRACT_{{constantCase entity}}_SEED,
} from "@/__contracts__/{{kebabCase entity}}-repository.contract";
describe("Mock{{pascalCase entity}}Repository", () => {
const tracer = new RecordingTracer();
{{camelCase entity}}RepositoryContract.run(
() => new Mock{{pascalCase entity}}Repository(new Map(CONTRACT_{{constantCase entity}}_SEED), tracer),
{ tracer: () => tracer },
);
});

View File

@@ -0,0 +1,45 @@
import "reflect-metadata";
import { injectable } from "inversify";
import {
NoopTracer,
NoopLogger,
type ITracer,
type ILogger,
} from "@repo/core-shared/instrumentation";
import type { I{{pascalCase entity}}Repository } from "../../application/repositories/{{kebabCase entity}}.repository.interface";
import type { {{pascalCase entity}} } from "../../entities/models/{{kebabCase entity}}";
const DEFAULT_DATA = new Map<string, {{pascalCase entity}}>([
["seed-1", { id: "seed-1", name: "Seed One" }],
["seed-2", { id: "seed-2", name: "Seed Two" }],
]);
@injectable()
export class Mock{{pascalCase entity}}Repository implements I{{pascalCase entity}}Repository {
private readonly data: Map<string, {{pascalCase entity}}>;
private tracer: ITracer;
private logger: ILogger;
constructor(
initialData?: Map<string, {{pascalCase entity}}>,
tracer: ITracer = new NoopTracer(),
logger: ILogger = new NoopLogger(),
) {
this.data = initialData ?? new Map(DEFAULT_DATA);
this.tracer = tracer;
this.logger = logger;
void this.logger; // currently unused; reserved for future mock-thrown captures
}
async get{{pascalCase entity}}(id: string): Promise<{{pascalCase entity}} | null> {
return this.tracer.startSpan(
{ name: "{{camelCase entity}}.get{{pascalCase entity}}", op: "repository", attributes: {} },
async (span) => {
const found = this.data.get(id) ?? null;
span.setAttribute("found", found !== null);
return found;
},
);
}
}

View File

@@ -0,0 +1,19 @@
import { describe, it, expect } from "vitest";
import { RecordingTracer, RecordingLogger } from "@repo/core-testing/instrumentation";
import { Mock{{pascalCase entity}}Repository } from "@/infrastructure/repositories/{{kebabCase entity}}.repository.mock";
// Mock repo also wraps in spans.
describe("Mock{{pascalCase entity}}Repository emits spans", () => {
it("get{{pascalCase entity}} emits one span with op='repository'", async () => {
const tracer = new RecordingTracer();
const logger = new RecordingLogger();
const repo = new Mock{{pascalCase entity}}Repository(undefined, tracer, logger);
await repo.get{{pascalCase entity}}("seed-1");
expect(tracer.spans).toHaveLength(1);
expect(tracer.spans[0]).toMatchObject({
name: "{{camelCase entity}}.get{{pascalCase entity}}",
op: "repository",
});
expect(typeof tracer.spans[0]!.attributes.found).toBe("boolean");
});
});

View File

@@ -0,0 +1,34 @@
import { describe, it, expect } from "vitest";
import { RecordingTracer, RecordingLogger } from "@repo/core-testing/instrumentation";
import { stubPayloadConfig } from "@repo/core-testing/payload/stub-config";
import { {{pascalCase entity}}Repository } from "@/infrastructure/repositories/{{kebabCase entity}}.repository";
// Phase-1 scaffold: the real repository returns null until the Payload
// collection is wired. These tests pin the span shape and the stub return
// value so that callers (use case + DI tests) keep working when the body is
// later replaced with a real `payload.find()` call.
describe("{{pascalCase entity}}Repository (Phase-1 stub)", () => {
it("returns null and emits a span with op='repository'", async () => {
const tracer = new RecordingTracer();
const logger = new RecordingLogger();
const repo = new {{pascalCase entity}}Repository(stubPayloadConfig, tracer, logger);
const result = await repo.get{{pascalCase entity}}("anything");
expect(result).toBeNull();
expect(tracer.spans).toHaveLength(1);
expect(tracer.spans[0]).toMatchObject({
name: "{{camelCase entity}}.get{{pascalCase entity}}",
op: "repository",
});
});
it("records the requested id as a span attribute", async () => {
const tracer = new RecordingTracer();
const repo = new {{pascalCase entity}}Repository(stubPayloadConfig, tracer);
await repo.get{{pascalCase entity}}("custom-id");
expect(tracer.spans[0]!.attributes.id).toBe("custom-id");
expect(tracer.spans[0]!.attributes.found).toBe(false);
});
});

View File

@@ -0,0 +1,62 @@
import "reflect-metadata";
import { injectable } from "inversify";
import type { SanitizedConfig } from "payload";
import {
NoopTracer,
NoopLogger,
type ITracer,
type ILogger,
} from "@repo/core-shared/instrumentation";
import type { I{{pascalCase entity}}Repository } from "../../application/repositories/{{kebabCase entity}}.repository.interface";
import type { {{pascalCase entity}} } from "../../entities/models/{{kebabCase entity}}";
const FEATURE = "{{kebabCase name}}" as const;
const REPO = "{{kebabCase entity}}" as const;
/**
* Phase-1 scaffold — the Payload collection has not been added yet, so this
* repository emits a span but always resolves to `null`. Once you add the
* collection at `integrations/cms/collections/{{kebabCase entity}}.ts` and
* register it with Payload, replace the stub below with a real `payload.find`
* call (see `packages/blog/src/infrastructure/repositories/articles.repository.ts`
* for the canonical pattern).
*/
@injectable()
export class {{pascalCase entity}}Repository implements I{{pascalCase entity}}Repository {
private config: SanitizedConfig;
private tracer: ITracer;
private logger: ILogger;
constructor(
config: SanitizedConfig,
tracer: ITracer = new NoopTracer(),
logger: ILogger = new NoopLogger(),
) {
this.config = config;
this.tracer = tracer;
this.logger = logger;
void this.config;
}
async get{{pascalCase entity}}(id: string): Promise<{{pascalCase entity}} | null> {
return this.tracer.startSpan(
{ name: "{{camelCase entity}}.get{{pascalCase entity}}", op: "repository", attributes: {} },
async (span) => {
try {
// TODO: replace with `payload.find({ collection: "{{entityPlural}}", where: { id: { equals: id } } })`
// once the Payload collection is registered.
span.setAttribute("id", id);
span.setAttribute("found", false);
return null;
} catch (err) {
this.logger.captureException(err, {
tags: { feature: FEATURE, repo: REPO, method: "get{{pascalCase entity}}" },
});
span.setStatus("error", err instanceof Error ? err.message : String(err));
throw err;
}
},
);
}
}

View File

@@ -0,0 +1,12 @@
import { t } from "@repo/core-shared/trpc/init";
import { defineErrorMiddleware } from "@repo/core-shared/trpc/define-error-middleware";
import { {{pascalCase entity}}NotFoundError } from "../../entities/errors/{{kebabCase entity}}";
import { InputParseError } from "../../entities/errors/common";
export const {{camelCase name}}Procedure = t.procedure.use(
defineErrorMiddleware([
[InputParseError, "BAD_REQUEST"],
[{{pascalCase entity}}NotFoundError, "NOT_FOUND"],
]),
);

View File

@@ -0,0 +1,93 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { TRPCError } from "@trpc/server";
import { injectable } from "inversify";
import { {{camelCase name}}Container } from "@/di/container";
import { {{pascalCase name}}Module } from "@/di/module";
import { {{constantCase name}}_SYMBOLS } from "@/di/symbols";
import { get{{pascalCase entity}}UseCase } from "@/application/use-cases/get-{{kebabCase entity}}.use-case";
import { get{{pascalCase entity}}Controller } from "@/interface-adapters/controllers/get-{{kebabCase entity}}.controller";
import { {{camelCase name}}Router } from "@/integrations/api/router";
describe("{{camelCase name}}Router", () => {
beforeEach(() => {
{{camelCase name}}Container.unbindAll();
{{camelCase name}}Container.load({{pascalCase name}}Module);
});
afterEach(() => {
{{camelCase name}}Container.unbindAll();
});
it("exposes the get{{pascalCase entity}} procedure", () => {
const names = Object.keys({{camelCase name}}Router._def.procedures);
expect(names).toContain("get{{pascalCase entity}}");
});
it("get{{pascalCase entity}} returns the seeded {{camelCase entity}}", async () => {
const caller = {{camelCase name}}Router.createCaller({});
const result = await caller.get{{pascalCase entity}}({ id: "seed-1" });
expect(result.id).toBe("seed-1");
});
});
describe("{{camelCase name}}Router (error mapping)", () => {
beforeEach(() => {
{{camelCase name}}Container.unbindAll();
{{camelCase name}}Container.load({{pascalCase name}}Module);
});
afterEach(() => {
{{camelCase name}}Container.unbindAll();
});
it("translates InputParseError → BAD_REQUEST when extra fields are passed", async () => {
const caller = {{camelCase name}}Router.createCaller({});
try {
await caller.get{{pascalCase entity}}({
id: "seed-1",
unexpected: "field",
} as unknown as { id: string });
throw new Error("expected throw");
} catch (e) {
expect(e).toBeInstanceOf(TRPCError);
expect((e as TRPCError).code).toBe("BAD_REQUEST");
}
});
it("translates {{pascalCase entity}}NotFoundError → NOT_FOUND when repository returns null", async () => {
@injectable()
class Null{{pascalCase entity}}Repository {
async get{{pascalCase entity}}() {
return null;
}
}
{{camelCase name}}Container.unbindAll();
{{camelCase name}}Container
.bind({{constantCase name}}_SYMBOLS.I{{pascalCase entity}}Repository)
.to(Null{{pascalCase entity}}Repository);
{{camelCase name}}Container
.bind({{constantCase name}}_SYMBOLS.IGet{{pascalCase entity}}UseCase)
.toDynamicValue((ctx) =>
get{{pascalCase entity}}UseCase(
ctx.container.get({{constantCase name}}_SYMBOLS.I{{pascalCase entity}}Repository),
),
);
{{camelCase name}}Container
.bind({{constantCase name}}_SYMBOLS.IGet{{pascalCase entity}}Controller)
.toDynamicValue((ctx) =>
get{{pascalCase entity}}Controller(
ctx.container.get({{constantCase name}}_SYMBOLS.IGet{{pascalCase entity}}UseCase),
),
);
const caller = {{camelCase name}}Router.createCaller({});
try {
await caller.get{{pascalCase entity}}({ id: "anything" });
throw new Error("expected throw");
} catch (e) {
expect(e).toBeInstanceOf(TRPCError);
expect((e as TRPCError).code).toBe("NOT_FOUND");
}
});
});

View File

@@ -0,0 +1,22 @@
import { router } from "@repo/core-shared/trpc/init";
import { {{camelCase name}}Container } from "../../di/container";
import { {{constantCase name}}_SYMBOLS } from "../../di/symbols";
import { get{{pascalCase entity}}InputSchema } from "../../application/use-cases/get-{{kebabCase entity}}.use-case";
import type { IGet{{pascalCase entity}}Controller } from "../../interface-adapters/controllers/get-{{kebabCase entity}}.controller";
import { {{camelCase name}}Procedure } from "./procedures";
export const {{camelCase name}}Router = router({
get{{pascalCase entity}}: {{camelCase name}}Procedure
.input(get{{pascalCase entity}}InputSchema)
.query(({ input }) => {
const ctrl = {{camelCase name}}Container.get<IGet{{pascalCase entity}}Controller>(
{{constantCase name}}_SYMBOLS.IGet{{pascalCase entity}}Controller,
);
return ctrl(input);
}),
});
export type {{pascalCase name}}Router = typeof {{camelCase name}}Router;

View File

@@ -0,0 +1,7 @@
// Payload CMS integration barrel for @repo/{{kebabCase name}}.
// Re-export this feature's collections and globals here as you add them
// under ./collections and ./globals. See packages/auth/src/integrations/cms
// for the canonical shape. The `<gen:job-tasks>` anchor below is required by
// `pnpm turbo gen job` and `pnpm turbo gen event` — do not remove it.
export {};
// <gen:job-tasks>

View File

@@ -0,0 +1,35 @@
import { describe, expect, it } from "vitest";
import { get{{pascalCase entity}}Controller } from "@/interface-adapters/controllers/get-{{kebabCase entity}}.controller";
import { get{{pascalCase entity}}UseCase } from "@/application/use-cases/get-{{kebabCase entity}}.use-case";
import { Mock{{pascalCase entity}}Repository } from "@/infrastructure/repositories/{{kebabCase entity}}.repository.mock";
import { InputParseError } from "@/entities/errors/common";
describe("get{{pascalCase entity}}Controller", () => {
it("returns the {{camelCase entity}} for a valid id", async () => {
const repo = new Mock{{pascalCase entity}}Repository();
const useCase = get{{pascalCase entity}}UseCase(repo);
const controller = get{{pascalCase entity}}Controller(useCase);
const result = await controller({ id: "seed-1" });
expect(result.id).toBe("seed-1");
});
it("throws InputParseError when input is missing fields", async () => {
const repo = new Mock{{pascalCase entity}}Repository();
const useCase = get{{pascalCase entity}}UseCase(repo);
const controller = get{{pascalCase entity}}Controller(useCase);
await expect(controller({})).rejects.toBeInstanceOf(InputParseError);
});
it("throws InputParseError when input has unexpected fields (strict)", async () => {
const repo = new Mock{{pascalCase entity}}Repository();
const useCase = get{{pascalCase entity}}UseCase(repo);
const controller = get{{pascalCase entity}}Controller(useCase);
await expect(
controller({ id: "seed-1", unexpected: true }),
).rejects.toBeInstanceOf(InputParseError);
});
});

View File

@@ -0,0 +1,23 @@
import { InputParseError } from "../../entities/errors/common";
import {
get{{pascalCase entity}}InputSchema,
type Get{{pascalCase entity}}Output,
type IGet{{pascalCase entity}}UseCase,
} from "../../application/use-cases/get-{{kebabCase entity}}.use-case";
function presenter(value: Get{{pascalCase entity}}Output) {
return value;
}
export type IGet{{pascalCase entity}}Controller = ReturnType<typeof get{{pascalCase entity}}Controller>;
export const get{{pascalCase entity}}Controller =
(get{{pascalCase entity}}UseCase: IGet{{pascalCase entity}}UseCase) =>
async (input: unknown): Promise<ReturnType<typeof presenter>> => {
const parsed = get{{pascalCase entity}}InputSchema.safeParse(input);
if (!parsed.success) {
throw new InputParseError("Invalid get-{{kebabCase entity}} input", { cause: parsed.error });
}
const result = await get{{pascalCase entity}}UseCase(parsed.data);
return presenter(result);
};

View File

@@ -0,0 +1,4 @@
// Phase-1 placeholder. Re-export React Query option builders here once you
// add `ui/query.ts`. See packages/auth/src/ui/index.ts for the
// canonical shape.
export {};

View File

@@ -0,0 +1,5 @@
{
"$schema": "../../node_modules/@stryker-mutator/core/schema/stryker-schema.json",
"_comment": "{{kebabCase name}} feature mutation testing config. Extends @repo/core-testing/stryker.base.json (ADR-020 L3). Run with `pnpm mutate --filter @repo/{{kebabCase name}}`.",
"extends": "@repo/core-testing/stryker.base.json"
}

View File

@@ -0,0 +1,14 @@
{
"extends": "@repo/core-typescript/base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": ".",
"lib": ["ES2022", "DOM"],
"jsx": "preserve",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*", "tests/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,4 @@
{
"extends": ["//"],
"tags": ["feature"]
}

View File

@@ -0,0 +1,32 @@
import path from "node:path";
import { mergeConfig } from "vitest/config";
import { nodeVitestConfig } from "@repo/core-typescript/vitest.base.node";
import {
DEFAULT_COVERAGE_BANDS,
vitestThresholdsFromBands,
} from "@repo/core-shared/conformance/coverage";
// Coverage thresholds derived from DEFAULT_COVERAGE_BANDS via the shared
// helper (ADR-020). The feature.manifest.ts `coverage.bands` section
// declares these for boot-time `assertFeatureConformance`. Edit the
// manifest when adjusting per-feature bands.
export default mergeConfig(nodeVitestConfig, {
test: {
coverage: {
exclude: [
// DI bootstrap — wires InversifyJS at app startup; not unit-testable
"src/di/bind-production.ts",
// Pure TypeScript interface files — not executable
"src/application/repositories/**",
// Payload CMS templates — declarative data, tested via integration
"src/integrations/cms/**",
// React Query option builders — integration-tested in apps
"src/ui/**",
],
thresholds: vitestThresholdsFromBands(DEFAULT_COVERAGE_BANDS),
},
},
resolve: {
alias: { "@": path.resolve(__dirname, "./src") },
},
});