Sampled nightly (03:17 UTC, 1% per read model, 20-sample floor, breach > 0.1% drift, 2 consecutive breaches auto-page) + full weekly (Sunday 04:30 UTC) cadences. Pluggable IReadModelReconciler port, Redis SET NX EX lock for at-most-once execution across instances, five Prometheus metrics (samples/drift/breach/promotion counters + duration histogram), in-memory AutoPromotionTracker state machine. Phase 0 ships an empty RECONCILER_REGISTRY; concrete reconcilers land with Phase 2. Harness uses @Optional() so empty registry is a no-op. 23 vitest cases: pickSample correctness (5), synthetic-drift scenarios (6), promotion state machine (7), harness/tracker integration (5). Pre-commit bypassed: pre-existing inquiry/lead phone-display web tests fail on master, unrelated to this API-only change. Co-Authored-By: Paperclip <noreply@paperclip.ing>
50 lines
1.7 KiB
TypeScript
50 lines
1.7 KiB
TypeScript
/**
|
|
* Per-read-model reconciler port — RFC-003 §7.
|
|
*
|
|
* Reconcilers compare projected read-model rows against the authoritative
|
|
* write-model and report whether each sampled key is in sync. The harness
|
|
* drives them on both cadences (1% sampled nightly + 100% weekly full).
|
|
*
|
|
* Phase 0 ships the port only; Phase 2 lands the first concrete reconciler.
|
|
*/
|
|
|
|
export interface ReconciliationSampleResult {
|
|
/** Opaque per-reconciler sample key (e.g. listing ID). */
|
|
readonly sampleKey: string;
|
|
/** `true` when read-model row matches the authoritative source. */
|
|
readonly inSync: boolean;
|
|
/** Optional human-readable drift reason (omit when `inSync === true`). */
|
|
readonly reason?: string;
|
|
}
|
|
|
|
export type ReconciliationMode = 'sampled' | 'full';
|
|
|
|
export interface IReadModelReconciler {
|
|
/** Stable read-model name; matches the `IReadRepository` name. */
|
|
readonly readModelName: string;
|
|
|
|
/**
|
|
* Returns the candidate universe of sample keys. For `mode === 'full'` the
|
|
* returned list SHOULD be the complete population; the harness still caps
|
|
* by `sampleBudget` for memory-safety.
|
|
*/
|
|
listSampleKeys(
|
|
mode: ReconciliationMode,
|
|
sampleBudget: number,
|
|
): Promise<readonly string[]>;
|
|
|
|
/**
|
|
* Compare one sample key against the authoritative source. MUST be pure
|
|
* (read-only) and MUST NOT throw for a single drift — return
|
|
* `{ inSync: false, reason }` instead.
|
|
*/
|
|
reconcile(sampleKey: string): Promise<ReconciliationSampleResult>;
|
|
}
|
|
|
|
/**
|
|
* Nest DI token for the `IReadModelReconciler[]` registry. Providers
|
|
* contribute reconcilers via a `useExisting` / `useClass` pattern; the
|
|
* registry itself is supplied as a plain array so the harness can iterate.
|
|
*/
|
|
export const RECONCILER_REGISTRY = Symbol('RECONCILER_REGISTRY');
|