Files
goodgo-platform/apps/api/src/modules/read-models/domain/reconciler.ts
Ho Ngoc Hai 05a629cf21 feat(read-models): reconciliation harness for RFC-003 Phase 0 (GOO-191)
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>
2026-04-24 12:34:23 +07:00

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');