@pattern-stack/codegen 0.6.4 → 0.6.6

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 (93) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +2 -0
  3. package/dist/runtime/subsystems/auth/auth-oauth-state.schema.d.ts +81 -0
  4. package/dist/runtime/subsystems/auth/auth-oauth-state.schema.js +12 -0
  5. package/dist/runtime/subsystems/auth/auth-oauth-state.schema.js.map +1 -0
  6. package/dist/runtime/subsystems/auth/auth.module.d.ts +39 -24
  7. package/dist/runtime/subsystems/auth/auth.module.js +247 -14
  8. package/dist/runtime/subsystems/auth/auth.module.js.map +1 -1
  9. package/dist/runtime/subsystems/auth/auth.tokens.d.ts +15 -2
  10. package/dist/runtime/subsystems/auth/auth.tokens.js +9 -1
  11. package/dist/runtime/subsystems/auth/auth.tokens.js.map +1 -1
  12. package/dist/runtime/subsystems/auth/backends/encryption-key/env.d.ts +1 -1
  13. package/dist/runtime/subsystems/auth/backends/encryption-key/env.js +1 -1
  14. package/dist/runtime/subsystems/auth/backends/encryption-key/env.js.map +1 -1
  15. package/dist/runtime/subsystems/auth/backends/state-store.drizzle-backend.d.ts +23 -0
  16. package/dist/runtime/subsystems/auth/backends/state-store.drizzle-backend.js +68 -0
  17. package/dist/runtime/subsystems/auth/backends/state-store.drizzle-backend.js.map +1 -0
  18. package/dist/runtime/subsystems/auth/backends/state-store.memory-backend.d.ts +21 -0
  19. package/dist/runtime/subsystems/auth/backends/state-store.memory-backend.js +51 -0
  20. package/dist/runtime/subsystems/auth/backends/state-store.memory-backend.js.map +1 -0
  21. package/dist/runtime/subsystems/auth/controllers/auth.controller.d.ts +31 -0
  22. package/dist/runtime/subsystems/auth/controllers/auth.controller.js +137 -0
  23. package/dist/runtime/subsystems/auth/controllers/auth.controller.js.map +1 -0
  24. package/dist/runtime/subsystems/auth/index.d.ts +13 -4
  25. package/dist/runtime/subsystems/auth/index.js +254 -15
  26. package/dist/runtime/subsystems/auth/index.js.map +1 -1
  27. package/dist/runtime/subsystems/auth/protocols/auth-strategy.d.ts +1 -1
  28. package/dist/runtime/subsystems/auth/protocols/integration-store.d.ts +37 -2
  29. package/dist/runtime/subsystems/auth/protocols/oauth-state-store.d.ts +33 -7
  30. package/dist/runtime/subsystems/auth/protocols/oauth-state-store.js +12 -0
  31. package/dist/runtime/subsystems/auth/protocols/oauth-state-store.js.map +1 -1
  32. package/dist/runtime/subsystems/auth/protocols/provider-strategy.d.ts +54 -0
  33. package/dist/runtime/subsystems/auth/protocols/provider-strategy.js +1 -0
  34. package/dist/runtime/subsystems/auth/protocols/provider-strategy.js.map +1 -0
  35. package/dist/runtime/subsystems/auth/protocols/user-context.d.ts +24 -0
  36. package/dist/runtime/subsystems/auth/protocols/user-context.js +1 -0
  37. package/dist/runtime/subsystems/auth/protocols/user-context.js.map +1 -0
  38. package/dist/runtime/subsystems/auth/runtime/oauth2-refresh.strategy.d.ts +2 -2
  39. package/dist/runtime/subsystems/auth/runtime/oauth2-refresh.strategy.js.map +1 -1
  40. package/dist/runtime/subsystems/auth/runtime/session-expired.error.d.ts +2 -2
  41. package/dist/runtime/subsystems/auth/runtime/session-expired.error.js.map +1 -1
  42. package/dist/runtime/subsystems/auth/runtime/with-auth-retry.d.ts +1 -1
  43. package/dist/runtime/subsystems/auth/runtime/with-auth-retry.js.map +1 -1
  44. package/dist/runtime/subsystems/index.d.ts +9 -4
  45. package/dist/runtime/subsystems/index.js +248 -15
  46. package/dist/runtime/subsystems/index.js.map +1 -1
  47. package/dist/runtime/subsystems/sync/deep-equal.differ.js.map +1 -1
  48. package/dist/runtime/subsystems/sync/execute-sync.use-case.js.map +1 -1
  49. package/dist/runtime/subsystems/sync/index.js.map +1 -1
  50. package/dist/runtime/subsystems/sync/sync-change-source.protocol.d.ts +1 -1
  51. package/dist/runtime/subsystems/sync/sync-cursor-store.memory-backend.js.map +1 -1
  52. package/dist/runtime/subsystems/sync/sync-loopback.protocol.d.ts +3 -4
  53. package/dist/runtime/subsystems/sync/sync-run-recorder.drizzle-backend.js.map +1 -1
  54. package/dist/runtime/subsystems/sync/sync.module.js.map +1 -1
  55. package/dist/src/cli/index.js +574 -142
  56. package/dist/src/cli/index.js.map +1 -1
  57. package/dist/src/index.js.map +1 -1
  58. package/package.json +1 -1
  59. package/runtime/subsystems/auth/auth-oauth-state.schema.ts +30 -0
  60. package/runtime/subsystems/auth/auth.module.ts +89 -32
  61. package/runtime/subsystems/auth/auth.tokens.ts +14 -1
  62. package/runtime/subsystems/auth/backends/encryption-key/env.ts +3 -3
  63. package/runtime/subsystems/auth/backends/state-store.drizzle-backend.ts +83 -0
  64. package/runtime/subsystems/auth/backends/state-store.memory-backend.ts +76 -0
  65. package/runtime/subsystems/auth/controllers/auth.controller.ts +155 -0
  66. package/runtime/subsystems/auth/index.ts +43 -4
  67. package/runtime/subsystems/auth/protocols/auth-strategy.ts +1 -1
  68. package/runtime/subsystems/auth/protocols/integration-store.ts +38 -1
  69. package/runtime/subsystems/auth/protocols/oauth-state-store.ts +38 -6
  70. package/runtime/subsystems/auth/protocols/provider-strategy.ts +48 -0
  71. package/runtime/subsystems/auth/protocols/user-context.ts +22 -0
  72. package/runtime/subsystems/auth/runtime/oauth2-refresh.strategy.ts +2 -2
  73. package/runtime/subsystems/auth/runtime/session-expired.error.ts +2 -2
  74. package/runtime/subsystems/auth/runtime/with-auth-retry.ts +1 -1
  75. package/runtime/subsystems/index.ts +17 -2
  76. package/runtime/subsystems/sync/deep-equal.differ.ts +1 -1
  77. package/runtime/subsystems/sync/execute-sync.use-case.ts +1 -1
  78. package/runtime/subsystems/sync/sync-change-source.protocol.ts +1 -1
  79. package/runtime/subsystems/sync/sync-cursor-store.memory-backend.ts +1 -1
  80. package/runtime/subsystems/sync/sync-loopback.protocol.ts +3 -4
  81. package/runtime/subsystems/sync/sync-run-recorder.drizzle-backend.ts +1 -1
  82. package/templates/subsystem/auth/app-module-hook.ejs.t +21 -0
  83. package/templates/subsystem/auth/auth-oauth-state.schema.ejs.t +35 -0
  84. package/templates/subsystem/auth/env-config.ejs.t +20 -0
  85. package/templates/subsystem/auth/prompt.js +46 -0
  86. package/templates/subsystem/auth-config/codegen-config-auth-block.ejs.t +20 -0
  87. package/templates/subsystem/auth-config/prompt.js +20 -0
  88. package/templates/subsystem/auth-integrations/app-module-hook.ejs.t +16 -0
  89. package/templates/subsystem/auth-integrations/prompt.js +23 -0
  90. package/dist/runtime/subsystems/auth/backends/oauth-state-store/in-memory.d.ts +0 -24
  91. package/dist/runtime/subsystems/auth/backends/oauth-state-store/in-memory.js +0 -24
  92. package/dist/runtime/subsystems/auth/backends/oauth-state-store/in-memory.js.map +0 -1
  93. package/runtime/subsystems/auth/backends/oauth-state-store/in-memory.ts +0 -42
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../runtime/subsystems/sync/sync-field-diff.protocol.ts","../../../../runtime/subsystems/sync/detection-config.schema.ts","../../../../runtime/subsystems/sync/loopback.middleware.ts","../../../../runtime/subsystems/sync/poll-change-source.ts","../../../../runtime/subsystems/sync/webhook-change-source.ts","../../../../runtime/subsystems/sync/build-change-source.ts","../../../../runtime/subsystems/sync/sync.tokens.ts","../../../../runtime/subsystems/sync/sync-errors.ts","../../../../runtime/subsystems/sync/sync-audit.schema.ts","../../../../runtime/subsystems/sync/sync-cursor-store.memory-backend.ts","../../../../runtime/subsystems/sync/sync-run-recorder.memory-backend.ts","../../../../runtime/subsystems/sync/deep-equal.differ.ts","../../../../runtime/subsystems/sync/execute-sync.use-case.ts","../../../../runtime/subsystems/sync/sync-cursor-store.drizzle-backend.ts","../../../../runtime/constants/tokens.ts","../../../../runtime/subsystems/sync/sync-run-recorder.drizzle-backend.ts","../../../../runtime/subsystems/sync/sync.module.ts"],"sourcesContent":["/**\n * Sync subsystem — field-diff protocol (port)\n *\n * `IFieldDiffer<T>` is the pluggable differ seam. The default implementation\n * (`DeepEqualDiffer`, ships in SYNC-5) walks every field except an ignore\n * list; CDC-aware differs can skip comparison for fields the provider didn't\n * flag as changed.\n *\n * `FieldDiffSchema` is the structural enforcement of the `changed_fields`\n * column per ADR-0003 — enforced at write time by the recorder service so\n * consumers can rely on the shape in downstream queries.\n */\nimport { z } from 'zod';\n\n// ============================================================================\n// FieldDiff shape — the ADR-0003 contract\n// ============================================================================\n\n/**\n * Structured per-field change. Enforced shape for `sync_run_items.changed_fields`.\n *\n * `created` items set `from: null, to: <value>` for every non-null field.\n * `deleted` items set `from: <value>, to: null`.\n * `noop` items carry `{}`.\n */\nexport const FieldDiffValueSchema = z.object({\n from: z.unknown(),\n to: z.unknown(),\n});\n\nexport const FieldDiffSchema = z.record(z.string(), FieldDiffValueSchema);\n\nexport type FieldDiffValue = z.infer<typeof FieldDiffValueSchema>;\nexport type FieldDiff = z.infer<typeof FieldDiffSchema>;\n\n/** Result of comparing a new record against its existing local state. */\nexport type DiffResult = FieldDiff | 'noop';\n\n// ============================================================================\n// IFieldDiffer\n// ============================================================================\n\n/**\n * Pluggable differ. Default ships in SYNC-5 as `DeepEqualDiffer<T>` —\n * deep-equal over every field except an ignore list (`updated_at` and other\n * row metadata). CDC-aware differs restrict comparison to\n * `providerChangedFields` when supplied.\n */\nexport interface IFieldDiffer<T> {\n /**\n * @param existing — current local state, or `null` when the record is new\n * @param incoming — the canonical record coming from the adapter\n * @param providerChangedFields — optional hint from CDC-capable sources;\n * when present, differ may restrict the comparison to these fields\n */\n diff(\n existing: T | null,\n incoming: T,\n providerChangedFields?: string[],\n ): DiffResult;\n}\n","/**\n * Sync subsystem — DetectionConfig schema (#226-1)\n *\n * Canonical Zod schema for per-entity sync detection config. The schema is\n * the single source of truth for filter/mapping shape and is consumed by:\n *\n * 1. Runtime primitives — `PollChangeSource<T>`, `WebhookChangeSource<T>`\n * (#226-3, #226-4) accept a parsed `DetectionConfig` at construction.\n * 2. Codegen — `src/schema/entity-definition.schema.ts` (#226-6) imports\n * this schema so per-entity YAML `detection:` blocks validate against\n * the same shape the runtime enforces.\n *\n * Locked decisions (see ADR-033 + decision memo Q1–Q6):\n * - Filter vocabulary is flat AND of `{ field, op, value }` triples; richer\n * boolean expressions (OR / NOT / nested) are deferred per epic open Q3.\n * - Cursor strategy is a tagged union over the four shapes the three modes\n * need (`systemModstamp`, `replayId`, `timestamp`, `eventId`). Each\n * strategy types its cursor internally; the orchestrator persists what\n * the iterator last yielded (sync skill rule 2).\n * - `mode: 'poll'` may opt into `provenance: 'cdc'` so Stripe-style event\n * endpoints (mechanically a poll, semantically CDC) reuse the poll\n * primitive while emitting `Change<T>.source = 'cdc'`. Long-lived\n * streaming CDC (SFDC Pub-Sub, Debezium) is a separate primitive\n * deferred to #226-8.\n * - `webhook` mode requires `eventIdField` so `WebhookChangeSource<T>`\n * can populate `Change<T>.dedupKey` from the inbound staging row.\n */\nimport { z } from 'zod';\n\n// ============================================================================\n// Field mapping — provider field → canonical target\n// ============================================================================\n\n/**\n * Maps a single provider field onto the canonical record. `transform` is an\n * opt-in tag the adapter callback may inspect (`date-iso`, `decimal-string`,\n * etc.); the schema does not enumerate transforms — adapters interpret them.\n */\nexport const FieldMappingSchema = z.object({\n source: z.string().min(1),\n target: z.string().min(1),\n transform: z.string().min(1).optional(),\n});\n\nexport type FieldMapping = z.infer<typeof FieldMappingSchema>;\n\n// ============================================================================\n// Resolved filter — flat-AND triple\n// ============================================================================\n\n/**\n * A single resolved filter clause applied at fetch time. `value` is `unknown`\n * to admit primitives, arrays (for `in` / `nin`), and dates as ISO strings —\n * adapters interpret per provider.\n */\nexport const ResolvedFilterSchema = z.object({\n field: z.string().min(1),\n op: z.enum(['eq', 'neq', 'in', 'nin', 'gt', 'gte', 'lt', 'lte']),\n value: z.unknown(),\n});\n\nexport type ResolvedFilter = z.infer<typeof ResolvedFilterSchema>;\n\n// ============================================================================\n// Cursor strategy — tagged union over the four shapes the modes need\n// ============================================================================\n\nconst SystemModstampCursorSchema = z.object({\n kind: z.literal('systemModstamp'),\n field: z.string().min(1),\n});\n\nconst ReplayIdCursorSchema = z.object({\n kind: z.literal('replayId'),\n field: z.string().min(1),\n});\n\nconst TimestampCursorSchema = z.object({\n kind: z.literal('timestamp'),\n field: z.string().min(1),\n});\n\nconst EventIdCursorSchema = z.object({\n kind: z.literal('eventId'),\n field: z.string().min(1),\n});\n\nexport const CursorStrategySchema = z.discriminatedUnion('kind', [\n SystemModstampCursorSchema,\n ReplayIdCursorSchema,\n TimestampCursorSchema,\n EventIdCursorSchema,\n]);\n\nexport type CursorStrategy = z.infer<typeof CursorStrategySchema>;\n\n// ============================================================================\n// Mode-specific blocks\n// ============================================================================\n\n/**\n * Poll-mode block. `provenance: 'cdc'` opts the poll primitive into stamping\n * `Change<T>.source = 'cdc'` and populating `dedupKey` from the cursor's\n * `field` — used for Stripe-style event endpoints. Defaults to `'poll'`.\n */\nexport const PollDetectionSchema = z.object({\n cursor: CursorStrategySchema,\n provenance: z.enum(['poll', 'cdc']).optional(),\n});\n\nexport type PollDetection = z.infer<typeof PollDetectionSchema>;\n\n/**\n * Webhook-mode block. `eventIdField` names the column in the consumer-owned\n * inbound staging row that `WebhookChangeSource<T>` reads to set\n * `Change<T>.dedupKey`.\n */\nexport const WebhookDetectionSchema = z.object({\n eventIdField: z.string().min(1),\n});\n\nexport type WebhookDetection = z.infer<typeof WebhookDetectionSchema>;\n\n// ============================================================================\n// DetectionConfig — top-level discriminated union over `mode`\n// ============================================================================\n\nconst PollModeSchema = z.object({\n mode: z.literal('poll'),\n poll: PollDetectionSchema,\n mapping: z.array(FieldMappingSchema).min(1),\n filters: z.array(ResolvedFilterSchema).default([]),\n});\n\nconst WebhookModeSchema = z.object({\n mode: z.literal('webhook'),\n webhook: WebhookDetectionSchema,\n mapping: z.array(FieldMappingSchema).min(1),\n filters: z.array(ResolvedFilterSchema).default([]),\n});\n\n/**\n * Top-level detection config. Discriminated on `mode` so the relevant\n * mode-block (poll/webhook) is structurally required for that mode. CDC as a\n * long-lived streaming primitive is deferred (#226-8); CDC-as-provenance\n * (Stripe-style event endpoints) is expressed via `mode: 'poll'` with\n * `poll.provenance: 'cdc'`.\n */\nexport const DetectionConfigSchema = z.discriminatedUnion('mode', [\n PollModeSchema,\n WebhookModeSchema,\n]);\n\nexport type DetectionConfig = z.infer<typeof DetectionConfigSchema>;\n","/**\n * Sync subsystem — loopback `ChangeMiddleware` factory (#226-5, ADR-033).\n *\n * Replaces the prior orchestrator-side `@Optional() SYNC_LOOPBACK_FINGERPRINT_STORE`\n * branch. Consumers that need to suppress echoes of their own outbound\n * writes compose `createLoopbackMiddleware(store)` into a primitive's\n * middleware chain — typically alongside `PollChangeSource<T>` — instead\n * of injecting a store into the orchestrator.\n *\n * Why middleware, not orchestrator branch:\n * - keeps the orchestrator port-agnostic (no per-mode special cases);\n * - lets primitives compose loopback with redaction / throttling /\n * other cross-cutting wrappers in a single chain;\n * - lets consumers pick per-source whether loopback is wired (some\n * entities have outbound writeback, others don't) without DI gymnastics.\n *\n * Behavior:\n * - For every change yielded by the inner iterator, call\n * `store.isEchoOfOwnWrite(subscription.domain, change.externalId, change.record)`.\n * - If the store returns `true`, the change is dropped (not yielded).\n * - Otherwise the change is passed through untouched.\n *\n * The middleware does not record audit rows for suppressed changes —\n * the orchestrator no longer learns about them. This is the deliberate\n * trade: loopback echoes are noise, not signal; their prior `skipped+noop`\n * audit rows existed only because the orchestrator was the suppression\n * site. Consumers wanting visibility into suppression counts can wrap\n * their store or layer a counting middleware alongside this one.\n */\n\nimport type { Change } from './sync-change-source.protocol';\nimport type {\n ChangeIterator,\n ChangeMiddleware,\n} from './sync-middleware.protocol';\nimport type { ILoopbackFingerprintStore } from './sync-loopback.protocol';\n\n/**\n * Build a `ChangeMiddleware<T>` that suppresses changes whose fingerprint\n * matches a recent local write according to the supplied store.\n *\n * Composition — first middleware in the array is outermost. Consumers\n * typically place loopback as the outermost layer so suppressed changes\n * never reach downstream middleware (redaction, transforms, etc.).\n */\nexport function createLoopbackMiddleware<T>(\n store: ILoopbackFingerprintStore<T>,\n): ChangeMiddleware<T> {\n return (next: ChangeIterator<T>): ChangeIterator<T> => {\n return async function* (subscription, cursor): AsyncIterable<Change<T>> {\n for await (const change of next(subscription, cursor)) {\n const isEcho = await store.isEchoOfOwnWrite(\n subscription.domain,\n change.externalId,\n change.record,\n );\n if (isEcho) continue;\n yield change;\n }\n };\n };\n}\n","/**\n * Sync subsystem — `PollChangeSource<T>` primitive (#226-3, ADR-033).\n *\n * Generic poll-mode `IChangeSource<T>` implementation parameterized by a\n * parsed `DetectionConfig` (poll mode) and a consumer-supplied\n * `PollFetchCallback<T>`. The primitive owns:\n *\n * - filter resolution (flat-AND vocabulary per epic decision Q3 — richer\n * boolean expressions deferred);\n * - field mapping → `Change.externalId` derivation;\n * - cursor strategy passthrough (the orchestrator passes the prior cursor\n * by-value per ADR-033 / #226-2; the callback yields the next cursor\n * per record; the primitive simply stamps it onto `Change<T>`);\n * - `Change<T>.source` provenance (`'poll'` by default);\n * - middleware-chain composition (the `ChangeMiddleware<T>` shape\n * locked in #226-1).\n *\n * Shape locks (decision memo Q5):\n * - `PollFetchContext = { subscription, cursor, filters }` — explicitly\n * NO `userId` / `tenantId`. Run-scope identity is closed over by the\n * consumer at adapter construction (or resolved inside the callback\n * via consumer services). Threading it through the seam would force\n * port expansion every time run-context grows.\n *\n * The adapter callback returns `{ record: T; cursor: PollCursor }` — the\n * primitive does not reach into the record to extract a cursor itself.\n * `cursor.field` from `DetectionConfig.poll.cursor` is metadata for codegen\n * + adapters; the primitive trusts what the callback yielded.\n */\n\nimport type {\n DetectionConfig,\n ResolvedFilter,\n} from './detection-config.schema';\nimport type {\n Change,\n ChangeSource,\n IChangeSource,\n SyncSubscriptionView,\n} from './sync-change-source.protocol';\nimport type {\n ChangeIterator,\n ChangeMiddleware,\n} from './sync-middleware.protocol';\n\n// ============================================================================\n// Cursor + adapter callback shapes\n// ============================================================================\n\n/**\n * Opaque poll-cursor shape. Each provider/entity pair binds it concretely\n * via the cursor strategy (`{ systemModstamp }`, `{ replayId }`, etc.); the\n * primitive treats it as an opaque value to pass through.\n */\nexport type PollCursor = unknown;\n\n/**\n * The context the primitive forwards to the adapter callback. Locked to\n * exactly three fields per decision memo Q5 — `userId` / `tenantId` are\n * NOT here on purpose.\n */\nexport interface PollFetchContext {\n readonly subscription: SyncSubscriptionView;\n readonly cursor: PollCursor | null;\n readonly filters: readonly ResolvedFilter[];\n}\n\n/**\n * Consumer-supplied fetch callback. Returns an async iterable of\n * `{ record, cursor }` pairs — `record` is already the canonical `T`\n * (the adapter does provider-side translation), `cursor` is the post-record\n * cursor the orchestrator should persist if the run completes successfully.\n */\nexport type PollFetchCallback<T> = (\n ctx: PollFetchContext,\n) => AsyncIterable<{ record: T; cursor: PollCursor }>;\n\n// ============================================================================\n// Constructor options\n// ============================================================================\n\nexport interface PollChangeSourceOptions<T> {\n /** Consumer-supplied fetch callback. */\n readonly adapter: PollFetchCallback<T>;\n /**\n * Parsed detection config. MUST be `mode: 'poll'`; the constructor\n * throws if a webhook config is supplied. Codegen-emitted factories\n * call `DetectionConfigSchema.parse(...)` upstream so this is a safety\n * net, not the primary validation point.\n */\n readonly config: DetectionConfig;\n /**\n * Optional middleware chain. First element is the outermost layer:\n * sees `(subscription, cursor)` first and yielded `Change<T>` last.\n * Locked shape (#226-1) — the primitive composes them with its own\n * `listChanges` implementation as the innermost iterator.\n */\n readonly middlewares?: ReadonlyArray<ChangeMiddleware<T>>;\n /**\n * Optional human label for run logs (e.g. `'salesforce-poll-opportunity'`).\n * Defaults to a derived label based on the subscription domain at\n * construction time fallback — adapters are encouraged to provide one.\n */\n readonly label?: string;\n}\n\n// ============================================================================\n// PollChangeSource<T>\n// ============================================================================\n\nexport class PollChangeSource<T> implements IChangeSource<T> {\n public readonly label: string;\n\n private readonly adapter: PollFetchCallback<T>;\n private readonly filters: readonly ResolvedFilter[];\n private readonly externalIdSourceField: string;\n private readonly source: ChangeSource;\n /**\n * When `poll.provenance === 'cdc'`, the field on the emitted record from\n * which `Change<T>.dedupKey` is read — sourced from `poll.cursor.field`\n * (Stripe-style event endpoints carry the event id on the record itself\n * and the cursor advances over the same id). `null` for default poll\n * provenance, which does NOT populate `dedupKey`.\n */\n private readonly dedupKeySourceField: string | null;\n private readonly composed: ChangeIterator<T>;\n\n constructor(opts: PollChangeSourceOptions<T>) {\n if (opts.config.mode !== 'poll') {\n throw new Error(\n `PollChangeSource requires DetectionConfig.mode === 'poll'; got '${(opts.config as { mode: string }).mode}'`,\n );\n }\n const config = opts.config;\n\n // Field mapping: locate the canonical `external_id` target. Adapters\n // emit T already-mapped, but the primitive needs to know which key on\n // T carries the external id so it can stamp `Change.externalId`. Source\n // of truth is the mapping table — codegen emits it from YAML, the\n // primitive reads it here.\n const externalIdMapping = config.mapping.find(\n (m) => m.target === 'external_id',\n );\n if (!externalIdMapping) {\n throw new Error(\n \"PollChangeSource: DetectionConfig.mapping must include an entry with target 'external_id' so emitted Change<T>.externalId can be populated\",\n );\n }\n this.externalIdSourceField = externalIdMapping.target;\n\n this.adapter = opts.adapter;\n this.filters = config.filters;\n // Provenance: `mode: 'poll'` defaults to `'poll'`; opt into `'cdc'` via\n // `poll.provenance` (Stripe-style event endpoints — wired in #226-4).\n // CDC provenance also stamps `dedupKey` from the cursor's `field`, since\n // those endpoints surface a per-event id on each record (the same id the\n // cursor advances over).\n const isCdc = config.poll.provenance === 'cdc';\n this.source = isCdc ? 'cdc' : 'poll';\n this.dedupKeySourceField = isCdc ? config.poll.cursor.field : null;\n\n this.label =\n opts.label ?? `poll-change-source:${externalIdMapping.source}`;\n\n // Compose middleware chain. The terminal iterator is `this.fetch`\n // bound to `this`. First middleware in the array is the outermost\n // layer (sees subscription/cursor first, yielded changes last).\n const inner: ChangeIterator<T> = (sub, cur) => this.fetch(sub, cur);\n const middlewares = opts.middlewares ?? [];\n this.composed = middlewares.reduceRight<ChangeIterator<T>>(\n (next, mw) => mw(next),\n inner,\n );\n }\n\n listChanges(\n subscription: SyncSubscriptionView,\n cursor: unknown | null,\n ): AsyncIterable<Change<T>> {\n return this.composed(subscription, cursor);\n }\n\n private async *fetch(\n subscription: SyncSubscriptionView,\n cursor: unknown | null,\n ): AsyncIterable<Change<T>> {\n const ctx: PollFetchContext = {\n subscription,\n cursor: cursor as PollCursor | null,\n filters: this.filters,\n };\n\n for await (const { record, cursor: nextCursor } of this.adapter(ctx)) {\n const externalIdRaw = (record as Record<string, unknown>)[\n this.externalIdSourceField\n ];\n if (typeof externalIdRaw !== 'string' || externalIdRaw.length === 0) {\n throw new Error(\n `PollChangeSource: record missing string '${this.externalIdSourceField}' — emitted records MUST carry the canonical external id keyed by the mapping target`,\n );\n }\n let dedupKey: string | undefined;\n if (this.dedupKeySourceField !== null) {\n const dedupRaw = (record as Record<string, unknown>)[\n this.dedupKeySourceField\n ];\n if (typeof dedupRaw !== 'string' || dedupRaw.length === 0) {\n throw new Error(\n `PollChangeSource: cdc-provenance record missing string '${this.dedupKeySourceField}' — when poll.provenance === 'cdc' the cursor.field must be present on each record so dedupKey can be populated`,\n );\n }\n dedupKey = dedupRaw;\n }\n\n const change: Change<T> = {\n externalId: externalIdRaw,\n // Polling cannot distinguish create vs. update vs. delete on its\n // own — all yielded records are surfaced as 'updated'. The\n // orchestrator's diff stage classifies create-vs-update against\n // local state; soft-delete detection is out of scope for the\n // primitive (consumer drives via tombstone records or a separate\n // sweep — see ADR-033).\n operation: 'updated',\n record,\n cursor: nextCursor,\n source: this.source,\n ...(dedupKey !== undefined ? { dedupKey } : {}),\n };\n yield change;\n }\n }\n}\n","/**\n * Sync subsystem — `WebhookChangeSource<T>` primitive (#226-4, ADR-033).\n *\n * Generic webhook-mode `IChangeSource<T>` implementation parameterized by a\n * parsed `DetectionConfig` (webhook mode) and a consumer-supplied\n * `WebhookFetchCallback<T>` that iterates a consumer-owned inbound staging\n * queue. The primitive owns:\n *\n * - canonical `Change<T>.source = 'webhook'` stamping;\n * - `dedupKey` derivation from the configured `webhook.eventIdField` on\n * the emitted record;\n * - `externalId` derivation from the mapping table's `external_id` target\n * (mirrors `PollChangeSource`);\n * - middleware-chain composition via the locked `ChangeMiddleware<T>` shape\n * (#226-1) — same composition seam as the poll primitive.\n *\n * The primitive is **passive**: it iterates whatever the consumer-owned\n * queue yields. It does NOT synchronously drive the orchestrator, does NOT\n * own a transport, and does NOT manage acks. The inbound staging table\n * schema is consumer-owned and deferred per ADR-0002 §Phase 4 — the\n * `WebhookFetchCallback<T>` is the queue contract the consumer injects.\n *\n * Shape locks (decision memo Q5, mirrored from poll primitive):\n * - `WebhookFetchContext = { subscription, cursor }` — explicitly NO\n * `userId` / `tenantId`. Run-scope identity is closed over by the\n * consumer at queue construction or resolved inside the callback via\n * consumer services. There are no `filters` on the webhook context —\n * filtering is done at registration / on the staging row, not at the\n * port seam (the queue is already filtered by the time the primitive\n * iterates).\n *\n * Long-lived streaming CDC primitives (SFDC Pub-Sub gRPC, Debezium/Kafka,\n * Postgres logical replication) are deferred to `#226-8` — they need a\n * fundamentally different lifecycle (`subscribe(onChange, onError)`,\n * server-paced backpressure, ack-on-yield) and shouldn't be retrofitted\n * into either this primitive or the poll primitive.\n */\n\nimport type { DetectionConfig } from './detection-config.schema';\nimport type {\n Change,\n IChangeSource,\n SyncSubscriptionView,\n} from './sync-change-source.protocol';\nimport type {\n ChangeIterator,\n ChangeMiddleware,\n} from './sync-middleware.protocol';\n\n// ============================================================================\n// Cursor + queue callback shapes\n// ============================================================================\n\n/**\n * Opaque webhook cursor shape. Webhook mode typically has a cursor of\n * `{ ts: ISO-string }` (last drained staging-row timestamp) but the\n * primitive treats it as opaque. Consumer-owned queue iterators interpret\n * it however the staging schema needs.\n */\nexport type WebhookCursor = unknown;\n\n/**\n * Context the primitive forwards to the queue iterator. Locked to exactly\n * two fields per the same Q5 reasoning that locks `PollFetchContext` — no\n * `userId` / `tenantId`.\n */\nexport interface WebhookFetchContext {\n readonly subscription: SyncSubscriptionView;\n readonly cursor: WebhookCursor | null;\n}\n\n/**\n * Consumer-supplied queue iterator. Returns an async iterable of\n * `{ record }` pairs — the consumer drains the inbound staging queue and\n * emits already-mapped canonical records `T`. The primitive stamps\n * `source: 'webhook'` and `dedupKey` from the record's configured\n * `webhook.eventIdField`; the consumer is the one who decided when a\n * staging row is \"ready\" to drain.\n *\n * Webhook mode has no per-record cursor advance — the staging-row drain\n * order is consumer policy (FIFO by ingestion timestamp, by event id, etc.)\n * and is opaque to the primitive. The orchestrator's last-yielded cursor\n * is whatever the consumer chooses to surface, if anything.\n */\nexport type WebhookFetchCallback<T> = (\n ctx: WebhookFetchContext,\n) => AsyncIterable<{ record: T; cursor?: WebhookCursor }>;\n\n// ============================================================================\n// Constructor options\n// ============================================================================\n\nexport interface WebhookChangeSourceOptions<T> {\n /** Consumer-supplied inbound queue iterator. */\n readonly queue: WebhookFetchCallback<T>;\n /**\n * Parsed detection config. MUST be `mode: 'webhook'`; the constructor\n * throws if a poll config is supplied. Codegen-emitted factories call\n * `DetectionConfigSchema.parse(...)` upstream so this is a safety net,\n * not the primary validation point.\n */\n readonly config: DetectionConfig;\n /**\n * Optional middleware chain. Same shape and composition rules as\n * `PollChangeSource` — first element is the outermost layer.\n */\n readonly middlewares?: ReadonlyArray<ChangeMiddleware<T>>;\n /**\n * Optional human label for run logs (e.g. `'stripe-webhook-charge'`).\n * Defaults to a derived label based on the mapping at construction.\n */\n readonly label?: string;\n}\n\n// ============================================================================\n// WebhookChangeSource<T>\n// ============================================================================\n\nexport class WebhookChangeSource<T> implements IChangeSource<T> {\n public readonly label: string;\n\n private readonly queue: WebhookFetchCallback<T>;\n private readonly externalIdSourceField: string;\n private readonly eventIdSourceField: string;\n private readonly composed: ChangeIterator<T>;\n\n constructor(opts: WebhookChangeSourceOptions<T>) {\n if (opts.config.mode !== 'webhook') {\n throw new Error(\n `WebhookChangeSource requires DetectionConfig.mode === 'webhook'; got '${(opts.config as { mode: string }).mode}'`,\n );\n }\n const config = opts.config;\n\n // Field mapping: locate the canonical `external_id` target — mirrors the\n // poll primitive's contract. Adapters emit records already-mapped; the\n // primitive needs to know which key on T carries the external id so it\n // can stamp `Change.externalId`.\n const externalIdMapping = config.mapping.find(\n (m) => m.target === 'external_id',\n );\n if (!externalIdMapping) {\n throw new Error(\n \"WebhookChangeSource: DetectionConfig.mapping must include an entry with target 'external_id' so emitted Change<T>.externalId can be populated\",\n );\n }\n this.externalIdSourceField = externalIdMapping.target;\n this.eventIdSourceField = config.webhook.eventIdField;\n\n this.queue = opts.queue;\n\n this.label =\n opts.label ?? `webhook-change-source:${externalIdMapping.source}`;\n\n // Compose middleware chain — same shape as PollChangeSource.\n const inner: ChangeIterator<T> = (sub, cur) => this.fetch(sub, cur);\n const middlewares = opts.middlewares ?? [];\n this.composed = middlewares.reduceRight<ChangeIterator<T>>(\n (next, mw) => mw(next),\n inner,\n );\n }\n\n listChanges(\n subscription: SyncSubscriptionView,\n cursor: unknown | null,\n ): AsyncIterable<Change<T>> {\n return this.composed(subscription, cursor);\n }\n\n private async *fetch(\n subscription: SyncSubscriptionView,\n cursor: unknown | null,\n ): AsyncIterable<Change<T>> {\n const ctx: WebhookFetchContext = {\n subscription,\n cursor: cursor as WebhookCursor | null,\n };\n\n for await (const { record, cursor: nextCursor } of this.queue(ctx)) {\n const externalIdRaw = (record as Record<string, unknown>)[\n this.externalIdSourceField\n ];\n if (typeof externalIdRaw !== 'string' || externalIdRaw.length === 0) {\n throw new Error(\n `WebhookChangeSource: record missing string '${this.externalIdSourceField}' — emitted records MUST carry the canonical external id keyed by the mapping target`,\n );\n }\n const eventIdRaw = (record as Record<string, unknown>)[\n this.eventIdSourceField\n ];\n if (typeof eventIdRaw !== 'string' || eventIdRaw.length === 0) {\n throw new Error(\n `WebhookChangeSource: record missing string '${this.eventIdSourceField}' — webhook records MUST carry the event id (DetectionConfig.webhook.eventIdField) so Change<T>.dedupKey can be populated`,\n );\n }\n\n const change: Change<T> = {\n externalId: externalIdRaw,\n // Webhook mode cannot distinguish create vs. update vs. delete on\n // its own — the orchestrator's diff stage handles classification.\n // Tombstone / soft-delete detection is consumer-driven (same as\n // poll mode — see ADR-033).\n operation: 'updated',\n record,\n cursor: nextCursor ?? null,\n source: 'webhook',\n dedupKey: eventIdRaw,\n };\n yield change;\n }\n }\n}\n","/**\n * Sync subsystem — `buildChangeSource()` runtime factory (#250, ADR-033.1 b).\n *\n * Mode-dispatching constructor for `IChangeSource<T>`. Codegen-emitted\n * provider modules call this from `useFactory` once they've resolved the\n * provider-keyed `fetches[provider]` callback and parsed the per-entity\n * `DetectionConfig`. Switching is on `cfg.mode` so the option-bag shape\n * difference between primitives (`adapter` vs. `queue`) stays internal —\n * consumers pass one fetch callback regardless of mode.\n */\nimport type { DetectionConfig } from './detection-config.schema';\nimport type { IChangeSource } from './sync-change-source.protocol';\nimport type { ChangeMiddleware } from './sync-middleware.protocol';\nimport {\n PollChangeSource,\n type PollFetchCallback,\n} from './poll-change-source';\nimport {\n WebhookChangeSource,\n type WebhookFetchCallback,\n} from './webhook-change-source';\n\nexport function buildChangeSource<T>(\n cfg: DetectionConfig,\n fetch: PollFetchCallback<T> | WebhookFetchCallback<T>,\n middlewares: ReadonlyArray<ChangeMiddleware<T>> = [],\n): IChangeSource<T> {\n switch (cfg.mode) {\n case 'poll':\n return new PollChangeSource<T>({\n adapter: fetch as PollFetchCallback<T>,\n config: cfg,\n middlewares,\n });\n case 'webhook':\n return new WebhookChangeSource<T>({\n queue: fetch as WebhookFetchCallback<T>,\n config: cfg,\n middlewares,\n });\n }\n}\n","/**\n * Sync subsystem — DI tokens\n *\n * String constants (not Symbols) so they match by value across import\n * boundaries — same convention as the events subsystem (`EVENT_BUS`). The\n * jobs subsystem uses Symbols for its analogous tokens; events and sync\n * stay internally consistent with strings.\n *\n * Usage in use cases:\n * ```ts\n * constructor(\n * @Inject(SYNC_CHANGE_SOURCE) private readonly source: IChangeSource<CanonicalOpportunity>,\n * @Inject(SYNC_CURSOR_STORE) private readonly cursors: ICursorStore,\n * @Inject(SYNC_FIELD_DIFFER) private readonly differ: IFieldDiffer<CanonicalOpportunity>,\n * @Inject(SYNC_SINK) private readonly sink: ISyncSink<CanonicalOpportunity>,\n * @Inject(SYNC_RUN_RECORDER) private readonly recorder: ISyncRunRecorder,\n * ) {}\n * ```\n *\n * Concrete bindings are registered by `SyncModule.forRoot(...)` (SYNC-6).\n */\n\nexport const SYNC_CHANGE_SOURCE = 'SYNC_CHANGE_SOURCE' as const;\nexport const SYNC_CURSOR_STORE = 'SYNC_CURSOR_STORE' as const;\nexport const SYNC_FIELD_DIFFER = 'SYNC_FIELD_DIFFER' as const;\nexport const SYNC_SINK = 'SYNC_SINK' as const;\n\n/**\n * Run-recorder token (SYNC-5). Backed by `ISyncRunRecorder`. Drizzle impl\n * lands in SYNC-4; tests provide inline fakes.\n */\nexport const SYNC_RUN_RECORDER = 'SYNC_RUN_RECORDER' as const;\n\n/**\n * Injection token for the resolved `SyncModuleOptions` object (SYNC-6).\n *\n * Backends that need to observe module configuration (e.g. `multiTenant`\n * flag, pool filters) inject via this token. Provided automatically by\n * `SyncModule.forRoot(...)` / `SyncModule.forRootAsync(...)`.\n */\nexport const SYNC_MODULE_OPTIONS = 'SYNC_MODULE_OPTIONS' as const;\n\n/**\n * Injection token for the resolved multi-tenancy flag (SYNC-6).\n *\n * Provided by `SyncModule.forRoot(...)` as `options.multiTenant ?? false`.\n * Consumed by `ExecuteSyncUseCase` to enforce the tenantId-is-required rule.\n */\nexport const SYNC_MULTI_TENANT = 'SYNC_MULTI_TENANT' as const;\n","/**\n * Typed errors + shared boundary helpers for the sync subsystem.\n *\n * Classes (not bare Error) so consumers can `instanceof` them in catch\n * blocks and exception filters can map them to HTTP codes.\n *\n * Mirrors the shape of `events-errors.ts` and `jobs-errors.ts`.\n */\n\n/**\n * Thrown by the Drizzle cursor-store / run-recorder backends AND by the\n * orchestrator entry point when `SYNC_MULTI_TENANT` is enabled but the\n * caller did not supply a non-null `tenantId`. Strict enforcement at the\n * boundary — explicit `null` still throws.\n *\n * Disable multi-tenancy on the module (`multiTenant: false`, the default)\n * to opt out of the requirement entirely.\n *\n * `operation` identifies the call site (e.g. `'cursor.put'`,\n * `'startRun'`, `'execute'`) so the stack-trace message points at the\n * specific boundary that rejected the input.\n */\nexport class MissingTenantIdError extends Error {\n override readonly name = 'MissingTenantIdError';\n constructor(operation: string) {\n super(\n `Missing tenantId for sync operation '${operation}'. SyncModule is ` +\n `configured with multiTenant: true — every call must include a ` +\n `non-null tenantId. Either pass the tenantId or disable multi-` +\n `tenancy on the module.`,\n );\n }\n}\n\n/**\n * Shared boundary guard — used at the orchestrator entry AND inside the\n * Drizzle backends. Keeping the check in one function guarantees every\n * `MissingTenantIdError` carries the same message shape regardless of the\n * site that raised it, which makes it easier for consumers to pattern-\n * match on the error in logs/metrics.\n *\n * When `multiTenant` is false, the function is a no-op — `tenantId` may\n * be anything (including `undefined`). When true, `undefined` or `null`\n * throws.\n */\nexport function assertTenantId(\n tenantId: string | null | undefined,\n options: { multiTenant: boolean; operation: string },\n): asserts tenantId is string {\n if (!options.multiTenant) return;\n if (tenantId === undefined || tenantId === null) {\n throw new MissingTenantIdError(options.operation);\n }\n}\n","/**\n * Drizzle schema for the sync subsystem audit/observability tables (SYNC-1).\n *\n * Three tables model end-to-end sync observability, keyed by the single port\n * every sync adapter implements (`IChangeSource<T>` from SYNC-2):\n *\n * - `sync_subscriptions` — owns the cursor per\n * `(integration_id, adapter, domain, external_ref)` tuple. Addressed\n * by id by `ICursorStore` (SYNC-3/SYNC-4).\n * - `sync_runs` — per-run audit log: start/complete, status,\n * cursor before/after, counts, direction (inbound|outbound),\n * action (poll|cdc|webhook|manual|writeback).\n * - `sync_run_items` — per-record change log with structured\n * `changed_fields` jsonb enforced by the Zod `FieldDiffSchema`\n * contract (ADR-0003; protocol lives in SYNC-2's\n * sync-field-diff.protocol.ts).\n *\n * Design calls (vs. issue #126 open questions):\n *\n * - `sync_subscriptions` ships in the subsystem (not consumer-owned).\n * Rationale: SYNC-4's `PostgresCursorStore` needs to read/write this\n * table directly; making it consumer-owned would require consumers to\n * hand-wire a shape the backend already knows. The row is addressable\n * by id and scoped by the uniqueness tuple; consumers can still\n * query/list it freely. Same stance as `job_run` being subsystem-\n * owned while remaining consumer-queryable.\n *\n * - `tenant_id` is always emitted on the three tables as nullable text.\n * The `SYNC_MULTI_TENANT` DI flag (SYNC-6) is what enforces the\n * non-null + cross-tenant-isolation contract at the service/orchestrator\n * boundary. This mirrors JOB-1/JOB-8's final shape — runtime guard, not\n * a scaffold-time conditional column. Keeps the schema file uniform\n * across single-tenant and multi-tenant deployments.\n *\n * - `changed_fields` on `sync_run_items` is typed via the Zod-inferred\n * `FieldDiff` shape from SYNC-2 (`{ [fieldName]: { from, to } }`). The\n * recorder service (SYNC-5) validates every write against\n * `FieldDiffSchema.parse` so consumers can rely on the shape.\n */\nimport {\n pgEnum,\n pgTable,\n uuid,\n text,\n jsonb,\n integer,\n boolean,\n timestamp,\n index,\n uniqueIndex,\n} from 'drizzle-orm/pg-core';\nimport type { InferSelectModel } from 'drizzle-orm';\n\nimport type { FieldDiff } from './sync-field-diff.protocol';\n\n// ─── Enums ──────────────────────────────────────────────────────────────────\n\n/**\n * Direction of a sync run relative to local state.\n *\n * - `inbound` — external → local (the common case: SFDC poll → local DB).\n * - `outbound` — local → external (writeback; deferred per epic but the\n * column shape is reserved so future writeback runs share the audit log).\n */\nexport const syncRunDirectionEnum = pgEnum('sync_run_direction', [\n 'inbound',\n 'outbound',\n]);\n\n/**\n * How the run detected upstream changes. Maps 1:1 to the `Change.source`\n * provenance on inbound runs; `manual` captures operator-triggered re-syncs\n * and `writeback` captures outbound runs.\n */\nexport const syncRunActionEnum = pgEnum('sync_run_action', [\n 'poll',\n 'cdc',\n 'webhook',\n 'manual',\n 'writeback',\n]);\n\n/**\n * Lifecycle status of a sync run.\n *\n * - `running` — in-flight; recorder has started but not completed.\n * - `success` — completed with at least one change processed.\n * - `no_changes` — completed cleanly, no upstream changes found.\n * - `failed` — errored before completion; `error` column carries the\n * message. `records_processed` may be non-zero (partial progress).\n */\nexport const syncRunStatusEnum = pgEnum('sync_run_status', [\n 'running',\n 'success',\n 'no_changes',\n 'failed',\n]);\n\n/**\n * Operation applied per record. Mirrors `Change<T>.operation` from SYNC-2,\n * plus the recorder's own `'noop'` for changes that matched existing state.\n */\nexport const syncRunItemOperationEnum = pgEnum('sync_run_item_operation', [\n 'created',\n 'updated',\n 'deleted',\n 'noop',\n]);\n\n/**\n * Per-record status within a run. `skipped` captures loopback-detected echoes\n * of the local system's own writes (see `ILoopbackFingerprintStore` in the\n * epic), which record the external_id but intentionally do not touch local\n * state.\n */\nexport const syncRunItemStatusEnum = pgEnum('sync_run_item_status', [\n 'success',\n 'failed',\n 'skipped',\n]);\n\n// ─── sync_subscriptions ─────────────────────────────────────────────────────\n\n/**\n * One cursor owner per (integration, adapter, domain, external_ref).\n *\n * - `integration_id` — opaque id of the connected account/instance. E.g.\n * the SFDC org id for polling strategies, the GitHub installation id\n * for webhook strategies.\n * - `adapter` — short adapter label, e.g. `'salesforce'`, `'hubspot'`.\n * - `domain` — canonical entity domain this subscription tracks,\n * e.g. `'opportunity'`, `'contact'`.\n * - `external_ref` — optional upstream scope (e.g. a filter id, a\n * webhook subscription id). NULL when the subscription covers the\n * entire domain.\n *\n * The cursor shape is opaque jsonb — strategies type it internally (poll:\n * `{ systemModstamp }`, cdc: `{ replayId }`, webhook: `{ ts }`). Overwritten\n * by `ICursorStore.put(id, cursor)`.\n */\nexport const syncSubscriptions = pgTable(\n 'sync_subscriptions',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n integrationId: text('integration_id').notNull(),\n adapter: text('adapter').notNull(),\n domain: text('domain').notNull(),\n externalRef: text('external_ref'),\n enabled: boolean('enabled').notNull().default(true),\n /**\n * Per-subscription configuration bag. Strategies type it internally;\n * e.g. polling strategies stash `{ batchSize, highWatermark }` here.\n */\n config: jsonb('config').notNull().default({}).$type<Record<string, unknown>>(),\n /**\n * Opaque cursor persisted by `ICursorStore.put()`. NULL until the first\n * successful run advances it.\n */\n cursor: jsonb('cursor').$type<unknown>(),\n lastSyncAt: timestamp('last_sync_at', { withTimezone: true }),\n /** Runtime-enforced when `SYNC_MULTI_TENANT` is true; see SYNC-6. */\n tenantId: text('tenant_id'),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (t) => ({\n /**\n * Composite uniqueness per the epic shape. `external_ref` is nullable;\n * Postgres treats NULLs as distinct in a UNIQUE constraint, which means\n * two rows with the same `(integration_id, adapter, domain)` and NULL\n * external_ref are allowed. That's intentional — a subscription with\n * NULL external_ref covers the full domain, and duplicates there would\n * be a consumer-layer modeling issue, not a schema concern.\n */\n uqSyncSubscriptionTuple: uniqueIndex('uq_sync_subscriptions_tuple').on(\n t.integrationId,\n t.adapter,\n t.domain,\n t.externalRef,\n ),\n /** Scheduling query: list enabled subscriptions ordered by staleness. */\n idxSyncSubscriptionsEnabledLastSync: index(\n 'idx_sync_subscriptions_enabled_last_sync',\n ).on(t.enabled, t.lastSyncAt),\n }),\n);\n\nexport type SyncSubscriptionRow = InferSelectModel<typeof syncSubscriptions>;\n\n// ─── sync_runs ──────────────────────────────────────────────────────────────\n\n/**\n * One row per invocation of `ExecuteSyncUseCase`. `started_at` is set when\n * the recorder opens the run; `completed_at`, `status`, `records_*`,\n * `cursor_after`, and `duration_ms` are filled on completion.\n *\n * `cursor_before` / `cursor_after` carry the opaque cursor snapshots so the\n * run log is fully self-describing — given a run id, an operator can reason\n * about exactly what window was scanned without cross-referencing another\n * table.\n */\nexport const syncRuns = pgTable(\n 'sync_runs',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n subscriptionId: uuid('subscription_id')\n .notNull()\n .references(() => syncSubscriptions.id, { onDelete: 'cascade' }),\n direction: syncRunDirectionEnum('direction').notNull(),\n action: syncRunActionEnum('action').notNull(),\n status: syncRunStatusEnum('status').notNull().default('running'),\n recordsFound: integer('records_found').notNull().default(0),\n recordsProcessed: integer('records_processed').notNull().default(0),\n cursorBefore: jsonb('cursor_before').$type<unknown>(),\n cursorAfter: jsonb('cursor_after').$type<unknown>(),\n durationMs: integer('duration_ms'),\n error: text('error'),\n startedAt: timestamp('started_at', { withTimezone: true })\n .notNull()\n .defaultNow(),\n completedAt: timestamp('completed_at', { withTimezone: true }),\n /** Runtime-enforced when `SYNC_MULTI_TENANT` is true; see SYNC-6. */\n tenantId: text('tenant_id'),\n },\n (t) => ({\n /** Timeline read: \"most recent runs for this subscription\". */\n idxSyncRunsSubscriptionStartedAt: index(\n 'idx_sync_runs_subscription_started_at',\n ).on(t.subscriptionId, t.startedAt),\n /** Stale-run sweeper: \"runs that started > N minutes ago and are still running\". */\n idxSyncRunsStatusStartedAt: index('idx_sync_runs_status_started_at').on(\n t.status,\n t.startedAt,\n ),\n }),\n);\n\nexport type SyncRunRow = InferSelectModel<typeof syncRuns>;\n\n// ─── sync_run_items ─────────────────────────────────────────────────────────\n\n/**\n * One row per upstream change processed within a run. Captures the canonical\n * decision the orchestrator made (`operation` + `status`), the structured\n * per-field diff (`changed_fields`, ADR-0003), and the local row id\n * (`local_id`) for drill-down joins.\n *\n * `changed_fields` is validated at the recorder layer via `FieldDiffSchema`\n * (SYNC-2) — the $type<FieldDiff> annotation here only documents the shape\n * for Drizzle consumers. The runtime enforcement is non-negotiable: downstream\n * drift-detection queries rely on the `{from, to}` shape per field.\n *\n * `title` is an optional human-readable label captured at write time (e.g.\n * `\"Pinnacle opportunity\"`) so run-log UIs don't need to re-hydrate the\n * canonical record.\n */\nexport const syncRunItems = pgTable(\n 'sync_run_items',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n syncRunId: uuid('sync_run_id')\n .notNull()\n .references(() => syncRuns.id, { onDelete: 'cascade' }),\n entityType: text('entity_type').notNull(),\n externalId: text('external_id').notNull(),\n localId: text('local_id'),\n operation: syncRunItemOperationEnum('operation').notNull(),\n status: syncRunItemStatusEnum('status').notNull(),\n /**\n * Structured per-field diff — ADR-0003 shape enforced by\n * `FieldDiffSchema.parse` at the recorder service layer.\n *\n * Shape: `{ [fieldName]: { from: unknown, to: unknown } }`.\n * Empty `{}` for `noop` items; `{ [field]: { from: null, to: <value> } }`\n * for created items; `{ [field]: { from: <value>, to: null } }` for\n * deleted items.\n */\n changedFields: jsonb('changed_fields').notNull().default({}).$type<FieldDiff>(),\n title: text('title'),\n error: text('error'),\n createdAt: timestamp('created_at', { withTimezone: true })\n .notNull()\n .defaultNow(),\n /** Runtime-enforced when `SYNC_MULTI_TENANT` is true; see SYNC-6. */\n tenantId: text('tenant_id'),\n },\n (t) => ({\n /** Ordered timeline within a run. */\n idxSyncRunItemsRunCreatedAt: index('idx_sync_run_items_run_created_at').on(\n t.syncRunId,\n t.createdAt,\n ),\n /** Per-record history: \"every sync that touched opportunity/$extId\". */\n idxSyncRunItemsEntityExternal: index(\n 'idx_sync_run_items_entity_external',\n ).on(t.entityType, t.externalId),\n }),\n);\n\nexport type SyncRunItemRow = InferSelectModel<typeof syncRunItems>;\n","/**\n * MemoryCursorStore — in-memory backend for `ICursorStore` (SYNC-3).\n *\n * Test double that lets consumers exercise `ExecuteSyncUseCase` (SYNC-5) and\n * other cursor-consuming code paths without Postgres. Mirrors the role of\n * `MemoryEventBus` and `MemoryJobStore`: plain keyed state, tests take a\n * direct reference for `beforeEach` resets.\n *\n * Cursor values are stored by reference — the port's `get`/`put` contract\n * treats them as opaque `unknown`. Callers that want durable value-equality\n * semantics should snapshot via JSON before `put` and reparse after `get`;\n * this is what the Drizzle backend (SYNC-4) does implicitly via jsonb\n * round-trip. The memory backend intentionally does not simulate the\n * serialize/deserialize cycle — consumers who care should test against\n * Postgres.\n *\n * ## Multi-tenancy\n *\n * `tenantId` is accepted but ignored. The memory backend's state is\n * process-local — there's no durable storage where a cross-tenant leak\n * could occur. Tests that want to assert per-tenant isolation should\n * target the Drizzle backend.\n *\n * Not shipped in dealbrain-v2; this is a subsystem-first addition for the\n * test surface. Consumed by:\n * - SYNC-5 unit tests (`ExecuteSyncUseCase` against synthetic sources)\n * - SYNC-6 module tests (`SyncModule.forRoot({ backend: 'memory' })`)\n */\nimport { Injectable } from '@nestjs/common';\nimport type {\n CursorSnapshot,\n ICursorStore,\n} from './sync-cursor-store.protocol';\nimport type { MemorySyncSubscription } from './sync-run-recorder.memory-backend';\n\n@Injectable()\nexport class MemoryCursorStore implements ICursorStore {\n /**\n * Subscription-id → last persisted cursor. Public so tests can inspect\n * or pre-seed state; production callers MUST go through `get`/`put`.\n */\n readonly cursors: Map<string, unknown> = new Map();\n\n /**\n * Seedable subscription metadata for `listAll` — the memory backend\n * stores only `subscriptionId → cursor` in its write path, so the\n * snapshot shape (`integrationId`, `adapter`, `domain`, `externalRef`,\n * timestamps) has no natural source without test seeding. Tests populate\n * this map; unseeded entries get empty-string metadata and `new Date(0)`\n * timestamps so the shape stays stable. Production paths go through the\n * Drizzle backend.\n */\n readonly subscriptions: Map<string, MemorySyncSubscription> = new Map();\n\n async get(\n subscriptionId: string,\n _tenantId?: string | null,\n ): Promise<unknown | null> {\n // `Map.get` returns `undefined` for missing keys; the port contract\n // returns `null`. Normalize here so callers can `=== null`-check.\n const value = this.cursors.get(subscriptionId);\n return value === undefined ? null : value;\n }\n\n async put(\n subscriptionId: string,\n cursor: unknown,\n _tenantId?: string | null,\n ): Promise<void> {\n // Overwrite semantics — matches the port contract and the Drizzle\n // backend's `ON CONFLICT DO UPDATE` behavior.\n this.cursors.set(subscriptionId, cursor);\n }\n\n async listAll(_tenantId?: string | null): Promise<CursorSnapshot[]> {\n // Accepts tenantId for contract symmetry but does not filter on it —\n // the memory backend never enforces tenancy (see class-level comment).\n const snapshots: CursorSnapshot[] = [];\n for (const [subscriptionId, cursor] of this.cursors.entries()) {\n const meta = this.subscriptions.get(subscriptionId);\n snapshots.push({\n subscriptionId,\n integrationId: meta?.integrationId ?? '',\n adapter: meta?.adapter ?? '',\n domain: meta?.domain ?? '',\n externalRef: meta?.externalRef ?? null,\n cursor: cursor ?? null,\n lastSyncAt: meta?.lastSyncAt ?? null,\n updatedAt: meta?.updatedAt ?? new Date(0),\n tenantId: null,\n });\n }\n return snapshots.sort(\n (a, b) => b.updatedAt.getTime() - a.updatedAt.getTime(),\n );\n }\n\n /** Reset state. Tests call this in `beforeEach`. */\n clear(): void {\n this.cursors.clear();\n this.subscriptions.clear();\n }\n}\n","/**\n * MemoryRunRecorder — in-memory backend for `ISyncRunRecorder` (SYNC-6).\n *\n * Test double so `SyncModule.forRoot({ backend: 'memory' })` is genuinely\n * end-to-end runnable without Postgres. Mirrors the role of\n * `MemoryCursorStore`: plain keyed state, `clear()` helper for\n * `beforeEach` resets, public inspection surface so tests can assert on\n * the recorded run + item timeline without scraping logs.\n *\n * Validates `changedFields` through `FieldDiffSchema.parse` on every\n * `recordItem` call — same ADR-0003 contract as the Drizzle backend. An\n * in-memory recorder that skipped the validation would be a silently\n * weaker contract than production.\n *\n * `startRun` generates a uuid via `crypto.randomUUID()` (Node 19+ / Bun).\n * We don't import `uuid` because the subsystem has no other use for it.\n *\n * ## Multi-tenancy\n *\n * `tenantId` is accepted (and recorded on the in-memory row so tests can\n * assert it) but enforcement lives at the module boundary. The memory\n * backend intentionally does not throw on missing `tenantId` — that's\n * the orchestrator's job when `multiTenant=true` (SYNC-6). A permissive\n * memory recorder lets tests exercise error paths where the orchestrator\n * short-circuits before ever reaching the recorder.\n */\nimport { Injectable } from '@nestjs/common';\nimport type {\n CompleteRunInput,\n ISyncRunRecorder,\n RecordItemInput,\n StartRunInput,\n SyncRunSummary,\n} from './sync-run-recorder.protocol';\nimport { FieldDiffSchema } from './sync-field-diff.protocol';\n\n/**\n * Optional per-subscription metadata a test can seed on the memory backend\n * so `listRecent` can surface `integrationId`. The memory recorder's write\n * path never persists subscription rows (it only stores runs + items), so\n * `listRecent` has no way to derive `integrationId` on its own. Tests that\n * care about the field should populate `subscriptions` before calling\n * `listRecent`; calls that don't seed get an empty string and a stable\n * shape (see `listRecent` below).\n */\nexport interface MemorySyncSubscription {\n integrationId: string;\n adapter: string;\n domain: string;\n externalRef: string | null;\n lastSyncAt?: Date | null;\n updatedAt: Date;\n}\n\n/**\n * Concrete run row as held in memory. Shape mirrors the interesting\n * columns on `sync_runs` so assertions read like DB queries.\n */\nexport interface MemoryRunRecord {\n id: string;\n subscriptionId: string;\n direction: 'inbound' | 'outbound';\n action: 'poll' | 'cdc' | 'webhook' | 'manual' | 'writeback';\n status: 'running' | 'success' | 'no_changes' | 'failed';\n cursorBefore: unknown | null;\n cursorAfter: unknown | null;\n recordsFound: number;\n recordsProcessed: number;\n durationMs: number | null;\n error: string | null;\n tenantId: string | null;\n startedAt: Date;\n completedAt: Date | null;\n}\n\n@Injectable()\nexport class MemoryRunRecorder implements ISyncRunRecorder {\n /**\n * All started runs keyed by id. Public so tests can inspect lifecycle\n * transitions without poking through recording methods.\n */\n readonly runs: Map<string, MemoryRunRecord> = new Map();\n\n /**\n * Items keyed by `sync_run_id`, array order matches insertion order —\n * mirrors the timeline the `(sync_run_id, created_at)` index produces\n * in Postgres.\n */\n readonly items: Map<string, RecordItemInput[]> = new Map();\n\n /**\n * Seedable subscription metadata — tests populate this to make\n * `listRecent` return meaningful `integrationId` values. The memory\n * backend doesn't track subscriptions on its own (only runs + items), so\n * this map is the intentional extension point: tests write entries for\n * the subscription ids they use, production code never touches it.\n */\n readonly subscriptions: Map<string, MemorySyncSubscription> = new Map();\n\n async startRun(input: StartRunInput): Promise<{ id: string }> {\n const id = crypto.randomUUID();\n this.runs.set(id, {\n id,\n subscriptionId: input.subscriptionId,\n direction: input.direction,\n action: input.action,\n status: 'running',\n cursorBefore: input.cursorBefore ?? null,\n cursorAfter: null,\n recordsFound: 0,\n recordsProcessed: 0,\n durationMs: null,\n error: null,\n tenantId: input.tenantId ?? null,\n startedAt: new Date(),\n completedAt: null,\n });\n this.items.set(id, []);\n return { id };\n }\n\n async recordItem(input: RecordItemInput): Promise<void> {\n // Same ADR-0003 contract as the Drizzle backend.\n FieldDiffSchema.parse(input.changedFields);\n\n const bucket = this.items.get(input.syncRunId);\n if (!bucket) {\n throw new Error(\n `MemoryRunRecorder.recordItem: no run started for id '${input.syncRunId}'. ` +\n `Call startRun(...) first.`,\n );\n }\n bucket.push(input);\n }\n\n async completeRun(runId: string, input: CompleteRunInput): Promise<void> {\n const run = this.runs.get(runId);\n if (!run) {\n throw new Error(\n `MemoryRunRecorder.completeRun: no run started for id '${runId}'.`,\n );\n }\n run.status = input.status;\n run.recordsFound = input.recordsFound;\n run.recordsProcessed = input.recordsProcessed;\n run.cursorAfter = input.cursorAfter ?? null;\n run.durationMs = input.durationMs;\n run.error = input.error ?? null;\n run.completedAt = new Date();\n }\n\n async listRecent(\n limit: number,\n subscriptionId?: string,\n _tenantId?: string | null,\n ): Promise<SyncRunSummary[]> {\n // Memory backend accepts tenantId for contract symmetry but does not\n // filter on it — state is process-local and cross-tenant isolation is\n // not meaningful here (matches MemoryCursorStore behavior).\n const all = Array.from(this.runs.values());\n const filtered =\n subscriptionId === undefined\n ? all\n : all.filter((r) => r.subscriptionId === subscriptionId);\n return filtered\n .sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime())\n .slice(0, limit)\n .map((r) => ({\n id: r.id,\n subscriptionId: r.subscriptionId,\n // integrationId is only knowable if the test seeded subscriptions\n // metadata; empty string otherwise. The Drizzle backend resolves\n // it via JOIN, which is the production path.\n integrationId:\n this.subscriptions.get(r.subscriptionId)?.integrationId ?? '',\n status: r.status,\n startedAt: r.startedAt,\n completedAt: r.completedAt,\n recordsProcessed: r.recordsProcessed,\n tenantId: r.tenantId,\n }));\n }\n\n /** Reset state. Tests call this in `beforeEach`. */\n clear(): void {\n this.runs.clear();\n this.items.clear();\n this.subscriptions.clear();\n }\n\n // ─── test ergonomics ─────────────────────────────────────────────────\n\n /** All runs for a subscription, newest first. Timeline reads. */\n getRunsForSubscription(subscriptionId: string): MemoryRunRecord[] {\n return Array.from(this.runs.values())\n .filter((r) => r.subscriptionId === subscriptionId)\n .sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime());\n }\n\n /** All item rows for a run, insertion-ordered. */\n getItemsForRun(runId: string): RecordItemInput[] {\n return this.items.get(runId) ?? [];\n }\n}\n","/**\n * DeepEqualDiffer — default `IFieldDiffer<T>` for the sync 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 dealbrain-v2 + 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 './sync-field-diff.protocol';\n\n/**\n * Default ignore list. Keep in sync with consumer canonical-record shapes —\n * adding a row-metadata field here means no sync will ever mark it changed.\n */\nconst DEFAULT_IGNORE_FIELDS: ReadonlySet<string> = new Set([\n 'id',\n 'createdAt',\n 'updatedAt',\n 'deletedAt',\n 'type',\n 'lastModifiedAt',\n 'fields',\n 'providerMetadata',\n]);\n\nexport interface DeepEqualDifferOptions {\n /**\n * Extra field names to ignore in addition to the defaults. Consumers can\n * pass `['sync_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-sync 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","/**\n * ExecuteSyncUseCase — the generic sync orchestrator (SYNC-5).\n *\n * One class. Reused across every `(provider, detection-mode, canonical-entity)`\n * tuple. Parameterized over `T` so canonical records stay typed end-to-end.\n *\n * Flow per run:\n *\n * 1. `recorder.startRun(...)` — opens a `sync_runs` row in 'running'.\n * 2. for each change yielded by `source.listChanges(subscription, cursorBefore)`:\n * a. differ.diff(existing, incoming) → 'noop' short-circuits to\n * a noop audit row (no sink write).\n * b. sink.upsertByExternalId / softDeleteByExternalId → records\n * the local id on the audit row.\n * c. per-item try/catch — a failed item increments the failed\n * counter and records `status: 'failed'` with `error`, but\n * does NOT abort the run.\n * d. advance `latestCursor = change.cursor` as the iterator moves.\n * 3. `cursors.put(subscription.id, latestCursor)` when the loop completes\n * AND at least one cursor advance happened. On exceptions from the\n * source iterator (auth expiry, network error), we persist the\n * last-good cursor so the next run resumes from the last known\n * successful position.\n * 4. `finally { recorder.completeRun(...) }` — always terminates the run.\n *\n * Loopback suppression — when a consumer's writes echo back on the next\n * inbound poll/CDC/webhook — is composed into the source's middleware\n * chain via `createLoopbackMiddleware(store)` (#226-5 / ADR-033). The\n * orchestrator no longer special-cases echoes: middleware drops them\n * before they reach this loop. Consumers that don't have outbound\n * writeback paths simply omit the middleware.\n *\n * ## Generics\n *\n * - `T` = canonical record shape from the adapter side. Same `T` flows\n * through `IChangeSource<T>`, `IFieldDiffer<T>`, `ISyncSink<T>`.\n *\n * ## No CRM bleed\n *\n * Per the SYNC-5 issue's extraction notes (HS-9 finding), this orchestrator\n * is strictly provider-agnostic:\n * - `entityType` is `string` throughout; no `'opportunity' | 'account' | ...`\n * narrowing leaks into the use case\n * - dealbrain's `SyncRunRecorderService` class injection replaced with the\n * `ISyncRunRecorder` protocol (backend lands in SYNC-4)\n */\nimport { Inject, Injectable, Logger, Optional } from '@nestjs/common';\nimport type { IChangeSource, Change } from './sync-change-source.protocol';\nimport type { ICursorStore } from './sync-cursor-store.protocol';\nimport type { IFieldDiffer, FieldDiff } from './sync-field-diff.protocol';\nimport type { ISyncSink } from './sync-sink.protocol';\nimport type { ISyncRunRecorder } from './sync-run-recorder.protocol';\nimport { assertTenantId } from './sync-errors';\nimport {\n SYNC_CHANGE_SOURCE,\n SYNC_CURSOR_STORE,\n SYNC_FIELD_DIFFER,\n SYNC_MULTI_TENANT,\n SYNC_RUN_RECORDER,\n SYNC_SINK,\n} from './sync.tokens';\n\n// ============================================================================\n// Inputs + result\n// ============================================================================\n\nexport interface ExecuteSyncInput<T> {\n /** The subscription whose cursor/identity frames this run. */\n readonly subscription: {\n readonly id: string;\n readonly domain: string; // entityType — used on audit rows\n readonly externalRef?: string | null;\n };\n /** Per-run user context; threaded through sink writes. */\n readonly userId: string;\n /** Provider label persisted on saved rows, e.g. `'salesforce-crm'`. */\n readonly provider: string;\n /** Run direction — almost always `'inbound'`. Reserved for writeback. */\n readonly direction: 'inbound' | 'outbound';\n /** Detection mode — maps 1:1 to `sync_runs.action`. */\n readonly action: 'poll' | 'cdc' | 'webhook' | 'manual' | 'writeback';\n /** Multi-tenant deployments pass the tenant id through. */\n readonly tenantId?: string | null;\n /**\n * Optional override — inject a specific change source for this run when\n * the DI-bound source is not the one to use (e.g. manual backfill with\n * a custom cursor). Defaults to the DI-resolved `SYNC_CHANGE_SOURCE`.\n */\n readonly sourceOverride?: IChangeSource<T>;\n}\n\nexport interface ExecuteSyncResult {\n readonly runId: string;\n readonly status: 'success' | 'no_changes' | 'failed';\n readonly recordsFound: number;\n readonly recordsProcessed: number;\n readonly recordsFailed: number;\n readonly cursorBefore: unknown | null;\n readonly cursorAfter: unknown | null;\n readonly durationMs: number;\n readonly error?: string | null;\n}\n\n// ============================================================================\n// ExecuteSyncUseCase\n// ============================================================================\n\n@Injectable()\nexport class ExecuteSyncUseCase<T extends Record<string, unknown>> {\n private readonly logger = new Logger(ExecuteSyncUseCase.name);\n\n constructor(\n @Inject(SYNC_CHANGE_SOURCE) private readonly source: IChangeSource<T>,\n @Inject(SYNC_CURSOR_STORE) private readonly cursors: ICursorStore,\n @Inject(SYNC_FIELD_DIFFER) private readonly differ: IFieldDiffer<T>,\n @Inject(SYNC_SINK) private readonly sink: ISyncSink<T>,\n @Inject(SYNC_RUN_RECORDER) private readonly recorder: ISyncRunRecorder,\n @Optional()\n @Inject(SYNC_MULTI_TENANT)\n private readonly multiTenant: boolean = false,\n ) {}\n\n async execute(input: ExecuteSyncInput<T>): Promise<ExecuteSyncResult> {\n // Defense-in-depth tenancy guard — fire BEFORE startRun so a rejected\n // input never leaves a dangling `status=running` row. Backends also\n // enforce (SYNC-4), but failing fast at the orchestrator boundary is\n // cheaper for observability, metrics, and manual cleanup.\n assertTenantId(input.tenantId, {\n multiTenant: this.multiTenant,\n operation: 'execute',\n });\n\n const source = input.sourceOverride ?? this.source;\n const startedAt = Date.now();\n const cursorBefore = await this.cursors.get(input.subscription.id, input.tenantId);\n\n const { id: runId } = await this.recorder.startRun({\n subscriptionId: input.subscription.id,\n direction: input.direction,\n action: input.action,\n cursorBefore,\n tenantId: input.tenantId,\n });\n\n let recordsFound = 0;\n let recordsProcessed = 0;\n let recordsFailed = 0;\n let latestCursor: unknown | null = cursorBefore;\n let cursorAdvanced = false;\n let runError: string | null = null;\n let status: 'success' | 'no_changes' | 'failed' = 'no_changes';\n\n try {\n for await (const change of source.listChanges(input.subscription, cursorBefore)) {\n recordsFound++;\n latestCursor = change.cursor;\n cursorAdvanced = true;\n\n try {\n await this.processChange(runId, input, change);\n recordsProcessed++;\n } catch (err) {\n recordsFailed++;\n const message = err instanceof Error ? err.message : String(err);\n this.logger.warn(\n `sync item failed: subscription=${input.subscription.id} externalId=${change.externalId}: ${message}`,\n );\n await this.recorder.recordItem({\n syncRunId: runId,\n entityType: input.subscription.domain,\n externalId: change.externalId,\n operation: change.operation === 'deleted' ? 'deleted' : 'updated',\n status: 'failed',\n changedFields: {},\n error: message,\n tenantId: input.tenantId,\n });\n }\n }\n\n if (recordsFailed > 0 && recordsProcessed === 0 && recordsFound > 0) {\n // Every record we saw failed — call the run a failure, not a\n // success. Partial success (some processed, some failed) still\n // counts as 'success' so the cursor advances.\n status = 'failed';\n runError = `all ${recordsFailed} records failed`;\n } else if (recordsFound === 0) {\n status = 'no_changes';\n } else {\n status = 'success';\n }\n } catch (err) {\n // Source iterator itself threw — cursor DOES NOT advance past the\n // last-successful cursor. `latestCursor` still holds the last\n // `change.cursor` we observed, which is the furthest we know to\n // have delivered. Persist it (below) so next run resumes there.\n status = 'failed';\n runError = err instanceof Error ? err.message : String(err);\n this.logger.error(\n `sync source failed: subscription=${input.subscription.id}: ${runError}`,\n );\n }\n\n // Persist cursor advance only when something actually moved. Never\n // overwrite a valid cursor with `null` on a no-change run.\n if (cursorAdvanced && latestCursor !== null && latestCursor !== undefined) {\n try {\n await this.cursors.put(input.subscription.id, latestCursor, input.tenantId);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n this.logger.error(\n `cursor put failed: subscription=${input.subscription.id}: ${message}`,\n );\n if (status !== 'failed') {\n status = 'failed';\n runError = `cursor put failed: ${message}`;\n }\n }\n }\n\n const durationMs = Date.now() - startedAt;\n\n await this.recorder.completeRun(runId, {\n status,\n recordsFound,\n recordsProcessed,\n cursorAfter: cursorAdvanced ? latestCursor : cursorBefore,\n durationMs,\n error: runError,\n });\n\n return {\n runId,\n status,\n recordsFound,\n recordsProcessed,\n recordsFailed,\n cursorBefore,\n cursorAfter: cursorAdvanced ? latestCursor : cursorBefore,\n durationMs,\n error: runError,\n };\n }\n\n private async processChange(\n runId: string,\n input: ExecuteSyncInput<T>,\n change: Change<T>,\n ): Promise<void> {\n // Deletion branch — no diff, no upsert; soft-delete via sink.\n if (change.operation === 'deleted') {\n const result = await this.sink.softDeleteByExternalId(\n input.userId,\n change.externalId,\n );\n await this.recorder.recordItem({\n syncRunId: runId,\n entityType: input.subscription.domain,\n externalId: change.externalId,\n localId: result?.id ?? null,\n operation: result ? 'deleted' : 'noop',\n status: 'success',\n changedFields: {},\n tenantId: input.tenantId,\n });\n return;\n }\n\n // Create/update path — diff against local state, short-circuit on noop.\n const existing = await this.sink.findByExternalId(\n input.userId,\n change.externalId,\n );\n const diff = this.differ.diff(\n existing,\n change.record,\n change.providerChangedFields,\n );\n\n if (diff === 'noop') {\n await this.recorder.recordItem({\n syncRunId: runId,\n entityType: input.subscription.domain,\n externalId: change.externalId,\n localId: null,\n operation: 'noop',\n status: 'success',\n changedFields: {},\n tenantId: input.tenantId,\n });\n return;\n }\n\n const { id: localId } = await this.sink.upsertByExternalId(\n input.userId,\n change.record,\n input.provider,\n );\n\n await this.recorder.recordItem({\n syncRunId: runId,\n entityType: input.subscription.domain,\n externalId: change.externalId,\n localId,\n operation: existing === null ? 'created' : 'updated',\n status: 'success',\n changedFields: diff as FieldDiff,\n tenantId: input.tenantId,\n });\n }\n}\n","/**\n * PostgresCursorStore — Drizzle-backed `ICursorStore` (SYNC-4).\n *\n * Reads/writes `sync_subscriptions.cursor` directly — no service\n * composition. Consumers that want a service layer around subscriptions\n * wire it themselves; the port's contract is just cursor persistence.\n *\n * ## What `put` stamps\n *\n * `put` writes three columns in one statement: `cursor`, `last_sync_at`,\n * and `updated_at`. Rationale: SYNC-1's scheduling index\n * `(enabled, last_sync_at)` is useless if `last_sync_at` doesn't advance\n * with every cursor put. Every real consumer needs this stamped, so\n * bundling it here avoids every consumer wrapping the port in a service\n * layer just to stamp a timestamp.\n *\n * ## Multi-tenancy\n *\n * When `SYNC_MULTI_TENANT` is true (SYNC-6):\n * - every read/write is scoped by `AND tenant_id = $tenantId`\n * - a null/missing `tenantId` throws `MissingTenantIdError` via the\n * shared `assertTenantId` helper (one message shape across the\n * orchestrator + both backends, SYNC-6)\n * - explicit `null` also throws — matches JOB-8 / EVT-6 strict-enforcement\n *\n * When the flag is off, `tenantId` is ignored. Cross-tenant isolation is\n * the caller's problem in single-tenant deployments.\n */\nimport { Inject, Injectable, Optional } from '@nestjs/common';\nimport { and, desc, eq, type SQL } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport { DRIZZLE } from '../../constants/tokens';\nimport type {\n CursorSnapshot,\n ICursorStore,\n} from './sync-cursor-store.protocol';\nimport { syncSubscriptions } from './sync-audit.schema';\nimport { SYNC_MULTI_TENANT } from './sync.tokens';\nimport { assertTenantId } from './sync-errors';\n\n@Injectable()\nexport class PostgresCursorStore implements ICursorStore {\n private readonly multiTenant: boolean;\n\n constructor(\n @Inject(DRIZZLE) private readonly db: DrizzleClient,\n @Optional() @Inject(SYNC_MULTI_TENANT) multiTenant?: boolean,\n ) {\n this.multiTenant = multiTenant ?? false;\n }\n\n async get(\n subscriptionId: string,\n tenantId?: string | null,\n ): Promise<unknown | null> {\n const where = this.buildWhere(subscriptionId, tenantId, 'cursor.get');\n\n const rows = await this.db\n .select({ cursor: syncSubscriptions.cursor })\n .from(syncSubscriptions)\n .where(where)\n .limit(1);\n\n if (rows.length === 0) return null;\n return rows[0]?.cursor ?? null;\n }\n\n async put(\n subscriptionId: string,\n cursor: unknown,\n tenantId?: string | null,\n ): Promise<void> {\n const where = this.buildWhere(subscriptionId, tenantId, 'cursor.put');\n\n await this.db\n .update(syncSubscriptions)\n .set({\n cursor,\n lastSyncAt: new Date(),\n updatedAt: new Date(),\n })\n .where(where);\n }\n\n async listAll(tenantId?: string | null): Promise<CursorSnapshot[]> {\n assertTenantId(tenantId, {\n multiTenant: this.multiTenant,\n operation: 'cursor.listAll',\n });\n\n const where = this.multiTenant\n ? eq(syncSubscriptions.tenantId, tenantId as string)\n : undefined;\n\n const rows = await this.db\n .select({\n id: syncSubscriptions.id,\n integrationId: syncSubscriptions.integrationId,\n adapter: syncSubscriptions.adapter,\n domain: syncSubscriptions.domain,\n externalRef: syncSubscriptions.externalRef,\n cursor: syncSubscriptions.cursor,\n lastSyncAt: syncSubscriptions.lastSyncAt,\n updatedAt: syncSubscriptions.updatedAt,\n tenantId: syncSubscriptions.tenantId,\n })\n .from(syncSubscriptions)\n .where(where)\n .orderBy(desc(syncSubscriptions.updatedAt));\n\n return rows.map((row) => ({\n subscriptionId: row.id,\n integrationId: row.integrationId,\n adapter: row.adapter,\n domain: row.domain,\n externalRef: row.externalRef,\n cursor: row.cursor ?? null,\n lastSyncAt: row.lastSyncAt,\n updatedAt: row.updatedAt,\n tenantId: row.tenantId,\n }));\n }\n\n /**\n * Centralized WHERE clause — `get` and `put` share identical semantics.\n * Drift here would let a caller read under one tenancy rule and write\n * under another.\n */\n private buildWhere(\n subscriptionId: string,\n tenantId: string | null | undefined,\n operation: string,\n ): SQL | undefined {\n assertTenantId(tenantId, {\n multiTenant: this.multiTenant,\n operation,\n });\n if (this.multiTenant) {\n return and(\n eq(syncSubscriptions.id, subscriptionId),\n eq(syncSubscriptions.tenantId, tenantId as string),\n );\n }\n return eq(syncSubscriptions.id, subscriptionId);\n }\n}\n","/**\n * NestJS injection tokens\n *\n * Used with @Inject() decorator in concrete repository constructors.\n */\n\n/**\n * Injection token for the Drizzle ORM database client.\n *\n * Usage in concrete repositories:\n * ```typescript\n * constructor(@Inject(DRIZZLE) db: DrizzleClient) { super(db); }\n * ```\n */\nexport const DRIZZLE = 'DRIZZLE' as const;\n\n/**\n * Injection token for the event bus (IEventBus).\n *\n * Optional — only resolved when EventsModule.forRoot() is registered.\n * BaseService uses this with @Optional() to emit lifecycle events\n * without requiring the events subsystem to be installed.\n *\n * Usage in services/use cases:\n * ```typescript\n * @Optional() @Inject(EVENT_BUS) eventBus?: IEventBus\n * ```\n */\nexport const EVENT_BUS = 'EVENT_BUS' as const;\n","/**\n * DrizzleSyncRunRecorder — Drizzle-backed `ISyncRunRecorder` (SYNC-4).\n *\n * Generic write path only — extracted from dealbrain-v2's\n * `SyncRunRecorderService`, minus CRM-specific convenience methods. Those\n * stay consumer-owned; the subsystem ships the substrate.\n *\n * ## Responsibilities\n *\n * - `startRun` — INSERT sync_runs row in status='running', returns id.\n * - `recordItem` — validates `changedFields` via `FieldDiffSchema.parse`\n * BEFORE the INSERT; a malformed shape throws before\n * any DB call fires. Enforces the ADR-0003 contract at\n * the write boundary.\n * - `completeRun` — UPDATE sync_runs with terminal status, counts,\n * cursor_after, duration_ms, completed_at.\n *\n * ## Multi-tenancy\n *\n * When `SYNC_MULTI_TENANT` is true (SYNC-6):\n * - `startRun` and `recordItem` require non-null `tenantId` on input.\n * Enforcement goes through the shared `assertTenantId` helper so the\n * error message shape matches the orchestrator entry point + the\n * cursor-store backends.\n * - `completeRun` does NOT re-check tenancy — the run id was returned\n * by `startRun` which already enforced it, and run ids are uuids that\n * aren't guessable cross-tenant. Matches JOB-3's pattern of trusting\n * the run-id for downstream mutations.\n */\nimport { Inject, Injectable, Optional } from '@nestjs/common';\nimport { and, desc, eq, type SQL } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport { DRIZZLE } from '../../constants/tokens';\nimport type {\n CompleteRunInput,\n ISyncRunRecorder,\n RecordItemInput,\n StartRunInput,\n SyncRunSummary,\n} from './sync-run-recorder.protocol';\nimport { syncRuns, syncRunItems, syncSubscriptions } from './sync-audit.schema';\nimport { FieldDiffSchema } from './sync-field-diff.protocol';\nimport { SYNC_MULTI_TENANT } from './sync.tokens';\nimport { assertTenantId } from './sync-errors';\n\n@Injectable()\nexport class DrizzleSyncRunRecorder implements ISyncRunRecorder {\n private readonly multiTenant: boolean;\n\n constructor(\n @Inject(DRIZZLE) private readonly db: DrizzleClient,\n @Optional() @Inject(SYNC_MULTI_TENANT) multiTenant?: boolean,\n ) {\n this.multiTenant = multiTenant ?? false;\n }\n\n async startRun(input: StartRunInput): Promise<{ id: string }> {\n assertTenantId(input.tenantId, {\n multiTenant: this.multiTenant,\n operation: 'startRun',\n });\n\n const rows = await this.db\n .insert(syncRuns)\n .values({\n subscriptionId: input.subscriptionId,\n direction: input.direction,\n action: input.action,\n status: 'running',\n cursorBefore: input.cursorBefore ?? null,\n tenantId: input.tenantId ?? null,\n })\n .returning({ id: syncRuns.id });\n\n const id = rows[0]?.id;\n if (!id) {\n // Drizzle's insert().returning() contract: at least one row is\n // returned for every successful INSERT. A missing id would indicate\n // a driver misbehavior; throw loudly rather than return bogus data.\n throw new Error('DrizzleSyncRunRecorder: INSERT RETURNING produced no id');\n }\n return { id };\n }\n\n async recordItem(input: RecordItemInput): Promise<void> {\n assertTenantId(input.tenantId, {\n multiTenant: this.multiTenant,\n operation: 'recordItem',\n });\n\n // ADR-0003 contract enforcement — reject malformed changedFields\n // before the DB call fires. `parse` throws a ZodError; callers see\n // the validation failure, not a DB constraint error.\n FieldDiffSchema.parse(input.changedFields);\n\n await this.db.insert(syncRunItems).values({\n syncRunId: input.syncRunId,\n entityType: input.entityType,\n externalId: input.externalId,\n localId: input.localId ?? null,\n operation: input.operation,\n status: input.status,\n changedFields: input.changedFields,\n title: input.title ?? null,\n error: input.error ?? null,\n tenantId: input.tenantId ?? null,\n });\n }\n\n async listRecent(\n limit: number,\n subscriptionId?: string,\n tenantId?: string | null,\n ): Promise<SyncRunSummary[]> {\n assertTenantId(tenantId, {\n multiTenant: this.multiTenant,\n operation: 'listRecent',\n });\n\n // JOIN against sync_subscriptions to resolve `integration_id` per run.\n // `sync_runs.subscription_id` is a non-null FK so INNER JOIN is correct;\n // there should be no orphaned runs.\n const conditions: SQL[] = [];\n if (subscriptionId !== undefined) {\n conditions.push(eq(syncRuns.subscriptionId, subscriptionId));\n }\n if (this.multiTenant) {\n conditions.push(eq(syncRuns.tenantId, tenantId as string));\n }\n const where =\n conditions.length === 0\n ? undefined\n : conditions.length === 1\n ? conditions[0]\n : and(...conditions);\n\n const rows = await this.db\n .select({\n id: syncRuns.id,\n subscriptionId: syncRuns.subscriptionId,\n integrationId: syncSubscriptions.integrationId,\n status: syncRuns.status,\n startedAt: syncRuns.startedAt,\n completedAt: syncRuns.completedAt,\n recordsProcessed: syncRuns.recordsProcessed,\n tenantId: syncRuns.tenantId,\n })\n .from(syncRuns)\n .innerJoin(\n syncSubscriptions,\n eq(syncRuns.subscriptionId, syncSubscriptions.id),\n )\n .where(where)\n .orderBy(desc(syncRuns.startedAt))\n .limit(limit);\n\n return rows.map((row) => ({\n id: row.id,\n subscriptionId: row.subscriptionId,\n integrationId: row.integrationId,\n status: row.status,\n startedAt: row.startedAt,\n completedAt: row.completedAt,\n recordsProcessed: row.recordsProcessed,\n tenantId: row.tenantId,\n }));\n }\n\n async completeRun(runId: string, input: CompleteRunInput): Promise<void> {\n await this.db\n .update(syncRuns)\n .set({\n status: input.status,\n recordsFound: input.recordsFound,\n recordsProcessed: input.recordsProcessed,\n cursorAfter: input.cursorAfter ?? null,\n durationMs: input.durationMs,\n error: input.error ?? null,\n completedAt: new Date(),\n })\n .where(eq(syncRuns.id, runId));\n }\n}\n","/**\n * SyncModule — `DynamicModule.forRoot({ backend, multiTenant? })` factory\n * wiring the sync subsystem's substrate (SYNC-6, ADR-008 subsystem pattern).\n *\n * ## What this module provides\n *\n * - `SYNC_CURSOR_STORE` — Drizzle or Memory cursor store\n * - `SYNC_RUN_RECORDER` — Drizzle or Memory run recorder\n * - `SYNC_FIELD_DIFFER` — default `DeepEqualDiffer`\n * - `SYNC_MULTI_TENANT` — resolved boolean flag (defaults to false)\n * - `SYNC_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 * - `SYNC_CHANGE_SOURCE` — per-provider per-entity; consumer binds in\n * their feature module (e.g. `OpportunitySyncModule` 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 * - `SYNC_SINK` — per canonical entity; consumer binds in their feature\n * module.\n * - `ExecuteSyncUseCase` — registered by the feature module alongside\n * its source + sink bindings. Providing the orchestrator here would\n * force Nest to resolve SYNC_CHANGE_SOURCE + SYNC_SINK at module\n * compile time, which fails when the feature module hasn't been\n * imported yet. Consumers register `ExecuteSyncUseCase` 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: [SyncModule.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: SYNC_CHANGE_SOURCE, useClass: SalesforceOpportunitySource },\n * { provide: SYNC_SINK, useClass: OpportunitySyncSink },\n * ExecuteSyncUseCase,\n * ],\n * })\n * export class OpportunitySyncModule {\n * constructor(\n * private readonly execute: ExecuteSyncUseCase<CanonicalOpportunity>,\n * ) {}\n * }\n * ```\n *\n * `global: true` means feature modules do not need to re-import\n * `SyncModule` — the substrate tokens are available project-wide.\n */\nimport { Module, type DynamicModule, type Provider } from '@nestjs/common';\nimport {\n SYNC_CURSOR_STORE,\n SYNC_FIELD_DIFFER,\n SYNC_MODULE_OPTIONS,\n SYNC_MULTI_TENANT,\n SYNC_RUN_RECORDER,\n} from './sync.tokens';\nimport { MemoryCursorStore } from './sync-cursor-store.memory-backend';\nimport { MemoryRunRecorder } from './sync-run-recorder.memory-backend';\nimport { PostgresCursorStore } from './sync-cursor-store.drizzle-backend';\nimport { DrizzleSyncRunRecorder } from './sync-run-recorder.drizzle-backend';\nimport { DeepEqualDiffer } from './deep-equal.differ';\n\nexport interface SyncModuleOptions {\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@Module({})\nexport class SyncModule {\n static forRoot(options: SyncModuleOptions): DynamicModule {\n const multiTenant = options.multiTenant ?? false;\n\n const sharedProviders: Provider[] = [\n { provide: SYNC_MODULE_OPTIONS, useValue: options },\n { provide: SYNC_MULTI_TENANT, useValue: multiTenant },\n // Default differ — consumers can override by binding a different\n // `IFieldDiffer<T>` to `SYNC_FIELD_DIFFER` in their feature module.\n { provide: SYNC_FIELD_DIFFER, useValue: new DeepEqualDiffer() },\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: SYNC_CURSOR_STORE,\n useExisting: MemoryCursorStore,\n },\n { provide: MemoryRunRecorder, useValue: new MemoryRunRecorder() },\n {\n provide: SYNC_RUN_RECORDER,\n useExisting: MemoryRunRecorder,\n },\n ]\n : [\n // Drizzle backends — injected with DRIZZLE (provided by the\n // consumer's DrizzleModule) + the SYNC_MULTI_TENANT flag\n // we bound above.\n { provide: SYNC_CURSOR_STORE, useClass: PostgresCursorStore },\n { provide: SYNC_RUN_RECORDER, useClass: DrizzleSyncRunRecorder },\n ];\n\n return {\n module: SyncModule,\n global: true,\n providers: [...sharedProviders, ...backendProviders],\n exports: [\n SYNC_MODULE_OPTIONS,\n SYNC_MULTI_TENANT,\n SYNC_FIELD_DIFFER,\n SYNC_CURSOR_STORE,\n SYNC_RUN_RECORDER,\n ],\n };\n }\n}\n"],"mappings":"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","names":["z","Injectable","Injectable","Injectable","out","Injectable","Injectable","Injectable","Inject","Injectable","Optional","Injectable","Inject","Optional","Inject","Injectable","Optional","and","desc","eq","eq","and","desc","Injectable","Inject","Optional"]}
1
+ {"version":3,"sources":["../../../../runtime/subsystems/sync/sync-field-diff.protocol.ts","../../../../runtime/subsystems/sync/detection-config.schema.ts","../../../../runtime/subsystems/sync/loopback.middleware.ts","../../../../runtime/subsystems/sync/poll-change-source.ts","../../../../runtime/subsystems/sync/webhook-change-source.ts","../../../../runtime/subsystems/sync/build-change-source.ts","../../../../runtime/subsystems/sync/sync.tokens.ts","../../../../runtime/subsystems/sync/sync-errors.ts","../../../../runtime/subsystems/sync/sync-audit.schema.ts","../../../../runtime/subsystems/sync/sync-cursor-store.memory-backend.ts","../../../../runtime/subsystems/sync/sync-run-recorder.memory-backend.ts","../../../../runtime/subsystems/sync/deep-equal.differ.ts","../../../../runtime/subsystems/sync/execute-sync.use-case.ts","../../../../runtime/subsystems/sync/sync-cursor-store.drizzle-backend.ts","../../../../runtime/constants/tokens.ts","../../../../runtime/subsystems/sync/sync-run-recorder.drizzle-backend.ts","../../../../runtime/subsystems/sync/sync.module.ts"],"sourcesContent":["/**\n * Sync subsystem — field-diff protocol (port)\n *\n * `IFieldDiffer<T>` is the pluggable differ seam. The default implementation\n * (`DeepEqualDiffer`, ships in SYNC-5) walks every field except an ignore\n * list; CDC-aware differs can skip comparison for fields the provider didn't\n * flag as changed.\n *\n * `FieldDiffSchema` is the structural enforcement of the `changed_fields`\n * column per ADR-0003 — enforced at write time by the recorder service so\n * consumers can rely on the shape in downstream queries.\n */\nimport { z } from 'zod';\n\n// ============================================================================\n// FieldDiff shape — the ADR-0003 contract\n// ============================================================================\n\n/**\n * Structured per-field change. Enforced shape for `sync_run_items.changed_fields`.\n *\n * `created` items set `from: null, to: <value>` for every non-null field.\n * `deleted` items set `from: <value>, to: null`.\n * `noop` items carry `{}`.\n */\nexport const FieldDiffValueSchema = z.object({\n from: z.unknown(),\n to: z.unknown(),\n});\n\nexport const FieldDiffSchema = z.record(z.string(), FieldDiffValueSchema);\n\nexport type FieldDiffValue = z.infer<typeof FieldDiffValueSchema>;\nexport type FieldDiff = z.infer<typeof FieldDiffSchema>;\n\n/** Result of comparing a new record against its existing local state. */\nexport type DiffResult = FieldDiff | 'noop';\n\n// ============================================================================\n// IFieldDiffer\n// ============================================================================\n\n/**\n * Pluggable differ. Default ships in SYNC-5 as `DeepEqualDiffer<T>` —\n * deep-equal over every field except an ignore list (`updated_at` and other\n * row metadata). CDC-aware differs restrict comparison to\n * `providerChangedFields` when supplied.\n */\nexport interface IFieldDiffer<T> {\n /**\n * @param existing — current local state, or `null` when the record is new\n * @param incoming — the canonical record coming from the adapter\n * @param providerChangedFields — optional hint from CDC-capable sources;\n * when present, differ may restrict the comparison to these fields\n */\n diff(\n existing: T | null,\n incoming: T,\n providerChangedFields?: string[],\n ): DiffResult;\n}\n","/**\n * Sync subsystem — DetectionConfig schema (#226-1)\n *\n * Canonical Zod schema for per-entity sync detection config. The schema is\n * the single source of truth for filter/mapping shape and is consumed by:\n *\n * 1. Runtime primitives — `PollChangeSource<T>`, `WebhookChangeSource<T>`\n * (#226-3, #226-4) accept a parsed `DetectionConfig` at construction.\n * 2. Codegen — `src/schema/entity-definition.schema.ts` (#226-6) imports\n * this schema so per-entity YAML `detection:` blocks validate against\n * the same shape the runtime enforces.\n *\n * Locked decisions (see ADR-033 + decision memo Q1–Q6):\n * - Filter vocabulary is flat AND of `{ field, op, value }` triples; richer\n * boolean expressions (OR / NOT / nested) are deferred per epic open Q3.\n * - Cursor strategy is a tagged union over the four shapes the three modes\n * need (`systemModstamp`, `replayId`, `timestamp`, `eventId`). Each\n * strategy types its cursor internally; the orchestrator persists what\n * the iterator last yielded (sync skill rule 2).\n * - `mode: 'poll'` may opt into `provenance: 'cdc'` so Stripe-style event\n * endpoints (mechanically a poll, semantically CDC) reuse the poll\n * primitive while emitting `Change<T>.source = 'cdc'`. Long-lived\n * streaming CDC (SFDC Pub-Sub, Debezium) is a separate primitive\n * deferred to #226-8.\n * - `webhook` mode requires `eventIdField` so `WebhookChangeSource<T>`\n * can populate `Change<T>.dedupKey` from the inbound staging row.\n */\nimport { z } from 'zod';\n\n// ============================================================================\n// Field mapping — provider field → canonical target\n// ============================================================================\n\n/**\n * Maps a single provider field onto the canonical record. `transform` is an\n * opt-in tag the adapter callback may inspect (`date-iso`, `decimal-string`,\n * etc.); the schema does not enumerate transforms — adapters interpret them.\n */\nexport const FieldMappingSchema = z.object({\n source: z.string().min(1),\n target: z.string().min(1),\n transform: z.string().min(1).optional(),\n});\n\nexport type FieldMapping = z.infer<typeof FieldMappingSchema>;\n\n// ============================================================================\n// Resolved filter — flat-AND triple\n// ============================================================================\n\n/**\n * A single resolved filter clause applied at fetch time. `value` is `unknown`\n * to admit primitives, arrays (for `in` / `nin`), and dates as ISO strings —\n * adapters interpret per provider.\n */\nexport const ResolvedFilterSchema = z.object({\n field: z.string().min(1),\n op: z.enum(['eq', 'neq', 'in', 'nin', 'gt', 'gte', 'lt', 'lte']),\n value: z.unknown(),\n});\n\nexport type ResolvedFilter = z.infer<typeof ResolvedFilterSchema>;\n\n// ============================================================================\n// Cursor strategy — tagged union over the four shapes the modes need\n// ============================================================================\n\nconst SystemModstampCursorSchema = z.object({\n kind: z.literal('systemModstamp'),\n field: z.string().min(1),\n});\n\nconst ReplayIdCursorSchema = z.object({\n kind: z.literal('replayId'),\n field: z.string().min(1),\n});\n\nconst TimestampCursorSchema = z.object({\n kind: z.literal('timestamp'),\n field: z.string().min(1),\n});\n\nconst EventIdCursorSchema = z.object({\n kind: z.literal('eventId'),\n field: z.string().min(1),\n});\n\nexport const CursorStrategySchema = z.discriminatedUnion('kind', [\n SystemModstampCursorSchema,\n ReplayIdCursorSchema,\n TimestampCursorSchema,\n EventIdCursorSchema,\n]);\n\nexport type CursorStrategy = z.infer<typeof CursorStrategySchema>;\n\n// ============================================================================\n// Mode-specific blocks\n// ============================================================================\n\n/**\n * Poll-mode block. `provenance: 'cdc'` opts the poll primitive into stamping\n * `Change<T>.source = 'cdc'` and populating `dedupKey` from the cursor's\n * `field` — used for Stripe-style event endpoints. Defaults to `'poll'`.\n */\nexport const PollDetectionSchema = z.object({\n cursor: CursorStrategySchema,\n provenance: z.enum(['poll', 'cdc']).optional(),\n});\n\nexport type PollDetection = z.infer<typeof PollDetectionSchema>;\n\n/**\n * Webhook-mode block. `eventIdField` names the column in the consumer-owned\n * inbound staging row that `WebhookChangeSource<T>` reads to set\n * `Change<T>.dedupKey`.\n */\nexport const WebhookDetectionSchema = z.object({\n eventIdField: z.string().min(1),\n});\n\nexport type WebhookDetection = z.infer<typeof WebhookDetectionSchema>;\n\n// ============================================================================\n// DetectionConfig — top-level discriminated union over `mode`\n// ============================================================================\n\nconst PollModeSchema = z.object({\n mode: z.literal('poll'),\n poll: PollDetectionSchema,\n mapping: z.array(FieldMappingSchema).min(1),\n filters: z.array(ResolvedFilterSchema).default([]),\n});\n\nconst WebhookModeSchema = z.object({\n mode: z.literal('webhook'),\n webhook: WebhookDetectionSchema,\n mapping: z.array(FieldMappingSchema).min(1),\n filters: z.array(ResolvedFilterSchema).default([]),\n});\n\n/**\n * Top-level detection config. Discriminated on `mode` so the relevant\n * mode-block (poll/webhook) is structurally required for that mode. CDC as a\n * long-lived streaming primitive is deferred (#226-8); CDC-as-provenance\n * (Stripe-style event endpoints) is expressed via `mode: 'poll'` with\n * `poll.provenance: 'cdc'`.\n */\nexport const DetectionConfigSchema = z.discriminatedUnion('mode', [\n PollModeSchema,\n WebhookModeSchema,\n]);\n\nexport type DetectionConfig = z.infer<typeof DetectionConfigSchema>;\n","/**\n * Sync subsystem — loopback `ChangeMiddleware` factory (#226-5, ADR-033).\n *\n * Replaces the prior orchestrator-side `@Optional() SYNC_LOOPBACK_FINGERPRINT_STORE`\n * branch. Consumers that need to suppress echoes of their own outbound\n * writes compose `createLoopbackMiddleware(store)` into a primitive's\n * middleware chain — typically alongside `PollChangeSource<T>` — instead\n * of injecting a store into the orchestrator.\n *\n * Why middleware, not orchestrator branch:\n * - keeps the orchestrator port-agnostic (no per-mode special cases);\n * - lets primitives compose loopback with redaction / throttling /\n * other cross-cutting wrappers in a single chain;\n * - lets consumers pick per-source whether loopback is wired (some\n * entities have outbound writeback, others don't) without DI gymnastics.\n *\n * Behavior:\n * - For every change yielded by the inner iterator, call\n * `store.isEchoOfOwnWrite(subscription.domain, change.externalId, change.record)`.\n * - If the store returns `true`, the change is dropped (not yielded).\n * - Otherwise the change is passed through untouched.\n *\n * The middleware does not record audit rows for suppressed changes —\n * the orchestrator no longer learns about them. This is the deliberate\n * trade: loopback echoes are noise, not signal; their prior `skipped+noop`\n * audit rows existed only because the orchestrator was the suppression\n * site. Consumers wanting visibility into suppression counts can wrap\n * their store or layer a counting middleware alongside this one.\n */\n\nimport type { Change } from './sync-change-source.protocol';\nimport type {\n ChangeIterator,\n ChangeMiddleware,\n} from './sync-middleware.protocol';\nimport type { ILoopbackFingerprintStore } from './sync-loopback.protocol';\n\n/**\n * Build a `ChangeMiddleware<T>` that suppresses changes whose fingerprint\n * matches a recent local write according to the supplied store.\n *\n * Composition — first middleware in the array is outermost. Consumers\n * typically place loopback as the outermost layer so suppressed changes\n * never reach downstream middleware (redaction, transforms, etc.).\n */\nexport function createLoopbackMiddleware<T>(\n store: ILoopbackFingerprintStore<T>,\n): ChangeMiddleware<T> {\n return (next: ChangeIterator<T>): ChangeIterator<T> => {\n return async function* (subscription, cursor): AsyncIterable<Change<T>> {\n for await (const change of next(subscription, cursor)) {\n const isEcho = await store.isEchoOfOwnWrite(\n subscription.domain,\n change.externalId,\n change.record,\n );\n if (isEcho) continue;\n yield change;\n }\n };\n };\n}\n","/**\n * Sync subsystem — `PollChangeSource<T>` primitive (#226-3, ADR-033).\n *\n * Generic poll-mode `IChangeSource<T>` implementation parameterized by a\n * parsed `DetectionConfig` (poll mode) and a consumer-supplied\n * `PollFetchCallback<T>`. The primitive owns:\n *\n * - filter resolution (flat-AND vocabulary per epic decision Q3 — richer\n * boolean expressions deferred);\n * - field mapping → `Change.externalId` derivation;\n * - cursor strategy passthrough (the orchestrator passes the prior cursor\n * by-value per ADR-033 / #226-2; the callback yields the next cursor\n * per record; the primitive simply stamps it onto `Change<T>`);\n * - `Change<T>.source` provenance (`'poll'` by default);\n * - middleware-chain composition (the `ChangeMiddleware<T>` shape\n * locked in #226-1).\n *\n * Shape locks (decision memo Q5):\n * - `PollFetchContext = { subscription, cursor, filters }` — explicitly\n * NO `userId` / `tenantId`. Run-scope identity is closed over by the\n * consumer at adapter construction (or resolved inside the callback\n * via consumer services). Threading it through the seam would force\n * port expansion every time run-context grows.\n *\n * The adapter callback returns `{ record: T; cursor: PollCursor }` — the\n * primitive does not reach into the record to extract a cursor itself.\n * `cursor.field` from `DetectionConfig.poll.cursor` is metadata for codegen\n * + adapters; the primitive trusts what the callback yielded.\n */\n\nimport type {\n DetectionConfig,\n ResolvedFilter,\n} from './detection-config.schema';\nimport type {\n Change,\n ChangeSource,\n IChangeSource,\n SyncSubscriptionView,\n} from './sync-change-source.protocol';\nimport type {\n ChangeIterator,\n ChangeMiddleware,\n} from './sync-middleware.protocol';\n\n// ============================================================================\n// Cursor + adapter callback shapes\n// ============================================================================\n\n/**\n * Opaque poll-cursor shape. Each provider/entity pair binds it concretely\n * via the cursor strategy (`{ systemModstamp }`, `{ replayId }`, etc.); the\n * primitive treats it as an opaque value to pass through.\n */\nexport type PollCursor = unknown;\n\n/**\n * The context the primitive forwards to the adapter callback. Locked to\n * exactly three fields per decision memo Q5 — `userId` / `tenantId` are\n * NOT here on purpose.\n */\nexport interface PollFetchContext {\n readonly subscription: SyncSubscriptionView;\n readonly cursor: PollCursor | null;\n readonly filters: readonly ResolvedFilter[];\n}\n\n/**\n * Consumer-supplied fetch callback. Returns an async iterable of\n * `{ record, cursor }` pairs — `record` is already the canonical `T`\n * (the adapter does provider-side translation), `cursor` is the post-record\n * cursor the orchestrator should persist if the run completes successfully.\n */\nexport type PollFetchCallback<T> = (\n ctx: PollFetchContext,\n) => AsyncIterable<{ record: T; cursor: PollCursor }>;\n\n// ============================================================================\n// Constructor options\n// ============================================================================\n\nexport interface PollChangeSourceOptions<T> {\n /** Consumer-supplied fetch callback. */\n readonly adapter: PollFetchCallback<T>;\n /**\n * Parsed detection config. MUST be `mode: 'poll'`; the constructor\n * throws if a webhook config is supplied. Codegen-emitted factories\n * call `DetectionConfigSchema.parse(...)` upstream so this is a safety\n * net, not the primary validation point.\n */\n readonly config: DetectionConfig;\n /**\n * Optional middleware chain. First element is the outermost layer:\n * sees `(subscription, cursor)` first and yielded `Change<T>` last.\n * Locked shape (#226-1) — the primitive composes them with its own\n * `listChanges` implementation as the innermost iterator.\n */\n readonly middlewares?: ReadonlyArray<ChangeMiddleware<T>>;\n /**\n * Optional human label for run logs (e.g. `'salesforce-poll-opportunity'`).\n * Defaults to a derived label based on the subscription domain at\n * construction time fallback — adapters are encouraged to provide one.\n */\n readonly label?: string;\n}\n\n// ============================================================================\n// PollChangeSource<T>\n// ============================================================================\n\nexport class PollChangeSource<T> implements IChangeSource<T> {\n public readonly label: string;\n\n private readonly adapter: PollFetchCallback<T>;\n private readonly filters: readonly ResolvedFilter[];\n private readonly externalIdSourceField: string;\n private readonly source: ChangeSource;\n /**\n * When `poll.provenance === 'cdc'`, the field on the emitted record from\n * which `Change<T>.dedupKey` is read — sourced from `poll.cursor.field`\n * (Stripe-style event endpoints carry the event id on the record itself\n * and the cursor advances over the same id). `null` for default poll\n * provenance, which does NOT populate `dedupKey`.\n */\n private readonly dedupKeySourceField: string | null;\n private readonly composed: ChangeIterator<T>;\n\n constructor(opts: PollChangeSourceOptions<T>) {\n if (opts.config.mode !== 'poll') {\n throw new Error(\n `PollChangeSource requires DetectionConfig.mode === 'poll'; got '${(opts.config as { mode: string }).mode}'`,\n );\n }\n const config = opts.config;\n\n // Field mapping: locate the canonical `external_id` target. Adapters\n // emit T already-mapped, but the primitive needs to know which key on\n // T carries the external id so it can stamp `Change.externalId`. Source\n // of truth is the mapping table — codegen emits it from YAML, the\n // primitive reads it here.\n const externalIdMapping = config.mapping.find(\n (m) => m.target === 'external_id',\n );\n if (!externalIdMapping) {\n throw new Error(\n \"PollChangeSource: DetectionConfig.mapping must include an entry with target 'external_id' so emitted Change<T>.externalId can be populated\",\n );\n }\n this.externalIdSourceField = externalIdMapping.target;\n\n this.adapter = opts.adapter;\n this.filters = config.filters;\n // Provenance: `mode: 'poll'` defaults to `'poll'`; opt into `'cdc'` via\n // `poll.provenance` (Stripe-style event endpoints — wired in #226-4).\n // CDC provenance also stamps `dedupKey` from the cursor's `field`, since\n // those endpoints surface a per-event id on each record (the same id the\n // cursor advances over).\n const isCdc = config.poll.provenance === 'cdc';\n this.source = isCdc ? 'cdc' : 'poll';\n this.dedupKeySourceField = isCdc ? config.poll.cursor.field : null;\n\n this.label =\n opts.label ?? `poll-change-source:${externalIdMapping.source}`;\n\n // Compose middleware chain. The terminal iterator is `this.fetch`\n // bound to `this`. First middleware in the array is the outermost\n // layer (sees subscription/cursor first, yielded changes last).\n const inner: ChangeIterator<T> = (sub, cur) => this.fetch(sub, cur);\n const middlewares = opts.middlewares ?? [];\n this.composed = middlewares.reduceRight<ChangeIterator<T>>(\n (next, mw) => mw(next),\n inner,\n );\n }\n\n listChanges(\n subscription: SyncSubscriptionView,\n cursor: unknown | null,\n ): AsyncIterable<Change<T>> {\n return this.composed(subscription, cursor);\n }\n\n private async *fetch(\n subscription: SyncSubscriptionView,\n cursor: unknown | null,\n ): AsyncIterable<Change<T>> {\n const ctx: PollFetchContext = {\n subscription,\n cursor: cursor as PollCursor | null,\n filters: this.filters,\n };\n\n for await (const { record, cursor: nextCursor } of this.adapter(ctx)) {\n const externalIdRaw = (record as Record<string, unknown>)[\n this.externalIdSourceField\n ];\n if (typeof externalIdRaw !== 'string' || externalIdRaw.length === 0) {\n throw new Error(\n `PollChangeSource: record missing string '${this.externalIdSourceField}' — emitted records MUST carry the canonical external id keyed by the mapping target`,\n );\n }\n let dedupKey: string | undefined;\n if (this.dedupKeySourceField !== null) {\n const dedupRaw = (record as Record<string, unknown>)[\n this.dedupKeySourceField\n ];\n if (typeof dedupRaw !== 'string' || dedupRaw.length === 0) {\n throw new Error(\n `PollChangeSource: cdc-provenance record missing string '${this.dedupKeySourceField}' — when poll.provenance === 'cdc' the cursor.field must be present on each record so dedupKey can be populated`,\n );\n }\n dedupKey = dedupRaw;\n }\n\n const change: Change<T> = {\n externalId: externalIdRaw,\n // Polling cannot distinguish create vs. update vs. delete on its\n // own — all yielded records are surfaced as 'updated'. The\n // orchestrator's diff stage classifies create-vs-update against\n // local state; soft-delete detection is out of scope for the\n // primitive (consumer drives via tombstone records or a separate\n // sweep — see ADR-033).\n operation: 'updated',\n record,\n cursor: nextCursor,\n source: this.source,\n ...(dedupKey !== undefined ? { dedupKey } : {}),\n };\n yield change;\n }\n }\n}\n","/**\n * Sync subsystem — `WebhookChangeSource<T>` primitive (#226-4, ADR-033).\n *\n * Generic webhook-mode `IChangeSource<T>` implementation parameterized by a\n * parsed `DetectionConfig` (webhook mode) and a consumer-supplied\n * `WebhookFetchCallback<T>` that iterates a consumer-owned inbound staging\n * queue. The primitive owns:\n *\n * - canonical `Change<T>.source = 'webhook'` stamping;\n * - `dedupKey` derivation from the configured `webhook.eventIdField` on\n * the emitted record;\n * - `externalId` derivation from the mapping table's `external_id` target\n * (mirrors `PollChangeSource`);\n * - middleware-chain composition via the locked `ChangeMiddleware<T>` shape\n * (#226-1) — same composition seam as the poll primitive.\n *\n * The primitive is **passive**: it iterates whatever the consumer-owned\n * queue yields. It does NOT synchronously drive the orchestrator, does NOT\n * own a transport, and does NOT manage acks. The inbound staging table\n * schema is consumer-owned and deferred per ADR-0002 §Phase 4 — the\n * `WebhookFetchCallback<T>` is the queue contract the consumer injects.\n *\n * Shape locks (decision memo Q5, mirrored from poll primitive):\n * - `WebhookFetchContext = { subscription, cursor }` — explicitly NO\n * `userId` / `tenantId`. Run-scope identity is closed over by the\n * consumer at queue construction or resolved inside the callback via\n * consumer services. There are no `filters` on the webhook context —\n * filtering is done at registration / on the staging row, not at the\n * port seam (the queue is already filtered by the time the primitive\n * iterates).\n *\n * Long-lived streaming CDC primitives (SFDC Pub-Sub gRPC, Debezium/Kafka,\n * Postgres logical replication) are deferred to `#226-8` — they need a\n * fundamentally different lifecycle (`subscribe(onChange, onError)`,\n * server-paced backpressure, ack-on-yield) and shouldn't be retrofitted\n * into either this primitive or the poll primitive.\n */\n\nimport type { DetectionConfig } from './detection-config.schema';\nimport type {\n Change,\n IChangeSource,\n SyncSubscriptionView,\n} from './sync-change-source.protocol';\nimport type {\n ChangeIterator,\n ChangeMiddleware,\n} from './sync-middleware.protocol';\n\n// ============================================================================\n// Cursor + queue callback shapes\n// ============================================================================\n\n/**\n * Opaque webhook cursor shape. Webhook mode typically has a cursor of\n * `{ ts: ISO-string }` (last drained staging-row timestamp) but the\n * primitive treats it as opaque. Consumer-owned queue iterators interpret\n * it however the staging schema needs.\n */\nexport type WebhookCursor = unknown;\n\n/**\n * Context the primitive forwards to the queue iterator. Locked to exactly\n * two fields per the same Q5 reasoning that locks `PollFetchContext` — no\n * `userId` / `tenantId`.\n */\nexport interface WebhookFetchContext {\n readonly subscription: SyncSubscriptionView;\n readonly cursor: WebhookCursor | null;\n}\n\n/**\n * Consumer-supplied queue iterator. Returns an async iterable of\n * `{ record }` pairs — the consumer drains the inbound staging queue and\n * emits already-mapped canonical records `T`. The primitive stamps\n * `source: 'webhook'` and `dedupKey` from the record's configured\n * `webhook.eventIdField`; the consumer is the one who decided when a\n * staging row is \"ready\" to drain.\n *\n * Webhook mode has no per-record cursor advance — the staging-row drain\n * order is consumer policy (FIFO by ingestion timestamp, by event id, etc.)\n * and is opaque to the primitive. The orchestrator's last-yielded cursor\n * is whatever the consumer chooses to surface, if anything.\n */\nexport type WebhookFetchCallback<T> = (\n ctx: WebhookFetchContext,\n) => AsyncIterable<{ record: T; cursor?: WebhookCursor }>;\n\n// ============================================================================\n// Constructor options\n// ============================================================================\n\nexport interface WebhookChangeSourceOptions<T> {\n /** Consumer-supplied inbound queue iterator. */\n readonly queue: WebhookFetchCallback<T>;\n /**\n * Parsed detection config. MUST be `mode: 'webhook'`; the constructor\n * throws if a poll config is supplied. Codegen-emitted factories call\n * `DetectionConfigSchema.parse(...)` upstream so this is a safety net,\n * not the primary validation point.\n */\n readonly config: DetectionConfig;\n /**\n * Optional middleware chain. Same shape and composition rules as\n * `PollChangeSource` — first element is the outermost layer.\n */\n readonly middlewares?: ReadonlyArray<ChangeMiddleware<T>>;\n /**\n * Optional human label for run logs (e.g. `'stripe-webhook-charge'`).\n * Defaults to a derived label based on the mapping at construction.\n */\n readonly label?: string;\n}\n\n// ============================================================================\n// WebhookChangeSource<T>\n// ============================================================================\n\nexport class WebhookChangeSource<T> implements IChangeSource<T> {\n public readonly label: string;\n\n private readonly queue: WebhookFetchCallback<T>;\n private readonly externalIdSourceField: string;\n private readonly eventIdSourceField: string;\n private readonly composed: ChangeIterator<T>;\n\n constructor(opts: WebhookChangeSourceOptions<T>) {\n if (opts.config.mode !== 'webhook') {\n throw new Error(\n `WebhookChangeSource requires DetectionConfig.mode === 'webhook'; got '${(opts.config as { mode: string }).mode}'`,\n );\n }\n const config = opts.config;\n\n // Field mapping: locate the canonical `external_id` target — mirrors the\n // poll primitive's contract. Adapters emit records already-mapped; the\n // primitive needs to know which key on T carries the external id so it\n // can stamp `Change.externalId`.\n const externalIdMapping = config.mapping.find(\n (m) => m.target === 'external_id',\n );\n if (!externalIdMapping) {\n throw new Error(\n \"WebhookChangeSource: DetectionConfig.mapping must include an entry with target 'external_id' so emitted Change<T>.externalId can be populated\",\n );\n }\n this.externalIdSourceField = externalIdMapping.target;\n this.eventIdSourceField = config.webhook.eventIdField;\n\n this.queue = opts.queue;\n\n this.label =\n opts.label ?? `webhook-change-source:${externalIdMapping.source}`;\n\n // Compose middleware chain — same shape as PollChangeSource.\n const inner: ChangeIterator<T> = (sub, cur) => this.fetch(sub, cur);\n const middlewares = opts.middlewares ?? [];\n this.composed = middlewares.reduceRight<ChangeIterator<T>>(\n (next, mw) => mw(next),\n inner,\n );\n }\n\n listChanges(\n subscription: SyncSubscriptionView,\n cursor: unknown | null,\n ): AsyncIterable<Change<T>> {\n return this.composed(subscription, cursor);\n }\n\n private async *fetch(\n subscription: SyncSubscriptionView,\n cursor: unknown | null,\n ): AsyncIterable<Change<T>> {\n const ctx: WebhookFetchContext = {\n subscription,\n cursor: cursor as WebhookCursor | null,\n };\n\n for await (const { record, cursor: nextCursor } of this.queue(ctx)) {\n const externalIdRaw = (record as Record<string, unknown>)[\n this.externalIdSourceField\n ];\n if (typeof externalIdRaw !== 'string' || externalIdRaw.length === 0) {\n throw new Error(\n `WebhookChangeSource: record missing string '${this.externalIdSourceField}' — emitted records MUST carry the canonical external id keyed by the mapping target`,\n );\n }\n const eventIdRaw = (record as Record<string, unknown>)[\n this.eventIdSourceField\n ];\n if (typeof eventIdRaw !== 'string' || eventIdRaw.length === 0) {\n throw new Error(\n `WebhookChangeSource: record missing string '${this.eventIdSourceField}' — webhook records MUST carry the event id (DetectionConfig.webhook.eventIdField) so Change<T>.dedupKey can be populated`,\n );\n }\n\n const change: Change<T> = {\n externalId: externalIdRaw,\n // Webhook mode cannot distinguish create vs. update vs. delete on\n // its own — the orchestrator's diff stage handles classification.\n // Tombstone / soft-delete detection is consumer-driven (same as\n // poll mode — see ADR-033).\n operation: 'updated',\n record,\n cursor: nextCursor ?? null,\n source: 'webhook',\n dedupKey: eventIdRaw,\n };\n yield change;\n }\n }\n}\n","/**\n * Sync subsystem — `buildChangeSource()` runtime factory (#250, ADR-033.1 b).\n *\n * Mode-dispatching constructor for `IChangeSource<T>`. Codegen-emitted\n * provider modules call this from `useFactory` once they've resolved the\n * provider-keyed `fetches[provider]` callback and parsed the per-entity\n * `DetectionConfig`. Switching is on `cfg.mode` so the option-bag shape\n * difference between primitives (`adapter` vs. `queue`) stays internal —\n * consumers pass one fetch callback regardless of mode.\n */\nimport type { DetectionConfig } from './detection-config.schema';\nimport type { IChangeSource } from './sync-change-source.protocol';\nimport type { ChangeMiddleware } from './sync-middleware.protocol';\nimport {\n PollChangeSource,\n type PollFetchCallback,\n} from './poll-change-source';\nimport {\n WebhookChangeSource,\n type WebhookFetchCallback,\n} from './webhook-change-source';\n\nexport function buildChangeSource<T>(\n cfg: DetectionConfig,\n fetch: PollFetchCallback<T> | WebhookFetchCallback<T>,\n middlewares: ReadonlyArray<ChangeMiddleware<T>> = [],\n): IChangeSource<T> {\n switch (cfg.mode) {\n case 'poll':\n return new PollChangeSource<T>({\n adapter: fetch as PollFetchCallback<T>,\n config: cfg,\n middlewares,\n });\n case 'webhook':\n return new WebhookChangeSource<T>({\n queue: fetch as WebhookFetchCallback<T>,\n config: cfg,\n middlewares,\n });\n }\n}\n","/**\n * Sync subsystem — DI tokens\n *\n * String constants (not Symbols) so they match by value across import\n * boundaries — same convention as the events subsystem (`EVENT_BUS`). The\n * jobs subsystem uses Symbols for its analogous tokens; events and sync\n * stay internally consistent with strings.\n *\n * Usage in use cases:\n * ```ts\n * constructor(\n * @Inject(SYNC_CHANGE_SOURCE) private readonly source: IChangeSource<CanonicalOpportunity>,\n * @Inject(SYNC_CURSOR_STORE) private readonly cursors: ICursorStore,\n * @Inject(SYNC_FIELD_DIFFER) private readonly differ: IFieldDiffer<CanonicalOpportunity>,\n * @Inject(SYNC_SINK) private readonly sink: ISyncSink<CanonicalOpportunity>,\n * @Inject(SYNC_RUN_RECORDER) private readonly recorder: ISyncRunRecorder,\n * ) {}\n * ```\n *\n * Concrete bindings are registered by `SyncModule.forRoot(...)` (SYNC-6).\n */\n\nexport const SYNC_CHANGE_SOURCE = 'SYNC_CHANGE_SOURCE' as const;\nexport const SYNC_CURSOR_STORE = 'SYNC_CURSOR_STORE' as const;\nexport const SYNC_FIELD_DIFFER = 'SYNC_FIELD_DIFFER' as const;\nexport const SYNC_SINK = 'SYNC_SINK' as const;\n\n/**\n * Run-recorder token (SYNC-5). Backed by `ISyncRunRecorder`. Drizzle impl\n * lands in SYNC-4; tests provide inline fakes.\n */\nexport const SYNC_RUN_RECORDER = 'SYNC_RUN_RECORDER' as const;\n\n/**\n * Injection token for the resolved `SyncModuleOptions` object (SYNC-6).\n *\n * Backends that need to observe module configuration (e.g. `multiTenant`\n * flag, pool filters) inject via this token. Provided automatically by\n * `SyncModule.forRoot(...)` / `SyncModule.forRootAsync(...)`.\n */\nexport const SYNC_MODULE_OPTIONS = 'SYNC_MODULE_OPTIONS' as const;\n\n/**\n * Injection token for the resolved multi-tenancy flag (SYNC-6).\n *\n * Provided by `SyncModule.forRoot(...)` as `options.multiTenant ?? false`.\n * Consumed by `ExecuteSyncUseCase` to enforce the tenantId-is-required rule.\n */\nexport const SYNC_MULTI_TENANT = 'SYNC_MULTI_TENANT' as const;\n","/**\n * Typed errors + shared boundary helpers for the sync subsystem.\n *\n * Classes (not bare Error) so consumers can `instanceof` them in catch\n * blocks and exception filters can map them to HTTP codes.\n *\n * Mirrors the shape of `events-errors.ts` and `jobs-errors.ts`.\n */\n\n/**\n * Thrown by the Drizzle cursor-store / run-recorder backends AND by the\n * orchestrator entry point when `SYNC_MULTI_TENANT` is enabled but the\n * caller did not supply a non-null `tenantId`. Strict enforcement at the\n * boundary — explicit `null` still throws.\n *\n * Disable multi-tenancy on the module (`multiTenant: false`, the default)\n * to opt out of the requirement entirely.\n *\n * `operation` identifies the call site (e.g. `'cursor.put'`,\n * `'startRun'`, `'execute'`) so the stack-trace message points at the\n * specific boundary that rejected the input.\n */\nexport class MissingTenantIdError extends Error {\n override readonly name = 'MissingTenantIdError';\n constructor(operation: string) {\n super(\n `Missing tenantId for sync operation '${operation}'. SyncModule is ` +\n `configured with multiTenant: true — every call must include a ` +\n `non-null tenantId. Either pass the tenantId or disable multi-` +\n `tenancy on the module.`,\n );\n }\n}\n\n/**\n * Shared boundary guard — used at the orchestrator entry AND inside the\n * Drizzle backends. Keeping the check in one function guarantees every\n * `MissingTenantIdError` carries the same message shape regardless of the\n * site that raised it, which makes it easier for consumers to pattern-\n * match on the error in logs/metrics.\n *\n * When `multiTenant` is false, the function is a no-op — `tenantId` may\n * be anything (including `undefined`). When true, `undefined` or `null`\n * throws.\n */\nexport function assertTenantId(\n tenantId: string | null | undefined,\n options: { multiTenant: boolean; operation: string },\n): asserts tenantId is string {\n if (!options.multiTenant) return;\n if (tenantId === undefined || tenantId === null) {\n throw new MissingTenantIdError(options.operation);\n }\n}\n","/**\n * Drizzle schema for the sync subsystem audit/observability tables (SYNC-1).\n *\n * Three tables model end-to-end sync observability, keyed by the single port\n * every sync adapter implements (`IChangeSource<T>` from SYNC-2):\n *\n * - `sync_subscriptions` — owns the cursor per\n * `(integration_id, adapter, domain, external_ref)` tuple. Addressed\n * by id by `ICursorStore` (SYNC-3/SYNC-4).\n * - `sync_runs` — per-run audit log: start/complete, status,\n * cursor before/after, counts, direction (inbound|outbound),\n * action (poll|cdc|webhook|manual|writeback).\n * - `sync_run_items` — per-record change log with structured\n * `changed_fields` jsonb enforced by the Zod `FieldDiffSchema`\n * contract (ADR-0003; protocol lives in SYNC-2's\n * sync-field-diff.protocol.ts).\n *\n * Design calls (vs. issue #126 open questions):\n *\n * - `sync_subscriptions` ships in the subsystem (not consumer-owned).\n * Rationale: SYNC-4's `PostgresCursorStore` needs to read/write this\n * table directly; making it consumer-owned would require consumers to\n * hand-wire a shape the backend already knows. The row is addressable\n * by id and scoped by the uniqueness tuple; consumers can still\n * query/list it freely. Same stance as `job_run` being subsystem-\n * owned while remaining consumer-queryable.\n *\n * - `tenant_id` is always emitted on the three tables as nullable text.\n * The `SYNC_MULTI_TENANT` DI flag (SYNC-6) is what enforces the\n * non-null + cross-tenant-isolation contract at the service/orchestrator\n * boundary. This mirrors JOB-1/JOB-8's final shape — runtime guard, not\n * a scaffold-time conditional column. Keeps the schema file uniform\n * across single-tenant and multi-tenant deployments.\n *\n * - `changed_fields` on `sync_run_items` is typed via the Zod-inferred\n * `FieldDiff` shape from SYNC-2 (`{ [fieldName]: { from, to } }`). The\n * recorder service (SYNC-5) validates every write against\n * `FieldDiffSchema.parse` so consumers can rely on the shape.\n */\nimport {\n pgEnum,\n pgTable,\n uuid,\n text,\n jsonb,\n integer,\n boolean,\n timestamp,\n index,\n uniqueIndex,\n} from 'drizzle-orm/pg-core';\nimport type { InferSelectModel } from 'drizzle-orm';\n\nimport type { FieldDiff } from './sync-field-diff.protocol';\n\n// ─── Enums ──────────────────────────────────────────────────────────────────\n\n/**\n * Direction of a sync run relative to local state.\n *\n * - `inbound` — external → local (the common case: SFDC poll → local DB).\n * - `outbound` — local → external (writeback; deferred per epic but the\n * column shape is reserved so future writeback runs share the audit log).\n */\nexport const syncRunDirectionEnum = pgEnum('sync_run_direction', [\n 'inbound',\n 'outbound',\n]);\n\n/**\n * How the run detected upstream changes. Maps 1:1 to the `Change.source`\n * provenance on inbound runs; `manual` captures operator-triggered re-syncs\n * and `writeback` captures outbound runs.\n */\nexport const syncRunActionEnum = pgEnum('sync_run_action', [\n 'poll',\n 'cdc',\n 'webhook',\n 'manual',\n 'writeback',\n]);\n\n/**\n * Lifecycle status of a sync run.\n *\n * - `running` — in-flight; recorder has started but not completed.\n * - `success` — completed with at least one change processed.\n * - `no_changes` — completed cleanly, no upstream changes found.\n * - `failed` — errored before completion; `error` column carries the\n * message. `records_processed` may be non-zero (partial progress).\n */\nexport const syncRunStatusEnum = pgEnum('sync_run_status', [\n 'running',\n 'success',\n 'no_changes',\n 'failed',\n]);\n\n/**\n * Operation applied per record. Mirrors `Change<T>.operation` from SYNC-2,\n * plus the recorder's own `'noop'` for changes that matched existing state.\n */\nexport const syncRunItemOperationEnum = pgEnum('sync_run_item_operation', [\n 'created',\n 'updated',\n 'deleted',\n 'noop',\n]);\n\n/**\n * Per-record status within a run. `skipped` captures loopback-detected echoes\n * of the local system's own writes (see `ILoopbackFingerprintStore` in the\n * epic), which record the external_id but intentionally do not touch local\n * state.\n */\nexport const syncRunItemStatusEnum = pgEnum('sync_run_item_status', [\n 'success',\n 'failed',\n 'skipped',\n]);\n\n// ─── sync_subscriptions ─────────────────────────────────────────────────────\n\n/**\n * One cursor owner per (integration, adapter, domain, external_ref).\n *\n * - `integration_id` — opaque id of the connected account/instance. E.g.\n * the SFDC org id for polling strategies, the GitHub installation id\n * for webhook strategies.\n * - `adapter` — short adapter label, e.g. `'salesforce'`, `'hubspot'`.\n * - `domain` — canonical entity domain this subscription tracks,\n * e.g. `'opportunity'`, `'contact'`.\n * - `external_ref` — optional upstream scope (e.g. a filter id, a\n * webhook subscription id). NULL when the subscription covers the\n * entire domain.\n *\n * The cursor shape is opaque jsonb — strategies type it internally (poll:\n * `{ systemModstamp }`, cdc: `{ replayId }`, webhook: `{ ts }`). Overwritten\n * by `ICursorStore.put(id, cursor)`.\n */\nexport const syncSubscriptions = pgTable(\n 'sync_subscriptions',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n integrationId: text('integration_id').notNull(),\n adapter: text('adapter').notNull(),\n domain: text('domain').notNull(),\n externalRef: text('external_ref'),\n enabled: boolean('enabled').notNull().default(true),\n /**\n * Per-subscription configuration bag. Strategies type it internally;\n * e.g. polling strategies stash `{ batchSize, highWatermark }` here.\n */\n config: jsonb('config').notNull().default({}).$type<Record<string, unknown>>(),\n /**\n * Opaque cursor persisted by `ICursorStore.put()`. NULL until the first\n * successful run advances it.\n */\n cursor: jsonb('cursor').$type<unknown>(),\n lastSyncAt: timestamp('last_sync_at', { withTimezone: true }),\n /** Runtime-enforced when `SYNC_MULTI_TENANT` is true; see SYNC-6. */\n tenantId: text('tenant_id'),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (t) => ({\n /**\n * Composite uniqueness per the epic shape. `external_ref` is nullable;\n * Postgres treats NULLs as distinct in a UNIQUE constraint, which means\n * two rows with the same `(integration_id, adapter, domain)` and NULL\n * external_ref are allowed. That's intentional — a subscription with\n * NULL external_ref covers the full domain, and duplicates there would\n * be a consumer-layer modeling issue, not a schema concern.\n */\n uqSyncSubscriptionTuple: uniqueIndex('uq_sync_subscriptions_tuple').on(\n t.integrationId,\n t.adapter,\n t.domain,\n t.externalRef,\n ),\n /** Scheduling query: list enabled subscriptions ordered by staleness. */\n idxSyncSubscriptionsEnabledLastSync: index(\n 'idx_sync_subscriptions_enabled_last_sync',\n ).on(t.enabled, t.lastSyncAt),\n }),\n);\n\nexport type SyncSubscriptionRow = InferSelectModel<typeof syncSubscriptions>;\n\n// ─── sync_runs ──────────────────────────────────────────────────────────────\n\n/**\n * One row per invocation of `ExecuteSyncUseCase`. `started_at` is set when\n * the recorder opens the run; `completed_at`, `status`, `records_*`,\n * `cursor_after`, and `duration_ms` are filled on completion.\n *\n * `cursor_before` / `cursor_after` carry the opaque cursor snapshots so the\n * run log is fully self-describing — given a run id, an operator can reason\n * about exactly what window was scanned without cross-referencing another\n * table.\n */\nexport const syncRuns = pgTable(\n 'sync_runs',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n subscriptionId: uuid('subscription_id')\n .notNull()\n .references(() => syncSubscriptions.id, { onDelete: 'cascade' }),\n direction: syncRunDirectionEnum('direction').notNull(),\n action: syncRunActionEnum('action').notNull(),\n status: syncRunStatusEnum('status').notNull().default('running'),\n recordsFound: integer('records_found').notNull().default(0),\n recordsProcessed: integer('records_processed').notNull().default(0),\n cursorBefore: jsonb('cursor_before').$type<unknown>(),\n cursorAfter: jsonb('cursor_after').$type<unknown>(),\n durationMs: integer('duration_ms'),\n error: text('error'),\n startedAt: timestamp('started_at', { withTimezone: true })\n .notNull()\n .defaultNow(),\n completedAt: timestamp('completed_at', { withTimezone: true }),\n /** Runtime-enforced when `SYNC_MULTI_TENANT` is true; see SYNC-6. */\n tenantId: text('tenant_id'),\n },\n (t) => ({\n /** Timeline read: \"most recent runs for this subscription\". */\n idxSyncRunsSubscriptionStartedAt: index(\n 'idx_sync_runs_subscription_started_at',\n ).on(t.subscriptionId, t.startedAt),\n /** Stale-run sweeper: \"runs that started > N minutes ago and are still running\". */\n idxSyncRunsStatusStartedAt: index('idx_sync_runs_status_started_at').on(\n t.status,\n t.startedAt,\n ),\n }),\n);\n\nexport type SyncRunRow = InferSelectModel<typeof syncRuns>;\n\n// ─── sync_run_items ─────────────────────────────────────────────────────────\n\n/**\n * One row per upstream change processed within a run. Captures the canonical\n * decision the orchestrator made (`operation` + `status`), the structured\n * per-field diff (`changed_fields`, ADR-0003), and the local row id\n * (`local_id`) for drill-down joins.\n *\n * `changed_fields` is validated at the recorder layer via `FieldDiffSchema`\n * (SYNC-2) — the $type<FieldDiff> annotation here only documents the shape\n * for Drizzle consumers. The runtime enforcement is non-negotiable: downstream\n * drift-detection queries rely on the `{from, to}` shape per field.\n *\n * `title` is an optional human-readable label captured at write time (e.g.\n * `\"Pinnacle opportunity\"`) so run-log UIs don't need to re-hydrate the\n * canonical record.\n */\nexport const syncRunItems = pgTable(\n 'sync_run_items',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n syncRunId: uuid('sync_run_id')\n .notNull()\n .references(() => syncRuns.id, { onDelete: 'cascade' }),\n entityType: text('entity_type').notNull(),\n externalId: text('external_id').notNull(),\n localId: text('local_id'),\n operation: syncRunItemOperationEnum('operation').notNull(),\n status: syncRunItemStatusEnum('status').notNull(),\n /**\n * Structured per-field diff — ADR-0003 shape enforced by\n * `FieldDiffSchema.parse` at the recorder service layer.\n *\n * Shape: `{ [fieldName]: { from: unknown, to: unknown } }`.\n * Empty `{}` for `noop` items; `{ [field]: { from: null, to: <value> } }`\n * for created items; `{ [field]: { from: <value>, to: null } }` for\n * deleted items.\n */\n changedFields: jsonb('changed_fields').notNull().default({}).$type<FieldDiff>(),\n title: text('title'),\n error: text('error'),\n createdAt: timestamp('created_at', { withTimezone: true })\n .notNull()\n .defaultNow(),\n /** Runtime-enforced when `SYNC_MULTI_TENANT` is true; see SYNC-6. */\n tenantId: text('tenant_id'),\n },\n (t) => ({\n /** Ordered timeline within a run. */\n idxSyncRunItemsRunCreatedAt: index('idx_sync_run_items_run_created_at').on(\n t.syncRunId,\n t.createdAt,\n ),\n /** Per-record history: \"every sync that touched opportunity/$extId\". */\n idxSyncRunItemsEntityExternal: index(\n 'idx_sync_run_items_entity_external',\n ).on(t.entityType, t.externalId),\n }),\n);\n\nexport type SyncRunItemRow = InferSelectModel<typeof syncRunItems>;\n","/**\n * MemoryCursorStore — in-memory backend for `ICursorStore` (SYNC-3).\n *\n * Test double that lets consumers exercise `ExecuteSyncUseCase` (SYNC-5) and\n * other cursor-consuming code paths without Postgres. Mirrors the role of\n * `MemoryEventBus` and `MemoryJobStore`: plain keyed state, tests take a\n * direct reference for `beforeEach` resets.\n *\n * Cursor values are stored by reference — the port's `get`/`put` contract\n * treats them as opaque `unknown`. Callers that want durable value-equality\n * semantics should snapshot via JSON before `put` and reparse after `get`;\n * this is what the Drizzle backend (SYNC-4) does implicitly via jsonb\n * round-trip. The memory backend intentionally does not simulate the\n * serialize/deserialize cycle — consumers who care should test against\n * Postgres.\n *\n * ## Multi-tenancy\n *\n * `tenantId` is accepted but ignored. The memory backend's state is\n * process-local — there's no durable storage where a cross-tenant leak\n * could occur. Tests that want to assert per-tenant isolation should\n * target the Drizzle backend.\n *\n * Not shipped in the upstream consumer; this is a subsystem-first addition for the\n * test surface. Consumed by:\n * - SYNC-5 unit tests (`ExecuteSyncUseCase` against synthetic sources)\n * - SYNC-6 module tests (`SyncModule.forRoot({ backend: 'memory' })`)\n */\nimport { Injectable } from '@nestjs/common';\nimport type {\n CursorSnapshot,\n ICursorStore,\n} from './sync-cursor-store.protocol';\nimport type { MemorySyncSubscription } from './sync-run-recorder.memory-backend';\n\n@Injectable()\nexport class MemoryCursorStore implements ICursorStore {\n /**\n * Subscription-id → last persisted cursor. Public so tests can inspect\n * or pre-seed state; production callers MUST go through `get`/`put`.\n */\n readonly cursors: Map<string, unknown> = new Map();\n\n /**\n * Seedable subscription metadata for `listAll` — the memory backend\n * stores only `subscriptionId → cursor` in its write path, so the\n * snapshot shape (`integrationId`, `adapter`, `domain`, `externalRef`,\n * timestamps) has no natural source without test seeding. Tests populate\n * this map; unseeded entries get empty-string metadata and `new Date(0)`\n * timestamps so the shape stays stable. Production paths go through the\n * Drizzle backend.\n */\n readonly subscriptions: Map<string, MemorySyncSubscription> = new Map();\n\n async get(\n subscriptionId: string,\n _tenantId?: string | null,\n ): Promise<unknown | null> {\n // `Map.get` returns `undefined` for missing keys; the port contract\n // returns `null`. Normalize here so callers can `=== null`-check.\n const value = this.cursors.get(subscriptionId);\n return value === undefined ? null : value;\n }\n\n async put(\n subscriptionId: string,\n cursor: unknown,\n _tenantId?: string | null,\n ): Promise<void> {\n // Overwrite semantics — matches the port contract and the Drizzle\n // backend's `ON CONFLICT DO UPDATE` behavior.\n this.cursors.set(subscriptionId, cursor);\n }\n\n async listAll(_tenantId?: string | null): Promise<CursorSnapshot[]> {\n // Accepts tenantId for contract symmetry but does not filter on it —\n // the memory backend never enforces tenancy (see class-level comment).\n const snapshots: CursorSnapshot[] = [];\n for (const [subscriptionId, cursor] of this.cursors.entries()) {\n const meta = this.subscriptions.get(subscriptionId);\n snapshots.push({\n subscriptionId,\n integrationId: meta?.integrationId ?? '',\n adapter: meta?.adapter ?? '',\n domain: meta?.domain ?? '',\n externalRef: meta?.externalRef ?? null,\n cursor: cursor ?? null,\n lastSyncAt: meta?.lastSyncAt ?? null,\n updatedAt: meta?.updatedAt ?? new Date(0),\n tenantId: null,\n });\n }\n return snapshots.sort(\n (a, b) => b.updatedAt.getTime() - a.updatedAt.getTime(),\n );\n }\n\n /** Reset state. Tests call this in `beforeEach`. */\n clear(): void {\n this.cursors.clear();\n this.subscriptions.clear();\n }\n}\n","/**\n * MemoryRunRecorder — in-memory backend for `ISyncRunRecorder` (SYNC-6).\n *\n * Test double so `SyncModule.forRoot({ backend: 'memory' })` is genuinely\n * end-to-end runnable without Postgres. Mirrors the role of\n * `MemoryCursorStore`: plain keyed state, `clear()` helper for\n * `beforeEach` resets, public inspection surface so tests can assert on\n * the recorded run + item timeline without scraping logs.\n *\n * Validates `changedFields` through `FieldDiffSchema.parse` on every\n * `recordItem` call — same ADR-0003 contract as the Drizzle backend. An\n * in-memory recorder that skipped the validation would be a silently\n * weaker contract than production.\n *\n * `startRun` generates a uuid via `crypto.randomUUID()` (Node 19+ / Bun).\n * We don't import `uuid` because the subsystem has no other use for it.\n *\n * ## Multi-tenancy\n *\n * `tenantId` is accepted (and recorded on the in-memory row so tests can\n * assert it) but enforcement lives at the module boundary. The memory\n * backend intentionally does not throw on missing `tenantId` — that's\n * the orchestrator's job when `multiTenant=true` (SYNC-6). A permissive\n * memory recorder lets tests exercise error paths where the orchestrator\n * short-circuits before ever reaching the recorder.\n */\nimport { Injectable } from '@nestjs/common';\nimport type {\n CompleteRunInput,\n ISyncRunRecorder,\n RecordItemInput,\n StartRunInput,\n SyncRunSummary,\n} from './sync-run-recorder.protocol';\nimport { FieldDiffSchema } from './sync-field-diff.protocol';\n\n/**\n * Optional per-subscription metadata a test can seed on the memory backend\n * so `listRecent` can surface `integrationId`. The memory recorder's write\n * path never persists subscription rows (it only stores runs + items), so\n * `listRecent` has no way to derive `integrationId` on its own. Tests that\n * care about the field should populate `subscriptions` before calling\n * `listRecent`; calls that don't seed get an empty string and a stable\n * shape (see `listRecent` below).\n */\nexport interface MemorySyncSubscription {\n integrationId: string;\n adapter: string;\n domain: string;\n externalRef: string | null;\n lastSyncAt?: Date | null;\n updatedAt: Date;\n}\n\n/**\n * Concrete run row as held in memory. Shape mirrors the interesting\n * columns on `sync_runs` so assertions read like DB queries.\n */\nexport interface MemoryRunRecord {\n id: string;\n subscriptionId: string;\n direction: 'inbound' | 'outbound';\n action: 'poll' | 'cdc' | 'webhook' | 'manual' | 'writeback';\n status: 'running' | 'success' | 'no_changes' | 'failed';\n cursorBefore: unknown | null;\n cursorAfter: unknown | null;\n recordsFound: number;\n recordsProcessed: number;\n durationMs: number | null;\n error: string | null;\n tenantId: string | null;\n startedAt: Date;\n completedAt: Date | null;\n}\n\n@Injectable()\nexport class MemoryRunRecorder implements ISyncRunRecorder {\n /**\n * All started runs keyed by id. Public so tests can inspect lifecycle\n * transitions without poking through recording methods.\n */\n readonly runs: Map<string, MemoryRunRecord> = new Map();\n\n /**\n * Items keyed by `sync_run_id`, array order matches insertion order —\n * mirrors the timeline the `(sync_run_id, created_at)` index produces\n * in Postgres.\n */\n readonly items: Map<string, RecordItemInput[]> = new Map();\n\n /**\n * Seedable subscription metadata — tests populate this to make\n * `listRecent` return meaningful `integrationId` values. The memory\n * backend doesn't track subscriptions on its own (only runs + items), so\n * this map is the intentional extension point: tests write entries for\n * the subscription ids they use, production code never touches it.\n */\n readonly subscriptions: Map<string, MemorySyncSubscription> = new Map();\n\n async startRun(input: StartRunInput): Promise<{ id: string }> {\n const id = crypto.randomUUID();\n this.runs.set(id, {\n id,\n subscriptionId: input.subscriptionId,\n direction: input.direction,\n action: input.action,\n status: 'running',\n cursorBefore: input.cursorBefore ?? null,\n cursorAfter: null,\n recordsFound: 0,\n recordsProcessed: 0,\n durationMs: null,\n error: null,\n tenantId: input.tenantId ?? null,\n startedAt: new Date(),\n completedAt: null,\n });\n this.items.set(id, []);\n return { id };\n }\n\n async recordItem(input: RecordItemInput): Promise<void> {\n // Same ADR-0003 contract as the Drizzle backend.\n FieldDiffSchema.parse(input.changedFields);\n\n const bucket = this.items.get(input.syncRunId);\n if (!bucket) {\n throw new Error(\n `MemoryRunRecorder.recordItem: no run started for id '${input.syncRunId}'. ` +\n `Call startRun(...) first.`,\n );\n }\n bucket.push(input);\n }\n\n async completeRun(runId: string, input: CompleteRunInput): Promise<void> {\n const run = this.runs.get(runId);\n if (!run) {\n throw new Error(\n `MemoryRunRecorder.completeRun: no run started for id '${runId}'.`,\n );\n }\n run.status = input.status;\n run.recordsFound = input.recordsFound;\n run.recordsProcessed = input.recordsProcessed;\n run.cursorAfter = input.cursorAfter ?? null;\n run.durationMs = input.durationMs;\n run.error = input.error ?? null;\n run.completedAt = new Date();\n }\n\n async listRecent(\n limit: number,\n subscriptionId?: string,\n _tenantId?: string | null,\n ): Promise<SyncRunSummary[]> {\n // Memory backend accepts tenantId for contract symmetry but does not\n // filter on it — state is process-local and cross-tenant isolation is\n // not meaningful here (matches MemoryCursorStore behavior).\n const all = Array.from(this.runs.values());\n const filtered =\n subscriptionId === undefined\n ? all\n : all.filter((r) => r.subscriptionId === subscriptionId);\n return filtered\n .sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime())\n .slice(0, limit)\n .map((r) => ({\n id: r.id,\n subscriptionId: r.subscriptionId,\n // integrationId is only knowable if the test seeded subscriptions\n // metadata; empty string otherwise. The Drizzle backend resolves\n // it via JOIN, which is the production path.\n integrationId:\n this.subscriptions.get(r.subscriptionId)?.integrationId ?? '',\n status: r.status,\n startedAt: r.startedAt,\n completedAt: r.completedAt,\n recordsProcessed: r.recordsProcessed,\n tenantId: r.tenantId,\n }));\n }\n\n /** Reset state. Tests call this in `beforeEach`. */\n clear(): void {\n this.runs.clear();\n this.items.clear();\n this.subscriptions.clear();\n }\n\n // ─── test ergonomics ─────────────────────────────────────────────────\n\n /** All runs for a subscription, newest first. Timeline reads. */\n getRunsForSubscription(subscriptionId: string): MemoryRunRecord[] {\n return Array.from(this.runs.values())\n .filter((r) => r.subscriptionId === subscriptionId)\n .sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime());\n }\n\n /** All item rows for a run, insertion-ordered. */\n getItemsForRun(runId: string): RecordItemInput[] {\n return this.items.get(runId) ?? [];\n }\n}\n","/**\n * DeepEqualDiffer — default `IFieldDiffer<T>` for the sync 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 './sync-field-diff.protocol';\n\n/**\n * Default ignore list. Keep in sync with consumer canonical-record shapes —\n * adding a row-metadata field here means no sync will ever mark it changed.\n */\nconst DEFAULT_IGNORE_FIELDS: ReadonlySet<string> = new Set([\n 'id',\n 'createdAt',\n 'updatedAt',\n 'deletedAt',\n 'type',\n 'lastModifiedAt',\n 'fields',\n 'providerMetadata',\n]);\n\nexport interface DeepEqualDifferOptions {\n /**\n * Extra field names to ignore in addition to the defaults. Consumers can\n * pass `['sync_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-sync 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","/**\n * ExecuteSyncUseCase — the generic sync orchestrator (SYNC-5).\n *\n * One class. Reused across every `(provider, detection-mode, canonical-entity)`\n * tuple. Parameterized over `T` so canonical records stay typed end-to-end.\n *\n * Flow per run:\n *\n * 1. `recorder.startRun(...)` — opens a `sync_runs` row in 'running'.\n * 2. for each change yielded by `source.listChanges(subscription, cursorBefore)`:\n * a. differ.diff(existing, incoming) → 'noop' short-circuits to\n * a noop audit row (no sink write).\n * b. sink.upsertByExternalId / softDeleteByExternalId → records\n * the local id on the audit row.\n * c. per-item try/catch — a failed item increments the failed\n * counter and records `status: 'failed'` with `error`, but\n * does NOT abort the run.\n * d. advance `latestCursor = change.cursor` as the iterator moves.\n * 3. `cursors.put(subscription.id, latestCursor)` when the loop completes\n * AND at least one cursor advance happened. On exceptions from the\n * source iterator (auth expiry, network error), we persist the\n * last-good cursor so the next run resumes from the last known\n * successful position.\n * 4. `finally { recorder.completeRun(...) }` — always terminates the run.\n *\n * Loopback suppression — when a consumer's writes echo back on the next\n * inbound poll/CDC/webhook — is composed into the source's middleware\n * chain via `createLoopbackMiddleware(store)` (#226-5 / ADR-033). The\n * orchestrator no longer special-cases echoes: middleware drops them\n * before they reach this loop. Consumers that don't have outbound\n * writeback paths simply omit the middleware.\n *\n * ## Generics\n *\n * - `T` = canonical record shape from the adapter side. Same `T` flows\n * through `IChangeSource<T>`, `IFieldDiffer<T>`, `ISyncSink<T>`.\n *\n * ## No CRM bleed\n *\n * Per the SYNC-5 issue's extraction notes (HS-9 finding), this orchestrator\n * is strictly provider-agnostic:\n * - `entityType` is `string` throughout; no `'opportunity' | 'account' | ...`\n * narrowing leaks into the use case\n * - the upstream consumer's `SyncRunRecorderService` class injection replaced with the\n * `ISyncRunRecorder` protocol (backend lands in SYNC-4)\n */\nimport { Inject, Injectable, Logger, Optional } from '@nestjs/common';\nimport type { IChangeSource, Change } from './sync-change-source.protocol';\nimport type { ICursorStore } from './sync-cursor-store.protocol';\nimport type { IFieldDiffer, FieldDiff } from './sync-field-diff.protocol';\nimport type { ISyncSink } from './sync-sink.protocol';\nimport type { ISyncRunRecorder } from './sync-run-recorder.protocol';\nimport { assertTenantId } from './sync-errors';\nimport {\n SYNC_CHANGE_SOURCE,\n SYNC_CURSOR_STORE,\n SYNC_FIELD_DIFFER,\n SYNC_MULTI_TENANT,\n SYNC_RUN_RECORDER,\n SYNC_SINK,\n} from './sync.tokens';\n\n// ============================================================================\n// Inputs + result\n// ============================================================================\n\nexport interface ExecuteSyncInput<T> {\n /** The subscription whose cursor/identity frames this run. */\n readonly subscription: {\n readonly id: string;\n readonly domain: string; // entityType — used on audit rows\n readonly externalRef?: string | null;\n };\n /** Per-run user context; threaded through sink writes. */\n readonly userId: string;\n /** Provider label persisted on saved rows, e.g. `'salesforce-crm'`. */\n readonly provider: string;\n /** Run direction — almost always `'inbound'`. Reserved for writeback. */\n readonly direction: 'inbound' | 'outbound';\n /** Detection mode — maps 1:1 to `sync_runs.action`. */\n readonly action: 'poll' | 'cdc' | 'webhook' | 'manual' | 'writeback';\n /** Multi-tenant deployments pass the tenant id through. */\n readonly tenantId?: string | null;\n /**\n * Optional override — inject a specific change source for this run when\n * the DI-bound source is not the one to use (e.g. manual backfill with\n * a custom cursor). Defaults to the DI-resolved `SYNC_CHANGE_SOURCE`.\n */\n readonly sourceOverride?: IChangeSource<T>;\n}\n\nexport interface ExecuteSyncResult {\n readonly runId: string;\n readonly status: 'success' | 'no_changes' | 'failed';\n readonly recordsFound: number;\n readonly recordsProcessed: number;\n readonly recordsFailed: number;\n readonly cursorBefore: unknown | null;\n readonly cursorAfter: unknown | null;\n readonly durationMs: number;\n readonly error?: string | null;\n}\n\n// ============================================================================\n// ExecuteSyncUseCase\n// ============================================================================\n\n@Injectable()\nexport class ExecuteSyncUseCase<T extends Record<string, unknown>> {\n private readonly logger = new Logger(ExecuteSyncUseCase.name);\n\n constructor(\n @Inject(SYNC_CHANGE_SOURCE) private readonly source: IChangeSource<T>,\n @Inject(SYNC_CURSOR_STORE) private readonly cursors: ICursorStore,\n @Inject(SYNC_FIELD_DIFFER) private readonly differ: IFieldDiffer<T>,\n @Inject(SYNC_SINK) private readonly sink: ISyncSink<T>,\n @Inject(SYNC_RUN_RECORDER) private readonly recorder: ISyncRunRecorder,\n @Optional()\n @Inject(SYNC_MULTI_TENANT)\n private readonly multiTenant: boolean = false,\n ) {}\n\n async execute(input: ExecuteSyncInput<T>): Promise<ExecuteSyncResult> {\n // Defense-in-depth tenancy guard — fire BEFORE startRun so a rejected\n // input never leaves a dangling `status=running` row. Backends also\n // enforce (SYNC-4), but failing fast at the orchestrator boundary is\n // cheaper for observability, metrics, and manual cleanup.\n assertTenantId(input.tenantId, {\n multiTenant: this.multiTenant,\n operation: 'execute',\n });\n\n const source = input.sourceOverride ?? this.source;\n const startedAt = Date.now();\n const cursorBefore = await this.cursors.get(input.subscription.id, input.tenantId);\n\n const { id: runId } = await this.recorder.startRun({\n subscriptionId: input.subscription.id,\n direction: input.direction,\n action: input.action,\n cursorBefore,\n tenantId: input.tenantId,\n });\n\n let recordsFound = 0;\n let recordsProcessed = 0;\n let recordsFailed = 0;\n let latestCursor: unknown | null = cursorBefore;\n let cursorAdvanced = false;\n let runError: string | null = null;\n let status: 'success' | 'no_changes' | 'failed' = 'no_changes';\n\n try {\n for await (const change of source.listChanges(input.subscription, cursorBefore)) {\n recordsFound++;\n latestCursor = change.cursor;\n cursorAdvanced = true;\n\n try {\n await this.processChange(runId, input, change);\n recordsProcessed++;\n } catch (err) {\n recordsFailed++;\n const message = err instanceof Error ? err.message : String(err);\n this.logger.warn(\n `sync item failed: subscription=${input.subscription.id} externalId=${change.externalId}: ${message}`,\n );\n await this.recorder.recordItem({\n syncRunId: runId,\n entityType: input.subscription.domain,\n externalId: change.externalId,\n operation: change.operation === 'deleted' ? 'deleted' : 'updated',\n status: 'failed',\n changedFields: {},\n error: message,\n tenantId: input.tenantId,\n });\n }\n }\n\n if (recordsFailed > 0 && recordsProcessed === 0 && recordsFound > 0) {\n // Every record we saw failed — call the run a failure, not a\n // success. Partial success (some processed, some failed) still\n // counts as 'success' so the cursor advances.\n status = 'failed';\n runError = `all ${recordsFailed} records failed`;\n } else if (recordsFound === 0) {\n status = 'no_changes';\n } else {\n status = 'success';\n }\n } catch (err) {\n // Source iterator itself threw — cursor DOES NOT advance past the\n // last-successful cursor. `latestCursor` still holds the last\n // `change.cursor` we observed, which is the furthest we know to\n // have delivered. Persist it (below) so next run resumes there.\n status = 'failed';\n runError = err instanceof Error ? err.message : String(err);\n this.logger.error(\n `sync source failed: subscription=${input.subscription.id}: ${runError}`,\n );\n }\n\n // Persist cursor advance only when something actually moved. Never\n // overwrite a valid cursor with `null` on a no-change run.\n if (cursorAdvanced && latestCursor !== null && latestCursor !== undefined) {\n try {\n await this.cursors.put(input.subscription.id, latestCursor, input.tenantId);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n this.logger.error(\n `cursor put failed: subscription=${input.subscription.id}: ${message}`,\n );\n if (status !== 'failed') {\n status = 'failed';\n runError = `cursor put failed: ${message}`;\n }\n }\n }\n\n const durationMs = Date.now() - startedAt;\n\n await this.recorder.completeRun(runId, {\n status,\n recordsFound,\n recordsProcessed,\n cursorAfter: cursorAdvanced ? latestCursor : cursorBefore,\n durationMs,\n error: runError,\n });\n\n return {\n runId,\n status,\n recordsFound,\n recordsProcessed,\n recordsFailed,\n cursorBefore,\n cursorAfter: cursorAdvanced ? latestCursor : cursorBefore,\n durationMs,\n error: runError,\n };\n }\n\n private async processChange(\n runId: string,\n input: ExecuteSyncInput<T>,\n change: Change<T>,\n ): Promise<void> {\n // Deletion branch — no diff, no upsert; soft-delete via sink.\n if (change.operation === 'deleted') {\n const result = await this.sink.softDeleteByExternalId(\n input.userId,\n change.externalId,\n );\n await this.recorder.recordItem({\n syncRunId: runId,\n entityType: input.subscription.domain,\n externalId: change.externalId,\n localId: result?.id ?? null,\n operation: result ? 'deleted' : 'noop',\n status: 'success',\n changedFields: {},\n tenantId: input.tenantId,\n });\n return;\n }\n\n // Create/update path — diff against local state, short-circuit on noop.\n const existing = await this.sink.findByExternalId(\n input.userId,\n change.externalId,\n );\n const diff = this.differ.diff(\n existing,\n change.record,\n change.providerChangedFields,\n );\n\n if (diff === 'noop') {\n await this.recorder.recordItem({\n syncRunId: runId,\n entityType: input.subscription.domain,\n externalId: change.externalId,\n localId: null,\n operation: 'noop',\n status: 'success',\n changedFields: {},\n tenantId: input.tenantId,\n });\n return;\n }\n\n const { id: localId } = await this.sink.upsertByExternalId(\n input.userId,\n change.record,\n input.provider,\n );\n\n await this.recorder.recordItem({\n syncRunId: runId,\n entityType: input.subscription.domain,\n externalId: change.externalId,\n localId,\n operation: existing === null ? 'created' : 'updated',\n status: 'success',\n changedFields: diff as FieldDiff,\n tenantId: input.tenantId,\n });\n }\n}\n","/**\n * PostgresCursorStore — Drizzle-backed `ICursorStore` (SYNC-4).\n *\n * Reads/writes `sync_subscriptions.cursor` directly — no service\n * composition. Consumers that want a service layer around subscriptions\n * wire it themselves; the port's contract is just cursor persistence.\n *\n * ## What `put` stamps\n *\n * `put` writes three columns in one statement: `cursor`, `last_sync_at`,\n * and `updated_at`. Rationale: SYNC-1's scheduling index\n * `(enabled, last_sync_at)` is useless if `last_sync_at` doesn't advance\n * with every cursor put. Every real consumer needs this stamped, so\n * bundling it here avoids every consumer wrapping the port in a service\n * layer just to stamp a timestamp.\n *\n * ## Multi-tenancy\n *\n * When `SYNC_MULTI_TENANT` is true (SYNC-6):\n * - every read/write is scoped by `AND tenant_id = $tenantId`\n * - a null/missing `tenantId` throws `MissingTenantIdError` via the\n * shared `assertTenantId` helper (one message shape across the\n * orchestrator + both backends, SYNC-6)\n * - explicit `null` also throws — matches JOB-8 / EVT-6 strict-enforcement\n *\n * When the flag is off, `tenantId` is ignored. Cross-tenant isolation is\n * the caller's problem in single-tenant deployments.\n */\nimport { Inject, Injectable, Optional } from '@nestjs/common';\nimport { and, desc, eq, type SQL } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport { DRIZZLE } from '../../constants/tokens';\nimport type {\n CursorSnapshot,\n ICursorStore,\n} from './sync-cursor-store.protocol';\nimport { syncSubscriptions } from './sync-audit.schema';\nimport { SYNC_MULTI_TENANT } from './sync.tokens';\nimport { assertTenantId } from './sync-errors';\n\n@Injectable()\nexport class PostgresCursorStore implements ICursorStore {\n private readonly multiTenant: boolean;\n\n constructor(\n @Inject(DRIZZLE) private readonly db: DrizzleClient,\n @Optional() @Inject(SYNC_MULTI_TENANT) multiTenant?: boolean,\n ) {\n this.multiTenant = multiTenant ?? false;\n }\n\n async get(\n subscriptionId: string,\n tenantId?: string | null,\n ): Promise<unknown | null> {\n const where = this.buildWhere(subscriptionId, tenantId, 'cursor.get');\n\n const rows = await this.db\n .select({ cursor: syncSubscriptions.cursor })\n .from(syncSubscriptions)\n .where(where)\n .limit(1);\n\n if (rows.length === 0) return null;\n return rows[0]?.cursor ?? null;\n }\n\n async put(\n subscriptionId: string,\n cursor: unknown,\n tenantId?: string | null,\n ): Promise<void> {\n const where = this.buildWhere(subscriptionId, tenantId, 'cursor.put');\n\n await this.db\n .update(syncSubscriptions)\n .set({\n cursor,\n lastSyncAt: new Date(),\n updatedAt: new Date(),\n })\n .where(where);\n }\n\n async listAll(tenantId?: string | null): Promise<CursorSnapshot[]> {\n assertTenantId(tenantId, {\n multiTenant: this.multiTenant,\n operation: 'cursor.listAll',\n });\n\n const where = this.multiTenant\n ? eq(syncSubscriptions.tenantId, tenantId as string)\n : undefined;\n\n const rows = await this.db\n .select({\n id: syncSubscriptions.id,\n integrationId: syncSubscriptions.integrationId,\n adapter: syncSubscriptions.adapter,\n domain: syncSubscriptions.domain,\n externalRef: syncSubscriptions.externalRef,\n cursor: syncSubscriptions.cursor,\n lastSyncAt: syncSubscriptions.lastSyncAt,\n updatedAt: syncSubscriptions.updatedAt,\n tenantId: syncSubscriptions.tenantId,\n })\n .from(syncSubscriptions)\n .where(where)\n .orderBy(desc(syncSubscriptions.updatedAt));\n\n return rows.map((row) => ({\n subscriptionId: row.id,\n integrationId: row.integrationId,\n adapter: row.adapter,\n domain: row.domain,\n externalRef: row.externalRef,\n cursor: row.cursor ?? null,\n lastSyncAt: row.lastSyncAt,\n updatedAt: row.updatedAt,\n tenantId: row.tenantId,\n }));\n }\n\n /**\n * Centralized WHERE clause — `get` and `put` share identical semantics.\n * Drift here would let a caller read under one tenancy rule and write\n * under another.\n */\n private buildWhere(\n subscriptionId: string,\n tenantId: string | null | undefined,\n operation: string,\n ): SQL | undefined {\n assertTenantId(tenantId, {\n multiTenant: this.multiTenant,\n operation,\n });\n if (this.multiTenant) {\n return and(\n eq(syncSubscriptions.id, subscriptionId),\n eq(syncSubscriptions.tenantId, tenantId as string),\n );\n }\n return eq(syncSubscriptions.id, subscriptionId);\n }\n}\n","/**\n * NestJS injection tokens\n *\n * Used with @Inject() decorator in concrete repository constructors.\n */\n\n/**\n * Injection token for the Drizzle ORM database client.\n *\n * Usage in concrete repositories:\n * ```typescript\n * constructor(@Inject(DRIZZLE) db: DrizzleClient) { super(db); }\n * ```\n */\nexport const DRIZZLE = 'DRIZZLE' as const;\n\n/**\n * Injection token for the event bus (IEventBus).\n *\n * Optional — only resolved when EventsModule.forRoot() is registered.\n * BaseService uses this with @Optional() to emit lifecycle events\n * without requiring the events subsystem to be installed.\n *\n * Usage in services/use cases:\n * ```typescript\n * @Optional() @Inject(EVENT_BUS) eventBus?: IEventBus\n * ```\n */\nexport const EVENT_BUS = 'EVENT_BUS' as const;\n","/**\n * DrizzleSyncRunRecorder — Drizzle-backed `ISyncRunRecorder` (SYNC-4).\n *\n * Generic write path only — extracted from the source app's\n * `SyncRunRecorderService`, minus CRM-specific convenience methods. Those\n * stay consumer-owned; the subsystem ships the substrate.\n *\n * ## Responsibilities\n *\n * - `startRun` — INSERT sync_runs row in status='running', returns id.\n * - `recordItem` — validates `changedFields` via `FieldDiffSchema.parse`\n * BEFORE the INSERT; a malformed shape throws before\n * any DB call fires. Enforces the ADR-0003 contract at\n * the write boundary.\n * - `completeRun` — UPDATE sync_runs with terminal status, counts,\n * cursor_after, duration_ms, completed_at.\n *\n * ## Multi-tenancy\n *\n * When `SYNC_MULTI_TENANT` is true (SYNC-6):\n * - `startRun` and `recordItem` require non-null `tenantId` on input.\n * Enforcement goes through the shared `assertTenantId` helper so the\n * error message shape matches the orchestrator entry point + the\n * cursor-store backends.\n * - `completeRun` does NOT re-check tenancy — the run id was returned\n * by `startRun` which already enforced it, and run ids are uuids that\n * aren't guessable cross-tenant. Matches JOB-3's pattern of trusting\n * the run-id for downstream mutations.\n */\nimport { Inject, Injectable, Optional } from '@nestjs/common';\nimport { and, desc, eq, type SQL } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport { DRIZZLE } from '../../constants/tokens';\nimport type {\n CompleteRunInput,\n ISyncRunRecorder,\n RecordItemInput,\n StartRunInput,\n SyncRunSummary,\n} from './sync-run-recorder.protocol';\nimport { syncRuns, syncRunItems, syncSubscriptions } from './sync-audit.schema';\nimport { FieldDiffSchema } from './sync-field-diff.protocol';\nimport { SYNC_MULTI_TENANT } from './sync.tokens';\nimport { assertTenantId } from './sync-errors';\n\n@Injectable()\nexport class DrizzleSyncRunRecorder implements ISyncRunRecorder {\n private readonly multiTenant: boolean;\n\n constructor(\n @Inject(DRIZZLE) private readonly db: DrizzleClient,\n @Optional() @Inject(SYNC_MULTI_TENANT) multiTenant?: boolean,\n ) {\n this.multiTenant = multiTenant ?? false;\n }\n\n async startRun(input: StartRunInput): Promise<{ id: string }> {\n assertTenantId(input.tenantId, {\n multiTenant: this.multiTenant,\n operation: 'startRun',\n });\n\n const rows = await this.db\n .insert(syncRuns)\n .values({\n subscriptionId: input.subscriptionId,\n direction: input.direction,\n action: input.action,\n status: 'running',\n cursorBefore: input.cursorBefore ?? null,\n tenantId: input.tenantId ?? null,\n })\n .returning({ id: syncRuns.id });\n\n const id = rows[0]?.id;\n if (!id) {\n // Drizzle's insert().returning() contract: at least one row is\n // returned for every successful INSERT. A missing id would indicate\n // a driver misbehavior; throw loudly rather than return bogus data.\n throw new Error('DrizzleSyncRunRecorder: INSERT RETURNING produced no id');\n }\n return { id };\n }\n\n async recordItem(input: RecordItemInput): Promise<void> {\n assertTenantId(input.tenantId, {\n multiTenant: this.multiTenant,\n operation: 'recordItem',\n });\n\n // ADR-0003 contract enforcement — reject malformed changedFields\n // before the DB call fires. `parse` throws a ZodError; callers see\n // the validation failure, not a DB constraint error.\n FieldDiffSchema.parse(input.changedFields);\n\n await this.db.insert(syncRunItems).values({\n syncRunId: input.syncRunId,\n entityType: input.entityType,\n externalId: input.externalId,\n localId: input.localId ?? null,\n operation: input.operation,\n status: input.status,\n changedFields: input.changedFields,\n title: input.title ?? null,\n error: input.error ?? null,\n tenantId: input.tenantId ?? null,\n });\n }\n\n async listRecent(\n limit: number,\n subscriptionId?: string,\n tenantId?: string | null,\n ): Promise<SyncRunSummary[]> {\n assertTenantId(tenantId, {\n multiTenant: this.multiTenant,\n operation: 'listRecent',\n });\n\n // JOIN against sync_subscriptions to resolve `integration_id` per run.\n // `sync_runs.subscription_id` is a non-null FK so INNER JOIN is correct;\n // there should be no orphaned runs.\n const conditions: SQL[] = [];\n if (subscriptionId !== undefined) {\n conditions.push(eq(syncRuns.subscriptionId, subscriptionId));\n }\n if (this.multiTenant) {\n conditions.push(eq(syncRuns.tenantId, tenantId as string));\n }\n const where =\n conditions.length === 0\n ? undefined\n : conditions.length === 1\n ? conditions[0]\n : and(...conditions);\n\n const rows = await this.db\n .select({\n id: syncRuns.id,\n subscriptionId: syncRuns.subscriptionId,\n integrationId: syncSubscriptions.integrationId,\n status: syncRuns.status,\n startedAt: syncRuns.startedAt,\n completedAt: syncRuns.completedAt,\n recordsProcessed: syncRuns.recordsProcessed,\n tenantId: syncRuns.tenantId,\n })\n .from(syncRuns)\n .innerJoin(\n syncSubscriptions,\n eq(syncRuns.subscriptionId, syncSubscriptions.id),\n )\n .where(where)\n .orderBy(desc(syncRuns.startedAt))\n .limit(limit);\n\n return rows.map((row) => ({\n id: row.id,\n subscriptionId: row.subscriptionId,\n integrationId: row.integrationId,\n status: row.status,\n startedAt: row.startedAt,\n completedAt: row.completedAt,\n recordsProcessed: row.recordsProcessed,\n tenantId: row.tenantId,\n }));\n }\n\n async completeRun(runId: string, input: CompleteRunInput): Promise<void> {\n await this.db\n .update(syncRuns)\n .set({\n status: input.status,\n recordsFound: input.recordsFound,\n recordsProcessed: input.recordsProcessed,\n cursorAfter: input.cursorAfter ?? null,\n durationMs: input.durationMs,\n error: input.error ?? null,\n completedAt: new Date(),\n })\n .where(eq(syncRuns.id, runId));\n }\n}\n","/**\n * SyncModule — `DynamicModule.forRoot({ backend, multiTenant? })` factory\n * wiring the sync subsystem's substrate (SYNC-6, ADR-008 subsystem pattern).\n *\n * ## What this module provides\n *\n * - `SYNC_CURSOR_STORE` — Drizzle or Memory cursor store\n * - `SYNC_RUN_RECORDER` — Drizzle or Memory run recorder\n * - `SYNC_FIELD_DIFFER` — default `DeepEqualDiffer`\n * - `SYNC_MULTI_TENANT` — resolved boolean flag (defaults to false)\n * - `SYNC_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 * - `SYNC_CHANGE_SOURCE` — per-provider per-entity; consumer binds in\n * their feature module (e.g. `OpportunitySyncModule` 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 * - `SYNC_SINK` — per canonical entity; consumer binds in their feature\n * module.\n * - `ExecuteSyncUseCase` — registered by the feature module alongside\n * its source + sink bindings. Providing the orchestrator here would\n * force Nest to resolve SYNC_CHANGE_SOURCE + SYNC_SINK at module\n * compile time, which fails when the feature module hasn't been\n * imported yet. Consumers register `ExecuteSyncUseCase` 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: [SyncModule.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: SYNC_CHANGE_SOURCE, useClass: SalesforceOpportunitySource },\n * { provide: SYNC_SINK, useClass: OpportunitySyncSink },\n * ExecuteSyncUseCase,\n * ],\n * })\n * export class OpportunitySyncModule {\n * constructor(\n * private readonly execute: ExecuteSyncUseCase<CanonicalOpportunity>,\n * ) {}\n * }\n * ```\n *\n * `global: true` means feature modules do not need to re-import\n * `SyncModule` — the substrate tokens are available project-wide.\n */\nimport { Module, type DynamicModule, type Provider } from '@nestjs/common';\nimport {\n SYNC_CURSOR_STORE,\n SYNC_FIELD_DIFFER,\n SYNC_MODULE_OPTIONS,\n SYNC_MULTI_TENANT,\n SYNC_RUN_RECORDER,\n} from './sync.tokens';\nimport { MemoryCursorStore } from './sync-cursor-store.memory-backend';\nimport { MemoryRunRecorder } from './sync-run-recorder.memory-backend';\nimport { PostgresCursorStore } from './sync-cursor-store.drizzle-backend';\nimport { DrizzleSyncRunRecorder } from './sync-run-recorder.drizzle-backend';\nimport { DeepEqualDiffer } from './deep-equal.differ';\n\nexport interface SyncModuleOptions {\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@Module({})\nexport class SyncModule {\n static forRoot(options: SyncModuleOptions): DynamicModule {\n const multiTenant = options.multiTenant ?? false;\n\n const sharedProviders: Provider[] = [\n { provide: SYNC_MODULE_OPTIONS, useValue: options },\n { provide: SYNC_MULTI_TENANT, useValue: multiTenant },\n // Default differ — consumers can override by binding a different\n // `IFieldDiffer<T>` to `SYNC_FIELD_DIFFER` in their feature module.\n { provide: SYNC_FIELD_DIFFER, useValue: new DeepEqualDiffer() },\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: SYNC_CURSOR_STORE,\n useExisting: MemoryCursorStore,\n },\n { provide: MemoryRunRecorder, useValue: new MemoryRunRecorder() },\n {\n provide: SYNC_RUN_RECORDER,\n useExisting: MemoryRunRecorder,\n },\n ]\n : [\n // Drizzle backends — injected with DRIZZLE (provided by the\n // consumer's DrizzleModule) + the SYNC_MULTI_TENANT flag\n // we bound above.\n { provide: SYNC_CURSOR_STORE, useClass: PostgresCursorStore },\n { provide: SYNC_RUN_RECORDER, useClass: DrizzleSyncRunRecorder },\n ];\n\n return {\n module: SyncModule,\n global: true,\n providers: [...sharedProviders, ...backendProviders],\n exports: [\n SYNC_MODULE_OPTIONS,\n SYNC_MULTI_TENANT,\n SYNC_FIELD_DIFFER,\n SYNC_CURSOR_STORE,\n SYNC_RUN_RECORDER,\n ],\n };\n }\n}\n"],"mappings":"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","names":["z","Injectable","Injectable","Injectable","out","Injectable","Injectable","Injectable","Inject","Injectable","Optional","Injectable","Inject","Optional","Inject","Injectable","Optional","and","desc","eq","eq","and","desc","Injectable","Inject","Optional"]}