@pattern-stack/codegen 0.12.0 → 0.12.2

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/CHANGELOG.md CHANGED
@@ -4,6 +4,66 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.12.2] — 2026-05-31
8
+
9
+ Track D consumer-CLI fix. The 0.12.0/0.12.1 generator was correct in the
10
+ hermetic D7 path but broke for real consumers driving it through the CLI. Two
11
+ schema/loader bugs plus a DX gap that masked the first. No swe-brain YAML change
12
+ is required — consumer YAML already wrote the keys in the natural place; the
13
+ schema is now corrected to match.
14
+
15
+ ### Fixed
16
+
17
+ - **`entity.surface` / `entity.context` schema level.** `surface:` and
18
+ `context:` were defined at the ROOT of `EntityDefinitionSchema` (sibling of
19
+ `entity:`/`fields:`), but consumers naturally write them INSIDE the `entity:`
20
+ block (next to `pattern:`/`name:`/`table:`). Because the `entity:` block is
21
+ `.strict()`, those YAMLs were rejected with "Unrecognized key(s) in object:
22
+ 'surface' at 'entity'". The fields now live in `EntityConfigSchema` and are
23
+ read as `entity.surface` / `entity.context`. Clean break — root-level
24
+ placement no longer validates. Read sites updated:
25
+ `collectEntitySurfaces` (`validate-providers.ts`), `collectEntitiesBySurface`
26
+ (`adapter-emission-generator.ts`), the provider surface cross-check
27
+ (`entity.ts`), and the clean-lite-ps output-subfolder consumer
28
+ (`prompt-extension.js` `buildCleanLitePsLocals`).
29
+ - **Entity `--all` discovery no longer globs `definitions/providers/`.** With
30
+ `entities_dir: definitions`, the recursive YAML walk pulled provider files
31
+ into the entity loader, where they fail entity validation. Entity discovery
32
+ (`findYamlFiles`, `loadEntities`, `listEntityYamls`) now excludes the
33
+ configured providers dir (`paths.providers`, default `definitions/providers`).
34
+ Provider files route only through `ProviderDefinitionSchema`.
35
+
36
+ ### Changed
37
+
38
+ - **`entity new --dry-run` surfaces the Zod detail.** The failure path printed
39
+ only "Validation failed for <file>"; it now emits the same per-issue Zod
40
+ diagnostics as `entity validate`, so a misplaced key reports which key/level
41
+ is wrong (the DX gap that hid the `entity.surface` rejection).
42
+
43
+ ## [0.12.1] — 2026-05-31
44
+
45
+ Track D (provider/adapter integration codegen) discoverability fix. The 0.12.0
46
+ generator wiring is correct and shipped — provider + adapter emission runs as a
47
+ post-step of `codegen entity new` whenever `definitions/providers/*.yaml` exist
48
+ — but nothing in the CLI surfaced that, so consumers searched `--help` for a
49
+ `provider` / `integration` / `gen` command, found none, and mistook a working
50
+ feature for a publish gap. Docs-and-help only; no generator behavior changed.
51
+
52
+ ### Changed
53
+
54
+ - **`codegen entity new --help`** now documents every post-generation step it
55
+ runs (event / bridge / orchestration / **provider + adapter (Track D)**
56
+ codegen), including Track D's trigger (`definitions/providers/*.yaml`), output
57
+ paths (`<backendSrc>/integrations/{providers,}`), emit-once semantics, and an
58
+ explicit note that there is no standalone `provider` / `integration` / `gen`
59
+ command — Track D is driven entirely by re-running `entity new`.
60
+ - **`codegen entity` summary hints** now surface a Track D regen hint when the
61
+ project has a providers directory — a discoverability path that does not
62
+ require reading `entity new --help`.
63
+ - **Integration domain skill** (`protocols-and-ports.md`, `SKILL.md`) documents
64
+ the Track D invocation, the skip-when-no-providers-dir behavior, and that the
65
+ generated scaffold (not the `.d.ts`) is ground truth for adapter port shape.
66
+
7
67
  ## [0.12.0] — 2026-05-31
8
68
 
9
69
  Integration codegen retarget (RFC-0001): a **provider/adapter/surface** model
