@pattern-stack/codegen 0.17.2 → 0.19.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +157 -2
  3. package/consumer-skills/codegen/SKILL.md +32 -0
  4. package/consumer-skills/entities/SKILL.md +2 -0
  5. package/dist/{chunk-I6UXRJ3Q.js → chunk-43SBT72G.js} +4 -4
  6. package/dist/{chunk-T6SCOJF4.js → chunk-7LKAMLV4.js} +4 -4
  7. package/dist/{chunk-IOQMMH6C.js → chunk-F7KN3U6U.js} +122 -8
  8. package/dist/chunk-F7KN3U6U.js.map +1 -0
  9. package/dist/{chunk-CZQUOIDY.js → chunk-J7JMVS2B.js} +4 -4
  10. package/dist/{chunk-KSTZIULO.js → chunk-K2I6XIK5.js} +4 -4
  11. package/dist/{chunk-ATVGYF3D.js → chunk-PKDS6QIJ.js} +7 -7
  12. package/dist/{chunk-KZDHMZ45.js → chunk-SNH35CNA.js} +8 -8
  13. package/dist/runtime/base-classes/index.js +24 -24
  14. package/dist/runtime/subsystems/analytics/analytics.module.js +2 -2
  15. package/dist/runtime/subsystems/analytics/index.js +4 -4
  16. package/dist/runtime/subsystems/auth/auth.module.js +3 -3
  17. package/dist/runtime/subsystems/auth/index.js +14 -14
  18. package/dist/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.js +2 -2
  19. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js +1 -1
  20. package/dist/runtime/subsystems/bridge/bridge.module.js +5 -5
  21. package/dist/runtime/subsystems/bridge/index.js +13 -13
  22. package/dist/runtime/subsystems/cache/cache.module.js +1 -1
  23. package/dist/runtime/subsystems/cache/index.js +3 -3
  24. package/dist/runtime/subsystems/index.js +94 -94
  25. package/dist/runtime/subsystems/integration/build-change-source.js +2 -2
  26. package/dist/runtime/subsystems/integration/index.js +36 -36
  27. package/dist/runtime/subsystems/integration/integration.module.js +4 -4
  28. package/dist/src/cli/index.js +1552 -254
  29. package/dist/src/cli/index.js.map +1 -1
  30. package/dist/src/index.d.ts +18 -0
  31. package/dist/src/index.js +12 -12
  32. package/package.json +1 -1
  33. package/src/config/locations.mjs +0 -6
  34. package/src/config/paths.mjs +0 -13
  35. package/templates/entity/new/prompt.js +12 -88
  36. package/dist/chunk-IOQMMH6C.js.map +0 -1
  37. package/templates/entity/new/frontend/_inject-entities-entry.ejs.t +0 -7
  38. package/templates/entity/new/frontend/_inject-entities-import.ejs.t +0 -7
  39. package/templates/entity/new/frontend/collections/_ensure-anchor-collections.ejs.t +0 -10
  40. package/templates/entity/new/frontend/collections/_inject-index.ejs.t +0 -9
  41. package/templates/entity/new/frontend/collections/_inject-schema-import.ejs.t +0 -9
  42. package/templates/entity/new/frontend/collections/collection.ejs.t +0 -86
  43. package/templates/entity/new/frontend/collections/collections-base.ejs.t +0 -35
  44. package/templates/entity/new/frontend/entity/collection.ejs.t +0 -173
  45. package/templates/entity/new/frontend/entity/combined.ejs.t +0 -505
  46. package/templates/entity/new/frontend/entity/fields.ejs.t +0 -105
  47. package/templates/entity/new/frontend/entity/hooks.ejs.t +0 -74
  48. package/templates/entity/new/frontend/entity/index.ejs.t +0 -22
  49. package/templates/entity/new/frontend/entity/mutation-hooks.ejs.t +0 -85
  50. package/templates/entity/new/frontend/entity/mutations.ejs.t +0 -39
  51. package/templates/entity/new/frontend/entity/types.ejs.t +0 -60
  52. package/templates/entity/new/frontend/generated/_inject-index-export.ejs.t +0 -7
  53. package/templates/entity/new/frontend/generated/_inject-index-import.ejs.t +0 -7
  54. package/templates/entity/new/frontend/generated/_inject-index-registry.ejs.t +0 -7
  55. package/templates/entity/new/frontend/store/_inject-collection-import.ejs.t +0 -9
  56. package/templates/entity/new/frontend/store/_inject-collections.ejs.t +0 -9
  57. package/templates/entity/new/frontend/store/_inject-entity.ejs.t +0 -9
  58. package/templates/entity/new/frontend/store/_inject-import.ejs.t +0 -9
  59. package/templates/entity/new/frontend/store/_inject-lookups.ejs.t +0 -9
  60. package/templates/entity/new/frontend/store/_inject-resolve.ejs.t +0 -10
  61. package/templates/entity/new/frontend/store/hooks.ejs.t +0 -73
  62. package/templates/entity/new/frontend/unified-entity.ejs.t +0 -29
  63. /package/dist/{chunk-I6UXRJ3Q.js.map → chunk-43SBT72G.js.map} +0 -0
  64. /package/dist/{chunk-T6SCOJF4.js.map → chunk-7LKAMLV4.js.map} +0 -0
  65. /package/dist/{chunk-CZQUOIDY.js.map → chunk-J7JMVS2B.js.map} +0 -0
  66. /package/dist/{chunk-KSTZIULO.js.map → chunk-K2I6XIK5.js.map} +0 -0
  67. /package/dist/{chunk-ATVGYF3D.js.map → chunk-PKDS6QIJ.js.map} +0 -0
  68. /package/dist/{chunk-KZDHMZ45.js.map → chunk-SNH35CNA.js.map} +0 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,38 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.19.0] — 2026-06-05
