@pattern-stack/codegen 0.17.0 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/consumer-skills/integration/audit-and-detection.md +29 -4
  3. package/dist/{chunk-GJDEPTPY.js → chunk-235ZMMJR.js} +8 -8
  4. package/dist/{chunk-DTXH24LR.js → chunk-65MO75WM.js} +9 -9
  5. package/dist/{chunk-5RT7JGKT.js → chunk-7OVCARTQ.js} +4 -4
  6. package/dist/{chunk-CO6LUM72.js → chunk-7P5ODGLA.js} +34 -2
  7. package/dist/chunk-7P5ODGLA.js.map +1 -0
  8. package/dist/{chunk-P3AYBRP6.js → chunk-ATVGYF3D.js} +11 -5
  9. package/dist/chunk-ATVGYF3D.js.map +1 -0
  10. package/dist/{chunk-3MAZ4TQH.js → chunk-AZLUWG5S.js} +9 -9
  11. package/dist/{chunk-UTNWFHJF.js → chunk-B34G6PHD.js} +10 -10
  12. package/dist/{chunk-CDLWYZVQ.js → chunk-BHZP6LOV.js} +7 -7
  13. package/dist/{chunk-W2UIDI3R.js → chunk-CLWBNXKF.js} +4 -4
  14. package/dist/{chunk-OTR44OH6.js → chunk-E6PLM6QG.js} +34 -13
  15. package/dist/chunk-E6PLM6QG.js.map +1 -0
  16. package/dist/{chunk-43SBT72G.js → chunk-I6UXRJ3Q.js} +4 -4
  17. package/dist/{chunk-36U5UGIO.js → chunk-JEINYUJH.js} +8 -5
  18. package/dist/chunk-JEINYUJH.js.map +1 -0
  19. package/dist/{chunk-K2I6XIK5.js → chunk-KSTZIULO.js} +4 -4
  20. package/dist/{chunk-L3VJ47BU.js → chunk-KZDHMZ45.js} +6 -6
  21. package/dist/{chunk-RHYNACZS.js → chunk-OZEPJGMA.js} +2 -2
  22. package/dist/{chunk-MYQIQ27N.js → chunk-Q6LRJ4VI.js} +51 -2
  23. package/dist/chunk-Q6LRJ4VI.js.map +1 -0
  24. package/dist/{chunk-NXNVTXKG.js → chunk-R6F6KFIL.js} +5 -5
  25. package/dist/{chunk-7LKAMLV4.js → chunk-T6SCOJF4.js} +4 -4
  26. package/dist/{chunk-OGIZXGPY.js → chunk-TDEHU73T.js} +4 -4
  27. package/dist/{chunk-OITTYGJS.js → chunk-VDL5CJ5C.js} +24 -14
  28. package/dist/chunk-VDL5CJ5C.js.map +1 -0
  29. package/dist/{chunk-3VEVGL74.js → chunk-VNBC3VXM.js} +4 -4
  30. package/dist/{chunk-BULPAAD3.js → chunk-VQOAATIG.js} +42 -11
  31. package/dist/chunk-VQOAATIG.js.map +1 -0
  32. package/dist/{chunk-E45CSC33.js → chunk-XKWOJZZ4.js} +2 -2
  33. package/dist/{chunk-DCCZB4UC.js → chunk-XWBK3XJK.js} +4 -4
  34. package/dist/{chunk-4GLNY5V6.js → chunk-Y7GDG744.js} +5 -5
  35. package/dist/{chunk-SR7F3TJY.js → chunk-YK5JEVLX.js} +4 -4
  36. package/dist/{job-orchestrator.protocol-DubMVbm9.d.ts → job-orchestrator.protocol-ZuJ3ow-O.d.ts} +77 -3
  37. package/dist/runtime/base-classes/index.js +12 -12
  38. package/dist/runtime/shared/openapi/index.js +5 -5
  39. package/dist/runtime/shared/openapi/registry.js +2 -2
  40. package/dist/runtime/subsystems/auth/auth.module.js +3 -3
  41. package/dist/runtime/subsystems/auth/index.js +12 -12
  42. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.d.ts +1 -1
  43. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js +3 -3
  44. package/dist/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.js +3 -3
  45. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js +7 -7
  46. package/dist/runtime/subsystems/bridge/bridge.module.d.ts +1 -1
  47. package/dist/runtime/subsystems/bridge/bridge.module.js +19 -19
  48. package/dist/runtime/subsystems/bridge/event-flow.service.d.ts +1 -1
  49. package/dist/runtime/subsystems/bridge/event-flow.service.js +2 -2
  50. package/dist/runtime/subsystems/bridge/index.d.ts +1 -1
  51. package/dist/runtime/subsystems/bridge/index.js +21 -21
  52. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +4 -4
  53. package/dist/runtime/subsystems/events/events.module.js +5 -5
  54. package/dist/runtime/subsystems/events/index.js +7 -7
  55. package/dist/runtime/subsystems/index.d.ts +1 -1
  56. package/dist/runtime/subsystems/index.js +100 -100
  57. package/dist/runtime/subsystems/integration/build-change-source.js +2 -2
  58. package/dist/runtime/subsystems/integration/deep-equal.differ.d.ts +19 -0
  59. package/dist/runtime/subsystems/integration/deep-equal.differ.js +1 -1
  60. package/dist/runtime/subsystems/integration/execute-integration.use-case.js +2 -2
  61. package/dist/runtime/subsystems/integration/index.js +39 -39
  62. package/dist/runtime/subsystems/integration/integration-cursor-store.drizzle-backend.js +2 -2
  63. package/dist/runtime/subsystems/integration/integration-run-recorder.drizzle-backend.js +2 -2
  64. package/dist/runtime/subsystems/integration/integration.module.d.ts +20 -0
  65. package/dist/runtime/subsystems/integration/integration.module.js +6 -6
  66. package/dist/runtime/subsystems/jobs/index.d.ts +1 -1
  67. package/dist/runtime/subsystems/jobs/index.js +27 -27
  68. package/dist/runtime/subsystems/jobs/job-handler.base.d.ts +1 -1
  69. package/dist/runtime/subsystems/jobs/job-handler.base.js +11 -3
  70. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.d.ts +1 -1
  71. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +6 -5
  72. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -1
  73. package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.d.ts +1 -1
  74. package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.js +3 -2
  75. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.d.ts +11 -1
  76. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js +2 -2
  77. package/dist/runtime/subsystems/jobs/job-orchestrator.protocol.d.ts +1 -1
  78. package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.d.ts +1 -1
  79. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.d.ts +1 -1
  80. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +2 -2
  81. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.d.ts +1 -1
  82. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +2 -2
  83. package/dist/runtime/subsystems/jobs/job-run-service.protocol.d.ts +1 -1
  84. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.d.ts +1 -1
  85. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +1 -1
  86. package/dist/runtime/subsystems/jobs/job-worker.d.ts +9 -1
  87. package/dist/runtime/subsystems/jobs/job-worker.js +3 -3
  88. package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +1 -1
  89. package/dist/runtime/subsystems/jobs/job-worker.module.js +11 -11
  90. package/dist/runtime/subsystems/jobs/jobs-domain.module.js +9 -9
  91. package/dist/runtime/subsystems/jobs/jobs-errors.d.ts +1 -1
  92. package/dist/runtime/subsystems/jobs/pg-notify.d.ts +25 -1
  93. package/dist/runtime/subsystems/jobs/pg-notify.js +1 -1
  94. package/dist/runtime/subsystems/observability/index.d.ts +1 -1
  95. package/dist/runtime/subsystems/observability/index.js +3 -3
  96. package/dist/runtime/subsystems/observability/observability.module.js +3 -3
  97. package/dist/runtime/subsystems/observability/observability.protocol.d.ts +1 -1
  98. package/dist/runtime/subsystems/observability/observability.service.d.ts +1 -1
  99. package/dist/runtime/subsystems/observability/observability.service.js +2 -2
  100. package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.d.ts +1 -1
  101. package/dist/runtime/subsystems/observability/reporters/index.d.ts +1 -1
  102. package/dist/runtime/subsystems/storage/index.js +1 -1
  103. package/dist/runtime/subsystems/storage/storage.module.js +1 -1
  104. package/dist/src/cli/index.js +38 -16
  105. package/dist/src/cli/index.js.map +1 -1
  106. package/dist/src/index.js +13 -13
  107. package/package.json +1 -1
  108. package/runtime/subsystems/integration/deep-equal.differ.ts +34 -5
  109. package/runtime/subsystems/integration/integration.module.ts +26 -2
  110. package/runtime/subsystems/jobs/job-handler.base.ts +115 -2
  111. package/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.ts +43 -16
  112. package/runtime/subsystems/jobs/job-orchestrator.memory-backend.ts +58 -18
  113. package/runtime/subsystems/jobs/job-worker.ts +29 -11
  114. package/runtime/subsystems/jobs/pg-notify.ts +63 -3
  115. package/templates/subsystem/integration-config/codegen-config-integration-block.ejs.t +17 -0
  116. package/dist/chunk-36U5UGIO.js.map +0 -1
  117. package/dist/chunk-BULPAAD3.js.map +0 -1
  118. package/dist/chunk-CO6LUM72.js.map +0 -1
  119. package/dist/chunk-MYQIQ27N.js.map +0 -1
  120. package/dist/chunk-OITTYGJS.js.map +0 -1
  121. package/dist/chunk-OTR44OH6.js.map +0 -1
  122. package/dist/chunk-P3AYBRP6.js.map +0 -1
  123. /package/dist/{chunk-GJDEPTPY.js.map → chunk-235ZMMJR.js.map} +0 -0
  124. /package/dist/{chunk-DTXH24LR.js.map → chunk-65MO75WM.js.map} +0 -0
  125. /package/dist/{chunk-5RT7JGKT.js.map → chunk-7OVCARTQ.js.map} +0 -0
  126. /package/dist/{chunk-3MAZ4TQH.js.map → chunk-AZLUWG5S.js.map} +0 -0
  127. /package/dist/{chunk-UTNWFHJF.js.map → chunk-B34G6PHD.js.map} +0 -0
  128. /package/dist/{chunk-CDLWYZVQ.js.map → chunk-BHZP6LOV.js.map} +0 -0
  129. /package/dist/{chunk-W2UIDI3R.js.map → chunk-CLWBNXKF.js.map} +0 -0
  130. /package/dist/{chunk-43SBT72G.js.map → chunk-I6UXRJ3Q.js.map} +0 -0
  131. /package/dist/{chunk-K2I6XIK5.js.map → chunk-KSTZIULO.js.map} +0 -0
  132. /package/dist/{chunk-L3VJ47BU.js.map → chunk-KZDHMZ45.js.map} +0 -0
  133. /package/dist/{chunk-RHYNACZS.js.map → chunk-OZEPJGMA.js.map} +0 -0
  134. /package/dist/{chunk-NXNVTXKG.js.map → chunk-R6F6KFIL.js.map} +0 -0
  135. /package/dist/{chunk-7LKAMLV4.js.map → chunk-T6SCOJF4.js.map} +0 -0
  136. /package/dist/{chunk-OGIZXGPY.js.map → chunk-TDEHU73T.js.map} +0 -0
  137. /package/dist/{chunk-3VEVGL74.js.map → chunk-VNBC3VXM.js.map} +0 -0
  138. /package/dist/{chunk-E45CSC33.js.map → chunk-XKWOJZZ4.js.map} +0 -0
  139. /package/dist/{chunk-DCCZB4UC.js.map → chunk-XWBK3XJK.js.map} +0 -0
  140. /package/dist/{chunk-4GLNY5V6.js.map → chunk-Y7GDG744.js.map} +0 -0
  141. /package/dist/{chunk-SR7F3TJY.js.map → chunk-YK5JEVLX.js.map} +0 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,110 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.17.2] — 2026-06-04