@@ -12,8 +72,11 @@ for integration codegen, plus a **surface-package framework**. Additive over
12
72
  is unchanged. The one breaking item (the ADR-033.2 per-entity provider tuples)
13
73
  has no consumers. Consumers adopt by adding `definitions/providers/*.yaml` and
14
74
  regenerating. Ships alongside four independently-versioned **surface packages**
15
- (`@pattern-stack/codegen-{crm,calendar,mail,transcript}`) publishing fresh at
16
- `0.1.0`.
75
+ (`@pattern-stack/codegen-{crm,calendar,mail,transcript}`) at `0.1.1`. (Their
76
+ initial `0.1.0` release peer-depended on `@pattern-stack/codegen` `^0.11.0`,
77
+ which predates the `./subsystems` export they require; `0.1.1` corrects the peer
78
+ to `^0.12.0`. The peer source fix merged with 0.12.0 but the version bump was
79
+ missed — this completes it.)
17
80
 
18
81
  ### ⚠ BREAKING CHANGES
19
82
 
@@ -41,7 +104,7 @@ regenerating. Ships alongside four independently-versioned **surface packages**
41
104
  conformance helper (on the `/testing` subpath).
42
105
  - **`@pattern-stack/codegen-{calendar,mail,transcript}`** — incremental-read
43
106
  interaction surfaces (`CalendarPort` / `MailPort` / `TranscriptPort`,
44
- canonical types, capability descriptors). Independently versioned (`0.1.0`).
107
+ canonical types, capability descriptors). Independently versioned (`0.1.1`).
45
108
  - **Track D — provider/adapter integration codegen (RFC-0001).**
46
109
  - `definitions/providers/<provider>.yaml` — providers as first-class
47
110
  declarative artifacts (slug, auth strategy, client, surfaces); Zod schema +
@@ -30,14 +30,17 @@ import { join, resolve } from "path";
30
30
  function isYaml(name) {
31
31
  return name.endsWith(".yaml") || name.endsWith(".yml");
32
32
  }
