Context
The #14026 detect-signal class now has a family of pure detect-producers — coverage-drift (dataIntegrityCoverageDiagnosis, merged #14075), vector-count monotonicity (#14095), SQLite-integrity (#14096), store-bloat (PR #14103), embedding-dimension (PR #14104) — all the same ({injected inputs}) → recovery-diagnosis | null shape. But every one is an inert pure function: nothing schedules them, gathers their inputs, or routes their diagnoses. The detect-class does not actually detect anything live until this integration lands. This is the #14026 "scheduled wiring" that was explicitly deferred from the producer leaves.
This is the single integration leaf (not N more producer tickets) that closes the full immune-system loop for data-integrity.
The Problem
A diagnostics producer that is never invoked is dead code. The data-integrity dimension that ADR-0025 §2.4 now documents (amendment #14091) and that #13999 proved we need has no live presence: no cadence, no input-gathering, no escalation. The recovery half is useless without the detect half running and reaching it.
The Architectural Reality
The established detect→diagnose→act wiring this plugs into:
- Detect/diagnose scheduling precedent:
ContainerHealthDiagnosisService runs via DeploymentStateBridgeService on a cadence (Orchestrator.mjs:311-316). The data-integrity runner is a sibling — a scheduled service that produces diagnoses from gathered facts.
- Act/escalate consume (already exists, V-B-A'd):
RecoveryActuatorService.escalateDiagnosis (#14061, RecoveryActuatorService.mjs:373) is class/kind-agnostic — it escalates ANY diagnosis whose details.actionClass === 'escalate' (:390), reads targetIdentity, records escalated, pages. The data-integrity diagnoses (recoveryClass: data-integrity, actionClass: escalate, compose-service target) flow through it untouched — confirmed by the live backup-failure escalation (taskOutcomeDiagnosis → escalateDiagnosis → page AGENT:*). No actuator change needed.
- Input sources:
auditChromaVectorCoverage (checkChromaIntegrity.mjs:447) for coverage + per-collection counts; store-dir size for bloat; a per-collection embedding-dimension audit + WAL-age sampling (the as-yet-unbuilt audit inputs for the dimension / WAL-age producers).
The Fix
A scheduled data-integrity diagnostics runner (sibling to the container-health diagnosis path) that, on a configurable cadence (ADR-0019 leaf):
- Gathers facts — runs
auditChromaVectorCoverage, measures store size, audits per-collection dimension + WAL-age; loads the prior sample (persisted) for the monotonicity/growth producers.
- Invokes the producers — feeds the gathered facts into each pure producer (
buildDataIntegrityCoverageDiagnosis, …Monotonicity…, …StoreBloat…, …DimensionConsistency…, SQLite), collecting the non-null diagnoses.
- Routes to the recovery daemon — passes each emitted diagnosis to
RecoveryActuatorService.escalateDiagnosis (the existing, generic escalate path) → page. Detect-only; NO auto-repair (data mutation stays operator-gated per ADR-0026).
- Persists the current sample as the next run's prior (the history store the monotonicity/growth signals need).
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
| Data-integrity diagnostics runner (new scheduled service) |
DeploymentStateBridgeService/ContainerHealthDiagnosisService scheduling precedent |
Gather facts → invoke producers → route diagnoses to escalateDiagnosis → persist sample |
A fact-gather failure reports probe-unavailable + skips that signal; never auto-acts |
service JSDoc + ADR-0025 §2.4 |
Unit: injected facts → producers fire → escalateDiagnosis invoked; clean facts → no escalation |
escalateDiagnosis consume |
#14061 (RecoveryActuatorService.mjs:390) |
UNCHANGED — accepts actionClass: escalate data-integrity/compose-service diagnoses as-is |
n/a |
— |
V-B-A'd generic; live backup-escalation proof |
| History/sample persistence |
monotonicity/growth producers need a prior sample |
Persist per-run per-collection counts + store-size (deployment-state-adjacent) |
First run = no prior → those signals no-op (no false escalation) |
JSDoc |
Unit: second run compares against first |
| Cadence + thresholds |
ADR-0019 SSOT |
All read at the runner use-site (no parallel readers) |
— |
config leaves |
— |
Decision Record impact
aligned-with ADR-0025 §2.4 (amendment #14091 — the data-integrity detect dimension) + ADR-0026 (escalate-only; no auto-repair). No ADR change — this is the live wiring of the documented model.
Acceptance Criteria
Out of Scope
- The pure producers themselves (their own leaves — coverage/monotonicity/SQLite/store-bloat/dimension).
- The as-yet-unbuilt dimension-audit + WAL-age fact-gathering helpers (these may be small sub-slices if non-trivial; the runner consumes them).
- Any auto-repair / recovery actuator action (escalate-only here; the data-recovery actuator is #14032/#14066 territory, operator-gated).
- The
escalateDiagnosis actuator itself (unchanged — already generic).
Related
- #14026 (parent detect-signal class — this is its "scheduled wiring" integration leaf), #14075/#14095/#14096/#14103/#14104 (the producers this schedules), #14061 (the escalate sink it routes to), #14091 (the ADR-0025 §2.4 amendment), #14046 (the e2e gate — this is the live counterpart to its in-test scheduled-signal proof), #13999 (the incident the live loop would have caught).
Authored by Vega (Claude Opus 4.8, Claude Code) — #14026 steward; the integration leaf that turns the inert producers into a live immune-system detect→escalate loop. Session c94ea3b2-1ae8-48fd-8f34-1c54d90f5caa.
Context
The #14026 detect-signal class now has a family of pure detect-producers — coverage-drift (
dataIntegrityCoverageDiagnosis, merged #14075), vector-count monotonicity (#14095), SQLite-integrity (#14096), store-bloat (PR #14103), embedding-dimension (PR #14104) — all the same({injected inputs}) → recovery-diagnosis | nullshape. But every one is an inert pure function: nothing schedules them, gathers their inputs, or routes their diagnoses. The detect-class does not actually detect anything live until this integration lands. This is the #14026 "scheduled wiring" that was explicitly deferred from the producer leaves.This is the single integration leaf (not N more producer tickets) that closes the full immune-system loop for data-integrity.
The Problem
A diagnostics producer that is never invoked is dead code. The data-integrity dimension that ADR-0025 §2.4 now documents (amendment #14091) and that #13999 proved we need has no live presence: no cadence, no input-gathering, no escalation. The recovery half is useless without the detect half running and reaching it.
The Architectural Reality
The established detect→diagnose→act wiring this plugs into:
ContainerHealthDiagnosisServiceruns viaDeploymentStateBridgeServiceon a cadence (Orchestrator.mjs:311-316). The data-integrity runner is a sibling — a scheduled service that produces diagnoses from gathered facts.RecoveryActuatorService.escalateDiagnosis(#14061,RecoveryActuatorService.mjs:373) is class/kind-agnostic — it escalates ANY diagnosis whosedetails.actionClass === 'escalate'(:390), readstargetIdentity, recordsescalated, pages. The data-integrity diagnoses (recoveryClass: data-integrity,actionClass: escalate,compose-servicetarget) flow through it untouched — confirmed by the live backup-failure escalation (taskOutcomeDiagnosis→escalateDiagnosis→ pageAGENT:*). No actuator change needed.auditChromaVectorCoverage(checkChromaIntegrity.mjs:447) for coverage + per-collection counts; store-dir size for bloat; a per-collection embedding-dimension audit + WAL-age sampling (the as-yet-unbuilt audit inputs for the dimension / WAL-age producers).The Fix
A scheduled data-integrity diagnostics runner (sibling to the container-health diagnosis path) that, on a configurable cadence (ADR-0019 leaf):
auditChromaVectorCoverage, measures store size, audits per-collection dimension + WAL-age; loads the prior sample (persisted) for the monotonicity/growth producers.buildDataIntegrityCoverageDiagnosis,…Monotonicity…,…StoreBloat…,…DimensionConsistency…, SQLite), collecting the non-null diagnoses.RecoveryActuatorService.escalateDiagnosis(the existing, generic escalate path) → page. Detect-only; NO auto-repair (data mutation stays operator-gated per ADR-0026).Contract Ledger Matrix
DeploymentStateBridgeService/ContainerHealthDiagnosisServicescheduling precedentescalateDiagnosis→ persist sampleescalateDiagnosisinvoked; clean facts → no escalationescalateDiagnosisconsumeRecoveryActuatorService.mjs:390)actionClass: escalatedata-integrity/compose-service diagnoses as-isDecision Record impact
aligned-withADR-0025 §2.4 (amendment #14091 — the data-integrity detect dimension) + ADR-0026 (escalate-only; no auto-repair). No ADR change — this is the live wiring of the documented model.Acceptance Criteria
RecoveryActuatorService.escalateDiagnosis.escalateDiagnosisinvoked with the right diagnoses; clean facts → no escalation.Out of Scope
escalateDiagnosisactuator itself (unchanged — already generic).Related
Authored by Vega (Claude Opus 4.8, Claude Code) — #14026 steward; the integration leaf that turns the inert producers into a live immune-system detect→escalate loop. Session c94ea3b2-1ae8-48fd-8f34-1c54d90f5caa.