8
+
9
+ **Shutdown leak fix** (LISTEN-NOTIFY-2; swe-brain dogfood). With
10
+ `listen_notify: true` (the LISTEN/NOTIFY wake extension shipped in 0.16.0), a
11
+ Nest app that booted and then `app.close()`d — e.g. a boot-check / CI smoke step —
12
+ never exited: at least one `LISTEN codegen_jobs_wake` client survived
13
+ `app.close()`, holding an ESTABLISHED Postgres socket open forever (two swe-brain
14
+ CI runs hung for hours). Backward-compatible; affects only consumers that opted
15
+ into `listen_notify`.
16
+
17
+ ### Fixed
18
+
19
+ - **`PgNotifyListener.stop()` is race-safe against an in-flight `connect()`**
20
+ (LISTEN-NOTIFY-2 RC1 — the defect that actually fired). `connect()` checked
21
+ `this.stopped` only at entry, then `await pool.connect()`, wired handlers,
22
+ issued `LISTEN`, and assigned `this.client` last. A `stop()` arriving during
23
+ the checkout await ran `releaseClient()` against a still-null `this.client`
24
+ (released nothing); the resuming `connect()` then assigned the client and
25
+ issued `LISTEN` — leaking a checked-out connection with no owner left to
26
+ release it. With 5–6 listeners (one per jobs pool + the events drainer) all
27
+ starting at bootstrap and a tight `app.close()`, the race fired on ~1 of 6
28
+ listeners — exactly the observed signature (one survivor, the rest clean).
29
+ Now `connect()` re-checks `stopped` after the checkout AND after `LISTEN`,
30
+ destroying the just-acquired client and bailing before assignment; `stop()`
31
+ tracks and awaits the in-flight connect promise before its own release, so
32
+ `app.close()` can't return while a checkout is still mid-flight. Releases use
33
+ `release(true)` (destroy) so a half-listening socket is never reused.
34
+ - **`JobWorker.onModuleDestroy` stops the wake listener on EVERY destroy path**
35
+ (LISTEN-NOTIFY-2 RC2 — latent). The listener `stop()` lived only on the first
36
+ (non-`shuttingDown`) branch, so a SIGTERM-then-Nest double-destroy hit the
37
+ `if (this.shuttingDown) { …; return; }` early return and skipped it, leaking
38
+ the listener under the normal SIGTERM shutdown path. Teardown is now an
39
+ idempotent `stopNotifyListener()` called unconditionally at the top of every
40
+ destroy. `DrizzleEventBus` already stopped its listener unconditionally; it
41
+ shared `PgNotifyListener` and so benefits from the RC1 fix directly.
42
+
43
+ ## [0.17.1] — 2026-06-04
44
+
45
+ **Two dogfood fixes that bit the same swe-brain mutation drain** (ADR-0009
46
+ Amendment B): the jobs orchestrator silently dropped function-form
47
+ concurrency/dedupe keys, and the integration differ unconditionally ignored
48
+ `deletedAt`. Both are honored now; both are backward-compatible.
49
+
50
+ ### Fixed
51
+
52
+ - **`@JobHandler` function-form `concurrency.key` / `dedupe.key` are honored
53
+ end-to-end** (JOB-FN-KEY; swe-brain ADR-0009 Amendment B §B3). The typed API
54
+ (`ConcurrencyPolicy.key`/`DedupePolicy.key`) had ALWAYS required a function,
55
+ but registration (`upsertJobRows`) stored only `typeof key === 'string' ? key
56
+ : null` — a function key persisted as a NULL `concurrency_key_template`, so
57
+ `start()` wrote a NULL `job_run.concurrency_key` and the worker's queue-release
58
+ gate (which keys off `claimed.concurrencyKey`) never engaged. Observed in
59
+ swe-brain: three `inbound-sync` runs the handler believed shared one
60
+ `collisionMode: 'queue'` lane ran fully concurrently, three `integration_runs`
61
+ racing one message row. Now both backends persist a function key as the
62
+ `FN_KEY_SENTINEL` marker (non-null, so the collision/dedupe path engages, and
63
+ hash-stable so the definition-hash gate doesn't churn) and re-resolve the live
64
+ function from `JOB_HANDLER_REGISTRY` at `start()`. A `FN_KEY_SENTINEL` with no
65
+ live function throws `JobKeyFunctionUnavailableError` (fail loud, never
66
+ silently degrade to no-key). Drizzle + memory + BullMQ (which delegates to the
67
+ Drizzle `start`) all agree.
68
+
69
+ ### Changed
70
+
71
+ - **`ConcurrencyPolicy.key` / `DedupePolicy.key` widen to
72
+ `JobKeySelector<TInput> = string | ((input) => string)`.** The string form is
73
+ documented as a `{{field}}` template (evaluated by `evaluateKeyTemplate`); the
74
+ function form is the one that previously type-checked but was dropped. Existing
75
+ function keys now WORK (were silently no-key before); existing string-template
76
+ keys behave identically. New exports from `@pattern-stack/codegen/runtime/*`
77
+ jobs: `JobKeySelector`, `FN_KEY_SENTINEL`, `keySelectorToTemplate`,
78
+ `resolveJobKey`, `JobKeyFunctionUnavailableError`.
79
+
80
+ ### Added
81
+
82
+ - **`DeepEqualDifferOptions.unignore`** — the inverse of `ignore`, subtracted
83
+ from the default ignore set after the merge (DIFFER-UNIGNORE; swe-brain
84
+ ADR-0009 Amendment B §B4). Lets a consumer declare that a normally-metadata
85
+ column is DOMAIN DATA for their entity. The canonical case: an entity with
86
+ `softDelete: false` whose `deletedAt` carries a vendor-observed retraction
87
+ tombstone ON the canonical record (a Slack `message_deleted` → `deletedAt`,
88
+ ADR-0008 §1). `deletedAt` is in `DEFAULT_IGNORE_FIELDS`, so the tombstone
89
+ overlay diffed to `'noop'` → the upsert was skipped → `deleted_at` never
90
+ landed (observed: `integration_run_items` `{operation: noop, changed_fields:
91
+ {}}` for every delete). `new DeepEqualDiffer({ unignore: ['deletedAt'] })` now
92
+ makes the field register as a change. `unignore` wins over a field also in
93
+ `ignore`; un-ignoring a field not in the set is a harmless no-op; per-instance
94
+ (never mutates `DEFAULT_IGNORE_FIELDS`).
95
+ - **`IntegrationModuleOptions.differ` + `integration.differ.{ignore,unignore}`
96
+ config threading.** `IntegrationModule.forRoot({ differ: { unignore:
97
+ [...] } })` threads into the default `DeepEqualDiffer` bound to
98
+ `INTEGRATION_FIELD_DIFFER`, and the subsystem barrel generator emits that
99
+ `forRoot` option from `integration.differ.*` in `codegen.config.yaml` (same
100
+ off-by-default config-threading shape as 0.16.0's `listen_notify`; vendored +
101
+ package mode both covered). A feature module that binds its own
102
+ `IFieldDiffer<T>` still overrides entirely.
103
+
104
+ ### Docs
105
+
106
+ - Differ header comment + `integration` consumer skill (`audit-and-detection.md`,
107
+ `protocols-and-ports.md`) document the `unignore` knob and the
108
+ `integration.differ.*` config path; the `integration-config` codegen.config
109
+ template gains a commented `differ:` block.
110
+
7
111
  ## [0.17.0] — 2026-06-04
8
112
 
9
113
  **`ActivityPattern` subject scoping is config-driven** (ACTIVITY-SUBJECT-1) —
@@ -285,15 +285,40 @@ as changes), but the diff output preserves the *raw* values:
285
285
  Drizzle while adapters deliver numbers; a numeric and a finite-parseable
