@pattern-stack/codegen 0.9.2 → 0.10.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +5 -0
  3. package/consumer-skills/bridge/SKILL.md +265 -0
  4. package/consumer-skills/codegen/SKILL.md +115 -0
  5. package/consumer-skills/entities/SKILL.md +111 -0
  6. package/consumer-skills/entities/families-and-queries.md +82 -0
  7. package/consumer-skills/entities/yaml-reference.md +118 -0
  8. package/consumer-skills/events/SKILL.md +71 -0
  9. package/consumer-skills/events/authoring-events.md +164 -0
  10. package/consumer-skills/events/typed-bus-and-outbox.md +163 -0
  11. package/consumer-skills/jobs/SKILL.md +66 -0
  12. package/consumer-skills/jobs/handler-authoring.md +236 -0
  13. package/consumer-skills/jobs/pools-and-ordering.md +161 -0
  14. package/consumer-skills/subsystems/SKILL.md +161 -0
  15. package/consumer-skills/subsystems/wiring-and-order.md +120 -0
  16. package/consumer-skills/sync/SKILL.md +134 -0
  17. package/consumer-skills/sync/audit-and-detection.md +302 -0
  18. package/consumer-skills/sync/change-sources-and-sinks.md +442 -0
  19. package/dist/runtime/subsystems/bridge/bridge.module.d.ts +0 -1
  20. package/dist/runtime/subsystems/bridge/bridge.module.js +294 -710
  21. package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
  22. package/dist/runtime/subsystems/bridge/index.d.ts +0 -1
  23. package/dist/runtime/subsystems/bridge/index.js +248 -664
  24. package/dist/runtime/subsystems/bridge/index.js.map +1 -1
  25. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +18 -10
  26. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js.map +1 -1
  27. package/dist/runtime/subsystems/events/events.module.js +43 -244
  28. package/dist/runtime/subsystems/events/events.module.js.map +1 -1
  29. package/dist/runtime/subsystems/events/index.d.ts +0 -1
  30. package/dist/runtime/subsystems/events/index.js +39 -241
  31. package/dist/runtime/subsystems/events/index.js.map +1 -1
  32. package/dist/runtime/subsystems/index.js +174 -791
  33. package/dist/runtime/subsystems/index.js.map +1 -1
  34. package/dist/runtime/subsystems/jobs/bullmq.config.d.ts +22 -3
  35. package/dist/runtime/subsystems/jobs/bullmq.config.js.map +1 -1
  36. package/dist/runtime/subsystems/jobs/index.d.ts +1 -4
  37. package/dist/runtime/subsystems/jobs/index.js +87 -506
  38. package/dist/runtime/subsystems/jobs/index.js.map +1 -1
  39. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -1
  40. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +3 -0
  41. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js.map +1 -1
  42. package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +11 -4
  43. package/dist/runtime/subsystems/jobs/job-worker.module.js +248 -664
  44. package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
  45. package/dist/runtime/subsystems/jobs/jobs-domain.module.d.ts +0 -1
  46. package/dist/runtime/subsystems/jobs/jobs-domain.module.js +89 -391
  47. package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
  48. package/dist/src/cli/index.js +1065 -440
  49. package/dist/src/cli/index.js.map +1 -1
  50. package/dist/src/index.js +26 -4
  51. package/dist/src/index.js.map +1 -1
  52. package/package.json +2 -1
  53. package/runtime/subsystems/events/event-bus.drizzle-backend.ts +32 -10
  54. package/runtime/subsystems/events/events.module.ts +38 -6
  55. package/runtime/subsystems/events/index.ts +7 -1
  56. package/runtime/subsystems/jobs/bullmq.config.ts +23 -3
  57. package/runtime/subsystems/jobs/index.ts +13 -8
  58. package/runtime/subsystems/jobs/job-worker.bullmq-backend.ts +5 -2
  59. package/runtime/subsystems/jobs/job-worker.module.ts +27 -7
  60. package/runtime/subsystems/jobs/jobs-domain.module.ts +27 -2
  61. package/templates/subsystem/events/domain-events.schema.ejs.t +43 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,79 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.10.1] — 2026-05-28
