fullstack-agentic-flow 2.0.0
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/LICENSE +21 -0
- package/README.md +348 -0
- package/dist/src/cli/args.js +74 -0
- package/dist/src/cli/commands/doctor.js +133 -0
- package/dist/src/cli/commands/init.js +103 -0
- package/dist/src/cli/commands/shared.js +20 -0
- package/dist/src/cli/commands/tools.js +21 -0
- package/dist/src/cli/commands/update.js +21 -0
- package/dist/src/cli/main.js +6 -0
- package/dist/src/cli/output.js +11 -0
- package/dist/src/cli/prompter.js +13 -0
- package/dist/src/cli/report.js +13 -0
- package/dist/src/cli/run.js +48 -0
- package/dist/src/domain/canon-mapping.js +26 -0
- package/dist/src/domain/claude-md-mode.js +17 -0
- package/dist/src/domain/enum-parse.js +9 -0
- package/dist/src/domain/errors.js +13 -0
- package/dist/src/domain/pipeline-config.js +58 -0
- package/dist/src/domain/runtime.js +19 -0
- package/dist/src/domain/tool.js +129 -0
- package/dist/src/domain/topology.js +39 -0
- package/dist/src/install/adapters.js +39 -0
- package/dist/src/install/applier.js +150 -0
- package/dist/src/install/detect.js +70 -0
- package/dist/src/install/managed-section.js +35 -0
- package/dist/src/install/manifest.js +63 -0
- package/dist/src/install/planned-file.js +77 -0
- package/dist/src/install/planner.js +139 -0
- package/dist/src/install/rules-preset.js +37 -0
- package/dist/src/install/setup-guide.js +60 -0
- package/dist/src/io/fs.js +39 -0
- package/dist/src/io/paths.js +6 -0
- package/dist/src/io/shell.js +25 -0
- package/dist/src/registry/commands.js +52 -0
- package/dist/src/registry/subagents.js +52 -0
- package/dist/src/registry/tools.js +99 -0
- package/package.json +53 -0
- package/payload/ai-agents/agents/00-bootstrap.md +473 -0
- package/payload/ai-agents/agents/00a-scaffold.md +295 -0
- package/payload/ai-agents/agents/00b-module-scaffold.md +108 -0
- package/payload/ai-agents/agents/01-intake.md +308 -0
- package/payload/ai-agents/agents/01b-impact-analysis.md +274 -0
- package/payload/ai-agents/agents/02a-dependency-map.md +247 -0
- package/payload/ai-agents/agents/02b-backend-contract.md +434 -0
- package/payload/ai-agents/agents/02c-ui-contract.md +367 -0
- package/payload/ai-agents/agents/02d-interface-seam.md +373 -0
- package/payload/ai-agents/agents/03a-backend-sequence.md +233 -0
- package/payload/ai-agents/agents/03b-ui-sequence.md +230 -0
- package/payload/ai-agents/agents/04a-backend-implementer.md +290 -0
- package/payload/ai-agents/agents/04b-ui-implementer.md +302 -0
- package/payload/ai-agents/agents/05-ci-validator.md +125 -0
- package/payload/ai-agents/agents/06-qa-spec.md +249 -0
- package/payload/ai-agents/agents/07-security.md +228 -0
- package/payload/ai-agents/agents/08-performance.md +208 -0
- package/payload/ai-agents/agents/09a-feature-notes.md +156 -0
- package/payload/ai-agents/agents/09b-release.md +111 -0
- package/payload/ai-agents/architecture-context.template.md +554 -0
- package/payload/ai-agents/archive/README.md +22 -0
- package/payload/ai-agents/canon/backend-canon.md +295 -0
- package/payload/ai-agents/canon/frontend-canon.md +47 -0
- package/payload/ai-agents/canon/typed-data.md +158 -0
- package/payload/ai-agents/observations/README.md +11 -0
- package/payload/ai-agents/scripts/check-test-with-change.mjs +103 -0
- package/payload/ai-agents/scripts/check-typed-boundaries.mjs +329 -0
- package/payload/ai-agents/scripts/lib/config.mjs +63 -0
- package/payload/ai-agents/scripts/lib/glob.mjs +40 -0
- package/payload/ai-agents/state/README.md +108 -0
- package/payload/ai-agents/state/current-stage.md +10 -0
- package/payload/ai-agents/templates/README.md +22 -0
- package/payload/ai-agents/templates/ci/github-actions.rules.yml +35 -0
- package/payload/ai-agents/templates/ci/gitlab-ci.rules.yml +30 -0
- package/payload/ai-agents/toolchain.md +214 -0
- package/payload/commands/bootstrap.md +17 -0
- package/payload/commands/contract.md +64 -0
- package/payload/commands/finalize.md +26 -0
- package/payload/commands/impact.md +25 -0
- package/payload/commands/implement.md +58 -0
- package/payload/commands/intake.md +16 -0
- package/payload/commands/perf.md +20 -0
- package/payload/commands/qa.md +15 -0
- package/payload/commands/release.md +22 -0
- package/payload/commands/resume.md +52 -0
- package/payload/commands/scaffold-module.md +20 -0
- package/payload/commands/scaffold.md +35 -0
- package/payload/commands/seam.md +28 -0
- package/payload/commands/security.md +16 -0
- package/payload/commands/sequence.md +22 -0
- package/payload/commands/status.md +37 -0
- package/payload/legacy/v1-checksums.json +197 -0
- package/payload/root/contracts-README.md +19 -0
- package/payload/root/pipeline-section.md +33 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# Backend Canon
|
|
2
|
+
|
|
3
|
+
The one module shape every backend in this pipeline takes, whatever the
|
|
4
|
+
framework. A developer who has read one module in a Laravel repo should be able
|
|
5
|
+
to open a NestJS repo built by this pipeline and find the same things in the
|
|
6
|
+
same places, under the same names.
|
|
7
|
+
|
|
8
|
+
This file is **pipeline-owned**, not repository-owned. `architecture-context.md`
|
|
9
|
+
§5.0 records *how* this repo maps the canon onto its framework, and at which
|
|
10
|
+
strictness. The canon itself does not change per repo; the mapping does.
|
|
11
|
+
|
|
12
|
+
Agents that read this file: 00a scaffold, 00b module scaffold, 00 bootstrap,
|
|
13
|
+
02b backend contract, 03a backend sequence, 04a backend implementer.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. Why a canon
|
|
18
|
+
|
|
19
|
+
Framework conventions answer "where does the framework look for a controller".
|
|
20
|
+
They do not answer "where does the rule that an invoice cannot be edited after
|
|
21
|
+
approval live", and every team answers that differently. Readability across a
|
|
22
|
+
consultancy's portfolio comes from answering it once.
|
|
23
|
+
|
|
24
|
+
The canon is deliberately boring: four layers, one direction of dependency, one
|
|
25
|
+
file per use case, typed data at every boundary. Nothing here is novel. The
|
|
26
|
+
value is that it is the same everywhere.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 2. The four layers
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
{modules-root}/{Module}/
|
|
34
|
+
├── Domain/ what the business is
|
|
35
|
+
├── Application/ what the system does
|
|
36
|
+
├── Infrastructure/ how it is stored and connected
|
|
37
|
+
└── Interface/ how the outside world reaches it
|
|
38
|
+
+ {Module} registration (service provider, Nest module, app config)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| Layer | Contains | May depend on | Must never |
|
|
42
|
+
|-------|----------|---------------|------------|
|
|
43
|
+
| **Domain** | Entities, value objects, enums, domain errors, domain events, repository **interfaces** (ports), pure domain services | Nothing outside Domain. The language standard library. | Import the framework, the ORM (strict mode), HTTP, the queue, the clock or randomness directly (inject them) |
|
|
44
|
+
| **Application** | Use cases (one per action), input Data classes (commands and queries), output Data classes (results), ports for external services, transaction boundaries | Domain | Build HTTP responses, read the request, know about the view, run raw queries |
|
|
45
|
+
| **Infrastructure** | ORM models (strict mode), repository implementations, query objects, migrations, external API adapters, queue jobs, mail/notification adapters | Domain, Application | Contain business rules. A rule found here is a bug in placement |
|
|
46
|
+
| **Interface** | Controllers, GraphQL resolvers, CLI commands, request-to-Data mapping, presenters/resources, view models (MVC), route definitions, policies/guards | Application (and Domain types for reading) | Call repositories or the ORM directly. Contain business rules. Open transactions |
|
|
47
|
+
|
|
48
|
+
**Dependency direction is one way**: Interface → Application → Domain.
|
|
49
|
+
Infrastructure implements ports declared in Domain and Application, and is wired
|
|
50
|
+
in the module registration. Nothing depends on Interface.
|
|
51
|
+
|
|
52
|
+
**The rule that matters most**: *a controller calls exactly one use case.* If a
|
|
53
|
+
controller needs two, there is a use case missing.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 3. Strictness: `strict` or `pragmatic`
|
|
58
|
+
|
|
59
|
+
Chosen once per repository at scaffold time and recorded in
|
|
60
|
+
`architecture-context.md` §5.0. Agents never mix them inside one repo.
|
|
61
|
+
|
|
62
|
+
| | `strict` | `pragmatic` |
|
|
63
|
+
|--|----------|-------------|
|
|
64
|
+
| Domain entities | Plain classes, no ORM | ORM models live in `Domain/Models` and act as entities |
|
|
65
|
+
| Repositories | Interface in Domain, implementation in Infrastructure | Interface in Domain, implementation in Infrastructure (unchanged) |
|
|
66
|
+
| Mapping cost | Entity ↔ ORM model mapper per aggregate | None |
|
|
67
|
+
| When to choose | Complex rules, long-lived core domain, multiple persistence targets | CRUD-heavy modules, active-record frameworks, small team speed |
|
|
68
|
+
| What never relaxes | Typed Data classes at boundaries; one use case per action; controllers never touch persistence | Same |
|
|
69
|
+
|
|
70
|
+
`pragmatic` is the honest default for Laravel and Rails-style frameworks. It
|
|
71
|
+
relaxes exactly one thing, the purity of Domain, and nothing else. If an agent
|
|
72
|
+
finds itself relaxing a second thing under the banner of "pragmatic", that is a
|
|
73
|
+
finding, not a convention.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 4. Naming — no synonyms
|
|
78
|
+
|
|
79
|
+
| Thing | Name | Example |
|
|
80
|
+
|-------|------|---------|
|
|
81
|
+
| Use case | `{Verb}{Noun}` — imperative, business verb | `ApproveInvoice`, `ListBrands`, `ImportHsnCodes` |
|
|
82
|
+
| Use case entry method | `execute` / `handle` (one, chosen per repo, recorded in §5.0) | `ApproveInvoice::execute(ApproveInvoiceData $data): InvoiceResult` |
|
|
83
|
+
| Input Data (write) | `{UseCase}Data` | `CreateBrandData` |
|
|
84
|
+
| Input Data (read filters) | `{Noun}FilterData` | `BrandFilterData` |
|
|
85
|
+
| Output Data | `{Noun}Result` or `{Noun}View` (one, per repo) | `BrandResult` |
|
|
86
|
+
| Repository port | `{Noun}Repository` | `BrandRepository` |
|
|
87
|
+
| Repository implementation | `{Tech}{Noun}Repository` | `EloquentBrandRepository`, `PrismaBrandRepository` |
|
|
88
|
+
| Domain error | `{Noun}{Condition}` + language suffix | `BrandNotFound` + `Exception`/`Error` |
|
|
89
|
+
| Domain event | `{Noun}{PastTenseVerb}` | `InvoiceApproved` |
|
|
90
|
+
| Query object | `{Noun}{Purpose}Query` | `OutstandingBalancesByAgeQuery` |
|
|
91
|
+
| Enum | Singular noun | `BrandStatus` |
|
|
92
|
+
|
|
93
|
+
Repository read methods are fixed across every repo:
|
|
94
|
+
|
|
95
|
+
| Method | Returns | On not found |
|
|
96
|
+
|--------|---------|--------------|
|
|
97
|
+
| `findById(id)` | the entity | throws `{Noun}NotFound` |
|
|
98
|
+
| `findByIdOrNull(id)` | entity or null | null — only when absence is a normal branch |
|
|
99
|
+
| `paginate(filter: {Noun}FilterData)` | typed page of entities | empty page |
|
|
100
|
+
| `listForDropdown()` | typed collection of `OptionData` | empty collection |
|
|
101
|
+
| `existsBy{Field}(value)` | bool | false |
|
|
102
|
+
|
|
103
|
+
A repo may extend this list in §5.3. **New repositories use these names
|
|
104
|
+
exactly.** A repository bootstrapped from existing code may record an
|
|
105
|
+
established spelling as its canonical one in §5.3 — for example
|
|
106
|
+
`getForDropdown` in place of `listForDropdown`, or `getAll(FilterData)` in place
|
|
107
|
+
of `paginate`. That spelling is then the only one allowed in that repo, and
|
|
108
|
+
the canon name becomes a forbidden synonym there. What is never allowed is two
|
|
109
|
+
spellings in one repository.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 5. Typed data at every boundary
|
|
114
|
+
|
|
115
|
+
See `canon/typed-data.md`. In short: **nothing crosses a layer boundary as an
|
|
116
|
+
untyped array, map, dictionary, or `mixed`/`any` value.** Input arrives as a
|
|
117
|
+
Data class, output leaves as a Data class, collections are typed collections of
|
|
118
|
+
Data classes. This is a pipeline-wide rule, not a repo preference, and it
|
|
119
|
+
outranks brevity every time.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 6. Tests mirror the layers
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
tests/
|
|
127
|
+
├── Unit/{Module}/Domain/ pure, no framework boot, no database
|
|
128
|
+
├── Unit/{Module}/Application/ use cases with in-memory fakes of the ports
|
|
129
|
+
├── Integration/{Module}/Infrastructure/ repositories against a real database
|
|
130
|
+
└── Feature/{Module}/Interface/ HTTP / GraphQL / page tests end to end
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
(Or co-located `*.spec.ts` beside the file, when the framework's convention is
|
|
134
|
+
co-location — recorded in §9. The *levels* are fixed; the *location* is mapped.)
|
|
135
|
+
|
|
136
|
+
Every use case ships with an in-memory fake for each port it uses, in
|
|
137
|
+
`tests/Support/Fakes/{Module}/`. Fakes are what make the Application layer
|
|
138
|
+
testable in milliseconds, which is what makes TDD tolerable at scale.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 7. Framework mappings
|
|
143
|
+
|
|
144
|
+
`00a` and `00b` generate from these. `00` bootstrap verifies the repo still
|
|
145
|
+
matches the mapping recorded in §5.0.
|
|
146
|
+
|
|
147
|
+
### 7.1 Laravel (PHP 8.3+)
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
app/Modules/{Module}/
|
|
151
|
+
├── {Module}ServiceProvider.php binds ports → implementations, loads routes + migrations
|
|
152
|
+
├── Domain/
|
|
153
|
+
│ ├── Models/ pragmatic only: Eloquent models
|
|
154
|
+
│ ├── Entities/ strict only: plain PHP classes
|
|
155
|
+
│ ├── ValueObjects/
|
|
156
|
+
│ ├── Enums/ backed enums
|
|
157
|
+
│ ├── Events/
|
|
158
|
+
│ ├── Exceptions/
|
|
159
|
+
│ └── Contracts/ repository + service ports (interfaces)
|
|
160
|
+
├── Application/
|
|
161
|
+
│ ├── UseCases/ ApproveInvoice.php — one public method
|
|
162
|
+
│ ├── Data/ Spatie Data (or readonly classes): *Data, *FilterData, *Result
|
|
163
|
+
│ └── Contracts/ ports to external services
|
|
164
|
+
├── Infrastructure/
|
|
165
|
+
│ ├── Persistence/
|
|
166
|
+
│ │ ├── Repositories/ EloquentBrandRepository.php
|
|
167
|
+
│ │ ├── Queries/ raw/aggregate query objects
|
|
168
|
+
│ │ ├── Scopes/
|
|
169
|
+
│ │ └── Migrations/
|
|
170
|
+
│ ├── Jobs/
|
|
171
|
+
│ ├── Excel/ import/export adapters, if any
|
|
172
|
+
│ └── External/ HTTP clients, SDK adapters
|
|
173
|
+
└── Interface/
|
|
174
|
+
├── Http/
|
|
175
|
+
│ ├── Controllers/ thin: map request → Data → use case → response
|
|
176
|
+
│ ├── Resources/ JSON output shaping (API topologies)
|
|
177
|
+
│ ├── ViewModels/ Inertia/Blade page props as Data (MVC topology)
|
|
178
|
+
│ └── routes.php
|
|
179
|
+
├── Policies/
|
|
180
|
+
└── Console/
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
- Input validation: Spatie Laravel Data (`rules()` on the Data class), injected
|
|
184
|
+
as the controller method parameter. Never `$request->validate()`, never
|
|
185
|
+
`$request->all()` past the controller.
|
|
186
|
+
- Collections: `DataCollection<BrandResult>` / `Collection<int, Brand>` with
|
|
187
|
+
generics declared in docblocks; PHPStan checks them.
|
|
188
|
+
- Static analysis: Larastan at the level recorded in §3, with iterable value
|
|
189
|
+
types required.
|
|
190
|
+
- Tests: Pest. Unit tests extend a base that does **not** boot the app.
|
|
191
|
+
|
|
192
|
+
### 7.2 NestJS (TypeScript strict)
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
src/modules/{module}/
|
|
196
|
+
├── {module}.module.ts providers: port tokens → implementations
|
|
197
|
+
├── domain/
|
|
198
|
+
│ ├── entities/ plain classes (strict) or ORM entities (pragmatic)
|
|
199
|
+
│ ├── value-objects/
|
|
200
|
+
│ ├── enums/
|
|
201
|
+
│ ├── events/
|
|
202
|
+
│ ├── errors/
|
|
203
|
+
│ └── ports/ brand.repository.ts (interface + injection token)
|
|
204
|
+
├── application/
|
|
205
|
+
│ ├── use-cases/ approve-invoice.use-case.ts — one class, one execute()
|
|
206
|
+
│ ├── data/ *.data.ts, *.filter.ts, *.result.ts — classes, readonly fields
|
|
207
|
+
│ └── ports/
|
|
208
|
+
├── infrastructure/
|
|
209
|
+
│ ├── persistence/
|
|
210
|
+
│ │ ├── repositories/ prisma-brand.repository.ts / typeorm-…
|
|
211
|
+
│ │ ├── queries/
|
|
212
|
+
│ │ └── migrations/ or the ORM's migration dir, mapped in §4
|
|
213
|
+
│ ├── jobs/ BullMQ processors
|
|
214
|
+
│ └── external/
|
|
215
|
+
└── interface/
|
|
216
|
+
├── http/
|
|
217
|
+
│ ├── controllers/
|
|
218
|
+
│ ├── dto/ class-validator request DTOs → mapped to application Data
|
|
219
|
+
│ └── presenters/
|
|
220
|
+
├── graphql/ resolvers + object types (if GraphQL)
|
|
221
|
+
└── guards/
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
- Request DTOs (class-validator) live in Interface and are **mapped** to
|
|
225
|
+
Application Data classes. Application never imports `class-validator`
|
|
226
|
+
decorators — that is a framework concern leaking inward.
|
|
227
|
+
- `strict: true`, `noUncheckedIndexedAccess: true`, `exactOptionalPropertyTypes: true`.
|
|
228
|
+
- ESLint: `@typescript-eslint/no-explicit-any: error`, `no-unsafe-*: error`.
|
|
229
|
+
- Tests: Jest or Vitest per repo; `*.spec.ts` co-located, `test/` for e2e.
|
|
230
|
+
|
|
231
|
+
### 7.3 Express / Fastify / Hono (TypeScript)
|
|
232
|
+
|
|
233
|
+
Same tree as NestJS without the module file. Wiring happens in
|
|
234
|
+
`src/modules/{module}/index.ts`, which exports a `register{Module}(app, deps)`
|
|
235
|
+
function. Validation with Zod at Interface; `z.infer` types are **not** used as
|
|
236
|
+
Application Data — map them to classes or branded readonly types so the
|
|
237
|
+
Application layer does not depend on the validator.
|
|
238
|
+
|
|
239
|
+
### 7.4 Django / FastAPI (Python 3.12+)
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
{project}/modules/{module}/
|
|
243
|
+
├── domain/ dataclasses(frozen=True, slots=True), Enum, exceptions, Protocol ports
|
|
244
|
+
├── application/ use_cases/, data/ (dataclasses or pydantic models — one choice per repo)
|
|
245
|
+
├── infrastructure/ persistence/ (Django models or SQLAlchemy), repositories/, tasks/
|
|
246
|
+
└── interface/ http/ (views / routers), serializers or request schemas, urls.py
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
`mypy --strict` or `pyright strict`. `dict[str, Any]` crossing a layer is the
|
|
250
|
+
Python spelling of an untyped array and is forbidden the same way.
|
|
251
|
+
|
|
252
|
+
### 7.5 Spring Boot (Java 21 / Kotlin)
|
|
253
|
+
|
|
254
|
+
Package-by-module, then by layer: `com.acme.{module}.{domain|application|infrastructure|interface}`.
|
|
255
|
+
Records for Data classes. ArchUnit test enforces the dependency direction.
|
|
256
|
+
|
|
257
|
+
### 7.6 Go
|
|
258
|
+
|
|
259
|
+
`internal/{module}/{domain,app,store,transport}`. Go's naming replaces
|
|
260
|
+
Application/Infrastructure/Interface with `app`/`store`/`transport`; the
|
|
261
|
+
mapping table in §5.0 records the equivalence. Structs for Data, no
|
|
262
|
+
`map[string]any` across package boundaries.
|
|
263
|
+
|
|
264
|
+
### 7.7 A framework not listed
|
|
265
|
+
|
|
266
|
+
`00a` proposes a mapping using the table in §2 as the invariant and the
|
|
267
|
+
framework's own conventions for everything else, records it in
|
|
268
|
+
`scaffold-decisions.md` as **defaulted**, and asks the developer to confirm it
|
|
269
|
+
before the first commit. `find-skills` may be used to look for an established
|
|
270
|
+
skill for that framework; any skill adopted is recorded in `toolchain.md`
|
|
271
|
+
§Project additions.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## 8. MVC topology specifics
|
|
276
|
+
|
|
277
|
+
In an MVC repository the Interface layer also owns **view models**: one Data
|
|
278
|
+
class per screen, holding exactly what the template or page component receives.
|
|
279
|
+
|
|
280
|
+
- Controllers return `render(view, ViewModel)`. Never an array of props.
|
|
281
|
+
- The view model is the seam between backend and UI tracks (see
|
|
282
|
+
`agents/02d-interface-seam.md`, mode `page-contract`).
|
|
283
|
+
- Form error shape and flash messages are defined once in §7 and reused.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## 9. What the canon does not decide
|
|
288
|
+
|
|
289
|
+
Deliberately left to the repo, recorded in the architecture context:
|
|
290
|
+
|
|
291
|
+
- The ORM, the validator library, the test runner
|
|
292
|
+
- Co-located vs mirrored tests
|
|
293
|
+
- `execute` vs `handle`; `Result` vs `View`
|
|
294
|
+
- Whether events are synchronous or queued
|
|
295
|
+
- Frontend structure (see `canon/frontend-canon.md`, which is lighter by design)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Frontend Canon
|
|
2
|
+
|
|
3
|
+
Lighter than the backend canon on purpose. Frontend frameworks disagree more
|
|
4
|
+
about structure than backend frameworks do, and the component inventory (§8.3)
|
|
5
|
+
and design tokens (§8.4) already carry most of the consistency. This file fixes
|
|
6
|
+
only the things that stay true across React, Vue, Svelte, Angular, Blade,
|
|
7
|
+
Livewire, and mobile.
|
|
8
|
+
|
|
9
|
+
## 1. Feature folders over type folders
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
{ui-root}/
|
|
13
|
+
├── app/ or pages/ routing entry points — thin: fetch/receive data, compose a feature
|
|
14
|
+
├── features/{feature}/
|
|
15
|
+
│ ├── api/ typed client calls for this feature (generated or hand-written against the seam)
|
|
16
|
+
│ ├── model/ types, state, derived data — named types only (typed-data T1)
|
|
17
|
+
│ └── ui/ feature-local components, composed from the shared inventory
|
|
18
|
+
├── shared/
|
|
19
|
+
│ ├── ui/ the approved component inventory (§8.3) — nothing else lives here
|
|
20
|
+
│ ├── lib/ framework-agnostic helpers
|
|
21
|
+
│ └── api/ the HTTP/GraphQL client, error normalisation
|
|
22
|
+
└── styles/ or theme/ design tokens (§8.4) — or the path DESIGN.md points to
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
A repo maps these names onto its framework in `architecture-context.md` §8.0.
|
|
26
|
+
MVC repos with server templates map `features/{feature}/ui` onto the
|
|
27
|
+
template directory for that module, and `api/` onto the view-model types.
|
|
28
|
+
|
|
29
|
+
## 2. Rules that hold everywhere
|
|
30
|
+
|
|
31
|
+
1. Pages/routes never call `fetch` directly — they go through `features/*/api`.
|
|
32
|
+
2. Response bodies are parsed into named types at the edge (typed-data T5).
|
|
33
|
+
3. `shared/ui` grows only through Gate 1 approval.
|
|
34
|
+
4. Tokens, never literal values.
|
|
35
|
+
5. Every data-bound region implements the required states (§8.5).
|
|
36
|
+
|
|
37
|
+
## 3. Topology notes
|
|
38
|
+
|
|
39
|
+
- **mvc**: the "API" is the view model the controller passes. Types for it are
|
|
40
|
+
generated or hand-mirrored from the backend ViewModel Data classes; the seam
|
|
41
|
+
document is the source of truth for both.
|
|
42
|
+
- **unified-api**: types are generated from the backend's schema (OpenAPI,
|
|
43
|
+
GraphQL SDL, tRPC router) where the repo supports it; otherwise mirrored by
|
|
44
|
+
hand from the seam and checked in the integration task.
|
|
45
|
+
- **split-frontend**: types come from the imported seam in
|
|
46
|
+
`contracts/{slug}/` (see `agents/02d-interface-seam.md`). The frontend repo
|
|
47
|
+
never edits an imported contract.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Typed Data — pipeline-wide rule
|
|
2
|
+
|
|
3
|
+
**Nothing crosses a boundary as an untyped array, map, dictionary, or
|
|
4
|
+
`mixed`/`any` value. Every shape that moves between layers, modules, processes,
|
|
5
|
+
or the network has a named type. Even when that means many small Data classes.**
|
|
6
|
+
|
|
7
|
+
This is a pipeline rule, installed into every repository's §10 as rules
|
|
8
|
+
`T1`–`T6`. A repo may tighten it. It may not relax it without a recorded Gate 1
|
|
9
|
+
decision naming the exact file and reason.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Why
|
|
14
|
+
|
|
15
|
+
Arrays are the fastest way to write the first version and the slowest way to
|
|
16
|
+
change the tenth. A `$filters` array accumulates keys nobody documented; a
|
|
17
|
+
`Record<string, unknown>` gets a third optional field that half the callers set.
|
|
18
|
+
Each iteration is individually reasonable and collectively unreadable. A Data
|
|
19
|
+
class makes the shape a thing you can find, rename, and let the type checker
|
|
20
|
+
chase through the codebase for you.
|
|
21
|
+
|
|
22
|
+
For agents specifically: a named type is the cheapest possible context. An
|
|
23
|
+
implementer that sees `paginate(BrandFilterData $filter)` knows every legal
|
|
24
|
+
filter from one file. One that sees `paginate(array $filters)` has to read every
|
|
25
|
+
call site and guess.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## The rules
|
|
30
|
+
|
|
31
|
+
| # | Rule | Checked by |
|
|
32
|
+
|---|------|------------|
|
|
33
|
+
| T1 | Layer boundaries (controller→use case, use case→repository, module→module, job payloads, events) accept and return **named types only**. No `array`, `mixed`, `any`, `object`, `unknown`, `dict`, `Record<…>`, `Map<string, …>` in those signatures. | Static analysis + `scripts/check-typed-boundaries.mjs` on the diff |
|
|
34
|
+
| T2 | Collections are **typed collections of named types**: `list<BrandResult>`, `DataCollection<BrandResult>`, `ReadonlyArray<BrandResult>`, `list[BrandResult]`. A bare `array` / `any[]` is never a return type. | Static analysis (iterable value types required) |
|
|
35
|
+
| T3 | Fixed sets of values are **enums**, not strings. Status, type, kind, role, direction. | Review |
|
|
36
|
+
| T4 | Optional is explicit: nullable types or dedicated variants — never "key may be missing". | Type checker (`exactOptionalPropertyTypes`, PHPStan) |
|
|
37
|
+
| T5 | Untyped data is allowed **only at the parse edge**: the line that reads JSON, a request body, a CSV row, or an env var — and it is converted to a named type **on the next line**. | Review |
|
|
38
|
+
| T6 | Money, quantities, identifiers, and dates are value objects or branded types when they carry rules (currency, precision, format). A `float` is never money. | Review |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## What "boundary" means
|
|
43
|
+
|
|
44
|
+
Inside a single private method, a local array used as a scratch list is fine.
|
|
45
|
+
The rule applies where one piece of code hands data to another that was written
|
|
46
|
+
at a different time by a different mind:
|
|
47
|
+
|
|
48
|
+
- Public and protected method signatures in Domain, Application, Infrastructure
|
|
49
|
+
- Controller → use case, use case → repository, use case → port
|
|
50
|
+
- Anything serialised: API responses, job payloads, events, cache entries,
|
|
51
|
+
view models, exports
|
|
52
|
+
- Configuration read at runtime
|
|
53
|
+
- Test fixtures shared across more than one test file
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Spelling it per language
|
|
58
|
+
|
|
59
|
+
### PHP (Laravel)
|
|
60
|
+
|
|
61
|
+
```php
|
|
62
|
+
// Wrong
|
|
63
|
+
public function getAll(array $filters): LengthAwarePaginator
|
|
64
|
+
public function getForDropdown(): array
|
|
65
|
+
|
|
66
|
+
// Right
|
|
67
|
+
public function paginate(BrandFilterData $filter): LengthAwarePaginator // @return LengthAwarePaginator<int, Brand>
|
|
68
|
+
public function listForDropdown(): DataCollection // @return DataCollection<int, OptionData>
|
|
69
|
+
|
|
70
|
+
final class BrandFilterData extends Data
|
|
71
|
+
{
|
|
72
|
+
public function __construct(
|
|
73
|
+
public readonly ?string $search = null,
|
|
74
|
+
public readonly ?BrandStatus $status = null,
|
|
75
|
+
public readonly int $perPage = 15,
|
|
76
|
+
) {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
final class OptionData extends Data
|
|
80
|
+
{
|
|
81
|
+
public function __construct(
|
|
82
|
+
public readonly int $id,
|
|
83
|
+
public readonly string $label,
|
|
84
|
+
) {}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Analytics that previously returned `['labels' => [...], 'series' => [...]]`:
|
|
89
|
+
|
|
90
|
+
```php
|
|
91
|
+
final class TimeSeriesData extends Data
|
|
92
|
+
{
|
|
93
|
+
/** @param list<string> $labels @param list<float> $series */
|
|
94
|
+
public function __construct(
|
|
95
|
+
public readonly array $labels, // typed via docblock; the class is the boundary type
|
|
96
|
+
public readonly array $series,
|
|
97
|
+
) {}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Note the last case: a PHP `array` property **inside** a Data class with a
|
|
102
|
+
precise `list<T>` docblock is acceptable — the boundary type is `TimeSeriesData`,
|
|
103
|
+
and PHPStan enforces the element type. A bare `array` in a signature is not.
|
|
104
|
+
|
|
105
|
+
Tooling: Larastan/PHPStan level ≥ 6 (iterable value types required),
|
|
106
|
+
`phpstan-strict-rules`, `spatie/laravel-data`.
|
|
107
|
+
|
|
108
|
+
### TypeScript (NestJS, Express, any frontend)
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// Wrong
|
|
112
|
+
async list(filters: Record<string, unknown>): Promise<any[]>
|
|
113
|
+
|
|
114
|
+
// Right
|
|
115
|
+
export class BrandFilter {
|
|
116
|
+
constructor(
|
|
117
|
+
readonly search: string | null,
|
|
118
|
+
readonly status: BrandStatus | null,
|
|
119
|
+
readonly perPage: number,
|
|
120
|
+
) {}
|
|
121
|
+
}
|
|
122
|
+
async paginate(filter: BrandFilter): Promise<Page<BrandResult>>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Frontend: every API response has a named type generated from, or checked
|
|
126
|
+
against, the frozen seam. `as` casts on response bodies are a T5 violation — parse
|
|
127
|
+
(Zod, Valibot, or a generated client) at the edge instead.
|
|
128
|
+
|
|
129
|
+
Tooling: `strict`, `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`,
|
|
130
|
+
`@typescript-eslint/no-explicit-any`, `no-unsafe-assignment`,
|
|
131
|
+
`no-unsafe-member-access`, `no-unsafe-return`.
|
|
132
|
+
|
|
133
|
+
### Python
|
|
134
|
+
|
|
135
|
+
`@dataclass(frozen=True, slots=True)` or pydantic models; `Enum`/`StrEnum`;
|
|
136
|
+
`mypy --strict`. `dict[str, Any]` in a signature is the Python spelling of an
|
|
137
|
+
untyped array.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## How agents apply it
|
|
142
|
+
|
|
143
|
+
- **02b backend contract** names every Data class in §3 and §5, with fields and
|
|
144
|
+
types. "Accepts filters" is not a specification; `BrandFilterData { search:
|
|
145
|
+
?string, status: ?BrandStatus, perPage: int = 15 }` is.
|
|
146
|
+
- **02c UI contract** names the frontend types it consumes; **02d seam** freezes
|
|
147
|
+
them, field by field.
|
|
148
|
+
- **04a / 04b implementers** create the Data classes the contract names, in the
|
|
149
|
+
task that first needs them. Creating an extra one the contract did not name is
|
|
150
|
+
fine when it removes an array from a boundary — record it in the decisions log.
|
|
151
|
+
- **Gate 2** reviewers look for arrays in signatures before anything else.
|
|
152
|
+
|
|
153
|
+
## Interaction with ponytail
|
|
154
|
+
|
|
155
|
+
Ponytail pushes toward writing less code. That applies to **logic, abstractions,
|
|
156
|
+
and dependencies** — not to type declarations. A Data class with four readonly
|
|
157
|
+
fields is not over-engineering; it is the rule. When ponytail's review flags a
|
|
158
|
+
Data class as unnecessary, T1 wins. See `toolchain.md` §Precedence.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Observations
|
|
2
|
+
|
|
3
|
+
Written by the task-observer skill while agents work: repeated corrections,
|
|
4
|
+
preferences, and gaps it notices in the agent instructions or skills.
|
|
5
|
+
|
|
6
|
+
Nothing here changes the pipeline by itself. At release, agent 09b reads the
|
|
7
|
+
logs written while those features were built, groups recurring ones, and writes
|
|
8
|
+
`proposals-{version}.md` — specific edits to specific files — for a human to
|
|
9
|
+
accept or reject. Processed logs move to `processed/`.
|
|
10
|
+
|
|
11
|
+
Commit this directory. The patterns in it are team knowledge.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
/**
|
|
4
|
+
* Rule TDD-1: a commit that changes source under the configured paths must add
|
|
5
|
+
* or change a test in the same commit — or say why not with `[no-test: reason]`
|
|
6
|
+
* in its message. It cannot prove the test came first (tdd-log.md and Gate 2
|
|
7
|
+
* do that); it proves the test exists, which is the part CI can check honestly.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node .ai-agents/scripts/check-test-with-change.mjs --base origin/main # every commit in base..HEAD
|
|
11
|
+
* node .ai-agents/scripts/check-test-with-change.mjs --staged # staged files, before commit
|
|
12
|
+
*/
|
|
13
|
+
import { execFileSync } from 'node:child_process';
|
|
14
|
+
import { pathToFileURL } from 'node:url';
|
|
15
|
+
|
|
16
|
+
import { matchesAny } from './lib/glob.mjs';
|
|
17
|
+
import { loadRulesConfig } from './lib/config.mjs';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} CommitFacts
|
|
21
|
+
* @property {string} sha
|
|
22
|
+
* @property {string} message
|
|
23
|
+
* @property {ReadonlyArray<string>} files
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {object} TddViolation
|
|
28
|
+
* @property {'TDD-1'} rule
|
|
29
|
+
* @property {string} sha
|
|
30
|
+
* @property {ReadonlyArray<string>} sourceFiles
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {CommitFacts} commit
|
|
35
|
+
* @param {import('./lib/config.mjs').TestWithChangeConfig} config
|
|
36
|
+
* @returns {TddViolation | null}
|
|
37
|
+
*/
|
|
38
|
+
export function evaluateCommit(commit, config) {
|
|
39
|
+
const tests = commit.files.filter((file) => matchesAny(file, config.testPaths));
|
|
40
|
+
const sources = commit.files.filter(
|
|
41
|
+
(file) => matchesAny(file, config.sourcePaths) && !matchesAny(file, config.testPaths),
|
|
42
|
+
);
|
|
43
|
+
if (sources.length === 0 || tests.length > 0) return null;
|
|
44
|
+
const exemption = new RegExp(`\\[${config.exemptionTag}:\\s*[^\\]\\s][^\\]]*\\]`);
|
|
45
|
+
if (exemption.test(commit.message)) return null;
|
|
46
|
+
return { rule: 'TDD-1', sha: commit.sha, sourceFiles: sources };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {ReadonlyArray<string>} args
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
function git(args) {
|
|
54
|
+
return execFileSync('git', args, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }).trim();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {ReadonlyArray<string>} argv
|
|
59
|
+
* @returns {number}
|
|
60
|
+
*/
|
|
61
|
+
export function main(argv) {
|
|
62
|
+
const config = loadRulesConfig(process.cwd()).testWithChange;
|
|
63
|
+
if (config.sourcePaths.length === 0) {
|
|
64
|
+
console.log('TDD-1 test-with-change: not configured (testWithChange.sourcePaths is empty) — skipping.');
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @type {Array<CommitFacts>} */
|
|
69
|
+
const commits = [];
|
|
70
|
+
if (argv.includes('--staged')) {
|
|
71
|
+
const files = git(['diff', '--cached', '--name-only']).split('\n').filter(Boolean);
|
|
72
|
+
commits.push({ sha: 'staged', message: '', files });
|
|
73
|
+
} else {
|
|
74
|
+
const baseIndex = argv.indexOf('--base');
|
|
75
|
+
const base = baseIndex >= 0 ? argv[baseIndex + 1] ?? 'origin/main' : 'origin/main';
|
|
76
|
+
const shas = git(['rev-list', '--no-merges', `${base}..HEAD`]).split('\n').filter(Boolean);
|
|
77
|
+
for (const sha of shas) {
|
|
78
|
+
commits.push({
|
|
79
|
+
sha,
|
|
80
|
+
message: git(['log', '-1', '--format=%B', sha]),
|
|
81
|
+
files: git(['show', '--name-only', '--format=', sha]).split('\n').filter(Boolean),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const violations = commits
|
|
87
|
+
.map((commit) => evaluateCommit(commit, config))
|
|
88
|
+
.filter(/** @returns {v is TddViolation} */ (v) => v !== null);
|
|
89
|
+
|
|
90
|
+
for (const v of violations) {
|
|
91
|
+
console.log(
|
|
92
|
+
`${v.rule} ${v.sha.slice(0, 8)} — source changed with no test change: ${v.sourceFiles.join(', ')}. ` +
|
|
93
|
+
`Add the test, or explain with [${config.exemptionTag}: reason] in the commit message.`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
console.log(`TDD-1 test-with-change: ${violations.length} violation(s) in ${commits.length} commit(s).`);
|
|
97
|
+
return violations.length === 0 ? 0 : 1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const invoked = process.argv[1];
|
|
101
|
+
if (invoked !== undefined && import.meta.url === pathToFileURL(invoked).href) {
|
|
102
|
+
process.exit(main(process.argv.slice(2)));
|
|
103
|
+
}
|