@pattern-stack/codegen 0.17.0 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/consumer-skills/integration/audit-and-detection.md +29 -4
  3. package/dist/{chunk-UTNWFHJF.js → chunk-4PFF3ED4.js} +4 -4
  4. package/dist/{chunk-CO6LUM72.js → chunk-7P5ODGLA.js} +34 -2
  5. package/dist/chunk-7P5ODGLA.js.map +1 -0
  6. package/dist/{chunk-CDLWYZVQ.js → chunk-BHZP6LOV.js} +7 -7
  7. package/dist/{chunk-4MVGAMUA.js → chunk-BK5ICA2F.js} +4 -4
  8. package/dist/{chunk-BULPAAD3.js → chunk-DUMI2J5M.js} +42 -11
  9. package/dist/chunk-DUMI2J5M.js.map +1 -0
  10. package/dist/{chunk-RHYNACZS.js → chunk-EJBK7I4F.js} +3 -3
  11. package/dist/{chunk-OTR44OH6.js → chunk-FVNAU7VO.js} +30 -9
  12. package/dist/chunk-FVNAU7VO.js.map +1 -0
  13. package/dist/{chunk-OITTYGJS.js → chunk-FWRL7BZ5.js} +4 -4
  14. package/dist/{chunk-3MAZ4TQH.js → chunk-HOIRY5XP.js} +13 -13
  15. package/dist/{chunk-GJDEPTPY.js → chunk-HPS554L4.js} +10 -10
  16. package/dist/{chunk-P3AYBRP6.js → chunk-JA7GJDNI.js} +16 -10
  17. package/dist/chunk-JA7GJDNI.js.map +1 -0
  18. package/dist/{chunk-36U5UGIO.js → chunk-JEINYUJH.js} +8 -5
  19. package/dist/chunk-JEINYUJH.js.map +1 -0
  20. package/dist/{chunk-K2I6XIK5.js → chunk-KSTZIULO.js} +4 -4
  21. package/dist/{chunk-Z7PQCAVK.js → chunk-LQ6PYFU6.js} +4 -4
  22. package/dist/{chunk-L3VJ47BU.js → chunk-PSDVGPQR.js} +5 -5
  23. package/dist/{chunk-DTXH24LR.js → chunk-SFQRETXJ.js} +2 -2
  24. package/dist/{chunk-NXNVTXKG.js → chunk-SGSWVNNB.js} +5 -5
  25. package/dist/{chunk-7LKAMLV4.js → chunk-T6SCOJF4.js} +4 -4
  26. package/dist/{chunk-OGIZXGPY.js → chunk-TDEHU73T.js} +4 -4
  27. package/dist/{chunk-3VEVGL74.js → chunk-VNBC3VXM.js} +4 -4
  28. package/dist/{chunk-DCCZB4UC.js → chunk-XWBK3XJK.js} +4 -4
  29. package/dist/{chunk-4GLNY5V6.js → chunk-Y7GDG744.js} +5 -5
  30. package/dist/{chunk-SR7F3TJY.js → chunk-YK5JEVLX.js} +4 -4
  31. package/dist/{job-orchestrator.protocol-DubMVbm9.d.ts → job-orchestrator.protocol-ZuJ3ow-O.d.ts} +77 -3
  32. package/dist/runtime/base-classes/index.js +19 -19
  33. package/dist/runtime/shared/openapi/index.js +7 -7
  34. package/dist/runtime/shared/openapi/registry.js +2 -2
  35. package/dist/runtime/subsystems/auth/auth.module.js +2 -2
  36. package/dist/runtime/subsystems/auth/index.js +5 -5
  37. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.d.ts +1 -1
  38. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js +2 -2
  39. package/dist/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.js +2 -2
  40. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js +4 -4
  41. package/dist/runtime/subsystems/bridge/bridge.module.d.ts +1 -1
  42. package/dist/runtime/subsystems/bridge/bridge.module.js +15 -15
  43. package/dist/runtime/subsystems/bridge/event-flow.service.d.ts +1 -1
  44. package/dist/runtime/subsystems/bridge/index.d.ts +1 -1
  45. package/dist/runtime/subsystems/bridge/index.js +17 -17
  46. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +2 -2
  47. package/dist/runtime/subsystems/events/event-bus.memory-backend.js +2 -2
  48. package/dist/runtime/subsystems/events/events.module.js +4 -4
  49. package/dist/runtime/subsystems/events/index.js +4 -4
  50. package/dist/runtime/subsystems/index.d.ts +1 -1
  51. package/dist/runtime/subsystems/index.js +90 -90
  52. package/dist/runtime/subsystems/integration/deep-equal.differ.d.ts +19 -0
  53. package/dist/runtime/subsystems/integration/deep-equal.differ.js +1 -1
  54. package/dist/runtime/subsystems/integration/execute-integration.use-case.js +2 -2
  55. package/dist/runtime/subsystems/integration/index.js +24 -24
  56. package/dist/runtime/subsystems/integration/integration-cursor-store.drizzle-backend.js +2 -2
  57. package/dist/runtime/subsystems/integration/integration-run-recorder.drizzle-backend.js +2 -2
  58. package/dist/runtime/subsystems/integration/integration.module.d.ts +20 -0
  59. package/dist/runtime/subsystems/integration/integration.module.js +6 -6
  60. package/dist/runtime/subsystems/jobs/index.d.ts +1 -1
  61. package/dist/runtime/subsystems/jobs/index.js +32 -32
  62. package/dist/runtime/subsystems/jobs/job-handler.base.d.ts +1 -1
  63. package/dist/runtime/subsystems/jobs/job-handler.base.js +11 -3
  64. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.d.ts +1 -1
  65. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +5 -4
  66. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -1
  67. package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.d.ts +1 -1
  68. package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.js +2 -1
  69. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.d.ts +11 -1
  70. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js +2 -2
  71. package/dist/runtime/subsystems/jobs/job-orchestrator.protocol.d.ts +1 -1
  72. package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.d.ts +1 -1
  73. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.d.ts +1 -1
  74. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +2 -2
  75. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.d.ts +1 -1
  76. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +2 -2
  77. package/dist/runtime/subsystems/jobs/job-run-service.protocol.d.ts +1 -1
  78. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.d.ts +1 -1
  79. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +1 -1
  80. package/dist/runtime/subsystems/jobs/job-worker.d.ts +1 -1
  81. package/dist/runtime/subsystems/jobs/job-worker.js +2 -2
  82. package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +1 -1
  83. package/dist/runtime/subsystems/jobs/job-worker.module.js +10 -10
  84. package/dist/runtime/subsystems/jobs/jobs-domain.module.js +8 -8
  85. package/dist/runtime/subsystems/jobs/jobs-errors.d.ts +1 -1
  86. package/dist/runtime/subsystems/observability/index.d.ts +1 -1
  87. package/dist/runtime/subsystems/observability/observability.protocol.d.ts +1 -1
  88. package/dist/runtime/subsystems/observability/observability.service.d.ts +1 -1
  89. package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.d.ts +1 -1
  90. package/dist/runtime/subsystems/observability/reporters/index.d.ts +1 -1
  91. package/dist/runtime/subsystems/storage/index.js +4 -4
  92. package/dist/runtime/subsystems/storage/storage.module.js +2 -2
  93. package/dist/src/cli/index.js +34 -12
  94. package/dist/src/cli/index.js.map +1 -1
  95. package/dist/src/index.js +9 -9
  96. package/package.json +1 -1
  97. package/runtime/subsystems/integration/deep-equal.differ.ts +34 -5
  98. package/runtime/subsystems/integration/integration.module.ts +26 -2
  99. package/runtime/subsystems/jobs/job-handler.base.ts +115 -2
  100. package/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.ts +43 -16
  101. package/runtime/subsystems/jobs/job-orchestrator.memory-backend.ts +58 -18
  102. package/templates/subsystem/integration-config/codegen-config-integration-block.ejs.t +17 -0
  103. package/dist/chunk-36U5UGIO.js.map +0 -1
  104. package/dist/chunk-BULPAAD3.js.map +0 -1
  105. package/dist/chunk-CO6LUM72.js.map +0 -1
  106. package/dist/chunk-OTR44OH6.js.map +0 -1
  107. package/dist/chunk-P3AYBRP6.js.map +0 -1
  108. /package/dist/{chunk-UTNWFHJF.js.map → chunk-4PFF3ED4.js.map} +0 -0
  109. /package/dist/{chunk-CDLWYZVQ.js.map → chunk-BHZP6LOV.js.map} +0 -0
  110. /package/dist/{chunk-4MVGAMUA.js.map → chunk-BK5ICA2F.js.map} +0 -0
  111. /package/dist/{chunk-RHYNACZS.js.map → chunk-EJBK7I4F.js.map} +0 -0
  112. /package/dist/{chunk-OITTYGJS.js.map → chunk-FWRL7BZ5.js.map} +0 -0
  113. /package/dist/{chunk-3MAZ4TQH.js.map → chunk-HOIRY5XP.js.map} +0 -0
  114. /package/dist/{chunk-GJDEPTPY.js.map → chunk-HPS554L4.js.map} +0 -0
  115. /package/dist/{chunk-K2I6XIK5.js.map → chunk-KSTZIULO.js.map} +0 -0
  116. /package/dist/{chunk-Z7PQCAVK.js.map → chunk-LQ6PYFU6.js.map} +0 -0
  117. /package/dist/{chunk-L3VJ47BU.js.map → chunk-PSDVGPQR.js.map} +0 -0
  118. /package/dist/{chunk-DTXH24LR.js.map → chunk-SFQRETXJ.js.map} +0 -0
  119. /package/dist/{chunk-NXNVTXKG.js.map → chunk-SGSWVNNB.js.map} +0 -0
  120. /package/dist/{chunk-7LKAMLV4.js.map → chunk-T6SCOJF4.js.map} +0 -0
  121. /package/dist/{chunk-OGIZXGPY.js.map → chunk-TDEHU73T.js.map} +0 -0
  122. /package/dist/{chunk-3VEVGL74.js.map → chunk-VNBC3VXM.js.map} +0 -0
  123. /package/dist/{chunk-DCCZB4UC.js.map → chunk-XWBK3XJK.js.map} +0 -0
  124. /package/dist/{chunk-4GLNY5V6.js.map → chunk-Y7GDG744.js.map} +0 -0
  125. /package/dist/{chunk-SR7F3TJY.js.map → chunk-YK5JEVLX.js.map} +0 -0