8
+
9
+ **Providers catalog emission + planned providers** (ADR-038 follow-on;
10
+ swe-brain consumer-test finding — the Connections surface hand-duplicated
11
+ provider knowledge that `definitions/providers/*.yaml` already owns).
12
+
13
+ ### Added
14
+
15
+ - **Frontend providers catalog (`generated/providers.ts`)** — emitted by the
16
+ frontend whole-set emitter when `definitions/providers/` exists (entity-only
17
+ consumers see no new file; the root barrel gains the export conditionally).
18
+ `PROVIDERS` (flat, slug-sorted) + `PROVIDER_CATALOG` (grouped by
19
+ `display.category` into the ordered `frontend.catalog.categories`). Providers
20
+ are gen-time knowledge — the catalog is emitted, not queried.
21
+ - **`display:` block on the provider schema** (`category`, `blurb`, `hint`) —
22
+ presentation metadata consumed only by the catalog emission; backend
23
+ provider/adapter codegen ignores it.
24
+ - **`status: active | planned` on the provider schema** (default `active`).
25
+ `planned` providers are roadmap stubs: catalog tile only — `auth`/`client`
26
+ optional, surface closed-set + import pre-flight cross-checks skipped (slug
27
+ uniqueness still enforced), and ALL backend emission (provider modules,
28
+ adapters, assemblies) filters them out. Flip to `active` when the
29
+ integration lands.
30
+ - **`frontend.catalog.categories`** in `codegen.config.yaml` — ordered display
31
+ groups (`id`, `name`, `blurb`) the catalog renders.
32
+
33
+ ### Changed
34
+
35
+ - `generateProviderModule` / `generateAdapterScaffold` now take
36
+ `ActiveProviderDefinition` (auth/client guaranteed); use the exported
37
+ `isActiveProvider` guard to narrow.
38
+
7
39
  ## [0.17.2] — 2026-06-04
8
40
 
9
41
  **Shutdown leak fix** (LISTEN-NOTIFY-2; swe-brain dogfood). With
package/README.md CHANGED
@@ -138,6 +138,155 @@ modules/{plural}/
138
138
  use-cases/ FindById, List, declarative queries
