Status: Accepted · Date: 2026-07-09
Two coupled data-integrity defects (roadmap B1, FIX-01; both live in every
olive-monitor run before this change):
- Calibration clobber.
monitor/service.pywrotestore.set_calibration(config.calibration_offset, …)on every start, overwriting the single DB calibration row with the config value. Runningolive-calibrateand thenolive-monitorwith a default config silently reverted the device to uncalibrated, and pre-fix calibration provenance is unrecoverable by construction (the single row was overwritten in place). - Baked-in offsets. Event levels were stored offset-adjusted
(
dbfs(frame, calibration_offset=…)), so recalibrating changed the meaning of new rows relative to old rows with no reconciliation, corrupting the longitudinal record.
This tool's entire value is honest, longitudinal noise evidence; a stored level whose meaning depends on when it was written breaks that promise.
- Events store raw dBFS. No calibration is ever baked into a persisted level.
threshold_dbfsis therefore defined against the raw scale too, so recalibrating never changes detection sensitivity (users who tuned a threshold under a baked nonzero offset must re-tune — disclosed inmonitor/config.pyand the README). - Calibration is an append-only history (
calibration_history:effective_from,offset,note,reference_instrument), schema v3.olive-calibrateis the only production writer and records--reference-instrumentprovenance. The v2→v3 migration preserves any legacycalibrationid=1 row as epoch 0 (effective_from = 0). - Calibration is applied at render time, uniformly. A single per-event resolver
(
report/render.py::_per_event_offsets; attribution by event start) feeds the HTML report and every export (--csv,--violations-csv,--violations-html), so no two artifacts generated from the same log can disagree numerically. Each CSV row records the offset included in its values (calibration_offset_db; raw = value − offset), and the violations report derives its calibrated/uncalibrated statement from the store's history — never from the deprecated config field. Windows spanning more than one epoch render a per-epoch disclosure. - Rows before the first epoch carry its offset retroactively — and say so. A
timestamp earlier than the first
effective_fromresolves to that first epoch (epoch 0 covers all historical rows), which keeps every level in a report on one scale and keeps re-rendering stable. It also means a calibration taken on day 20 is applied to readings from day 1, on the assumption that the microphone, gain, and placement did not change in between — an assumption the record cannot confirm. Since 2026-08-21 (issue #50) every artifact discloses it: the report's calibration banner and methodology line name how many events (and ambient-ledger minutes) the first calibration postdates and the date it was taken; every CSV row and the violations table carry acalibration_basisofin-forceorback-applied(orbootstrap-config/nonewithout a history). The migration's epoch 0 ateffective_from = 0is not reported this way — it genuinely covers everything, and has the legacy caveat above instead. - Migration timestamps are recorded in a
schema_migrationstable (version,applied_at), written byEventStore._migratefrom this version on. The v3 timestamp is the era boundary: sessions/events written before it may carry a baked-in offset; rows after it are raw. Migrations applied by older binaries have no row (time of application honestly unknown). config.calibration_offset/calibration_noteare bootstrap-only (deprecated): they seed a never-calibrated database and are ignored once history exists.
- Re-rendering any historical date range yields identical numbers before and after a recalibration; recalibration provenance is durable from this version forward.
- Legacy data (honest limits): default-config histories are numerically unaffected
(the clobber itself forced the stored offset to 0.0, so stored levels already equal
raw dBFS). If a nonzero
calibration_offsetwas ever configured pre-v3, those events carry it baked in and epoch 0 re-applies it at render — they render over-adjusted. No automated rewrite of historical rows is performed; recovery, where session links exist (schema v2+), israw = stored − sessions.calibration_offsetfor sessions that started before the v3applied_attimestamp. v1-era events with no session link are not attributable to an offset and are honestly unrecoverable. Pre-fix calibration provenance cannot be reconstructed. The multi-epoch report disclosure states this caveat. - The legacy single-row
calibrationtable remains (no writers); dropping it is deferred to a future migration so old binaries pointed at a new DB fail soft rather than corrupt. - Schema versioning discipline: v3 is taken by this change. Any other pending schema change (e.g. FIX-02 parameter provenance, PR #12) must rebase onto this and become v4+ — two branches must never both define v3.
- Rewrite historical rows during migration (subtract the baked offset): rejected — it would silently mutate evidence, and v1 rows have no attributable offset at all. Disclosure plus a recorded era boundary is the honest posture.
- Keep baked-in storage but log calibration changes: rejected — every render would still need per-row era knowledge, and stored numbers would keep changing meaning.
- Version-bump the app to mark the era (
sessions.app_version): weaker than a DB-recorded timestamp (the version string does not say when the database crossed the boundary) and conflated with release semantics;schema_migrationsrecords it directly.