286
286
  string compare equal (with an empty-string guard against silent 0-equality).
287
287
 
288
- **Augment the ignore list per entity** (values merge with the defaults; you
289
- cannot remove defaults):
288
+ **Augment the ignore list** (`ignore` values merge with the defaults):
290
289
 
291
290
  ```ts
292
291
  { provide: INTEGRATION_FIELD_DIFFER, useValue: new DeepEqualDiffer({ ignore: ['integration_version'] }) }
293
292
  ```
294
293
 
295
- Bind it as `useValue: new DeepEqualDiffer(...)`, not `useClass` the
296
- constructor's optional options object confuses Nest's metadata reflection.
294
+ **Un-ignore a default** (`unignore` the inverse knob). A normally-metadata
295
+ column can be *domain data* for a given entity. The canonical case: an entity
296
+ with `softDelete: false` whose `deletedAt` carries a vendor-observed retraction
297
+ tombstone *on the canonical record* (e.g. a Slack `message_deleted` maps to
298
+ `deletedAt`). Because `deletedAt` is in the default ignore list, the tombstone
299
+ overlay diffs to `'noop'`, the upsert is skipped, and `deleted_at` never lands.
300
+ `unignore` removes it from the ignore set so it registers as a field change:
301
+
302
+ ```ts
303
+ { provide: INTEGRATION_FIELD_DIFFER, useValue: new DeepEqualDiffer({ unignore: ['deletedAt'] }) }
304
+ ```
305
+
306
+ `unignore` is subtracted after `ignore` is merged, so it wins on a field listed
307
+ in both. Un-ignoring a field that isn't in the (merged) set is a harmless no-op.
308
+
309
+ **Set it once for the whole app via config** instead of binding per feature
310
+ module — `integration.differ.{ignore,unignore}` in `codegen.config.yaml` threads
311
+ into the default differ that `IntegrationModule.forRoot` provides:
312
+
313
+ ```yaml
314
+ integration:
315
+ backend: drizzle
316
+ differ:
317
+ unignore: [deletedAt] # this entity's deletedAt is domain data
318
+ ```
319
+
320
+ Bind a per-module differ as `useValue: new DeepEqualDiffer(...)`, not `useClass`
321
+ — the constructor's optional options object confuses Nest's metadata reflection.
297
322
 
298
323
  **`providerChangedFields` is advisory.** When a CDC provider tells you which
299
324
  columns changed, set it on the `Change<T>` and the differ skips deep-equal over
@@ -1,9 +1,13 @@
1
1
  import {
2
2
  JobWorker
3
- } from "./chunk-OITTYGJS.js";
3
+ } from "./chunk-VDL5CJ5C.js";
4
4
  import {
5
5
  JobsDomainModule
6
- } from "./chunk-3MAZ4TQH.js";
6
+ } from "./chunk-AZLUWG5S.js";
7
+ import {
8
+ BootValidationError,
9
+ ReservedPoolViolationError
10
+ } from "./chunk-T4BIIU5E.js";
7
11
  import {
8
12
  BULLMQ_CONNECTION,
9
13
  BULLMQ_RESOLVED_CONFIG,
@@ -14,13 +18,9 @@ import {
14
18
  allPoolNames,
15
19
  loadPoolConfig
16
20
  } from "./chunk-RHVN6NA7.js";
17
- import {
18
- BootValidationError,
19
- ReservedPoolViolationError
20
- } from "./chunk-T4BIIU5E.js";
21
21
  import {
22
22
  HandlerRegistry
23
- } from "./chunk-CO6LUM72.js";
23
+ } from "./chunk-7P5ODGLA.js";
24
24
  import {
25
25
  JOB_ORCHESTRATOR,
26
26
  JOB_RUN_SERVICE,
@@ -290,4 +290,4 @@ export {
290
290
  JobWorkerOrchestrator,
291
291
  JobWorkerModule
292
292
  };
293
- //# sourceMappingURL=chunk-GJDEPTPY.js.map
293
+ //# sourceMappingURL=chunk-235ZMMJR.js.map
@@ -1,22 +1,22 @@
1
1
  import {
2
2
  BRIDGE_DELIVERY_JOB_TYPE
3
- } from "./chunk-NXNVTXKG.js";
3
+ } from "./chunk-R6F6KFIL.js";
4
4
  import {
5
5
  bridgeDelivery
6
6
  } from "./chunk-2TVVBC53.js";
7
- import {
8
- JOBS_LISTEN_NOTIFY
9
- } from "./chunk-ZPL74UQN.js";
10
- import {
11
- jobRuns
12
- } from "./chunk-OKXZ63IA.js";
13
7
  import {
14
8
  JOBS_WAKE_CHANNEL,
15
9
  pgNotify
16
- } from "./chunk-MYQIQ27N.js";
10
+ } from "./chunk-Q6LRJ4VI.js";
11
+ import {
12
+ JOBS_LISTEN_NOTIFY
13
+ } from "./chunk-ZPL74UQN.js";
17
14
  import {
18
15
  BRIDGE_REGISTRY
19
16
  } from "./chunk-4LH67P4U.js";