139
139
  ```
140
140
 
141
+ **Frontend** (`generate.frontend: true`) — see [Frontend generation](#frontend-generation) below.
142
+
143
+ ## Frontend generation
144
+
145
+ Gated entirely by `generate.frontend` (default `false`; the scanner sets it
146
+ `true` when it finds an `apps/frontend/` directory). When on, the `entity new`
147
+ post-step (and therefore `gen-all`) renders the **complete frontend tree from the
148
+ full entity set in one pass** — a TypeScript emitter (`src/emitters/frontend/`),
149
+ not hygen templates. Re-running is idempotent: every file is a complete-file
150
+ write with a `@generated` banner, no inject/anchor machinery, no overwrite
151
+ prompts (ADR-038).
152
+
153
+ Output lands under `locations.frontendGenerated` (default
154
+ `apps/frontend/src/generated/`):
155
+
156
+ ```
157
+ generated/
158
+ index.ts whole-set barrel (+ version-pairing comment)
159
+ config.ts per-entity sync modes + runtime overrides
160
+ query-client.ts shared TanStack QueryClient
161
+ api/<entity>.ts REST client → the generated NestJS controllers
162
+ collections/<entity>.ts createCollection, branched on the entity's sync mode
163
+ entities/<entity>.ts createEntityHooks({ collection, api }) wiring
164
+ store/index.ts createStore over the full set (+ resolvers, lookups)
165
+ fields/<entity>.ts field metadata (FieldMeta, <entity>Fields)
166
+ providers.ts providers catalog — only when definitions/providers/ exists
167
+ ```
168
+
169
+ Entity types and Zod schemas are **imported** from `locations.dbEntities`
170
+ (default `@repo/db/entities`), not re-emitted. The emitter imports the plain
171
+ class name — `import type { <Class> } from '<dbEntities>/<name>'` — so
172
+ `dbEntities` is the contract: it MUST export the entity type under its plain
173
+ `<Class>` name (e.g. `Contact`, not `ContactEntity`). If your db package only
174
+ exports `<Class>Entity`, that is the one knob to change in the emitter.
175
+
176
+ The hook/mutation/store/provider logic is consumed from
177
+ `@pattern-stack/frontend-patterns` (`createEntityHooks`, `createStore`) — the
178
+ generated files are thin wiring. **The consumer's frontend installs that package
179
+ plus the paired TanStack libraries.** `project init` adds the version-pairing
180
+ deps to `apps/frontend/package.json` when `generate.frontend: true` (idempotent
181
+ merge — only missing keys added, existing version ranges preserved); when no
182
+ frontend package.json exists it prints the list to install. `@pattern-stack/codegen`
183
+ itself gains no runtime dependency.
184
+
185
+ | Package | Range |
186
+ |---|---|
187
+ | `@pattern-stack/frontend-patterns` | `^0.2.0-alpha.18` |
188
+ | `@tanstack/react-db` | `^0.1.55` |
189
+ | `@tanstack/electric-db-collection` | `^0.2.11` |
190
+ | `@tanstack/query-db-collection` | `^1.0.6` |
191
+ | `@tanstack/react-query` | `^5.0.0` |
192
+
193
+ ### `frontend:` config block
194
+
195
+ All knobs are inert unless `generate.frontend: true`. Defaults shown; the block
196
+ is optional (fully defaulted when absent):
197
+
198
+ ```yaml
199
+ frontend:
200
+ auth:
201
+ function: getAuthorizationHeader # auth-header fn; null DISABLES the header
202
+ parsers: # Electric column-type → parser fn source
203
+ timestamptz: '(date: string) => new Date(date)'
204
+ sync:
205
+ mode: electric # global default sync mode (api | electric)
206
+ shapeUrl: /v1/shape # Electric shape base path
207
+ useTableParam: true # emit `params: { table }` shape-URL form
208
+ columnMapper: snakeCamelMapper # Electric column mapper fn; null to omit
209
+ columnMapperNeedsCall: true # call the mapper (fn()) vs reference (fn)
210
+ apiBaseUrlImport: null # when set, import API_BASE_URL from it as baseURL
211
+ apiUrl: /api # REST base path when no apiBaseUrlImport
212
+ ```
213
+
214
+ `null`-disables convention: an **absent** `auth.function` defaults to
215
+ `getAuthorizationHeader`; an **explicit `null`** disables it entirely (no header
216
+ lines emitted). Likewise `sync.columnMapper: null` omits the Electric mapper.
217
+
218
+ ### Per-entity sync mode (`entity.sync`)
219
+
220
+ Each entity may override the global `frontend.sync.mode` inside its `entity:`
221
+ block (sibling to `surface:`/`context:`):
222
+
223
+ ```yaml
224
+ entity:
225
+ name: contact
226
+ plural: contacts
227
+ table: contacts
228
+ sync: api # api | electric — overrides frontend.sync.mode for this entity
229
+ ```
230
+
231
+ `api` wires `queryCollectionOptions` (REST via TanStack Query); `electric` wires
232
+ `electricCollectionOptions` (real-time shape sync). Absent → the global default.
233
+ `offline` (Electric + Dexie) is deferred — the schema rejects it.
234
+
235
+ Cross-entity FK names (file, plural, class, collection var) are resolved against
236
+ the **target entity's own YAML** via the registry — never re-pluralized from a
237
+ string at emit time (so an explicit `plural:` like `person`→`persons` is honored
238
+ by every consumer).
239
+
240
+ ### Providers catalog (`providers.ts`)
241
+
242
+ Providers are gen-time knowledge (`definitions/providers/<slug>.yaml`,
243
+ RFC-0001) — the provider set changes only when code deploys — so the frontend
244
+ catalog is **emitted, not queried**. When the project has provider definitions,
245
+ the emitter renders `generated/providers.ts`:
246
+
247
+ - `PROVIDERS` — every provider, flat (active + planned), slug-sorted:
248
+ `{ provider, name, planned, surfaces, blurb?, hint? }`. Join live rows on
249
+ `provider` (the canonical slug, e.g. `Connection.provider`).
250
+ - `PROVIDER_CATALOG` — grouped into `frontend.catalog.categories` (config
251
+ order) via each provider's `display.category`. Uncategorized providers
252
+ appear only in `PROVIDERS`.
253
+
254
+ Two provider-YAML additions feed it:
255
+
256
+ ```yaml
257
+ # definitions/providers/google.yaml (active — full definition)
258
+ slug: google
259
+ display_name: Google Workspace
260
+ display:
261
+ category: google-workspace # joins frontend.catalog.categories[].id
262
+ hint: connect # optional sub-line on an unconnected tile
263
+ surfaces: [calendar, mail, transcript]
264
+ auth: { ... } # required for active providers
265
+ client: { ... }
266
+
267
+ # definitions/providers/github.yaml (planned — roadmap stub)
268
+ slug: github
269
+ display_name: GitHub
270
+ status: planned # catalog tile only; NO backend emission,
271
+ display: # no auth/client required, surface + import
272
+ category: source-control # cross-checks skipped
273
+ surfaces: [source_control]
274
+ ```
275
+
276
+ ```yaml
277
+ # codegen.config.yaml — the ordered display groups
278
+ frontend:
279
+ catalog:
280
+ categories:
281
+ - id: source-control
282
+ name: Source Control & Issues
283
+ blurb: Repositories, pull requests, issues, and project planning
284
+ ```
285
+
286
+ When the integration for a `planned` provider lands, flip it to `status:
287
+ active` (or drop the key) and add `auth`/`client` — the definitions tree is
288
+ the integration roadmap.
289
+
141
290
  ## Integration Codegen (provider/adapter + assembly + read primitive)
142
291
 
143
292
  When an entity carries a `surface:` tag and `definitions/providers/*.yaml` exist,
@@ -261,9 +410,14 @@ naming:
261
410
  terminology:
262
411
  command: use-case
263
412
  query: use-case
413
+
414
+ # frontend: # inert unless generate.frontend: true
415
+ # ... # see "Frontend generation" above for the full block
264
416
  ```
265
417
 
266
- Auto-detect your project's conventions with `codegen project scan`.
418
+ The `frontend:` block (auth, parsers, sync) is documented under
419
+ [Frontend generation](#frontend-generation). Auto-detect your project's
420
+ conventions with `codegen project scan`.
267
421
 
268
422
  ## Using in Your Project
269
423
 
@@ -276,6 +430,7 @@ See [docs/GETTING-STARTED.md](docs/GETTING-STARTED.md) for a walkthrough of enti
276
430
  ```
277
431
  src/ Generator source code
278
432
  cli/ Clipanion CLI (noun-verb architecture)
433
+ emitters/ TypeScript emitters (integration, frontend — ADR-038)
279
434
  analyzer/ Graph building, consistency checking
280
435
  parser/ YAML loading, cross-reference resolution
281
436
  scanner/ Project pattern detection
@@ -287,7 +442,7 @@ src/ Generator source code
287
442
  runtime/ Code shipped into consumer projects
288
443
  base-classes/ BaseRepository, BaseService, pattern bases
289
444
  subsystems/ Events, Jobs, Cache, Storage
290
- templates/ Hygen EJS templates
445
+ templates/ Hygen EJS templates (backend pipelines)
291
446
  test/ Baseline snapshots, scaffold integration, smoke test
292
447
  docs/ ADRs, consumer setup, getting started
293
448
  ```
@@ -55,6 +55,38 @@ generation run the two barrels are rewritten and you wire them into
55
55
  | React to an event with a durable async job (the event-to-job bridge) | the `bridge` skill |
56
56
  | Pull/push data from an external system (`IChangeSource` / `IIntegrationSink`) | the `integration` skill |
57
57
 
58
+ ## Frontend generation (`generate.frontend: true`)
59
+
60
+ When on, `entity new` (and `--all`) ends with a **whole-set frontend emitter**
61
+ (ADR-038): the complete data layer is rendered into
62
+ `locations.frontendGenerated` (default `apps/frontend/src/generated/`) from the
63
+ full entity set — REST api client, TanStack DB collections (per-entity
64
+ `sync: api | electric`), `createEntityHooks` wiring, field metadata, a
65
+ plural-keyed `createStore`, `config.ts`, `query-client.ts`, root barrel. Every
66
+ file is a complete write with a `@generated` banner; re-runs are byte-identical.
67
+
68
+ Non-obvious bits:
69
+
70
+ - **The dbEntities contract**: generated files import the plain `<Class>` type
71
+ AND a `<camel>Schema` Zod schema from `locations.dbEntities` per entity. If
72
+ the backend doesn't emit such a package (clean-lite-ps doesn't), the consumer
73
+ provides a shim barrel re-exporting each module's Output DTO.
74
+ - **The consumer mounts two providers** in the app root, both from generated
75
+ code: `QueryClientProvider(queryClient)` ▸ `EntityStoreProvider(store)`
76
+ (`EntityStoreProvider` from `@pattern-stack/frontend-patterns`).
77
+ - **Version pairing**: the emitted imports require
78
+ `@pattern-stack/frontend-patterns` + four TanStack packages — the exact
79
+ ranges are listed in the generated `index.ts` header comment.
80
+ - **Path aliases are assumed**: default imports use `@/…` (and your
81
+ `locations.*.import` values) — wire tsconfig `paths` + the bundler alias.
82
+ - **Providers catalog**: when `definitions/providers/` exists, the emitter also
83
+ renders `providers.ts` — `PROVIDERS` (flat) + `PROVIDER_CATALOG` (grouped by
84
+ each provider's `display.category` into `frontend.catalog.categories`).
85
+ Providers are gen-time knowledge: the catalog is emitted, never queried, and
86
+ never hand-duplicated in the frontend. `status: planned` provider YAMLs are
87
+ roadmap stubs — catalog tile only, no auth/client needed, skipped by all
88
+ backend emission.
89
+
58
90
  ## CLI quick reference
59
91
 
60
92
  ```bash
@@ -48,6 +48,8 @@ module — plus an entry in the `GENERATED_MODULES` and schema barrels.
48
48
  entity:
49
49
  name: account # singular snake_case
50
50
  pattern: Synced # Base | Synced | Activity | Metadata | Knowledge (or app-defined)
51
+ # sync: api # frontend collection mode (api | electric) — overrides
52
+ # # frontend.sync.mode; only meaningful with generate.frontend: true
51
53
 
52
54
  fields:
53
55
  name:
@@ -1,9 +1,9 @@
1
- import {
2
- WebhookChangeSource
3
- } from "./chunk-TIZXQU26.js";
4
1
  import {
5
2
  PollChangeSource
6
3
  } from "./chunk-4MF3HKJA.js";
4
+ import {
5
+ WebhookChangeSource
6
+ } from "./chunk-TIZXQU26.js";
7
7
 
8
8
  // runtime/subsystems/integration/build-change-source.ts
9
9
  function buildChangeSource(cfg, fetch, middlewares = []) {
@@ -26,4 +26,4 @@ function buildChangeSource(cfg, fetch, middlewares = []) {
26
26
  export {
27
27
  buildChangeSource
28
28
  };
29
- //# sourceMappingURL=chunk-I6UXRJ3Q.js.map
29
+ //# sourceMappingURL=chunk-43SBT72G.js.map
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  EnvEncryptionKey
3
3
  } from "./chunk-IP4OO26U.js";
4
- import {
5
- AuthController
6
- } from "./chunk-SZVPIHWE.js";
7
4
  import {
8
5
  DrizzleOAuthStateStore
9
6
  } from "./chunk-N5OTOWTP.js";
10
7
  import {
11
8
  MemoryOAuthStateStore
12
9
  } from "./chunk-QLTJSCE6.js";
10
+ import {
11
+ AuthController
12
+ } from "./chunk-SZVPIHWE.js";
13
13
  import {
14
14
  AUTH_OPTIONS,
15
15
  ENCRYPTION_KEY,
@@ -89,4 +89,4 @@ AuthModule = __decorateClass([
89
89
  export {
90
90
  AuthModule
91
91
  };
92
- //# sourceMappingURL=chunk-T6SCOJF4.js.map
92
+ //# sourceMappingURL=chunk-7LKAMLV4.js.map
@@ -349,7 +349,14 @@ var EntityConfigSchema = z.object({
349
349
  context: z.string().regex(
350
350
  /^[a-z][a-z0-9_]*$/,
351
351
  "context must be lowercase snake_case (e.g. 'integration')"
352
- ).optional()
352
+ ).optional(),
353
+ // ADR-038: per-entity frontend sync mode. Overrides global frontend.sync.mode.
354
+ // 'api' → queryCollectionOptions (REST via TanStack Query)
355
+ // 'electric' → electricCollectionOptions (real-time shape sync)
356
+ // 'offline' (Electric + Dexie) is deferred — see
357
+ // docs/specs/2026-06-04-frontend-pipeline-rebuild.md OQ-6.
358
+ // Sibling to `surface:`/`context:`; lives inside the `entity:` block.
359
+ sync: z.enum(["api", "electric"]).optional()
353
360
  }).strict().refine((d) => !(d.pattern && d.patterns), {
354
361
  message: "'pattern' and 'patterns' are mutually exclusive"
355
362
  });
@@ -1004,6 +1011,12 @@ var ClientSchema = z5.object({
1004
1011
  class: ImportRefSchema,
1005
1012
  base_url: z5.string().url("client.base_url must be an absolute URL")
1006
1013
  }).strict();
1014
+ var DisplaySchema = z5.object({
1015
+ category: z5.string().optional(),
1016
+ blurb: z5.string().optional(),
1017
+ // Sub-line shown on an unconnected ("available") tile.
1018
+ hint: z5.string().optional()
1019
+ }).strict();
1007
1020
  var ProviderDefinitionSchema = z5.object({
1008
1021
  // Provider id — the canonical string used as detection: keys, audit rows,
1009
1022
  // subscription rows. kebab/lower; unique across definitions/providers/
@@ -1013,19 +1026,49 @@ var ProviderDefinitionSchema = z5.object({
1013
1026
  "slug must be kebab-case lower (e.g. 'google', 'hubspot')"
1014
1027
  ),
1015
1028
  display_name: z5.string().optional(),
1016
- auth: AuthSchema,
1017
- client: ClientSchema,
1029
+ // Lifecycle: 'active' providers drive backend provider/adapter emission
1030
+ // and require auth + client. 'planned' providers are roadmap stubs — they
1031
+ // appear in the frontend catalog (as unconnectable tiles) but are skipped
1032
+ // by all backend emission and by the surface/import cross-checks, so a
1033
+ // stub can exist before its surface has entities or its strategy/client
1034
+ // are written.
1035
+ status: z5.enum(["active", "planned"]).default("active"),
1036
+ // Frontend catalog presentation (see DisplaySchema).
1037
+ display: DisplaySchema.optional(),
1038
+ // Required iff status === 'active'; see superRefine below.
1039
+ auth: AuthSchema.optional(),
1040
+ client: ClientSchema.optional(),
1018
1041
  // Surfaces this provider serves (ADR-0006: surfaces span contexts — one
1019
1042
  // Google OAuth feeds calendar+mail+transcript). Each must reference a real
1020
1043
  // `surface:` declared on some entity; that cross-check is in
1021
- // validate-providers.ts. Non-empty enforced here.
1044
+ // validate-providers.ts (skipped for 'planned'). Non-empty enforced here.
1022
1045
  surfaces: z5.array(z5.string()).min(1, "surfaces must list at least one surface"),
1023
1046
  // Optional auth lifecycle hints consumed by provider-module emission (D2).
1024
1047
  // `refresh_behavior` is left as a free string in D1 — its domain firms up
1025
1048
  // when D2 consumes it; carrying it now keeps the YAML lossless.
1026
1049
  token_lifetime: z5.number().int().positive().optional(),
1027
1050
  refresh_behavior: z5.string().optional()
1028
- }).strict();
1051
+ }).strict().superRefine((def, ctx) => {
1052
+ if (def.status === "active") {
1053
+ if (!def.auth) {
1054
+ ctx.addIssue({
1055
+ code: z5.ZodIssueCode.custom,
1056
+ message: "auth is required when status is 'active'",
1057
+ path: ["auth"]
1058
+ });
1059
+ }
1060
+ if (!def.client) {
1061
+ ctx.addIssue({
1062
+ code: z5.ZodIssueCode.custom,
1063
+ message: "client is required when status is 'active'",
1064
+ path: ["client"]
1065
+ });
1066
+ }
1067
+ }
1068
+ });
1069
+ function isActiveProvider(def) {
1070
+ return def.status !== "planned";
1071
+ }
1029
1072
 
1030
1073
  // src/utils/yaml-loader.ts
1031
1074
  function loadEntityFromYaml(filePath) {
@@ -1320,6 +1363,7 @@ function transformToEntity(result) {
1320
1363
  patterns: definition.entity.patterns,
1321
1364
  patternConfig: definition.entity.config,
1322
1365
  scopeable: definition.entity.scopeable ?? false,
1366
+ expose: definition.entity.expose ?? ["repository", "rest", "trpc"],
1323
1367
  folderStructure: definition.entity.folder_structure ?? "nested",
1324
1368
  fields: /* @__PURE__ */ new Map(),