8
+
9
+ Dogfood fixes found wiring `@pattern-stack/codegen@0.10.0` into a second
10
+ consumer (swe-brain): the type-check blockers that forced consumers to exclude
11
+ the vendored subsystem tree from `tsc` are gone. A drizzle-only install now
12
+ type-checks its full tree (`src/shared/subsystems/**` included) with no
13
+ `ioredis`/`bullmq` peer deps.
14
+
15
+ ### Fixed
16
+
17
+ - **`fix(subsystems)` — detection + barrel emission key on `<name>.module.ts`
18
+ (#4, #2).** Installing one subsystem can vendor *protocol stubs* of another
19
+ (e.g. events vendors `bridge/bridge.protocol.ts`); detection used to report
20
+ those stub-only dirs as `installed` and the barrel emitted a phantom
21
+ `BridgeModule` import for a module that was never installed (TS2307).
22
+ Detection now requires the module file; `subsystem list` reports `incomplete`
23
+ for stub-only dirs; the barrel skips them.
24
+ - **`fix(events)` — drizzle backend type-checks against its paired schema
25
+ (#3).** `event-bus.drizzle-backend.ts` read a `tier` column the schema never
26
+ emitted and `tenant_id` columns only present under multi-tenancy. `tier` is
27
+ now always emitted; `tenant_id` access is gated behind `multiTenant`, so the
28
+ backend type-checks under any configuration.
29
+ - **`fix(subsystems)` — installs no longer vendor unselected backends (#6).** A
30
+ `--backend drizzle` install previously vendored the Redis and BullMQ backend
31
+ sources too, dragging `ioredis`/`bullmq` (uninstalled optional peers) into the
32
+ consumer's type-check. The copy filter now prunes `*.redis-backend.ts` /
33
+ `*.bullmq-backend.ts` for non-matching installs; modules lazy-load the chosen
34
+ backend via a non-literal dynamic import; backend-specific classes are no
35
+ longer re-exported from the public barrels; `bullmq.config.ts` is kept on
36
+ every install (peer-dep-free) for its static token references; the BullMQ
37
+ backend is `noImplicitAny`-clean.
38
+ - **`fix(barrel)` — empty-composer output emits the `DynamicModule` import.** A
39
+ generated `subsystems.ts` with no composer calls referenced `DynamicModule`
40
+ without importing it (latent since BULLMQ-1, surfaced by the stricter
41
+ detection above).
42
+
43
+ ### Added
44
+
45
+ - **`feat(cli)` — `codegen subsystem remove` (#5, #7).** Real implementation:
46
+ deletes the vendored subsystem dir, regenerates the barrel, git-safety gated
47
+ with `--force`, and `--yes`/`-y` parity with `install`. Prints the manual
48
+ follow-ups it deliberately does *not* perform (config-block strip,
49
+ `forRoot` un-registration).
50
+ - **`test(smoke)` — `run-smoke-subsystems.ts`.** Exercises an events + jobs +
51
+ bridge drizzle install with a full-tree `tsc` (no subsystem excludes) + a
52
+ programmatic NestJS boot that validates the bridge reserved-pool dependency
53
+ graph. Wired into `just test-all`.
54
+
55
+ ## [0.10.0] — 2026-05-27
56
+
57
+ ### Added
58
+
59
+ - **`feat(cli)` — consumer skill distribution (ADR-035).** A curated
60
+ `consumer-skills/` set (a `codegen` router plus `entities`, `subsystems`,
61
+ `jobs`, `events`, `bridge`, `sync`) is vendored into a consumer's
62
+ `.claude/skills/` via a new `skills` noun (`codegen skills install` / `list`),
63
+ and by `codegen init` by default (`--no-skills` to opt out). Authored fresh
64
+ for a consumer audience; shipped in the npm `files` array.
65
+ - **`feat(cli)` — `codegen update`.** Re-syncs the vendored runtime closure,
66
+ installed subsystems' runtime, and consumer skills to the installed package
67
+ version after a bump. Drift-aware, git-clean gated (`--force`), `--dry-run`;
68
+ never touches consumer-owned files (config, `app.module.ts`, barrels).
69
+ - **`feat(parser)` — recursive YAML discovery.** Entity / relationship /
70
+ junction / event discovery routes through a single `findYamlFiles` helper;
71
+ domain-folder layouts (`entities/crm/account.yaml`) are first-class.
72
+
73
+ ### Changed
74
+
75
+ - **Docs split.** `CONSUMER-SETUP.md` became a hub; the per-subsystem deep dives
76
+ moved to `docs/consumer/{events,bridge,sync,auth,openapi}.md` (progressive
77
+ disclosure), with jobs-API drift (`JobsModule` → `JobsDomainModule` +
78
+ `JobWorkerModule`, `JobHandlerBase`, `concurrency: { key }`) corrected.
79
+
7
80
  ## [0.9.0] — 2026-05-25
8
81
 
9
82
  Bundles four merged PRs (none carried a version bump): the BullMQ backend and
package/README.md CHANGED
@@ -193,6 +193,11 @@ detection:
193
193
 
194
194
  `codegen.config.yaml` in your project root:
195
195
 
196
+ Definition directories (`entities_dir`, `events_dir`) are discovered
197
+ recursively — a flat `entities/contact.yaml` and a domain-foldered
198
+ `entities/crm/contact.yaml` are both picked up. Group definitions into
199
+ per-domain subfolders freely; codegen walks the whole tree.
200
+
196
201
  ```yaml
197
202
  paths:
198
203
  backend_src: src
@@ -0,0 +1,265 @@
1
+ ---
2
+ name: bridge
3
+ description: Load when wiring the event-to-job bridge or authoring `@JobHandler.triggers` in a project that ran `codegen subsystem install bridge`. Triggers include declaring `triggers:` on a `@JobHandler`; deciding between an in-process subscriber, `eventFlow.publishAndStart`, and a bridge trigger; registering `BridgeModule.forRoot()` in `app.module.ts`; wiring the reserved `events_*` pools via `BRIDGE_RESERVED_POOLS`; same-aggregate ordering with a `concurrency` key; or running `codegen events consumers <type>` to find who reacts to an event.
4
+ allowed-tools: Read, Write, Edit, Glob, Grep, Bash
5
+ user-invocable: false
6
+ ---
7
+
8
+ <!-- managed by @pattern-stack/codegen — re-run `codegen skills install` to refresh. Edit the package source, not this vendored copy. -->
9
+
10
+ # Event-to-Job Bridge
11
+
12
+ The bridge is the durable, typed, observable path from *"an event was
13
+ published"* to *"a job was started"* in your app. It is its own subsystem —
14
+ the combiner between the `events` and `jobs` subsystems, owned by neither. You
15
+ opt into it by running `codegen subsystem install bridge`, which vendors the
16
+ runtime into `<paths.subsystems>/bridge/` (imported as
17
+ `@shared/subsystems/bridge`) and adds a `bridge:` block to
18
+ `codegen.config.yaml`.
19
+
20
+ Use this skill when you want one event to fan out to one or more durable async
21
+ jobs, authored by teams that don't know about each other. If you only need a
22
+ cheap in-process reaction, or a request-path job the caller already knows by
23
+ name, you probably want a lower tier instead — see the decision table below.
24
+
25
+ ## Mental model: three tiers of event-driven work
26
+
27
+ You pick the tier by use case. The bridge is Tier 3.
28
+
29
+ | Tier | Mechanism | Durability | Latency | Use for |
30
+ |---|---|---|---|---|
31
+ | 1. Subscribe | `@OnEvent('x.y')` / `IEventBus.subscribe()` in-process | None (at-most-once) | ~ms | metrics, cache busts, logs |
32
+ | 2. Direct invoke | `eventFlow.publishAndStart(event, jobType, input)` | Yes (caller's tx) | ~1 poll cycle | request-path work, caller knows the job |
33
+ | 3. Bridge | `@JobHandler({ triggers: [{ event, map, when }] })` | Yes (outbox + ledger) | 2–3 poll cycles | durable async fanout, decoupled authors |
34
+
35
+ Tier 1 is events-only and never touches the bridge. Tier 2 and Tier 3 both
36
+ flow through the bridge at runtime — the difference is *who declares the
37
+ link*. In Tier 2 the caller writes the `publishAndStart` call explicitly; in
38
+ Tier 3 the job declares `triggers:` and the link fires automatically whenever
39
+ the event is published anywhere.
40
+
41
+ **How a trigger actually runs (Tier 3).** When the events outbox drain claims
42
+ a `domain_events` row, it inserts — in one per-event transaction — one ledger
43
+ row in `bridge_delivery` plus one *wrapper* job in a reserved `events_*` pool,
44
+ per matched trigger. A normal job worker claims the wrapper. The wrapper reads
45
+ the ledger, evaluates your `when:` predicate, applies your `map:` function, and
46
+ calls the orchestrator to start your real job in *its* declared pool, parented
47
+ to the wrapper so cascade-cancel works. Then it marks the delivery `delivered`.
48
+ The reserved `events_*` pools thus host cheap wrappers (high concurrency); your
49
+ own pools host the actual work (concurrency tuned to its scarce resource).
50
+
51
+ **The ledger is the source of truth.** `bridge_delivery` has a
52
+ `UNIQUE (event_id, trigger_id)` constraint. That is the idempotency primitive
53
+ — it dedups outbox replay, and it dedups the case where a caller uses
54
+ `publishAndStart` AND the same job also declares a matching `triggers:` entry
55
+ (exactly one execution per event/trigger pair, whichever path got there first).
56
+
57
+ ## Install and wiring
58
+
59
+ ```bash
60
+ codegen subsystem install bridge
61
+ ```
62
+
63
+ Config block (`codegen.config.yaml`):
64
+
65
+ ```yaml
66
+ bridge:
67
+ backend: drizzle # 'drizzle' (production) or 'memory' (tests)
68
+ multi_tenant: false # pair with BridgeModule.forRoot({ multiTenant: true })
69
+ ```
70
+
71
+ Register the module in `app.module.ts`:
72
+
73
+ ```ts
74
+ import { BridgeModule } from '@shared/subsystems/bridge';
75
+
76
+ BridgeModule.forRoot({ backend: 'drizzle', multiTenant: false }),
77
+ ```
78
+
79
+ ### Wire the reserved `events_*` pools
80
+
81
+ The bridge wrappers run in three reserved pools — `events_inbound`,
82
+ `events_change`, `events_outbound`. A worker process must actually *drain* them,
83
+ or wrappers sit `pending` forever (and `BridgeModule` fails fast at boot). The
84
+ exported `BRIDGE_RESERVED_POOLS` is the list of those three pool names — spread
85
+ it into the active-pools list of your `JobWorkerModule.forRoot` (see the
86
+ `subsystems` skill for the full wiring + order):
87
+
88
+ ```ts
89
+ import { BRIDGE_RESERVED_POOLS } from '@shared/subsystems/bridge';
90
+ import { JobWorkerModule } from '@shared/subsystems/jobs';
91
+
92
+ JobWorkerModule.forRoot({
93
+ mode: 'embedded',
94
+ backend: 'drizzle',
95
+ // active pool names this worker drains — include the reserved lanes:
96
+ pools: ['interactive', 'batch', ...BRIDGE_RESERVED_POOLS],
97
+ }),
98
+ ```
99
+
100
+ Pool *definitions* (concurrency per lane) live in `codegen.config.yaml` under
101
+ `jobs.pools`, not in `forRoot`; `JobWorkerModule.forRoot({ pools })` only names
102
+ which lanes this process drains. (Alternatively, `JobWorkerModule.forRoot({
103
+ allPools: true })` drains every pool including the reserved ones — that's what
104
+ the standalone `worker.ts` uses.) Wrappers are cheap (read ledger → evaluate
105
+ `when:` → start the user job → update ledger), so the reserved lanes can run at
106
+ high concurrency safely.
107
+
108
+ ## Authoring triggers
109
+
110
+ Triggers are **job-owned**. Declare them on the `@JobHandler` decorator of the
111
+ job you want to run — never on the event side.
112
+
113
+ ```ts
114
+ @JobHandler<SendWelcomeEmailInput>('send_welcome_email', {
115
+ pool: 'outbound_email',
116
+ triggers: [
117
+ {
118
+ event: 'user.created',
119
+ map: (e) => ({ userId: e.aggregateId, email: e.payload.email }),
120
+ when: (e) => e.payload.email !== undefined, // optional
121
+ },
122
+ ],
123
+ })
124
+ export class SendWelcomeEmailJob extends JobHandlerBase<SendWelcomeEmailInput> {
125
+ // ...
126
+ }
127
+ ```
128
+
129
+ `map:` and `when:` are typed TS callbacks — they typecheck against the payload
130
+ type of the event you named. They must be **self-contained**: no calls to
131
+ project helpers, services, or imports. The codegen inlines the arrow body
132
+ verbatim into the generated bridge registry, so anything outside the arrow's
133
+ own scope will not be in scope there.
134
+
135
+ After authoring or changing a trigger, regenerate the registry
136
+ (`codegen entity new --all` or your project's gen-all task). Unknown event
137
+ types referenced in `triggers[].event` fail the build at generation time —
138
+ that is the build-time validation against the event registry, and it is the
139
+ primary safety net.
140
+
141
+ If `when:` returns false at runtime, the wrapper records the delivery as
142
+ `skipped` (with a reason) and does not start your job.
143
+
144
+ ## Ordering
145
+
146
+ **The default configuration gives parallelism, not ordering.** Two events of
147
+ the same type may be processed concurrently; same-aggregate ordering is NOT
148
+ guaranteed out of the box. Pick the knob that matches your real requirement:
149
+
150
+ 1. **`concurrency` key on the user job** *(recommended when ordering matters)*
151
+ — granular per-aggregate serialization, parallelism preserved across
152
+ unrelated aggregates. The `key` callback receives the job input:
153
+
154
+ ```ts
155
+ @JobHandler<ProvisionInput>('provision_workspace', {
156
+ concurrency: { key: (input) => input.accountId, collisionMode: 'queue' },
157
+ triggers: [/* ... */],
158
+ })
159
+ ```
160
+
161
+ 2. **`events_<direction>` pool `concurrency: 1`** *(blunt)* — serializes
162
+ **every** wrapper in that direction, i.e. every bridge fanout for that
163
+ direction end to end. Simplest config, highest throughput cost. Use only
164
+ when every event in the direction genuinely needs strict order.
165
+
166
+ ## When NOT to use the bridge
167
+
168
+ The bridge adds 2–3 outbox poll cycles of latency (typically 1–3 s). If you
169
+ need request-path durability at lower latency and the caller already knows the
170
+ job, use the `IEventFlow` facade directly (Tier 2 — runs off the next poll
171
+ cycle, in the caller's transaction):
172
+
173
+ ```ts
174
+ constructor(private eventFlow: IEventFlow) {}
175
+
176
+ async signup(input: SignupInput, tx: Tx): Promise<void> {
177
+ await this.eventFlow.publishAndStart(
178
+ 'user.created',
179
+ 'provision_workspace',
180
+ { userId: input.id },
181
+ { tx },
182
+ );
183
+ }
184
+ ```
185
+
186
+ `IEventFlow` exposes exactly two verbs: `publish()` and `publishAndStart()`.
187
+ All request-path publishing goes through this facade, not through `IEventBus`
188
+ directly. Tier 1 subscribers stay declarative (`@OnEvent`) and bypass it.
189
+
190
+ Decision table:
191
+
192
+ | Need | Tier | Pattern |
193
+ |---|---|---|
194
+ | Cheap in-process reaction (metrics, cache bust) | 1 | `@OnEvent('x.y')` or `IEventBus.subscribe` |
195
+ | Request-path durable, caller knows the job | 2 | `eventFlow.publishAndStart(...)` |
196
+ | Async fanout, decoupled authors, multiple handlers per event | 3 | `@JobHandler.triggers[]` (the bridge) |
197
+
198
+ ## Discovering fanout: `codegen events consumers <type>`
199
+
200
+ ```bash
201
+ codegen events consumers user.created
202
+ ```
203
+
204
+ Prints a greppable report with all three tiers and file:line citations:
205
+
206
+ ```
207
+ Event: user.created
208
+ Tier 3 — Bridge triggers (2):
209
+ - send_welcome_email#0 (src/jobs/send-welcome-email.job.ts:14)
210
+ - provision_workspace#0 (src/jobs/provision-workspace.job.ts:18)
211
+ Tier 2 — Direct invoke via publishAndStart (1):
212
+ - src/use-cases/signup.uc.ts:42
213
+ Tier 1 — Subscribers (1):
214
+ - MetricsListener.onCreate @OnEvent('user.created') at src/observability/metrics.ts:28
215
+ ```
216
+
217
+ Unknown event types print a suggestion-bearing warning to stderr but the
218
+ command still exits 0. If the scan finds zero `publishAndStart` call sites but
219
+ `EventFlowService` exists in the codebase, a fallback warning prints — the AST
220
+ scan can miss non-standard injection (property injection, dynamic dispatch);
221
+ grep for `publishAndStart` to verify Tier 2 manually.
222
+
223
+ ## Multi-tenancy
224
+
225
+ Set `multi_tenant: true` in the config block and pass
226
+ `BridgeModule.forRoot({ backend: 'drizzle', multiTenant: true })`. When on,
227
+ three write-side sites throw `MissingTenantIdError` if `tenantId === undefined`
228
+ (explicit `null` passes, for deliberate cross-tenant work): the
229
+ `publishAndStart` request-path entry, the wrapper handler entry, and the
230
+ delivery-repo write boundary. Event metadata carries `tenantId` from the typed
231
+ event bus; the bridge threads it into the job's `tenant_id` when it starts your
232
+ job. Your bridge, events, and jobs configs must all agree on the flag.
233
+
234
+ ## Renaming or removing a trigger
235
+
236
+ The generated `trigger_id` is `<jobType>#<index>` — stable across generations,
237
+ so replays resolve to the same ledger row. Renaming a `@JobHandler('<name>')`
238
+ changes that id, so in-flight `pending` deliveries with the old `trigger_id`
239
+ become orphans: the wrapper detects the missing registry entry, marks the
240
+ delivery `skipped` with `skip_reason='trigger_unregistered'`, and stops. No
241
+ auto-migration, no replay. If you need the old deliveries to run under the new
242
+ name, drain the queue before deploying the rename.
243
+
244
+ ## Do not
245
+
246
+ - **Do not collapse the three tiers** into "the bridge is the only path."
247
+ Tier 1 stays valid for cheap in-process reactions; Tier 2 is the request-path
248
+ durable option; Tier 3 is async fanout. The right tool depends on durability
249
+ and latency.
250
+ - **Do not put your `@JobHandler` classes on reserved `events_*` pools.**
251
+ Module init rejects it. Those pools host framework wrappers only; your work
252
+ lives in a pool you declare.
253
+ - **Do not declare triggers on the event side.** Triggers are job-owned. The
254
+ events subsystem stays zero-knowledge about jobs.
255
+ - **Do not reference helpers, services, or imports inside `map:` / `when:`.**
256
+ They are inlined verbatim into the generated registry and must be
257
+ self-contained.
258
+ - **Do not skip regenerating the registry** after changing a trigger. The
259
+ build-time validation against the event registry only runs at generation; a
260
+ stale registry silently drifts from your decorators.
261
+ - **Do not expect ordering from the default config.** Add a `concurrency` key
262
+ (granular) or set a reserved pool's `concurrency: 1` (blunt) when
263
+ same-aggregate order matters.
264
+ - **Do not drop `tenantId`** when `multi_tenant: true`. Missing tenant context
265
+ throws `MissingTenantIdError` at the write-side enforcement sites.
@@ -0,0 +1,115 @@
1
+ ---
2
+ name: codegen
3
+ description: >-
4
+ Load when working in a project that uses @pattern-stack/codegen to scaffold a
5
+ NestJS + Drizzle backend from YAML — i.e. when the request is to add or change
6
+ an entity / module / CRUD resource, generate code from `entities/*.yaml`, run
7
+ the `codegen` (aka `cdp`) CLI, install or wire an infrastructure subsystem, or
8
+ refresh the project after a package upgrade. This is the entry-point router;
9
+ it points at the focused `entities`, `subsystems`, `jobs`, `events`, `bridge`,
10
+ and `sync` skills for deep work.
11
+ allowed-tools: Read, Write, Edit, Glob, Grep, Bash
12
+ user-invocable: false
13
+ ---
14
+
15
+ <!-- managed by @pattern-stack/codegen — re-run `codegen skills install` to refresh. Edit the package source, not this vendored copy. -->
16
+
17
+ # Using @pattern-stack/codegen in this project
18
+
19
+ This project generates its backend (domain entities, repositories, services,
20
+ controllers, DTOs, use cases, Drizzle schemas, NestJS modules) from YAML entity
21
+ definitions, following Clean Architecture. You author small YAML files and run
22
+ the `codegen` CLI; the generator owns a few directories and never touches the
23
+ rest of your app.
24
+
25
+ The CLI binary is `codegen` (alias `cdp`). Every noun supports `--json` for
26
+ machine-readable output and `--cwd <path>` to target another project root.
27
+
28
+ ## Mental model
29
+
30
+ - **You own**: `entities/*.yaml`, `events/*.yaml`, `app.module.ts`, `main.ts`,
31
+ `database.module.ts`, `codegen.config.yaml`, and your hand-written use cases /
32
+ adapters.
33
+ - **Codegen owns** (don't hand-edit — regenerated every run):
34
+ - `src/generated/modules.ts` — the `GENERATED_MODULES` barrel
35
+ - `src/generated/schema.ts` — the Drizzle schema barrel
36
+ - the per-entity module tree (`src/modules/<plural>/…` in clean-lite-ps)
37
+ - **The package vendors** managed copies of its runtime into `src/shared/**`
38
+ (base classes, types, the `DRIZZLE` token, the Zod pipe, the OpenAPI
39
+ registry) and installed subsystems into `<subsystems-root>/<name>/`. Treat
40
+ these as generated output: don't hand-edit; subclass instead. `codegen
41
+ update` refreshes them after a package bump.
42
+
43
+ The generation pipeline: `YAML → parse → analyze → templates → code`. After any
44
+ generation run the two barrels are rewritten and you wire them into
45
+ `app.module.ts` exactly once — codegen never edits that file again.
46
+
47
+ ## Routing — load the focused skill for deep work
48
+
49
+ | Task | Read |
50
+ |---|---|
51
+ | Author / change an entity YAML (fields, families, queries, EAV, relationships) | the `entities` skill |
52
+ | Install or wire an infrastructure subsystem; get the `forRoot` registration order right | the `subsystems` skill |
53
+ | Write a background `@JobHandler`, configure pools, set concurrency/ordering | the `jobs` skill |
54
+ | Author a domain event, publish via the outbox, use the typed event bus | the `events` skill |
55
+ | React to an event with a durable async job (the event-to-job bridge) | the `bridge` skill |
56
+ | Pull/push data from an external system (`IChangeSource` / `ISyncSink`) | the `sync` skill |
57
+
58
+ ## CLI quick reference
59
+
60
+ ```bash
61
+ # Project lifecycle
62
+ codegen init # scaffold this project's shared layer + config + skills
63
+ codegen project scan # detect framework/ORM/architecture → propose config
64
+ codegen project config # print the resolved codegen.config.yaml
65
+ codegen update # re-sync vendored runtime + subsystems + skills after a package bump
66
+
67
+ # Entities
68
+ codegen entity new entities/<file>.yaml # generate one entity
69
+ codegen entity new --all # regenerate every entity in entities/
70
+ codegen entity new --all --dry-run # preview
71
+ codegen entity list # tabular list
72
+ codegen entity validate --strict # validate YAML + cross-refs (warnings fail)
73
+
74
+ # Subsystems (see the `subsystems` skill for wiring + order)
75
+ codegen subsystem # summary: installed vs available
76
+ codegen subsystem install <name> # vendor a subsystem's runtime + inject its config block
77
+ codegen subsystem list
78
+
79
+ # Skills
80
+ codegen skills install # (re)vendor these consumer skills into .claude/skills
81
+ codegen skills list
82
+ ```
83
+
84
+ ## Non-obvious rules
85
+
86
+ - **YAML is `snake_case`; generated TS properties are `camelCase`.** The
87
+ templates derive `accountId` from `account_id`. Entity names are singular
88
+ `snake_case` (`opportunity`).
89
+ - **Two architectures, mutually exclusive.** `generate.architecture` in
90
+ `codegen.config.yaml` is either `clean-lite-ps` (the supported consumer
91
+ default — lighter per-entity module layout) or `clean` (full split). Don't mix.
92
+ - **Barrels are wired once.** After the first `entity new`, add
93
+ `...GENERATED_MODULES` to `app.module.ts` and `export * from
94
+ './generated/schema'` to your schema root. Codegen keeps the barrel contents
95
+ fresh; you never re-touch `app.module.ts` for new entities.
96
+ - **Migrations are not `drizzle-kit push` in shared/CI/prod.** Generate
97
+ reviewable SQL with Atlas (`atlas migrate diff` → review → `atlas migrate
98
+ apply`). `push` is dev-loop-only.
99
+ - **Upgrades need a re-sync.** After `bun add @pattern-stack/codegen@latest`, run
100
+ `codegen update` — the vendored `src/shared/**` and installed subsystems are
101
+ otherwise stale against the new package.
102
+
103
+ ## Do not
104
+
105
+ - **Do not hand-edit anything under `src/generated/`** or the vendored
106
+ `src/shared/**` runtime files — the next `entity new` / `codegen update`
107
+ overwrites them. Need different behavior? Subclass the base in your own module.
108
+ - **Do not declare a fresh `DRIZZLE` token.** Import the one from
109
+ `@shared/constants/tokens`; a second token has a different identity and DI
110
+ won't resolve.
111
+ - **Do not add tables directly to `src/generated/schema.ts`.** Hand-authored
112
+ tables go in your own file and are combined in the schema root re-export.
113
+ - **Do not reach for `clean` vs `clean-lite-ps` arbitrarily.** Match the
114
+ project's existing `generate.architecture`; switching mid-project rewrites the
115
+ whole module layout.
@@ -0,0 +1,111 @@
1
+ ---
2
+ name: entities
3
+ description: >-
4
+ Load when authoring or changing an entity definition for a project that uses
5
+ @pattern-stack/codegen — creating `entities/<name>.yaml`, choosing an entity
6
+ family (Synced / Activity / Metadata / Knowledge / Base), adding fields,
7
+ behaviors, relationships, declarative `queries:`, or opting an entity into EAV
8
+ custom fields. Covers what each YAML block generates and the layer rules the
9
+ generated code obeys.
10
+ allowed-tools: Read, Write, Edit, Glob, Grep, Bash
11
+ user-invocable: false
12
+ ---
13
+
14
+ <!-- managed by @pattern-stack/codegen — re-run `codegen skills install` to refresh. Edit the package source, not this vendored copy. -->
15
+
16
+ # Authoring entities
17
+
18
+ An entity is one `entities/<name>.yaml` file. Running `codegen entity new
19
+ entities/<name>.yaml` (or `codegen entity new --all`) turns it into a full
20
+ vertical slice: a Drizzle table, a repository (extending a family base class), a
21
+ service, use cases, a controller with Zod-validated routes, DTOs, and a NestJS
22
+ module — plus an entry in the `GENERATED_MODULES` and schema barrels.
23
+
24
+ ## Mental model
25
+
26
+ - **One YAML → one module.** You describe *what* the entity is (fields,
27
+ relationships, behaviors, queries); the templates produce the *how* (CRUD,
28
+ validation, wiring).
29
+ - **Family = inherited capability.** Every entity picks a `pattern` (family).
30
+ The family decides which extra repository/service methods you get for free on
31
+ top of the standard CRUD set. See `families-and-queries.md`.
32
+ - **Declarative queries beat hand-written finders.** A `queries:` block
33
+ generates typed repository methods, interface signatures, injectable query
34
+ use cases, and module registration. See `families-and-queries.md`.
35
+ - **Naming**: YAML is `snake_case` (matches DB columns); generated TS
36
+ properties are `camelCase`; entity `name` is singular `snake_case`.
37
+
38
+ ## Routing
39
+
40
+ | For | Read |
41
+ |---|---|
42
+ | The full YAML block reference — fields, types, behaviors, relationships, EAV flags, `generate:` | `yaml-reference.md` |
43
+ | Choosing a family, the methods each family adds, and the `queries:` block shapes | `families-and-queries.md` |
44
+
45
+ ## Minimum viable entity
46
+
47
+ ```yaml
48
+ entity:
49
+ name: account # singular snake_case
50
+ pattern: Synced # Base | Synced | Activity | Metadata | Knowledge (or app-defined)
51
+
52
+ fields:
53
+ name:
54
+ type: string
55
+ required: true
56
+ email:
57
+ type: string
58
+ index: true
59
+ status:
60
+ type: enum
61
+ choices: [active, inactive]
62
+
63
+ behaviors:
64
+ - timestamps # createdAt, updatedAt
65
+ - soft_delete # deletedAt + automatic query filtering
66
+
67
+ queries:
68
+ - by: [email]
69
+ unique: true # → FindAccountByEmail use case (unique)
70
+ ```
71
+
72
+ ```bash
73
+ codegen entity new entities/account.yaml
74
+ # barrels auto-update — no manual wiring for the new module
75
+ ```
76
+
77
+ ## Layer rules the generated code obeys
78
+
79
+ Generated code is layered; your hand-written use cases must respect the same
80
+ boundaries:
81
+
82
+ - **Repository** — single table. Extends a family base. No business logic.
83
+ Write methods take an optional `tx?: DrizzleTx`.
84
+ - **Service** — one aggregate. Composes repositories; may read cross-domain;
85
+ may call same-domain services. **May NOT write cross-domain.** This is the
86
+ mandatory API boundary.
87
+ - **Use case** — a workflow. Composes services (including cross-domain), owns
88
+ the transaction for cross-domain writes, emits events, calls external ports.
89
+ - **Controller** — thin adapter. Calls use cases only. `@Body()` is run through
90
+ `ZodValidationPipe` (422 on failure); `GET :id` throws 404 when the row is
91
+ missing or soft-deleted.
92
+
93
+ ## Non-obvious rules
94
+
95
+ - **`generate.writes: true` (default) emits POST/PATCH/DELETE** + create/update/
96
+ delete use cases. Set it `false` for read-only entities.
97
+ - **Cross-entity references must resolve.** If `account.yaml` references
98
+ `contact`, the analyzer needs `contact.yaml` present; `codegen entity new
99
+ --all` is the safe way to regenerate a set with cross-refs.
100
+ - **Regenerating is safe and idempotent.** Re-run after any YAML edit; the
101
+ module tree + barrels are codegen-owned.
102
+ - **Validate before generating** when unsure: `codegen entity validate --strict`.
103
+
104
+ ## Do not
105
+
106
+ - **Do not hand-edit generated module files.** Override by composing in your own
107
+ module / subclassing the generated service.
108
+ - **Do not put business logic in a repository** or cross-domain writes in a
109
+ service — the layer rules above are enforced by convention and reviewed.
110
+ - **Do not invent field types.** Use the documented set (`yaml-reference.md`);
111
+ unknown types fail validation.
@@ -0,0 +1,82 @@
1
+ <!-- managed by @pattern-stack/codegen — re-run `codegen skills install` to refresh. Edit the package source, not this vendored copy. -->
2
+
3
+ # Entity families & declarative queries
4
+
5
+ ## Families (the `pattern:` field)
6
+
7
+ Every entity declares a `pattern`. The family decides which methods the
8
+ generated repository and service inherit on top of the standard CRUD set. The
9
+ base classes are vendored into `@shared/base-classes/*` at project init.
10
+
11
+ **Standard CRUD set (every family):** `findById`, `findByIds`, `list`, `count`,
12
+ `exists`, `create`, `update`, `delete`, `upsertMany`. Every write method accepts
13
+ an optional `tx?: DrizzleTx` for transactional composition.
14
+
15
+ | Family | Use it for | Adds on top of CRUD |
16
+ |---|---|---|
17
+ | `Base` | plain tables with no special access pattern | nothing — standard CRUD only |
18
+ | `Synced` | records mirrored from an external system (have an external id + per-user visibility) | `findByExternalId`, `findAllByUserId`, `findVisibleByUserId`, `syncUpsert` |
19
+ | `Activity` | time-ordered activity/event rows tied to a parent | `findByDateRange`, `findByUserId`, `findByOpportunityId`, `findRecentByOpportunityId` |
20
+ | `Metadata` | key/value or definition/value rows describing other entities | `findByEntityIdAndType`, `listByEntityId`, `listHistoryByEntityId` |
21
+ | `Knowledge` | semantically-searchable knowledge rows (pgvector at runtime) | `semanticSearch`, `findPendingByOpportunityId`, `updateStatus`, `updateStatusBatch` |
22
+
23
+ Choosing a family:
24
+ - Mirrors an external system (CRM, etc.)? → `Synced` (often paired with the
25
+ `sync` skill).
26
+ - Append-only timeline tied to a parent record? → `Activity`.
27
+ - Describes/annotates other entities (incl. the EAV value table)? → `Metadata`.
28
+ - Vector search / RAG? → `Knowledge`.
29
+ - None of the above? → `Base`.
30
+
31
+ App-defined patterns are also supported (a project can register its own family
32
+ base); use the project's existing convention if one is present.
33
+
34
+ ## Declarative queries (the `queries:` block)
35
+
36
+ A `queries:` entry generates a typed repository method, the interface
37
+ signature, an injectable query use case, and its module registration — no
38
+ hand-written finder.
39
+
40
+ ```yaml
41
+ queries:
42
+ - by: [user_id] # → findByUserId()
43
+ - by: [email] # → findByEmail() (unique)
44
+ unique: true
45
+ - by: [account_id] # → findByAccountId(), ordered
46
+ order: created_at desc
47
+ - by: [user_id, account_id] # → findByUserIdAndAccountId()
48
+ ```
49
+
50
+ Shapes:
51
+ - **`by: [col, …]`** — equality finder on one or more columns. Method name is
52
+ derived (`findByUserIdAndAccountId`). Multi-column finders AND the conditions.
53
+ - **`unique: true`** — returns a single row (or null) instead of an array, and
54
+ marks the underlying index unique.
55
+ - **`order: <col> <dir>`** — default ordering for the finder (e.g. `created_at
56
+ desc`).
57
+
58
+ ### Filtered search with pagination
59
+
60
+ ```yaml
61
+ queries:
62
+ - name: search # → SearchContacts use case + GET /contacts/search
63
+ filters: [user_id, account_id, email] # optional equality filters
64
+ search: name # ilike column for free-text
65
+ paginate: true # returns { items, total, limit, offset }
66
+ ```
67
+
68
+ A `name`d query with `filters`/`search`/`paginate` generates a search use case
69
+ and a `GET /<plural>/search` route. `paginate: true` makes the route accept
70
+ `limit`/`offset` and return a paged envelope.
71
+
72
+ ## Non-obvious rules
73
+
74
+ - **Finder names are generated** from the column list — don't also hand-write a
75
+ finder of the same name; compose on top instead.
76
+ - **Unique finders return a single nullable result**; non-unique return arrays.
77
+ - **`order:` is the default sort**, not a parameter — add a `queries:` search
78
+ entry if you need caller-controlled ordering.
79
+ - **Family methods assume their columns exist.** `Synced` expects an external-id
80
+ + user-visibility shape; `Activity` expects a parent FK like
81
+ `opportunity_id`. If your table doesn't fit, pick `Base` and add explicit
82
+ `queries:`.