package/dist/src/index.js CHANGED
@@ -46,25 +46,25 @@ import {
46
46
  validatePatternProject
47
47
  } from "../chunk-IOQMMH6C.js";
48
48
  import "../chunk-KVOWSC5S.js";
49
- import "../chunk-P3AYBRP6.js";
49
+ import "../chunk-JA7GJDNI.js";
50
50
  import "../chunk-PRWIX6UW.js";
51
- import "../chunk-SR7F3TJY.js";
51
+ import "../chunk-AHV4GDYM.js";
52
+ import "../chunk-YK5JEVLX.js";
52
53
  import "../chunk-EO2QPOKH.js";
53
54
  import "../chunk-SQDOBLBP.js";
54
- import "../chunk-OGIZXGPY.js";
55
+ import "../chunk-4KNXX6TI.js";
56
+ import "../chunk-3CJFPU6Q.js";
57
+ import "../chunk-TDEHU73T.js";
55
58
  import "../chunk-LG57S2SC.js";
56
- import "../chunk-DCCZB4UC.js";
59
+ import "../chunk-XWBK3XJK.js";
60
+ import "../chunk-S7C6TIIF.js";
57
61
  import "../chunk-MZ6GV4YF.js";
58
62
  import "../chunk-HNWZFNKP.js";
59
- import "../chunk-AHV4GDYM.js";
60
63
  import "../chunk-43SBT72G.js";
61
64
  import "../chunk-4MF3HKJA.js";
62
65
  import "../chunk-TIZXQU26.js";
63
- import "../chunk-36U5UGIO.js";
66
+ import "../chunk-JEINYUJH.js";
64
67
  import "../chunk-5TK7MEN4.js";
65
- import "../chunk-4KNXX6TI.js";
66
- import "../chunk-3CJFPU6Q.js";
67
- import "../chunk-S7C6TIIF.js";
68
68
  import "../chunk-U64T4YZE.js";
69
69
  import "../chunk-2E224ZSN.js";
70
70
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pattern-stack/codegen",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
4
4
  "description": "Entity-driven code generation for full-stack TypeScript applications",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,7 +12,11 @@
12
12
  * `id`, `createdAt`, `updatedAt`, `deletedAt`, `type`,
13
13
  * `lastModifiedAt`, `fields`, `providerMetadata`
14
14
  * (`fields` is the EAV bag — it's diffed by the sink's EAV dual-write
15
- * path, not at the canonical-record layer.)
15
+ * path, not at the canonical-record layer.) Consumers augment the list via
16
+ * `options.ignore` and — when a default is domain data for their entity —
17
+ * REMOVE a default via `options.unignore` (e.g. an entity whose
18
+ * `deletedAt` is a vendor-observed retraction tombstone, not row metadata;
19
+ * see `DeepEqualDifferOptions.unignore`).
16
20
  *