1325
1369
  relationships: /* @__PURE__ */ new Map(),
@@ -1664,9 +1708,75 @@ function resolveRelationshipReferences(relationshipDefs, entities) {
1664
1708
  return issues;
1665
1709
  }
1666
1710
 
1711
+ // src/parser/entity-registry.ts
1712
+ import { resolve as resolve3 } from "path";
1713
+ var camelCase = (s) => s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
1714
+ var pascalCase = (s) => {
1715
+ const camel = camelCase(s);
1716
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
1717
+ };
1718
+ function loadErrorToIssue2(error) {
1719
+ const issues = [
1720
+ {
1721
+ severity: "error",
1722
+ type: "parse_error",
1723
+ message: error.error,
1724
+ path: error.filePath
1725
+ }
1726
+ ];
1727
+ if (error.details) {
1728
+ for (const detail of error.details) {
1729
+ issues.push({
1730
+ severity: "error",
1731
+ type: "schema_error",
1732
+ message: detail,
1733
+ path: error.filePath
1734
+ });
1735
+ }
1736
+ }
1737
+ return issues;
1738
+ }
1739
+ function loadEntityRegistry(entitiesDir) {
1740
+ const registry = /* @__PURE__ */ new Map();
1741
+ const issues = [];
1742
+ const resolvedDir = resolve3(entitiesDir);
1743
+ let files;
1744
+ try {
1745
+ files = findYamlFiles(resolvedDir);
1746
+ } catch {
1747
+ issues.push({
1748
+ severity: "error",
1749
+ type: "parse_error",
1750
+ message: `Failed to read directory: ${resolvedDir}`,
1751
+ path: resolvedDir
1752
+ });
1753
+ return { registry, issues };
1754
+ }
1755
+ for (const filePath of files) {
1756
+ const result = loadEntityFromYaml(filePath);
1757
+ if (!result.success) {
1758
+ issues.push(...loadErrorToIssue2(result));
1759
+ continue;
1760
+ }
1761
+ const { entity } = result.definition;
1762
+ registry.set(entity.name, {
1763
+ name: entity.name,
1764
+ plural: entity.plural,
1765
+ // authoritative — never derived
1766
+ table: entity.table,
1767
+ className: pascalCase(entity.name),
1768
+ classNamePlural: pascalCase(entity.plural),
1769
+ camelName: camelCase(entity.name),
1770
+ pluralCamelName: camelCase(entity.plural),
1771
+ sync: entity.sync ?? null
1772
+ });
1773
+ }
1774
+ return { registry, issues };
1775
+ }
1776
+
1667
1777
  // src/parser/validate-providers.ts