33
- function findYamlFiles(dir) {
33
+ function findYamlFiles(dir, opts) {
34
34
  const root = resolve(dir);
35
35
  const out = [];
36
+ const excluded = new Set((opts?.excludeDirs ?? []).map((d) => resolve(d)));
36
37
  const walk = (current) => {
37
38
  for (const entry of readdirSync(current, { withFileTypes: true })) {
38
39
  if (entry.isDirectory()) {
39
40
  if (entry.name.startsWith(".")) continue;
40
- walk(join(current, entry.name));
41
+ const child = join(current, entry.name);
42
+ if (excluded.has(resolve(child))) continue;
43
+ walk(child);
41
44
  } else if (isYaml(entry.name)) {
42
45
  out.push(join(current, entry.name));
43
46
  }
@@ -2312,7 +2315,37 @@ var EntityConfigSchema = z3.object({
2312
2315
  // JOB-7: marks this entity as a valid scope target for job scoping.
2313
2316
  // Drives the generated ScopeEntityType union in
2314
2317
  // runtime/subsystems/jobs/generated/scope-entity-type.ts.
2315
- scopeable: z3.boolean().optional()
2318
+ scopeable: z3.boolean().optional(),
2319
+ // RFC-0001 §1/§8: the integration *surface* this entity belongs to
2320
+ // (e.g. 'calendar', 'mail', 'crm'). Surfaces span provider contexts
2321
+ // (ADR-0006) — one Google OAuth feeds calendar+mail+transcript. The union
2322
+ // of `surface:` values across all entity YAML is the closed set that a
2323
+ // provider's `surfaces:` must be a subset of (cross-checked in
2324
+ // src/parser/validate-providers.ts). Optional: entities without an
2325
+ // integration surface omit it. The surface-package *emission* convention
2326
+ // is Track C (#329); this field is only the declarative input both tracks
2327
+ // read. Lives inside the `entity:` block (next to `pattern:`/`name:`/`table:`).
2328
+ surface: z3.string().optional(),
2329
+ // Bounded-context declaration (ADR-0004) — "which bounded context this
2330
+ // entity belongs to". This is the DURABLE decision; it is a plain
2331
+ // bounded-context slug, NOT a folder knob. Different features consume it:
2332
+ //
2333
+ // - #403 (the FIRST consumer): drives the generated code's
2334
+ // module output folder. clean-lite-ps nests the entity's module under
2335
+ // `<modules>/<context>/<entity>/` so same-context entities group
2336
+ // together; untagged entities stay flat (`<modules>/<entity>/`).
2337
+ // - ADR-0004 (deferred): a later `naming: prefix | schema` knob reads
2338
+ // this SAME field to drive the Postgres physical layout —
2339
+ // `prefix` → `pgTable('<context>__<table>')`, then the flip to
2340
+ // `schema` → `pgSchema('<context>').table('<table>')`. NOT wired here.
2341
+ //
2342
+ // Sibling to `surface:` and orthogonal to it (ADR-0006): context = model
2343
+ // cohesion (which domain), surface = vendor composition (which integration).
2344
+ // Lives inside the `entity:` block (next to `pattern:`/`name:`/`table:`).
2345
+ context: z3.string().regex(
2346
+ /^[a-z][a-z0-9_]*$/,
2347
+ "context must be lowercase snake_case (e.g. 'integration')"
2348
+ ).optional()
2316
2349
  }).strict().refine((d) => !(d.pattern && d.patterns), {
2317
2350
  message: "'pattern' and 'patterns' are mutually exclusive"
2318
2351
  });
@@ -2465,36 +2498,11 @@ var EntityDefinitionSchema = z3.object({
2465
2498
  // appear in `integration.providers` — see the superRefine on
2466
2499
  // `EntityDefinitionSchema` below.
2467
2500
  detection: z3.record(z3.string(), DetectionConfigSchema).optional(),
2468
- // RFC-0001 §1/§8: the integration *surface* this entity belongs to
2469
- // (e.g. 'calendar', 'mail', 'crm'). Surfaces span provider contexts
2470
- // (ADR-0006) one Google OAuth feeds calendar+mail+transcript. The union
2471
- // of `surface:` values across all entity YAML is the closed set that a
2472
- // provider's `surfaces:` must be a subset of (cross-checked in
2473
- // src/parser/validate-providers.ts). Optional: entities without an
2474
- // integration surface omit it. The surface-package *emission* convention
2475
- // is Track C (#329); this field is only the declarative input both tracks
2476
- // read.
2477
- surface: z3.string().optional(),
2478
- // Bounded-context declaration (ADR-0004) — "which bounded context this
2479
- // entity belongs to". This is the DURABLE decision; it is a plain
2480
- // bounded-context slug, NOT a folder knob. Different features consume it:
2481
- //
2482
- // - #403 (this PR, the FIRST consumer): drives the generated code's
2483
- // module output folder. clean-lite-ps nests the entity's module under
2484
- // `<modules>/<context>/<entity>/` so same-context entities group
2485
- // together; untagged entities stay flat (`<modules>/<entity>/`).
2486
- // - ADR-0004 (deferred): a later `naming: prefix | schema` knob reads
2487
- // this SAME field to drive the Postgres physical layout —
2488
- // `prefix` → `pgTable('<context>__<table>')`, then the flip to
2489
- // `schema` → `pgSchema('<context>').table('<table>')`. NOT wired here;
2490
- // #403 makes no table/column/schema changes.
2491
- //
2492
- // Sibling to `surface:` and orthogonal to it (ADR-0006): context = model
2493
- // cohesion (which domain), surface = vendor composition (which integration).
2494
- context: z3.string().regex(
2495
- /^[a-z][a-z0-9_]*$/,
2496
- "context must be lowercase snake_case (e.g. 'integration')"
2497
- ).optional(),
2501
+ // NOTE: `surface:` and `context:` moved INTO EntityConfigSchema (the
2502
+ // `entity:` block) in 0.12.2 consumers write them next to
2503
+ // `pattern:`/`name:`/`table:`, which is the natural place. They are
2504
+ // read via `entity.surface` / `entity.context`. Clean break: no
2505
+ // root-level placement is accepted.
2498
2506
  // v2: Domain event declarations (CODEGEN-EVOLUTION-PLAN Phase 2)
2499
2507
  // Generates typed event classes, handlers, and queue registration
2500
2508
  events: z3.array(EventDeclarationSchema).optional(),
@@ -3409,13 +3417,13 @@ function loadErrorToIssue(error) {
3409
3417
  }
3410
3418
  return issues;
3411
3419
  }
3412
- function loadEntities(entitiesDir) {
3420
+ function loadEntities(entitiesDir, opts) {
3413
3421
  const entities = [];
3414
3422
  const issues = [];
3415
3423
  const resolvedDir = resolve2(entitiesDir);
3416
3424
  let files;
3417
3425
  try {
3418
- files = findYamlFiles(resolvedDir);
3426
+ files = findYamlFiles(resolvedDir, { excludeDirs: opts?.excludeDirs });
3419
3427
  } catch (err) {
3420
3428
  issues.push({
3421
3429
  severity: "error",
@@ -3650,7 +3658,7 @@ import ts from "typescript";
3650
3658
  function collectEntitySurfaces(entities) {
3651
3659
  const surfaces = /* @__PURE__ */ new Set();
3652
3660
  for (const e of entities) {
3653
- if (e.surface) surfaces.add(e.surface);
3661
+ if (e.entity.surface) surfaces.add(e.entity.surface);
3654
3662
  }
3655
3663
  return surfaces;
3656
3664
  }
@@ -6019,8 +6027,8 @@ function collectEntities(entitiesDir) {
6019
6027
  entities.push({
6020
6028
  name: def.entity.name,
6021
6029
  plural: def.entity.plural,
6022
- // #403: top-level `context:` nests the module folder; undefined → flat.
6023
- context: def.context
6030
+ // #403: `entity.context:` nests the module folder; undefined → flat.
6031
+ context: def.entity.context
6024
6032
  });
6025
6033
  }
6026
6034
  entities.sort((a, b) => a.name.localeCompare(b.name));
@@ -8297,10 +8305,11 @@ function generatedBanner(sourceDesc) {
8297
8305
  function collectEntitiesBySurface(entities) {
8298
8306
  const bySurface = /* @__PURE__ */ new Map();
8299
8307
  for (const e of entities) {
8300
- if (!e.surface) continue;
8301
- const list = bySurface.get(e.surface) ?? [];
8308
+ const surface = e.entity.surface;
8309
+ if (!surface) continue;
8310
+ const list = bySurface.get(surface) ?? [];
8302
8311
  list.push(e.entity.name);
8303
- bySurface.set(e.surface, list);
8312
+ bySurface.set(surface, list);
8304
8313
  }
8305
8314
  for (const [surface, list] of bySurface) {
8306
8315
  bySurface.set(surface, [...list].sort());
@@ -8668,9 +8677,15 @@ function printInfo(msg) {
8668
8677
  }
8669
8678
 
8670
8679
  // src/cli/commands/entity.ts
8671
- function listEntityYamls2(dir) {
8680
+ function resolveProvidersDir(ctx) {
8681
+ const fromConfig = ctx.config?.paths?.providers;
8682
+ return fromConfig != null ? path13.resolve(ctx.cwd, fromConfig) : path13.resolve(ctx.cwd, "definitions/providers");
8683
+ }
8684
+ function listEntityYamls2(dir, providersDir) {
8672
8685
  if (!fs9.existsSync(dir)) return [];
8673
- return findYamlFiles(dir);
8686
+ return findYamlFiles(dir, {
8687
+ excludeDirs: providersDir ? [providersDir] : []
8688
+ });
8674
8689
  }
8675
8690
  function summarizePatternLabel(entity) {
8676
8691
  if (typeof entity.pattern === "string" && entity.pattern.length > 0) {
@@ -8709,7 +8724,7 @@ async function summary(ctx) {
8709
8724
  ]
8710
8725
  };
8711
8726
  }
8712
- const files = listEntityYamls2(ctx.entitiesDir);
8727
+ const files = listEntityYamls2(ctx.entitiesDir, resolveProvidersDir(ctx));
8713
8728
  const rows = files.map(summarizeEntityFile).filter((r) => r !== null);
8714
8729
  const patterns = new Set(rows.map((r) => r.pattern));
8715
8730
  const queryCount = rows.reduce((sum, r) => sum + r.queries, 0);
@@ -8740,20 +8755,45 @@ async function hints(ctx) {
8740
8755
  }
8741
8756
  ];
8742
8757
  }
8743
- return [
8758
+ const baseHints = [
8744
8759
  { command: "codegen entity new <file>", description: "Generate one entity" },
8745
8760
  { command: "codegen entity new --all", description: "Regenerate all entities" },
8746
8761
  { command: "codegen entity validate", description: "Validate YAML definitions" },
8747
8762
  { command: "codegen entity list", description: "List entities as a table" }
8748
8763
  ];
8764
+ const providersDir = ctx.config?.paths?.providers != null ? path13.resolve(
8765
+ ctx.cwd,
8766
+ ctx.config.paths.providers
8767
+ ) : path13.resolve(ctx.cwd, "definitions/providers");
8768
+ if (fs9.existsSync(providersDir)) {
8769
+ baseHints.push({
8770
+ command: "codegen entity new --all",
8771
+ description: "Regenerate provider modules + adapter scaffolds (Track D)"
8772
+ });
8773
+ }
8774
+ return baseHints;
8749
8775
  }
8750
8776
  var EntityNewCommand = class extends Command2 {
8751
8777
  static paths = [["entity", "new"]];
8752
8778
  static usage = Command2.Usage({
8753
8779
  description: "Generate code for one or more entities from YAML",
8780
+ details: `
8781
+ Generates Clean Architecture code for the named entity (or all entities with \`--all\`), then runs the post-generation codegen steps that share this entrypoint:
8782
+
8783
+ - **Event codegen** \u2014 \`AppDomainEvent\` union + typed bus from \`events/*.yaml\`.
8784
+ - **Bridge registry** \u2014 when the bridge subsystem is installed.
8785
+ - **Orchestration modules** \u2014 from orchestration patterns.
8786
+ - **Provider + adapter codegen (Track D, RFC-0001)** \u2014 when \`definitions/providers/*.yaml\` exist, emits one provider module per file into \`<backendSrc>/integrations/providers/\` and the matching adapter scaffolds + \`@generated\` files into \`<backendSrc>/integrations/\`. Author-owned scaffolds are emit-once (never overwritten); \`@generated\` files re-emit each run. With no providers dir the step is silently skipped.
8787
+
8788
+ There is no separate \`provider\`, \`integration\`, or \`gen\` command \u2014 Track D codegen is driven entirely by re-running \`entity new\`. The \`just gen\` / \`just gen-all\` recipes are thin wrappers over it.
8789
+ `,
8754
8790
  examples: [
8755
8791
  ["Generate a single entity", "codegen entity new entities/contact.yaml"],
8756
8792
  ["Regenerate all entities", "codegen entity new --all"],
8793
+ [
8794
+ "Regenerate everything incl. provider/adapter codegen",
8795
+ "codegen entity new --all"
8796
+ ],
8757
8797
  ["Preview without writing", "codegen entity new entities/contact.yaml --dry-run"]
8758
8798
  ]
8759
8799
  });
@@ -8781,7 +8821,7 @@ var EntityNewCommand = class extends Command2 {
8781
8821
  let targets = [];
8782
8822
  if (this.all) {
8783
8823
  const dir = ctx.entitiesDir ?? path13.resolve(ctx.cwd, "entities");
8784
- targets = listEntityYamls2(dir);
8824
+ targets = listEntityYamls2(dir, resolveProvidersDir(ctx));
8785
8825
  if (targets.length === 0) {
8786
8826
  printError(`No entity YAML files found in ${dir}`);
8787
8827
  return 1;
@@ -8799,12 +8839,15 @@ var EntityNewCommand = class extends Command2 {
8799
8839
  if (result.success) {
8800
8840
  validated.push({ file, name: result.definition.entity.name });
8801
8841
  } else {
8802
- invalid.push({ file, message: result.error });
8842
+ invalid.push({ file, message: result.error, details: result.details });
8803
8843
  }
8804
8844
  }
8805
8845
  if (invalid.length > 0 && !this.continueOnError) {
8806
8846
  for (const i of invalid) {
8807
8847
  printError(`${path13.basename(i.file)} \u2014 ${i.message}`);
8848
+ for (const detail of i.details ?? []) {
8849
+ printError(` \u2022 ${detail}`);
8850
+ }
8808
8851
  }
8809
8852
  if (!isJsonMode()) {
8810
8853
  return 1;
@@ -8812,7 +8855,9 @@ var EntityNewCommand = class extends Command2 {
8812
8855
  }
8813
8856
  const entitiesDirForEmits = ctx.entitiesDir ?? path13.resolve(ctx.cwd, "entities");
8814
8857
  const eventsDirForEmits = resolveEventsDir(ctx);
8815
- const allEntitiesForEmits = loadEntities(entitiesDirForEmits).entities;
8858
+ const allEntitiesForEmits = loadEntities(entitiesDirForEmits, {
8859
+ excludeDirs: [resolveProvidersDir(ctx)]
8860
+ }).entities;
8816
8861
  const validatedNames = new Set(validated.map((v) => v.name));
8817
8862
  const emitsTargetEntities = allEntitiesForEmits.filter(
8818
8863
  (e) => validatedNames.has(e.name)
@@ -9144,19 +9189,16 @@ var EntityNewCommand = class extends Command2 {
9144
9189
  }
9145
9190
  let providerResult = null;
9146
9191
  try {
9147
- const providersDir = ctx.config?.paths?.providers != null ? path13.resolve(
9148
- ctx.cwd,
9149
- ctx.config.paths.providers
9150
- ) : path13.resolve(ctx.cwd, "definitions/providers");
9192
+ const providersDir = resolveProvidersDir(ctx);
9151
9193
  const providerOutputRoot = path13.resolve(
9152
9194
  ctx.cwd,
9153
9195
  backendSrcForHandlers,
9154
9196
  "integrations/providers"
9155
9197
  );
9156
9198
  const entitySurfaces = fs9.existsSync(entitiesDir) ? collectEntitySurfaces(
9157
- loadEntitiesFromYaml(findYamlFiles(entitiesDir)).successes.map(
9158
- (s) => s.definition
9159
- )
9199
+ loadEntitiesFromYaml(
9200
+ findYamlFiles(entitiesDir, { excludeDirs: [providersDir] })
9201
+ ).successes.map((s) => s.definition)
9160
9202
  ) : /* @__PURE__ */ new Set();
9161
9203
  const tsAliases = resolveTsconfigAliases(ctx.cwd);
9162
9204
  providerResult = generateProviderModules({
@@ -9194,9 +9236,9 @@ var EntityNewCommand = class extends Command2 {
9194
9236
  backendSrcForHandlers,
9195
9237
  "integrations"
9196
9238
  );
9197
- const entityDefs = fs9.existsSync(entitiesDir) ? loadEntitiesFromYaml(findYamlFiles(entitiesDir)).successes.map(
9198
- (s) => s.definition
9199
- ) : [];
9239
+ const entityDefs = fs9.existsSync(entitiesDir) ? loadEntitiesFromYaml(
9240
+ findYamlFiles(entitiesDir, { excludeDirs: [providersDir] })
9241
+ ).successes.map((s) => s.definition) : [];
9200
9242
  const loadedProviders = loadProvidersFromYaml(
9201
9243
  findYamlFiles(providersDir)
9202
9244
  ).successes.map((s) => ({
@@ -9342,7 +9384,7 @@ var EntityListCommand = class extends Command2 {
9342
9384
  printError("No entities directory found.");
9343
9385
  return 1;
9344
9386
  }
9345
- const files = listEntityYamls2(ctx.entitiesDir);
9387
+ const files = listEntityYamls2(ctx.entitiesDir, resolveProvidersDir(ctx));
9346
9388
  const rows = files.map(summarizeEntityFile).filter((r) => r !== null).filter((r) => this.pattern ? r.pattern === this.pattern : true);
9347
9389
  if (isJsonMode()) {
9348
9390
  printJson({