@pattern-stack/codegen 0.17.0 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/consumer-skills/integration/audit-and-detection.md +29 -4
  3. package/dist/{chunk-UTNWFHJF.js → chunk-4PFF3ED4.js} +4 -4
  4. package/dist/{chunk-CO6LUM72.js → chunk-7P5ODGLA.js} +34 -2
  5. package/dist/chunk-7P5ODGLA.js.map +1 -0
  6. package/dist/{chunk-CDLWYZVQ.js → chunk-BHZP6LOV.js} +7 -7
  7. package/dist/{chunk-4MVGAMUA.js → chunk-BK5ICA2F.js} +4 -4
  8. package/dist/{chunk-BULPAAD3.js → chunk-DUMI2J5M.js} +42 -11
  9. package/dist/chunk-DUMI2J5M.js.map +1 -0
  10. package/dist/{chunk-RHYNACZS.js → chunk-EJBK7I4F.js} +3 -3
  11. package/dist/{chunk-OTR44OH6.js → chunk-FVNAU7VO.js} +30 -9
  12. package/dist/chunk-FVNAU7VO.js.map +1 -0
  13. package/dist/{chunk-OITTYGJS.js → chunk-FWRL7BZ5.js} +4 -4
  14. package/dist/{chunk-3MAZ4TQH.js → chunk-HOIRY5XP.js} +13 -13
  15. package/dist/{chunk-GJDEPTPY.js → chunk-HPS554L4.js} +10 -10
  16. package/dist/{chunk-P3AYBRP6.js → chunk-JA7GJDNI.js} +16 -10
  17. package/dist/chunk-JA7GJDNI.js.map +1 -0
  18. package/dist/{chunk-36U5UGIO.js → chunk-JEINYUJH.js} +8 -5
  19. package/dist/chunk-JEINYUJH.js.map +1 -0
  20. package/dist/{chunk-K2I6XIK5.js → chunk-KSTZIULO.js} +4 -4
  21. package/dist/{chunk-Z7PQCAVK.js → chunk-LQ6PYFU6.js} +4 -4
  22. package/dist/{chunk-L3VJ47BU.js → chunk-PSDVGPQR.js} +5 -5
  23. package/dist/{chunk-DTXH24LR.js → chunk-SFQRETXJ.js} +2 -2
  24. package/dist/{chunk-NXNVTXKG.js → chunk-SGSWVNNB.js} +5 -5
  25. package/dist/{chunk-7LKAMLV4.js → chunk-T6SCOJF4.js} +4 -4
  26. package/dist/{chunk-OGIZXGPY.js → chunk-TDEHU73T.js} +4 -4
  27. package/dist/{chunk-3VEVGL74.js → chunk-VNBC3VXM.js} +4 -4
  28. package/dist/{chunk-DCCZB4UC.js → chunk-XWBK3XJK.js} +4 -4
  29. package/dist/{chunk-4GLNY5V6.js → chunk-Y7GDG744.js} +5 -5
  30. package/dist/{chunk-SR7F3TJY.js → chunk-YK5JEVLX.js} +4 -4
  31. package/dist/{job-orchestrator.protocol-DubMVbm9.d.ts → job-orchestrator.protocol-ZuJ3ow-O.d.ts} +77 -3
  32. package/dist/runtime/base-classes/index.js +19 -19
  33. package/dist/runtime/shared/openapi/index.js +7 -7
  34. package/dist/runtime/shared/openapi/registry.js +2 -2
  35. package/dist/runtime/subsystems/auth/auth.module.js +2 -2
  36. package/dist/runtime/subsystems/auth/index.js +5 -5
  37. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.d.ts +1 -1
  38. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js +2 -2
  39. package/dist/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.js +2 -2
  40. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js +4 -4
  41. package/dist/runtime/subsystems/bridge/bridge.module.d.ts +1 -1
  42. package/dist/runtime/subsystems/bridge/bridge.module.js +15 -15
  43. package/dist/runtime/subsystems/bridge/event-flow.service.d.ts +1 -1
  44. package/dist/runtime/subsystems/bridge/index.d.ts +1 -1
  45. package/dist/runtime/subsystems/bridge/index.js +17 -17
  46. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +2 -2
  47. package/dist/runtime/subsystems/events/event-bus.memory-backend.js +2 -2
  48. package/dist/runtime/subsystems/events/events.module.js +4 -4
  49. package/dist/runtime/subsystems/events/index.js +4 -4
  50. package/dist/runtime/subsystems/index.d.ts +1 -1
  51. package/dist/runtime/subsystems/index.js +90 -90
  52. package/dist/runtime/subsystems/integration/deep-equal.differ.d.ts +19 -0
  53. package/dist/runtime/subsystems/integration/deep-equal.differ.js +1 -1
  54. package/dist/runtime/subsystems/integration/execute-integration.use-case.js +2 -2
  55. package/dist/runtime/subsystems/integration/index.js +24 -24
  56. package/dist/runtime/subsystems/integration/integration-cursor-store.drizzle-backend.js +2 -2
  57. package/dist/runtime/subsystems/integration/integration-run-recorder.drizzle-backend.js +2 -2
  58. package/dist/runtime/subsystems/integration/integration.module.d.ts +20 -0
  59. package/dist/runtime/subsystems/integration/integration.module.js +6 -6
  60. package/dist/runtime/subsystems/jobs/index.d.ts +1 -1
  61. package/dist/runtime/subsystems/jobs/index.js +32 -32
  62. package/dist/runtime/subsystems/jobs/job-handler.base.d.ts +1 -1
  63. package/dist/runtime/subsystems/jobs/job-handler.base.js +11 -3
  64. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.d.ts +1 -1
  65. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +5 -4
  66. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -1
  67. package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.d.ts +1 -1
  68. package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.js +2 -1
  69. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.d.ts +11 -1
  70. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js +2 -2
  71. package/dist/runtime/subsystems/jobs/job-orchestrator.protocol.d.ts +1 -1
  72. package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.d.ts +1 -1
  73. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.d.ts +1 -1
  74. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +2 -2
  75. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.d.ts +1 -1
  76. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +2 -2
  77. package/dist/runtime/subsystems/jobs/job-run-service.protocol.d.ts +1 -1
  78. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.d.ts +1 -1
  79. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +1 -1
  80. package/dist/runtime/subsystems/jobs/job-worker.d.ts +1 -1
  81. package/dist/runtime/subsystems/jobs/job-worker.js +2 -2
  82. package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +1 -1
  83. package/dist/runtime/subsystems/jobs/job-worker.module.js +10 -10
  84. package/dist/runtime/subsystems/jobs/jobs-domain.module.js +8 -8
  85. package/dist/runtime/subsystems/jobs/jobs-errors.d.ts +1 -1
  86. package/dist/runtime/subsystems/observability/index.d.ts +1 -1
  87. package/dist/runtime/subsystems/observability/observability.protocol.d.ts +1 -1
  88. package/dist/runtime/subsystems/observability/observability.service.d.ts +1 -1
  89. package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.d.ts +1 -1
  90. package/dist/runtime/subsystems/observability/reporters/index.d.ts +1 -1
  91. package/dist/runtime/subsystems/storage/index.js +4 -4
  92. package/dist/runtime/subsystems/storage/storage.module.js +2 -2
  93. package/dist/src/cli/index.js +34 -12
  94. package/dist/src/cli/index.js.map +1 -1
  95. package/dist/src/index.js +9 -9
  96. package/package.json +1 -1
  97. package/runtime/subsystems/integration/deep-equal.differ.ts +34 -5
  98. package/runtime/subsystems/integration/integration.module.ts +26 -2
  99. package/runtime/subsystems/jobs/job-handler.base.ts +115 -2
  100. package/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.ts +43 -16
  101. package/runtime/subsystems/jobs/job-orchestrator.memory-backend.ts +58 -18
  102. package/templates/subsystem/integration-config/codegen-config-integration-block.ejs.t +17 -0
  103. package/dist/chunk-36U5UGIO.js.map +0 -1
  104. package/dist/chunk-BULPAAD3.js.map +0 -1
  105. package/dist/chunk-CO6LUM72.js.map +0 -1
  106. package/dist/chunk-OTR44OH6.js.map +0 -1
  107. package/dist/chunk-P3AYBRP6.js.map +0 -1
  108. /package/dist/{chunk-UTNWFHJF.js.map → chunk-4PFF3ED4.js.map} +0 -0
  109. /package/dist/{chunk-CDLWYZVQ.js.map → chunk-BHZP6LOV.js.map} +0 -0
  110. /package/dist/{chunk-4MVGAMUA.js.map → chunk-BK5ICA2F.js.map} +0 -0
  111. /package/dist/{chunk-RHYNACZS.js.map → chunk-EJBK7I4F.js.map} +0 -0
  112. /package/dist/{chunk-OITTYGJS.js.map → chunk-FWRL7BZ5.js.map} +0 -0
  113. /package/dist/{chunk-3MAZ4TQH.js.map → chunk-HOIRY5XP.js.map} +0 -0
  114. /package/dist/{chunk-GJDEPTPY.js.map → chunk-HPS554L4.js.map} +0 -0
  115. /package/dist/{chunk-K2I6XIK5.js.map → chunk-KSTZIULO.js.map} +0 -0
  116. /package/dist/{chunk-Z7PQCAVK.js.map → chunk-LQ6PYFU6.js.map} +0 -0
  117. /package/dist/{chunk-L3VJ47BU.js.map → chunk-PSDVGPQR.js.map} +0 -0
  118. /package/dist/{chunk-DTXH24LR.js.map → chunk-SFQRETXJ.js.map} +0 -0
  119. /package/dist/{chunk-NXNVTXKG.js.map → chunk-SGSWVNNB.js.map} +0 -0
  120. /package/dist/{chunk-7LKAMLV4.js.map → chunk-T6SCOJF4.js.map} +0 -0
  121. /package/dist/{chunk-OGIZXGPY.js.map → chunk-TDEHU73T.js.map} +0 -0
  122. /package/dist/{chunk-3VEVGL74.js.map → chunk-VNBC3VXM.js.map} +0 -0
  123. /package/dist/{chunk-DCCZB4UC.js.map → chunk-XWBK3XJK.js.map} +0 -0
  124. /package/dist/{chunk-4GLNY5V6.js.map → chunk-Y7GDG744.js.map} +0 -0
  125. /package/dist/{chunk-SR7F3TJY.js.map → chunk-YK5JEVLX.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../runtime/subsystems/jobs/job-orchestrator.drizzle-backend.ts"],"sourcesContent":["/**\n * DrizzleJobOrchestrator — Postgres-backed implementation of\n * `IJobOrchestrator` (ADR-022, JOB-3).\n *\n * Single-layer architecture: `start` writes a single `job_run` row; the\n * `JobWorker` polling loop claims it directly via `FOR UPDATE SKIP LOCKED`.\n * No `job_queue` table, no executor port. See `docs/specs/JOB-3.md`.\n */\nimport { randomUUID } from 'node:crypto';\nimport { Inject, Injectable, Logger, Optional } from '@nestjs/common';\nimport { and, desc, eq, gt, inArray, isNotNull, ne, notInArray, sql } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport type { DrizzleTransaction } from '../events/event-bus.protocol';\nimport { DRIZZLE } from '../../constants/tokens';\nimport {\n jobRuns,\n jobs,\n type JobDefinitionRow,\n type JobRunRow,\n} from './job-orchestration.schema';\nimport type {\n CancelOptions,\n IJobOrchestrator,\n JobPoolDef,\n JobRun,\n JobUpsertEntry,\n StartOptions,\n} from './job-orchestrator.protocol';\nimport {\n JobCollisionError,\n JobNotReplayableError,\n JobTemplateFieldMissingError,\n JobTypeNotFoundError,\n MissingTenantIdError,\n} from './jobs-errors';\nimport { jobSteps } from './job-orchestration.schema';\nimport { JOBS_MULTI_TENANT, JOBS_LISTEN_NOTIFY } from './jobs-domain.tokens';\nimport { JOBS_WAKE_CHANNEL, pgNotify } from './pg-notify';\nimport {\n keySelectorToTemplate,\n resolveJobKey,\n type JobKeySelector,\n} from './job-handler.base';\n\n/**\n * Terminal statuses — transitions into these are final. Used by `cancel`\n * (to short-circuit idempotently) and by `replay` (as the guard gate).\n */\nexport const TERMINAL_STATUSES = [\n 'completed',\n 'failed',\n 'timed_out',\n 'canceled',\n] as const;\ntype TerminalStatus = (typeof TERMINAL_STATUSES)[number];\ntype JobRunStatus = JobRunRow['status'];\n\n/** Statuses excluded from dedupe window matches per ADR-022. */\nconst DEDUPE_EXCLUDED_STATUSES: JobRunStatus[] = ['canceled', 'failed'];\n/** Statuses that count as in-flight for concurrency collision checks. */\nconst IN_FLIGHT_STATUSES: JobRunStatus[] = ['pending', 'running'];\n\n/**\n * Substitute `{{field}}` placeholders against the input payload.\n *\n * Implementation decision (JOB-3, 2026-04-19): simple `{{field}}` single-key\n * substitution, no dotted paths, no Mustache/Handlebars dependency. A missing\n * field throws `JobTemplateFieldMissingError` synchronously — cheaper than\n * discovering the misconfiguration at claim time.\n */\nexport function evaluateKeyTemplate(\n template: string,\n input: Record<string, unknown>,\n): string {\n return template.replace(/\\{\\{\\s*([a-zA-Z0-9_]+)\\s*\\}\\}/g, (_match, field: string) => {\n const value = input[field];\n if (value === undefined || value === null) {\n throw new JobTemplateFieldMissingError(template, field);\n }\n return String(value);\n });\n}\n\n@Injectable()\nexport class DrizzleJobOrchestrator implements IJobOrchestrator {\n // TODO(logging-subsystem): swap to ILogger once ADR-028 lands\n private readonly logger = new Logger(DrizzleJobOrchestrator.name);\n\n constructor(\n @Inject(DRIZZLE) private readonly db: DrizzleClient,\n @Inject(JOBS_MULTI_TENANT) private readonly multiTenant: boolean,\n // LISTEN-NOTIFY-1 — when true, `start()` emits an in-tx\n // `pg_notify(codegen_jobs_wake, <pool>)` so a `listen_notify` worker wakes\n // on enqueue-commit. `@Optional()` defaulting to false so direct\n // construction (integration tests not going through DI) keeps working.\n @Optional()\n @Inject(JOBS_LISTEN_NOTIFY)\n private readonly listenNotify: boolean = false,\n ) {}\n\n /**\n * JOB-8 — resolve `tenantId` for a mutating / targeted-read call.\n * Returns the tenant value that should be written to the row (or compared\n * against in a WHERE clause). When `multiTenant` is off, the column is\n * forced to `null` regardless of what callers pass. When on, `undefined`\n * throws; `null` and strings pass through untouched.\n */\n private resolveTenantId(\n method: string,\n tenantId: string | null | undefined,\n ): string | null {\n if (!this.multiTenant) return null;\n if (tenantId === undefined) throw new MissingTenantIdError(method);\n return tenantId;\n }\n\n // ==========================================================================\n // start\n // ==========================================================================\n\n async start(\n type: string,\n input: unknown,\n opts: StartOptions = {},\n tx?: DrizzleTransaction,\n ): Promise<JobRun> {\n const payload = (input ?? {}) as Record<string, unknown>;\n\n // JOB-8 — resolve tenant gate up front so `multi_tenant=true` +\n // undefined surfaces before any row is touched.\n const tenantId = this.resolveTenantId('start', opts.tenantId);\n\n // BRIDGE-7: thread the optional caller tx through every read/write\n // in this method so EventFlowService.publishAndStart can bundle the\n // outbox insert, the eager job_run insert, and (for Case B) the\n // bridge_delivery pre-write into a single transaction.\n const client = (tx ?? this.db) as DrizzleClient;\n\n // 1a. Load job definition.\n const [def] = await client\n .select()\n .from(jobs)\n .where(eq(jobs.type, type))\n .limit(1);\n if (!def) throw new JobTypeNotFoundError(type);\n const definition = def as JobDefinitionRow;\n\n // 1b. Dedupe check. JOB-FN-KEY: `resolveJobKey` honors both the `{{field}}`\n // template AND a function key persisted as `FN_KEY_SENTINEL` (re-resolved\n // live from JOB_HANDLER_REGISTRY).\n if (definition.dedupeKeyTemplate && definition.dedupeWindowMs) {\n const dedupeKey = resolveJobKey(\n 'dedupe',\n type,\n definition.dedupeKeyTemplate,\n payload,\n evaluateKeyTemplate,\n ) as string;\n const windowStart = new Date(Date.now() - definition.dedupeWindowMs);\n const existing = await client\n .select()\n .from(jobRuns)\n .where(\n and(\n eq(jobRuns.jobType, type),\n eq(jobRuns.dedupeKey, dedupeKey),\n gt(jobRuns.createdAt, windowStart),\n // status NOT IN ('canceled', 'failed')\n notInStatus(DEDUPE_EXCLUDED_STATUSES),\n ),\n )\n .orderBy(desc(jobRuns.createdAt))\n .limit(1);\n if (existing.length > 0) {\n return existing[0] as JobRun;\n }\n }\n\n // 1c. Concurrency collision check.\n let concurrencyKey: string | null = null;\n if (definition.concurrencyKeyTemplate) {\n // Non-null cast: the branch guard proves the template is present, so the\n // resolver never returns null here (it only nulls on a null template).\n concurrencyKey = resolveJobKey(\n 'concurrency',\n type,\n definition.concurrencyKeyTemplate,\n payload,\n evaluateKeyTemplate,\n ) as string;\n const inFlight = await client\n .select()\n .from(jobRuns)\n .where(\n and(\n eq(jobRuns.concurrencyKey, concurrencyKey),\n inArray(jobRuns.status, IN_FLIGHT_STATUSES),\n ),\n )\n .limit(1);\n if (inFlight.length > 0) {\n const incumbent = inFlight[0] as JobRun;\n switch (definition.collisionMode) {\n case 'reject':\n throw new JobCollisionError(type, concurrencyKey, incumbent);\n case 'replace':\n // JOB-8 — thread the incumbent's own tenantId through the\n // internal cascade. Without this, every `replace`-collision\n // start() under multiTenant=true throws MissingTenantIdError\n // from the inner cancel() call instead of cancelling the\n // incumbent. Mirrors the memory backend's `cancelLocked(\n // incumbent.id, ..., incumbent.tenantId)` pattern.\n await this.cancel(incumbent.id, {\n cascade: true,\n reason: 'replaced',\n tenantId: incumbent.tenantId,\n });\n break;\n case 'queue':\n // Fall through — row is inserted; claim query gates it until\n // the incumbent transitions (see JobWorker.processRun queue gate).\n break;\n }\n }\n }\n\n // 1d. Resolve id + rootRunId, INSERT.\n const newId = randomUUID();\n let rootRunId: string = newId;\n if (opts.parentRunId) {\n const [parent] = await client\n .select({ rootRunId: jobRuns.rootRunId })\n .from(jobRuns)\n .where(eq(jobRuns.id, opts.parentRunId))\n .limit(1);\n if (!parent) {\n throw new Error(\n `parentRunId ${opts.parentRunId} does not reference an existing job_run`,\n );\n }\n rootRunId = parent.rootRunId;\n }\n\n const dedupeKey = resolveJobKey(\n 'dedupe',\n type,\n definition.dedupeKeyTemplate,\n payload,\n evaluateKeyTemplate,\n );\n\n const [inserted] = await client\n .insert(jobRuns)\n .values({\n id: newId,\n jobType: type,\n jobVersion: definition.version,\n parentRunId: opts.parentRunId ?? null,\n rootRunId,\n parentClosePolicy: opts.parentClosePolicy ?? 'terminate',\n scopeEntityType: opts.scope?.entityType ?? null,\n scopeEntityId: opts.scope?.entityId ?? null,\n tenantId,\n tags: opts.tags ?? {},\n pool: opts.pool ?? definition.pool,\n priority: opts.priority ?? definition.priorityDefault,\n concurrencyKey,\n dedupeKey,\n status: 'pending',\n input: payload,\n output: null,\n error: null,\n triggerSource: opts.triggerSource ?? 'manual',\n triggerRef: opts.triggerRef ?? null,\n runAt: opts.runAt ?? new Date(),\n startedAt: null,\n finishedAt: null,\n claimedAt: null,\n attempts: 0,\n })\n .returning();\n\n // LISTEN-NOTIFY-1 — wake a listening worker the instant this enqueue\n // commits. Emitted through the SAME `client` (the caller's tx when one was\n // passed, else the pool) so delivery is gated on commit — a rolled-back\n // enqueue emits no phantom wake (D2). The pool name is the payload; the\n // worker re-runs its own pool-filtered claim query on wake. Polling is the\n // fallback, so a failed notify is non-fatal: log + continue.\n if (this.listenNotify) {\n const wakePool = (inserted as JobRunRow).pool;\n try {\n await pgNotify(client, JOBS_WAKE_CHANNEL, wakePool);\n } catch (err) {\n this.logger.warn(\n `pg_notify(${JOBS_WAKE_CHANNEL}, ${wakePool}) failed for run ` +\n `${(inserted as JobRunRow).id}: ${(err as Error).message} ` +\n `(non-fatal — interval polling still claims the run).`,\n );\n }\n }\n\n return inserted as JobRun;\n }\n\n // ==========================================================================\n // cancel\n // ==========================================================================\n\n async cancel(runId: string, opts: CancelOptions = {}): Promise<void> {\n // JOB-8 — resolve tenant gate up front (strict undefined-throws).\n const tenantId = this.resolveTenantId('cancel', opts.tenantId);\n\n // Load target.\n const [target] = await this.db\n .select()\n .from(jobRuns)\n .where(eq(jobRuns.id, runId))\n .limit(1);\n if (!target) return;\n // JOB-8 — cross-tenant cancel is a silent no-op (no existence leak).\n if (this.multiTenant && target.tenantId !== tenantId) return;\n if (TERMINAL_STATUSES.includes(target.status as TerminalStatus)) {\n return; // idempotent\n }\n\n // Atomic transition, guarded against concurrent terminal moves.\n const [cancelled] = await this.db\n .update(jobRuns)\n .set({\n status: 'canceled',\n finishedAt: new Date(),\n updatedAt: new Date(),\n })\n .where(\n and(eq(jobRuns.id, runId), notInStatus([...TERMINAL_STATUSES])),\n )\n .returning();\n\n if (!cancelled) return; // lost the race; already terminal\n\n if (opts.cascade === false) return;\n\n // Fetch descendants and branch on parent_close_policy.\n const descendants = await this.db\n .select()\n .from(jobRuns)\n .where(\n and(\n eq(jobRuns.rootRunId, target.rootRunId),\n ne(jobRuns.id, runId),\n notInStatus([...TERMINAL_STATUSES]),\n ),\n );\n\n for (const child of descendants) {\n const policy = (child as JobRunRow).parentClosePolicy;\n if (policy === 'abandon') continue;\n // 'terminate' | 'cancel' — both transition the child to canceled.\n await this.db\n .update(jobRuns)\n .set({\n status: 'canceled',\n finishedAt: new Date(),\n updatedAt: new Date(),\n })\n .where(\n and(\n eq(jobRuns.id, (child as JobRunRow).id),\n notInStatus([...TERMINAL_STATUSES]),\n ),\n );\n }\n\n void opts.reason; // reserved for future audit logging\n }\n\n // ==========================================================================\n // replay\n // ==========================================================================\n\n async replay(runId: string): Promise<JobRun> {\n // Load target + its job definition (we need replay_from).\n const [target] = await this.db\n .select()\n .from(jobRuns)\n .where(eq(jobRuns.id, runId))\n .limit(1);\n if (!target) {\n throw new Error(`replay: run ${runId} not found`);\n }\n const run = target as JobRunRow;\n if (!TERMINAL_STATUSES.includes(run.status as TerminalStatus)) {\n throw new JobNotReplayableError(runId, run.status);\n }\n\n const [def] = await this.db\n .select()\n .from(jobs)\n .where(eq(jobs.type, run.jobType))\n .limit(1);\n if (!def) throw new JobTypeNotFoundError(run.jobType);\n const mode = (def as JobDefinitionRow).replayFrom;\n\n // Atomic: step reset + run reset must commit together.\n const result = await this.db.transaction(async (tx) => {\n if (mode === 'scratch') {\n await tx.delete(jobSteps).where(eq(jobSteps.jobRunId, runId));\n } else if (mode === 'last_step') {\n // Delete only non-completed step rows — completed steps stay memoised.\n await tx\n .delete(jobSteps)\n .where(\n and(eq(jobSteps.jobRunId, runId), ne(jobSteps.status, 'completed')),\n );\n } else {\n // 'last_checkpoint' — Phase 1 has no explicit checkpoint markers, so\n // behaviour collapses to `last_step`. See docs/specs/JOB-3.md\n // \"Implementation Decisions\" — planned divergence in a later phase.\n await tx\n .delete(jobSteps)\n .where(\n and(eq(jobSteps.jobRunId, runId), ne(jobSteps.status, 'completed')),\n );\n }\n\n const [updated] = await tx\n .update(jobRuns)\n .set({\n status: 'pending',\n attempts: 0,\n runAt: new Date(),\n startedAt: null,\n finishedAt: null,\n claimedAt: null,\n error: null,\n output: null,\n updatedAt: new Date(),\n })\n .where(eq(jobRuns.id, runId))\n .returning();\n return updated as JobRunRow;\n });\n\n return result as JobRun;\n }\n\n // ==========================================================================\n // upsertJobRows — boot-time materialisation of `job` definitions\n // ==========================================================================\n\n /**\n * Hash-gated `INSERT … ON CONFLICT (type) DO UPDATE … WHERE` per Q3\n * resolution (2026-04-19): the `UPDATE` branch executes only when one\n * of the persisted metadata fields differs from the incoming payload;\n * `version` bumps only on real change; concurrent boots with identical\n * content are idempotent no-ops.\n *\n * Why this shape (not `DO NOTHING`, not advisory locks):\n * - `DO NOTHING` would let an old-version instance leave a stale row\n * that a new-version instance can't overwrite during a rolling deploy.\n * - Advisory locks add latency and leak risk under crashes.\n * - The `WHERE … IS DISTINCT FROM …` clause makes the conditional\n * atomic — no read-modify-write race on `version` between concurrent\n * boots.\n *\n * Orphan detection: a single `SELECT type FROM job WHERE type NOT IN (...)`\n * returns the types present in DB but absent from `entries`. Caller (boot\n * validator) decides whether to throw `BootValidationError`.\n */\n async upsertJobRows(\n entries: JobUpsertEntry[],\n poolConfig: ReadonlyMap<string, JobPoolDef>,\n ): Promise<{ orphaned: string[] }> {\n void poolConfig; // pool validation is the module's responsibility; orchestrator just persists\n\n for (const entry of entries) {\n const meta = entry.meta;\n const pool = meta.pool ?? 'batch';\n const retryPolicy = meta.retry ?? {\n attempts: 1,\n backoff: 'fixed' as const,\n baseMs: 0,\n };\n // JOB-FN-KEY (0.16.2): both authored key forms are honored. A `{{field}}`\n // string is persisted verbatim; a function is persisted as\n // `FN_KEY_SENTINEL` (non-null so the collision/dedupe path engages, and\n // hash-stable so the definition-hash gate doesn't churn on every boot —\n // the function identity can't be hashed). `start()` re-resolves the live\n // function from `JOB_HANDLER_REGISTRY`. The pre-0.16.2 `typeof === string\n // ? … : null` dropped function keys to null, so `collisionMode` silently\n // never engaged.\n const concurrencyKeyTemplateStr = keySelectorToTemplate(\n meta.concurrency?.key as JobKeySelector<unknown> | undefined,\n );\n const collisionMode =\n (meta.concurrency?.collisionMode as JobDefinitionRow['collisionMode']) ??\n 'queue';\n const dedupeKeyTemplateStr = keySelectorToTemplate(\n meta.dedupe?.key as JobKeySelector<unknown> | undefined,\n );\n const dedupeWindowMs = meta.dedupe?.windowMs ?? null;\n const timeoutMs = meta.timeoutMs ?? null;\n const replayFrom = meta.replayFrom ?? 'last_checkpoint';\n const scopeEntityType = meta.scope?.entity ?? null;\n // Q3 resolution: priority_default and replay_from are part of the\n // hashed metadata even though they aren't currently set via decorator\n // metadata above (priority_default has no `@JobHandler` field yet).\n // Default to 0 to keep UPDATE branch quiet across deploys.\n const priorityDefault = 0;\n\n // Hash-gated upsert: every metadata column appears in the WHERE clause\n // so the UPDATE branch only fires on a real change. `version` bumps\n // exactly when the WHERE matches.\n await this.db\n .insert(jobs)\n .values({\n type: entry.type,\n version: 1,\n pool,\n scopeEntityType,\n retryPolicy,\n timeoutMs,\n concurrencyKeyTemplate: concurrencyKeyTemplateStr,\n collisionMode,\n dedupeKeyTemplate: dedupeKeyTemplateStr,\n dedupeWindowMs,\n priorityDefault,\n replayFrom,\n })\n .onConflictDoUpdate({\n target: jobs.type,\n set: {\n pool: sql`EXCLUDED.pool`,\n scopeEntityType: sql`EXCLUDED.scope_entity_type`,\n retryPolicy: sql`EXCLUDED.retry_policy`,\n timeoutMs: sql`EXCLUDED.timeout_ms`,\n concurrencyKeyTemplate: sql`EXCLUDED.concurrency_key_template`,\n collisionMode: sql`EXCLUDED.collision_mode`,\n dedupeKeyTemplate: sql`EXCLUDED.dedupe_key_template`,\n dedupeWindowMs: sql`EXCLUDED.dedupe_window_ms`,\n priorityDefault: sql`EXCLUDED.priority_default`,\n replayFrom: sql`EXCLUDED.replay_from`,\n version: sql`${jobs.version} + 1`,\n updatedAt: sql`now()`,\n },\n // The hash gate: every field listed in the Q3 resolution appears\n // here. `IS DISTINCT FROM` is the null-safe inequality operator;\n // jsonb cast to text gives stable comparison without invoking a\n // dedicated hash column (avoids a JOB-1 schema migration).\n setWhere: sql`\n ${jobs.pool} IS DISTINCT FROM EXCLUDED.pool OR\n ${jobs.retryPolicy}::text IS DISTINCT FROM EXCLUDED.retry_policy::text OR\n ${jobs.timeoutMs} IS DISTINCT FROM EXCLUDED.timeout_ms OR\n ${jobs.concurrencyKeyTemplate} IS DISTINCT FROM EXCLUDED.concurrency_key_template OR\n ${jobs.collisionMode} IS DISTINCT FROM EXCLUDED.collision_mode OR\n ${jobs.dedupeKeyTemplate} IS DISTINCT FROM EXCLUDED.dedupe_key_template OR\n ${jobs.dedupeWindowMs} IS DISTINCT FROM EXCLUDED.dedupe_window_ms OR\n ${jobs.priorityDefault} IS DISTINCT FROM EXCLUDED.priority_default OR\n ${jobs.replayFrom} IS DISTINCT FROM EXCLUDED.replay_from OR\n ${jobs.scopeEntityType} IS DISTINCT FROM EXCLUDED.scope_entity_type\n `,\n });\n }\n\n // Orphan detection: any `job` row whose type is not in the registry.\n const types = entries.map((e) => e.type);\n const orphans =\n types.length === 0\n ? await this.db.select({ type: jobs.type }).from(jobs)\n : await this.db\n .select({ type: jobs.type })\n .from(jobs)\n .where(notInArray(jobs.type, types));\n\n return { orphaned: orphans.map((o) => o.type) };\n }\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nfunction notInStatus(statuses: JobRunStatus[]) {\n // Drizzle's inArray composes with `not` via negation helper; use raw sql\n // to stay readable. `inArray` + `.not()` isn't idiomatic in 0.45.\n const negated = statuses.map((s) => ne(jobRuns.status, s));\n return and(...negated);\n}\n\n// `isNotNull` + `gt` imports are retained for potential future use; silence\n// unused-import lint by re-exporting via `void`.\nvoid isNotNull;\n"],"mappings":"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","names":["dedupeKey"]}
@@ -1,11 +1,11 @@
1
- import {
2
- JOB_HANDLER_REGISTRY
3
- } from "./chunk-CO6LUM72.js";
4
1
  import {
5
2
  JOB_ORCHESTRATOR,
6
3
  JOB_RUN_SERVICE,
7
4
  JOB_STEP_SERVICE
8
5
  } from "./chunk-ZPL74UQN.js";
6
+ import {
7
+ JOB_HANDLER_REGISTRY
8
+ } from "./chunk-7P5ODGLA.js";
9
9
  import {
10
10
  jobRuns
11
11
  } from "./chunk-OKXZ63IA.js";
@@ -518,4 +518,4 @@ export {
518
518
  buildStaleSweepQuery,
519
519
  JobWorker
520
520
  };
521
- //# sourceMappingURL=chunk-OITTYGJS.js.map
521
+ //# sourceMappingURL=chunk-FWRL7BZ5.js.map
@@ -1,18 +1,21 @@
1
- import {
2
- MemoryJobOrchestrator
3
- } from "./chunk-BULPAAD3.js";
4
- import {
5
- DrizzleJobRunService
6
- } from "./chunk-3VEVGL74.js";
7
- import {
8
- MemoryJobRunService
9
- } from "./chunk-CDLWYZVQ.js";
10
1
  import {
11
2
  DrizzleJobStepService
12
3
  } from "./chunk-DV4RV2DC.js";
4
+ import {
5
+ DrizzleJobOrchestrator
6
+ } from "./chunk-FVNAU7VO.js";
7
+ import {
8
+ MemoryJobOrchestrator
9
+ } from "./chunk-DUMI2J5M.js";
13
10
  import {
14
11
  MemoryJobStepService
15
12
  } from "./chunk-PNZSGAB2.js";
13
+ import {
14
+ DrizzleJobRunService
15
+ } from "./chunk-VNBC3VXM.js";
16
+ import {
17
+ MemoryJobRunService
18
+ } from "./chunk-BHZP6LOV.js";
16
19
  import {
17
20
  MemoryJobStore
18
21
  } from "./chunk-SNQ3TOWP.js";
@@ -21,9 +24,6 @@ import {
21
24
  BULLMQ_RESOLVED_CONFIG,
22
25
  resolveBullMqConfig
23
26
  } from "./chunk-I6MVCB5A.js";
24
- import {
25
- DrizzleJobOrchestrator
26
- } from "./chunk-OTR44OH6.js";
27
27
  import {
28
28
  JOBS_LISTEN_NOTIFY,
29
29
  JOBS_MULTI_TENANT,
@@ -114,4 +114,4 @@ JobsDomainModule = __decorateClass([
114
114
  export {
115
115
  JobsDomainModule
116
116
  };
117
- //# sourceMappingURL=chunk-3MAZ4TQH.js.map
117
+ //# sourceMappingURL=chunk-HOIRY5XP.js.map
@@ -1,9 +1,13 @@
1
1
  import {
2
2
  JobWorker
3
- } from "./chunk-OITTYGJS.js";
3
+ } from "./chunk-FWRL7BZ5.js";
4
4
  import {
5
5
  JobsDomainModule
6
- } from "./chunk-3MAZ4TQH.js";
6
+ } from "./chunk-HOIRY5XP.js";
7
+ import {
8
+ BootValidationError,
9
+ ReservedPoolViolationError
10
+ } from "./chunk-T4BIIU5E.js";
7
11
  import {
8
12
  BULLMQ_CONNECTION,
9
13
  BULLMQ_RESOLVED_CONFIG,
@@ -14,18 +18,14 @@ import {
14
18
  allPoolNames,
15
19
  loadPoolConfig
16
20
  } from "./chunk-RHVN6NA7.js";
17
- import {
18
- BootValidationError,
19
- ReservedPoolViolationError
20
- } from "./chunk-T4BIIU5E.js";
21
- import {
22
- HandlerRegistry
23
- } from "./chunk-CO6LUM72.js";
24
21
  import {
25
22
  JOB_ORCHESTRATOR,
26
23
  JOB_RUN_SERVICE,
27
24
  JOB_STEP_SERVICE
28
25
  } from "./chunk-ZPL74UQN.js";
26
+ import {
27
+ HandlerRegistry
28
+ } from "./chunk-7P5ODGLA.js";
29
29
  import {
30
30
  tokenKey
31
31
  } from "./chunk-GYGNEQSC.js";
@@ -290,4 +290,4 @@ export {
290
290
  JobWorkerOrchestrator,
291
291
  JobWorkerModule
292
292
  };
293
- //# sourceMappingURL=chunk-GJDEPTPY.js.map
293
+ //# sourceMappingURL=chunk-HPS554L4.js.map
@@ -1,18 +1,15 @@
1
+ import {
2
+ MemoryCursorStore
3
+ } from "./chunk-AHV4GDYM.js";
1
4
  import {
2
5
  DrizzleIntegrationRunRecorder
3
- } from "./chunk-SR7F3TJY.js";
6
+ } from "./chunk-YK5JEVLX.js";
4
7
  import {
5
8
  MemoryRunRecorder
6
9
  } from "./chunk-EO2QPOKH.js";
7
10
  import {
8
11
  PostgresCursorStore
9
- } from "./chunk-DCCZB4UC.js";
10
- import {
11
- MemoryCursorStore
12
- } from "./chunk-AHV4GDYM.js";
13
- import {
14
- DeepEqualDiffer
15
- } from "./chunk-36U5UGIO.js";
12
+ } from "./chunk-XWBK3XJK.js";
16
13
  import {
17
14
  INTEGRATION_CURSOR_STORE,
18
15
  INTEGRATION_FIELD_DIFFER,
@@ -20,6 +17,9 @@ import {
20
17
  INTEGRATION_MULTI_TENANT,
21
18
  INTEGRATION_RUN_RECORDER
22
19
  } from "./chunk-S7C6TIIF.js";
20
+ import {
21
+ DeepEqualDiffer
22
+ } from "./chunk-JEINYUJH.js";
23
23
  import {
24
24
  __decorateClass
25
25
  } from "./chunk-2E224ZSN.js";
@@ -34,7 +34,13 @@ var IntegrationModule = class {
34
34
  { provide: INTEGRATION_MULTI_TENANT, useValue: multiTenant },
35
35
  // Default differ — consumers can override by binding a different
36
36
  // `IFieldDiffer<T>` to `INTEGRATION_FIELD_DIFFER` in their feature module.
37
- { provide: INTEGRATION_FIELD_DIFFER, useValue: new DeepEqualDiffer() }
37
+ // DIFFER-UNIGNORE: `options.differ` (ignore/unignore) is threaded here so
38
+ // a consumer can declare a default-ignored column (e.g. `deletedAt`) as
39
+ // domain data for their entities without binding a bespoke differ.
40
+ {
41
+ provide: INTEGRATION_FIELD_DIFFER,
42
+ useValue: new DeepEqualDiffer(options.differ ?? {})
43
+ }
38
44
  ];
39
45
  const backendProviders = options.backend === "memory" ? [
40
46
  // Wired as singletons via `useValue` so tests can pull
@@ -78,4 +84,4 @@ IntegrationModule = __decorateClass([
78
84
  export {
79
85
  IntegrationModule
80
86
  };
81
- //# sourceMappingURL=chunk-P3AYBRP6.js.map
87
+ //# sourceMappingURL=chunk-JA7GJDNI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../runtime/subsystems/integration/integration.module.ts"],"sourcesContent":["/**\n * IntegrationModule — `DynamicModule.forRoot({ backend, multiTenant? })` factory\n * wiring the integration subsystem's substrate (SYNC-6, ADR-008 subsystem pattern).\n *\n * ## What this module provides\n *\n * - `INTEGRATION_CURSOR_STORE` — Drizzle or Memory cursor store\n * - `INTEGRATION_RUN_RECORDER` — Drizzle or Memory run recorder\n * - `INTEGRATION_FIELD_DIFFER` — default `DeepEqualDiffer`\n * - `INTEGRATION_MULTI_TENANT` — resolved boolean flag (defaults to false)\n * - `INTEGRATION_MODULE_OPTIONS` — the options object itself, for backends\n * that need to inspect config at construction time\n *\n * ## What this module does NOT provide\n *\n * - `INTEGRATION_CHANGE_SOURCE` — per-provider per-entity; consumer binds in\n * their feature module (e.g. `OpportunityIntegrationModule` provides a\n * `SalesforceOpportunityChangeSource`). Loopback suppression — when\n * needed — is composed into the primitive's middleware chain via\n * `createLoopbackMiddleware(store)` (#226-5 / ADR-033); the\n * orchestrator no longer accepts a fingerprint store directly.\n * - `INTEGRATION_SINK` — per canonical entity; consumer binds in their feature\n * module.\n * - `ExecuteIntegrationUseCase` — registered by the feature module alongside\n * its source + sink bindings. Providing the orchestrator here would\n * force Nest to resolve INTEGRATION_CHANGE_SOURCE + INTEGRATION_SINK at module\n * compile time, which fails when the feature module hasn't been\n * imported yet. Consumers register `ExecuteIntegrationUseCase` in the same\n * `providers` array as their source + sink so resolution is local\n * to where all three are bound.\n *\n * Same shape as `EventsModule.forRoot` — the module wires the bus; you\n * bring your own handlers. Here: the module wires the substrate; you\n * bring your own source + sink.\n *\n * ## Usage\n *\n * ```ts\n * // AppModule — single source of truth for backend + multi-tenancy.\n * @Module({\n * imports: [IntegrationModule.forRoot({ backend: 'drizzle' })],\n * })\n * export class AppModule {}\n *\n * // Per-entity feature module — binds source + sink, gets the\n * // orchestrator for free.\n * @Module({\n * providers: [\n * { provide: INTEGRATION_CHANGE_SOURCE, useClass: SalesforceOpportunitySource },\n * { provide: INTEGRATION_SINK, useClass: OpportunityIntegrationSink },\n * ExecuteIntegrationUseCase,\n * ],\n * })\n * export class OpportunityIntegrationModule {\n * constructor(\n * private readonly execute: ExecuteIntegrationUseCase<CanonicalOpportunity>,\n * ) {}\n * }\n * ```\n *\n * `global: true` means feature modules do not need to re-import\n * `IntegrationModule` — the substrate tokens are available project-wide.\n */\nimport { Module, type DynamicModule, type Provider } from '@nestjs/common';\nimport {\n INTEGRATION_CURSOR_STORE,\n INTEGRATION_FIELD_DIFFER,\n INTEGRATION_MODULE_OPTIONS,\n INTEGRATION_MULTI_TENANT,\n INTEGRATION_RUN_RECORDER,\n} from './integration.tokens';\nimport { MemoryCursorStore } from './integration-cursor-store.memory-backend';\nimport { MemoryRunRecorder } from './integration-run-recorder.memory-backend';\nimport { PostgresCursorStore } from './integration-cursor-store.drizzle-backend';\nimport { DrizzleIntegrationRunRecorder } from './integration-run-recorder.drizzle-backend';\nimport { DeepEqualDiffer, type DeepEqualDifferOptions } from './deep-equal.differ';\n\nexport interface IntegrationModuleOptions {\n /**\n * Backend selection. `drizzle` wires the Postgres cursor store +\n * run-log recorder; `memory` wires in-memory doubles suitable for\n * tests + local dev.\n */\n backend: 'drizzle' | 'memory';\n\n /**\n * Multi-tenancy opt-in (SYNC-6).\n *\n * When `true`, every call to the orchestrator + both Drizzle backends\n * must supply a non-null `tenantId`; missing values throw\n * `MissingTenantIdError`. Defense-in-depth: the orchestrator rejects\n * at entry (no dangling `status=running` rows) AND the Drizzle\n * backends reject at their write boundary (belt-and-braces for any\n * path that bypasses the orchestrator). Both sites use the shared\n * `assertTenantId` helper so error messages match.\n *\n * Memory backends accept `tenantId` unconditionally — their state is\n * process-local; cross-tenant isolation there is not meaningful.\n *\n * Defaults to `false`.\n */\n multiTenant?: boolean;\n\n /**\n * Default-differ configuration (DIFFER-UNIGNORE, 0.17.1). Threaded into the\n * `DeepEqualDiffer` bound to `INTEGRATION_FIELD_DIFFER`. Omit for the\n * historical behaviour (the default ignore list, unchanged).\n *\n * Mirrors `DeepEqualDifferOptions`:\n * - `ignore` — extra field names to ignore (merged with the defaults).\n * - `unignore` — default-ignored field names to RE-include as domain data\n * (e.g. `['deletedAt']` for an entity whose `deletedAt` is a\n * vendor-observed retraction tombstone, not row metadata — swe-brain\n * ADR-0008 §1). Subtracted after the merge, so it wins.\n *\n * A feature module that binds its own `IFieldDiffer<T>` to\n * `INTEGRATION_FIELD_DIFFER` overrides this entirely (per-entity escape hatch\n * unchanged).\n */\n differ?: DeepEqualDifferOptions;\n}\n\n@Module({})\nexport class IntegrationModule {\n static forRoot(options: IntegrationModuleOptions): DynamicModule {\n const multiTenant = options.multiTenant ?? false;\n\n const sharedProviders: Provider[] = [\n { provide: INTEGRATION_MODULE_OPTIONS, useValue: options },\n { provide: INTEGRATION_MULTI_TENANT, useValue: multiTenant },\n // Default differ — consumers can override by binding a different\n // `IFieldDiffer<T>` to `INTEGRATION_FIELD_DIFFER` in their feature module.\n // DIFFER-UNIGNORE: `options.differ` (ignore/unignore) is threaded here so\n // a consumer can declare a default-ignored column (e.g. `deletedAt`) as\n // domain data for their entities without binding a bespoke differ.\n {\n provide: INTEGRATION_FIELD_DIFFER,\n useValue: new DeepEqualDiffer(options.differ ?? {}),\n },\n ];\n\n const backendProviders: Provider[] =\n options.backend === 'memory'\n ? [\n // Wired as singletons via `useValue` so tests can pull\n // them out via `moduleRef.get(MemoryCursorStore)` for\n // direct assertions. Matches JOB-4 / MemoryJobStore shape.\n { provide: MemoryCursorStore, useValue: new MemoryCursorStore() },\n {\n provide: INTEGRATION_CURSOR_STORE,\n useExisting: MemoryCursorStore,\n },\n { provide: MemoryRunRecorder, useValue: new MemoryRunRecorder() },\n {\n provide: INTEGRATION_RUN_RECORDER,\n useExisting: MemoryRunRecorder,\n },\n ]\n : [\n // Drizzle backends — injected with DRIZZLE (provided by the\n // consumer's DrizzleModule) + the INTEGRATION_MULTI_TENANT flag\n // we bound above.\n { provide: INTEGRATION_CURSOR_STORE, useClass: PostgresCursorStore },\n { provide: INTEGRATION_RUN_RECORDER, useClass: DrizzleIntegrationRunRecorder },\n ];\n\n return {\n module: IntegrationModule,\n global: true,\n providers: [...sharedProviders, ...backendProviders],\n exports: [\n INTEGRATION_MODULE_OPTIONS,\n INTEGRATION_MULTI_TENANT,\n INTEGRATION_FIELD_DIFFER,\n INTEGRATION_CURSOR_STORE,\n INTEGRATION_RUN_RECORDER,\n ],\n };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA+DA,SAAS,cAAiD;AA4DnD,IAAM,oBAAN,MAAwB;AAAA,EAC7B,OAAO,QAAQ,SAAkD;AAC/D,UAAM,cAAc,QAAQ,eAAe;AAE3C,UAAM,kBAA8B;AAAA,MAClC,EAAE,SAAS,4BAA4B,UAAU,QAAQ;AAAA,MACzD,EAAE,SAAS,0BAA0B,UAAU,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAM3D;AAAA,QACE,SAAS;AAAA,QACT,UAAU,IAAI,gBAAgB,QAAQ,UAAU,CAAC,CAAC;AAAA,MACpD;AAAA,IACF;AAEA,UAAM,mBACJ,QAAQ,YAAY,WAChB;AAAA;AAAA;AAAA;AAAA,MAIE,EAAE,SAAS,mBAAmB,UAAU,IAAI,kBAAkB,EAAE;AAAA,MAChE;AAAA,QACE,SAAS;AAAA,QACT,aAAa;AAAA,MACf;AAAA,MACA,EAAE,SAAS,mBAAmB,UAAU,IAAI,kBAAkB,EAAE;AAAA,MAChE;AAAA,QACE,SAAS;AAAA,QACT,aAAa;AAAA,MACf;AAAA,IACF,IACA;AAAA;AAAA;AAAA;AAAA,MAIE,EAAE,SAAS,0BAA0B,UAAU,oBAAoB;AAAA,MACnE,EAAE,SAAS,0BAA0B,UAAU,8BAA8B;AAAA,IAC/E;AAEN,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW,CAAC,GAAG,iBAAiB,GAAG,gBAAgB;AAAA,MACnD,SAAS;AAAA,QACP;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAxDa,oBAAN;AAAA,EADN,OAAO,CAAC,CAAC;AAAA,GACG;","names":[]}
@@ -21,11 +21,14 @@ var DEFAULT_IGNORE_FIELDS = /* @__PURE__ */ new Set([
21
21
  var DeepEqualDiffer = class {
22
22
  ignore;
23
23
  constructor(opts = {}) {
24
- if (opts.ignore && opts.ignore.length > 0) {
25
- this.ignore = /* @__PURE__ */ new Set([...DEFAULT_IGNORE_FIELDS, ...opts.ignore]);
26
- } else {
27
- this.ignore = DEFAULT_IGNORE_FIELDS;
24
+ const merged = new Set(DEFAULT_IGNORE_FIELDS);
25
+ if (opts.ignore) {
26
+ for (const field of opts.ignore) merged.add(field);
27
+ }
28
+ if (opts.unignore) {
29
+ for (const field of opts.unignore) merged.delete(field);
28
30
  }
31
+ this.ignore = merged;
29
32
  }
30
33
  diff(existing, incoming, providerChangedFields) {
31
34
  if (existing === null) {
@@ -104,4 +107,4 @@ function deepEqualObject(a, b) {
104
107
  export {
105
108
  DeepEqualDiffer
106
109
  };
107
- //# sourceMappingURL=chunk-36U5UGIO.js.map
110
+ //# sourceMappingURL=chunk-JEINYUJH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../runtime/subsystems/integration/deep-equal.differ.ts"],"sourcesContent":["/**\n * DeepEqualDiffer — default `IFieldDiffer<T>` for the integration subsystem (SYNC-5).\n *\n * Walks every field of `incoming` against `existing`, emitting a structured\n * per-field diff (`{ from, to }`) for every field whose value changed.\n * Returns `'noop'` when the record is unchanged.\n *\n * Design decisions (extracted from the upstream consumer + HS-9 findings):\n *\n * 1. **Ignore list** — row metadata that sinks/services stamp unconditionally\n * so upstream cannot reasonably disagree:\n * `id`, `createdAt`, `updatedAt`, `deletedAt`, `type`,\n * `lastModifiedAt`, `fields`, `providerMetadata`\n * (`fields` is the EAV bag — it's diffed by the sink's EAV dual-write\n * path, not at the canonical-record layer.) Consumers augment the list via\n * `options.ignore` and — when a default is domain data for their entity —\n * REMOVE a default via `options.unignore` (e.g. an entity whose\n * `deletedAt` is a vendor-observed retraction tombstone, not row metadata;\n * see `DeepEqualDifferOptions.unignore`).\n *\n * 2. **`providerChangedFields` hint (CDC)** — when present, restricts the\n * comparison to the hinted field set. The hint is advisory; fields in\n * the ignore list are still filtered out even when hinted. Provider\n * hints are field-NAME-level; they don't override the ignore rules.\n *\n * 3. **Date → ISO string** — `Date` instances are normalized to\n * `toISOString()` before comparison. Sinks return `Date` from the DB\n * driver; adapters typically deliver strings. Direct `===` would\n * always say \"changed.\"\n *\n * 4. **Decimal-string vs number** — Postgres `numeric` columns return as\n * strings through Drizzle; adapters deliver numbers. When one side is a\n * number and the other is a numeric string that parses to the same\n * number, they're equal. The normalizer does NOT coerce non-numeric\n * strings, and it preserves zero-vs-null distinction.\n *\n * 5. **null-existing path** — `diff(null, incoming)` produces a full\n * created-shape diff (`{from: null, to: <value>}` for every non-ignored\n * field). Orchestrator sees this and records `operation: 'created'`.\n */\nimport { Injectable } from '@nestjs/common';\nimport type {\n DiffResult,\n FieldDiff,\n IFieldDiffer,\n} from './integration-field-diff.protocol';\n\n/**\n * Default ignore list. Keep in integration with consumer canonical-record shapes —\n * adding a row-metadata field here means no integration will ever mark it changed.\n *\n * Includes the columns contributed by the `external_id_tracking` behavior\n * (`external_id`/`externalId`, `provider`, `provider_metadata`/`providerMetadata`).\n * These are integration-tracking metadata, not domain attributes: they ride on the\n * canonical record but must never register as a field change (the external id\n * is the record's identity, not a mutable value). Listed in both snake_case\n * and camelCase so the differ ignores them regardless of the consumer's\n * canonical projection casing.\n */\nconst DEFAULT_IGNORE_FIELDS: ReadonlySet<string> = new Set([\n 'id',\n 'createdAt',\n 'updatedAt',\n 'deletedAt',\n 'type',\n 'lastModifiedAt',\n 'fields',\n 'external_id',\n 'externalId',\n 'provider',\n 'provider_metadata',\n 'providerMetadata',\n]);\n\nexport interface DeepEqualDifferOptions {\n /**\n * Extra field names to ignore in addition to the defaults. Consumers can\n * pass `['integration_version']` etc. to augment the base list; values here are\n * merged (not replaced) with `DEFAULT_IGNORE_FIELDS`.\n */\n readonly ignore?: readonly string[];\n\n /**\n * Field names to REMOVE from the default ignore list — the inverse of\n * `ignore`. Use this to declare that a normally-metadata column is in fact\n * DOMAIN DATA for this entity and must register as a field change.\n *\n * The canonical case (swe-brain ADR-0008 §1, the gap this knob closes):\n * `deletedAt` is in `DEFAULT_IGNORE_FIELDS` because most sinks stamp it as\n * row metadata sinks own unconditionally. But an entity with\n * `softDelete: false` and a domain-owned `deleted_at` carries the\n * vendor-observed retraction tombstone ON the canonical record (a Slack\n * `message_deleted` → `deletedAt`). Without un-ignoring it, the tombstone\n * overlay diffs to `'noop'`, the upsert is skipped, and `deleted_at` never\n * lands. `unignore: ['deletedAt']` makes the differ treat it as domain data.\n *\n * Applied AFTER `ignore` is merged, so `unignore` wins on a field listed in\n * both. Subtracting a field not in the (merged) ignore set is a harmless\n * no-op. Does not touch `DEFAULT_IGNORE_FIELDS` for any other instance.\n */\n readonly unignore?: readonly string[];\n}\n\n@Injectable()\nexport class DeepEqualDiffer<T extends Record<string, unknown>>\n implements IFieldDiffer<T>\n{\n private readonly ignore: ReadonlySet<string>;\n\n constructor(opts: DeepEqualDifferOptions = {}) {\n const merged = new Set<string>(DEFAULT_IGNORE_FIELDS);\n if (opts.ignore) {\n for (const field of opts.ignore) merged.add(field);\n }\n // `unignore` is subtracted last so it wins over a field that also appears\n // in `ignore` or the defaults — \"this column is domain data here.\"\n if (opts.unignore) {\n for (const field of opts.unignore) merged.delete(field);\n }\n this.ignore = merged;\n }\n\n diff(\n existing: T | null,\n incoming: T,\n providerChangedFields?: string[],\n ): DiffResult {\n // Created-shape: every non-ignored field becomes `{from: null, to}`.\n if (existing === null) {\n const out: FieldDiff = {};\n for (const key of Object.keys(incoming)) {\n if (this.ignore.has(key)) continue;\n const value = (incoming as Record<string, unknown>)[key];\n // Skip fields that are themselves null/undefined — a created record\n // doesn't need to declare \"this field is null now\" for every\n // untouched column.\n if (value === null || value === undefined) continue;\n out[key] = { from: null, to: value };\n }\n return Object.keys(out).length === 0 ? 'noop' : out;\n }\n\n // Field set to compare. `providerChangedFields` narrows to a hint set;\n // ignored fields are filtered out regardless of hint.\n const candidates = new Set<string>();\n if (providerChangedFields && providerChangedFields.length > 0) {\n for (const key of providerChangedFields) {\n if (!this.ignore.has(key)) candidates.add(key);\n }\n } else {\n for (const key of Object.keys(incoming)) {\n if (!this.ignore.has(key)) candidates.add(key);\n }\n // Also include keys that exist on existing but not on incoming —\n // e.g. a field that was cleared. This would otherwise be missed when\n // incoming carries an undefined column we drop from the iteration.\n for (const key of Object.keys(existing)) {\n if (this.ignore.has(key)) continue;\n if (!(key in (incoming as Record<string, unknown>))) continue;\n candidates.add(key);\n }\n }\n\n const out: FieldDiff = {};\n for (const key of candidates) {\n const before = (existing as Record<string, unknown>)[key];\n const after = (incoming as Record<string, unknown>)[key];\n if (!isEqual(before, after)) {\n out[key] = { from: before ?? null, to: after ?? null };\n }\n }\n\n return Object.keys(out).length === 0 ? 'noop' : out;\n }\n}\n\n// ─── equality helpers ───────────────────────────────────────────────────────\n\n/**\n * Field-level equality with the canonical-integration normalizations:\n * - Date → toISOString (adapters deliver strings)\n * - numeric-string vs number → numeric equality when both parse\n * - deep equality for plain objects/arrays (single-level is enough for\n * canonical records; nested records travel as jsonb columns where the\n * sink already owns the comparison)\n */\nfunction isEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n\n const na = normalize(a);\n const nb = normalize(b);\n if (na === nb) return true;\n\n // After normalization: both may still be non-primitive objects.\n if (\n typeof na === 'object' &&\n typeof nb === 'object' &&\n na !== null &&\n nb !== null\n ) {\n return deepEqualObject(na as Record<string, unknown>, nb as Record<string, unknown>);\n }\n\n // Numeric string ↔ number: when one side is a number and the other is a\n // string that parses to the same finite number.\n const numericEqual = maybeNumericEqual(na, nb) || maybeNumericEqual(nb, na);\n return numericEqual;\n}\n\nfunction normalize(value: unknown): unknown {\n if (value instanceof Date) return value.toISOString();\n return value;\n}\n\nfunction maybeNumericEqual(a: unknown, b: unknown): boolean {\n // a is string-shape, b is number — parse a and compare. Only when the\n // string looks numeric AND the parse round-trips (no silent NaN pass-\n // through on non-numeric strings).\n if (typeof a !== 'string' || typeof b !== 'number') return false;\n if (a.trim() === '') return false;\n const parsed = Number(a);\n if (!Number.isFinite(parsed)) return false;\n return parsed === b;\n}\n\nfunction deepEqualObject(\n a: Record<string, unknown>,\n b: Record<string, unknown>,\n): boolean {\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n const aKeys = Object.keys(a);\n const bKeys = Object.keys(b);\n if (aKeys.length !== bKeys.length) return false;\n for (const key of aKeys) {\n if (!(key in b)) return false;\n if (!isEqual(a[key], b[key])) return false;\n }\n return true;\n}\n"],"mappings":"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","names":["out"]}
@@ -1,9 +1,9 @@
1
- import {
2
- assertTenantId
3
- } from "./chunk-6DWFJNIK.js";
4
1
  import {
5
2
  bridgeDelivery
6
3
  } from "./chunk-2TVVBC53.js";
4
+ import {
5
+ assertTenantId
6
+ } from "./chunk-6DWFJNIK.js";
7
7
  import {
8
8
  BRIDGE_MULTI_TENANT
9
9
  } from "./chunk-4LH67P4U.js";
@@ -119,4 +119,4 @@ DrizzleBridgeDeliveryRepo = __decorateClass([
119
119
  export {
120
120
  DrizzleBridgeDeliveryRepo
121
121
  };
122
- //# sourceMappingURL=chunk-K2I6XIK5.js.map
122
+ //# sourceMappingURL=chunk-KSTZIULO.js.map
@@ -1,11 +1,11 @@
1
+ import {
2
+ EVENTS_MODULE_OPTIONS
3
+ } from "./chunk-H5NH7KPE.js";
1
4
  import {
2
5
  clampEventLimit,
3
6
  decodeEventCursor,
4
7
  encodeEventCursor
5
8
  } from "./chunk-UQ5EHOH2.js";
6
- import {
7
- EVENTS_MODULE_OPTIONS
8
- } from "./chunk-H5NH7KPE.js";
9
9
  import {
10
10
  __decorateClass,
11
11
  __decorateParam
@@ -197,4 +197,4 @@ MemoryEventBus = __decorateClass([
197
197
  export {
198
198
  MemoryEventBus
199
199
  };
200
- //# sourceMappingURL=chunk-Z7PQCAVK.js.map
200
+ //# sourceMappingURL=chunk-LQ6PYFU6.js.map
@@ -9,22 +9,22 @@ import {
9
9
  } from "./chunk-EDKJU5BO.js";
10
10
  import {
11
11
  DrizzleBridgeDeliveryRepo
12
- } from "./chunk-K2I6XIK5.js";
12
+ } from "./chunk-KSTZIULO.js";
13
13
  import {
14
14
  MemoryBridgeDeliveryRepo
15
15
  } from "./chunk-4DOJBQTP.js";
16
16
  import {
17
17
  BridgeOutboxDrainHook
18
- } from "./chunk-DTXH24LR.js";
18
+ } from "./chunk-SFQRETXJ.js";
19
19
  import {
20
20
  BridgeDeliveryHandler
21
- } from "./chunk-NXNVTXKG.js";
21
+ } from "./chunk-SGSWVNNB.js";
22
22
  import {
23
23
  BridgeReservedPoolsNotPolledError
24
24
  } from "./chunk-NXXDZ6ZF.js";
25
25
  import {
26
26
  JOB_WORKER_MODULE_OPTIONS
27
- } from "./chunk-GJDEPTPY.js";
27
+ } from "./chunk-HPS554L4.js";
28
28
  import {
29
29
  BRIDGE_DELIVERY_REPO,
30
30
  BRIDGE_MODULE_OPTIONS,
@@ -119,4 +119,4 @@ BridgeModule = __decorateClass([
119
119
  export {
120
120
  BridgeModule
121
121
  };
122
- //# sourceMappingURL=chunk-L3VJ47BU.js.map
122
+ //# sourceMappingURL=chunk-PSDVGPQR.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  BRIDGE_DELIVERY_JOB_TYPE
3
- } from "./chunk-NXNVTXKG.js";
3
+ } from "./chunk-SGSWVNNB.js";
4
4
  import {
5
5
  bridgeDelivery
6
6
  } from "./chunk-2TVVBC53.js";
@@ -151,4 +151,4 @@ BridgeOutboxDrainHook = __decorateClass([
151
151
  export {
152
152
  BridgeOutboxDrainHook
153
153
  };
154
- //# sourceMappingURL=chunk-DTXH24LR.js.map
154
+ //# sourceMappingURL=chunk-SFQRETXJ.js.map
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  assertTenantId
3
3
  } from "./chunk-6DWFJNIK.js";
4
- import {
5
- JobHandler,
6
- JobHandlerBase
7
- } from "./chunk-CO6LUM72.js";
8
4
  import {
9
5
  JOB_ORCHESTRATOR
10
6
  } from "./chunk-ZPL74UQN.js";
7
+ import {
8
+ JobHandler,
9
+ JobHandlerBase
10
+ } from "./chunk-7P5ODGLA.js";
11
11
  import {
12
12
  BRIDGE_DELIVERY_REPO,
13
13
  BRIDGE_MULTI_TENANT,
@@ -118,4 +118,4 @@ export {
118
118
  BRIDGE_DELIVERY_JOB_TYPE,
119
119
  BridgeDeliveryHandler
120
120
  };
121
- //# sourceMappingURL=chunk-NXNVTXKG.js.map
121
+ //# sourceMappingURL=chunk-SGSWVNNB.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";
4
7
  import {
5
8
  DrizzleOAuthStateStore
6
9
  } from "./chunk-N5OTOWTP.js";
7
10
  import {
8
11
  MemoryOAuthStateStore
9
12
  } 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-7LKAMLV4.js.map
92
+ //# sourceMappingURL=chunk-T6SCOJF4.js.map
@@ -1,6 +1,3 @@
1
- import {
2
- assertTenantId
3
- } from "./chunk-MZ6GV4YF.js";
4
1
  import {
5
2
  INTEGRATION_CHANGE_SOURCE,
6
3
  INTEGRATION_CURSOR_STORE,
@@ -9,6 +6,9 @@ import {
9
6
  INTEGRATION_RUN_RECORDER,
10
7
  INTEGRATION_SINK
11
8
  } from "./chunk-S7C6TIIF.js";
9
+ import {
10
+ assertTenantId
11
+ } from "./chunk-MZ6GV4YF.js";
12
12
  import {
13
13
  __decorateClass,
14
14
  __decorateParam
@@ -219,4 +219,4 @@ ExecuteIntegrationUseCase = __decorateClass([
219
219
  export {
220
220
  ExecuteIntegrationUseCase
221
221
  };
222
- //# sourceMappingURL=chunk-OGIZXGPY.js.map
222
+ //# sourceMappingURL=chunk-TDEHU73T.js.map
@@ -1,12 +1,12 @@
1
+ import {
2
+ MissingTenantIdError
3
+ } from "./chunk-T4BIIU5E.js";
1
4
  import {
2
5
  clampLimit,
3
6
  decodeKeysetCursor,
4
7
  encodeKeysetCursor,
5
8
  toJobRunSummary
6
9
  } from "./chunk-L3LZWWSX.js";
7
- import {
8
- MissingTenantIdError
9
- } from "./chunk-T4BIIU5E.js";
10
10
  import {
11
11
  JOBS_MULTI_TENANT,
12
12
  JOB_ORCHESTRATOR
@@ -198,4 +198,4 @@ DrizzleJobRunService = __decorateClass([
198
198
  export {
199
199
  DrizzleJobRunService
200
200
  };
201
- //# sourceMappingURL=chunk-3VEVGL74.js.map
201
+ //# sourceMappingURL=chunk-VNBC3VXM.js.map
@@ -1,12 +1,12 @@
1
+ import {
2
+ INTEGRATION_MULTI_TENANT
3
+ } from "./chunk-S7C6TIIF.js";
1
4
  import {
2
5
  assertTenantId
3
6
  } from "./chunk-MZ6GV4YF.js";
4
7
  import {
5
8
  integrationSubscriptions
6
9
  } from "./chunk-HNWZFNKP.js";
7
- import {
8
- INTEGRATION_MULTI_TENANT
9
- } from "./chunk-S7C6TIIF.js";
10
10
  import {
11
11
  DRIZZLE
12
12
  } from "./chunk-U64T4YZE.js";
@@ -97,4 +97,4 @@ PostgresCursorStore = __decorateClass([
97
97
  export {
98
98
  PostgresCursorStore
99
99
  };
100
- //# sourceMappingURL=chunk-DCCZB4UC.js.map
100
+ //# sourceMappingURL=chunk-XWBK3XJK.js.map
@@ -1,11 +1,11 @@
1
- import {
2
- DuplicateSchemaError,
3
- OpenApiPeerDepMissingError
4
- } from "./chunk-YSLTTQLC.js";
5
1
  import {
6
2
  ERROR_RESPONSE_SCHEMA_NAME,
7
3
  errorResponseSchema
8
4
  } from "./chunk-SOVM2VEK.js";
5
+ import {
6
+ DuplicateSchemaError,
7
+ OpenApiPeerDepMissingError
8
+ } from "./chunk-YSLTTQLC.js";
9
9
 
10
10
  // runtime/shared/openapi/registry.ts
11
11
  var OpenApiRegistry = class {
@@ -85,4 +85,4 @@ var OpenApiRegistry = class {
85
85
  export {
86
86
  OpenApiRegistry
87
87
  };
88
- //# sourceMappingURL=chunk-4GLNY5V6.js.map
88
+ //# sourceMappingURL=chunk-Y7GDG744.js.map
@@ -1,6 +1,9 @@
1
1
  import {
2
2
  FieldDiffSchema
3
3
  } from "./chunk-SQDOBLBP.js";
4
+ import {
5
+ INTEGRATION_MULTI_TENANT
6
+ } from "./chunk-S7C6TIIF.js";
4
7
  import {
5
8
  assertTenantId
6
9
  } from "./chunk-MZ6GV4YF.js";
@@ -9,9 +12,6 @@ import {
9
12
  integrationRuns,
10
13
  integrationSubscriptions
11
14
  } from "./chunk-HNWZFNKP.js";
12
- import {
13
- INTEGRATION_MULTI_TENANT
14
- } from "./chunk-S7C6TIIF.js";
15
15
  import {
16
16
  DRIZZLE
17
17
  } from "./chunk-U64T4YZE.js";
@@ -127,4 +127,4 @@ DrizzleIntegrationRunRecorder = __decorateClass([
127
127
  export {
128
128
  DrizzleIntegrationRunRecorder
129
129
  };
130
- //# sourceMappingURL=chunk-SR7F3TJY.js.map
130
+ //# sourceMappingURL=chunk-YK5JEVLX.js.map