1668
1778
  import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "fs";
1669
- import { isAbsolute, join as join2, resolve as resolve3 } from "path";
1779
+ import { isAbsolute, join as join2, resolve as resolve4 } from "path";
1670
1780
  import ts from "typescript";
1671
1781
  function collectEntitySurfaces(entities) {
1672
1782
  const surfaces = /* @__PURE__ */ new Set();
@@ -1698,7 +1808,7 @@ function resolveModuleFile(importPath, opts) {
1698
1808
  }
1699
1809
  }
1700
1810
  if (base === null) {
1701
- base = isAbsolute(importPath) ? importPath : resolve3(opts.sourceRoot, importPath);
1811
+ base = isAbsolute(importPath) ? importPath : resolve4(opts.sourceRoot, importPath);
1702
1812
  }
1703
1813
  const candidates = [
1704
1814
  base,
@@ -1769,6 +1879,7 @@ function validateProviders(providers, opts) {
1769
1879
  }
1770
1880
  for (const { definition, filePath } of providers) {
1771
1881
  const { slug } = definition;
1882
+ if (definition.status === "planned") continue;
1772
1883
  for (const surface of definition.surfaces) {
1773
1884
  if (!knownSurfaces.has(surface)) {
1774
1885
  const known = [...knownSurfaces].sort().join(", ") || "(none declared)";
@@ -1790,6 +1901,7 @@ function validateProviders(providers, opts) {
1790
1901
  sourceRoot: opts.sourceRoot,
1791
1902
  aliases: opts.aliases
1792
1903
  };
1904
+ if (!isActiveProvider(definition)) continue;
1793
1905
  const refs = [
1794
1906
  { field: "auth.strategy", ref: definition.auth.strategy },
1795
1907
  { field: "client.class", ref: definition.client.class }
@@ -3990,6 +4102,7 @@ export {
3990
4102
  validateJunctionDefinition,
3991
4103
  safeValidateJunctionDefinition,
3992
4104
  parseImportRef,
4105
+ isActiveProvider,
3993
4106
  loadEntityFromYaml,
3994
4107
  loadEntitiesFromYaml,
3995
4108
  loadRelationshipFromYaml,
@@ -3999,6 +4112,7 @@ export {
3999
4112
  detectYamlType,
4000
4113
  loadEntities,
4001
4114
  loadRelationships,
4115
+ loadEntityRegistry,
4002
4116
  collectEntitySurfaces,
4003
4117
  validateProviders,
4004
4118
  buildDomainGraph,
@@ -4049,4 +4163,4 @@ export {
4049
4163
  analyzeDomain,
4050
4164
  validateEntities
4051
4165
  };
4052
- //# sourceMappingURL=chunk-IOQMMH6C.js.map
4166
+ //# sourceMappingURL=chunk-F7KN3U6U.js.map