17
+ import {
18
+ jobRuns
19
+ } from "./chunk-OKXZ63IA.js";
20
20
  import {
21
21
  __decorateClass,
22
22
  __decorateParam
@@ -151,4 +151,4 @@ BridgeOutboxDrainHook = __decorateClass([
151
151
  export {
152
152
  BridgeOutboxDrainHook
153
153
  };
154
- //# sourceMappingURL=chunk-DTXH24LR.js.map
154
+ //# sourceMappingURL=chunk-65MO75WM.js.map
@@ -4,14 +4,14 @@ import {
4
4
  import {
5
5
  JOB_ORCHESTRATOR
6
6
  } from "./chunk-ZPL74UQN.js";
7
+ import {
8
+ EVENT_BUS
9
+ } from "./chunk-H5NH7KPE.js";
7
10
  import {
8
11
  BRIDGE_DELIVERY_REPO,
9
12
  BRIDGE_MULTI_TENANT,
10
13
  BRIDGE_REGISTRY
11
14
  } from "./chunk-4LH67P4U.js";
12
- import {
13
- EVENT_BUS
14
- } from "./chunk-H5NH7KPE.js";
15
15
  import {
16
16
  DRIZZLE
17
17
  } from "./chunk-U64T4YZE.js";
@@ -106,4 +106,4 @@ EventFlowService = __decorateClass([
106
106
  export {
107
107
  EventFlowService
108
108
  };
109
- //# sourceMappingURL=chunk-5RT7JGKT.js.map
109
+ //# sourceMappingURL=chunk-7OVCARTQ.js.map
@@ -47,6 +47,34 @@ var HandlerRegistry;
47
47
  }
48
48
  HandlerRegistry2.get = get;
49
49
  })(HandlerRegistry || (HandlerRegistry = {}));
50
+ var FN_KEY_SENTINEL = "<fn>";
51
+ function keySelectorToTemplate(key) {
52
+ if (typeof key === "string") return key;
53
+ if (typeof key === "function") return FN_KEY_SENTINEL;
54
+ return null;
55
+ }
56
+ function resolveJobKey(kind, type, template, payload, evaluateTemplate) {
57
+ if (template == null) return null;
58
+ if (template !== FN_KEY_SENTINEL) return evaluateTemplate(template, payload);
59
+ const meta = JOB_HANDLER_REGISTRY.get(type)?.meta;
60
+ const key = meta?.[kind]?.key;
61
+ if (typeof key !== "function") {
62
+ throw new JobKeyFunctionUnavailableError(type, kind);
63
+ }
64
+ return key(payload);
65
+ }
66
+ var JobKeyFunctionUnavailableError = class extends Error {
67
+ constructor(jobType, kind) {
68
+ super(
69
+ `[jobs] ${kind} key for job '${jobType}' was persisted as a function sentinel ('${FN_KEY_SENTINEL}') but no live function is registered for it. The @JobHandler must be imported before start() so its meta is in JOB_HANDLER_REGISTRY.`
70
+ );
71
+ this.jobType = jobType;
72
+ this.kind = kind;
73
+ this.name = "JobKeyFunctionUnavailableError";
74
+ }
75
+ jobType;
76
+ kind;
77
+ };
50
78
 
51
79
  export {
52
80
  ParentClosePolicy,
@@ -54,6 +82,10 @@ export {
54
82
  JOB_HANDLER_REGISTRY,
55
83
  JOB_HANDLER_METADATA_KEY,
56
84
  JobHandler,
57
- HandlerRegistry
85
+ HandlerRegistry,
86
+ FN_KEY_SENTINEL,
87
+ keySelectorToTemplate,
88
+ resolveJobKey,
89
+ JobKeyFunctionUnavailableError
58
90
  };
59
- //# sourceMappingURL=chunk-CO6LUM72.js.map
91
+ //# sourceMappingURL=chunk-7P5ODGLA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../runtime/subsystems/jobs/job-handler.base.ts"],"sourcesContent":["/**\n * Handler base class, JobContext, @JobHandler decorator, and policy types\n * for the job orchestration domain (ADR-022, JOB-2).\n *\n * User-authored jobs subclass `JobHandlerBase<TInput, TOutput>` and decorate\n * the class with `@JobHandler<TInput>('job_type', meta)`. The decorator\n * 1. stores metadata via `Reflect.defineMetadata` so Nest's reflector can\n * pick it up at module boot, and\n * 2. populates `JOB_HANDLER_REGISTRY` — a module-singleton map consumed by\n * `JobWorkerModule` (JOB-5) to materialise `job` rows and resolve\n * handler classes during claim/execute.\n *\n * No runtime orchestration lives here; this file is a pure type + decorator\n * surface so downstream PRs (JOB-3..JOB-5) can implement against a stable\n * shape.\n */\n// TODO(logging-subsystem): swap to ILogger once ADR-028 lands\nimport type { Logger } from '@nestjs/common';\nimport { tokenKey } from '../token-key';\nimport type { EventOfType, EventTypeName } from '../events/event-registry';\nimport type { JobRun } from './job-orchestrator.protocol';\n\n// ─── ParentClosePolicy ──────────────────────────────────────────────────────\n\n/**\n * What happens to running child runs when a parent enters a terminal state.\n * Stored on the child at spawn; changes to the parent after spawn do NOT\n * retroactively rewrite children.\n */\nexport enum ParentClosePolicy {\n Terminate = 'terminate',\n Cancel = 'cancel',\n Abandon = 'abandon',\n}\n\n// ─── Policy types ───────────────────────────────────────────────────────────\n\nexport interface RetryPolicy {\n attempts: number;\n backoff: 'fixed' | 'exponential';\n baseMs: number;\n nonRetryableErrors?: string[];\n}\n\n/**\n * Concurrency lane key (JOB-FN-KEY, 0.16.2).\n *\n * Two authoring forms, both honored end-to-end (the typed function form was\n * previously dropped to `null` at registration — see `upsertJobRows` — so\n * `collisionMode` silently never engaged):\n *\n * - **`string`** — a `{{field}}` template evaluated against the start\n * payload by `evaluateKeyTemplate` (single-key substitution, no dotted\n * paths). Persisted verbatim to `job.concurrency_key_template`.\n * - **`(input) => string`** — an arbitrary function of the input. Persisted\n * as the `FN_KEY_SENTINEL` marker so the definition-hash gate stays stable\n * and the collision path engages; `start()` re-resolves the live function\n * from `JOB_HANDLER_REGISTRY` and evaluates it against the payload.\n *\n * Both forms produce a per-lane key; same key + in-flight incumbent ⇒\n * `collisionMode` ('queue' | 'reject' | 'replace') decides.\n */\nexport type JobKeySelector<TInput> = string | ((input: TInput) => string);\n\nexport interface ConcurrencyPolicy<TInput> {\n key: JobKeySelector<TInput>;\n collisionMode: 'queue' | 'reject' | 'replace';\n}\n\nexport interface DedupePolicy<TInput> {\n key: JobKeySelector<TInput>;\n windowMs: number;\n}\n\n/**\n * Declarative scope reference. `TScope` is parameterised so JOB-7 can narrow\n * `entity` to the generated `ScopeEntityType` union at the call site without\n * modifying this file (OQ-1 resolution, 2026-04-20).\n */\nexport interface ScopeRef<TInput, TScope extends string = string> {\n entity: TScope;\n from: (input: TInput) => string;\n}\n\n/**\n * Bridge trigger authoring shape (BRIDGE-6 follow-up — BRIDGE-6 shipped the\n * generator + runtime for `@JobHandler({ triggers })` but never added the\n * authoring field to this type; the generator's tests scan source as strings,\n * so a real decorator was never compiled and the gap went uncaught).\n *\n * Declared on `@JobHandler({ triggers })`; the codegen bridge-registry\n * generator (`src/cli/shared/bridge-registry-generator.ts`) scans these from\n * source and emits `bridge/generated/registry.ts`, validating each `event`\n * against the generated `eventRegistry` at `gen-all`. The distributed union\n * narrows `map`/`when` per `event`, so callbacks are typed against the event\n * payload (ADR-023, \"typed against PayloadOfType<T>\").\n *\n * Typed against events' generated types — the same `import type` coupling the\n * bridge already has (erased at runtime). `jobs` must NOT import `bridge`, so\n * the post-gen `BridgeTriggerEntry` is deliberately not referenced here;\n * `triggerId`/`jobType` are computed by the generator, not authored.\n */\nexport type JobTrigger<TInput> = {\n [T in EventTypeName]: {\n /** Event type that fires this trigger. Validated against `eventRegistry`. */\n event: T;\n /** Maps the event to the job input. Inlined verbatim into the registry. */\n map: (event: EventOfType<T>) => TInput;\n /** Optional guard; `false` → wrapper records `status='skipped'`. */\n when?: (event: EventOfType<T>) => boolean;\n };\n}[EventTypeName];\n\nexport interface JobHandlerMeta<TInput> {\n pool?: string;\n scope?: ScopeRef<TInput>;\n retry?: RetryPolicy;\n concurrency?: ConcurrencyPolicy<TInput>;\n dedupe?: DedupePolicy<TInput>;\n timeoutMs?: number;\n replayFrom?: 'scratch' | 'last_step' | 'last_checkpoint';\n /**\n * Bridge triggers (ADR-023 Tier 3). Codegen scans these into `bridgeRegistry`;\n * the framework `BridgeDeliveryHandler` starts this job per matched event.\n * Absent for jobs started directly or via `IEventFlow.publishAndStart`.\n */\n triggers?: readonly JobTrigger<TInput>[];\n}\n\n// ─── Runtime option shapes ──────────────────────────────────────────────────\n\nexport interface StepOptions {\n retry?: RetryPolicy;\n timeoutMs?: number;\n}\n\nexport interface SpawnChildOptions {\n closePolicy?: ParentClosePolicy;\n runAt?: Date;\n priority?: number;\n tags?: Record<string, string>;\n}\n\n// ─── JobContext ─────────────────────────────────────────────────────────────\n\nexport interface JobContext<TInput> {\n readonly input: TInput;\n readonly run: JobRun;\n step<TOutput>(\n stepId: string,\n fn: () => Promise<TOutput>,\n opts?: StepOptions,\n ): Promise<TOutput>;\n spawnChild(type: string, input: unknown, opts?: SpawnChildOptions): Promise<JobRun>;\n readonly logger: Logger;\n // NOT in Phase 1 — deferred to ADR-025:\n // waitFor(kind, token, opts)\n // signal(token, payload)\n // sleep(ms)\n}\n\n// ─── JobHandlerBase ─────────────────────────────────────────────────────────\n\nexport abstract class JobHandlerBase<TInput, TOutput = unknown> {\n abstract run(ctx: JobContext<TInput>): Promise<TOutput>;\n}\n\n// ─── Registry + decorator ───────────────────────────────────────────────────\n\n/**\n * Module-singleton map keyed by job type. Populated by the `@JobHandler`\n * decorator at class definition time; consumed by `JobWorkerModule` (JOB-5)\n * to upsert `job` rows and resolve handler classes during claim/execute.\n */\nexport const JOB_HANDLER_REGISTRY = new Map<\n string,\n {\n type: string;\n meta: JobHandlerMeta<unknown>;\n handlerClass: new (...args: unknown[]) => JobHandlerBase<unknown>;\n }\n>();\n\n// ADR-037: namespaced `Symbol.for(...)` (via `tokenKey()`) so the reflection-metadata\n// key matches by value across import boundaries (the @JobHandler decorator and the\n// reader may resolve different runtime copies). Distinct from the DI tokens but\n// subject to the same dual-package identity hazard.\nexport const JOB_HANDLER_METADATA_KEY = Symbol.for(tokenKey('jobs', 'handler-metadata'));\n\n/**\n * Class decorator that registers a handler with the job type, the full\n * metadata shape, and the target class constructor.\n *\n * Duplicate-type behaviour (OQ-3, resolved 2026-04-18):\n * - `NODE_ENV === 'production'` → throw; silent overwrite in prod is a\n * correctness bug.\n * - `NODE_ENV === 'test'` → silent overwrite (tests intentionally\n * re-register handlers).\n * - otherwise (dev) → `console.warn` + overwrite. `console`\n * is used intentionally instead of the Nest `Logger` — decorators run\n * at module-load time before any Nest container exists.\n */\nexport function JobHandler<TInput>(\n type: string,\n meta: JobHandlerMeta<TInput>,\n): ClassDecorator {\n return (target) => {\n if (JOB_HANDLER_REGISTRY.has(type)) {\n const env = process.env.NODE_ENV;\n if (env === 'production') {\n throw new Error(\n `[JobHandler] Duplicate registration for job type '${type}'. ` +\n `Each @JobHandler must declare a unique type.`,\n );\n }\n if (env !== 'test') {\n // eslint-disable-next-line no-console\n console.warn(\n `[JobHandler] Duplicate registration for job type '${type}'. ` +\n `Overwriting previous handler — this is almost certainly a bug.`,\n );\n }\n }\n\n Reflect.defineMetadata(JOB_HANDLER_METADATA_KEY, { type, meta }, target);\n JOB_HANDLER_REGISTRY.set(type, {\n type,\n meta: meta as JobHandlerMeta<unknown>,\n handlerClass: target as unknown as new (\n ...args: unknown[]\n ) => JobHandlerBase<unknown>,\n });\n };\n}\n\n// ─── HandlerRegistry — read helpers consumed by JobWorkerModule (JOB-5) ─────\n\n/**\n * Single entry shape returned by `HandlerRegistry.getAll()` / `.get()` and\n * exposed to `JobWorkerModule.onModuleInit` for boot-time upserts.\n *\n * Structurally compatible with `IJobOrchestrator.upsertJobRows`'s\n * `JobUpsertEntry` so the worker module can pass entries through verbatim\n * without re-mapping.\n */\nexport interface HandlerRegistryEntry {\n type: string;\n meta: JobHandlerMeta<unknown>;\n handlerClass: new (...args: unknown[]) => JobHandlerBase<unknown>;\n}\n\n/**\n * Read facade over `JOB_HANDLER_REGISTRY`. The decorator's write path is\n * unchanged; this namespace exists so consumers (the worker module, tests)\n * don't import the raw `Map` and accidentally mutate it.\n */\nexport namespace HandlerRegistry {\n /** All registered entries in insertion order. */\n export function getAll(): HandlerRegistryEntry[] {\n return Array.from(JOB_HANDLER_REGISTRY.values());\n }\n\n /** Lookup by job type, or `undefined` if no `@JobHandler` is registered. */\n export function get(type: string): HandlerRegistryEntry | undefined {\n return JOB_HANDLER_REGISTRY.get(type);\n }\n}\n\n// ─── Key resolution (JOB-FN-KEY, 0.16.2) ────────────────────────────────────\n\n/**\n * Sentinel persisted to `job.concurrency_key_template` / `dedupe_key_template`\n * when the authored `key` is a function rather than a `{{field}}` template.\n *\n * Why a sentinel (not `null`): the collision/dedupe paths in both backends gate\n * on `definition.concurrencyKeyTemplate != null`. A function key persisted as\n * `null` (the pre-0.16.2 bug) left those columns empty, so `collisionMode` /\n * the dedupe window never engaged — the job ran with NO key. A stable sentinel\n * keeps the column non-null (path engages) AND keeps the definition-hash gate\n * (`upsertJobRows`' `IS DISTINCT FROM` clause) stable across boots, since the\n * function identity itself can't be hashed. `start()` detects the sentinel and\n * re-resolves the live function from `JOB_HANDLER_REGISTRY`.\n *\n * Chosen as an angle-bracketed token so it can never collide with a real\n * `{{field}}` template (which never contains a literal `<`).\n */\nexport const FN_KEY_SENTINEL = '<fn>';\n\n/**\n * Registration-time projection: collapse an authored `JobKeySelector` to the\n * string stored in the `job` definition row. A string template is stored\n * verbatim; a function is stored as `FN_KEY_SENTINEL`; absence stays `null`.\n */\nexport function keySelectorToTemplate(\n key: JobKeySelector<unknown> | undefined,\n): string | null {\n if (typeof key === 'string') return key;\n if (typeof key === 'function') return FN_KEY_SENTINEL;\n return null;\n}\n\n/** Which meta policy a key belongs to — selects the live fn at `start()`. */\nexport type KeyKind = 'concurrency' | 'dedupe';\n\n/**\n * `start()`-time resolution shared by every backend. Turns the persisted\n * template column into the concrete per-run key for the given payload.\n *\n * - `template == null` → `null` (no key; caller skips the collision/dedupe path).\n * - `template === FN_KEY_SENTINEL` → look the live `@JobHandler` meta up in\n * `JOB_HANDLER_REGISTRY`, pull `meta[kind].key`, and invoke it against the\n * payload. The registry is the runtime source of truth (the worker already\n * resolves handler classes the same way), so the function survives the DB\n * round-trip even though it can't be persisted.\n * - otherwise → a `{{field}}` template, evaluated via the injected\n * `evaluateTemplate` (each backend passes its own copy to avoid a runtime\n * import cycle).\n *\n * Throws `JobKeyFunctionUnavailableError` if the sentinel is present but no\n * live function can be found (e.g. the registry was reset, or a function key\n * was persisted by a newer build and read by an older one). Failing loud beats\n * silently degrading to no-key — the exact regression this fix exists to kill.\n */\nexport function resolveJobKey(\n kind: KeyKind,\n type: string,\n template: string | null,\n payload: Record<string, unknown>,\n evaluateTemplate: (template: string, payload: Record<string, unknown>) => string,\n): string | null {\n if (template == null) return null;\n if (template !== FN_KEY_SENTINEL) return evaluateTemplate(template, payload);\n\n const meta = JOB_HANDLER_REGISTRY.get(type)?.meta;\n const key = (meta?.[kind] as { key?: unknown } | undefined)?.key;\n if (typeof key !== 'function') {\n throw new JobKeyFunctionUnavailableError(type, kind);\n }\n return (key as (input: unknown) => string)(payload);\n}\n\n/**\n * Raised when a `${FN_KEY_SENTINEL}` template is read but the live function\n * key is missing from `JOB_HANDLER_REGISTRY`. Kept here (not in `jobs-errors`)\n * so `job-handler.base` stays import-cycle-free.\n */\nexport class JobKeyFunctionUnavailableError extends Error {\n constructor(\n readonly jobType: string,\n readonly kind: KeyKind,\n ) {\n super(\n `[jobs] ${kind} key for job '${jobType}' was persisted as a function ` +\n `sentinel ('${FN_KEY_SENTINEL}') but no live function is registered ` +\n `for it. The @JobHandler must be imported before start() so its meta ` +\n `is in JOB_HANDLER_REGISTRY.`,\n );\n this.name = 'JobKeyFunctionUnavailableError';\n }\n}\n"],"mappings":";;;;;AA6BO,IAAK,oBAAL,kBAAKA,uBAAL;AACL,EAAAA,mBAAA,eAAY;AACZ,EAAAA,mBAAA,YAAS;AACT,EAAAA,mBAAA,aAAU;AAHA,SAAAA;AAAA,GAAA;AAsIL,IAAe,iBAAf,MAAyD;AAEhE;AASO,IAAM,uBAAuB,oBAAI,IAOtC;AAMK,IAAM,2BAA2B,OAAO,IAAI,SAAS,QAAQ,kBAAkB,CAAC;AAehF,SAAS,WACd,MACA,MACgB;AAChB,SAAO,CAAC,WAAW;AACjB,QAAI,qBAAqB,IAAI,IAAI,GAAG;AAClC,YAAM,MAAM,QAAQ,IAAI;AACxB,UAAI,QAAQ,cAAc;AACxB,cAAM,IAAI;AAAA,UACR,qDAAqD,IAAI;AAAA,QAE3D;AAAA,MACF;AACA,UAAI,QAAQ,QAAQ;AAElB,gBAAQ;AAAA,UACN,qDAAqD,IAAI;AAAA,QAE3D;AAAA,MACF;AAAA,IACF;AAEA,YAAQ,eAAe,0BAA0B,EAAE,MAAM,KAAK,GAAG,MAAM;AACvE,yBAAqB,IAAI,MAAM;AAAA,MAC7B;AAAA,MACA;AAAA,MACA,cAAc;AAAA,IAGhB,CAAC;AAAA,EACH;AACF;AAuBO,IAAU;AAAA,CAAV,CAAUC,qBAAV;AAEE,WAAS,SAAiC;AAC/C,WAAO,MAAM,KAAK,qBAAqB,OAAO,CAAC;AAAA,EACjD;AAFO,EAAAA,iBAAS;AAKT,WAAS,IAAI,MAAgD;AAClE,WAAO,qBAAqB,IAAI,IAAI;AAAA,EACtC;AAFO,EAAAA,iBAAS;AAAA,GAPD;AA8BV,IAAM,kBAAkB;AAOxB,SAAS,sBACd,KACe;AACf,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,MAAI,OAAO,QAAQ,WAAY,QAAO;AACtC,SAAO;AACT;AAwBO,SAAS,cACd,MACA,MACA,UACA,SACA,kBACe;AACf,MAAI,YAAY,KAAM,QAAO;AAC7B,MAAI,aAAa,gBAAiB,QAAO,iBAAiB,UAAU,OAAO;AAE3E,QAAM,OAAO,qBAAqB,IAAI,IAAI,GAAG;AAC7C,QAAM,MAAO,OAAO,IAAI,GAAqC;AAC7D,MAAI,OAAO,QAAQ,YAAY;AAC7B,UAAM,IAAI,+BAA+B,MAAM,IAAI;AAAA,EACrD;AACA,SAAQ,IAAmC,OAAO;AACpD;AAOO,IAAM,iCAAN,cAA6C,MAAM;AAAA,EACxD,YACW,SACA,MACT;AACA;AAAA,MACE,UAAU,IAAI,iBAAiB,OAAO,4CACtB,eAAe;AAAA,IAGjC;AARS;AACA;AAQT,SAAK,OAAO;AAAA,EACd;AAAA,EAVW;AAAA,EACA;AAUb;","names":["ParentClosePolicy","HandlerRegistry"]}
@@ -1,18 +1,18 @@
1
1
  import {
2
2
  DrizzleIntegrationRunRecorder
3
- } from "./chunk-SR7F3TJY.js";
3
+ } from "./chunk-YK5JEVLX.js";
4
4
  import {
5
5
  MemoryRunRecorder
6
6
  } from "./chunk-EO2QPOKH.js";
7
7
  import {
8
8
  PostgresCursorStore
9
- } from "./chunk-DCCZB4UC.js";
9
+ } from "./chunk-XWBK3XJK.js";
10
10
  import {
11
11
  MemoryCursorStore
12
12
  } from "./chunk-AHV4GDYM.js";
13
13
  import {
14
14
  DeepEqualDiffer
15
- } from "./chunk-36U5UGIO.js";
15
+ } from "./chunk-JEINYUJH.js";
16
16
  import {
17
17
  INTEGRATION_CURSOR_STORE,
18
18
  INTEGRATION_FIELD_DIFFER,
@@ -34,7 +34,13 @@ var IntegrationModule = class {
34
34
  { provide: INTEGRATION_MULTI_TENANT, useValue: multiTenant },
35
35
  // Default differ — consumers can override by binding a different
36
36
  // `IFieldDiffer<T>` to `INTEGRATION_FIELD_DIFFER` in their feature module.
37
- { provide: INTEGRATION_FIELD_DIFFER, useValue: new DeepEqualDiffer() }
37
+ // DIFFER-UNIGNORE: `options.differ` (ignore/unignore) is threaded here so
38
+ // a consumer can declare a default-ignored column (e.g. `deletedAt`) as
39
+ // domain data for their entities without binding a bespoke differ.
40
+ {
41
+ provide: INTEGRATION_FIELD_DIFFER,
42
+ useValue: new DeepEqualDiffer(options.differ ?? {})
43
+ }
38
44
  ];
39
45
  const backendProviders = options.backend === "memory" ? [
40
46
  // Wired as singletons via `useValue` so tests can pull
@@ -78,4 +84,4 @@ IntegrationModule = __decorateClass([
78
84
  export {
79
85
  IntegrationModule
80
86
  };
81
- //# sourceMappingURL=chunk-P3AYBRP6.js.map
87
+ //# sourceMappingURL=chunk-ATVGYF3D.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../runtime/subsystems/integration/integration.module.ts"],"sourcesContent":["/**\n * IntegrationModule — `DynamicModule.forRoot({ backend, multiTenant? })` factory\n * wiring the integration subsystem's substrate (SYNC-6, ADR-008 subsystem pattern).\n *\n * ## What this module provides\n *\n * - `INTEGRATION_CURSOR_STORE` — Drizzle or Memory cursor store\n * - `INTEGRATION_RUN_RECORDER` — Drizzle or Memory run recorder\n * - `INTEGRATION_FIELD_DIFFER` — default `DeepEqualDiffer`\n * - `INTEGRATION_MULTI_TENANT` — resolved boolean flag (defaults to false)\n * - `INTEGRATION_MODULE_OPTIONS` — the options object itself, for backends\n * that need to inspect config at construction time\n *\n * ## What this module does NOT provide\n *\n * - `INTEGRATION_CHANGE_SOURCE` — per-provider per-entity; consumer binds in\n * their feature module (e.g. `OpportunityIntegrationModule` provides a\n * `SalesforceOpportunityChangeSource`). Loopback suppression — when\n * needed — is composed into the primitive's middleware chain via\n * `createLoopbackMiddleware(store)` (#226-5 / ADR-033); the\n * orchestrator no longer accepts a fingerprint store directly.\n * - `INTEGRATION_SINK` — per canonical entity; consumer binds in their feature\n * module.\n * - `ExecuteIntegrationUseCase` — registered by the feature module alongside\n * its source + sink bindings. Providing the orchestrator here would\n * force Nest to resolve INTEGRATION_CHANGE_SOURCE + INTEGRATION_SINK at module\n * compile time, which fails when the feature module hasn't been\n * imported yet. Consumers register `ExecuteIntegrationUseCase` in the same\n * `providers` array as their source + sink so resolution is local\n * to where all three are bound.\n *\n * Same shape as `EventsModule.forRoot` — the module wires the bus; you\n * bring your own handlers. Here: the module wires the substrate; you\n * bring your own source + sink.\n *\n * ## Usage\n *\n * ```ts\n * // AppModule — single source of truth for backend + multi-tenancy.\n * @Module({\n * imports: [IntegrationModule.forRoot({ backend: 'drizzle' })],\n * })\n * export class AppModule {}\n *\n * // Per-entity feature module — binds source + sink, gets the\n * // orchestrator for free.\n * @Module({\n * providers: [\n * { provide: INTEGRATION_CHANGE_SOURCE, useClass: SalesforceOpportunitySource },\n * { provide: INTEGRATION_SINK, useClass: OpportunityIntegrationSink },\n * ExecuteIntegrationUseCase,\n * ],\n * })\n * export class OpportunityIntegrationModule {\n * constructor(\n * private readonly execute: ExecuteIntegrationUseCase<CanonicalOpportunity>,\n * ) {}\n * }\n * ```\n *\n * `global: true` means feature modules do not need to re-import\n * `IntegrationModule` — the substrate tokens are available project-wide.\n */\nimport { Module, type DynamicModule, type Provider } from '@nestjs/common';\nimport {\n INTEGRATION_CURSOR_STORE,\n INTEGRATION_FIELD_DIFFER,\n INTEGRATION_MODULE_OPTIONS,\n INTEGRATION_MULTI_TENANT,\n INTEGRATION_RUN_RECORDER,\n} from './integration.tokens';\nimport { MemoryCursorStore } from './integration-cursor-store.memory-backend';\nimport { MemoryRunRecorder } from './integration-run-recorder.memory-backend';\nimport { PostgresCursorStore } from './integration-cursor-store.drizzle-backend';\nimport { DrizzleIntegrationRunRecorder } from './integration-run-recorder.drizzle-backend';\nimport { DeepEqualDiffer, type DeepEqualDifferOptions } from './deep-equal.differ';\n\nexport interface IntegrationModuleOptions {\n /**\n * Backend selection. `drizzle` wires the Postgres cursor store +\n * run-log recorder; `memory` wires in-memory doubles suitable for\n * tests + local dev.\n */\n backend: 'drizzle' | 'memory';\n\n /**\n * Multi-tenancy opt-in (SYNC-6).\n *\n * When `true`, every call to the orchestrator + both Drizzle backends\n * must supply a non-null `tenantId`; missing values throw\n * `MissingTenantIdError`. Defense-in-depth: the orchestrator rejects\n * at entry (no dangling `status=running` rows) AND the Drizzle\n * backends reject at their write boundary (belt-and-braces for any\n * path that bypasses the orchestrator). Both sites use the shared\n * `assertTenantId` helper so error messages match.\n *\n * Memory backends accept `tenantId` unconditionally — their state is\n * process-local; cross-tenant isolation there is not meaningful.\n *\n * Defaults to `false`.\n */\n multiTenant?: boolean;\n\n /**\n * Default-differ configuration (DIFFER-UNIGNORE, 0.17.1). Threaded into the\n * `DeepEqualDiffer` bound to `INTEGRATION_FIELD_DIFFER`. Omit for the\n * historical behaviour (the default ignore list, unchanged).\n *\n * Mirrors `DeepEqualDifferOptions`:\n * - `ignore` — extra field names to ignore (merged with the defaults).\n * - `unignore` — default-ignored field names to RE-include as domain data\n * (e.g. `['deletedAt']` for an entity whose `deletedAt` is a\n * vendor-observed retraction tombstone, not row metadata — swe-brain\n * ADR-0008 §1). Subtracted after the merge, so it wins.\n *\n * A feature module that binds its own `IFieldDiffer<T>` to\n * `INTEGRATION_FIELD_DIFFER` overrides this entirely (per-entity escape hatch\n * unchanged).\n */\n differ?: DeepEqualDifferOptions;\n}\n\n@Module({})\nexport class IntegrationModule {\n static forRoot(options: IntegrationModuleOptions): DynamicModule {\n const multiTenant = options.multiTenant ?? false;\n\n const sharedProviders: Provider[] = [\n { provide: INTEGRATION_MODULE_OPTIONS, useValue: options },\n { provide: INTEGRATION_MULTI_TENANT, useValue: multiTenant },\n // Default differ — consumers can override by binding a different\n // `IFieldDiffer<T>` to `INTEGRATION_FIELD_DIFFER` in their feature module.\n // DIFFER-UNIGNORE: `options.differ` (ignore/unignore) is threaded here so\n // a consumer can declare a default-ignored column (e.g. `deletedAt`) as\n // domain data for their entities without binding a bespoke differ.\n {\n provide: INTEGRATION_FIELD_DIFFER,\n useValue: new DeepEqualDiffer(options.differ ?? {}),\n },\n ];\n\n const backendProviders: Provider[] =\n options.backend === 'memory'\n ? [\n // Wired as singletons via `useValue` so tests can pull\n // them out via `moduleRef.get(MemoryCursorStore)` for\n // direct assertions. Matches JOB-4 / MemoryJobStore shape.\n { provide: MemoryCursorStore, useValue: new MemoryCursorStore() },\n {\n provide: INTEGRATION_CURSOR_STORE,\n useExisting: MemoryCursorStore,\n },\n { provide: MemoryRunRecorder, useValue: new MemoryRunRecorder() },\n {\n provide: INTEGRATION_RUN_RECORDER,\n useExisting: MemoryRunRecorder,\n },\n ]\n : [\n // Drizzle backends — injected with DRIZZLE (provided by the\n // consumer's DrizzleModule) + the INTEGRATION_MULTI_TENANT flag\n // we bound above.\n { provide: INTEGRATION_CURSOR_STORE, useClass: PostgresCursorStore },\n { provide: INTEGRATION_RUN_RECORDER, useClass: DrizzleIntegrationRunRecorder },\n ];\n\n return {\n module: IntegrationModule,\n global: true,\n providers: [...sharedProviders, ...backendProviders],\n exports: [\n INTEGRATION_MODULE_OPTIONS,\n INTEGRATION_MULTI_TENANT,\n INTEGRATION_FIELD_DIFFER,\n INTEGRATION_CURSOR_STORE,\n INTEGRATION_RUN_RECORDER,\n ],\n };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA+DA,SAAS,cAAiD;AA4DnD,IAAM,oBAAN,MAAwB;AAAA,EAC7B,OAAO,QAAQ,SAAkD;AAC/D,UAAM,cAAc,QAAQ,eAAe;AAE3C,UAAM,kBAA8B;AAAA,MAClC,EAAE,SAAS,4BAA4B,UAAU,QAAQ;AAAA,MACzD,EAAE,SAAS,0BAA0B,UAAU,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAM3D;AAAA,QACE,SAAS;AAAA,QACT,UAAU,IAAI,gBAAgB,QAAQ,UAAU,CAAC,CAAC;AAAA,MACpD;AAAA,IACF;AAEA,UAAM,mBACJ,QAAQ,YAAY,WAChB;AAAA;AAAA;AAAA;AAAA,MAIE,EAAE,SAAS,mBAAmB,UAAU,IAAI,kBAAkB,EAAE;AAAA,MAChE;AAAA,QACE,SAAS;AAAA,QACT,aAAa;AAAA,MACf;AAAA,MACA,EAAE,SAAS,mBAAmB,UAAU,IAAI,kBAAkB,EAAE;AAAA,MAChE;AAAA,QACE,SAAS;AAAA,QACT,aAAa;AAAA,MACf;AAAA,IACF,IACA;AAAA;AAAA;AAAA;AAAA,MAIE,EAAE,SAAS,0BAA0B,UAAU,oBAAoB;AAAA,MACnE,EAAE,SAAS,0BAA0B,UAAU,8BAA8B;AAAA,IAC/E;AAEN,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW,CAAC,GAAG,iBAAiB,GAAG,gBAAgB;AAAA,MACnD,SAAS;AAAA,QACP;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAxDa,oBAAN;AAAA,EADN,OAAO,CAAC,CAAC;AAAA,GACG;","names":[]}
@@ -1,15 +1,18 @@
1
- import {
2
- MemoryJobOrchestrator
3
- } from "./chunk-BULPAAD3.js";
4
1
  import {
5
2
  DrizzleJobRunService
6
- } from "./chunk-3VEVGL74.js";
3
+ } from "./chunk-VNBC3VXM.js";
7
4
  import {
8
5
  MemoryJobRunService
9
- } from "./chunk-CDLWYZVQ.js";
6
+ } from "./chunk-BHZP6LOV.js";
10
7
  import {
11
8
  DrizzleJobStepService
12
9
  } from "./chunk-DV4RV2DC.js";
10
+ import {
11
+ DrizzleJobOrchestrator
12
+ } from "./chunk-E6PLM6QG.js";
13
+ import {
14
+ MemoryJobOrchestrator
15
+ } from "./chunk-VQOAATIG.js";
13
16
  import {
14
17
  MemoryJobStepService
15
18
  } from "./chunk-PNZSGAB2.js";
@@ -21,9 +24,6 @@ import {
21
24
  BULLMQ_RESOLVED_CONFIG,
22
25
  resolveBullMqConfig
23
26
  } from "./chunk-I6MVCB5A.js";
24
- import {
25
- DrizzleJobOrchestrator
26
- } from "./chunk-OTR44OH6.js";
27
27
  import {
28
28
  JOBS_LISTEN_NOTIFY,
29
29
  JOBS_MULTI_TENANT,
@@ -114,4 +114,4 @@ JobsDomainModule = __decorateClass([
114
114
  export {
115
115
  JobsDomainModule
116
116
  };
117
- //# sourceMappingURL=chunk-3MAZ4TQH.js.map
117
+ //# sourceMappingURL=chunk-AZLUWG5S.js.map
@@ -1,22 +1,22 @@
1
+ import {
2
+ clampEventLimit,
3
+ decodeEventCursor,
4
+ encodeEventCursor
5
+ } from "./chunk-UQ5EHOH2.js";
1
6
  import {
2
7
  EVENTS_WAKE_CHANNEL,
3
8
  PgNotifyListener,
4
9
  pgNotify
5
- } from "./chunk-MYQIQ27N.js";
10
+ } from "./chunk-Q6LRJ4VI.js";
11
+ import {
12
+ EVENTS_MODULE_OPTIONS
13
+ } from "./chunk-H5NH7KPE.js";
6
14
  import {
7
15
  BRIDGE_OUTBOX_DRAIN_HOOK
8
16
  } from "./chunk-4LH67P4U.js";
9
17
  import {
10
18
  domainEvents
11
19
  } from "./chunk-OFRRBC7M.js";
12
- import {
13
- clampEventLimit,
14
- decodeEventCursor,
15
- encodeEventCursor
16
- } from "./chunk-UQ5EHOH2.js";
17
- import {
18
- EVENTS_MODULE_OPTIONS
19
- } from "./chunk-H5NH7KPE.js";
20
20
  import {
21
21
  DRIZZLE
22
22
  } from "./chunk-U64T4YZE.js";
@@ -393,4 +393,4 @@ DrizzleEventBus = __decorateClass([
393
393
  export {
394
394
  DrizzleEventBus
395
395
  };
396
- //# sourceMappingURL=chunk-UTNWFHJF.js.map
396
+ //# sourceMappingURL=chunk-B34G6PHD.js.map
@@ -1,15 +1,15 @@
1
- import {
2
- clampLimit,
3
- decodeKeysetCursor,
4
- encodeKeysetCursor,
5
- toJobRunSummary
6
- } from "./chunk-L3LZWWSX.js";
7
1
  import {
8
2
  MemoryJobStore
9
3
  } from "./chunk-SNQ3TOWP.js";
10
4
  import {
11
5
  MissingTenantIdError
12
6
  } from "./chunk-T4BIIU5E.js";
7
+ import {
8
+ clampLimit,
9
+ decodeKeysetCursor,
10
+ encodeKeysetCursor,
11
+ toJobRunSummary
12
+ } from "./chunk-L3LZWWSX.js";
13
13
  import {
14
14
  JOBS_MULTI_TENANT,
15
15
  JOB_ORCHESTRATOR
@@ -209,4 +209,4 @@ function compareBy(a, b, order) {
209
209
  export {
210
210
  MemoryJobRunService
211
211
  };
212
- //# sourceMappingURL=chunk-CDLWYZVQ.js.map
212
+ //# sourceMappingURL=chunk-BHZP6LOV.js.map
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  JOB_RUN_SERVICE
3
3
  } from "./chunk-ZPL74UQN.js";
4
- import {
5
- BRIDGE_DELIVERY_REPO
6
- } from "./chunk-4LH67P4U.js";
7
4
  import {
8
5
  EVENT_READ_PORT
9
6
  } from "./chunk-H5NH7KPE.js";
7
+ import {
8
+ BRIDGE_DELIVERY_REPO
9
+ } from "./chunk-4LH67P4U.js";
10
10
  import {
11
11
  INTEGRATION_CURSOR_STORE,
12
12
  INTEGRATION_RUN_RECORDER
@@ -181,4 +181,4 @@ ObservabilityService = __decorateClass([
181
181
  export {
182
182
  ObservabilityService
183
183
  };
184
- //# sourceMappingURL=chunk-W2UIDI3R.js.map
184
+ //# sourceMappingURL=chunk-CLWBNXKF.js.map
@@ -5,6 +5,14 @@ import {
5
5
  JobTypeNotFoundError,
6
6
  MissingTenantIdError
7
7
  } from "./chunk-T4BIIU5E.js";
8
+ import {
9
+ JOBS_WAKE_CHANNEL,
10
+ pgNotify
11
+ } from "./chunk-Q6LRJ4VI.js";
12
+ import {
13
+ keySelectorToTemplate,
14
+ resolveJobKey
15
+ } from "./chunk-7P5ODGLA.js";
8
16
  import {
9
17
  JOBS_LISTEN_NOTIFY,
10
18
  JOBS_MULTI_TENANT
@@ -14,10 +22,6 @@ import {
14
22
  jobSteps,
15
23
  jobs
16
24
  } from "./chunk-OKXZ63IA.js";
17
- import {
18
- JOBS_WAKE_CHANNEL,
19
- pgNotify
20
- } from "./chunk-MYQIQ27N.js";
21
25
  import {
22
26
  DRIZZLE
23
27
  } from "./chunk-U64T4YZE.js";
@@ -81,7 +85,13 @@ var DrizzleJobOrchestrator = class {
81
85
  if (!def) throw new JobTypeNotFoundError(type);
82
86
  const definition = def;
83
87
  if (definition.dedupeKeyTemplate && definition.dedupeWindowMs) {
84
- const dedupeKey2 = evaluateKeyTemplate(definition.dedupeKeyTemplate, payload);
88
+ const dedupeKey2 = resolveJobKey(
89
+ "dedupe",
90
+ type,
91
+ definition.dedupeKeyTemplate,
92
+ payload,
93
+ evaluateKeyTemplate
94
+ );
85
95
  const windowStart = new Date(Date.now() - definition.dedupeWindowMs);
86
96
  const existing = await client.select().from(jobRuns).where(
87
97
  and(
@@ -98,9 +108,12 @@ var DrizzleJobOrchestrator = class {
98
108
  }
99
109
  let concurrencyKey = null;
100
110
  if (definition.concurrencyKeyTemplate) {
101
- concurrencyKey = evaluateKeyTemplate(
111
+ concurrencyKey = resolveJobKey(
112
+ "concurrency",
113
+ type,
102
114
  definition.concurrencyKeyTemplate,
103
- payload
115
+ payload,
116
+ evaluateKeyTemplate
104
117
  );
105
118
  const inFlight = await client.select().from(jobRuns).where(
106
119
  and(
@@ -136,7 +149,13 @@ var DrizzleJobOrchestrator = class {
136
149
  }
137
150
  rootRunId = parent.rootRunId;
138
151
  }
139
- const dedupeKey = definition.dedupeKeyTemplate ? evaluateKeyTemplate(definition.dedupeKeyTemplate, payload) : null;
152
+ const dedupeKey = resolveJobKey(
153
+ "dedupe",
154
+ type,
155
+ definition.dedupeKeyTemplate,
156
+ payload,
157
+ evaluateKeyTemplate
158
+ );
140
159
  const [inserted] = await client.insert(jobRuns).values({
141
160
  id: newId,
142
161
  jobType: type,
@@ -293,11 +312,13 @@ var DrizzleJobOrchestrator = class {
293
312
  backoff: "fixed",
294
313
  baseMs: 0
295
314
  };
296
- const concurrencyKeyTemplate = meta.concurrency?.key;
297
- const concurrencyKeyTemplateStr = typeof concurrencyKeyTemplate === "string" ? concurrencyKeyTemplate : null;
315
+ const concurrencyKeyTemplateStr = keySelectorToTemplate(
316
+ meta.concurrency?.key
317
+ );
298
318
  const collisionMode = meta.concurrency?.collisionMode ?? "queue";
299
- const dedupeKeyTemplate = meta.dedupe?.key;
300
- const dedupeKeyTemplateStr = typeof dedupeKeyTemplate === "string" ? dedupeKeyTemplate : null;
319
+ const dedupeKeyTemplateStr = keySelectorToTemplate(
320
+ meta.dedupe?.key
321
+ );
301
322
  const dedupeWindowMs = meta.dedupe?.windowMs ?? null;
302
323
  const timeoutMs = meta.timeoutMs ?? null;
303
324
  const replayFrom = meta.replayFrom ?? "last_checkpoint";
@@ -372,4 +393,4 @@ export {
372
393
  evaluateKeyTemplate,
373
394
  DrizzleJobOrchestrator
374
395
  };
375
- //# sourceMappingURL=chunk-OTR44OH6.js.map
396
+ //# sourceMappingURL=chunk-E6PLM6QG.js.map