17
21
  * 2. **`providerChangedFields` hint (CDC)** — when present, restricts the
18
22
  * comparison to the hinted field set. The hint is advisory; fields in
@@ -75,6 +79,26 @@ export interface DeepEqualDifferOptions {
75
79
  * merged (not replaced) with `DEFAULT_IGNORE_FIELDS`.
76
80
  */
77
81
  readonly ignore?: readonly string[];
82
+
83
+ /**
84
+ * Field names to REMOVE from the default ignore list — the inverse of
85
+ * `ignore`. Use this to declare that a normally-metadata column is in fact
86
+ * DOMAIN DATA for this entity and must register as a field change.
87
+ *
88
+ * The canonical case (swe-brain ADR-0008 §1, the gap this knob closes):
89
+ * `deletedAt` is in `DEFAULT_IGNORE_FIELDS` because most sinks stamp it as
90
+ * row metadata sinks own unconditionally. But an entity with
91
+ * `softDelete: false` and a domain-owned `deleted_at` carries the
92
+ * vendor-observed retraction tombstone ON the canonical record (a Slack
93
+ * `message_deleted` → `deletedAt`). Without un-ignoring it, the tombstone
94
+ * overlay diffs to `'noop'`, the upsert is skipped, and `deleted_at` never
95
+ * lands. `unignore: ['deletedAt']` makes the differ treat it as domain data.
96
+ *
97
+ * Applied AFTER `ignore` is merged, so `unignore` wins on a field listed in
98
+ * both. Subtracting a field not in the (merged) ignore set is a harmless
99
+ * no-op. Does not touch `DEFAULT_IGNORE_FIELDS` for any other instance.
100
+ */
101
+ readonly unignore?: readonly string[];
78
102
  }
79
103
 
80
104
  @Injectable()
@@ -84,11 +108,16 @@ export class DeepEqualDiffer<T extends Record<string, unknown>>
84
108
  private readonly ignore: ReadonlySet<string>;
85
109
 
