@pattern-stack/codegen 0.13.1 → 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 (162) hide show
  1. package/dist/{job-orchestrator.protocol-CHOEqBDk.d.ts → job-orchestrator.protocol-CARhMLCO.d.ts} +1 -1
  2. package/dist/runtime/subsystems/analytics/analytics.module.js +6 -2
  3. package/dist/runtime/subsystems/analytics/analytics.module.js.map +1 -1
  4. package/dist/runtime/subsystems/analytics/analytics.tokens.d.ts +0 -11
  5. package/dist/runtime/subsystems/analytics/analytics.tokens.js +6 -2
  6. package/dist/runtime/subsystems/analytics/analytics.tokens.js.map +1 -1
  7. package/dist/runtime/subsystems/analytics/cube-backend.js +6 -2
  8. package/dist/runtime/subsystems/analytics/cube-backend.js.map +1 -1
  9. package/dist/runtime/subsystems/analytics/index.js +6 -2
  10. package/dist/runtime/subsystems/analytics/index.js.map +1 -1
  11. package/dist/runtime/subsystems/auth/auth.module.js +12 -6
  12. package/dist/runtime/subsystems/auth/auth.module.js.map +1 -1
  13. package/dist/runtime/subsystems/auth/auth.tokens.d.ts +0 -28
  14. package/dist/runtime/subsystems/auth/auth.tokens.js +12 -8
  15. package/dist/runtime/subsystems/auth/auth.tokens.js.map +1 -1
  16. package/dist/runtime/subsystems/auth/controllers/auth.controller.js +12 -5
  17. package/dist/runtime/subsystems/auth/controllers/auth.controller.js.map +1 -1
  18. package/dist/runtime/subsystems/auth/index.js +12 -8
  19. package/dist/runtime/subsystems/auth/index.js.map +1 -1
  20. package/dist/runtime/subsystems/auth/middleware/requester-context.js +12 -1
  21. package/dist/runtime/subsystems/auth/middleware/requester-context.js.map +1 -1
  22. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.d.ts +1 -1
  23. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js +10 -2
  24. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js.map +1 -1
  25. package/dist/runtime/subsystems/bridge/bridge-delivery.memory-backend.d.ts +1 -1
  26. package/dist/runtime/subsystems/bridge/bridge-delivery.memory-backend.js +2 -2
  27. package/dist/runtime/subsystems/bridge/bridge-delivery.memory-backend.js.map +1 -1
  28. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js +10 -2
  29. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js.map +1 -1
  30. package/dist/runtime/subsystems/bridge/bridge.module.d.ts +1 -1
  31. package/dist/runtime/subsystems/bridge/bridge.module.js +186 -168
  32. package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
  33. package/dist/runtime/subsystems/bridge/bridge.protocol.d.ts +1 -1
  34. package/dist/runtime/subsystems/bridge/event-flow.service.d.ts +1 -1
  35. package/dist/runtime/subsystems/bridge/event-flow.service.js +9 -1
  36. package/dist/runtime/subsystems/bridge/event-flow.service.js.map +1 -1
  37. package/dist/runtime/subsystems/bridge/index.d.ts +1 -1
  38. package/dist/runtime/subsystems/bridge/index.js +168 -150
  39. package/dist/runtime/subsystems/bridge/index.js.map +1 -1
  40. package/dist/runtime/subsystems/cache/cache.drizzle-backend.js +6 -1
  41. package/dist/runtime/subsystems/cache/cache.drizzle-backend.js.map +1 -1
  42. package/dist/runtime/subsystems/cache/cache.memory-backend.js +6 -1
  43. package/dist/runtime/subsystems/cache/cache.memory-backend.js.map +1 -1
  44. package/dist/runtime/subsystems/cache/cache.module.js +6 -2
  45. package/dist/runtime/subsystems/cache/cache.module.js.map +1 -1
  46. package/dist/runtime/subsystems/cache/cache.tokens.d.ts +0 -10
  47. package/dist/runtime/subsystems/cache/cache.tokens.js +6 -2
  48. package/dist/runtime/subsystems/cache/cache.tokens.js.map +1 -1
  49. package/dist/runtime/subsystems/cache/index.js +6 -2
  50. package/dist/runtime/subsystems/cache/index.js.map +1 -1
  51. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +5 -0
  52. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js.map +1 -1
  53. package/dist/runtime/subsystems/events/event-bus.memory-backend.js +5 -0
  54. package/dist/runtime/subsystems/events/event-bus.memory-backend.js.map +1 -1
  55. package/dist/runtime/subsystems/events/event-bus.redis-backend.js +5 -1
  56. package/dist/runtime/subsystems/events/event-bus.redis-backend.js.map +1 -1
  57. package/dist/runtime/subsystems/events/events.module.js +5 -1
  58. package/dist/runtime/subsystems/events/events.module.js.map +1 -1
  59. package/dist/runtime/subsystems/events/events.tokens.d.ts +5 -11
  60. package/dist/runtime/subsystems/events/events.tokens.js +5 -1
  61. package/dist/runtime/subsystems/events/events.tokens.js.map +1 -1
  62. package/dist/runtime/subsystems/events/generated/bus.js +5 -0
  63. package/dist/runtime/subsystems/events/generated/bus.js.map +1 -1
  64. package/dist/runtime/subsystems/events/generated/index.js +5 -0
  65. package/dist/runtime/subsystems/events/generated/index.js.map +1 -1
  66. package/dist/runtime/subsystems/events/index.js +5 -1
  67. package/dist/runtime/subsystems/events/index.js.map +1 -1
  68. package/dist/runtime/subsystems/index.d.ts +2 -2
  69. package/dist/runtime/subsystems/index.js +186 -168
  70. package/dist/runtime/subsystems/index.js.map +1 -1
  71. package/dist/runtime/subsystems/jobs/bullmq.config.d.ts +0 -9
  72. package/dist/runtime/subsystems/jobs/bullmq.config.js +6 -2
  73. package/dist/runtime/subsystems/jobs/bullmq.config.js.map +1 -1
  74. package/dist/runtime/subsystems/jobs/index.d.ts +1 -1
  75. package/dist/runtime/subsystems/jobs/index.js +141 -124
  76. package/dist/runtime/subsystems/jobs/index.js.map +1 -1
  77. package/dist/runtime/subsystems/jobs/job-handler.base.d.ts +1 -1
  78. package/dist/runtime/subsystems/jobs/job-handler.base.js +5 -1
  79. package/dist/runtime/subsystems/jobs/job-handler.base.js.map +1 -1
  80. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.d.ts +1 -1
  81. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +10 -3
  82. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -1
  83. package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.d.ts +1 -1
  84. package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.js +8 -1
  85. package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.js.map +1 -1
  86. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.d.ts +1 -1
  87. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js +137 -7
  88. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js.map +1 -1
  89. package/dist/runtime/subsystems/jobs/job-orchestrator.protocol.d.ts +1 -1
  90. package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.d.ts +1 -1
  91. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.d.ts +1 -1
  92. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +8 -2
  93. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js.map +1 -1
  94. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.d.ts +1 -1
  95. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +25 -2
  96. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js.map +1 -1
  97. package/dist/runtime/subsystems/jobs/job-run-service.protocol.d.ts +1 -1
  98. package/dist/runtime/subsystems/jobs/job-step-service.memory-backend.js +25 -2
  99. package/dist/runtime/subsystems/jobs/job-step-service.memory-backend.js.map +1 -1
  100. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.d.ts +1 -1
  101. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +5 -0
  102. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js.map +1 -1
  103. package/dist/runtime/subsystems/jobs/job-worker.d.ts +1 -1
  104. package/dist/runtime/subsystems/jobs/job-worker.js +10 -4
  105. package/dist/runtime/subsystems/jobs/job-worker.js.map +1 -1
  106. package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +31 -3
  107. package/dist/runtime/subsystems/jobs/job-worker.module.js +163 -145
  108. package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
  109. package/dist/runtime/subsystems/jobs/jobs-domain.module.js +144 -130
  110. package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
  111. package/dist/runtime/subsystems/jobs/jobs-domain.tokens.d.ts +0 -11
  112. package/dist/runtime/subsystems/jobs/jobs-domain.tokens.js +8 -4
  113. package/dist/runtime/subsystems/jobs/jobs-domain.tokens.js.map +1 -1
  114. package/dist/runtime/subsystems/jobs/jobs-errors.d.ts +1 -1
  115. package/dist/runtime/subsystems/observability/index.d.ts +1 -1
  116. package/dist/runtime/subsystems/observability/index.js +9 -1
  117. package/dist/runtime/subsystems/observability/index.js.map +1 -1
  118. package/dist/runtime/subsystems/observability/observability.module.js +9 -1
  119. package/dist/runtime/subsystems/observability/observability.module.js.map +1 -1
  120. package/dist/runtime/subsystems/observability/observability.protocol.d.ts +1 -1
  121. package/dist/runtime/subsystems/observability/observability.service.d.ts +1 -1
  122. package/dist/runtime/subsystems/observability/observability.service.js +9 -1
  123. package/dist/runtime/subsystems/observability/observability.service.js.map +1 -1
  124. package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.d.ts +1 -1
  125. package/dist/runtime/subsystems/observability/reporters/index.d.ts +1 -1
  126. package/dist/runtime/subsystems/storage/index.js +5 -1
  127. package/dist/runtime/subsystems/storage/index.js.map +1 -1
  128. package/dist/runtime/subsystems/storage/storage.module.js +5 -1
  129. package/dist/runtime/subsystems/storage/storage.module.js.map +1 -1
  130. package/dist/runtime/subsystems/storage/storage.tokens.d.ts +0 -8
  131. package/dist/runtime/subsystems/storage/storage.tokens.js +5 -1
  132. package/dist/runtime/subsystems/storage/storage.tokens.js.map +1 -1
  133. package/dist/runtime/subsystems/token-key.d.ts +7 -0
  134. package/dist/runtime/subsystems/token-key.js +8 -0
  135. package/dist/runtime/subsystems/token-key.js.map +1 -0
  136. package/dist/src/cli/index.js +1160 -694
  137. package/dist/src/cli/index.js.map +1 -1
  138. package/package.json +5 -1
  139. package/runtime/subsystems/analytics/analytics.tokens.ts +6 -2
  140. package/runtime/subsystems/auth/auth.tokens.ts +15 -8
  141. package/runtime/subsystems/bridge/bridge-delivery.memory-backend.ts +8 -1
  142. package/runtime/subsystems/cache/cache.tokens.ts +7 -2
  143. package/runtime/subsystems/events/events.tokens.ts +8 -1
  144. package/runtime/subsystems/index.ts +5 -1
  145. package/runtime/subsystems/jobs/bullmq.config.ts +5 -2
  146. package/runtime/subsystems/jobs/job-handler.base.ts +6 -1
  147. package/runtime/subsystems/jobs/job-orchestrator.memory-backend.ts +8 -3
  148. package/runtime/subsystems/jobs/job-run-service.memory-backend.ts +4 -1
  149. package/runtime/subsystems/jobs/job-step-service.memory-backend.ts +7 -2
  150. package/runtime/subsystems/jobs/job-worker.module.ts +18 -2
  151. package/runtime/subsystems/jobs/job-worker.ts +4 -1
  152. package/runtime/subsystems/jobs/jobs-domain.tokens.ts +10 -7
  153. package/runtime/subsystems/storage/storage.tokens.ts +6 -1
  154. package/runtime/subsystems/token-key.ts +7 -0
  155. package/src/config/runtime-mode.mjs +82 -0
  156. package/templates/entity/new/backend/modules/core/integration-source.ejs.t +3 -2
  157. package/templates/entity/new/clean-lite-ps/controller.ejs.t +1 -1
  158. package/templates/entity/new/clean-lite-ps/module.ejs.t +1 -1
  159. package/templates/entity/new/clean-lite-ps/prompt-extension.js +8 -2
  160. package/templates/entity/new/clean-lite-ps/repository.ejs.t +4 -4
  161. package/templates/entity/new/clean-lite-ps/service.ejs.t +4 -4
  162. package/templates/entity/new/prompt.js +49 -10
