@data-matters.io/agent-harness 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -2
- package/bin/harness +374 -0
- package/package.json +30 -4
- package/scripts/next-agent-rules.sh +67 -0
- package/scripts/toolchain.sh +14 -0
- package/standards/conventions.md +68 -0
- package/standards/dod.md +53 -0
- package/standards/stack.md +42 -0
- package/standards/stacks/nestjs/architecture.md +75 -0
- package/standards/stacks/nestjs/conventions.md +33 -0
- package/standards/stacks/nestjs/stack.md +34 -0
- package/standards/stacks/nestjs/testing.md +31 -0
- package/standards/stacks/nextjs/architecture.md +97 -0
- package/standards/stacks/nextjs/conventions.md +32 -0
- package/standards/stacks/nextjs/stack.md +40 -0
- package/standards/stacks/nextjs/testing.md +33 -0
- package/standards/testing.md +58 -0
- package/templates/.claude/hooks/stop-gates.sh +43 -0
- package/templates/.claude/settings.json +72 -0
- package/templates/.github/dependabot.yml +19 -0
- package/templates/.github/workflows/gates.yml +41 -0
- package/templates/.harness/db-smoke.sh +75 -0
- package/templates/.harness/deps.sh +219 -0
- package/templates/.harness/gates.conf.example +31 -0
- package/templates/.harness/gates.sh +108 -0
- package/templates/.harness/lib.sh +169 -0
- package/templates/.harness/loop.sh +169 -0
- package/templates/.harness/model-flags.sh +34 -0
- package/templates/.harness/models.conf +31 -0
- package/templates/.harness/notify.sh +49 -0
- package/templates/.harness/openspec.sh +28 -0
- package/templates/.harness/pr-scope.sh +44 -0
- package/templates/.harness/run-tasks.sh +461 -0
- package/templates/.harness/spec-pr.sh +157 -0
- package/templates/.harness/status.sh +155 -0
- package/templates/CLAUDE.md.tmpl +159 -0
- package/templates/openspec/config.yaml +38 -0
- package/templates/stacks/nestjs/gates.conf +35 -0
- package/templates/stacks/nestjs/gates.yml +71 -0
- package/templates/stacks/nextjs/claude-imports.md +4 -0
- package/templates/stacks/nextjs/gates.conf +36 -0
- package/templates/stacks/nextjs/gates.yml +68 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Stack
|
|
2
|
+
|
|
3
|
+
Derived from the reference repositories (see ADR 0007). Every product starts
|
|
4
|
+
from one of the app templates; both share the same harness, workflow and
|
|
5
|
+
standards, and differ only in the framework.
|
|
6
|
+
|
|
7
|
+
| Stack | Template | Use for | Details |
|
|
8
|
+
| -------- | -------------------------------- | -------------------------------------- | ------------------------ |
|
|
9
|
+
| `nextjs` | `data-matters-io/nextjs-new-app` | Web applications (UI + route handlers) | `stacks/nextjs/stack.md` |
|
|
10
|
+
| `nestjs` | `data-matters-io/nestjs-new-app` | Backend services (HTTP APIs) | `stacks/nestjs/stack.md` |
|
|
11
|
+
|
|
12
|
+
In a product repo, the chosen stack's files are in `.harness/standards/stack/`.
|
|
13
|
+
|
|
14
|
+
## Shared by every stack
|
|
15
|
+
|
|
16
|
+
| Area | Choice |
|
|
17
|
+
| --------------- | ------------------------------------------------------------------------------------- |
|
|
18
|
+
| Language | TypeScript, `strict: true` |
|
|
19
|
+
| Package manager | pnpm 12.11.2, exact in `packageManager` + `devEngines` (ADR 0014); one lockfile |
|
|
20
|
+
| npm | Never in a project (`devEngines` makes it refuse); 12.2.0 on machines, user-level |
|
|
21
|
+
| Validation | Zod 4 |
|
|
22
|
+
| ORM / database | Prisma 7 (`@prisma/adapter-pg`) / PostgreSQL 16+ |
|
|
23
|
+
| Testing | Jest 30 + `ts-jest` |
|
|
24
|
+
| Lint / format | `@data-matters.io/eslint-config` 4.x (stack preset; brings `eslint` and `prettier`) |
|
|
25
|
+
| Git hooks | `data-matters-eslint-init` (postinstall). No husky. |
|
|
26
|
+
| Local infra | Podman (preferred) or Docker via Compose, driven by `make` |
|
|
27
|
+
| Spec workflow | OpenSpec (ADR 0002) + agent-harness |
|
|
28
|
+
| OpenSpec CLI | `@fission-ai/openspec` devDependency, exact pin (`.harness/openspec.sh`; ADR 0013) |
|
|
29
|
+
| CI | GitHub Actions: the template's `ci.yaml` plus `gates.yml` running `.harness/gates.sh` |
|
|
30
|
+
| CI on spec PRs | Jobs gated on `.harness/pr-scope.sh`: spec-only PRs run `openspec validate` only |
|
|
31
|
+
| Hosting | VPS, provisioned via `vps-bootstrap` |
|
|
32
|
+
| Secrets | Environment variables (or a secrets manager); never committed |
|
|
33
|
+
|
|
34
|
+
## Stack presets
|
|
35
|
+
|
|
36
|
+
`templates/stacks/<stack>/` holds the gate commands (`gates.conf`) and the CI
|
|
37
|
+
workflow (`gates.yml`) validated against each template:
|
|
38
|
+
|
|
39
|
+
| Stack | Validated against |
|
|
40
|
+
| -------- | ---------------------------- |
|
|
41
|
+
| `nextjs` | see `stacks/nextjs/stack.md` |
|
|
42
|
+
| `nestjs` | see `stacks/nestjs/stack.md` |
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Architecture: NestJS
|
|
2
|
+
|
|
3
|
+
Architecture for backend services, as established by the app template
|
|
4
|
+
(`nestjs-new-app`). Deviations need an ADR in the product repo.
|
|
5
|
+
|
|
6
|
+
## Layout
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
src/
|
|
10
|
+
├── main.ts, app.module.ts # composition root
|
|
11
|
+
├── modules/<feature>/ # self-contained feature modules
|
|
12
|
+
│ ├── domain/ # entities, value objects, errors, ports — no framework imports
|
|
13
|
+
│ ├── application/ # services orchestrating the domain (use cases)
|
|
14
|
+
│ ├── adapters/
|
|
15
|
+
│ │ ├── inbound/ # controllers
|
|
16
|
+
│ │ └── outbound/ # Prisma repositories implementing domain ports
|
|
17
|
+
│ ├── dto/ # request/response shapes, validated with Zod
|
|
18
|
+
│ ├── <feature>.module.ts
|
|
19
|
+
│ └── README.md
|
|
20
|
+
└── shared/ # cross-cutting infrastructure (config, database,
|
|
21
|
+
# problem-details, authn, authz, observability, …)
|
|
22
|
+
db/ # schema.prisma, migrations/ (local), seed.ts
|
|
23
|
+
deploy/ # k8s manifests, reviewed SQL for deployed databases
|
|
24
|
+
test/ # e2e specs
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`me` is the reference for a feature module at its smallest; `health` shows the
|
|
28
|
+
same shape with an extension seam (indicators) instead of a database.
|
|
29
|
+
|
|
30
|
+
## Dependency rules
|
|
31
|
+
|
|
32
|
+
| Rule | Enforced by |
|
|
33
|
+
| ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
|
|
34
|
+
| A feature module never imports another feature module; shared code lives in `@/shared/*` | `@data-matters.io/nestjs-module-boundaries` (lint, error) |
|
|
35
|
+
| No parent-relative imports: use `@/` or a same-directory `./` | `@data-matters.io/no-parent-relative-imports` (lint, error) |
|
|
36
|
+
| No `process.env` outside the config layer (config loader and secrets bootstrap are exempt) | `@data-matters.io/no-process-env` (lint, error) |
|
|
37
|
+
| `domain/` has no framework or Prisma imports | architecture test (`*.arch.spec.ts`) |
|
|
38
|
+
| Controllers depend on application services, services on domain ports; adapters implement ports | PR review |
|
|
39
|
+
|
|
40
|
+
## Request flow
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
controller (adapters/inbound) → Zod-validated DTO, @RequireAccess
|
|
44
|
+
→ application service → domain logic, depends on ports only
|
|
45
|
+
→ repository port (domain) ← Prisma adapter (adapters/outbound)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- **Validation** at the boundary with the Zod validation pipe; DTO types come
|
|
49
|
+
from the schemas.
|
|
50
|
+
- **Errors** are RFC 9457 problem details. Throw subclasses of the abstract
|
|
51
|
+
`AppProblemError`; the global `problem-details` filter renders them. Never
|
|
52
|
+
hand-build error bodies.
|
|
53
|
+
- **Configuration** only through the typed `ConfigService`; new settings get a
|
|
54
|
+
Zod schema in their module's `schema/` and fail fast at startup.
|
|
55
|
+
- **Persistence** follows `src/shared/database/README.md`: one pooled
|
|
56
|
+
`PrismaService`, repositories behind domain ports, explicit mappers,
|
|
57
|
+
interactive transactions for multi-write operations.
|
|
58
|
+
- **Schema changes**: `db/schema.prisma` is the source of truth. Local
|
|
59
|
+
migrations via `make db-migrate`; deployed databases receive a reviewed SQL
|
|
60
|
+
diff (`make db-diff`) in `deploy/db/`, applied by the data pipeline. The app
|
|
61
|
+
never migrates on startup or deploy, so changes must be expand/contract
|
|
62
|
+
compatible.
|
|
63
|
+
|
|
64
|
+
## Authentication and authorization
|
|
65
|
+
|
|
66
|
+
Entra ID authentication is optional and switchable by configuration. Every
|
|
67
|
+
route is authenticated unless marked `@Public()`; protected routes declare
|
|
68
|
+
their access level with `@RequireAccess`, and tests cover both the allowed and
|
|
69
|
+
the denied path.
|
|
70
|
+
|
|
71
|
+
## Observability
|
|
72
|
+
|
|
73
|
+
Log through the injected Pino logger (structured, with correlation IDs and
|
|
74
|
+
redaction), never `console`. Outbound HTTP calls use the shared headers module
|
|
75
|
+
so correlation and trace context propagate.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Conventions: NestJS
|
|
2
|
+
|
|
3
|
+
Extends the shared `conventions.md`.
|
|
4
|
+
|
|
5
|
+
## Code
|
|
6
|
+
|
|
7
|
+
- Aliases: `@/…` (maps to `src/`) and `@root/…` (repo root). No `../`.
|
|
8
|
+
- Classes use NestJS decorators; dependency injection through constructors.
|
|
9
|
+
Ports are injected by token, not by concrete class.
|
|
10
|
+
- Each feature module and each shared module has a `README.md` describing its
|
|
11
|
+
purpose, configuration and extension points.
|
|
12
|
+
|
|
13
|
+
## Naming
|
|
14
|
+
|
|
15
|
+
| Kind | Convention | Example |
|
|
16
|
+
| ----------------------------- | ------------------------------------------------------------------------- | ------------------------------- |
|
|
17
|
+
| Feature module folder | kebab-case under `src/modules/` | `src/modules/me/` |
|
|
18
|
+
| Module / controller / service | `<name>.module.ts`, `<name>.controller.ts`, `<name>.service.ts` | `health.controller.ts` |
|
|
19
|
+
| DTO | `dto/<name>.dto.ts` | `dto/me-response.dto.ts` |
|
|
20
|
+
| Port | `domain/ports/<name>.port.ts` (use-case ports end in `.use-case.port.ts`) | `check-health.use-case.port.ts` |
|
|
21
|
+
| Config schema | `schema/<name>.schema.ts` | `schema/database.schema.ts` |
|
|
22
|
+
| Unit test | `<file>.spec.ts` beside the code or in `__tests__/` | `health.service.spec.ts` |
|
|
23
|
+
| Property-based test | `<topic>.pbt.spec.ts` | `headers.pbt.spec.ts` |
|
|
24
|
+
| Architecture test | `<topic>.arch.spec.ts` | `domain-purity.arch.spec.ts` |
|
|
25
|
+
| Integration test | `<topic>.integration.test.ts` | `prisma.integration.test.ts` |
|
|
26
|
+
| E2E test | `test/<topic>.e2e-spec.ts` | `test/auth.e2e-spec.ts` |
|
|
27
|
+
| Prisma models | PascalCase models, camelCase fields, UPPER_CASE enum values | — |
|
|
28
|
+
| Env vars | `UPPER_SNAKE_CASE`, grouped by prefix (`DB_…`, `AUTH_…`, `DD_…`) | `DB_POOL_MAX` |
|
|
29
|
+
|
|
30
|
+
## Local workflow
|
|
31
|
+
|
|
32
|
+
`make setup`, `make dev`, `make ci-check`; database targets `make db-migrate`,
|
|
33
|
+
`make db-diff`, `make db-verify`.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Stack: NestJS backend services
|
|
2
|
+
|
|
3
|
+
Template: `data-matters-io/nestjs-new-app` (reviewed at `b916be8`, with
|
|
4
|
+
`@data-matters.io/eslint-config` 4.0.0).
|
|
5
|
+
|
|
6
|
+
| Area | Choice | Version |
|
|
7
|
+
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
|
|
8
|
+
| Runtime | Node.js | 22.x+ |
|
|
9
|
+
| Framework | NestJS on Express (`@nestjs/platform-express`) | 11.x |
|
|
10
|
+
| Architecture | Hexagonal feature modules (see `architecture.md`) | — |
|
|
11
|
+
| Config | Typed, Zod-validated `ConfigService`, loaded once at startup (fail-fast) | — |
|
|
12
|
+
| Secrets | AWS Secrets Manager resolved into the config pipeline | — |
|
|
13
|
+
| API docs | OpenAPI via `@nestjs/swagger` (Zod-backed decorators) | 11.x |
|
|
14
|
+
| Errors | RFC 9457 problem details (`application/problem+json`) via a global filter | — |
|
|
15
|
+
| HTTP hardening | Helmet, CORS, `@nestjs/throttler` rate limiting | — |
|
|
16
|
+
| Auth | Microsoft Entra ID (JWT via `jose`, BFF cookie session via `@azure/msal-node`), optional | — |
|
|
17
|
+
| Authorization | `@RequireAccess` + `AccessLevelGuard` | — |
|
|
18
|
+
| Observability | Pino structured logs, correlation IDs, Datadog transport, OpenTelemetry API | — |
|
|
19
|
+
| Database migrations | `prisma migrate dev` locally only; deployed schemas change through a reviewed SQL diff (`prisma migrate diff --script`) run by the data pipeline, never by the app | — |
|
|
20
|
+
| Testing | Jest + `supertest` (e2e), `fast-check` (property-based), Testcontainers (integration) | 30.x |
|
|
21
|
+
| Lint preset | `nestjs` (typed linting, `@darraghor/nestjs-typed`, module-boundary rules) | 4.x |
|
|
22
|
+
| Packaging / deploy | Docker image; Kubernetes manifests in `deploy/k8s` | — |
|
|
23
|
+
|
|
24
|
+
## Gates (`templates/stacks/nestjs/gates.conf`)
|
|
25
|
+
|
|
26
|
+
| Gate | Command |
|
|
27
|
+
| ------------------- | -------------------------------------------------------------------------------------------- |
|
|
28
|
+
| setup | `prisma generate` |
|
|
29
|
+
| test | `jest --coverage --coverageThreshold` (floor: 85% lines, statements, branches and functions) |
|
|
30
|
+
| lint | `eslint . --max-warnings 0` |
|
|
31
|
+
| typecheck | `tsc --noEmit -p tsconfig.json` (includes tests; `tsconfig.build.json` does not) |
|
|
32
|
+
| build | `nest build` |
|
|
33
|
+
| e2e (extra) | `jest --config ./test/jest-e2e.json` |
|
|
34
|
+
| integration (extra) | `jest --config ./test/jest-integration.json` (needs Docker or Podman) |
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Testing: NestJS
|
|
2
|
+
|
|
3
|
+
Extends the shared `testing.md`.
|
|
4
|
+
|
|
5
|
+
Tooling: Jest 30 + `ts-jest` (`testEnvironment: node`), `@nestjs/testing`,
|
|
6
|
+
`supertest` for HTTP, `fast-check` for property-based tests, Testcontainers
|
|
7
|
+
(`@testcontainers/postgresql`) for integration.
|
|
8
|
+
|
|
9
|
+
## Tiers
|
|
10
|
+
|
|
11
|
+
| Tier | Pattern | Config | Status |
|
|
12
|
+
| -------------------------------------------------- | ------------------------------ | ---------------------------- | ------------------------------------------------------------ |
|
|
13
|
+
| Unit (incl. property-based and architecture tests) | `src/**/*.spec.ts` | `package.json#jest` | in use; `test` gate |
|
|
14
|
+
| Integration | `src/**/*.integration.test.ts` | `test/jest-integration.json` | in use; `integration` extra gate (Docker or Podman required) |
|
|
15
|
+
| E2E | `test/*.e2e-spec.ts` | `test/jest-e2e.json` | in use; `e2e` extra gate |
|
|
16
|
+
|
|
17
|
+
## Rules
|
|
18
|
+
|
|
19
|
+
- Unit tests build the module under test with `Test.createTestingModule`,
|
|
20
|
+
replacing ports with fakes; no database, no network.
|
|
21
|
+
- Invariants that hold for any input (header maps, redaction, pagination,
|
|
22
|
+
transaction atomicity) get a property-based test.
|
|
23
|
+
- Repository adapters are covered by integration tests against a real
|
|
24
|
+
PostgreSQL started by Testcontainers, never by mocking Prisma.
|
|
25
|
+
- E2E tests boot the application and assert on HTTP status, headers and
|
|
26
|
+
RFC 9457 bodies, including the unauthenticated and forbidden paths.
|
|
27
|
+
|
|
28
|
+
## Coverage floor
|
|
29
|
+
|
|
30
|
+
85% on lines, statements, branches and functions over every file in `src/`
|
|
31
|
+
(template measured 92.0% / 92.7% / 90.8% / 93.5% on 2026-10-01).
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Architecture: Next.js
|
|
2
|
+
|
|
3
|
+
Architecture for Next.js products, as established by the app template
|
|
4
|
+
(`nextjs-new-app`). Deviations need an ADR in the product repo.
|
|
5
|
+
|
|
6
|
+
## Feature-Based Architecture (FBA)
|
|
7
|
+
|
|
8
|
+
A feature is self-contained: deleting its folder must not break the project.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
app/
|
|
12
|
+
├── (features)/ # route group; not part of the URL
|
|
13
|
+
│ ├── (<feature>)/ # one route group per feature
|
|
14
|
+
│ │ ├── <route>/page.tsx # pages
|
|
15
|
+
│ │ ├── api/<route>/route.ts # route handlers (thin)
|
|
16
|
+
│ │ ├── api/<route>/<entity>.controller.ts
|
|
17
|
+
│ │ ├── components/ # feature components (+ __tests__/)
|
|
18
|
+
│ │ ├── hooks/ # feature hooks (+ __tests__/)
|
|
19
|
+
│ │ ├── contexts/ # feature React contexts
|
|
20
|
+
│ │ ├── types/ # feature types
|
|
21
|
+
│ │ └── test-fixtures.ts # make<Entity>() factories for tests
|
|
22
|
+
│ ├── layout.tsx
|
|
23
|
+
│ └── page.tsx
|
|
24
|
+
├── shared/ # code used by 3+ features
|
|
25
|
+
│ ├── components/{ui,layout,auth}/
|
|
26
|
+
│ ├── config/ # rbac.ts, route-permissions.ts, menuItems.json
|
|
27
|
+
│ ├── core/ # BaseController
|
|
28
|
+
│ ├── hooks/ libs/ providers/ types/ utils/ messages/
|
|
29
|
+
db/ # schema.prisma, migrations/, seed.ts
|
|
30
|
+
proxy.ts # Next.js middleware (route protection)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Dependency rules
|
|
34
|
+
|
|
35
|
+
| Rule | Enforced by |
|
|
36
|
+
| -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
37
|
+
| A feature never imports from another feature | `@data-matters.io/fba-boundaries` (lint, error) |
|
|
38
|
+
| `shared/` never imports from `(features)/` | `@data-matters.io/fba-boundaries` (lint, error) |
|
|
39
|
+
| Code moves to `shared/` only when a third feature needs it (rule of three) | PR review |
|
|
40
|
+
| Cross-folder imports use path aliases (`@/`, `@shared/*`, `@features/*`), not `../../` | `@data-matters.io/no-deep-relative-imports` (lint, warn → blocks with `--max-warnings 0`) |
|
|
41
|
+
|
|
42
|
+
## Request flow (server)
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
proxy.ts (authn, route permissions)
|
|
46
|
+
→ route.ts thin: check permission, delegate, map result to NextResponse
|
|
47
|
+
→ <Entity>Controller extends BaseController validation (Zod) + data access
|
|
48
|
+
→ Prisma
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- **Route handlers** check permissions with `requirePermission(request, PERMISSIONS.X)`
|
|
52
|
+
first, then delegate to the controller. No business logic or Prisma calls in
|
|
53
|
+
`route.ts`.
|
|
54
|
+
- **Controllers** extend `app/shared/core/base-controller.ts` for CRUD
|
|
55
|
+
(pagination, search via `buildSearchFilter`, Prisma error mapping) and
|
|
56
|
+
override only what differs.
|
|
57
|
+
- **Validation** at the boundary with Zod schemas. Schema-first: prefer
|
|
58
|
+
deriving TypeScript types from schemas (`z.infer`) or Prisma over
|
|
59
|
+
hand-written duplicates.
|
|
60
|
+
- **Response contract**: build responses with `app/shared/libs/http-responses.ts`,
|
|
61
|
+
never hand-built `NextResponse.json(...)`. Collections are JSON:API documents
|
|
62
|
+
(`{ data, meta, links }`, `buildPaginatedResponse`); errors are RFC 9457
|
|
63
|
+
problem details (`application/problem+json`, `problemResponse` and its
|
|
64
|
+
status shortcuts, `validationProblem` for Zod errors).
|
|
65
|
+
- **Errors**: Prisma errors map to problem details (P2002 → 409, P2025 → 404,
|
|
66
|
+
P2003 → 400). Log details only in development; never leak internals in
|
|
67
|
+
production responses.
|
|
68
|
+
- Route handler params are `Promise`s in Next.js 16: `await params`.
|
|
69
|
+
|
|
70
|
+
## Authorization (RBAC)
|
|
71
|
+
|
|
72
|
+
Three layers, all driven by `app/shared/config/rbac.ts` (`PERMISSIONS`) and
|
|
73
|
+
`route-permissions.ts`:
|
|
74
|
+
|
|
75
|
+
1. **Middleware** (`proxy.ts`): blocks unauthorized routes.
|
|
76
|
+
2. **API**: `requirePermission` in every route handler that mutates or reads
|
|
77
|
+
protected data.
|
|
78
|
+
3. **UI**: `RequireAuth` / `RequirePermission` / `RequireRole` components and
|
|
79
|
+
`useUserAccess`; menu entries declare `permissions`/`roles`.
|
|
80
|
+
|
|
81
|
+
A new protected capability adds a permission constant, protects all three
|
|
82
|
+
layers and has tests for the denied path.
|
|
83
|
+
|
|
84
|
+
## Client
|
|
85
|
+
|
|
86
|
+
- Data fetching in feature hooks (`useX`, `useCreateX`, …) returning
|
|
87
|
+
`{ data, isLoading, error, … }`; components stay presentational.
|
|
88
|
+
- UI primitives come from `@/app/shared/components/ui` (shadcn/ui). Do not
|
|
89
|
+
add another component library.
|
|
90
|
+
- Accessibility target: WCAG 2.1 AA (labels, keyboard navigation, focus,
|
|
91
|
+
contrast). jsx-a11y rules run in lint.
|
|
92
|
+
|
|
93
|
+
## Configuration and secrets
|
|
94
|
+
|
|
95
|
+
- Configuration from environment variables; `env-example` documents every
|
|
96
|
+
variable with **placeholder values only**.
|
|
97
|
+
- `.env*` files are git-ignored and never read by agents.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Conventions: Next.js
|
|
2
|
+
|
|
3
|
+
Extends the shared `conventions.md`.
|
|
4
|
+
|
|
5
|
+
## Code
|
|
6
|
+
|
|
7
|
+
- Components are arrow functions (`react/function-component-definition`).
|
|
8
|
+
- Type-only imports inline: `import { type Foo } from '…'`.
|
|
9
|
+
- Import order (auto-fixed by lint): builtin → external → internal (`@/…`) →
|
|
10
|
+
parent → sibling → index → type, alphabetical, blank line between groups,
|
|
11
|
+
React first.
|
|
12
|
+
- Aliases: `@/*`, `@app/*`, `@shared/*`, `@features/*`.
|
|
13
|
+
- User-facing strings go through `app/shared/messages/{en,es,pt}.json`.
|
|
14
|
+
|
|
15
|
+
## Naming
|
|
16
|
+
|
|
17
|
+
| Kind | Convention | Example |
|
|
18
|
+
| ------------------- | ----------------------------------------------------------- | ---------------------------------- |
|
|
19
|
+
| Feature folder | route group, kebab-case | `app/(features)/(admin)/` |
|
|
20
|
+
| Feature component | PascalCase `.tsx` | `UserForm.tsx` |
|
|
21
|
+
| shadcn/ui primitive | kebab-case `.tsx` (shadcn default) | `radio-group.tsx` |
|
|
22
|
+
| Hook | `useX.ts`, one hook per file | `useCreateUser.ts` |
|
|
23
|
+
| Controller | `<entity>.controller.ts` next to its route | `user.controller.ts` |
|
|
24
|
+
| Types | `types/<entity>.ts` | `types/user.ts` |
|
|
25
|
+
| Tests | `__tests__/<file>.test.ts(x)` beside the code | `hooks/__tests__/useUsers.test.ts` |
|
|
26
|
+
| Test factories | `make<Entity>()` in `test-fixtures.ts` | `makeUser()` |
|
|
27
|
+
| Prisma models | PascalCase models, camelCase fields, UPPER_CASE enum values | `User.status: ACTIVE` |
|
|
28
|
+
| Env vars | UPPER_SNAKE_CASE | `NEXTAUTH_SECRET` |
|
|
29
|
+
|
|
30
|
+
## Local workflow
|
|
31
|
+
|
|
32
|
+
`make setup`, `make dev`, `make ci-check` (format → lint → typecheck → tests).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Stack: Next.js web applications
|
|
2
|
+
|
|
3
|
+
Template: `data-matters-io/nextjs-new-app` (reviewed at `164057b`, with
|
|
4
|
+
`@data-matters.io/eslint-config` 4.0.0).
|
|
5
|
+
|
|
6
|
+
| Area | Choice | Version |
|
|
7
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------- | ------- |
|
|
8
|
+
| Runtime | Node.js | 24.x |
|
|
9
|
+
| Framework | Next.js, App Router, Turbopack builds | 16.x |
|
|
10
|
+
| UI | React | 19.x |
|
|
11
|
+
| Styling | Tailwind CSS | 4.x |
|
|
12
|
+
| Components | shadcn/ui (Radix UI primitives), `lucide-react` icons, `class-variance-authority` + `tailwind-merge` (`cn()`) | — |
|
|
13
|
+
| Theming | `next-themes` (dark mode) | — |
|
|
14
|
+
| Auth | NextAuth.js (Google provider) + in-app RBAC | 4.x |
|
|
15
|
+
| API responses | JSON:API documents; errors as RFC 9457 problem details | — |
|
|
16
|
+
| Notifications | `react-hot-toast` | — |
|
|
17
|
+
| Error tracking | GlitchTip via `@sentry/nextjs` | — |
|
|
18
|
+
| i18n | In-app messages (`app/shared/messages/{en,es,pt}.json`) | — |
|
|
19
|
+
| Testing | Jest + React Testing Library (jsdom) | 30.x |
|
|
20
|
+
| Lint preset | `nextjs` (React, a11y, Core Web Vitals, FBA rules) | 4.x |
|
|
21
|
+
|
|
22
|
+
## Agent rules from Next.js (ADR 0018)
|
|
23
|
+
|
|
24
|
+
`next dev` keeps a version-matched "agent rules" block in `AGENTS.md`, which is
|
|
25
|
+
committed and owned by Next.js. The harness-rendered `CLAUDE.md` imports it
|
|
26
|
+
(`@AGENTS.md`), and the block never goes into `CLAUDE.md`:
|
|
27
|
+
`scripts/next-agent-rules.sh` (run by `init-project.sh` and `sync-project.sh`)
|
|
28
|
+
moves it out of `CLAUDE.md` and creates `AGENTS.md` when it is missing. Commit
|
|
29
|
+
`AGENTS.md` changes with the Next.js upgrade that causes them.
|
|
30
|
+
|
|
31
|
+
## Gates (`templates/stacks/nextjs/gates.conf`)
|
|
32
|
+
|
|
33
|
+
| Gate | Command |
|
|
34
|
+
| ---------------- | --------------------------------------------------------------------------------- |
|
|
35
|
+
| setup | `prisma generate` |
|
|
36
|
+
| test | `jest --coverageThreshold` (floor: 85% lines, statements, branches and functions) |
|
|
37
|
+
| lint | `eslint . --max-warnings 0` |
|
|
38
|
+
| typecheck | `tsc --noEmit` |
|
|
39
|
+
| build | `next build` |
|
|
40
|
+
| integration, e2e | pending (see `testing.md`) |
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Testing: Next.js
|
|
2
|
+
|
|
3
|
+
Extends the shared `testing.md`.
|
|
4
|
+
|
|
5
|
+
Tooling: Jest 30 + React Testing Library (jsdom), `ts-jest`, setup in
|
|
6
|
+
`jest.setup.tsx`, helpers in `app/shared/test-utils.tsx` and
|
|
7
|
+
`app/shared/test-helpers.ts`. The template also has an e2e smoke test
|
|
8
|
+
(`scripts/smoke-test.ts`, `pnpm test:smoke`) run by its own `ci.yaml` against
|
|
9
|
+
a built app and PostgreSQL.
|
|
10
|
+
|
|
11
|
+
## Tiers
|
|
12
|
+
|
|
13
|
+
| Tier | Pattern | Status |
|
|
14
|
+
| ----------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------- |
|
|
15
|
+
| Unit | `__tests__/*.test.ts(x)` beside the code | in use; `test` gate |
|
|
16
|
+
| Integration | `*.integration.test.ts` (controllers + Prisma against PostgreSQL via Testcontainers, as in `nestjs-new-app`) | **pending**: to be implemented in a future change |
|
|
17
|
+
| E2E | `pnpm test:smoke` | template CI only; not yet a harness gate |
|
|
18
|
+
|
|
19
|
+
## Rules
|
|
20
|
+
|
|
21
|
+
- Unit tests do not touch the network or the database: mock `fetch` and the
|
|
22
|
+
Prisma client. Controllers are tested through their public methods with a
|
|
23
|
+
real `Request`.
|
|
24
|
+
- Components are tested through accessible queries (`getByRole`,
|
|
25
|
+
`getByLabelText`), not implementation details.
|
|
26
|
+
- RBAC: every protected route/component tests both the allowed and the denied
|
|
27
|
+
path.
|
|
28
|
+
|
|
29
|
+
## Coverage floor
|
|
30
|
+
|
|
31
|
+
85% on lines, statements, branches and functions, over every file in `app/`
|
|
32
|
+
plus `proxy.ts` (`collectCoverageFrom` in `jest.config.ts`; the `(examples)`
|
|
33
|
+
showcase pages are excluded).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
Principles shared by every stack. Tooling, file patterns and tiers in use are
|
|
4
|
+
in `stack/testing.md`.
|
|
5
|
+
|
|
6
|
+
## Tiers
|
|
7
|
+
|
|
8
|
+
| Tier | Scope | Runs in |
|
|
9
|
+
| ----------- | -------------------------------------------------------------- | --------------------------------- |
|
|
10
|
+
| Unit | One unit in isolation; no network, no database | `test` gate (Stop hook, loop, CI) |
|
|
11
|
+
| Integration | Adapters against real dependencies (PostgreSQL in a container) | extra gate (loop, CI) |
|
|
12
|
+
| E2E | Full flows through the public interface (HTTP API, UI) | extra gate (loop, CI) |
|
|
13
|
+
|
|
14
|
+
A tier the stack does not use yet is listed as **pending** in
|
|
15
|
+
`stack/testing.md`; the first change that needs it sets it up.
|
|
16
|
+
|
|
17
|
+
## Rules
|
|
18
|
+
|
|
19
|
+
- Each spec scenario (`#### Scenario:` in OpenSpec) has at least one test.
|
|
20
|
+
Put the scenario name in the test title.
|
|
21
|
+
- Write the failing test first, then the code.
|
|
22
|
+
- Never change what an existing test asserts to make it pass. A conflict
|
|
23
|
+
between a test and the code is reported to a human; a confirmed
|
|
24
|
+
out-of-scope bug is recorded with `it.failing` and an exact assertion.
|
|
25
|
+
- Test data comes from factories, not ad-hoc literals repeated across tests.
|
|
26
|
+
- Access control: every protected route tests both the allowed and the
|
|
27
|
+
denied path.
|
|
28
|
+
- Deterministic: no real clock or randomness without injection. Flaky tests
|
|
29
|
+
are bugs and block the gate.
|
|
30
|
+
|
|
31
|
+
## Coverage
|
|
32
|
+
|
|
33
|
+
- **Measured over all source files**, not only the ones some test imports:
|
|
34
|
+
untested modules must count as uncovered (`collectCoverageFrom`). The only
|
|
35
|
+
exclusions allowed are tests and their helpers, generated code, declaration
|
|
36
|
+
files, `*.example.*` reference files and template showcase code; each one is
|
|
37
|
+
listed in the coverage configuration.
|
|
38
|
+
- **Executable statements, not lines of text**: use the statement-based
|
|
39
|
+
(istanbul / `babel`) provider, so type-only files weigh nothing.
|
|
40
|
+
- **Floor: 85% on lines, statements, branches and functions**, in every
|
|
41
|
+
stack, enforced by the `test` gate. A product may raise it, never lower it;
|
|
42
|
+
lowering it needs an ADR.
|
|
43
|
+
|
|
44
|
+
## Existing projects
|
|
45
|
+
|
|
46
|
+
An existing project below 85% enters the harness with a **coverage ratchet**
|
|
47
|
+
(ADR 0009):
|
|
48
|
+
|
|
49
|
+
1. Measure coverage honestly (all source files, statement-based) and set the
|
|
50
|
+
gate floor to that baseline. `.harness/autonomy` is `disabled`.
|
|
51
|
+
2. Add **characterization tests** for the critical flows, run with
|
|
52
|
+
`run-tasks.sh --force` under supervision. They pin current behaviour, even
|
|
53
|
+
if wrong; suspicious behaviour gets a `// Characterization: looks like a
|
|
54
|
+
bug — …` comment and is reported, never fixed in the same change.
|
|
55
|
+
3. Set `.harness/autonomy` to `enabled`.
|
|
56
|
+
4. After every merged change, raise the floor to the new measured coverage.
|
|
57
|
+
The floor never goes down; 85% on every metric is the target, after which
|
|
58
|
+
the shared floor applies.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Claude Code Stop hook: blocks the agent from finishing while DoD gates fail.
|
|
3
|
+
#
|
|
4
|
+
# - No uncommitted changes → nothing to verify, allow stop.
|
|
5
|
+
# - Gates fail → exit 2; stderr is fed back to the agent so it keeps working.
|
|
6
|
+
# - After HARNESS_STOP_MAX_BLOCKS consecutive blocks in one session, allow the
|
|
7
|
+
# stop so a human (or the task loop's retry logic) can take over.
|
|
8
|
+
set -uo pipefail
|
|
9
|
+
|
|
10
|
+
input="$(cat)"
|
|
11
|
+
project_dir="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
12
|
+
cd "$project_dir" || exit 0
|
|
13
|
+
|
|
14
|
+
gates="$project_dir/.harness/gates.sh"
|
|
15
|
+
[[ -x "$gates" ]] || exit 0
|
|
16
|
+
[[ -n "$(git status --porcelain 2>/dev/null)" ]] || exit 0
|
|
17
|
+
|
|
18
|
+
session_id="$(printf '%s' "$input" | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)"
|
|
19
|
+
state_dir="$project_dir/.harness/state"
|
|
20
|
+
mkdir -p "$state_dir"
|
|
21
|
+
counter="$state_dir/stop-blocks-${session_id:-unknown}"
|
|
22
|
+
max="${HARNESS_STOP_MAX_BLOCKS:-5}"
|
|
23
|
+
|
|
24
|
+
if output="$("$gates" --stop 2>&1)"; then
|
|
25
|
+
rm -f "$counter"
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
n=$(( $(cat "$counter" 2>/dev/null || echo 0) + 1 ))
|
|
30
|
+
if (( n > max )); then
|
|
31
|
+
rm -f "$counter"
|
|
32
|
+
echo "Stop hook: gates still failing after $max attempts; allowing stop for human review." >&2
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
echo "$n" > "$counter"
|
|
36
|
+
|
|
37
|
+
{
|
|
38
|
+
echo "Definition of Done not met: gates failed (block $n/$max)."
|
|
39
|
+
echo "Fix the failures below, then finish. Do not weaken, skip or delete tests to pass."
|
|
40
|
+
echo
|
|
41
|
+
echo "$output"
|
|
42
|
+
} >&2
|
|
43
|
+
exit 2
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
|
3
|
+
"permissions": {
|
|
4
|
+
"allow": [
|
|
5
|
+
"Bash(.harness/gates.sh:*)",
|
|
6
|
+
"Bash(.harness/status.sh:*)",
|
|
7
|
+
"Bash(.harness/loop.sh:*)",
|
|
8
|
+
"Bash(.harness/spec-pr.sh:*)",
|
|
9
|
+
"Bash(.harness/openspec.sh:*)",
|
|
10
|
+
"Bash(git status:*)",
|
|
11
|
+
"Bash(git diff:*)",
|
|
12
|
+
"Bash(git log:*)",
|
|
13
|
+
"Bash(git show:*)",
|
|
14
|
+
"Bash(openspec list:*)",
|
|
15
|
+
"Bash(openspec show:*)",
|
|
16
|
+
"Bash(openspec validate:*)",
|
|
17
|
+
"Bash(node_modules/.bin/openspec:*)",
|
|
18
|
+
"Bash(gh pr view:*)",
|
|
19
|
+
"Bash(gh pr list:*)",
|
|
20
|
+
"Bash(pnpm test:*)",
|
|
21
|
+
"Bash(pnpm exec jest:*)",
|
|
22
|
+
"Bash(pnpm exec eslint:*)",
|
|
23
|
+
"Bash(pnpm exec tsc:*)",
|
|
24
|
+
"Bash(node_modules/.bin/jest:*)",
|
|
25
|
+
"Bash(node_modules/.bin/eslint:*)",
|
|
26
|
+
"Bash(node_modules/.bin/tsc:*)"
|
|
27
|
+
],
|
|
28
|
+
"deny": [
|
|
29
|
+
"Read(./.env)",
|
|
30
|
+
"Read(./.env.*)",
|
|
31
|
+
"Edit(./.harness/gates.sh)",
|
|
32
|
+
"Edit(./.harness/gates.conf)",
|
|
33
|
+
"Edit(./.claude/settings.json)",
|
|
34
|
+
"Edit(./.claude/hooks/**)",
|
|
35
|
+
"Bash(git push --force:*)",
|
|
36
|
+
"Bash(git push -f:*)",
|
|
37
|
+
"Edit(./.harness/loop.sh)",
|
|
38
|
+
"Edit(./.harness/notify.sh)",
|
|
39
|
+
"Edit(./.harness/run-tasks.sh)",
|
|
40
|
+
"Edit(./.harness/lib.sh)",
|
|
41
|
+
"Edit(./.harness/spec-pr.sh)",
|
|
42
|
+
"Edit(./.harness/openspec.sh)",
|
|
43
|
+
"Edit(./.harness/pr-scope.sh)",
|
|
44
|
+
"Edit(./.harness/deps.sh)",
|
|
45
|
+
"Edit(./.harness/db-smoke.sh)",
|
|
46
|
+
"Edit(./.harness/models.conf)",
|
|
47
|
+
"Edit(./.harness/models.local.conf)",
|
|
48
|
+
"Edit(./.harness/model-flags.sh)",
|
|
49
|
+
"Skill(opsx:apply)",
|
|
50
|
+
"Skill(skill:opsx:apply)",
|
|
51
|
+
"Skill(openspec-apply-change)",
|
|
52
|
+
"Skill(skill:openspec-apply-change)"
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"skillOverrides": {
|
|
56
|
+
"opsx:apply": "off",
|
|
57
|
+
"openspec-apply-change": "off"
|
|
58
|
+
},
|
|
59
|
+
"hooks": {
|
|
60
|
+
"Stop": [
|
|
61
|
+
{
|
|
62
|
+
"hooks": [
|
|
63
|
+
{
|
|
64
|
+
"type": "command",
|
|
65
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/stop-gates.sh",
|
|
66
|
+
"timeout": 900
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Harness bumps (ADR 0021): a weekly PR for the exactly pinned
|
|
2
|
+
# @data-matters.io/agent-harness, with its changelog in the PR body. Other
|
|
3
|
+
# dependencies are not managed by Dependabot here.
|
|
4
|
+
version: 2
|
|
5
|
+
updates:
|
|
6
|
+
- package-ecosystem: npm
|
|
7
|
+
directory: /
|
|
8
|
+
schedule:
|
|
9
|
+
interval: weekly
|
|
10
|
+
allow:
|
|
11
|
+
- dependency-name: '@data-matters.io/agent-harness'
|
|
12
|
+
# pnpm refuses versions younger than its minimum release age unless they are
|
|
13
|
+
# listed in minimumReleaseAgeExclude (pnpm-workspace.yaml); waiting keeps the
|
|
14
|
+
# frozen install in CI green without editing that list on every bump.
|
|
15
|
+
cooldown:
|
|
16
|
+
default-days: 2
|
|
17
|
+
commit-message:
|
|
18
|
+
prefix: chore
|
|
19
|
+
include: scope
|