@pattern-stack/codegen 0.14.0 → 0.14.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 (30) hide show
  1. package/dist/runtime/subsystems/bridge/bridge-delivery.memory-backend.d.ts +1 -1
  2. package/dist/runtime/subsystems/bridge/bridge-delivery.memory-backend.js +2 -2
  3. package/dist/runtime/subsystems/bridge/bridge-delivery.memory-backend.js.map +1 -1
  4. package/dist/runtime/subsystems/bridge/bridge.module.js +172 -159
  5. package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
  6. package/dist/runtime/subsystems/bridge/index.js +154 -141
  7. package/dist/runtime/subsystems/bridge/index.js.map +1 -1
  8. package/dist/runtime/subsystems/index.js +161 -148
  9. package/dist/runtime/subsystems/index.js.map +1 -1
  10. package/dist/runtime/subsystems/jobs/index.js +128 -115
  11. package/dist/runtime/subsystems/jobs/index.js.map +1 -1
  12. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js +128 -6
  13. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js.map +1 -1
  14. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +17 -0
  15. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js.map +1 -1
  16. package/dist/runtime/subsystems/jobs/job-step-service.memory-backend.js +25 -2
  17. package/dist/runtime/subsystems/jobs/job-step-service.memory-backend.js.map +1 -1
  18. package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +26 -1
  19. package/dist/runtime/subsystems/jobs/job-worker.module.js +150 -137
  20. package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
  21. package/dist/runtime/subsystems/jobs/jobs-domain.module.js +133 -124
  22. package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
  23. package/dist/src/cli/index.js +804 -454
  24. package/dist/src/cli/index.js.map +1 -1
  25. package/package.json +1 -1
  26. package/runtime/subsystems/bridge/bridge-delivery.memory-backend.ts +8 -1
  27. package/runtime/subsystems/jobs/job-orchestrator.memory-backend.ts +8 -3
  28. package/runtime/subsystems/jobs/job-run-service.memory-backend.ts +4 -1
  29. package/runtime/subsystems/jobs/job-step-service.memory-backend.ts +7 -2
  30. package/runtime/subsystems/jobs/job-worker.module.ts +13 -1