@@ -1,14 +1,5 @@
1
1
  import { PoolConfig } from './pool-config.loader.js';
2
2
 
3
- /**
4
- * BullMQ backend configuration surface (BULLMQ-1, ADR-022 extension slot).
5
- *
6
- * The core `IJobOrchestrator` contract is backend-agnostic; everything in
7
- * this file is BullMQ-specific and lives behind the
8
- * `jobs.extensions.bullmq.*` config namespace (CLAUDE.md core/extension
9
- * protocol). The Drizzle backend never reads any of it.
10
- */
11
-
12
3
  /**
13
4
  * #6 — Structural mirror of BullMQ's `ConnectionOptions`. Declared locally
14
5
  * so this config file (which ships into EVERY jobs install, drizzle or
@@ -1,3 +1,7 @@
1
+ // runtime/subsystems/token-key.ts
2
+ var PKG = "@pattern-stack/codegen";
3
+ var tokenKey = (area, name) => `${PKG}.${area}.${name}`;
4
+
1
5
  // runtime/subsystems/jobs/pool-config.loader.ts
2
6
  import { existsSync, readFileSync } from "fs";
3
7
  import { resolve } from "path";
@@ -111,8 +115,8 @@ function extractUserPools(raw) {
111
115
  }
112
116
 
113
117
  // runtime/subsystems/jobs/bullmq.config.ts
114
- var BULLMQ_CONNECTION = /* @__PURE__ */ Symbol("BULLMQ_CONNECTION");
115
- var BULLMQ_RESOLVED_CONFIG = /* @__PURE__ */ Symbol("BULLMQ_RESOLVED_CONFIG");
118
+ var BULLMQ_CONNECTION = Symbol.for(tokenKey("jobs", "bullmq-connection"));
119
+ var BULLMQ_RESOLVED_CONFIG = Symbol.for(tokenKey("jobs", "bullmq-resolved-config"));
116
120
  var DEFAULT_REDIS_URL = "redis://localhost:6379";
117
121
  var DEFAULT_BULL_BOARD_MOUNT = "/admin/queues";