86
110
  constructor(opts: DeepEqualDifferOptions = {}) {
87
- if (opts.ignore && opts.ignore.length > 0) {
88
- this.ignore = new Set([...DEFAULT_IGNORE_FIELDS, ...opts.ignore]);
89
- } else {
90
- this.ignore = DEFAULT_IGNORE_FIELDS;
111
+ const merged = new Set<string>(DEFAULT_IGNORE_FIELDS);
112
+ if (opts.ignore) {
113
+ for (const field of opts.ignore) merged.add(field);
114
+ }
115
+ // `unignore` is subtracted last so it wins over a field that also appears
116
+ // in `ignore` or the defaults — "this column is domain data here."
117
+ if (opts.unignore) {
118
+ for (const field of opts.unignore) merged.delete(field);
91
119
  }
120
+ this.ignore = merged;
92
121
  }
93
122
 
94
123
  diff(
@@ -73,7 +73,7 @@ import { MemoryCursorStore } from './integration-cursor-store.memory-backend';
73
73
  import { MemoryRunRecorder } from './integration-run-recorder.memory-backend';
74
74
  import { PostgresCursorStore } from './integration-cursor-store.drizzle-backend';
75
75
  import { DrizzleIntegrationRunRecorder } from './integration-run-recorder.drizzle-backend';
76
- import { DeepEqualDiffer } from './deep-equal.differ';
76
+ import { DeepEqualDiffer, type DeepEqualDifferOptions } from './deep-equal.differ';
77
77
 
78
78
  export interface IntegrationModuleOptions {
79
79
  /**
@@ -100,6 +100,24 @@ export interface IntegrationModuleOptions {
100
100
  * Defaults to `false`.
101
101
  */
102
102
  multiTenant?: boolean;
103
+
104
+ /**
105
+ * Default-differ configuration (DIFFER-UNIGNORE, 0.17.1). Threaded into the
106
+ * `DeepEqualDiffer` bound to `INTEGRATION_FIELD_DIFFER`. Omit for the
107
+ * historical behaviour (the default ignore list, unchanged).
108
+ *
109
+ * Mirrors `DeepEqualDifferOptions`:
110
+ * - `ignore` — extra field names to ignore (merged with the defaults).
111
+ * - `unignore` — default-ignored field names to RE-include as domain data
112
+ * (e.g. `['deletedAt']` for an entity whose `deletedAt` is a
113
+ * vendor-observed retraction tombstone, not row metadata — swe-brain
114
+ * ADR-0008 §1). Subtracted after the merge, so it wins.
115
+ *
116
+ * A feature module that binds its own `IFieldDiffer<T>` to
117
+ * `INTEGRATION_FIELD_DIFFER` overrides this entirely (per-entity escape hatch
118
+ * unchanged).
119
+ */
120
+ differ?: DeepEqualDifferOptions;
103
121
  }
104
122
 
105
123
  @Module({})
@@ -112,7 +130,13 @@ export class IntegrationModule {
112
130
  { provide: INTEGRATION_MULTI_TENANT, useValue: multiTenant },
113
131
  // Default differ — consumers can override by binding a different
114
132
  // `IFieldDiffer<T>` to `INTEGRATION_FIELD_DIFFER` in their feature module.
115
- { provide: INTEGRATION_FIELD_DIFFER, useValue: new DeepEqualDiffer() },
133
+ // DIFFER-UNIGNORE: `options.differ` (ignore/unignore) is threaded here so
134
+ // a consumer can declare a default-ignored column (e.g. `deletedAt`) as
135
+ // domain data for their entities without binding a bespoke differ.
136
+ {
137
+ provide: INTEGRATION_FIELD_DIFFER,
138
+ useValue: new DeepEqualDiffer(options.differ ?? {}),
139
+ },
116
140
  ];
117
141
 
118
142
  const backendProviders: Provider[] =
@@ -42,13 +42,33 @@ export interface RetryPolicy {
42
42
  nonRetryableErrors?: string[];
43
43
  }
44
44
 
45
+ /**
46
+ * Concurrency lane key (JOB-FN-KEY, 0.16.2).
47
+ *
48
+ * Two authoring forms, both honored end-to-end (the typed function form was
49
+ * previously dropped to `null` at registration — see `upsertJobRows` — so
50
+ * `collisionMode` silently never engaged):
51
+ *
52
+ * - **`string`** — a `{{field}}` template evaluated against the start
53
+ * payload by `evaluateKeyTemplate` (single-key substitution, no dotted
54
+ * paths). Persisted verbatim to `job.concurrency_key_template`.
55
+ * - **`(input) => string`** — an arbitrary function of the input. Persisted
56
+ * as the `FN_KEY_SENTINEL` marker so the definition-hash gate stays stable
57
+ * and the collision path engages; `start()` re-resolves the live function
58
+ * from `JOB_HANDLER_REGISTRY` and evaluates it against the payload.
59
+ *
60
+ * Both forms produce a per-lane key; same key + in-flight incumbent ⇒
61
+ * `collisionMode` ('queue' | 'reject' | 'replace') decides.
62
+ */
63
+ export type JobKeySelector<TInput> = string | ((input: TInput) => string);
64
+
45
65
  export interface ConcurrencyPolicy<TInput> {
46
- key: (input: TInput) => string;
66
+ key: JobKeySelector<TInput>;
47
67
  collisionMode: 'queue' | 'reject' | 'replace';
48
68
  }
49
69
 
50
70
  export interface DedupePolicy<TInput> {
51
- key: (input: TInput) => string;
71
+ key: JobKeySelector<TInput>;
52
72
  windowMs: number;
53
73
  }
54
74
 
@@ -245,3 +265,96 @@ export namespace HandlerRegistry {
245
265
  return JOB_HANDLER_REGISTRY.get(type);
246
266
  }
247
267
  }
268
+
269
+ // ─── Key resolution (JOB-FN-KEY, 0.16.2) ────────────────────────────────────
270
+
271
+ /**
272
+ * Sentinel persisted to `job.concurrency_key_template` / `dedupe_key_template`
273
+ * when the authored `key` is a function rather than a `{{field}}` template.
274
+ *
275
+ * Why a sentinel (not `null`): the collision/dedupe paths in both backends gate
276
+ * on `definition.concurrencyKeyTemplate != null`. A function key persisted as
277
+ * `null` (the pre-0.16.2 bug) left those columns empty, so `collisionMode` /
278
+ * the dedupe window never engaged — the job ran with NO key. A stable sentinel
279
+ * keeps the column non-null (path engages) AND keeps the definition-hash gate
280
+ * (`upsertJobRows`' `IS DISTINCT FROM` clause) stable across boots, since the
281
+ * function identity itself can't be hashed. `start()` detects the sentinel and
282
+ * re-resolves the live function from `JOB_HANDLER_REGISTRY`.
283
+ *
284
+ * Chosen as an angle-bracketed token so it can never collide with a real
285
+ * `{{field}}` template (which never contains a literal `<`).
286
+ */
287
+ export const FN_KEY_SENTINEL = '<fn>';
288
+
289
+ /**
290
+ * Registration-time projection: collapse an authored `JobKeySelector` to the
291
+ * string stored in the `job` definition row. A string template is stored
292
+ * verbatim; a function is stored as `FN_KEY_SENTINEL`; absence stays `null`.
293
+ */
294
+ export function keySelectorToTemplate(
295
+ key: JobKeySelector<unknown> | undefined,
296
+ ): string | null {
297
+ if (typeof key === 'string') return key;
298
+ if (typeof key === 'function') return FN_KEY_SENTINEL;
299
+ return null;
300
+ }
301
+
302
+ /** Which meta policy a key belongs to — selects the live fn at `start()`. */
303
+ export type KeyKind = 'concurrency' | 'dedupe';
304
+
305
+ /**
306
+ * `start()`-time resolution shared by every backend. Turns the persisted
307
+ * template column into the concrete per-run key for the given payload.
308
+ *
309
+ * - `template == null` → `null` (no key; caller skips the collision/dedupe path).
310
+ * - `template === FN_KEY_SENTINEL` → look the live `@JobHandler` meta up in
311
+ * `JOB_HANDLER_REGISTRY`, pull `meta[kind].key`, and invoke it against the
312
+ * payload. The registry is the runtime source of truth (the worker already
313
+ * resolves handler classes the same way), so the function survives the DB
314
+ * round-trip even though it can't be persisted.
315
+ * - otherwise → a `{{field}}` template, evaluated via the injected
316
+ * `evaluateTemplate` (each backend passes its own copy to avoid a runtime
317
+ * import cycle).
318
+ *
319
+ * Throws `JobKeyFunctionUnavailableError` if the sentinel is present but no
320
+ * live function can be found (e.g. the registry was reset, or a function key
321
+ * was persisted by a newer build and read by an older one). Failing loud beats
322
+ * silently degrading to no-key — the exact regression this fix exists to kill.
323
+ */
324
+ export function resolveJobKey(
325
+ kind: KeyKind,
326
+ type: string,
327
+ template: string | null,
328
+ payload: Record<string, unknown>,
329
+ evaluateTemplate: (template: string, payload: Record<string, unknown>) => string,
330
+ ): string | null {
331
+ if (template == null) return null;
332
+ if (template !== FN_KEY_SENTINEL) return evaluateTemplate(template, payload);
333
+
334
+ const meta = JOB_HANDLER_REGISTRY.get(type)?.meta;
335
+ const key = (meta?.[kind] as { key?: unknown } | undefined)?.key;
336
+ if (typeof key !== 'function') {
337
+ throw new JobKeyFunctionUnavailableError(type, kind);
338
+ }
339
+ return (key as (input: unknown) => string)(payload);
340
+ }
341
+
342
+ /**
343
+ * Raised when a `${FN_KEY_SENTINEL}` template is read but the live function
344
+ * key is missing from `JOB_HANDLER_REGISTRY`. Kept here (not in `jobs-errors`)
345
+ * so `job-handler.base` stays import-cycle-free.
346
+ */
347
+ export class JobKeyFunctionUnavailableError extends Error {
348
+ constructor(
349
+ readonly jobType: string,
350
+ readonly kind: KeyKind,
351
+ ) {
352
+ super(
353
+ `[jobs] ${kind} key for job '${jobType}' was persisted as a function ` +
354
+ `sentinel ('${FN_KEY_SENTINEL}') but no live function is registered ` +
355
+ `for it. The @JobHandler must be imported before start() so its meta ` +
356
+ `is in JOB_HANDLER_REGISTRY.`,
357
+ );
358
+ this.name = 'JobKeyFunctionUnavailableError';
359
+ }
360
+ }
@@ -36,6 +36,11 @@ import {
36
36
  import { jobSteps } from './job-orchestration.schema';
37
37
  import { JOBS_MULTI_TENANT, JOBS_LISTEN_NOTIFY } from './jobs-domain.tokens';
38
38
  import { JOBS_WAKE_CHANNEL, pgNotify } from './pg-notify';
39
+ import {
40
+ keySelectorToTemplate,
41
+ resolveJobKey,
42
+ type JobKeySelector,
43
+ } from './job-handler.base';
39
44
 
40
45
  /**
41
46
  * Terminal statuses — transitions into these are final. Used by `cancel`
@@ -140,9 +145,17 @@ export class DrizzleJobOrchestrator implements IJobOrchestrator {
140
145
  if (!def) throw new JobTypeNotFoundError(type);
141
146
  const definition = def as JobDefinitionRow;
142
147
 
143
- // 1b. Dedupe check.
148
+ // 1b. Dedupe check. JOB-FN-KEY: `resolveJobKey` honors both the `{{field}}`
149
+ // template AND a function key persisted as `FN_KEY_SENTINEL` (re-resolved
150
+ // live from JOB_HANDLER_REGISTRY).
144
151
  if (definition.dedupeKeyTemplate && definition.dedupeWindowMs) {
145
- const dedupeKey = evaluateKeyTemplate(definition.dedupeKeyTemplate, payload);
152
+ const dedupeKey = resolveJobKey(
153
+ 'dedupe',
154
+ type,
155
+ definition.dedupeKeyTemplate,
156
+ payload,
157
+ evaluateKeyTemplate,
158
+ ) as string;
146
159
  const windowStart = new Date(Date.now() - definition.dedupeWindowMs);
147
160
  const existing = await client
148
161
  .select()
@@ -166,10 +179,15 @@ export class DrizzleJobOrchestrator implements IJobOrchestrator {
166
179
  // 1c. Concurrency collision check.
167
180
  let concurrencyKey: string | null = null;
168
181
  if (definition.concurrencyKeyTemplate) {
169
- concurrencyKey = evaluateKeyTemplate(
182
+ // Non-null cast: the branch guard proves the template is present, so the
183
+ // resolver never returns null here (it only nulls on a null template).
184
+ concurrencyKey = resolveJobKey(
185
+ 'concurrency',
186
+ type,
170
187
  definition.concurrencyKeyTemplate,
171
188
  payload,
172
- );
189
+ evaluateKeyTemplate,
190
+ ) as string;
173
191
  const inFlight = await client
174
192
  .select()
175
193
  .from(jobRuns)
@@ -223,10 +241,13 @@ export class DrizzleJobOrchestrator implements IJobOrchestrator {
223
241
  rootRunId = parent.rootRunId;
224
242
  }
225
243
 
226
- const dedupeKey =
227
- definition.dedupeKeyTemplate
228
- ? evaluateKeyTemplate(definition.dedupeKeyTemplate, payload)
229
- : null;
244
+ const dedupeKey = resolveJobKey(
245
+ 'dedupe',
246
+ type,
247
+ definition.dedupeKeyTemplate,
248
+ payload,
249
+ evaluateKeyTemplate,
250
+ );
230
251
 
231
252
  const [inserted] = await client
232
253
  .insert(jobRuns)
@@ -460,17 +481,23 @@ export class DrizzleJobOrchestrator implements IJobOrchestrator {
460
481
  backoff: 'fixed' as const,
461
482
  baseMs: 0,
462
483
  };
463
- const concurrencyKeyTemplate =
464
- (meta.concurrency as { key?: unknown } | undefined)?.key;
465
- const concurrencyKeyTemplateStr =
466
- typeof concurrencyKeyTemplate === 'string' ? concurrencyKeyTemplate : null;
484
+ // JOB-FN-KEY (0.16.2): both authored key forms are honored. A `{{field}}`
485
+ // string is persisted verbatim; a function is persisted as
486
+ // `FN_KEY_SENTINEL` (non-null so the collision/dedupe path engages, and
487
+ // hash-stable so the definition-hash gate doesn't churn on every boot —
488
+ // the function identity can't be hashed). `start()` re-resolves the live
489
+ // function from `JOB_HANDLER_REGISTRY`. The pre-0.16.2 `typeof === string
490
+ // ? … : null` dropped function keys to null, so `collisionMode` silently
491
+ // never engaged.
492
+ const concurrencyKeyTemplateStr = keySelectorToTemplate(
493
+ meta.concurrency?.key as JobKeySelector<unknown> | undefined,
494
+ );
467
495
  const collisionMode =
468
496
  (meta.concurrency?.collisionMode as JobDefinitionRow['collisionMode']) ??
469
497
  'queue';
470
- const dedupeKeyTemplate =
471
- (meta.dedupe as { key?: unknown } | undefined)?.key;
472
- const dedupeKeyTemplateStr =
473
- typeof dedupeKeyTemplate === 'string' ? dedupeKeyTemplate : null;
498
+ const dedupeKeyTemplateStr = keySelectorToTemplate(
499
+ meta.dedupe?.key as JobKeySelector<unknown> | undefined,
500
+ );
474
501
  const dedupeWindowMs = meta.dedupe?.windowMs ?? null;
475
502
  const timeoutMs = meta.timeoutMs ?? null;
476
503
  const replayFrom = meta.replayFrom ?? 'last_checkpoint';
@@ -32,11 +32,17 @@ import type {
32
32
  JobContext,
33
33
  JobHandlerBase,
34
34
  JobHandlerMeta,
35
+ JobKeySelector,
35
36
  RetryPolicy,
36
37
  SpawnChildOptions,
37
38
  StepOptions,
38
39
  } from './job-handler.base';
39
- import { ParentClosePolicy } from './job-handler.base';
40
+ import {
41
+ ParentClosePolicy,
42
+ keySelectorToTemplate,
43
+ FN_KEY_SENTINEL,
44
+ JobKeyFunctionUnavailableError,
45
+ } from './job-handler.base';
40
46
  import {
41
47
  JobCollisionError,
42
48
  JobNotReplayableError,
@@ -176,10 +182,9 @@ export class MemoryJobOrchestrator implements IJobOrchestrator {
176
182
  meta: JobHandlerMeta<TInput>,
177
183
  handlerClass: new (...args: unknown[]) => JobHandlerBase<TInput>,
178
184
  ): void {
179
- const concurrencyKeyTemplate =
180
- (meta.concurrency as { key?: string } | undefined)?.key ?? null;
181
- const dedupeKeyTemplate =
182
- (meta.dedupe as { key?: string } | undefined)?.key ?? null;
185
+ // JOB-FN-KEY (0.16.2): mirror the Drizzle backend — collapse a function
186
+ // key to `FN_KEY_SENTINEL` so the def row stays non-null (collision/dedupe
187
+ // path engages); `start()` re-resolves the live fn from `handlerRegistry`.
183
188
  const dedupeWindowMs = meta.dedupe?.windowMs ?? null;
184
189
  const now = new Date();
185
190
 
@@ -194,13 +199,15 @@ export class MemoryJobOrchestrator implements IJobOrchestrator {
194
199
  baseMs: 0,
195
200
  },
196
201
  timeoutMs: meta.timeoutMs ?? null,
197
- concurrencyKeyTemplate:
198
- typeof concurrencyKeyTemplate === 'string' ? concurrencyKeyTemplate : null,
202
+ concurrencyKeyTemplate: keySelectorToTemplate(
203
+ meta.concurrency?.key as JobKeySelector<unknown> | undefined,
204
+ ),
199
205
  collisionMode:
200
206
  (meta.concurrency?.collisionMode as JobDefinitionRow['collisionMode']) ??
201
207
  'queue',
202
- dedupeKeyTemplate:
203
- typeof dedupeKeyTemplate === 'string' ? dedupeKeyTemplate : null,
208
+ dedupeKeyTemplate: keySelectorToTemplate(
209
+ meta.dedupe?.key as JobKeySelector<unknown> | undefined,
210
+ ),
204
211
  dedupeWindowMs,
205
212
  priorityDefault: 0,
206
213
  replayFrom: meta.replayFrom ?? 'last_checkpoint',
@@ -223,6 +230,34 @@ export class MemoryJobOrchestrator implements IJobOrchestrator {
223
230
  return this.handlerRegistry.get(type);
224
231
  }
225
232
 
233
+ /**
234
+ * JOB-FN-KEY (0.16.2): resolve a persisted key template against the payload.
235
+ * Delegates to the shared `resolveJobKey`, but binds the FUNCTION-sentinel
236
+ * lookup to THIS backend's local `handlerRegistry` (the memory backend keeps
237
+ * its own meta map seeded by `registerHandler`, distinct from the global
238
+ * `JOB_HANDLER_REGISTRY` that the Drizzle/worker path uses — memory tests
239
+ * register handlers directly, never via the `@JobHandler` decorator). The
240
+ * `evaluateTemplate` callback handles the ordinary `{{field}}` path.
241
+ */
242
+ private resolveKey(
243
+ kind: 'concurrency' | 'dedupe',
244
+ type: string,
245
+ template: string | null,
246
+ payload: Record<string, unknown>,
247
+ ): string | null {
248
+ if (template == null) return null;
249
+ if (template !== FN_KEY_SENTINEL) {
250
+ return evaluateKeyTemplate(template, payload);
251
+ }
252
+ const key = (this.handlerRegistry.get(type)?.meta?.[kind] as
253
+ | { key?: unknown }
254
+ | undefined)?.key;
255
+ if (typeof key !== 'function') {
256
+ throw new JobKeyFunctionUnavailableError(type, kind);
257
+ }
258
+ return (key as (input: unknown) => string)(payload);
259
+ }
260
+
226
261
  /**
227
262
  * Boot-time upsert per `IJobOrchestrator.upsertJobRows`. Memory backend
228
263
  * just funnels each entry through `registerHandler`. The validator is
@@ -270,11 +305,10 @@ export class MemoryJobOrchestrator implements IJobOrchestrator {
270
305
  if (!definition) throw new JobTypeNotFoundError(type);
271
306
 
272
307
  // 1. Dedupe — return existing non-excluded run within the window.
308
+ // JOB-FN-KEY: `resolveKey` honors the `{{field}}` template AND a function
309
+ // key persisted as `FN_KEY_SENTINEL` (re-resolved from `handlerRegistry`).
273
310
  if (definition.dedupeKeyTemplate && definition.dedupeWindowMs) {
274
- const dedupeKey = evaluateKeyTemplate(
275
- definition.dedupeKeyTemplate,
276
- payload,
277
- );
311
+ const dedupeKey = this.resolveKey('dedupe', type, definition.dedupeKeyTemplate, payload) as string;
278
312
  const windowStart = Date.now() - definition.dedupeWindowMs;
279
313
  const existing = this.findDedupeCandidate(type, dedupeKey, windowStart);
280
314
  if (existing) return existing;
@@ -284,10 +318,13 @@ export class MemoryJobOrchestrator implements IJobOrchestrator {
284
318
  let concurrencyKey: string | null = null;
285
319
  let queueBlockedBy: string | null = null;
286
320
  if (definition.concurrencyKeyTemplate) {
287
- concurrencyKey = evaluateKeyTemplate(
321
+ // Non-null cast: the branch guard proves the template is present.
322
+ concurrencyKey = this.resolveKey(
323
+ 'concurrency',
324
+ type,
288
325
  definition.concurrencyKeyTemplate,
289
326
  payload,
290
- );
327
+ ) as string;
291
328
  const incumbent = this.findInFlightByConcurrencyKey(concurrencyKey);
292
329
  if (incumbent) {
293
330
  switch (definition.collisionMode) {
@@ -331,9 +368,12 @@ export class MemoryJobOrchestrator implements IJobOrchestrator {
331
368
  // 4. Compute dedupe key for the persisted row (separate from dedupe
332
369
  // short-circuit above — we store it even when no prior run matched
333
370
  // so future dedupe checks see it).
334
- const dedupeKey = definition.dedupeKeyTemplate
335
- ? evaluateKeyTemplate(definition.dedupeKeyTemplate, payload)
336
- : null;
371
+ const dedupeKey = this.resolveKey(
372
+ 'dedupe',
373
+ type,
374
+ definition.dedupeKeyTemplate,
375
+ payload,
376
+ );
337
377
 
338
378
  const now = new Date();
339
379
  const runAt = queueBlockedBy
@@ -27,3 +27,20 @@ integration:
27
27
  # Enabling post-install requires a reinstall (`subsystem install integration
28
28
  # --force --force-config`) plus an Atlas migration.
29
29
  multi_tenant: false
30
+
31
+ # ── Default-differ tuning (DIFFER-UNIGNORE) ──
32
+ # Threaded into the `DeepEqualDiffer` bound to INTEGRATION_FIELD_DIFFER.
33
+ # Off-by-default — omit the whole `differ:` block for the historical
34
+ # behaviour (the built-in ignore list, unchanged). A feature module that
35
+ # binds its own `IFieldDiffer<T>` overrides this entirely.
36
+ #
37
+ # differ:
38
+ # # Extra field names to ALSO ignore (merged with the defaults).
39
+ # ignore: [internalSeq]
40
+ # # Default-ignored field names to RE-INCLUDE as domain data. The canonical
41
+ # # case: an entity with `softDelete: false` whose `deletedAt` carries a
42
+ # # vendor-observed retraction tombstone ON the canonical record. Without
43
+ # # this the tombstone overlay diffs to 'noop', the upsert is skipped, and
44
+ # # `deleted_at` never lands. `unignore` is subtracted after `ignore`, so it
45
+ # # wins on a field listed in both.
46
+ # unignore: [deletedAt]
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../runtime/subsystems/integration/deep-equal.differ.ts"],"sourcesContent":["/**\n * DeepEqualDiffer — default `IFieldDiffer<T>` for the integration subsystem (SYNC-5).\n *\n * Walks every field of `incoming` against `existing`, emitting a structured\n * per-field diff (`{ from, to }`) for every field whose value changed.\n * Returns `'noop'` when the record is unchanged.\n *\n * Design decisions (extracted from the upstream consumer + HS-9 findings):\n *\n * 1. **Ignore list** — row metadata that sinks/services stamp unconditionally\n * so upstream cannot reasonably disagree:\n * `id`, `createdAt`, `updatedAt`, `deletedAt`, `type`,\n * `lastModifiedAt`, `fields`, `providerMetadata`\n * (`fields` is the EAV bag — it's diffed by the sink's EAV dual-write\n * path, not at the canonical-record layer.)\n *\n * 2. **`providerChangedFields` hint (CDC)** — when present, restricts the\n * comparison to the hinted field set. The hint is advisory; fields in\n * the ignore list are still filtered out even when hinted. Provider\n * hints are field-NAME-level; they don't override the ignore rules.\n *\n * 3. **Date → ISO string** — `Date` instances are normalized to\n * `toISOString()` before comparison. Sinks return `Date` from the DB\n * driver; adapters typically deliver strings. Direct `===` would\n * always say \"changed.\"\n *\n * 4. **Decimal-string vs number** — Postgres `numeric` columns return as\n * strings through Drizzle; adapters deliver numbers. When one side is a\n * number and the other is a numeric string that parses to the same\n * number, they're equal. The normalizer does NOT coerce non-numeric\n * strings, and it preserves zero-vs-null distinction.\n *\n * 5. **null-existing path** — `diff(null, incoming)` produces a full\n * created-shape diff (`{from: null, to: <value>}` for every non-ignored\n * field). Orchestrator sees this and records `operation: 'created'`.\n */\nimport { Injectable } from '@nestjs/common';\nimport type {\n DiffResult,\n FieldDiff,\n IFieldDiffer,\n} from './integration-field-diff.protocol';\n\n/**\n * Default ignore list. Keep in integration with consumer canonical-record shapes —\n * adding a row-metadata field here means no integration will ever mark it changed.\n *\n * Includes the columns contributed by the `external_id_tracking` behavior\n * (`external_id`/`externalId`, `provider`, `provider_metadata`/`providerMetadata`).\n * These are integration-tracking metadata, not domain attributes: they ride on the\n * canonical record but must never register as a field change (the external id\n * is the record's identity, not a mutable value). Listed in both snake_case\n * and camelCase so the differ ignores them regardless of the consumer's\n * canonical projection casing.\n */\nconst DEFAULT_IGNORE_FIELDS: ReadonlySet<string> = new Set([\n 'id',\n 'createdAt',\n 'updatedAt',\n 'deletedAt',\n 'type',\n 'lastModifiedAt',\n 'fields',\n 'external_id',\n 'externalId',\n 'provider',\n 'provider_metadata',\n 'providerMetadata',\n]);\n\nexport interface DeepEqualDifferOptions {\n /**\n * Extra field names to ignore in addition to the defaults. Consumers can\n * pass `['integration_version']` etc. to augment the base list; values here are\n * merged (not replaced) with `DEFAULT_IGNORE_FIELDS`.\n */\n readonly ignore?: readonly string[];\n}\n\n@Injectable()\nexport class DeepEqualDiffer<T extends Record<string, unknown>>\n implements IFieldDiffer<T>\n{\n private readonly ignore: ReadonlySet<string>;\n\n constructor(opts: DeepEqualDifferOptions = {}) {\n if (opts.ignore && opts.ignore.length > 0) {\n this.ignore = new Set([...DEFAULT_IGNORE_FIELDS, ...opts.ignore]);\n } else {\n this.ignore = DEFAULT_IGNORE_FIELDS;\n }\n }\n\n diff(\n existing: T | null,\n incoming: T,\n providerChangedFields?: string[],\n ): DiffResult {\n // Created-shape: every non-ignored field becomes `{from: null, to}`.\n if (existing === null) {\n const out: FieldDiff = {};\n for (const key of Object.keys(incoming)) {\n if (this.ignore.has(key)) continue;\n const value = (incoming as Record<string, unknown>)[key];\n // Skip fields that are themselves null/undefined — a created record\n // doesn't need to declare \"this field is null now\" for every\n // untouched column.\n if (value === null || value === undefined) continue;\n out[key] = { from: null, to: value };\n }\n return Object.keys(out).length === 0 ? 'noop' : out;\n }\n\n // Field set to compare. `providerChangedFields` narrows to a hint set;\n // ignored fields are filtered out regardless of hint.\n const candidates = new Set<string>();\n if (providerChangedFields && providerChangedFields.length > 0) {\n for (const key of providerChangedFields) {\n if (!this.ignore.has(key)) candidates.add(key);\n }\n } else {\n for (const key of Object.keys(incoming)) {\n if (!this.ignore.has(key)) candidates.add(key);\n }\n // Also include keys that exist on existing but not on incoming —\n // e.g. a field that was cleared. This would otherwise be missed when\n // incoming carries an undefined column we drop from the iteration.\n for (const key of Object.keys(existing)) {\n if (this.ignore.has(key)) continue;\n if (!(key in (incoming as Record<string, unknown>))) continue;\n candidates.add(key);\n }\n }\n\n const out: FieldDiff = {};\n for (const key of candidates) {\n const before = (existing as Record<string, unknown>)[key];\n const after = (incoming as Record<string, unknown>)[key];\n if (!isEqual(before, after)) {\n out[key] = { from: before ?? null, to: after ?? null };\n }\n }\n\n return Object.keys(out).length === 0 ? 'noop' : out;\n }\n}\n\n// ─── equality helpers ───────────────────────────────────────────────────────\n\n/**\n * Field-level equality with the canonical-integration normalizations:\n * - Date → toISOString (adapters deliver strings)\n * - numeric-string vs number → numeric equality when both parse\n * - deep equality for plain objects/arrays (single-level is enough for\n * canonical records; nested records travel as jsonb columns where the\n * sink already owns the comparison)\n */\nfunction isEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n\n const na = normalize(a);\n const nb = normalize(b);\n if (na === nb) return true;\n\n // After normalization: both may still be non-primitive objects.\n if (\n typeof na === 'object' &&\n typeof nb === 'object' &&\n na !== null &&\n nb !== null\n ) {\n return deepEqualObject(na as Record<string, unknown>, nb as Record<string, unknown>);\n }\n\n // Numeric string ↔ number: when one side is a number and the other is a\n // string that parses to the same finite number.\n const numericEqual = maybeNumericEqual(na, nb) || maybeNumericEqual(nb, na);\n return numericEqual;\n}\n\nfunction normalize(value: unknown): unknown {\n if (value instanceof Date) return value.toISOString();\n return value;\n}\n\nfunction maybeNumericEqual(a: unknown, b: unknown): boolean {\n // a is string-shape, b is number — parse a and compare. Only when the\n // string looks numeric AND the parse round-trips (no silent NaN pass-\n // through on non-numeric strings).\n if (typeof a !== 'string' || typeof b !== 'number') return false;\n if (a.trim() === '') return false;\n const parsed = Number(a);\n if (!Number.isFinite(parsed)) return false;\n return parsed === b;\n}\n\nfunction deepEqualObject(\n a: Record<string, unknown>,\n b: Record<string, unknown>,\n): boolean {\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n const aKeys = Object.keys(a);\n const bKeys = Object.keys(b);\n if (aKeys.length !== bKeys.length) return false;\n for (const key of aKeys) {\n if (!(key in b)) return false;\n if (!isEqual(a[key], b[key])) return false;\n }\n return true;\n}\n"],"mappings":";;;;;AAoCA,SAAS,kBAAkB;AAmB3B,IAAM,wBAA6C,oBAAI,IAAI;AAAA,EACzD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAYM,IAAM,kBAAN,MAEP;AAAA,EACmB;AAAA,EAEjB,YAAY,OAA+B,CAAC,GAAG;AAC7C,QAAI,KAAK,UAAU,KAAK,OAAO,SAAS,GAAG;AACzC,WAAK,SAAS,oBAAI,IAAI,CAAC,GAAG,uBAAuB,GAAG,KAAK,MAAM,CAAC;AAAA,IAClE,OAAO;AACL,WAAK,SAAS;AAAA,IAChB;AAAA,EACF;AAAA,EAEA,KACE,UACA,UACA,uBACY;AAEZ,QAAI,aAAa,MAAM;AACrB,YAAMA,OAAiB,CAAC;AACxB,iBAAW,OAAO,OAAO,KAAK,QAAQ,GAAG;AACvC,YAAI,KAAK,OAAO,IAAI,GAAG,EAAG;AAC1B,cAAM,QAAS,SAAqC,GAAG;AAIvD,YAAI,UAAU,QAAQ,UAAU,OAAW;AAC3C,QAAAA,KAAI,GAAG,IAAI,EAAE,MAAM,MAAM,IAAI,MAAM;AAAA,MACrC;AACA,aAAO,OAAO,KAAKA,IAAG,EAAE,WAAW,IAAI,SAASA;AAAA,IAClD;AAIA,UAAM,aAAa,oBAAI,IAAY;AACnC,QAAI,yBAAyB,sBAAsB,SAAS,GAAG;AAC7D,iBAAW,OAAO,uBAAuB;AACvC,YAAI,CAAC,KAAK,OAAO,IAAI,GAAG,EAAG,YAAW,IAAI,GAAG;AAAA,MAC/C;AAAA,IACF,OAAO;AACL,iBAAW,OAAO,OAAO,KAAK,QAAQ,GAAG;AACvC,YAAI,CAAC,KAAK,OAAO,IAAI,GAAG,EAAG,YAAW,IAAI,GAAG;AAAA,MAC/C;AAIA,iBAAW,OAAO,OAAO,KAAK,QAAQ,GAAG;AACvC,YAAI,KAAK,OAAO,IAAI,GAAG,EAAG;AAC1B,YAAI,EAAE,OAAQ,UAAuC;AACrD,mBAAW,IAAI,GAAG;AAAA,MACpB;AAAA,IACF;AAEA,UAAM,MAAiB,CAAC;AACxB,eAAW,OAAO,YAAY;AAC5B,YAAM,SAAU,SAAqC,GAAG;AACxD,YAAM,QAAS,SAAqC,GAAG;AACvD,UAAI,CAAC,QAAQ,QAAQ,KAAK,GAAG;AAC3B,YAAI,GAAG,IAAI,EAAE,MAAM,UAAU,MAAM,IAAI,SAAS,KAAK;AAAA,MACvD;AAAA,IACF;AAEA,WAAO,OAAO,KAAK,GAAG,EAAE,WAAW,IAAI,SAAS;AAAA,EAClD;AACF;AAjEa,kBAAN;AAAA,EADN,WAAW;AAAA,GACC;AA6Eb,SAAS,QAAQ,GAAY,GAAqB;AAChD,MAAI,MAAM,EAAG,QAAO;AAEpB,QAAM,KAAK,UAAU,CAAC;AACtB,QAAM,KAAK,UAAU,CAAC;AACtB,MAAI,OAAO,GAAI,QAAO;AAGtB,MACE,OAAO,OAAO,YACd,OAAO,OAAO,YACd,OAAO,QACP,OAAO,MACP;AACA,WAAO,gBAAgB,IAA+B,EAA6B;AAAA,EACrF;AAIA,QAAM,eAAe,kBAAkB,IAAI,EAAE,KAAK,kBAAkB,IAAI,EAAE;AAC1E,SAAO;AACT;AAEA,SAAS,UAAU,OAAyB;AAC1C,MAAI,iBAAiB,KAAM,QAAO,MAAM,YAAY;AACpD,SAAO;AACT;AAEA,SAAS,kBAAkB,GAAY,GAAqB;AAI1D,MAAI,OAAO,MAAM,YAAY,OAAO,MAAM,SAAU,QAAO;AAC3D,MAAI,EAAE,KAAK,MAAM,GAAI,QAAO;AAC5B,QAAM,SAAS,OAAO,CAAC;AACvB,MAAI,CAAC,OAAO,SAAS,MAAM,EAAG,QAAO;AACrC,SAAO,WAAW;AACpB;AAEA,SAAS,gBACP,GACA,GACS;AACT,MAAI,MAAM,QAAQ,CAAC,MAAM,MAAM,QAAQ,CAAC,EAAG,QAAO;AAClD,QAAM,QAAQ,OAAO,KAAK,CAAC;AAC3B,QAAM,QAAQ,OAAO,KAAK,CAAC;AAC3B,MAAI,MAAM,WAAW,MAAM,OAAQ,QAAO;AAC1C,aAAW,OAAO,OAAO;AACvB,QAAI,EAAE,OAAO,GAAI,QAAO;AACxB,QAAI,CAAC,QAAQ,EAAE,GAAG,GAAG,EAAE,GAAG,CAAC,EAAG,QAAO;AAAA,EACvC;AACA,SAAO;AACT;","names":["out"]}