@@ -22,7 +22,7 @@ declare class MemoryBridgeDeliveryRepo implements IJobBridge {
22
22
  * Unlike `insertDelivery`, this read does NOT call `assertTenantId`:
23
23
  * `tenantId === undefined` is the supported cross-tenant admin view.
24
24
  */
25
- getStatusHistogram(windowHours: number, tenantId?: string | null): Promise<StatusHistogram>;
25
+ getStatusHistogram(windowHours: number, tenantId?: string | null, nowMs?: number): Promise<StatusHistogram>;
26
26
  /** All deliveries for a given event (any status, declaration order). */
27
27
  getDeliveriesForEvent(eventId: string): BridgeDeliveryRecord[];
28
28
  /** All deliveries currently in the given status. */
@@ -80,11 +80,11 @@ var MemoryBridgeDeliveryRepo = class {
80
80
  * Unlike `insertDelivery`, this read does NOT call `assertTenantId`:
81
81
  * `tenantId === undefined` is the supported cross-tenant admin view.
82
82
  */
83
- async getStatusHistogram(windowHours, tenantId) {
83
+ async getStatusHistogram(windowHours, tenantId, nowMs = Date.now()) {
84
84
  if (!Number.isFinite(windowHours) || windowHours <= 0) {
85
85
  throw new RangeError("windowHours must be positive");
86
86
  }
87
- const cutoffMs = Date.now() - windowHours * 36e5;
87
+ const cutoffMs = nowMs - windowHours * 36e5;
88
88
  const histogram = {
89
89
  pending: 0,
90
90
  delivered: 0,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../runtime/subsystems/bridge/bridge-delivery.memory-backend.ts","../../../../runtime/subsystems/bridge/bridge-errors.ts"],"sourcesContent":["/**\n * MemoryBridgeDeliveryRepo — in-memory `IJobBridge` (BRIDGE-3, ADR-023 Phase 2).\n *\n * Behavioral twin of the Drizzle backend (BRIDGE-4) for use in\n * `just test-unit`. No Docker, no Postgres, fully synchronous. Backs a\n * `Map<\"${eventId}::${triggerId}\", BridgeDeliveryRecord>` so the UNIQUE\n * `(event_id, trigger_id)` constraint can be simulated cheaply.\n *\n * Precedent: `MemoryEventBus` (EVT-5), `MemoryJobOrchestrator`\n * (jobs subsystem). Same shape — a class implementing the protocol plus\n * test-only helpers (`getDeliveriesForEvent`, `getByStatus`, `clear`) that\n * BRIDGE-5's framework-handler tests and BRIDGE-7's facade tests will\n * exercise.\n *\n * The synthetic `UniqueConstraintError` carries a `constraint` field equal\n * to the Drizzle constraint name (`uq_bridge_delivery_event_trigger`, set\n * in BRIDGE-1's schema) so consumers — including BRIDGE-4's\n * `INSERT … ON CONFLICT (event_id, trigger_id) DO NOTHING` path and\n * BRIDGE-7's Case B dedup tests — can branch on the same discriminator\n * regardless of which backend is wired up. ADR-023 explicitly relies on\n * this constraint as the dedup mechanism in two places (replay; facade-\n * vs-drain Case B); a typed error makes both call sites checkable.\n */\nimport { randomUUID } from 'node:crypto';\n\nimport type {\n BridgeDeliveryInsert,\n IJobBridge,\n StatusHistogram,\n} from './bridge.protocol';\nimport type { BridgeDeliveryRecord } from './bridge-delivery.schema';\nimport { UniqueConstraintError } from './bridge-errors';\n\nconst BRIDGE_DELIVERY_UNIQUE_CONSTRAINT =\n 'uq_bridge_delivery_event_trigger' as const;\n\nfunction key(eventId: string, triggerId: string): string {\n return `${eventId}::${triggerId}`;\n}\n\nexport class MemoryBridgeDeliveryRepo implements IJobBridge {\n private readonly deliveries = new Map<string, BridgeDeliveryRecord>();\n\n async insertDelivery(row: BridgeDeliveryInsert): Promise<void> {\n const k = key(row.eventId, row.triggerId);\n if (this.deliveries.has(k)) {\n throw new UniqueConstraintError(\n BRIDGE_DELIVERY_UNIQUE_CONSTRAINT,\n row.eventId,\n row.triggerId,\n );\n }\n // Materialize a full BridgeDeliveryRecord — fill in DB defaults that\n // the insert payload allowed to be omitted.\n const record: BridgeDeliveryRecord = {\n id: row.id ?? randomUUID(),\n eventId: row.eventId,\n triggerId: row.triggerId,\n wrapperRunId: row.wrapperRunId ?? null,\n userRunId: row.userRunId ?? null,\n status: row.status ?? 'pending',\n skipReason: row.skipReason ?? null,\n error: (row.error as Record<string, unknown> | null | undefined) ?? null,\n tenantId: row.tenantId ?? null,\n attemptedAt:\n row.attemptedAt instanceof Date ? row.attemptedAt : new Date(),\n deliveredAt:\n row.deliveredAt instanceof Date ? row.deliveredAt : null,\n };\n this.deliveries.set(k, record);\n }\n\n async findDelivery(\n eventId: string,\n triggerId: string,\n ): Promise<BridgeDeliveryRecord | null> {\n return this.deliveries.get(key(eventId, triggerId)) ?? null;\n }\n\n async findDeliveryById(id: string): Promise<BridgeDeliveryRecord | null> {\n for (const record of this.deliveries.values()) {\n if (record.id === id) return record;\n }\n return null;\n }\n\n async markDelivered(id: string, userRunId: string): Promise<void> {\n const record = this.findById(id);\n record.status = 'delivered';\n record.userRunId = userRunId;\n record.deliveredAt = new Date();\n }\n\n async markSkipped(id: string, reason: string): Promise<void> {\n const record = this.findById(id);\n record.status = 'skipped';\n record.skipReason = reason;\n }\n\n async markFailed(\n id: string,\n error: Record<string, unknown>,\n ): Promise<void> {\n const record = this.findById(id);\n record.status = 'failed';\n record.error = error;\n }\n\n /**\n * Observability read — see `IJobBridge.getStatusHistogram` JSDoc for the\n * tenant-filter and windowHours contract.\n *\n * Unlike `insertDelivery`, this read does NOT call `assertTenantId`:\n * `tenantId === undefined` is the supported cross-tenant admin view.\n */\n async getStatusHistogram(\n windowHours: number,\n tenantId?: string | null,\n ): Promise<StatusHistogram> {\n if (!Number.isFinite(windowHours) || windowHours <= 0) {\n throw new RangeError('windowHours must be positive');\n }\n\n const cutoffMs = Date.now() - windowHours * 3_600_000;\n const histogram: StatusHistogram = {\n pending: 0,\n delivered: 0,\n skipped: 0,\n failed: 0,\n };\n\n for (const record of this.deliveries.values()) {\n if (record.attemptedAt.getTime() < cutoffMs) continue;\n if (tenantId === null && record.tenantId !== null) continue;\n if (typeof tenantId === 'string' && record.tenantId !== tenantId) {\n continue;\n }\n // tenantId === undefined → no tenant filter.\n histogram[record.status] += 1;\n }\n\n return histogram;\n }\n\n // ─── Test helpers ────────────────────────────────────────────────────────\n\n /** All deliveries for a given event (any status, declaration order). */\n getDeliveriesForEvent(eventId: string): BridgeDeliveryRecord[] {\n return [...this.deliveries.values()].filter((r) => r.eventId === eventId);\n }\n\n /** All deliveries currently in the given status. */\n getByStatus(\n status: BridgeDeliveryRecord['status'],\n ): BridgeDeliveryRecord[] {\n return [...this.deliveries.values()].filter((r) => r.status === status);\n }\n\n /** Reset the store. Tests use this in `beforeEach`. */\n clear(): void {\n this.deliveries.clear();\n }\n\n // ─── Internals ───────────────────────────────────────────────────────────\n\n private findById(id: string): BridgeDeliveryRecord {\n for (const record of this.deliveries.values()) {\n if (record.id === id) return record;\n }\n throw new Error(\n `MemoryBridgeDeliveryRepo: no delivery with id '${id}' (transition ` +\n `methods may not be called for unknown rows; the framework handler ` +\n `should always findDelivery first or operate on a row it just ` +\n `inserted).`,\n );\n }\n}\n","/**\n * Typed errors for the bridge subsystem (ADR-023 Phase 2, BRIDGE-2).\n *\n * All thrown by the three enforcement sites named in ADR-023 §Multi-tenancy:\n * - `EventFlowService.publishAndStart` entry (BRIDGE-7)\n * - `BridgeDeliveryHandler.handle` entry (BRIDGE-5)\n * - `DrizzleBridgeDeliveryRepo.insertDelivery` pre-write (BRIDGE-4)\n *\n * Same shape as `runtime/subsystems/jobs/jobs-errors.ts` and\n * `runtime/subsystems/events/events-errors.ts` so consumers can catch them\n * with the same exception-filter pattern across all three subsystems.\n */\n\n/**\n * Thrown when `BridgeModule` was configured with `multiTenant: true` but\n * the caller did not pass a `tenantId` at one of the three enforcement\n * sites listed above.\n *\n * **Strict enforcement rationale (mirrors JOB-8 / SYNC-6 stance, locked\n * 2026-04-18 for jobs; same rationale applies here).** Cross-tenant data\n * leakage is the worst class of bug a multi-tenant system can ship;\n * surfacing the misuse loudly at the call site (rather than silently\n * defaulting to `null` or to \"the last tenant seen\") prevents both\n * accidental global writes and sneaky reads that return a union of tenants.\n *\n * - `undefined` `tenantId` → throw this error.\n * - Explicit `null` `tenantId` → passes; opts the call into cross-tenant\n * work (e.g. a system housekeeping event with no owning tenant). The\n * `bridge_delivery` row is persisted with `tenant_id = NULL`.\n *\n * The `callSite` constructor argument names which of the three enforcement\n * sites threw — review reports and ops dashboards rely on a stable site\n * name, so use the canonical strings: `'EventFlowService.publishAndStart'`,\n * `'BridgeDeliveryHandler.handle'`,\n * `'DrizzleBridgeDeliveryRepo.insertDelivery'`.\n */\nexport class MissingTenantIdError extends Error {\n override readonly name = 'MissingTenantIdError';\n constructor(public readonly callSite: string) {\n super(\n `MissingTenantIdError: BridgeModule was configured with ` +\n `multiTenant=true but ${callSite} was called without tenantId ` +\n `(undefined). Pass an explicit tenantId, or pass null for ` +\n `cross-tenant work.`,\n );\n }\n}\n\n/**\n * Synthetic error thrown by `MemoryBridgeDeliveryRepo.insertDelivery` when\n * a duplicate `(event_id, trigger_id)` insert hits the simulated UNIQUE\n * constraint (BRIDGE-3).\n *\n * Carries a `constraint` field equal to the Drizzle constraint name\n * declared in BRIDGE-1's schema (`uq_bridge_delivery_event_trigger`) so\n * call sites can branch on the same discriminator regardless of which\n * backend is wired up. This matters because ADR-023 explicitly leans on\n * the constraint as the dedup mechanism in two places — outbox replay\n * and `publishAndStart` Case B — and BRIDGE-4 / BRIDGE-7 will share a\n * type-check path with BRIDGE-3-driven tests.\n *\n * The Drizzle backend (BRIDGE-4) does NOT throw this error: it uses\n * `INSERT … ON CONFLICT (event_id, trigger_id) DO NOTHING RETURNING id`\n * per the BRIDGE-4 spec recommendation, so collisions surface as an empty\n * result set rather than an exception. The error exists so the memory\n * backend can faithfully model the \"duplicate raises\" behaviour for tests\n * that want to assert the constraint actually fires.\n */\n/**\n * Thrown by `BridgeModule.onModuleInit` when `JobWorkerModule` is wired\n * alongside the bridge but its active `pools` list does not include one\n * or more of the three reserved bridge pools (`events_inbound`,\n * `events_change`, `events_outbound`).\n *\n * Without a worker polling those pools, the wrapper `job_run` rows the\n * outbox drain inserts (BRIDGE-4) sit `pending` forever — a silent\n * footgun where `eventFlow.publish(...)` returns success but no user\n * job ever spawns. The boot-time check converts that into a fail-fast.\n *\n * Operators can either (a) add `...BRIDGE_RESERVED_POOLS` to their\n * `JobWorkerModule.forRoot({ pools })` configuration so the same\n * process polls the reserved pools, or (b) run a separate worker\n * process per reserved pool for lane isolation (ADR-022 §Pool\n * isolation).\n */\nexport class BridgeReservedPoolsNotPolledError extends Error {\n override readonly name = 'BridgeReservedPoolsNotPolledError';\n constructor(public readonly missingPools: readonly string[]) {\n super(\n `BridgeModule loaded but JobWorkerModule is not polling reserved ` +\n `pool '${missingPools[0]}'. Add ...BRIDGE_RESERVED_POOLS to your ` +\n `JobWorkerModule.forRoot({ pools }) configuration. Missing pools: ` +\n `${missingPools.join(', ')}. (Bridge-fanout wrappers will sit ` +\n `pending forever without these pollers.)`,\n );\n }\n}\n\nexport class UniqueConstraintError extends Error {\n override readonly name = 'UniqueConstraintError';\n constructor(\n public readonly constraint: string,\n public readonly eventId: string,\n public readonly triggerId: string,\n ) {\n super(\n `UniqueConstraintError: duplicate insert into bridge_delivery for ` +\n `(event_id='${eventId}', trigger_id='${triggerId}') — violates ` +\n `constraint '${constraint}'.`,\n );\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../runtime/subsystems/bridge/bridge-delivery.memory-backend.ts","../../../../runtime/subsystems/bridge/bridge-errors.ts"],"sourcesContent":["/**\n * MemoryBridgeDeliveryRepo — in-memory `IJobBridge` (BRIDGE-3, ADR-023 Phase 2).\n *\n * Behavioral twin of the Drizzle backend (BRIDGE-4) for use in\n * `just test-unit`. No Docker, no Postgres, fully synchronous. Backs a\n * `Map<\"${eventId}::${triggerId}\", BridgeDeliveryRecord>` so the UNIQUE\n * `(event_id, trigger_id)` constraint can be simulated cheaply.\n *\n * Precedent: `MemoryEventBus` (EVT-5), `MemoryJobOrchestrator`\n * (jobs subsystem). Same shape — a class implementing the protocol plus\n * test-only helpers (`getDeliveriesForEvent`, `getByStatus`, `clear`) that\n * BRIDGE-5's framework-handler tests and BRIDGE-7's facade tests will\n * exercise.\n *\n * The synthetic `UniqueConstraintError` carries a `constraint` field equal\n * to the Drizzle constraint name (`uq_bridge_delivery_event_trigger`, set\n * in BRIDGE-1's schema) so consumers — including BRIDGE-4's\n * `INSERT … ON CONFLICT (event_id, trigger_id) DO NOTHING` path and\n * BRIDGE-7's Case B dedup tests — can branch on the same discriminator\n * regardless of which backend is wired up. ADR-023 explicitly relies on\n * this constraint as the dedup mechanism in two places (replay; facade-\n * vs-drain Case B); a typed error makes both call sites checkable.\n */\nimport { randomUUID } from 'node:crypto';\n\nimport type {\n BridgeDeliveryInsert,\n IJobBridge,\n StatusHistogram,\n} from './bridge.protocol';\nimport type { BridgeDeliveryRecord } from './bridge-delivery.schema';\nimport { UniqueConstraintError } from './bridge-errors';\n\nconst BRIDGE_DELIVERY_UNIQUE_CONSTRAINT =\n 'uq_bridge_delivery_event_trigger' as const;\n\nfunction key(eventId: string, triggerId: string): string {\n return `${eventId}::${triggerId}`;\n}\n\nexport class MemoryBridgeDeliveryRepo implements IJobBridge {\n private readonly deliveries = new Map<string, BridgeDeliveryRecord>();\n\n async insertDelivery(row: BridgeDeliveryInsert): Promise<void> {\n const k = key(row.eventId, row.triggerId);\n if (this.deliveries.has(k)) {\n throw new UniqueConstraintError(\n BRIDGE_DELIVERY_UNIQUE_CONSTRAINT,\n row.eventId,\n row.triggerId,\n );\n }\n // Materialize a full BridgeDeliveryRecord — fill in DB defaults that\n // the insert payload allowed to be omitted.\n const record: BridgeDeliveryRecord = {\n id: row.id ?? randomUUID(),\n eventId: row.eventId,\n triggerId: row.triggerId,\n wrapperRunId: row.wrapperRunId ?? null,\n userRunId: row.userRunId ?? null,\n status: row.status ?? 'pending',\n skipReason: row.skipReason ?? null,\n error: (row.error as Record<string, unknown> | null | undefined) ?? null,\n tenantId: row.tenantId ?? null,\n attemptedAt:\n row.attemptedAt instanceof Date ? row.attemptedAt : new Date(),\n deliveredAt:\n row.deliveredAt instanceof Date ? row.deliveredAt : null,\n };\n this.deliveries.set(k, record);\n }\n\n async findDelivery(\n eventId: string,\n triggerId: string,\n ): Promise<BridgeDeliveryRecord | null> {\n return this.deliveries.get(key(eventId, triggerId)) ?? null;\n }\n\n async findDeliveryById(id: string): Promise<BridgeDeliveryRecord | null> {\n for (const record of this.deliveries.values()) {\n if (record.id === id) return record;\n }\n return null;\n }\n\n async markDelivered(id: string, userRunId: string): Promise<void> {\n const record = this.findById(id);\n record.status = 'delivered';\n record.userRunId = userRunId;\n record.deliveredAt = new Date();\n }\n\n async markSkipped(id: string, reason: string): Promise<void> {\n const record = this.findById(id);\n record.status = 'skipped';\n record.skipReason = reason;\n }\n\n async markFailed(\n id: string,\n error: Record<string, unknown>,\n ): Promise<void> {\n const record = this.findById(id);\n record.status = 'failed';\n record.error = error;\n }\n\n /**\n * Observability read — see `IJobBridge.getStatusHistogram` JSDoc for the\n * tenant-filter and windowHours contract.\n *\n * Unlike `insertDelivery`, this read does NOT call `assertTenantId`:\n * `tenantId === undefined` is the supported cross-tenant admin view.\n */\n async getStatusHistogram(\n windowHours: number,\n tenantId?: string | null,\n // Reference instant for the window cutoff. Defaults to the wall clock; exposed\n // as a test-injection seam so a caller can pin the cutoff to the same `now` it\n // positioned a boundary row from. Without it the cutoff is re-sampled here a few\n // ms later, pushing an \"exactly at the boundary\" row just below the window\n // (the OBS-3 boundary test flake). The drizzle backend has no analogue — it\n // evaluates the cutoff once in SQL via `now()`.\n nowMs: number = Date.now(),\n ): Promise<StatusHistogram> {\n if (!Number.isFinite(windowHours) || windowHours <= 0) {\n throw new RangeError('windowHours must be positive');\n }\n\n const cutoffMs = nowMs - windowHours * 3_600_000;\n const histogram: StatusHistogram = {\n pending: 0,\n delivered: 0,\n skipped: 0,\n failed: 0,\n };\n\n for (const record of this.deliveries.values()) {\n if (record.attemptedAt.getTime() < cutoffMs) continue;\n if (tenantId === null && record.tenantId !== null) continue;\n if (typeof tenantId === 'string' && record.tenantId !== tenantId) {\n continue;\n }\n // tenantId === undefined → no tenant filter.\n histogram[record.status] += 1;\n }\n\n return histogram;\n }\n\n // ─── Test helpers ────────────────────────────────────────────────────────\n\n /** All deliveries for a given event (any status, declaration order). */\n getDeliveriesForEvent(eventId: string): BridgeDeliveryRecord[] {\n return [...this.deliveries.values()].filter((r) => r.eventId === eventId);\n }\n\n /** All deliveries currently in the given status. */\n getByStatus(\n status: BridgeDeliveryRecord['status'],\n ): BridgeDeliveryRecord[] {\n return [...this.deliveries.values()].filter((r) => r.status === status);\n }\n\n /** Reset the store. Tests use this in `beforeEach`. */\n clear(): void {\n this.deliveries.clear();\n }\n\n // ─── Internals ───────────────────────────────────────────────────────────\n\n private findById(id: string): BridgeDeliveryRecord {\n for (const record of this.deliveries.values()) {\n if (record.id === id) return record;\n }\n throw new Error(\n `MemoryBridgeDeliveryRepo: no delivery with id '${id}' (transition ` +\n `methods may not be called for unknown rows; the framework handler ` +\n `should always findDelivery first or operate on a row it just ` +\n `inserted).`,\n );\n }\n}\n","/**\n * Typed errors for the bridge subsystem (ADR-023 Phase 2, BRIDGE-2).\n *\n * All thrown by the three enforcement sites named in ADR-023 §Multi-tenancy:\n * - `EventFlowService.publishAndStart` entry (BRIDGE-7)\n * - `BridgeDeliveryHandler.handle` entry (BRIDGE-5)\n * - `DrizzleBridgeDeliveryRepo.insertDelivery` pre-write (BRIDGE-4)\n *\n * Same shape as `runtime/subsystems/jobs/jobs-errors.ts` and\n * `runtime/subsystems/events/events-errors.ts` so consumers can catch them\n * with the same exception-filter pattern across all three subsystems.\n */\n\n/**\n * Thrown when `BridgeModule` was configured with `multiTenant: true` but\n * the caller did not pass a `tenantId` at one of the three enforcement\n * sites listed above.\n *\n * **Strict enforcement rationale (mirrors JOB-8 / SYNC-6 stance, locked\n * 2026-04-18 for jobs; same rationale applies here).** Cross-tenant data\n * leakage is the worst class of bug a multi-tenant system can ship;\n * surfacing the misuse loudly at the call site (rather than silently\n * defaulting to `null` or to \"the last tenant seen\") prevents both\n * accidental global writes and sneaky reads that return a union of tenants.\n *\n * - `undefined` `tenantId` → throw this error.\n * - Explicit `null` `tenantId` → passes; opts the call into cross-tenant\n * work (e.g. a system housekeeping event with no owning tenant). The\n * `bridge_delivery` row is persisted with `tenant_id = NULL`.\n *\n * The `callSite` constructor argument names which of the three enforcement\n * sites threw — review reports and ops dashboards rely on a stable site\n * name, so use the canonical strings: `'EventFlowService.publishAndStart'`,\n * `'BridgeDeliveryHandler.handle'`,\n * `'DrizzleBridgeDeliveryRepo.insertDelivery'`.\n */\nexport class MissingTenantIdError extends Error {\n override readonly name = 'MissingTenantIdError';\n constructor(public readonly callSite: string) {\n super(\n `MissingTenantIdError: BridgeModule was configured with ` +\n `multiTenant=true but ${callSite} was called without tenantId ` +\n `(undefined). Pass an explicit tenantId, or pass null for ` +\n `cross-tenant work.`,\n );\n }\n}\n\n/**\n * Synthetic error thrown by `MemoryBridgeDeliveryRepo.insertDelivery` when\n * a duplicate `(event_id, trigger_id)` insert hits the simulated UNIQUE\n * constraint (BRIDGE-3).\n *\n * Carries a `constraint` field equal to the Drizzle constraint name\n * declared in BRIDGE-1's schema (`uq_bridge_delivery_event_trigger`) so\n * call sites can branch on the same discriminator regardless of which\n * backend is wired up. This matters because ADR-023 explicitly leans on\n * the constraint as the dedup mechanism in two places — outbox replay\n * and `publishAndStart` Case B — and BRIDGE-4 / BRIDGE-7 will share a\n * type-check path with BRIDGE-3-driven tests.\n *\n * The Drizzle backend (BRIDGE-4) does NOT throw this error: it uses\n * `INSERT … ON CONFLICT (event_id, trigger_id) DO NOTHING RETURNING id`\n * per the BRIDGE-4 spec recommendation, so collisions surface as an empty\n * result set rather than an exception. The error exists so the memory\n * backend can faithfully model the \"duplicate raises\" behaviour for tests\n * that want to assert the constraint actually fires.\n */\n/**\n * Thrown by `BridgeModule.onModuleInit` when `JobWorkerModule` is wired\n * alongside the bridge but its active `pools` list does not include one\n * or more of the three reserved bridge pools (`events_inbound`,\n * `events_change`, `events_outbound`).\n *\n * Without a worker polling those pools, the wrapper `job_run` rows the\n * outbox drain inserts (BRIDGE-4) sit `pending` forever — a silent\n * footgun where `eventFlow.publish(...)` returns success but no user\n * job ever spawns. The boot-time check converts that into a fail-fast.\n *\n * Operators can either (a) add `...BRIDGE_RESERVED_POOLS` to their\n * `JobWorkerModule.forRoot({ pools })` configuration so the same\n * process polls the reserved pools, or (b) run a separate worker\n * process per reserved pool for lane isolation (ADR-022 §Pool\n * isolation).\n */\nexport class BridgeReservedPoolsNotPolledError extends Error {\n override readonly name = 'BridgeReservedPoolsNotPolledError';\n constructor(public readonly missingPools: readonly string[]) {\n super(\n `BridgeModule loaded but JobWorkerModule is not polling reserved ` +\n `pool '${missingPools[0]}'. Add ...BRIDGE_RESERVED_POOLS to your ` +\n `JobWorkerModule.forRoot({ pools }) configuration. Missing pools: ` +\n `${missingPools.join(', ')}. (Bridge-fanout wrappers will sit ` +\n `pending forever without these pollers.)`,\n );\n }\n}\n\nexport class UniqueConstraintError extends Error {\n override readonly name = 'UniqueConstraintError';\n constructor(\n public readonly constraint: string,\n public readonly eventId: string,\n public readonly triggerId: string,\n ) {\n super(\n `UniqueConstraintError: duplicate insert into bridge_delivery for ` +\n `(event_id='${eventId}', trigger_id='${triggerId}') — violates ` +\n `constraint '${constraint}'.`,\n );\n }\n}\n"],"mappings":"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","names":[]}
@@ -12,19 +12,20 @@ var __decorateParam = (index4, decorator) => (target, key2) => decorator(target,
12
12
 
13
13
  // runtime/subsystems/bridge/bridge.module.ts
14
14
  import {
15
- Inject as Inject12,
15
+ Inject as Inject13,
16
16
  Module as Module3,
17
17
  Optional as Optional7
18
18
  } from "@nestjs/common";
19
19
 
20
20
  // runtime/subsystems/jobs/job-worker.module.ts
21
21
  import {
22
- Inject as Inject7,
22
+ Inject as Inject8,
23
23
  Injectable as Injectable8,
24
24
  Logger as Logger4,
25
25
  Module as Module2,
26
26
  Optional as Optional2
27
27
  } from "@nestjs/common";
28
+ import { ModuleRef as ModuleRef2 } from "@nestjs/core";
28
29
 
29
30
  // runtime/subsystems/token-key.ts
30
31
  var PKG = "@pattern-stack/codegen";
@@ -916,8 +917,129 @@ DrizzleJobStepService = __decorateClass([
916
917
  ], DrizzleJobStepService);
917
918
 
918
919
  // runtime/subsystems/jobs/job-orchestrator.memory-backend.ts
920
+ import { randomUUID as randomUUID3 } from "crypto";
921
+ import { Inject as Inject5, Injectable as Injectable5, Logger as Logger2, Optional } from "@nestjs/common";
922
+ import { ModuleRef } from "@nestjs/core";
923
+
924
+ // runtime/subsystems/jobs/memory-job-store.ts
925
+ var MemoryJobStore = class {
926
+ /** Runs keyed by `id` (single source of truth for status/scope/lineage). */
927
+ runs = /* @__PURE__ */ new Map();
928
+ /** Steps keyed by `job_run_id`; array order matches insertion order. */
929
+ steps = /* @__PURE__ */ new Map();
930
+ /** Job definitions keyed by `type` — memory mirror of the `job` table. */
931
+ jobs = /* @__PURE__ */ new Map();
932
+ /** Reset everything. Tests call this in `beforeEach`. */
933
+ clear() {
934
+ this.runs.clear();
935
+ this.steps.clear();
936
+ this.jobs.clear();
937
+ }
938
+ };
939
+
940
+ // runtime/subsystems/jobs/job-step-service.memory-backend.ts
919
941
  import { randomUUID as randomUUID2 } from "crypto";
920
- import { Inject as Inject4, Injectable as Injectable4, Logger as Logger2, Optional } from "@nestjs/common";
942
+ import { Inject as Inject4, Injectable as Injectable4 } from "@nestjs/common";
943
+ var MemoryJobStepService = class {
944
+ // ADR-037 (package-mode DI): explicit `@Inject(MemoryJobStore)` — the
945
+ // published bundle carries no `design:paramtypes`, so a by-type inject
946
+ // would resolve to `undefined` in package mode.
947
+ constructor(store) {
948
+ this.store = store;
949
+ }
950
+ store;
951
+ async findStep(runId, stepId) {
952
+ const rows = this.store.steps.get(runId);
953
+ if (!rows) return null;
954
+ const match = rows.find(
955
+ (r) => r.stepId === stepId && r.status === "completed"
956
+ );
957
+ return match ?? null;
958
+ }
959
+ async recordStep(input) {
960
+ const rows = this.getOrCreateRows(input.jobRunId);
961
+ const existingIdx = rows.findIndex((r) => r.stepId === input.stepId);
962
+ const normalisedInput = input.input ?? null;
963
+ const normalisedOutput = input.output ?? null;
964
+ if (existingIdx >= 0) {
965
+ const prev = rows[existingIdx];
966
+ const next = {
967
+ ...prev,
968
+ status: input.status,
969
+ input: normalisedInput ?? prev.input,
970
+ output: normalisedOutput ?? prev.output,
971
+ error: input.error ?? prev.error,
972
+ attempts: input.attempts ?? prev.attempts,
973
+ startedAt: input.startedAt ?? prev.startedAt,
974
+ finishedAt: input.finishedAt ?? prev.finishedAt
975
+ };
976
+ rows[existingIdx] = next;
977
+ return next;
978
+ }
979
+ const seq = input.seq ?? this.nextSeq(rows);
980
+ const row = {
981
+ id: randomUUID2(),
982
+ jobRunId: input.jobRunId,
983
+ stepId: input.stepId,
984
+ kind: input.kind,
985
+ seq,
986
+ status: input.status,
987
+ input: normalisedInput,
988
+ output: normalisedOutput,
989
+ error: input.error ?? null,
990
+ attempts: input.attempts ?? 0,
991
+ startedAt: input.startedAt ?? null,
992
+ finishedAt: input.finishedAt ?? null
993
+ };
994
+ rows.push(row);
995
+ return row;
996
+ }
997
+ /**
998
+ * Replay helper — wipe every step row for a run. Mirrors the `scratch`
999
+ * replay mode of the Drizzle backend (`DELETE FROM job_step WHERE job_run_id = …`).
1000
+ */
1001
+ clearStepsForRun(runId) {
1002
+ this.store.steps.delete(runId);
1003
+ }
1004
+ /**
1005
+ * Remove every non-`completed` row for the run. Memoized (`completed`)
1006
+ * rows are preserved — this is the `last_checkpoint` / `last_step`
1007
+ * semantics the Drizzle backend implements via
1008
+ * `DELETE … WHERE status != 'completed'`. Both replay modes route here
1009
+ * (Phase 1 collapses `last_step` onto this behaviour; see JOB-3 notes).
1010
+ */
1011
+ clearIncompleteSteps(runId) {
1012
+ const rows = this.store.steps.get(runId);
1013
+ if (!rows) return;
1014
+ const kept = rows.filter((r) => r.status === "completed");
1015
+ if (kept.length === 0) {
1016
+ this.store.steps.delete(runId);
1017
+ } else {
1018
+ this.store.steps.set(runId, kept);
1019
+ }
1020
+ }
1021
+ getOrCreateRows(runId) {
1022
+ let rows = this.store.steps.get(runId);
1023
+ if (!rows) {
1024
+ rows = [];
1025
+ this.store.steps.set(runId, rows);
1026
+ }
1027
+ return rows;
1028
+ }
1029
+ nextSeq(rows) {
1030
+ let max = 0;
1031
+ for (const r of rows) {
1032
+ if (r.seq > max) max = r.seq;
1033
+ }
1034
+ return max + 1;
1035
+ }
1036
+ };
1037
+ MemoryJobStepService = __decorateClass([
1038
+ Injectable4(),
1039
+ __decorateParam(0, Inject4(MemoryJobStore))
1040
+ ], MemoryJobStepService);
1041
+
1042
+ // runtime/subsystems/jobs/job-orchestrator.memory-backend.ts
921
1043
  var QUEUED_RUN_AT = /* @__PURE__ */ new Date(864e13);
922
1044
  var TERMINAL_STATUSES2 = [
923
1045
  "completed",
@@ -1088,7 +1210,7 @@ var MemoryJobOrchestrator = class {
1088
1210
  }
1089
1211
  }
1090
1212
  }
1091
- const newId = randomUUID2();
1213
+ const newId = randomUUID3();
1092
1214
  let rootRunId = newId;
1093
1215
  if (opts.parentRunId) {
1094
1216
  const parent = this.store.runs.get(opts.parentRunId);
@@ -1486,9 +1608,12 @@ var MemoryJobOrchestrator = class {
1486
1608
  }
1487
1609
  };
1488
1610
  MemoryJobOrchestrator = __decorateClass([
1489
- Injectable4(),
1490
- __decorateParam(2, Inject4(JOBS_MULTI_TENANT)),
1491
- __decorateParam(3, Optional())
1611
+ Injectable5(),
1612
+ __decorateParam(0, Inject5(MemoryJobStore)),
1613
+ __decorateParam(1, Inject5(MemoryJobStepService)),
1614
+ __decorateParam(2, Inject5(JOBS_MULTI_TENANT)),
1615
+ __decorateParam(3, Optional()),
1616
+ __decorateParam(3, Inject5(ModuleRef))
1492
1617
  ], MemoryJobOrchestrator);
1493
1618
  function classifyError(err, policy, currentAttempts) {
1494
1619
  if (!policy) return "fail";
@@ -1522,7 +1647,7 @@ function serialiseError(err, attempt, retryable) {
1522
1647
  }
1523
1648
 
1524
1649
  // runtime/subsystems/jobs/job-run-service.memory-backend.ts
1525
- import { Inject as Inject5, Injectable as Injectable5 } from "@nestjs/common";
1650
+ import { Inject as Inject6, Injectable as Injectable6 } from "@nestjs/common";
1526
1651
  var NON_TERMINAL_STATUSES2 = [
1527
1652
  "pending",
1528
1653
  "running",
@@ -1689,9 +1814,10 @@ var MemoryJobRunService = class {
1689
1814
  }
1690
1815
  };
1691
1816
  MemoryJobRunService = __decorateClass([
1692
- Injectable5(),
1693
- __decorateParam(1, Inject5(JOB_ORCHESTRATOR)),
1694
- __decorateParam(2, Inject5(JOBS_MULTI_TENANT))
1817
+ Injectable6(),
1818
+ __decorateParam(0, Inject6(MemoryJobStore)),
1819
+ __decorateParam(1, Inject6(JOB_ORCHESTRATOR)),
1820
+ __decorateParam(2, Inject6(JOBS_MULTI_TENANT))
1695
1821
  ], MemoryJobRunService);
1696
1822
  function compareBy(a, b, order) {
1697
1823
  switch (order) {
@@ -1707,120 +1833,6 @@ function compareBy(a, b, order) {
1707
1833
  }
1708
1834
  }
1709
1835
 
1710
- // runtime/subsystems/jobs/job-step-service.memory-backend.ts
1711
- import { randomUUID as randomUUID3 } from "crypto";
1712
- import { Injectable as Injectable6 } from "@nestjs/common";
1713
- var MemoryJobStepService = class {
1714
- constructor(store) {
1715
- this.store = store;
1716
- }
1717
- store;
1718
- async findStep(runId, stepId) {
1719
- const rows = this.store.steps.get(runId);
1720
- if (!rows) return null;
1721
- const match = rows.find(
1722
- (r) => r.stepId === stepId && r.status === "completed"
1723
- );
1724
- return match ?? null;
1725
- }
1726
- async recordStep(input) {
1727
- const rows = this.getOrCreateRows(input.jobRunId);
1728
- const existingIdx = rows.findIndex((r) => r.stepId === input.stepId);
1729
- const normalisedInput = input.input ?? null;
1730
- const normalisedOutput = input.output ?? null;
1731
- if (existingIdx >= 0) {
1732
- const prev = rows[existingIdx];
1733
- const next = {
1734
- ...prev,
1735
- status: input.status,
1736
- input: normalisedInput ?? prev.input,
1737
- output: normalisedOutput ?? prev.output,
1738
- error: input.error ?? prev.error,
1739
- attempts: input.attempts ?? prev.attempts,
1740
- startedAt: input.startedAt ?? prev.startedAt,
1741
- finishedAt: input.finishedAt ?? prev.finishedAt
1742
- };
1743
- rows[existingIdx] = next;
1744
- return next;
1745
- }
1746
- const seq = input.seq ?? this.nextSeq(rows);
1747
- const row = {
1748
- id: randomUUID3(),
1749
- jobRunId: input.jobRunId,
1750
- stepId: input.stepId,
1751
- kind: input.kind,
1752
- seq,
1753
- status: input.status,
1754
- input: normalisedInput,
1755
- output: normalisedOutput,
1756
- error: input.error ?? null,
1757
- attempts: input.attempts ?? 0,
1758
- startedAt: input.startedAt ?? null,
1759
- finishedAt: input.finishedAt ?? null
1760
- };
1761
- rows.push(row);
1762
- return row;
1763
- }
1764
- /**
1765
- * Replay helper — wipe every step row for a run. Mirrors the `scratch`
1766
- * replay mode of the Drizzle backend (`DELETE FROM job_step WHERE job_run_id = …`).
1767
- */
1768
- clearStepsForRun(runId) {
1769
- this.store.steps.delete(runId);
1770
- }
1771
- /**
1772
- * Remove every non-`completed` row for the run. Memoized (`completed`)
1773
- * rows are preserved — this is the `last_checkpoint` / `last_step`
1774
- * semantics the Drizzle backend implements via
1775
- * `DELETE … WHERE status != 'completed'`. Both replay modes route here
1776
- * (Phase 1 collapses `last_step` onto this behaviour; see JOB-3 notes).
1777
- */
1778
- clearIncompleteSteps(runId) {
1779
- const rows = this.store.steps.get(runId);
1780
- if (!rows) return;
1781
- const kept = rows.filter((r) => r.status === "completed");
1782
- if (kept.length === 0) {
1783
- this.store.steps.delete(runId);
1784
- } else {
1785
- this.store.steps.set(runId, kept);
1786
- }
1787
- }
1788
- getOrCreateRows(runId) {
1789
- let rows = this.store.steps.get(runId);
1790
- if (!rows) {
1791
- rows = [];
1792
- this.store.steps.set(runId, rows);
1793
- }
1794
- return rows;
1795
- }
1796
- nextSeq(rows) {
1797
- let max = 0;
1798
- for (const r of rows) {
1799
- if (r.seq > max) max = r.seq;
1800
- }
1801
- return max + 1;
1802
- }
1803
- };
1804
- MemoryJobStepService = __decorateClass([
1805
- Injectable6()
1806
- ], MemoryJobStepService);
1807
-
1808
- // runtime/subsystems/jobs/memory-job-store.ts
1809
- var MemoryJobStore = class {
1810
- /** Runs keyed by `id` (single source of truth for status/scope/lineage). */
1811
- runs = /* @__PURE__ */ new Map();
1812
- /** Steps keyed by `job_run_id`; array order matches insertion order. */
1813
- steps = /* @__PURE__ */ new Map();
1814
- /** Job definitions keyed by `type` — memory mirror of the `job` table. */
1815
- jobs = /* @__PURE__ */ new Map();
1816
- /** Reset everything. Tests call this in `beforeEach`. */
1817
- clear() {
1818
- this.runs.clear();
1819
- this.steps.clear();
1820
- this.jobs.clear();
1821
- }
1822
- };
1823
-
1824
1836
  // runtime/subsystems/jobs/pool-config.loader.ts
1825
1837
  import { existsSync, readFileSync } from "fs";
1826
1838
  import { resolve } from "path";
@@ -2036,7 +2048,7 @@ JobsDomainModule = __decorateClass([
2036
2048
  ], JobsDomainModule);
2037
2049
 
2038
2050
  // runtime/subsystems/jobs/job-worker.ts
2039
- import { Inject as Inject6, Injectable as Injectable7, Logger as Logger3 } from "@nestjs/common";
2051
+ import { Inject as Inject7, Injectable as Injectable7, Logger as Logger3 } from "@nestjs/common";
2040
2052
  import { and as and4, asc as asc2, desc as desc3, eq as eq4, inArray as inArray3, lt as lt2, lte, sql as sql4 } from "drizzle-orm";
2041
2053
  var JOB_WORKER_OPTIONS = Symbol.for(tokenKey("jobs", "worker-options"));
2042
2054
  var DEFAULT_POLL_INTERVAL_MS = 1e3;
@@ -2423,11 +2435,11 @@ var JobWorker = class {
2423
2435
  };
2424
2436
  JobWorker = __decorateClass([
2425
2437
  Injectable7(),
2426
- __decorateParam(0, Inject6(DRIZZLE)),
2427
- __decorateParam(1, Inject6(JOB_ORCHESTRATOR)),
2428
- __decorateParam(2, Inject6(JOB_RUN_SERVICE)),
2429
- __decorateParam(3, Inject6(JOB_STEP_SERVICE)),
2430
- __decorateParam(4, Inject6(JOB_WORKER_OPTIONS))
2438
+ __decorateParam(0, Inject7(DRIZZLE)),
2439
+ __decorateParam(1, Inject7(JOB_ORCHESTRATOR)),
2440
+ __decorateParam(2, Inject7(JOB_RUN_SERVICE)),
2441
+ __decorateParam(3, Inject7(JOB_STEP_SERVICE)),
2442
+ __decorateParam(4, Inject7(JOB_WORKER_OPTIONS))
2431
2443
  ], JobWorker);
2432
2444
 
2433
2445
  // runtime/subsystems/jobs/job-worker.module.ts
@@ -2625,16 +2637,17 @@ var JobWorkerOrchestrator = class {
2625
2637
  };
2626
2638
  JobWorkerOrchestrator = __decorateClass([
2627
2639
  Injectable8(),
2628
- __decorateParam(0, Inject7(JOB_ORCHESTRATOR)),
2629
- __decorateParam(1, Inject7(JOB_RUN_SERVICE)),
2630
- __decorateParam(2, Inject7(JOB_STEP_SERVICE)),
2631
- __decorateParam(3, Inject7(JOB_WORKER_MODULE_OPTIONS)),
2640
+ __decorateParam(0, Inject8(JOB_ORCHESTRATOR)),
2641
+ __decorateParam(1, Inject8(JOB_RUN_SERVICE)),
2642
+ __decorateParam(2, Inject8(JOB_STEP_SERVICE)),
2643
+ __decorateParam(3, Inject8(JOB_WORKER_MODULE_OPTIONS)),
2632
2644
  __decorateParam(4, Optional2()),
2633
- __decorateParam(4, Inject7(DRIZZLE)),
2645
+ __decorateParam(4, Inject8(DRIZZLE)),
2646
+ __decorateParam(5, Inject8(ModuleRef2)),
2634
2647
  __decorateParam(6, Optional2()),
2635
- __decorateParam(6, Inject7(BULLMQ_CONNECTION)),
2648
+ __decorateParam(6, Inject8(BULLMQ_CONNECTION)),
2636
2649
  __decorateParam(7, Optional2()),
2637
- __decorateParam(7, Inject7(BULLMQ_RESOLVED_CONFIG))
2650
+ __decorateParam(7, Inject8(BULLMQ_RESOLVED_CONFIG))
2638
2651
  ], JobWorkerOrchestrator);
2639
2652
  var JobWorkerModule = class {
2640
2653
  static forRoot(opts) {
@@ -2774,11 +2787,11 @@ var MemoryBridgeDeliveryRepo = class {
2774
2787
  * Unlike `insertDelivery`, this read does NOT call `assertTenantId`:
2775
2788
  * `tenantId === undefined` is the supported cross-tenant admin view.
2776
2789
  */
2777
- async getStatusHistogram(windowHours, tenantId) {
2790
+ async getStatusHistogram(windowHours, tenantId, nowMs = Date.now()) {
2778
2791
  if (!Number.isFinite(windowHours) || windowHours <= 0) {
2779
2792
  throw new RangeError("windowHours must be positive");
2780
2793
  }
2781
- const cutoffMs = Date.now() - windowHours * 36e5;
2794
+ const cutoffMs = nowMs - windowHours * 36e5;
2782
2795
  const histogram = {
2783
2796
  pending: 0,
2784
2797
  delivered: 0,
@@ -2820,7 +2833,7 @@ var MemoryBridgeDeliveryRepo = class {
2820
2833
  };
2821
2834
 
2822
2835
  // runtime/subsystems/bridge/bridge-delivery.drizzle-backend.ts
2823
- import { Inject as Inject8, Injectable as Injectable9, Optional as Optional3 } from "@nestjs/common";
2836
+ import { Inject as Inject9, Injectable as Injectable9, Optional as Optional3 } from "@nestjs/common";
2824
2837
  import { eq as eq5, and as and5, gte as gte2, isNull as isNull2, sql as sql7 } from "drizzle-orm";
2825
2838
 
2826
2839
  // runtime/subsystems/bridge/bridge-delivery.schema.ts
@@ -3080,17 +3093,17 @@ var DrizzleBridgeDeliveryRepo = class {
3080
3093
  };
3081
3094
  DrizzleBridgeDeliveryRepo = __decorateClass([
3082
3095
  Injectable9(),
3083
- __decorateParam(0, Inject8(DRIZZLE)),
3096
+ __decorateParam(0, Inject9(DRIZZLE)),
3084
3097
  __decorateParam(1, Optional3()),
3085
- __decorateParam(1, Inject8(BRIDGE_MULTI_TENANT))
3098
+ __decorateParam(1, Inject9(BRIDGE_MULTI_TENANT))
3086
3099
  ], DrizzleBridgeDeliveryRepo);
3087
3100
 
3088
3101
  // runtime/subsystems/bridge/bridge-outbox-drain-hook.ts
3089
- import { Inject as Inject10, Injectable as Injectable11, Logger as Logger6, Optional as Optional5 } from "@nestjs/common";
3102
+ import { Inject as Inject11, Injectable as Injectable11, Logger as Logger6, Optional as Optional5 } from "@nestjs/common";
3090
3103
  import { randomUUID as randomUUID5 } from "crypto";
3091
3104
 
3092
3105
  // runtime/subsystems/bridge/bridge-delivery-handler.ts
3093
- import { Inject as Inject9, Injectable as Injectable10, Logger as Logger5, Optional as Optional4 } from "@nestjs/common";
3106
+ import { Inject as Inject10, Injectable as Injectable10, Logger as Logger5, Optional as Optional4 } from "@nestjs/common";
3094
3107
 
3095
3108
  // runtime/subsystems/events/events.tokens.ts
3096
3109
  var EVENT_BUS = "EVENT_BUS";
@@ -3180,12 +3193,12 @@ BridgeDeliveryHandler = __decorateClass([
3180
3193
  retry: { attempts: 3, backoff: "exponential", baseMs: 250 },
3181
3194
  replayFrom: "last_step"
3182
3195
  }),
3183
- __decorateParam(0, Inject9(BRIDGE_DELIVERY_REPO)),
3184
- __decorateParam(1, Inject9(JOB_ORCHESTRATOR)),
3185
- __decorateParam(2, Inject9(EVENT_BUS)),
3186
- __decorateParam(3, Inject9(BRIDGE_REGISTRY)),
3196
+ __decorateParam(0, Inject10(BRIDGE_DELIVERY_REPO)),
3197
+ __decorateParam(1, Inject10(JOB_ORCHESTRATOR)),
3198
+ __decorateParam(2, Inject10(EVENT_BUS)),
3199
+ __decorateParam(3, Inject10(BRIDGE_REGISTRY)),
3187
3200
  __decorateParam(4, Optional4()),
3188
- __decorateParam(4, Inject9(BRIDGE_MULTI_TENANT))
3201
+ __decorateParam(4, Inject10(BRIDGE_MULTI_TENANT))
3189
3202
  ], BridgeDeliveryHandler);
3190
3203
 
3191
3204
  // runtime/subsystems/bridge/bridge-outbox-drain-hook.ts
@@ -3294,11 +3307,11 @@ var BridgeOutboxDrainHook = class {
3294
3307
  BridgeOutboxDrainHook = __decorateClass([
3295
3308
  Injectable11(),
3296
3309
  __decorateParam(0, Optional5()),
3297
- __decorateParam(0, Inject10(BRIDGE_REGISTRY))
3310
+ __decorateParam(0, Inject11(BRIDGE_REGISTRY))
3298
3311
  ], BridgeOutboxDrainHook);
3299
3312
 
3300
3313
  // runtime/subsystems/bridge/event-flow.service.ts
3301
- import { Inject as Inject11, Injectable as Injectable12, Optional as Optional6 } from "@nestjs/common";
3314
+ import { Inject as Inject12, Injectable as Injectable12, Optional as Optional6 } from "@nestjs/common";
3302
3315
  var EventFlowService = class {
3303
3316
  constructor(db, eventBus, orchestrator, bridgeRepo, registry = {}, multiTenant = false) {
3304
3317
  this.db = db;
@@ -3370,14 +3383,14 @@ var EventFlowService = class {
3370
3383
  };
3371
3384
  EventFlowService = __decorateClass([
3372
3385
  Injectable12(),
3373
- __decorateParam(0, Inject11(DRIZZLE)),
3374
- __decorateParam(1, Inject11(EVENT_BUS)),
3375
- __decorateParam(2, Inject11(JOB_ORCHESTRATOR)),
3376
- __decorateParam(3, Inject11(BRIDGE_DELIVERY_REPO)),
3386
+ __decorateParam(0, Inject12(DRIZZLE)),
3387
+ __decorateParam(1, Inject12(EVENT_BUS)),
3388
+ __decorateParam(2, Inject12(JOB_ORCHESTRATOR)),
3389
+ __decorateParam(3, Inject12(BRIDGE_DELIVERY_REPO)),
3377
3390
  __decorateParam(4, Optional6()),
3378
- __decorateParam(4, Inject11(BRIDGE_REGISTRY)),
3391
+ __decorateParam(4, Inject12(BRIDGE_REGISTRY)),
3379
3392
  __decorateParam(5, Optional6()),
3380
- __decorateParam(5, Inject11(BRIDGE_MULTI_TENANT))
3393
+ __decorateParam(5, Inject12(BRIDGE_MULTI_TENANT))
3381
3394
  ], EventFlowService);
3382
3395
 
3383
3396
  // runtime/subsystems/bridge/generated/registry.ts
@@ -3456,7 +3469,7 @@ var BridgeModule = class {
3456
3469
  BridgeModule = __decorateClass([
3457
3470
  Module3({}),
3458
3471
  __decorateParam(0, Optional7()),
3459
- __decorateParam(0, Inject12(JOB_WORKER_MODULE_OPTIONS))
3472
+ __decorateParam(0, Inject13(JOB_WORKER_MODULE_OPTIONS))
3460
3473
  ], BridgeModule);
3461
3474
  export {
3462
3475
  BridgeModule