@pattern-stack/codegen 0.12.1 → 0.13.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.
@@ -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
  }
@@ -1117,12 +1120,33 @@ var EventIdCursorSchema = z2.object({
1117
1120
  kind: z2.literal("eventId"),
1118
1121
  field: z2.string().min(1)
1119
1122
  });
1123
+ var HistoryIdCursorSchema = z2.object({
1124
+ kind: z2.literal("historyId"),
1125
+ field: z2.string().min(1)
1126
+ });
1127
+ var SyncTokenCursorSchema = z2.object({
1128
+ kind: z2.literal("syncToken"),
1129
+ field: z2.string().min(1)
1130
+ });
1120
1131
  var CursorStrategySchema = z2.discriminatedUnion("kind", [
1121
1132
  SystemModstampCursorSchema,
1122
1133
  ReplayIdCursorSchema,
1123
1134
  TimestampCursorSchema,
1124
- EventIdCursorSchema
1135
+ EventIdCursorSchema,
1136
+ HistoryIdCursorSchema,
1137
+ SyncTokenCursorSchema
1125
1138
  ]);
1139
+ var CURSOR_DIVISIBILITY = {
1140
+ systemModstamp: true,
1141
+ timestamp: true,
1142
+ replayId: true,
1143
+ eventId: false,
1144
+ historyId: false,
1145
+ syncToken: false
1146
+ };
1147
+ function isDivisibleCursor(kind) {
1148
+ return CURSOR_DIVISIBILITY[kind];
1149
+ }
1126
1150
  var PollDetectionSchema = z2.object({
1127
1151
  cursor: CursorStrategySchema,
1128
1152
  provenance: z2.enum(["poll", "cdc"]).optional()
@@ -2312,7 +2336,37 @@ var EntityConfigSchema = z3.object({
2312
2336
  // JOB-7: marks this entity as a valid scope target for job scoping.
2313
2337
  // Drives the generated ScopeEntityType union in
2314
2338
  // runtime/subsystems/jobs/generated/scope-entity-type.ts.
2315
- scopeable: z3.boolean().optional()
2339
+ scopeable: z3.boolean().optional(),
2340
+ // RFC-0001 §1/§8: the integration *surface* this entity belongs to
2341
+ // (e.g. 'calendar', 'mail', 'crm'). Surfaces span provider contexts
2342
+ // (ADR-0006) — one Google OAuth feeds calendar+mail+transcript. The union
2343
+ // of `surface:` values across all entity YAML is the closed set that a
2344
+ // provider's `surfaces:` must be a subset of (cross-checked in
2345
+ // src/parser/validate-providers.ts). Optional: entities without an
2346
+ // integration surface omit it. The surface-package *emission* convention
2347
+ // is Track C (#329); this field is only the declarative input both tracks
2348
+ // read. Lives inside the `entity:` block (next to `pattern:`/`name:`/`table:`).
2349
+ surface: z3.string().optional(),
2350
+ // Bounded-context declaration (ADR-0004) — "which bounded context this
2351
+ // entity belongs to". This is the DURABLE decision; it is a plain
2352
+ // bounded-context slug, NOT a folder knob. Different features consume it:
2353
+ //
2354
+ // - #403 (the FIRST consumer): drives the generated code's
2355
+ // module output folder. clean-lite-ps nests the entity's module under
2356
+ // `<modules>/<context>/<entity>/` so same-context entities group
2357
+ // together; untagged entities stay flat (`<modules>/<entity>/`).
2358
+ // - ADR-0004 (deferred): a later `naming: prefix | schema` knob reads
2359
+ // this SAME field to drive the Postgres physical layout —
2360
+ // `prefix` → `pgTable('<context>__<table>')`, then the flip to
2361
+ // `schema` → `pgSchema('<context>').table('<table>')`. NOT wired here.
2362
+ //
2363
+ // Sibling to `surface:` and orthogonal to it (ADR-0006): context = model
2364
+ // cohesion (which domain), surface = vendor composition (which integration).
2365
+ // Lives inside the `entity:` block (next to `pattern:`/`name:`/`table:`).
2366
+ context: z3.string().regex(
2367
+ /^[a-z][a-z0-9_]*$/,
2368
+ "context must be lowercase snake_case (e.g. 'integration')"
2369
+ ).optional()
2316
2370
  }).strict().refine((d) => !(d.pattern && d.patterns), {
2317
2371
  message: "'pattern' and 'patterns' are mutually exclusive"
2318
2372
  });
@@ -2465,36 +2519,11 @@ var EntityDefinitionSchema = z3.object({
2465
2519
  // appear in `integration.providers` — see the superRefine on
2466
2520
  // `EntityDefinitionSchema` below.
2467
2521
  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(),
2522
+ // NOTE: `surface:` and `context:` moved INTO EntityConfigSchema (the
2523
+ // `entity:` block) in 0.12.2 consumers write them next to
2524
+ // `pattern:`/`name:`/`table:`, which is the natural place. They are
2525
+ // read via `entity.surface` / `entity.context`. Clean break: no
2526
+ // root-level placement is accepted.
2498
2527
  // v2: Domain event declarations (CODEGEN-EVOLUTION-PLAN Phase 2)
2499
2528
  // Generates typed event classes, handlers, and queue registration
2500
2529
  events: z3.array(EventDeclarationSchema).optional(),
@@ -3409,13 +3438,13 @@ function loadErrorToIssue(error) {
3409
3438
  }
3410
3439
  return issues;
3411
3440
  }
3412
- function loadEntities(entitiesDir) {
3441
+ function loadEntities(entitiesDir, opts) {
3413
3442
  const entities = [];
3414
3443
  const issues = [];
3415
3444
  const resolvedDir = resolve2(entitiesDir);
3416
3445
  let files;
3417
3446
  try {
3418
- files = findYamlFiles(resolvedDir);
3447
+ files = findYamlFiles(resolvedDir, { excludeDirs: opts?.excludeDirs });
3419
3448
  } catch (err) {
3420
3449
  issues.push({
3421
3450
  severity: "error",
@@ -3650,7 +3679,7 @@ import ts from "typescript";
3650
3679
  function collectEntitySurfaces(entities) {
3651
3680
  const surfaces = /* @__PURE__ */ new Set();
3652
3681
  for (const e of entities) {
3653
- if (e.surface) surfaces.add(e.surface);
3682
+ if (e.entity.surface) surfaces.add(e.entity.surface);
3654
3683
  }
3655
3684
  return surfaces;
3656
3685
  }
@@ -6019,8 +6048,8 @@ function collectEntities(entitiesDir) {
6019
6048
  entities.push({
6020
6049
  name: def.entity.name,
6021
6050
  plural: def.entity.plural,
6022
- // #403: top-level `context:` nests the module folder; undefined → flat.
6023
- context: def.context
6051
+ // #403: `entity.context:` nests the module folder; undefined → flat.
6052
+ context: def.entity.context
6024
6053
  });
6025
6054
  }
6026
6055
  entities.sort((a, b) => a.name.localeCompare(b.name));
@@ -8233,6 +8262,310 @@ import {
8233
8262
  writeFileSync as writeFileSync3
8234
8263
  } from "fs";
8235
8264
  import { dirname as dirname2, join as join12 } from "path";
8265
+
8266
+ // src/cli/shared/sink-emission-generator.ts
8267
+ var SCAFFOLD_SENTINEL = "// <CODEGEN-SCAFFOLD-V1>";
8268
+ var USER_ID_FIELD = "userId";
8269
+ function sinkNames(entityClass) {
8270
+ return {
8271
+ sinkClass: `${entityClass}Sink`,
8272
+ canonicalType: `${entityClass}Canonical`,
8273
+ repoClass: `${entityClass}Repository`,
8274
+ projectionType: `${entityClass}IntegrationProjection`,
8275
+ writeType: `${entityClass}IntegrationWrite`
8276
+ };
8277
+ }
8278
+ function generateDefaultSink(input) {
8279
+ if (input.pattern !== "Integrated") {
8280
+ throw new Error(
8281
+ `cannot emit default integration sink for entity '${input.entityName}': it is 'pattern: ${input.pattern}', but the default sink is emittable only for 'pattern: Integrated' entities (the only family with the integrationUpsertOne / findByExternalIdProjected projection path). Add 'pattern: Integrated' to the entity or provide a hand-authored sink.`
8282
+ );
8283
+ }
8284
+ const n = sinkNames(input.entityClass);
8285
+ const hasUserIdField = input.copyThroughFields.some(
8286
+ (f) => f.camelName === USER_ID_FIELD
8287
+ );
8288
+ const copyThroughLines = input.copyThroughFields.filter((f) => f.camelName !== USER_ID_FIELD).map((f) => ` ${f.camelName}: record.${f.camelName},`);
8289
+ const fkTodoLines = input.fkExternalKeys.map(
8290
+ (fk) => ` // ${fk.writeKey}: /* TODO(author): external id of the related ${relationLabel(fk.writeKey)} */ null,`
8291
+ );
8292
+ const writeBodyLines = [
8293
+ ` externalId: record.externalId,`
8294
+ ];
8295
+ if (copyThroughLines.length > 0) {
8296
+ writeBodyLines.push(
8297
+ ` // copy-through fields (one line per \`fields:\` entry):`,
8298
+ ...copyThroughLines
8299
+ );
8300
+ }
8301
+ if (fkTodoLines.length > 0) {
8302
+ writeBodyLines.push(
8303
+ ` // FK external join-keys \u2014 projection has no external key; supply from your canonical record:`,
8304
+ ...fkTodoLines
8305
+ );
8306
+ }
8307
+ if (hasUserIdField) {
8308
+ writeBodyLines.push(` userId,`);
8309
+ }
8310
+ const writeBody = writeBodyLines.join("\n");
8311
+ return `${SCAFFOLD_SENTINEL}
8312
+ // Scaffolded once by @pattern-stack/codegen, then author-owned. Re-running codegen
8313
+ // detects the sentinel above and SKIPS this file \u2014 your edits are safe.
8314
+ //
8315
+ // Default IIntegrationSink over the generated ${n.repoClass}. The PLUMBING
8316
+ // (constructor, provider-match assert, repo delegation, userId scoping, return
8317
+ // shapes) is generated. The canonical<->local FIELD MAPPING is the author seam:
8318
+ // the canonical type is whatever your adapter's changeSource yields \u2014 the same
8319
+ // seam as the IChangeSource.listChanges fetch body. For FK-free entities the
8320
+ // generated ${n.projectionType} IS the canonical shape (passthrough);
8321
+ // for entities with external FK join-keys, fill the marked TODO(s) below.
8322
+ // Source: definitions entity '${input.entityName}' (surface: ${input.surface}).
8323
+ import { Injectable } from '@nestjs/common';
8324
+ import type { IIntegrationSink } from '@pattern-stack/codegen/subsystems';
8325
+ import {
8326
+ ${n.repoClass},
8327
+ type ${n.projectionType},
8328
+ type ${n.writeType},
8329
+ } from '${input.repoImportSpecifier}';
8330
+
8331
+ /** Canonical type the orchestrator diffs. Defaults to the generated projection;
8332
+ * widen to your adapter's canonical shape if it carries fields the projection
8333
+ * does not (e.g. external FK join-keys). */
8334
+ export type ${n.canonicalType} = ${n.projectionType};
8335
+
8336
+ @Injectable()
8337
+ export class ${n.sinkClass} implements IIntegrationSink<${n.canonicalType}> {
8338
+ constructor(
8339
+ private readonly repo: ${n.repoClass},
8340
+ private readonly provider: string,
8341
+ ) {}
8342
+
8343
+ async findByExternalId(userId: string, externalId: string): Promise<${n.canonicalType} | null> {
8344
+ const row = await this.repo.findByExternalIdProjected(externalId, this.provider);
8345
+ // The repo lookup is (provider, externalId)-scoped. If your external_id is not
8346
+ // globally unique, enforce ownership here (e.g. row.userId === userId).
8347
+ return row;
8348
+ }
8349
+
8350
+ async upsertByExternalId(
8351
+ userId: string,
8352
+ record: ${n.canonicalType},
8353
+ provider: string,
8354
+ ): Promise<{ id: string; saved: ${n.canonicalType} }> {
8355
+ if (provider !== this.provider) {
8356
+ throw new Error(\`${n.sinkClass}: bound provider '\${this.provider}' != run provider '\${provider}'\`);
8357
+ }
8358
+ const write: ${n.writeType} = {
8359
+ ${writeBody}
8360
+ };
8361
+ const proj = await this.repo.integrationUpsertOne(write, this.provider);
8362
+ return { id: proj.id, saved: record };
8363
+ }
8364
+
8365
+ async softDeleteByExternalId(_userId: string, externalId: string): Promise<{ id: string } | null> {
8366
+ return this.repo.softDeleteByExternalId(externalId, this.provider);
8367
+ }
8368
+ }
8369
+ `;
8370
+ }
8371
+ function relationLabel(writeKey) {
8372
+ const stripped = writeKey.replace(/ExternalId$/, "");
8373
+ return stripped || "related entity";
8374
+ }
8375
+
8376
+ // src/cli/shared/assembly-emission-generator.ts
8377
+ import { relative, resolve as resolve6, sep } from "path";
8378
+ function generatedBanner(sourceDesc) {
8379
+ return `// @generated by @pattern-stack/codegen from ${sourceDesc} \u2014 DO NOT EDIT.
8380
+ // Hand edits are overwritten on re-emit. Regenerate with \`bun run codegen\`.`;
8381
+ }
8382
+ function entityPascalCase(name) {
8383
+ return name.split("_").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
8384
+ }
8385
+ function entityConstantCase(name) {
8386
+ return name.replace(/-/g, "_").toUpperCase();
8387
+ }
8388
+ function integrationUseCaseToken(entityName, providerSlug) {
8389
+ return `${entityConstantCase(entityName)}_INTEGRATION_USE_CASE__${providerConstantCase(providerSlug)}`;
8390
+ }
8391
+ function assemblyModuleClass(entityName, providerSlug) {
8392
+ return `${entityPascalCase(entityName)}IntegrationModule__${providerPascalCase(providerSlug)}`;
8393
+ }
8394
+ function tokenSymbolKey(surface, entityName, providerSlug) {
8395
+ return `@app/integrations/${surface}.${entityName}-integration-use-case.${providerSlug}`;
8396
+ }
8397
+ function generateAssemblyModule(input) {
8398
+ const providerPascal = providerPascalCase(input.provider);
8399
+ const surfacePascal = providerPascalCase(input.surface);
8400
+ const adapterClass = `${providerPascal}${surfacePascal}Adapter`;
8401
+ const adapterModuleClass = `${adapterClass}Module`;
8402
+ const adapterImport = `../../adapters/${input.provider}/${input.provider}-${input.surface}.adapter`;
8403
+ const adapterModuleImport = `${adapterImport}.module`;
8404
+ const sinkClass = `${input.entityClass}Sink`;
8405
+ const sinkImport = `../../sinks/${input.entityName}.sink`;
8406
+ const token = integrationUseCaseToken(input.entityName, input.provider);
8407
+ const moduleClass = assemblyModuleClass(input.entityName, input.provider);
8408
+ const tokensImport = `../../${input.surface}-integration.tokens`;
8409
+ return `${generatedBanner(input.sourceDesc)}
8410
+ import { Module } from '@nestjs/common';
8411
+ import {
8412
+ ExecuteIntegrationUseCase,
8413
+ INTEGRATION_CHANGE_SOURCE,
8414
+ INTEGRATION_SINK,
8415
+ } from '@pattern-stack/codegen/subsystems';
8416
+ import { ${adapterClass} } from '${adapterImport}';
8417
+ import { ${adapterModuleClass} } from '${adapterModuleImport}';
8418
+ import { ${sinkClass} } from '${sinkImport}';
8419
+ import { ${input.repoClass} } from '${input.repoImportSpecifier}';
8420
+ import { ${input.moduleClass} } from '${input.moduleImportSpecifier}';
8421
+ import { ${token} } from '${tokensImport}';
8422
+
8423
+ /**
8424
+ * ${moduleClass} \u2014 the ${input.surface}/${input.entityName} \u2190 ${input.provider}
8425
+ * inbound-integration assembly (RFC-0002 \xA72, Option A).
8426
+ *
8427
+ * Binds this module's INTEGRATION_CHANGE_SOURCE from the adapter's
8428
+ * \`changeSources['${input.entityName}']\` and INTEGRATION_SINK from
8429
+ * ${sinkClass}, provides a local ExecuteIntegrationUseCase, and aliases+exports
8430
+ * it under ${token} (the bare class token is ambiguous at app root \u2014 every
8431
+ * assembly provides it). The substrate (cursor store, run recorder, differ,
8432
+ * multi-tenant flag) comes from the global IntegrationModule.forRoot(...) in
8433
+ * AppModule, never re-bound here.
8434
+ */
8435
+ @Module({
8436
+ imports: [${input.moduleClass}, ${adapterModuleClass}],
8437
+ providers: [
8438
+ {
8439
+ provide: INTEGRATION_CHANGE_SOURCE,
8440
+ useFactory: (adapter: ${adapterClass}) => adapter.changeSources['${input.entityName}'],
8441
+ inject: [${adapterClass}],
8442
+ },
8443
+ {
8444
+ provide: INTEGRATION_SINK,
8445
+ useFactory: (repo: ${input.repoClass}) => new ${sinkClass}(repo, '${input.provider}'),
8446
+ inject: [${input.repoClass}],
8447
+ },
8448
+ ExecuteIntegrationUseCase,
8449
+ { provide: ${token}, useExisting: ExecuteIntegrationUseCase },
8450
+ ],
8451
+ exports: [${token}],
8452
+ })
8453
+ export class ${moduleClass} {}
8454
+ `;
8455
+ }
8456
+ function generateIntegrationTokens(surface, entries) {
8457
+ const sorted = [...entries].sort(
8458
+ (a, b) => a.entityName === b.entityName ? a.provider < b.provider ? -1 : 1 : a.entityName < b.entityName ? -1 : 1
8459
+ );
8460
+ const tokenLines = sorted.map((e) => {
8461
+ const token = integrationUseCaseToken(e.entityName, e.provider);
8462
+ const key = tokenSymbolKey(surface, e.entityName, e.provider);
8463
+ return `/** Unique handle for the ${e.entityName} \u2190 ${e.provider} integration use-case
8464
+ * (${assemblyModuleClass(e.entityName, e.provider)}). A trigger grabs this to run it. */
8465
+ export const ${token} = Symbol.for('${key}');`;
8466
+ }).join("\n\n");
8467
+ return `${generatedBanner(`surface: ${surface}`)}
8468
+ /**
8469
+ * Use-case handles for the \`${surface}\` surface \u2014 one per (entity, provider)
8470
+ * assembly. Each is a Symbol \`provide:\` token a per-entity
8471
+ * \`ExecuteIntegrationUseCase\` is aliased under (the bare class token is
8472
+ * ambiguous at app root) and exported for the consuming trigger to grab.
8473
+ */
8474
+
8475
+ ${tokenLines || "// no (entity, provider) integration assemblies on this surface yet"}
8476
+ `;
8477
+ }
8478
+ function generateIntegrationAggregator(surface, entries) {
8479
+ const surfacePascal = providerPascalCase(surface);
8480
+ const aggregatorClass = `${surfacePascal}IntegrationModule`;
8481
+ const sorted = [...entries].sort(
8482
+ (a, b) => a.provider === b.provider ? a.entityName < b.entityName ? -1 : 1 : a.provider < b.provider ? -1 : 1
8483
+ );
8484
+ const moduleClasses = sorted.map(
8485
+ (e) => assemblyModuleClass(e.entityName, e.provider)
8486
+ );
8487
+ const importLines = sorted.map((e) => {
8488
+ const cls = assemblyModuleClass(e.entityName, e.provider);
8489
+ const path34 = `./modules/${e.provider}/${e.entityName}-integration.module`;
8490
+ return `import { ${cls} } from '${path34}';`;
8491
+ }).join("\n");
8492
+ const membersInline = moduleClasses.join(", ");
8493
+ return `${generatedBanner(`surface: ${surface}`)}
8494
+ import { Module } from '@nestjs/common';
8495
+ ${importLines || "// no (entity, provider) integration assemblies on this surface yet"}
8496
+
8497
+ /**
8498
+ * ${aggregatorClass} \u2014 the \`${surface}\` integration aggregator. AppModule
8499
+ * imports THIS (not ${surfacePascal}AdaptersModule) to wire every per-entity
8500
+ * integration on the surface. Re-exports each assembly module so their
8501
+ * <ENTITY>_INTEGRATION_USE_CASE__<PROVIDER> tokens are available to the app (the
8502
+ * trigger/consumer grabs them). Per RFC-0002 \xA77 q3, the run path goes through
8503
+ * these assembly modules, keeping the collision-prone ${surfacePascal}AdaptersModule
8504
+ * fold off the graph.
8505
+ */
8506
+ @Module({
8507
+ imports: [${membersInline}],
8508
+ exports: [${membersInline}],
8509
+ })
8510
+ export class ${aggregatorClass} {}
8511
+ `;
8512
+ }
8513
+ function pluralPascalCase(plural) {
8514
+ return plural.split("_").filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
8515
+ }
8516
+ function toImportSpecifier(targetFileAbs, fromDirAbs, aliases) {
8517
+ const noExt = targetFileAbs.replace(/\.ts$/, "");
8518
+ let best = null;
8519
+ for (const [key, dirRaw] of Object.entries(aliases)) {
8520
+ const dir = dirRaw.endsWith(sep) ? dirRaw.slice(0, -1) : dirRaw;
8521
+ const prefix = dir + sep;
8522
+ if (noExt === dir || noExt.startsWith(prefix)) {
8523
+ const rest = noExt === dir ? "" : noExt.slice(prefix.length);
8524
+ if (!best || dir.length > aliases[best.key].length) {
8525
+ best = { key, rest };
8526
+ }
8527
+ }
8528
+ }
8529
+ if (best) {
8530
+ const restPosix = best.rest.split(sep).join("/");
8531
+ return restPosix ? `${best.key}/${restPosix}` : best.key;
8532
+ }
8533
+ let rel2 = relative(fromDirAbs, noExt).split(sep).join("/");
8534
+ if (!rel2.startsWith(".")) rel2 = `./${rel2}`;
8535
+ return rel2;
8536
+ }
8537
+ function resolveEntityModuleImports(input) {
8538
+ const entityClass = entityPascalCase(input.entityName);
8539
+ const repoClass = `${entityClass}Repository`;
8540
+ const moduleClass = `${pluralPascalCase(input.entityPlural)}Module`;
8541
+ const moduleGroupSegs = input.context ? ["modules", input.context, input.entityPlural] : ["modules", input.entityPlural];
8542
+ const repoFileAbs = resolve6(
8543
+ input.backendSrcAbs,
8544
+ ...moduleGroupSegs,
8545
+ `${input.entityName}.repository.ts`
8546
+ );
8547
+ const moduleFileAbs = resolve6(
8548
+ input.backendSrcAbs,
8549
+ ...moduleGroupSegs,
8550
+ `${input.entityPlural}.module.ts`
8551
+ );
8552
+ const assemblyDirAbs = resolve6(
8553
+ input.backendSrcAbs,
8554
+ "integrations",
8555
+ input.surface,
8556
+ "modules",
8557
+ input.provider
8558
+ );
8559
+ return {
8560
+ entityClass,
8561
+ repoClass,
8562
+ moduleClass,
8563
+ repoImportSpecifier: toImportSpecifier(repoFileAbs, assemblyDirAbs, input.aliases),
8564
+ moduleImportSpecifier: toImportSpecifier(moduleFileAbs, assemblyDirAbs, input.aliases)
8565
+ };
8566
+ }
8567
+
8568
+ // src/cli/shared/adapter-emission-generator.ts
8236
8569
  var SURFACE_REGISTRY = {
8237
8570
  crm: {
8238
8571
  packageName: "@pattern-stack/codegen-crm",
@@ -8272,35 +8605,39 @@ var SURFACE_REGISTRY = {
8272
8605
  portType: "CalendarPort",
8273
8606
  capabilitiesType: "CalendarCapabilities",
8274
8607
  noCapsConst: "NO_CALENDAR_CAPABILITIES",
8275
- l2Ports: []
8608
+ l2Ports: [],
8609
+ readPrimitive: true
8276
8610
  },
8277
8611
  mail: {
8278
8612
  packageName: "@pattern-stack/codegen-mail",
8279
8613
  portType: "MailPort",
8280
8614
  capabilitiesType: "MailCapabilities",
8281
8615
  noCapsConst: "NO_MAIL_CAPABILITIES",
8282
- l2Ports: []
8616
+ l2Ports: [],
8617
+ readPrimitive: true
8283
8618
  },
8284
8619
  transcript: {
8285
8620
  packageName: "@pattern-stack/codegen-transcript",
8286
8621
  portType: "TranscriptPort",
8287
8622
  capabilitiesType: "TranscriptCapabilities",
8288
8623
  noCapsConst: "NO_TRANSCRIPT_CAPABILITIES",
8289
- l2Ports: []
8624
+ l2Ports: [],
8625
+ readPrimitive: true
8290
8626
  }
8291
8627
  };
8292
- var SCAFFOLD_SENTINEL = "// <CODEGEN-SCAFFOLD-V1>";
8293
- function generatedBanner(sourceDesc) {
8628
+ var SCAFFOLD_SENTINEL2 = "// <CODEGEN-SCAFFOLD-V1>";
8629
+ function generatedBanner2(sourceDesc) {
8294
8630
  return `// @generated by @pattern-stack/codegen from ${sourceDesc} \u2014 DO NOT EDIT.
8295
8631
  // Hand edits are overwritten on re-emit. Regenerate with \`bun run codegen\`.`;
8296
8632
  }
8297
8633
  function collectEntitiesBySurface(entities) {
8298
8634
  const bySurface = /* @__PURE__ */ new Map();
8299
8635
  for (const e of entities) {
8300
- if (!e.surface) continue;
8301
- const list = bySurface.get(e.surface) ?? [];
8636
+ const surface = e.entity.surface;
8637
+ if (!surface) continue;
8638
+ const list = bySurface.get(surface) ?? [];
8302
8639
  list.push(e.entity.name);
8303
- bySurface.set(e.surface, list);
8640
+ bySurface.set(surface, list);
8304
8641
  }
8305
8642
  for (const [surface, list] of bySurface) {
8306
8643
  bySurface.set(surface, [...list].sort());
@@ -8327,17 +8664,139 @@ function names(providerSlug, surface) {
8327
8664
  entitySourcesToken: `${surfaceConst}_ENTITY_SOURCES`
8328
8665
  };
8329
8666
  }
8330
- function generateAdapterScaffold(def, surface, entities) {
8667
+ function entityPascalCase2(name) {
8668
+ return name.split(/[-_]/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
8669
+ }
8670
+ function entityConstCase(name) {
8671
+ return name.replace(/-/g, "_").toUpperCase();
8672
+ }
8673
+ function serializeFilterArray(filters) {
8674
+ if (filters.length === 0) return "[]";
8675
+ const items = filters.map(
8676
+ (f) => ` { field: ${JSON.stringify(f.field)}, op: ${JSON.stringify(f.op)}, value: ${JSON.stringify(f.value)} },`
8677
+ );
8678
+ return `[
8679
+ ${items.join("\n")}
8680
+ ]`;
8681
+ }
8682
+ function buildReadPrimitiveEmission(providerSlug, providerPascal, surface, entities, entityDetection, clientExportName) {
8683
+ const canonicalTypes = [];
8684
+ const blocks = [];
8685
+ const entries = [];
8686
+ for (const entity of entities) {
8687
+ const pascal = entityPascalCase2(entity);
8688
+ const canonical = `Canonical${pascal}`;
8689
+ const className = `${providerPascal}${pascal}IncrementalRead`;
8690
+ const constName = `${entityConstCase(entity)}_DETECTION_FILTERS`;
8691
+ canonicalTypes.push(canonical);
8692
+ const det = entityDetection?.get(entity);
8693
+ const filters = det?.filters ?? [];
8694
+ const cursorKind = det && det.mode === "poll" ? det.poll.cursor.kind : void 0;
8695
+ const atomic = cursorKind !== void 0 && !isDivisibleCursor(cursorKind);
8696
+ const cursorOverride = atomic ? `
8697
+ // \`${cursorKind}\` is an ATOMIC cursor (RFC-0003 \xA73): its next value only exists
8698
+ // at end-of-walk, so per-ref cursors are withheld and only the final record
8699
+ // carries the token \u2014 a mid-walk crash never persists an unresumable value.
8700
+ protected override readonly cursorDivisible = false;` : "";
8701
+ blocks.push(`/**
8702
+ * \`detection.filters\` for \`${entity}\`, emitted from YAML as a static
8703
+ * \`ResolvedFilter[]\` (RFC-0003 \xA74 fork #2); \`filterFor()\` returns it.
8704
+ */
8705
+ const ${constName}: ResolvedFilter[] = ${serializeFilterArray(filters)};
8706
+
8707
+ // Emit-once read primitive (author-owned). Fill the three vendor methods below.
8708
+ export class ${className} extends IncrementalReadBase<${canonical}, ResolvedFilter[]> {
8709
+ readonly label = '${providerSlug}-${surface}-${entity}';
8710
+ // Flip to \`true\` if your \`enumerate\` pushes the request filter to the vendor
8711
+ // (e.g. Gmail \`q=\`); leave \`false\` to filter post-hydrate via \`matchesRecord\`.
8712
+ protected override readonly filterPushdown = false;${cursorOverride}
8713
+
8714
+ constructor(
8715
+ private readonly auth: IAuthStrategy,
8716
+ private readonly client: ${clientExportName},
8717
+ ) {
8718
+ super();
8719
+ }
8720
+
8721
+ /** TODO: walk the vendor list endpoint \u2192 pages of \`Ref\` (id + cursor + meta). */
8722
+ protected async *enumerate(
8723
+ _mode: ReadMode,
8724
+ _filter?: ResolvedFilter[],
8725
+ _pageSize?: number,
8726
+ ): AsyncIterable<Ref[]> {
8727
+ throw new Error('not implemented: ${className}.enumerate');
8728
+ }
8729
+
8730
+ /** TODO: batched fetch-by-id \u2192 \`Map<id, raw>\` (\`mapConcurrent\`, or a vendor /batch). */
8731
+ protected async hydrate(_ids: string[]): Promise<Map<string, unknown>> {
8732
+ throw new Error('not implemented: ${className}.hydrate');
8733
+ }
8734
+
8735
+ /** TODO: vendor payload \u2192 \`${canonical}\` (return \`null\` to drop). */
8736
+ protected toCanonical(_raw: unknown): ${canonical} | null {
8737
+ throw new Error('not implemented: ${className}.toCanonical');
8738
+ }
8739
+
8740
+ protected override filterFor(
8741
+ _subscription: IntegrationSubscriptionView,
8742
+ ): ResolvedFilter[] {
8743
+ return ${constName};
8744
+ }
8745
+ }`);
8746
+ entries.push(` ${entity}: new ${className}(this.auth, this.client),`);
8747
+ }
8748
+ return { canonicalTypes, preamble: blocks.join("\n\n"), changeSourceEntries: entries };
8749
+ }
8750
+ function generateAdapterScaffold(def, surface, entities, entityDetection) {
8331
8751
  const spec = SURFACE_REGISTRY[surface];
8332
8752
  if (!spec) throw new Error(`no surface package for '${surface}'`);
8333
8753
  const n = names(def.slug, surface);
8334
8754
  const client = parseImportRef(def.client.class);
8335
8755
  const entitiesLiteral = entities.length ? `[${entities.map((e) => `'${e}'`).join(", ")}]` : "[]";
8756
+ const readPrimitive = !!spec.readPrimitive && entities.length > 0;
8757
+ const rp = readPrimitive ? buildReadPrimitiveEmission(
8758
+ def.slug,
8759
+ n.providerPascal,
8760
+ surface,
8761
+ entities,
8762
+ entityDetection,
8763
+ client.exportName
8764
+ ) : null;
8336
8765
  const surfaceTypeImports = [
8337
8766
  spec.portType,
8338
8767
  ...spec.l2Ports.map((p) => p.type),
8339
- spec.capabilitiesType
8768
+ spec.capabilitiesType,
8769
+ ...rp ? rp.canonicalTypes : []
8340
8770
  ].map((t) => ` ${t},`).join("\n");
8771
+ const subsystemValueImport = rp ? `import { IncrementalReadBase } from '@pattern-stack/codegen/subsystems';
8772
+ ` : "";
8773
+ const subsystemTypeImports = [
8774
+ "IAuthStrategy",
8775
+ "IChangeSource",
8776
+ ...rp ? ["IntegrationSubscriptionView", "ReadMode", "Ref", "ResolvedFilter"] : []
8777
+ ].map((t) => ` ${t},`).join("\n");
8778
+ const changeSourcesAssign = rp ? `
8779
+ this.changeSources = {
8780
+ ${rp.changeSourceEntries.join("\n")}
8781
+ };
8782
+ ` : "";
8783
+ const changeSourcesDecl = rp ? ` /**
8784
+ * Per-entity change sources contributed to the ${surface} registry, keyed by
8785
+ * entity name. The surface aggregator folds these into the
8786
+ * \`IEntityChangeSourceRegistry\` bound under \`${n.entitySourcesToken}\`.
8787
+ * Emit-once: edit the \`IncrementalReadBase\` subclasses above, not this map.
8788
+ */
8789
+ readonly changeSources: Record<string, IChangeSource<unknown>>;` : ` /**
8790
+ * Per-entity change sources this adapter contributes to the ${surface}
8791
+ * registry (ADR-033 \`buildChangeSource\`), keyed by entity name. The
8792
+ * surface aggregator folds these into the \`IEntityChangeSourceRegistry\`
8793
+ * bound under \`${n.entitySourcesToken}\`. Author-owned \u2014 populate one entry
8794
+ * per entity in \`capabilities.entities\`.
8795
+ */
8796
+ readonly changeSources: Record<string, IChangeSource<unknown>> = {};`;
8797
+ const preambleSection = rp ? `
8798
+ ${rp.preamble}
8799
+ ` : "";
8341
8800
  const capabilityBody = [
8342
8801
  ` ...${spec.noCapsConst},`,
8343
8802
  ...spec.l2Ports.map((p) => ` ${p.capFlag}: true,`),
@@ -8354,7 +8813,7 @@ function generateAdapterScaffold(def, surface, entities) {
8354
8813
  const l2Section = l2Members ? `
8355
8814
  ${l2Members}
8356
8815
  ` : "";
8357
- return `${SCAFFOLD_SENTINEL}
8816
+ return `${SCAFFOLD_SENTINEL2}
8358
8817
  // Scaffolded once by @pattern-stack/codegen, then author-owned. Re-running
8359
8818
  // codegen detects the sentinel above and SKIPS this file \u2014 your edits are safe.
8360
8819
  // Source: definitions/providers/${def.slug}.yaml (surface: ${surface}).
@@ -8363,15 +8822,12 @@ import type {
8363
8822
  ${surfaceTypeImports}
8364
8823
  } from '${spec.packageName}';
8365
8824
  import { ${spec.noCapsConst} } from '${spec.packageName}';
8366
- import type {
8367
- IAuthStrategy,
8368
- IChangeSource,
8369
- IEntityChangeSourceRegistry,
8825
+ ${subsystemValueImport}import type {
8826
+ ${subsystemTypeImports}
8370
8827
  } from '@pattern-stack/codegen/subsystems';
8371
8828
  import type { ${client.exportName} } from '${client.path}';
8372
8829
  import { ${n.strategyToken}, ${n.clientToken} } from '../../../providers/${def.slug}/${def.slug}.provider.module';
8373
- import { ${n.entitySourcesToken} } from '../../${surface}-adapters.tokens';
8374
-
8830
+ ${preambleSection}
8375
8831
  @Injectable()
8376
8832
  export class ${n.adapterClass} implements ${spec.portType} {
8377
8833
  /** Declared capabilities. \`entities\` derives from \`surface: ${surface}\` entity YAML. */
@@ -8382,17 +8838,9 @@ ${capabilityBody}
8382
8838
  constructor(
8383
8839
  @Inject(${n.strategyToken}) readonly auth: IAuthStrategy,
8384
8840
  @Inject(${n.clientToken}) private readonly client: ${client.exportName},
8385
- @Inject(${n.entitySourcesToken}) readonly sources: IEntityChangeSourceRegistry,
8386
- ) {}
8841
+ ) {${changeSourcesAssign}}
8387
8842
  ${l2Section}
8388
- /**
8389
- * Per-entity change sources this adapter contributes to the ${surface}
8390
- * registry (ADR-033 \`buildChangeSource\`), keyed by entity name. The
8391
- * surface aggregator folds these into the \`IEntityChangeSourceRegistry\`
8392
- * bound under \`${n.entitySourcesToken}\`. Author-owned \u2014 populate one entry
8393
- * per entity in \`capabilities.entities\`.
8394
- */
8395
- readonly changeSources: Record<string, IChangeSource<unknown>> = {};
8843
+ ${changeSourcesDecl}
8396
8844
 
8397
8845
  // surface-only methods (optional on ${spec.portType}): add here
8398
8846
  }
@@ -8400,7 +8848,7 @@ ${l2Section}
8400
8848
  }
8401
8849
  function generateAdapterModule(def, surface) {
8402
8850
  const n = names(def.slug, surface);
8403
- return `${generatedBanner(`definitions/providers/${def.slug}.yaml (surface: ${surface})`)}
8851
+ return `${generatedBanner2(`definitions/providers/${def.slug}.yaml (surface: ${surface})`)}
8404
8852
  import { Module } from '@nestjs/common';
8405
8853
  import { ${n.providerModuleClass} } from '../../../providers/${def.slug}/${def.slug}.provider.module';
8406
8854
  import { ${n.adapterClass} } from './${def.slug}-${surface}.adapter';
@@ -8418,13 +8866,13 @@ function generateAdaptersBarrel(surface, providerSlugs) {
8418
8866
  const n = names(slug, surface);
8419
8867
  return `export { ${n.adapterModuleClass} } from './${slug}/${slug}-${surface}.adapter.module';`;
8420
8868
  }).join("\n");
8421
- return `${generatedBanner(`definitions/providers/*.yaml (surface: ${surface})`)}
8869
+ return `${generatedBanner2(`definitions/providers/*.yaml (surface: ${surface})`)}
8422
8870
  ${lines}
8423
8871
  `;
8424
8872
  }
8425
8873
  function generateSurfaceTokens(surface) {
8426
8874
  const n = names("__placeholder__", surface);
8427
- return `${generatedBanner(`surface: ${surface}`)}
8875
+ return `${generatedBanner2(`surface: ${surface}`)}
8428
8876
  import type { IChangeSource } from '@pattern-stack/codegen/subsystems';
8429
8877
 
8430
8878
  /** The assembled list of every ${surface} adapter's contribution. */
@@ -8463,7 +8911,7 @@ function generateSurfaceAggregator(surface, providerSlugs) {
8463
8911
  return `${lowerFirst(p.adapterClass)}: ${p.adapterClass}`;
8464
8912
  }).join(", ");
8465
8913
  const injectTokens = per.map((p) => p.adapterClass).join(", ");
8466
- return `${generatedBanner(`surface: ${surface}`)}
8914
+ return `${generatedBanner2(`surface: ${surface}`)}
8467
8915
  import { Module } from '@nestjs/common';
8468
8916
  import {
8469
8917
  MemoryEntityChangeSourceRegistry,
@@ -8535,7 +8983,7 @@ function generateTypedView(surface, providerSlugs, entities) {
8535
8983
  const providerUnion = slugs.length ? slugs.map((s) => `'${s}'`).join(" | ") : "never";
8536
8984
  const entityUnion = ents.length ? ents.map((e) => `'${e}'`).join(" | ") : "never";
8537
8985
  const mapEntries = slugs.map((s) => ` ${jsKey(s)}: ${surfacePascal}Entity;`).join("\n");
8538
- return `${generatedBanner(`surface: ${surface}`)}
8986
+ return `${generatedBanner2(`surface: ${surface}`)}
8539
8987
  /**
8540
8988
  * Per-consumer typed view for the \`${surface}\` surface. Surface-scoped unions
8541
8989
  * + a (provider, entity) validity map for compile-time-checked consumer
@@ -8562,9 +9010,14 @@ function emitAdapters(opts) {
8562
9010
  written: [],
8563
9011
  scaffoldsWritten: [],
8564
9012
  scaffoldsSkipped: [],
8565
- skippedSurfaces: []
9013
+ skippedSurfaces: [],
9014
+ assembliesWritten: [],
9015
+ tokensWritten: [],
9016
+ integrationAggregatorsWritten: [],
9017
+ skippedAssemblies: []
8566
9018
  };
8567
9019
  const entitiesBySurface = collectEntitiesBySurface(opts.entities);
9020
+ const entityByName = new Map(opts.entities.map((e) => [e.entity.name, e]));
8568
9021
  const bySurface = /* @__PURE__ */ new Map();
8569
9022
  for (const { definition } of opts.providers) {
8570
9023
  for (const surface of definition.surfaces) {
@@ -8593,10 +9046,17 @@ function emitAdapters(opts) {
8593
9046
  if (existsSync10(scaffoldPath)) {
8594
9047
  result.scaffoldsSkipped.push(scaffoldPath);
8595
9048
  } else {
9049
+ const surfaceEntityNames = entitiesBySurface.get(surface) ?? [];
9050
+ const entityDetection = /* @__PURE__ */ new Map();
9051
+ for (const name of surfaceEntityNames) {
9052
+ const det = entityByName.get(name)?.entity.detection?.[slug];
9053
+ if (det) entityDetection.set(name, det);
9054
+ }
8596
9055
  const content = generateAdapterScaffold(
8597
9056
  def,
8598
9057
  surface,
8599
- entitiesBySurface.get(surface) ?? []
9058
+ surfaceEntityNames,
9059
+ entityDetection
8600
9060
  );
8601
9061
  if (!opts.dryRun) writeFile(scaffoldPath, content);
8602
9062
  result.scaffoldsWritten.push(scaffoldPath);
@@ -8619,9 +9079,141 @@ function emitAdapters(opts) {
8619
9079
  if (!opts.dryRun) writeIfChanged2(path34, content);
8620
9080
  result.written.push(path34);
8621
9081
  }
9082
+ if (opts.backendSrcAbs) {
9083
+ const aliases = opts.aliases ?? {};
9084
+ const surfaceEntities = entitiesBySurface.get(surface) ?? [];
9085
+ const tokenEntries = [];
9086
+ const assemblyEntries = [];
9087
+ const sinksDir = join12(surfaceDir, "sinks");
9088
+ const modulesDir = join12(surfaceDir, "modules");
9089
+ for (const entityName of surfaceEntities) {
9090
+ const def = entityByName.get(entityName);
9091
+ const pattern = def?.entity.pattern ?? (Array.isArray(def?.entity.patterns) ? def?.entity.patterns?.[0] : void 0);
9092
+ if (pattern !== "Integrated") {
9093
+ result.skippedAssemblies.push({
9094
+ surface,
9095
+ entity: entityName,
9096
+ reason: `entity '${entityName}' declares surface '${surface}' but is not 'pattern: Integrated'${pattern ? ` (got 'pattern: ${pattern}')` : " (no pattern declared)"} \u2014 the integration assembly + default sink need the Integrated projection/upsert path. Add 'pattern: Integrated' (or provide a hand-authored sink + assembly).`
9097
+ });
9098
+ continue;
9099
+ }
9100
+ const plural = def?.entity.plural ?? `${entityName}s`;
9101
+ const context = def?.entity.context ?? null;
9102
+ const loc = resolveEntityModuleImports({
9103
+ entityName,
9104
+ entityPlural: plural,
9105
+ context,
9106
+ surface,
9107
+ // The sink+repo import is provider-agnostic; pick any provider's
9108
+ // module dir for the relative-path base (all share the same parent).
9109
+ provider: slugs[0],
9110
+ backendSrcAbs: opts.backendSrcAbs,
9111
+ aliases
9112
+ });
9113
+ const sinkPath = join12(sinksDir, `${entityName}.sink.ts`);
9114
+ if (existsSync10(sinkPath)) {
9115
+ result.scaffoldsSkipped.push(sinkPath);
9116
+ } else {
9117
+ const sinkInput = buildSinkInput(def, surface, slugs[0], loc.repoImportSpecifier);
9118
+ const sinkContent = generateDefaultSink(sinkInput);
9119
+ if (!opts.dryRun) writeFile(sinkPath, sinkContent);
9120
+ result.scaffoldsWritten.push(sinkPath);
9121
+ }
9122
+ for (const slug of slugs) {
9123
+ const assemblyPath = join12(
9124
+ modulesDir,
9125
+ slug,
9126
+ `${entityName}-integration.module.ts`
9127
+ );
9128
+ const assemblyContent = generateAssemblyModule({
9129
+ surface,
9130
+ provider: slug,
9131
+ entityName,
9132
+ entityClass: loc.entityClass,
9133
+ moduleImportSpecifier: loc.moduleImportSpecifier,
9134
+ moduleClass: loc.moduleClass,
9135
+ repoImportSpecifier: loc.repoImportSpecifier,
9136
+ repoClass: loc.repoClass,
9137
+ sourceDesc: `definitions/providers/${slug}.yaml`
9138
+ });
9139
+ if (!opts.dryRun) writeIfChanged2(assemblyPath, assemblyContent);
9140
+ result.assembliesWritten.push(assemblyPath);
9141
+ tokenEntries.push({ entityName, entityClass: loc.entityClass, provider: slug });
9142
+ assemblyEntries.push({ entityName, provider: slug });
9143
+ }
9144
+ }
9145
+ const integrationTokensPath = join12(
9146
+ surfaceDir,
9147
+ `${surface}-integration.tokens.ts`
9148
+ );
9149
+ const tokensContent = generateIntegrationTokens(surface, tokenEntries);
9150
+ if (!opts.dryRun) writeIfChanged2(integrationTokensPath, tokensContent);
9151
+ result.tokensWritten.push(integrationTokensPath);
9152
+ if (assemblyEntries.length > 0) {
9153
+ const integrationAggregatorPath = join12(
9154
+ surfaceDir,
9155
+ `${surface}-integration.module.ts`
9156
+ );
9157
+ const aggregatorContent = generateIntegrationAggregator(
9158
+ surface,
9159
+ assemblyEntries
9160
+ );
9161
+ if (!opts.dryRun) writeIfChanged2(integrationAggregatorPath, aggregatorContent);
9162
+ result.integrationAggregatorsWritten.push(integrationAggregatorPath);
9163
+ }
9164
+ }
8622
9165
  }
8623
9166
  return result;
8624
9167
  }
9168
+ function buildSinkInput(def, surface, provider, repoImportSpecifier) {
9169
+ const fields = def.fields ?? {};
9170
+ const relationships = def.relationships ?? {};
9171
+ const fkColumns = /* @__PURE__ */ new Set();
9172
+ for (const rel2 of Object.values(relationships)) {
9173
+ if (rel2.type === "belongs_to" && typeof rel2.foreign_key === "string") {
9174
+ fkColumns.add(rel2.foreign_key);
9175
+ }
9176
+ }
9177
+ const copyThroughFields = Object.entries(fields).filter(([name]) => name !== "id" && !fkColumns.has(name)).map(([name, f]) => ({
9178
+ camelName: snakeToCamel(name),
9179
+ tsType: tsTypeFor(f.type, f.nullable)
9180
+ }));
9181
+ const fkExternalKeys = Object.entries(relationships).filter(([, rel2]) => rel2.type === "belongs_to").map(([relName, rel2]) => {
9182
+ const target = rel2.target ?? relName;
9183
+ return { writeKey: `${snakeToCamel(target)}ExternalId` };
9184
+ });
9185
+ return {
9186
+ entityName: def.entity.name,
9187
+ entityClass: pascalFromSnake(def.entity.name),
9188
+ surface,
9189
+ pattern: "Integrated",
9190
+ provider,
9191
+ copyThroughFields,
9192
+ fkExternalKeys,
9193
+ repoImportSpecifier
9194
+ };
9195
+ }
9196
+ var TS_TYPE_FOR_SINK = {
9197
+ string: "string",
9198
+ integer: "number",
9199
+ decimal: "string",
9200
+ boolean: "boolean",
9201
+ uuid: "string",
9202
+ date: "Date",
9203
+ datetime: "Date",
9204
+ json: "unknown"
9205
+ };
9206
+ function tsTypeFor(type, nullable) {
9207
+ const base = TS_TYPE_FOR_SINK[type ?? "string"] ?? "unknown";
9208
+ return nullable ? `${base} | null` : base;
9209
+ }
9210
+ function snakeToCamel(s) {
9211
+ return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
9212
+ }
9213
+ function pascalFromSnake(s) {
9214
+ const camel = snakeToCamel(s);
9215
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
9216
+ }
8625
9217
  function writeFile(outPath, content) {
8626
9218
  mkdirSync3(dirname2(outPath), { recursive: true });
8627
9219
  writeFileSync3(outPath, content);
@@ -8668,9 +9260,15 @@ function printInfo(msg) {
8668
9260
  }
8669
9261
 
8670
9262
  // src/cli/commands/entity.ts
8671
- function listEntityYamls2(dir) {
9263
+ function resolveProvidersDir(ctx) {
9264
+ const fromConfig = ctx.config?.paths?.providers;
9265
+ return fromConfig != null ? path13.resolve(ctx.cwd, fromConfig) : path13.resolve(ctx.cwd, "definitions/providers");
9266
+ }
9267
+ function listEntityYamls2(dir, providersDir) {
8672
9268
  if (!fs9.existsSync(dir)) return [];
8673
- return findYamlFiles(dir);
9269
+ return findYamlFiles(dir, {
9270
+ excludeDirs: providersDir ? [providersDir] : []
9271
+ });
8674
9272
  }
8675
9273
  function summarizePatternLabel(entity) {
8676
9274
  if (typeof entity.pattern === "string" && entity.pattern.length > 0) {
@@ -8709,7 +9307,7 @@ async function summary(ctx) {
8709
9307
  ]
8710
9308
  };
8711
9309
  }
8712
- const files = listEntityYamls2(ctx.entitiesDir);
9310
+ const files = listEntityYamls2(ctx.entitiesDir, resolveProvidersDir(ctx));
8713
9311
  const rows = files.map(summarizeEntityFile).filter((r) => r !== null);
8714
9312
  const patterns = new Set(rows.map((r) => r.pattern));
8715
9313
  const queryCount = rows.reduce((sum, r) => sum + r.queries, 0);
@@ -8806,7 +9404,7 @@ var EntityNewCommand = class extends Command2 {
8806
9404
  let targets = [];
8807
9405
  if (this.all) {
8808
9406
  const dir = ctx.entitiesDir ?? path13.resolve(ctx.cwd, "entities");
8809
- targets = listEntityYamls2(dir);
9407
+ targets = listEntityYamls2(dir, resolveProvidersDir(ctx));
8810
9408
  if (targets.length === 0) {
8811
9409
  printError(`No entity YAML files found in ${dir}`);
8812
9410
  return 1;
@@ -8824,12 +9422,15 @@ var EntityNewCommand = class extends Command2 {
8824
9422
  if (result.success) {
8825
9423
  validated.push({ file, name: result.definition.entity.name });
8826
9424
  } else {
8827
- invalid.push({ file, message: result.error });
9425
+ invalid.push({ file, message: result.error, details: result.details });
8828
9426
  }
8829
9427
  }
8830
9428
  if (invalid.length > 0 && !this.continueOnError) {
8831
9429
  for (const i of invalid) {
8832
9430
  printError(`${path13.basename(i.file)} \u2014 ${i.message}`);
9431
+ for (const detail of i.details ?? []) {
9432
+ printError(` \u2022 ${detail}`);
9433
+ }
8833
9434
  }
8834
9435
  if (!isJsonMode()) {
8835
9436
  return 1;
@@ -8837,7 +9438,9 @@ var EntityNewCommand = class extends Command2 {
8837
9438
  }
8838
9439
  const entitiesDirForEmits = ctx.entitiesDir ?? path13.resolve(ctx.cwd, "entities");
8839
9440
  const eventsDirForEmits = resolveEventsDir(ctx);
8840
- const allEntitiesForEmits = loadEntities(entitiesDirForEmits).entities;
9441
+ const allEntitiesForEmits = loadEntities(entitiesDirForEmits, {
9442
+ excludeDirs: [resolveProvidersDir(ctx)]
9443
+ }).entities;
8841
9444
  const validatedNames = new Set(validated.map((v) => v.name));
8842
9445
  const emitsTargetEntities = allEntitiesForEmits.filter(
8843
9446
  (e) => validatedNames.has(e.name)
@@ -9169,19 +9772,16 @@ var EntityNewCommand = class extends Command2 {
9169
9772
  }
9170
9773
  let providerResult = null;
9171
9774
  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");
9775
+ const providersDir = resolveProvidersDir(ctx);
9176
9776
  const providerOutputRoot = path13.resolve(
9177
9777
  ctx.cwd,
9178
9778
  backendSrcForHandlers,
9179
9779
  "integrations/providers"
9180
9780
  );
9181
9781
  const entitySurfaces = fs9.existsSync(entitiesDir) ? collectEntitySurfaces(
9182
- loadEntitiesFromYaml(findYamlFiles(entitiesDir)).successes.map(
9183
- (s) => s.definition
9184
- )
9782
+ loadEntitiesFromYaml(
9783
+ findYamlFiles(entitiesDir, { excludeDirs: [providersDir] })
9784
+ ).successes.map((s) => s.definition)
9185
9785
  ) : /* @__PURE__ */ new Set();
9186
9786
  const tsAliases = resolveTsconfigAliases(ctx.cwd);
9187
9787
  providerResult = generateProviderModules({
@@ -9219,19 +9819,22 @@ var EntityNewCommand = class extends Command2 {
9219
9819
  backendSrcForHandlers,
9220
9820
  "integrations"
9221
9821
  );
9222
- const entityDefs = fs9.existsSync(entitiesDir) ? loadEntitiesFromYaml(findYamlFiles(entitiesDir)).successes.map(
9223
- (s) => s.definition
9224
- ) : [];
9822
+ const entityDefs = fs9.existsSync(entitiesDir) ? loadEntitiesFromYaml(
9823
+ findYamlFiles(entitiesDir, { excludeDirs: [providersDir] })
9824
+ ).successes.map((s) => s.definition) : [];
9225
9825
  const loadedProviders = loadProvidersFromYaml(
9226
9826
  findYamlFiles(providersDir)
9227
9827
  ).successes.map((s) => ({
9228
9828
  definition: s.definition,
9229
9829
  filePath: s.filePath
9230
9830
  }));
9831
+ const assemblyTsAliases = resolveTsconfigAliases(ctx.cwd);
9231
9832
  const adapterResult = emitAdapters({
9232
9833
  providers: loadedProviders,
9233
9834
  entities: entityDefs,
9234
- outputRoot: adapterOutputRoot
9835
+ outputRoot: adapterOutputRoot,
9836
+ backendSrcAbs: path13.resolve(ctx.cwd, backendSrcForHandlers),
9837
+ aliases: assemblyTsAliases?.aliases ?? {}
9235
9838
  });
9236
9839
  if (!isJsonMode()) {
9237
9840
  if (adapterResult.written.length || adapterResult.scaffoldsWritten.length) {
@@ -9239,12 +9842,20 @@ var EntityNewCommand = class extends Command2 {
9239
9842
  `adapter codegen: ${adapterResult.scaffoldsWritten.length} scaffold(s) + ${adapterResult.written.length} @generated \u2192 ${adapterOutputRoot}`
9240
9843
  );
9241
9844
  }
9845
+ if (adapterResult.assembliesWritten.length || adapterResult.tokensWritten.length) {
9846
+ printInfo(
9847
+ `integration assembly codegen: ${adapterResult.assembliesWritten.length} module(s) + ${adapterResult.tokensWritten.length} tokens file(s) + ${adapterResult.integrationAggregatorsWritten.length} aggregator(s)`
9848
+ );
9849
+ }
9242
9850
  for (const s of adapterResult.scaffoldsSkipped) {
9243
9851
  printInfo(`skipped scaffold ${s} (author-owned)`);
9244
9852
  }
9245
9853
  for (const s of adapterResult.skippedSurfaces) {
9246
9854
  printWarning(`adapter codegen: ${s.reason} (provider ${s.provider})`);
9247
9855
  }
9856
+ for (const s of adapterResult.skippedAssemblies) {
9857
+ printWarning(`integration assembly: ${s.reason}`);
9858
+ }
9248
9859
  }
9249
9860
  }
9250
9861
  } catch (err) {
@@ -9367,7 +9978,7 @@ var EntityListCommand = class extends Command2 {
9367
9978
  printError("No entities directory found.");
9368
9979
  return 1;
9369
9980
  }
9370
- const files = listEntityYamls2(ctx.entitiesDir);
9981
+ const files = listEntityYamls2(ctx.entitiesDir, resolveProvidersDir(ctx));
9371
9982
  const rows = files.map(summarizeEntityFile).filter((r) => r !== null).filter((r) => this.pattern ? r.pattern === this.pattern : true);
9372
9983
  if (isJsonMode()) {
9373
9984
  printJson({
@@ -13097,10 +13708,10 @@ var ProjectInitCommand = class extends Command7 {
13097
13708
  };
13098
13709
  function askConfirm(question) {
13099
13710
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
13100
- return new Promise((resolve6) => {
13711
+ return new Promise((resolve7) => {
13101
13712
  rl.question(`${question} [Y/n] `, (answer) => {
13102
13713
  rl.close();
13103
- resolve6(answer.trim().toLowerCase() !== "n");
13714
+ resolve7(answer.trim().toLowerCase() !== "n");
13104
13715
  });
13105
13716
  });
13106
13717
  }