118
122
  function resolveBullMqConfig(ext) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../runtime/subsystems/jobs/pool-config.loader.ts","../../../../runtime/subsystems/jobs/bullmq.config.ts"],"sourcesContent":["/**\n * Pool config loader for the job orchestration domain (ADR-022, JOB-5).\n *\n * Reads `codegen.config.yaml: jobs.pools` from `process.cwd()` (or an\n * explicit `configPath` for tests), merges user-defined pools onto the five\n * framework defaults, and returns the resolved `Map<string, PoolDefinition>`\n * consumed by `JobWorkerModule.onModuleInit` and `JobsDomainModule`'s\n * config-validator surface.\n *\n * Invariants:\n * - User cannot flip `reserved: true` on a framework pool — silently\n * preserved. The three `events_*` pools are reserved infrastructure\n * for the events outbox drain.\n * - User-defined pools cannot set `reserved: true` — `reserved` is\n * framework-only metadata.\n * - Missing `codegen.config.yaml` is not an error; loader returns the\n * framework defaults verbatim.\n *\n * Result is cached at module scope after first call so repeated reads (e.g.\n * a worker module + a one-off scaffold validator in the same process) hit\n * the same parse. Tests that pass `configPath` skip the cache and isolate.\n */\nimport { existsSync, readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { parse as parseYaml } from 'yaml';\n\nexport interface PoolDefinition {\n /** Routing identifier — reused as the per-pool worker queue name. */\n queue: string;\n /** Max parallel in-flight `processRun` calls for this pool's worker. */\n concurrency: number;\n /** `true` ⇒ user `@JobHandler` may not target it. Framework-only. */\n reserved: boolean;\n /** Free-text annotation surfaced in admin UIs / logs. */\n description?: string;\n}\n\nexport type PoolConfig = Map<string, PoolDefinition>;\n\n/**\n * Five framework defaults. Three reserved `events_*` pools drain the\n * `IEventBus` outbox (one per `DomainEvent.direction`); `interactive` and\n * `batch` are user-default pools (`batch` is the `@JobHandler` default\n * when no `pool` is specified).\n */\nexport const FRAMEWORK_POOLS: Readonly<Record<string, PoolDefinition>> = Object.freeze({\n events_inbound: Object.freeze({\n queue: 'jobs-events-inbound',\n concurrency: 20,\n reserved: true,\n description: 'Inbound events drain (events subsystem outbox).',\n }),\n events_change: Object.freeze({\n queue: 'jobs-events-change',\n concurrency: 30,\n reserved: true,\n description: 'Change events drain (events subsystem outbox).',\n }),\n events_outbound: Object.freeze({\n queue: 'jobs-events-outbound',\n concurrency: 10,\n reserved: true,\n description: 'Outbound events drain (events subsystem outbox).',\n }),\n interactive: Object.freeze({\n queue: 'jobs-interactive',\n concurrency: 20,\n reserved: false,\n description: 'User-facing latency-sensitive jobs.',\n }),\n batch: Object.freeze({\n queue: 'jobs-batch',\n concurrency: 5,\n reserved: false,\n description: 'Default pool for background jobs.',\n }),\n});\n\n/** Names of the framework reserved pools. Cheap inline lookup for the worker. */\nexport const RESERVED_POOL_NAMES: ReadonlySet<string> = new Set(\n Object.entries(FRAMEWORK_POOLS)\n .filter(([, def]) => def.reserved)\n .map(([name]) => name),\n);\n\n/**\n * Cache by absolute config path. The `cwd` default is normalised before\n * lookup so two callers passing the same path share the cache; explicit\n * test-only paths cache separately.\n */\nconst cache = new Map<string, PoolConfig>();\n\n/**\n * Reset the loader cache. Test-only — not exported from the package\n * `index.ts`. Useful for tests that mutate `process.cwd()` between cases.\n */\nexport function _resetPoolConfigCacheForTests(): void {\n cache.clear();\n}\n\n/**\n * Resolve the merged pool config.\n *\n * @param configPath optional absolute or cwd-relative path; defaults to\n * `${process.cwd()}/codegen.config.yaml`.\n */\nexport function loadPoolConfig(configPath?: string): PoolConfig {\n const resolved = resolve(configPath ?? `${process.cwd()}/codegen.config.yaml`);\n const cached = cache.get(resolved);\n if (cached) return cached;\n\n const merged = new Map<string, PoolDefinition>();\n // Seed with framework defaults first — they always take precedence on\n // `reserved` and provide defaults for `queue` / `concurrency` if user\n // overrides only some fields.\n for (const [name, def] of Object.entries(FRAMEWORK_POOLS)) {\n merged.set(name, { ...def });\n }\n\n if (!existsSync(resolved)) {\n cache.set(resolved, merged);\n return merged;\n }\n\n let raw: unknown;\n try {\n raw = parseYaml(readFileSync(resolved, 'utf8'));\n } catch (err) {\n throw new Error(\n `pool-config.loader: failed to parse YAML at ${resolved}: ${(err as Error).message}`,\n );\n }\n\n const userPools = extractUserPools(raw);\n for (const [name, userDef] of Object.entries(userPools)) {\n const existing = merged.get(name);\n if (existing) {\n // Framework pool — user may tweak concurrency + description but\n // cannot flip `reserved`. `queue` is frozen too (reserved framework\n // pools' queue identifiers are part of the cross-subsystem contract\n // with the events outbox drain).\n const next: PoolDefinition = {\n queue: existing.queue,\n concurrency:\n typeof userDef.concurrency === 'number'\n ? userDef.concurrency\n : existing.concurrency,\n reserved: existing.reserved,\n description: userDef.description ?? existing.description,\n };\n merged.set(name, next);\n continue;\n }\n // User-defined pool. Validate required fields; reject reserved.\n if (typeof userDef.queue !== 'string' || userDef.queue.length === 0) {\n throw new Error(\n `pool-config.loader: pool '${name}' must declare a non-empty 'queue'.`,\n );\n }\n if (typeof userDef.concurrency !== 'number' || userDef.concurrency <= 0) {\n throw new Error(\n `pool-config.loader: pool '${name}' must declare a positive 'concurrency'.`,\n );\n }\n if (userDef.reserved === true) {\n throw new Error(\n `pool-config.loader: user-defined pool '${name}' cannot set ` +\n `'reserved: true' — reserved is framework-only.`,\n );\n }\n merged.set(name, {\n queue: userDef.queue,\n concurrency: userDef.concurrency,\n reserved: false,\n description: userDef.description,\n });\n }\n\n cache.set(resolved, merged);\n return merged;\n}\n\n/**\n * Names of every non-reserved pool in the resolved config. The default\n * worker activation set when `JobWorkerModuleOptions.pools` is omitted —\n * the worker process never claims the reserved `events_*` pools by\n * default; those are bound by the events subsystem's outbox bridge.\n */\nexport function allNonReservedPoolNames(config: PoolConfig): string[] {\n const out: string[] = [];\n for (const [name, def] of config) {\n if (!def.reserved) out.push(name);\n }\n return out;\n}\n\n/**\n * Names of **every** pool in the resolved config, reserved `events_*` lanes\n * included. The activation set for a standalone worker booted with\n * `JobWorkerModule.forRoot({ allPools: true })` (BULLMQ-1 Phase 1) — the\n * single worker process drains both user pools and the bridge's reserved\n * pools so wrapper `job_run` rows are never stranded.\n */\nexport function allPoolNames(config: PoolConfig): string[] {\n return [...config.keys()];\n}\n\n// ─── internals ──────────────────────────────────────────────────────────────\n\ninterface UserPoolShape {\n queue?: string;\n concurrency?: number;\n reserved?: boolean;\n description?: string;\n}\n\nfunction extractUserPools(raw: unknown): Record<string, UserPoolShape> {\n if (!raw || typeof raw !== 'object') return {};\n const jobs = (raw as { jobs?: unknown }).jobs;\n if (!jobs || typeof jobs !== 'object') return {};\n const pools = (jobs as { pools?: unknown }).pools;\n if (!pools || typeof pools !== 'object') return {};\n const out: Record<string, UserPoolShape> = {};\n for (const [name, def] of Object.entries(pools as Record<string, unknown>)) {\n if (!def || typeof def !== 'object') continue;\n out[name] = def as UserPoolShape;\n }\n return out;\n}\n","/**\n * BullMQ backend configuration surface (BULLMQ-1, ADR-022 extension slot).\n *\n * The core `IJobOrchestrator` contract is backend-agnostic; everything in\n * this file is BullMQ-specific and lives behind the\n * `jobs.extensions.bullmq.*` config namespace (CLAUDE.md core/extension\n * protocol). The Drizzle backend never reads any of it.\n */\nimport { loadPoolConfig, type PoolConfig } from './pool-config.loader';\n\n/**\n * #6 — Structural mirror of BullMQ's `ConnectionOptions`. Declared locally\n * so this config file (which ships into EVERY jobs install, drizzle or\n * bullmq) does NOT need the `bullmq` peer dep resolved by the consumer's\n * tsc. The bullmq backend internally casts to the real `ConnectionOptions`\n * — that file is only vendored when `--backend bullmq` is selected\n * (see `backendFileFilter`).\n *\n * Accepts the `{ url }` shape this resolver emits, plus the host/port/\n * password/db form BullMQ also accepts, with an open index for any extra\n * ioredis options consumers may flow through.\n */\nexport type BullMqConnectionOptions = {\n url?: string;\n host?: string;\n port?: number;\n password?: string;\n db?: number;\n [key: string]: unknown;\n};\n\n/**\n * Typed shape of `codegen.config.yaml: jobs.extensions.bullmq`. Snake_case\n * because it mirrors the YAML the consumer authors.\n *\n * ```yaml\n * jobs:\n * backend: bullmq\n * extensions:\n * bullmq:\n * redis_url: redis://localhost:6379 # or env REDIS_URL\n * queue_prefix: myapp # optional namespace (ADR-022 OQ)\n * bull_board:\n * enabled: true\n * mount_path: /api/admin/queues\n * ```\n */\nexport interface BullMqExtensionsConfig {\n /**\n * Redis/Valkey connection URL. When omitted, the runtime resolves\n * `process.env.REDIS_URL`, then falls back to `redis://localhost:6379`.\n */\n redis_url?: string;\n /**\n * Optional queue-name prefix to avoid collisions when several codegen apps\n * share one Redis (ADR-022 §\"BullMQ queue naming collisions\"). Applied to\n * every pool queue alias.\n */\n queue_prefix?: string;\n /**\n * Bull Board dashboard — opt-in extension (not core). Mounting is the\n * consumer's responsibility (it needs the consumer's Express/Nest adapter +\n * admin auth); we only carry the config. See README + spec §Extensions.\n */\n bull_board?: {\n enabled: boolean;\n mount_path?: string;\n };\n}\n\n/**\n * The runtime form after `redis_url`/env resolution. This is what the\n * orchestrator + worker actually consume.\n */\nexport interface BullMqResolvedConfig {\n connection: BullMqConnectionOptions;\n queuePrefix?: string;\n bullBoard?: { enabled: boolean; mountPath: string };\n}\n\n/** DI token for the resolved BullMQ `ConnectionOptions` (ioredis-compatible). */\nexport const BULLMQ_CONNECTION = Symbol('BULLMQ_CONNECTION');\n\n/** DI token for the full resolved BullMQ config (prefix + bull board). */\nexport const BULLMQ_RESOLVED_CONFIG = Symbol('BULLMQ_RESOLVED_CONFIG');\n\nconst DEFAULT_REDIS_URL = 'redis://localhost:6379';\nconst DEFAULT_BULL_BOARD_MOUNT = '/admin/queues';\n\n/**\n * Resolve the BullMQ runtime config from the extension block.\n *\n * Precedence for the connection URL:\n * 1. explicit `extensions.bullmq.redis_url`\n * 2. `process.env.REDIS_URL`\n * 3. `redis://localhost:6379`\n *\n * Returns a `{ url }` connection shape — BullMQ/ioredis accept a URL string\n * via the `{ url }` ConnectionOptions form.\n */\nexport function resolveBullMqConfig(\n ext: BullMqExtensionsConfig | undefined,\n): BullMqResolvedConfig {\n const url =\n ext?.redis_url ?? process.env.REDIS_URL ?? DEFAULT_REDIS_URL;\n\n const resolved: BullMqResolvedConfig = {\n connection: { url },\n queuePrefix: ext?.queue_prefix,\n };\n if (ext?.bull_board?.enabled) {\n resolved.bullBoard = {\n enabled: true,\n mountPath: ext.bull_board.mount_path ?? DEFAULT_BULL_BOARD_MOUNT,\n };\n }\n return resolved;\n}\n\n/**\n * Resolve the BullMQ queue name for a *logical pool name*. The orchestrator\n * and worker MUST agree on this mapping or jobs are enqueued onto a queue\n * nobody consumes. Both derive it identically:\n *\n * 1. Look up the pool's `queue` alias (e.g. `jobs-batch`) in the resolved\n * pool config — the same alias `JobWorkerModule.onModuleInit` logs and\n * that the BullMQ `Worker` binds to.\n * 2. Fall back to the logical pool name when the pool is unknown (defensive;\n * still a stable, colon-free identifier).\n * 3. Apply the optional `queue_prefix` namespace for multi-app Redis\n * sharing — `:` is fine in the *queue name* (it is only forbidden in the\n * `jobId`, hence the sha1 there).\n *\n * `poolConfig` defaults to the cached `loadPoolConfig()` so callers that only\n * hold the logical pool name (the orchestrator) don't need to thread the map.\n */\nexport function resolvePoolQueueName(\n pool: string,\n config: BullMqResolvedConfig | null | undefined,\n poolConfig: PoolConfig = loadPoolConfig(),\n): string {\n const alias = poolConfig.get(pool)?.queue ?? pool;\n const prefix = config?.queuePrefix;\n return prefix ? `${prefix}:${alias}` : alias;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../runtime/subsystems/token-key.ts","../../../../runtime/subsystems/jobs/pool-config.loader.ts","../../../../runtime/subsystems/jobs/bullmq.config.ts"],"sourcesContent":["/** Canonical package namespace for cross-boundary DI token keys. MUST be a hardcoded\n * constant (NOT derived from package.json) so a vendored copy — which lives inside the\n * CONSUMER's package — produces the identical key and the two copies share the symbol. */\nexport const PKG = '@pattern-stack/codegen';\n// TODO(token-version): if/when a runtime contract version is adopted, inject it HERE only\n// (e.g. `${PKG}#${ABI}.${area}.${name}`) — this helper is the single chokepoint.\nexport const tokenKey = (area: string, name: string): string => `${PKG}.${area}.${name}`;\n","/**\n * Pool config loader for the job orchestration domain (ADR-022, JOB-5).\n *\n * Reads `codegen.config.yaml: jobs.pools` from `process.cwd()` (or an\n * explicit `configPath` for tests), merges user-defined pools onto the five\n * framework defaults, and returns the resolved `Map<string, PoolDefinition>`\n * consumed by `JobWorkerModule.onModuleInit` and `JobsDomainModule`'s\n * config-validator surface.\n *\n * Invariants:\n * - User cannot flip `reserved: true` on a framework pool — silently\n * preserved. The three `events_*` pools are reserved infrastructure\n * for the events outbox drain.\n * - User-defined pools cannot set `reserved: true` — `reserved` is\n * framework-only metadata.\n * - Missing `codegen.config.yaml` is not an error; loader returns the\n * framework defaults verbatim.\n *\n * Result is cached at module scope after first call so repeated reads (e.g.\n * a worker module + a one-off scaffold validator in the same process) hit\n * the same parse. Tests that pass `configPath` skip the cache and isolate.\n */\nimport { existsSync, readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { parse as parseYaml } from 'yaml';\n\nexport interface PoolDefinition {\n /** Routing identifier — reused as the per-pool worker queue name. */\n queue: string;\n /** Max parallel in-flight `processRun` calls for this pool's worker. */\n concurrency: number;\n /** `true` ⇒ user `@JobHandler` may not target it. Framework-only. */\n reserved: boolean;\n /** Free-text annotation surfaced in admin UIs / logs. */\n description?: string;\n}\n\nexport type PoolConfig = Map<string, PoolDefinition>;\n\n/**\n * Five framework defaults. Three reserved `events_*` pools drain the\n * `IEventBus` outbox (one per `DomainEvent.direction`); `interactive` and\n * `batch` are user-default pools (`batch` is the `@JobHandler` default\n * when no `pool` is specified).\n */\nexport const FRAMEWORK_POOLS: Readonly<Record<string, PoolDefinition>> = Object.freeze({\n events_inbound: Object.freeze({\n queue: 'jobs-events-inbound',\n concurrency: 20,\n reserved: true,\n description: 'Inbound events drain (events subsystem outbox).',\n }),\n events_change: Object.freeze({\n queue: 'jobs-events-change',\n concurrency: 30,\n reserved: true,\n description: 'Change events drain (events subsystem outbox).',\n }),\n events_outbound: Object.freeze({\n queue: 'jobs-events-outbound',\n concurrency: 10,\n reserved: true,\n description: 'Outbound events drain (events subsystem outbox).',\n }),\n interactive: Object.freeze({\n queue: 'jobs-interactive',\n concurrency: 20,\n reserved: false,\n description: 'User-facing latency-sensitive jobs.',\n }),\n batch: Object.freeze({\n queue: 'jobs-batch',\n concurrency: 5,\n reserved: false,\n description: 'Default pool for background jobs.',\n }),\n});\n\n/** Names of the framework reserved pools. Cheap inline lookup for the worker. */\nexport const RESERVED_POOL_NAMES: ReadonlySet<string> = new Set(\n Object.entries(FRAMEWORK_POOLS)\n .filter(([, def]) => def.reserved)\n .map(([name]) => name),\n);\n\n/**\n * Cache by absolute config path. The `cwd` default is normalised before\n * lookup so two callers passing the same path share the cache; explicit\n * test-only paths cache separately.\n */\nconst cache = new Map<string, PoolConfig>();\n\n/**\n * Reset the loader cache. Test-only — not exported from the package\n * `index.ts`. Useful for tests that mutate `process.cwd()` between cases.\n */\nexport function _resetPoolConfigCacheForTests(): void {\n cache.clear();\n}\n\n/**\n * Resolve the merged pool config.\n *\n * @param configPath optional absolute or cwd-relative path; defaults to\n * `${process.cwd()}/codegen.config.yaml`.\n */\nexport function loadPoolConfig(configPath?: string): PoolConfig {\n const resolved = resolve(configPath ?? `${process.cwd()}/codegen.config.yaml`);\n const cached = cache.get(resolved);\n if (cached) return cached;\n\n const merged = new Map<string, PoolDefinition>();\n // Seed with framework defaults first — they always take precedence on\n // `reserved` and provide defaults for `queue` / `concurrency` if user\n // overrides only some fields.\n for (const [name, def] of Object.entries(FRAMEWORK_POOLS)) {\n merged.set(name, { ...def });\n }\n\n if (!existsSync(resolved)) {\n cache.set(resolved, merged);\n return merged;\n }\n\n let raw: unknown;\n try {\n raw = parseYaml(readFileSync(resolved, 'utf8'));\n } catch (err) {\n throw new Error(\n `pool-config.loader: failed to parse YAML at ${resolved}: ${(err as Error).message}`,\n );\n }\n\n const userPools = extractUserPools(raw);\n for (const [name, userDef] of Object.entries(userPools)) {\n const existing = merged.get(name);\n if (existing) {\n // Framework pool — user may tweak concurrency + description but\n // cannot flip `reserved`. `queue` is frozen too (reserved framework\n // pools' queue identifiers are part of the cross-subsystem contract\n // with the events outbox drain).\n const next: PoolDefinition = {\n queue: existing.queue,\n concurrency:\n typeof userDef.concurrency === 'number'\n ? userDef.concurrency\n : existing.concurrency,\n reserved: existing.reserved,\n description: userDef.description ?? existing.description,\n };\n merged.set(name, next);\n continue;\n }\n // User-defined pool. Validate required fields; reject reserved.\n if (typeof userDef.queue !== 'string' || userDef.queue.length === 0) {\n throw new Error(\n `pool-config.loader: pool '${name}' must declare a non-empty 'queue'.`,\n );\n }\n if (typeof userDef.concurrency !== 'number' || userDef.concurrency <= 0) {\n throw new Error(\n `pool-config.loader: pool '${name}' must declare a positive 'concurrency'.`,\n );\n }\n if (userDef.reserved === true) {\n throw new Error(\n `pool-config.loader: user-defined pool '${name}' cannot set ` +\n `'reserved: true' — reserved is framework-only.`,\n );\n }\n merged.set(name, {\n queue: userDef.queue,\n concurrency: userDef.concurrency,\n reserved: false,\n description: userDef.description,\n });\n }\n\n cache.set(resolved, merged);\n return merged;\n}\n\n/**\n * Names of every non-reserved pool in the resolved config. The default\n * worker activation set when `JobWorkerModuleOptions.pools` is omitted —\n * the worker process never claims the reserved `events_*` pools by\n * default; those are bound by the events subsystem's outbox bridge.\n */\nexport function allNonReservedPoolNames(config: PoolConfig): string[] {\n const out: string[] = [];\n for (const [name, def] of config) {\n if (!def.reserved) out.push(name);\n }\n return out;\n}\n\n/**\n * Names of **every** pool in the resolved config, reserved `events_*` lanes\n * included. The activation set for a standalone worker booted with\n * `JobWorkerModule.forRoot({ allPools: true })` (BULLMQ-1 Phase 1) — the\n * single worker process drains both user pools and the bridge's reserved\n * pools so wrapper `job_run` rows are never stranded.\n */\nexport function allPoolNames(config: PoolConfig): string[] {\n return [...config.keys()];\n}\n\n// ─── internals ──────────────────────────────────────────────────────────────\n\ninterface UserPoolShape {\n queue?: string;\n concurrency?: number;\n reserved?: boolean;\n description?: string;\n}\n\nfunction extractUserPools(raw: unknown): Record<string, UserPoolShape> {\n if (!raw || typeof raw !== 'object') return {};\n const jobs = (raw as { jobs?: unknown }).jobs;\n if (!jobs || typeof jobs !== 'object') return {};\n const pools = (jobs as { pools?: unknown }).pools;\n if (!pools || typeof pools !== 'object') return {};\n const out: Record<string, UserPoolShape> = {};\n for (const [name, def] of Object.entries(pools as Record<string, unknown>)) {\n if (!def || typeof def !== 'object') continue;\n out[name] = def as UserPoolShape;\n }\n return out;\n}\n","/**\n * BullMQ backend configuration surface (BULLMQ-1, ADR-022 extension slot).\n *\n * The core `IJobOrchestrator` contract is backend-agnostic; everything in\n * this file is BullMQ-specific and lives behind the\n * `jobs.extensions.bullmq.*` config namespace (CLAUDE.md core/extension\n * protocol). The Drizzle backend never reads any of it.\n */\nimport { tokenKey } from '../token-key';\nimport { loadPoolConfig, type PoolConfig } from './pool-config.loader';\n\n/**\n * #6 — Structural mirror of BullMQ's `ConnectionOptions`. Declared locally\n * so this config file (which ships into EVERY jobs install, drizzle or\n * bullmq) does NOT need the `bullmq` peer dep resolved by the consumer's\n * tsc. The bullmq backend internally casts to the real `ConnectionOptions`\n * — that file is only vendored when `--backend bullmq` is selected\n * (see `backendFileFilter`).\n *\n * Accepts the `{ url }` shape this resolver emits, plus the host/port/\n * password/db form BullMQ also accepts, with an open index for any extra\n * ioredis options consumers may flow through.\n */\nexport type BullMqConnectionOptions = {\n url?: string;\n host?: string;\n port?: number;\n password?: string;\n db?: number;\n [key: string]: unknown;\n};\n\n/**\n * Typed shape of `codegen.config.yaml: jobs.extensions.bullmq`. Snake_case\n * because it mirrors the YAML the consumer authors.\n *\n * ```yaml\n * jobs:\n * backend: bullmq\n * extensions:\n * bullmq:\n * redis_url: redis://localhost:6379 # or env REDIS_URL\n * queue_prefix: myapp # optional namespace (ADR-022 OQ)\n * bull_board:\n * enabled: true\n * mount_path: /api/admin/queues\n * ```\n */\nexport interface BullMqExtensionsConfig {\n /**\n * Redis/Valkey connection URL. When omitted, the runtime resolves\n * `process.env.REDIS_URL`, then falls back to `redis://localhost:6379`.\n */\n redis_url?: string;\n /**\n * Optional queue-name prefix to avoid collisions when several codegen apps\n * share one Redis (ADR-022 §\"BullMQ queue naming collisions\"). Applied to\n * every pool queue alias.\n */\n queue_prefix?: string;\n /**\n * Bull Board dashboard — opt-in extension (not core). Mounting is the\n * consumer's responsibility (it needs the consumer's Express/Nest adapter +\n * admin auth); we only carry the config. See README + spec §Extensions.\n */\n bull_board?: {\n enabled: boolean;\n mount_path?: string;\n };\n}\n\n/**\n * The runtime form after `redis_url`/env resolution. This is what the\n * orchestrator + worker actually consume.\n */\nexport interface BullMqResolvedConfig {\n connection: BullMqConnectionOptions;\n queuePrefix?: string;\n bullBoard?: { enabled: boolean; mountPath: string };\n}\n\n// ADR-037: namespaced `Symbol.for(...)` (via `tokenKey()`) — matches by value\n// across runtime copies.\n/** DI token for the resolved BullMQ `ConnectionOptions` (ioredis-compatible). */\nexport const BULLMQ_CONNECTION = Symbol.for(tokenKey('jobs', 'bullmq-connection'));\n\n/** DI token for the full resolved BullMQ config (prefix + bull board). */\nexport const BULLMQ_RESOLVED_CONFIG = Symbol.for(tokenKey('jobs', 'bullmq-resolved-config'));\n\nconst DEFAULT_REDIS_URL = 'redis://localhost:6379';\nconst DEFAULT_BULL_BOARD_MOUNT = '/admin/queues';\n\n/**\n * Resolve the BullMQ runtime config from the extension block.\n *\n * Precedence for the connection URL:\n * 1. explicit `extensions.bullmq.redis_url`\n * 2. `process.env.REDIS_URL`\n * 3. `redis://localhost:6379`\n *\n * Returns a `{ url }` connection shape — BullMQ/ioredis accept a URL string\n * via the `{ url }` ConnectionOptions form.\n */\nexport function resolveBullMqConfig(\n ext: BullMqExtensionsConfig | undefined,\n): BullMqResolvedConfig {\n const url =\n ext?.redis_url ?? process.env.REDIS_URL ?? DEFAULT_REDIS_URL;\n\n const resolved: BullMqResolvedConfig = {\n connection: { url },\n queuePrefix: ext?.queue_prefix,\n };\n if (ext?.bull_board?.enabled) {\n resolved.bullBoard = {\n enabled: true,\n mountPath: ext.bull_board.mount_path ?? DEFAULT_BULL_BOARD_MOUNT,\n };\n }\n return resolved;\n}\n\n/**\n * Resolve the BullMQ queue name for a *logical pool name*. The orchestrator\n * and worker MUST agree on this mapping or jobs are enqueued onto a queue\n * nobody consumes. Both derive it identically:\n *\n * 1. Look up the pool's `queue` alias (e.g. `jobs-batch`) in the resolved\n * pool config — the same alias `JobWorkerModule.onModuleInit` logs and\n * that the BullMQ `Worker` binds to.\n * 2. Fall back to the logical pool name when the pool is unknown (defensive;\n * still a stable, colon-free identifier).\n * 3. Apply the optional `queue_prefix` namespace for multi-app Redis\n * sharing — `:` is fine in the *queue name* (it is only forbidden in the\n * `jobId`, hence the sha1 there).\n *\n * `poolConfig` defaults to the cached `loadPoolConfig()` so callers that only\n * hold the logical pool name (the orchestrator) don't need to thread the map.\n */\nexport function resolvePoolQueueName(\n pool: string,\n config: BullMqResolvedConfig | null | undefined,\n poolConfig: PoolConfig = loadPoolConfig(),\n): string {\n const alias = poolConfig.get(pool)?.queue ?? pool;\n const prefix = config?.queuePrefix;\n return prefix ? `${prefix}:${alias}` : alias;\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  export { JobDefinitionRow, JobRunRow, JobStepRow, collisionModeEnum, jobRunStatusEnum, jobRuns, jobStepKindEnum, jobStepStatusEnum, jobSteps, jobs, parentClosePolicyEnum, replayFromEnum, triggerSourceEnum, waitKindEnum } from './job-orchestration.schema.js';
2
2
  export { JOBS_MULTI_TENANT, JOB_ORCHESTRATOR, JOB_RUN_SERVICE, JOB_STEP_SERVICE } from './jobs-domain.tokens.js';
3
- export { C as CancelOptions, a as ConcurrencyPolicy, D as DedupePolicy, H as HandlerRegistry, b as HandlerRegistryEntry, I as IJobOrchestrator, J as JOB_HANDLER_METADATA_KEY, c as JOB_HANDLER_REGISTRY, d as JobContext, e as JobHandler, f as JobHandlerBase, g as JobHandlerMeta, h as JobPoolDef, i as JobRun, k as JobUpsertEntry, P as ParentClosePolicy, R as RetryPolicy, S as ScopeRef, l as SpawnChildOptions, m as StartOptions, n as StepOptions } from '../../../job-orchestrator.protocol-CHOEqBDk.js';
3
+ export { C as CancelOptions, a as ConcurrencyPolicy, D as DedupePolicy, H as HandlerRegistry, b as HandlerRegistryEntry, I as IJobOrchestrator, J as JOB_HANDLER_METADATA_KEY, c as JOB_HANDLER_REGISTRY, d as JobContext, e as JobHandler, f as JobHandlerBase, g as JobHandlerMeta, h as JobPoolDef, i as JobRun, j as JobUpsertEntry, P as ParentClosePolicy, R as RetryPolicy, S as ScopeRef, k as SpawnChildOptions, l as StartOptions, m as StepOptions } from '../../../job-orchestrator.protocol-CARhMLCO.js';
4
4
  export { CancelForScopeOptions, IJobRunService, JobRunFailure, JobRunPage, JobRunSummary, ListForScopeOptions, ListJobRunsQuery, PoolStatusCount, RescheduleForScopeOptions } from './job-run-service.protocol.js';
5
5
  export { IJobStepService, JobStep, RecordStepInput } from './job-step-service.protocol.js';
6
6
  export { DrizzleJobOrchestrator } from './job-orchestrator.drizzle-backend.js';
@@ -164,11 +164,15 @@ var jobSteps = pgTable(
164
164
  })
165
165
  );
