@pattern-stack/codegen 0.12.1 → 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,42 @@ 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
+
7
43
  ## [0.12.1] — 2026-05-31
8
44
 
9
45
  Track D (provider/adapter integration codegen) discoverability fix. The 0.12.0
@@ -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);
@@ -8806,7 +8821,7 @@ var EntityNewCommand = class extends Command2 {
8806
8821
  let targets = [];
8807
8822
  if (this.all) {
8808
8823
  const dir = ctx.entitiesDir ?? path13.resolve(ctx.cwd, "entities");
8809
- targets = listEntityYamls2(dir);
8824
+ targets = listEntityYamls2(dir, resolveProvidersDir(ctx));
8810
8825
  if (targets.length === 0) {
8811
8826
  printError(`No entity YAML files found in ${dir}`);
8812
8827
  return 1;
@@ -8824,12 +8839,15 @@ var EntityNewCommand = class extends Command2 {
8824
8839
  if (result.success) {
8825
8840
  validated.push({ file, name: result.definition.entity.name });
8826
8841
  } else {
8827
- invalid.push({ file, message: result.error });
8842
+ invalid.push({ file, message: result.error, details: result.details });
8828
8843
  }
8829
8844
  }
8830
8845
  if (invalid.length > 0 && !this.continueOnError) {
8831
8846
  for (const i of invalid) {
8832
8847
  printError(`${path13.basename(i.file)} \u2014 ${i.message}`);
8848
+ for (const detail of i.details ?? []) {
8849
+ printError(` \u2022 ${detail}`);
8850
+ }
8833
8851
  }
8834
8852
  if (!isJsonMode()) {
8835
8853
  return 1;
@@ -8837,7 +8855,9 @@ var EntityNewCommand = class extends Command2 {
8837
8855
  }
8838
8856
  const entitiesDirForEmits = ctx.entitiesDir ?? path13.resolve(ctx.cwd, "entities");
8839
8857
  const eventsDirForEmits = resolveEventsDir(ctx);
8840
- const allEntitiesForEmits = loadEntities(entitiesDirForEmits).entities;
8858
+ const allEntitiesForEmits = loadEntities(entitiesDirForEmits, {
8859
+ excludeDirs: [resolveProvidersDir(ctx)]
8860
+ }).entities;
8841
8861
  const validatedNames = new Set(validated.map((v) => v.name));
8842
8862
  const emitsTargetEntities = allEntitiesForEmits.filter(
8843
8863
  (e) => validatedNames.has(e.name)
@@ -9169,19 +9189,16 @@ var EntityNewCommand = class extends Command2 {
9169
9189
  }
9170
9190
  let providerResult = null;
9171
9191
  try {
9172
- const providersDir = ctx.config?.paths?.providers != null ? path13.resolve(
9173
- ctx.cwd,
9174
- ctx.config.paths.providers
9175
- ) : path13.resolve(ctx.cwd, "definitions/providers");
9192
+ const providersDir = resolveProvidersDir(ctx);
9176
9193
  const providerOutputRoot = path13.resolve(
9177
9194
  ctx.cwd,
9178
9195
  backendSrcForHandlers,
9179
9196
  "integrations/providers"
9180
9197
  );
9181
9198
  const entitySurfaces = fs9.existsSync(entitiesDir) ? collectEntitySurfaces(
9182
- loadEntitiesFromYaml(findYamlFiles(entitiesDir)).successes.map(
9183
- (s) => s.definition
9184
- )
9199
+ loadEntitiesFromYaml(
9200
+ findYamlFiles(entitiesDir, { excludeDirs: [providersDir] })
9201
+ ).successes.map((s) => s.definition)
9185
9202
  ) : /* @__PURE__ */ new Set();
9186
9203
  const tsAliases = resolveTsconfigAliases(ctx.cwd);
9187
9204
  providerResult = generateProviderModules({
@@ -9219,9 +9236,9 @@ var EntityNewCommand = class extends Command2 {
9219
9236
  backendSrcForHandlers,
9220
9237
  "integrations"
9221
9238
  );
9222
- const entityDefs = fs9.existsSync(entitiesDir) ? loadEntitiesFromYaml(findYamlFiles(entitiesDir)).successes.map(
9223
- (s) => s.definition
9224
- ) : [];
9239
+ const entityDefs = fs9.existsSync(entitiesDir) ? loadEntitiesFromYaml(
9240
+ findYamlFiles(entitiesDir, { excludeDirs: [providersDir] })
9241
+ ).successes.map((s) => s.definition) : [];
9225
9242
  const loadedProviders = loadProvidersFromYaml(
9226
9243
  findYamlFiles(providersDir)
9227
9244
  ).successes.map((s) => ({
@@ -9367,7 +9384,7 @@ var EntityListCommand = class extends Command2 {
9367
9384
  printError("No entities directory found.");
9368
9385
  return 1;
9369
9386
  }
9370
- const files = listEntityYamls2(ctx.entitiesDir);
9387
+ const files = listEntityYamls2(ctx.entitiesDir, resolveProvidersDir(ctx));
9371
9388
  const rows = files.map(summarizeEntityFile).filter((r) => r !== null).filter((r) => this.pattern ? r.pattern === this.pattern : true);
9372
9389
  if (isJsonMode()) {
9373
9390
  printJson({