166
166
 
167
+ // runtime/subsystems/token-key.ts
168
+ var PKG = "@pattern-stack/codegen";
169
+ var tokenKey = (area, name) => `${PKG}.${area}.${name}`;
170
+
167
171
  // runtime/subsystems/jobs/jobs-domain.tokens.ts
168
- var JOB_ORCHESTRATOR = /* @__PURE__ */ Symbol("JOB_ORCHESTRATOR");
169
- var JOB_RUN_SERVICE = /* @__PURE__ */ Symbol("JOB_RUN_SERVICE");
170
- var JOB_STEP_SERVICE = /* @__PURE__ */ Symbol("JOB_STEP_SERVICE");
171
- var JOBS_MULTI_TENANT = /* @__PURE__ */ Symbol("JOBS_MULTI_TENANT");
172
+ var JOB_ORCHESTRATOR = Symbol.for(tokenKey("jobs", "orchestrator"));
173
+ var JOB_RUN_SERVICE = Symbol.for(tokenKey("jobs", "run-service"));
174
+ var JOB_STEP_SERVICE = Symbol.for(tokenKey("jobs", "step-service"));
175
+ var JOBS_MULTI_TENANT = Symbol.for(tokenKey("jobs", "multi-tenant"));
172
176
 
173
177
  // runtime/subsystems/jobs/job-handler.base.ts
174
178
  var ParentClosePolicy = /* @__PURE__ */ ((ParentClosePolicy2) => {
@@ -180,7 +184,7 @@ var ParentClosePolicy = /* @__PURE__ */ ((ParentClosePolicy2) => {
180
184
  var JobHandlerBase = class {
181
185
  };
182
186
  var JOB_HANDLER_REGISTRY = /* @__PURE__ */ new Map();
183
- var JOB_HANDLER_METADATA_KEY = /* @__PURE__ */ Symbol("JobHandlerMeta");
187
+ var JOB_HANDLER_METADATA_KEY = Symbol.for(tokenKey("jobs", "handler-metadata"));
184
188
  function JobHandler(type, meta) {
185
189
  return (target) => {
186
190
  if (JOB_HANDLER_REGISTRY.has(type)) {
@@ -1021,8 +1025,8 @@ function extractUserPools(raw) {
1021
1025
  }
1022
1026
 
1023
1027
  // runtime/subsystems/jobs/bullmq.config.ts
1024
- var BULLMQ_CONNECTION = /* @__PURE__ */ Symbol("BULLMQ_CONNECTION");
1025
- var BULLMQ_RESOLVED_CONFIG = /* @__PURE__ */ Symbol("BULLMQ_RESOLVED_CONFIG");
1028
+ var BULLMQ_CONNECTION = Symbol.for(tokenKey("jobs", "bullmq-connection"));
1029
+ var BULLMQ_RESOLVED_CONFIG = Symbol.for(tokenKey("jobs", "bullmq-resolved-config"));
1026
1030
  var DEFAULT_REDIS_URL = "redis://localhost:6379";
1027
1031
  var DEFAULT_BULL_BOARD_MOUNT = "/admin/queues";
1028
1032
  function resolveBullMqConfig(ext) {
@@ -1048,7 +1052,7 @@ function resolvePoolQueueName(pool, config, poolConfig = loadPoolConfig()) {
1048
1052
  // runtime/subsystems/jobs/job-worker.ts
1049
1053
  import { Inject as Inject4, Injectable as Injectable4, Logger as Logger2 } from "@nestjs/common";
1050
1054
  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";
1051
- var JOB_WORKER_OPTIONS = /* @__PURE__ */ Symbol("JOB_WORKER_OPTIONS");
1055
+ var JOB_WORKER_OPTIONS = Symbol.for(tokenKey("jobs", "worker-options"));
1052
1056
  var DEFAULT_POLL_INTERVAL_MS = 1e3;
1053
1057
  var DEFAULT_STALE_SWEEPER_INTERVAL_MS = 6e4;
1054
1058
  var DEFAULT_STALE_THRESHOLD_MS = 5 * 6e4;
@@ -1475,8 +1479,113 @@ var MemoryJobStore = class {
1475
1479
  };
1476
1480
 
1477
1481
  // runtime/subsystems/jobs/job-orchestrator.memory-backend.ts
1482
+ import { randomUUID as randomUUID3 } from "crypto";
1483
+ import { Inject as Inject6, Injectable as Injectable6, Logger as Logger3, Optional } from "@nestjs/common";
1484
+ import { ModuleRef } from "@nestjs/core";
1485
+
1486
+ // runtime/subsystems/jobs/job-step-service.memory-backend.ts
1478
1487
  import { randomUUID as randomUUID2 } from "crypto";
1479
- import { Inject as Inject5, Injectable as Injectable5, Logger as Logger3, Optional } from "@nestjs/common";
1488
+ import { Inject as Inject5, Injectable as Injectable5 } from "@nestjs/common";
1489
+ var MemoryJobStepService = class {
1490
+ // ADR-037 (package-mode DI): explicit `@Inject(MemoryJobStore)` — the
1491
+ // published bundle carries no `design:paramtypes`, so a by-type inject
1492
+ // would resolve to `undefined` in package mode.
1493
+ constructor(store) {
1494
+ this.store = store;
1495
+ }
1496
+ store;
1497
+ async findStep(runId, stepId) {
1498
+ const rows = this.store.steps.get(runId);
1499
+ if (!rows) return null;
1500
+ const match = rows.find(
1501
+ (r) => r.stepId === stepId && r.status === "completed"
1502
+ );
1503
+ return match ?? null;
1504
+ }
1505
+ async recordStep(input) {
1506
+ const rows = this.getOrCreateRows(input.jobRunId);
1507
+ const existingIdx = rows.findIndex((r) => r.stepId === input.stepId);
1508
+ const normalisedInput = input.input ?? null;
1509
+ const normalisedOutput = input.output ?? null;
1510
+ if (existingIdx >= 0) {
1511
+ const prev = rows[existingIdx];
1512
+ const next = {
1513
+ ...prev,
1514
+ status: input.status,
1515
+ input: normalisedInput ?? prev.input,
1516
+ output: normalisedOutput ?? prev.output,
1517
+ error: input.error ?? prev.error,
1518
+ attempts: input.attempts ?? prev.attempts,
1519
+ startedAt: input.startedAt ?? prev.startedAt,
1520
+ finishedAt: input.finishedAt ?? prev.finishedAt
1521
+ };
1522
+ rows[existingIdx] = next;
1523
+ return next;
1524
+ }
1525
+ const seq = input.seq ?? this.nextSeq(rows);
1526
+ const row = {
1527
+ id: randomUUID2(),
1528
+ jobRunId: input.jobRunId,
1529
+ stepId: input.stepId,
1530
+ kind: input.kind,
1531
+ seq,
1532
+ status: input.status,
1533
+ input: normalisedInput,
1534
+ output: normalisedOutput,
1535
+ error: input.error ?? null,
1536
+ attempts: input.attempts ?? 0,
1537
+ startedAt: input.startedAt ?? null,
1538
+ finishedAt: input.finishedAt ?? null
1539
+ };
1540
+ rows.push(row);
1541
+ return row;
1542
+ }
1543
+ /**
1544
+ * Replay helper — wipe every step row for a run. Mirrors the `scratch`
1545
+ * replay mode of the Drizzle backend (`DELETE FROM job_step WHERE job_run_id = …`).
1546
+ */
1547
+ clearStepsForRun(runId) {
1548
+ this.store.steps.delete(runId);
1549
+ }
1550
+ /**
1551
+ * Remove every non-`completed` row for the run. Memoized (`completed`)
1552
+ * rows are preserved — this is the `last_checkpoint` / `last_step`
1553
+ * semantics the Drizzle backend implements via
1554
+ * `DELETE … WHERE status != 'completed'`. Both replay modes route here
1555
+ * (Phase 1 collapses `last_step` onto this behaviour; see JOB-3 notes).
1556
+ */
1557
+ clearIncompleteSteps(runId) {
1558
+ const rows = this.store.steps.get(runId);
1559
+ if (!rows) return;
1560
+ const kept = rows.filter((r) => r.status === "completed");
1561
+ if (kept.length === 0) {
1562
+ this.store.steps.delete(runId);
1563
+ } else {
1564
+ this.store.steps.set(runId, kept);
1565
+ }
1566
+ }
1567
+ getOrCreateRows(runId) {
1568
+ let rows = this.store.steps.get(runId);
1569
+ if (!rows) {
1570
+ rows = [];
1571
+ this.store.steps.set(runId, rows);
1572
+ }
1573
+ return rows;
1574
+ }
1575
+ nextSeq(rows) {
1576
+ let max = 0;
1577
+ for (const r of rows) {
1578
+ if (r.seq > max) max = r.seq;
1579
+ }
1580
+ return max + 1;
1581
+ }
1582
+ };
1583
+ MemoryJobStepService = __decorateClass([
1584
+ Injectable5(),
1585
+ __decorateParam(0, Inject5(MemoryJobStore))
1586
+ ], MemoryJobStepService);
1587
+
1588
+ // runtime/subsystems/jobs/job-orchestrator.memory-backend.ts
1480
1589
  var QUEUED_RUN_AT = /* @__PURE__ */ new Date(864e13);
1481
1590
  var TERMINAL_STATUSES2 = [
1482
1591
  "completed",
@@ -1647,7 +1756,7 @@ var MemoryJobOrchestrator = class {
1647
1756
  }
1648
1757
  }
1649
1758
  }
1650
- const newId = randomUUID2();
1759
+ const newId = randomUUID3();
1651
1760
  let rootRunId = newId;
1652
1761
  if (opts.parentRunId) {
1653
1762
  const parent = this.store.runs.get(opts.parentRunId);
@@ -2045,9 +2154,12 @@ var MemoryJobOrchestrator = class {
2045
2154
  }
2046
2155
  };
2047
2156
  MemoryJobOrchestrator = __decorateClass([
2048
- Injectable5(),
2049
- __decorateParam(2, Inject5(JOBS_MULTI_TENANT)),
2050
- __decorateParam(3, Optional())
2157
+ Injectable6(),
2158
+ __decorateParam(0, Inject6(MemoryJobStore)),
2159
+ __decorateParam(1, Inject6(MemoryJobStepService)),
2160
+ __decorateParam(2, Inject6(JOBS_MULTI_TENANT)),
2161
+ __decorateParam(3, Optional()),
2162
+ __decorateParam(3, Inject6(ModuleRef))
2051
2163
  ], MemoryJobOrchestrator);
2052
2164
  function classifyError2(err, policy, currentAttempts) {
2053
2165
  if (!policy) return "fail";
@@ -2081,7 +2193,7 @@ function serialiseError2(err, attempt, retryable) {
2081
2193
  }
2082
2194
 
2083
2195
  // runtime/subsystems/jobs/job-run-service.memory-backend.ts
2084
- import { Inject as Inject6, Injectable as Injectable6 } from "@nestjs/common";
2196
+ import { Inject as Inject7, Injectable as Injectable7 } from "@nestjs/common";
2085
2197
  var NON_TERMINAL_STATUSES2 = [
2086
2198
  "pending",
2087
2199
  "running",
@@ -2248,9 +2360,10 @@ var MemoryJobRunService = class {
2248
2360
  }
2249
2361
  };
2250
2362
  MemoryJobRunService = __decorateClass([
2251
- Injectable6(),
2252
- __decorateParam(1, Inject6(JOB_ORCHESTRATOR)),
2253
- __decorateParam(2, Inject6(JOBS_MULTI_TENANT))
2363
+ Injectable7(),
2364
+ __decorateParam(0, Inject7(MemoryJobStore)),
2365
+ __decorateParam(1, Inject7(JOB_ORCHESTRATOR)),
2366
+ __decorateParam(2, Inject7(JOBS_MULTI_TENANT))
2254
2367
  ], MemoryJobRunService);
2255
2368
  function compareBy(a, b, order) {
2256
2369
  switch (order) {
@@ -2266,104 +2379,6 @@ function compareBy(a, b, order) {
2266
2379
  }
2267
2380
  }
2268
2381
 
2269
- // runtime/subsystems/jobs/job-step-service.memory-backend.ts
2270
- import { randomUUID as randomUUID3 } from "crypto";
2271
- import { Injectable as Injectable7 } from "@nestjs/common";
2272
- var MemoryJobStepService = class {
2273
- constructor(store) {
2274
- this.store = store;
2275
- }
2276
- store;
2277
- async findStep(runId, stepId) {
2278
- const rows = this.store.steps.get(runId);
2279
- if (!rows) return null;
2280
- const match = rows.find(
2281
- (r) => r.stepId === stepId && r.status === "completed"
2282
- );
2283
- return match ?? null;
2284
- }
2285
- async recordStep(input) {
2286
- const rows = this.getOrCreateRows(input.jobRunId);
2287
- const existingIdx = rows.findIndex((r) => r.stepId === input.stepId);
2288
- const normalisedInput = input.input ?? null;
2289
- const normalisedOutput = input.output ?? null;
2290
- if (existingIdx >= 0) {
2291
- const prev = rows[existingIdx];
2292
- const next = {
2293
- ...prev,
2294
- status: input.status,
2295
- input: normalisedInput ?? prev.input,
2296
- output: normalisedOutput ?? prev.output,
2297
- error: input.error ?? prev.error,
2298
- attempts: input.attempts ?? prev.attempts,
2299
- startedAt: input.startedAt ?? prev.startedAt,
2300
- finishedAt: input.finishedAt ?? prev.finishedAt
2301
- };
2302
- rows[existingIdx] = next;
2303
- return next;
2304
- }
2305
- const seq = input.seq ?? this.nextSeq(rows);
2306
- const row = {
2307
- id: randomUUID3(),
2308
- jobRunId: input.jobRunId,
2309
- stepId: input.stepId,
2310
- kind: input.kind,
2311
- seq,
2312
- status: input.status,
2313
- input: normalisedInput,
2314
- output: normalisedOutput,
2315
- error: input.error ?? null,
2316
- attempts: input.attempts ?? 0,
2317
- startedAt: input.startedAt ?? null,
2318
- finishedAt: input.finishedAt ?? null
2319
- };
2320
- rows.push(row);
2321
- return row;
2322
- }
2323
- /**
2324
- * Replay helper — wipe every step row for a run. Mirrors the `scratch`
2325
- * replay mode of the Drizzle backend (`DELETE FROM job_step WHERE job_run_id = …`).
2326
- */
2327
- clearStepsForRun(runId) {
2328
- this.store.steps.delete(runId);
2329
- }
2330
- /**
2331
- * Remove every non-`completed` row for the run. Memoized (`completed`)
2332
- * rows are preserved — this is the `last_checkpoint` / `last_step`
2333
- * semantics the Drizzle backend implements via
2334
- * `DELETE … WHERE status != 'completed'`. Both replay modes route here
2335
- * (Phase 1 collapses `last_step` onto this behaviour; see JOB-3 notes).
2336
- */
2337
- clearIncompleteSteps(runId) {
2338
- const rows = this.store.steps.get(runId);
2339
- if (!rows) return;
2340
- const kept = rows.filter((r) => r.status === "completed");
2341
- if (kept.length === 0) {
2342
- this.store.steps.delete(runId);
2343
- } else {
2344
- this.store.steps.set(runId, kept);
2345
- }
2346
- }
2347
- getOrCreateRows(runId) {
2348
- let rows = this.store.steps.get(runId);
2349
- if (!rows) {
2350
- rows = [];
2351
- this.store.steps.set(runId, rows);
2352
- }
2353
- return rows;
2354
- }
2355
- nextSeq(rows) {
2356
- let max = 0;
2357
- for (const r of rows) {
2358
- if (r.seq > max) max = r.seq;
2359
- }
2360
- return max + 1;
2361
- }
2362
- };
2363
- MemoryJobStepService = __decorateClass([
2364
- Injectable7()
2365
- ], MemoryJobStepService);
2366
-
2367
2382
  // runtime/subsystems/jobs/jobs-domain.module.ts
2368
2383
  import { Module } from "@nestjs/common";
2369
2384
  var JobsDomainModule = class {
@@ -2434,14 +2449,15 @@ JobsDomainModule = __decorateClass([
2434
2449
 
2435
2450
  // runtime/subsystems/jobs/job-worker.module.ts
2436
2451
  import {
2437
- Inject as Inject7,
2452
+ Inject as Inject8,
2438
2453
  Injectable as Injectable8,
2439
2454
  Logger as Logger4,
2440
2455
  Module as Module2,
2441
2456
  Optional as Optional2
2442
2457
  } from "@nestjs/common";
2458
+ import { ModuleRef as ModuleRef2 } from "@nestjs/core";
2443
2459
  var DEFAULT_SHUTDOWN_TIMEOUT_MS2 = 3e4;
2444
- var JOB_WORKER_MODULE_OPTIONS = /* @__PURE__ */ Symbol("JOB_WORKER_MODULE_OPTIONS");
2460
+ var JOB_WORKER_MODULE_OPTIONS = Symbol.for(tokenKey("jobs", "worker-module-options"));
2445
2461
  var JobWorkerOrchestrator = class {
2446
2462
  constructor(orchestrator, runService, stepService, options, db = null, moduleRef, bullConnection = null, bullConfig = null) {
2447
2463
  this.orchestrator = orchestrator;
@@ -2634,16 +2650,17 @@ var JobWorkerOrchestrator = class {
2634
2650
  };
2635
2651
  JobWorkerOrchestrator = __decorateClass([
2636
2652
  Injectable8(),
2637
- __decorateParam(0, Inject7(JOB_ORCHESTRATOR)),
2638
- __decorateParam(1, Inject7(JOB_RUN_SERVICE)),
2639
- __decorateParam(2, Inject7(JOB_STEP_SERVICE)),
2640
- __decorateParam(3, Inject7(JOB_WORKER_MODULE_OPTIONS)),
2653
+ __decorateParam(0, Inject8(JOB_ORCHESTRATOR)),
2654
+ __decorateParam(1, Inject8(JOB_RUN_SERVICE)),
2655
+ __decorateParam(2, Inject8(JOB_STEP_SERVICE)),
2656
+ __decorateParam(3, Inject8(JOB_WORKER_MODULE_OPTIONS)),
2641
2657
  __decorateParam(4, Optional2()),
2642
- __decorateParam(4, Inject7(DRIZZLE)),
2658
+ __decorateParam(4, Inject8(DRIZZLE)),
2659
+ __decorateParam(5, Inject8(ModuleRef2)),
2643
2660
  __decorateParam(6, Optional2()),
2644
- __decorateParam(6, Inject7(BULLMQ_CONNECTION)),
2661
+ __decorateParam(6, Inject8(BULLMQ_CONNECTION)),
2645
2662
  __decorateParam(7, Optional2()),
2646
- __decorateParam(7, Inject7(BULLMQ_RESOLVED_CONFIG))
2663
+ __decorateParam(7, Inject8(BULLMQ_RESOLVED_CONFIG))
2647
2664
  ], JobWorkerOrchestrator);
2648
2665
  var JobWorkerModule = class {
2649
2666
  static forRoot(opts) {