Series-3 decode correctness. Two body-model rewrites, a systematic
scale error affecting every geophone reading ever produced, a recovered
file format, and two artifact-hygiene bugs where stale files outlived the
decodes that made them. All 11,603 series-3 binaries in the production
snapshot now pass every check.
Fixed
Series-3 health sweep: 11,603 / 11,603 binaries now clean on every check.
Swept every series-3 file with the live decoder against five independent
checks — decode exceptions, zero samples, unequal geo channel lengths, peaks
above range full scale, decoded peak vs the device-reported PPV, and waveform
length vs the declared record time. Three real defects surfaced and were
fixed:
block[22] is not a constant and must not be tested. It was documented
as always 0x00 but carries data on loud blocks, and rejecting those threw
away the interval holding the event peak. BE18350/T350L7HR.NL0H block 92 has block[22]=0x26 and a Tran peak of 0x0563 = 1379 counts = 6.895 in/s — exactly the device-reported PPV —
while the file as a whole decoded to 0.015 in/s. block[0]==0x00, block[4]==0x0A and the 4-byte tail are six bytes of constraint, which is
what keeps trailer content out.
Block-model dispatch now goes on signature strength, not on whichever
decoder returns first. A multi-interval body also yields scattered
standard-tail blocks by coincidence; dispatching on "first non-empty"
handed 193 BE18193 files to the standard walker and produced peaks of
149 in/s against a 10 in/s full scale.
Multi-interval stride detection requires the block counter to increment
by exactly 1. Without it the detector false-positives on ordinary
standard-block bodies: those carry a header every 32 bytes, and 192 = 12 + 20×9 and 512 = 12 + 20×25 are both multiples of 32, so a
stride "fits" while actually skipping 6 or 16 real blocks. That misrouted
9,082 files.
Partial-block garbage is now trimmed within the final block only, stopping at
the first slot with a non-zero tail word or a geo peak above full scale.
Trimming purely from the end left garbage stranded behind one slot that
happened to have a zero tail word; trimming on the tail word alone truncated
four BE9440 files by up to 2,800 intervals.
Sub-minute histogram intervals are packed several to a block — 415 files
recovered. The device always writes one minute of data per block, so a
shorter interval just means more intervals in a longer block:
interval
intervals/block
stride
1 min
1
32 (the standard block)
15 s
4
92
2 s
30
612
stride = 12 + n * 20. Each 20-byte record carries 8 × uint16 little-endian values — peak and half-period per channel — plus a 2-word
tail whose first word is 0000 on every real interval (a session ending
mid-block leaves buffer garbage in the remaining slots, which decoded as
peaks thousands of times the real value until that check was added). The standard 32-byte block is big-endian; this variant is not.
These 415 files (216 on BE18193 at 2 s intervals, 199 on BE9440 at 15 s)
previously decoded to nothing at all — and before that were being accepted
by the waveform codec, which returned garbage peaking up to 400× the
device-reported PPV.
Ground truth BE9440/K440L3AQ.T70H — 5,710 intervals — matches its
Blastware ASCII export on 17,130/17,130 geo peaks, 22,840/22,840
frequencies and 5,710/5,710 mic dB(L) values. Across all 455 affected
files, 1,354/1,365 (99.2%) channel peaks match the device-reported PPV;
the 11 that don't are under-reads on BE9440 where the walk stops early.
backfill_sidecars.py now removes a stale .h5 when nothing decodes.
It previously skipped the write "so we don't replace whatever's there with an
empty placeholder", which silently preserved output from a superseded
decoder. After the record-chain fix, 415 histogram files stopped decoding (an
unmapped block variant on BE18193 and BE9440) but kept .h5 files whose peaks
ran up to 400× the device's own reported PPV — garbage feeding the charts
and the false-trigger detector with nothing marking it. Reports a stale_h5_removed count.
The series-3 waveform body is a RECORD CHAIN, not a tag stream — this
supersedes the segment-header model, including the fixes made earlier the
same day.
Records are self-delimiting. off+2 is a uint16 BE length and next_record = off + 2 + len; the chain ends on a record whose chan_id is 0x06. off+8 carries a 3-valued mode enum:
mode
header
data section
02 00
14 B
anchors, then cumulative deltas
01 00
10 B
no anchors, absolute values
00 03
10 B
no tags at all — raw 12-bit packed absolute
40 NN is an ordinary int16 BE data block (2*NN + 2), never a segment
header. Reading it as a 2*NN + 16 header is what made walks drift — and the
"variable-prefix segment descriptors" reported earlier today were not a format
feature at all, just walker drift of exactly 4 - (old_stop - true_record_start) on all 25 affected files.
Measured against the production snapshot:
before
after
all four channels equal length
156 / 1388
1388 / 1388
ASCII sample-count exact
72 / 75
75 / 75
ASCII fully exact
70 / 75
73 / 75
device PPV, waveform (live decode)
1288 / 1306
1306 / 1306
device PPV, histogram (live decode)
4434 / 4459
4458 / 4459
Mean absolute PPV ratio error on waveforms is now 0.00000. The 2 remaining
ASCII imperfections differ by exactly 1 LSB on samples sitting at the
±10.000 in/s rail.
This also eliminated the walker-over-read class. 24 of those 35 files
were histograms that read_blastware_file fed to the waveform codec first;
the old walker accepted them and returned garbage (one yielded 98,923
"intervals"), while the record-chain decoder correctly returns None so they
fall through to histogram_codec.
00 03 records are decoded rather than skipped. Skipping them does not merely
lose samples — it silently shifts the time base of everything after them on
that channel (observed on BE9558/K558LOF2.820W, MicL displaced by exactly
512 samples with nothing marking the gap).
Footer detection now prefers whichever 0e 08 candidate yields a chain
terminating on 0x06, since the signature can occur inside a sample stream.
Blast radius: 1 file of 1,388.
The superseded model is retained as decode_waveform_legacy and pinned by micromate/idf_file.py, whose Thor IDFW body-offset search trial-decodes
candidate offsets and keeps whichever yields the most samples — the new
decoder correctly returns None where the old one returned garbage, which
changes that heuristic's winner. Switching Thor over is deferred until that
search is reworked to use the record chain directly.
Series-3 histogram block is uniformly big-endian, and the stream's final
block has its own tail — the codec was clipping large peaks and dropping the
last interval of nearly every histogram.
Peaks and half-periods are uint16 big-endian, not uint8 plus an
"annotation" byte: T_peak[5:7], T_halfperiod[7:9], V_peak [9:11], and so on. Only block_ctr[2:4] is little-endian. The old
model silently clipped any peak above 1.275 in/s — the final interval
of BE18193/T193LQ9K.OE0H reads 8.270 in/s in Blastware's own export and
decoded as 0.590. The "annotation" byte was the half-period's high byte,
which is why it was non-zero exactly on the sub-Hz intervals BW renders
as <1.0.
The marker is block[4] alone. Testing [4:6] as a uint16 LE
marker forced block[5] == 0 — which is what capped the peak at one byte
in the first place.
The last block of each stream carries tail 9c 06 00 42 instead of 1e 0a 00 00, with arbitrary bytes at [21:23]. Rejecting it dropped
the final interval of nearly every histogram, and that interval is
frequently the one holding the event peak — so the file's reported PPV
came out low.
Verified against 1211 production histograms paired with their preserved
Blastware ASCII exports, which carry a full per-interval data table: 1211/1211 now decode exactly (interval count plus every per-interval
peak), and 842,442 per-interval frequency comparisons match with zero
mismatches. Before this fix: 1 of 1196.
decode_histogram_body_full records now expose is_terminal in place of
the removed annotations tuple.
Geophone full scale is 32000 ADC counts, not 32768 — every geo reading was
2.3% low. The verified body codec emits geo samples in 16-count units whose
documented LSB is exactly 0.005 in/s, and decoded_to_adc_counts multiplies
by 16, so one ADC count is 0.005/16 in/s and Normal range (10.000 in/s) is 10.0 / (0.005/16) = 32000 counts. Both sfm/event_hdf5.py and minimateplus/event_file_io.py divided by 32768, scaling every geophone
sample and every derived peak down by 1 - 32000/32768 = 2.34%.
Measured against 216 per-channel comparisons with preserved Blastware ASCII
exports: 32768 → 151/216 exact (worst error 0.238 in/s on a 10 in/s
event); 32000 → 216/216 exact, worst error 0.005 in/s (exactly 1 LSB —
pure quantization). The error scales with amplitude, so it was invisible on
quiet events and worst on the loud ones that matter for compliance.
The mic path is unaffected — it back-solves its own per-count factor from the
device-reported peak.
Scope: the scale lives in _samples_to_float, which every event passes
through regardless of which codec produced the samples — so this affected waveforms, histograms and series-4 (Thor IDF) alike, not just waveforms.
Verified after regeneration: series-3 histogram peaks vs their ASCII reports
now sit at a median ratio of 1.0000 across 1,137 comparisons (0.9766 under
32768); series-4 peaks vs device peaks moved from a median 0.960 to 0.983
across 1,468 comparisons. The four block-framing fixes below are
waveform-only — histograms decode via histogram_codec.decode_histogram_body,
which is untouched.
Series-3 waveform codec: four block-framing cases caused silent channel
truncation.walk_body hit its unknown-tag break mid-stream and every
channel decoded after that point came out short — typically Vert/Long/MicL,
sometimes at a third of their true length, with no error raised.
Wide-NN RLE 0X NN — the 12-bit NN encoding already handled for 1X NN / 2X NN also applies to the 00 NN RLE tag. Runs longer than
252 samples must use the wide form (e.g. 01 0c = 268 repeats).
30 NN with NN > 0x10 — the 0 < NN <= 0x10 guard was arbitrary;
data-section 30 NN blocks reach at least NN = 0x18. The length formula
(NN × 1.5 + 2) was already correct.
Variable-width 40 NN segment headers — NN is the count of
previous-channel continuation deltas, so the header is 2 × NN + 16
bytes and every field after the deltas shifts by 2 × NN. Only 40 02
(20 bytes) was handled; 40 01 (18) and 40 03 (22) both occur.
Tagless segment headers — a segment header can appear with no 40 NN tag at all: just the 14-byte tail [field2:2][len:2][channel_id:4][marker:2][anchors:4]. This is the NN=0
case (no continuation deltas needed, so no tag and no delta bytes). It is
where the walk stopped in 7 of the 8 events still truncating after the
first three fixes.
Changed
Segment channel now comes from the header's own channel-id byte rather
than from rotation position. The field previously documented as a
"monotonic uint32 LE counter" is really [channel][00][00][segment_index]
with 0x46=Tran 0x47=Vert 0x48=Long 0x49=MicL — verified on 1697 of 1697 segment headers across the ground-truth corpus with zero
disagreements. Rotation-by-position is kept only as a fallback for unknown
ids; it was fragile because a single missed or extra header (exactly what
tagless headers caused) desynced every channel after it.
parse_segment_header return shape — now n_prev_deltas, prev_deltas, marker, anchors, channel, segment_index in place of
the fixed-offset anchor_bytes / fixed_pattern / tail keys. The old fixed_pattern (02 00 00 01) conflated the 2-byte constant marker with
the first anchor. counter is retained as the raw uint32 of the id field.
Verification
Against the 75 ground-truth events (BW binary paired with its preserved _ASCII.TXT export), decoding end-to-end through the production path:
before
after
exact (full length, within 1 LSB)
37
72
truncated
23
3
full length, value error > 2 LSB
15
0
Worst remaining error among the 72: 0.0050 in/s = exactly 1 LSB.
No regressions — the byte-exact fixture suite still passes, and the full-suite
failure list is unchanged from baseline (16 pre-existing failures from
gitignored fixtures).
Notes
The "DC offset" symptom is not a decode bug. Events whose geo trace
sits at a constant level instead of oscillating around zero
(dominant-axis |mean| / peak >> 0) reproduce exactly in Blastware's own
ASCII export — e.g. BE12599/N599LQD7.8E0W Tran reads mean +0.345,
min +0.335, max +0.355 in both. It is a known recurring hardware fault (the
operators call it an "offset"): the affected channel's baseline exceeds the
unit's own geo trigger level, so the unit retriggers continuously and floods
the ACH queue with garbage events. Store-wide it affects 2 units of 21 across
6 episodes; see scratch/offset_candidates.csv and the project memory notes.
Still open: 3 of 75 ground-truth events truncate at a segment-header
variant with a variable-width prefix.Resolved later the same day — the
record-chain rewrite (above) showed there is no variable prefix; it was
walker drift. All 75 are now sample-count exact.
Still open after this release:
Series-4 (Thor / Micromate) is not verified — UM-series sits at ~48%
against device peaks with a ~1.7% systematic bias and a near-zero tail.
Thor IDFW is pinned to decode_waveform_legacy deliberately.
14 sensitive-range files show a decoded/truth ratio of exactly 8.0
(= 10.0/1.25) — a units bug, not a codec one. Never chased.
backfill_sidecars.py --force also inserts DB rows for store files
that have none (1,286 on the snapshot; one-time per store), and the
dry-run does not report that count before you commit to it.
Verification is uneven: per-sample proof on the 11% of files with a
preserved _ASCII.TXT, peak-and-structure consistency on the other 89%.
v0.25.0 — 2026-08-25
reviewed_real 3-state review flag + twin review-propagation. The events table and the /db/events feed now carry reviewed_real, a
3-state review flag mutually exclusive with false_trigger, mirrored from
the sidecar review block — plus histogram/waveform twin review-propagation
(flagging one flags both, matched by serial + identical PVS + timestamp
window).
Added
events columnreviewed_real — INTEGER NOT NULL DEFAULT 0, added
via the existing incremental _migrate ADD COLUMN pass (auto-migrates on SeismoDb() construction, no manual migration). query_events / get_event (and thus /db/events) return it automatically (SELECT *).
Mutual exclusivity with false_trigger — setting reviewed_real=1
clears false_trigger, and vice versa, enforced on both review paths:
the sidecar review PATCH (update_event_review) and the quick PATCH /db/events/{id}/false_trigger endpoint (set_false_trigger).
find_twins — matches an event's histogram/waveform twins by serial +
identical peak-vector-sum + a timestamp window.
propagate_review_to_twins — copies an event's false_trigger/ reviewed_real state onto its twins, wired into both the PATCH /db/events/{id}/sidecar review path and the quick PATCH /db/events/{id}/false_trigger path, so flagging one flags both
regardless of which endpoint made the change.
v0.24.0 — 2026-08-22
Waveform-shape metrics on events. The events table and the /db/events
feed now carry per-event crest factor and points-near-peak, computed from the
decoded waveform samples at ingest — groundwork for Terra-View's
false-trigger detection (Phase B).
Added
events columnsshape_crest_factor, shape_near_peak_count, shape_sample_count, shape_axis. Added via the existing incremental _migrate ADD COLUMN pass — auto-migrates on SeismoDb() construction,
no manual migration. query_events / get_event (and thus /db/events)
return them automatically (SELECT *).
Populated at ingest — crest factor + near-peak-count are computed from
the decoded samples in every save path (shape_from_h5/ shape_from_samples), and insert_events persists them on INSERT and
UPSERT.
Backfillscripts/backfill_event_shape.py — fills the columns for
existing events from their on-disk .h5 waveform samples (idempotent,
UPDATE-only).
Upgrade Notes
Run the backfill once after deploying, against the events DB + waveform store: python3 scripts/backfill_event_shape.py --db-path <seismo_relay.db> --store-root <waveforms/>
Events with no decodable samples (or no waveform file) stay NULL and render
"—" downstream.
## v0.26.0 — 2026-08-27
**Series-3 decode correctness.** Two body-model rewrites, a systematic
scale error affecting every geophone reading ever produced, a recovered
file format, and two artifact-hygiene bugs where stale files outlived the
decodes that made them. All 11,603 series-3 binaries in the production
snapshot now pass every check.
### Fixed
- **Series-3 health sweep: 11,603 / 11,603 binaries now clean on every check.**
Swept every series-3 file with the live decoder against five independent
checks — decode exceptions, zero samples, unequal geo channel lengths, peaks
above range full scale, decoded peak vs the device-reported PPV, and waveform
length vs the declared record time. Three real defects surfaced and were
fixed:
- **`block[22]` is not a constant and must not be tested.** It was documented
as always `0x00` but carries data on loud blocks, and rejecting those threw
away the interval holding the event peak.
`BE18350/T350L7HR.NL0H` block 92 has `block[22]=0x26` and a Tran peak of
`0x0563` = 1379 counts = **6.895 in/s** — exactly the device-reported PPV —
while the file as a whole decoded to 0.015 in/s. `block[0]==0x00`,
`block[4]==0x0A` and the 4-byte tail are six bytes of constraint, which is
what keeps trailer content out.
- **Block-model dispatch now goes on signature strength, not on whichever
decoder returns first.** A multi-interval body also yields scattered
standard-tail blocks by coincidence; dispatching on "first non-empty"
handed 193 BE18193 files to the standard walker and produced peaks of
149 in/s against a 10 in/s full scale.
- **Multi-interval stride detection requires the block counter to increment
by exactly 1.** Without it the detector false-positives on ordinary
standard-block bodies: those carry a header every 32 bytes, and
`192 = 12 + 20×9` and `512 = 12 + 20×25` are both multiples of 32, so a
stride "fits" while actually skipping 6 or 16 real blocks. That misrouted
9,082 files.
Partial-block garbage is now trimmed within the final block only, stopping at
the first slot with a non-zero tail word or a geo peak above full scale.
Trimming purely from the end left garbage stranded behind one slot that
happened to have a zero tail word; trimming on the tail word alone truncated
four BE9440 files by up to 2,800 intervals.
- **Sub-minute histogram intervals are packed several to a block — 415 files
recovered.** The device always writes one minute of data per block, so a
shorter interval just means more intervals in a longer block:
| interval | intervals/block | stride |
|---|---|---|
| 1 min | 1 | 32 (the standard block) |
| 15 s | 4 | 92 |
| 2 s | 30 | 612 |
`stride = 12 + n * 20`. Each 20-byte record carries 8 × uint16
**little**-endian values — peak and half-period per channel — plus a 2-word
tail whose first word is `0000` on every real interval (a session ending
mid-block leaves buffer garbage in the remaining slots, which decoded as
peaks thousands of times the real value until that check was added).
**The standard 32-byte block is big-endian; this variant is not.**
These 415 files (216 on BE18193 at 2 s intervals, 199 on BE9440 at 15 s)
previously decoded to nothing at all — and before that were being accepted
by the *waveform* codec, which returned garbage peaking up to 400× the
device-reported PPV.
Ground truth `BE9440/K440L3AQ.T70H` — 5,710 intervals — matches its
Blastware ASCII export on **17,130/17,130** geo peaks, **22,840/22,840**
frequencies and **5,710/5,710** mic dB(L) values. Across all 455 affected
files, **1,354/1,365 (99.2%)** channel peaks match the device-reported PPV;
the 11 that don't are under-reads on BE9440 where the walk stops early.
- **`backfill_sidecars.py` now removes a stale `.h5` when nothing decodes.**
It previously skipped the write "so we don't replace whatever's there with an
empty placeholder", which silently preserved output from a superseded
decoder. After the record-chain fix, 415 histogram files stopped decoding (an
unmapped block variant on BE18193 and BE9440) but kept `.h5` files whose peaks
ran up to **400× the device's own reported PPV** — garbage feeding the charts
and the false-trigger detector with nothing marking it. Reports a
`stale_h5_removed` count.
- **The series-3 waveform body is a RECORD CHAIN, not a tag stream — this
supersedes the segment-header model, including the fixes made earlier the
same day.**
Records are self-delimiting. `off+2` is a `uint16 BE` length and
`next_record = off + 2 + len`; the chain ends on a record whose `chan_id` is
`0x06`. `off+8` carries a 3-valued mode enum:
| mode | header | data section |
|---|---|---|
| `02 00` | 14 B | anchors, then **cumulative deltas** |
| `01 00` | 10 B | no anchors, **absolute** values |
| `00 03` | 10 B | **no tags at all** — raw 12-bit packed absolute |
**`40 NN` is an ordinary int16 BE data block** (`2*NN + 2`), never a segment
header. Reading it as a `2*NN + 16` header is what made walks drift — and the
"variable-prefix segment descriptors" reported earlier today were not a format
feature at all, just walker drift of exactly
`4 - (old_stop - true_record_start)` on all 25 affected files.
Measured against the production snapshot:
| | before | after |
|---|---|---|
| all four channels equal length | 156 / 1388 | **1388 / 1388** |
| ASCII sample-count exact | 72 / 75 | **75 / 75** |
| ASCII fully exact | 70 / 75 | **73 / 75** |
| device PPV, waveform (live decode) | 1288 / 1306 | **1306 / 1306** |
| device PPV, histogram (live decode) | 4434 / 4459 | **4458 / 4459** |
Mean absolute PPV ratio error on waveforms is now 0.00000. The 2 remaining
ASCII imperfections differ by exactly 1 LSB on samples sitting at the
±10.000 in/s rail.
**This also eliminated the walker-over-read class.** 24 of those 35 files
were histograms that `read_blastware_file` fed to the *waveform* codec first;
the old walker accepted them and returned garbage (one yielded 98,923
"intervals"), while the record-chain decoder correctly returns `None` so they
fall through to `histogram_codec`.
`00 03` records are decoded rather than skipped. Skipping them does not merely
lose samples — it silently shifts the time base of everything after them on
that channel (observed on `BE9558/K558LOF2.820W`, MicL displaced by exactly
512 samples with nothing marking the gap).
Footer detection now prefers whichever `0e 08` candidate yields a chain
terminating on `0x06`, since the signature can occur inside a sample stream.
Blast radius: 1 file of 1,388.
The superseded model is retained as `decode_waveform_legacy` and pinned by
`micromate/idf_file.py`, whose Thor IDFW body-offset search trial-decodes
candidate offsets and keeps whichever yields the most samples — the new
decoder correctly returns `None` where the old one returned garbage, which
changes that heuristic's winner. Switching Thor over is deferred until that
search is reworked to use the record chain directly.
- **Series-3 histogram block is uniformly big-endian, and the stream's final
block has its own tail — the codec was clipping large peaks and dropping the
last interval of nearly every histogram.**
- **Peaks and half-periods are `uint16` big-endian**, not `uint8` plus an
"annotation" byte: `T_peak` `[5:7]`, `T_halfperiod` `[7:9]`, `V_peak`
`[9:11]`, and so on. Only `block_ctr` `[2:4]` is little-endian. The old
model silently **clipped any peak above 1.275 in/s** — the final interval
of `BE18193/T193LQ9K.OE0H` reads 8.270 in/s in Blastware's own export and
decoded as 0.590. The "annotation" byte was the half-period's high byte,
which is why it was non-zero exactly on the sub-Hz intervals BW renders
as `<1.0`.
- **The marker is `block[4]` alone.** Testing `[4:6]` as a `uint16 LE`
marker forced `block[5] == 0` — which is what capped the peak at one byte
in the first place.
- **The last block of each stream carries tail `9c 06 00 42`** instead of
`1e 0a 00 00`, with arbitrary bytes at `[21:23]`. Rejecting it dropped
the final interval of nearly every histogram, and that interval is
frequently the one holding the event peak — so the file's reported PPV
came out low.
Verified against **1211 production histograms** paired with their preserved
Blastware ASCII exports, which carry a full per-interval data table:
**1211/1211 now decode exactly** (interval count plus every per-interval
peak), and 842,442 per-interval frequency comparisons match with zero
mismatches. Before this fix: **1 of 1196**.
`decode_histogram_body_full` records now expose `is_terminal` in place of
the removed `annotations` tuple.
- **Geophone full scale is 32000 ADC counts, not 32768 — every geo reading was
2.3% low.** The verified body codec emits geo samples in 16-count units whose
documented LSB is exactly 0.005 in/s, and `decoded_to_adc_counts` multiplies
by 16, so one ADC count is `0.005/16` in/s and Normal range (10.000 in/s) is
`10.0 / (0.005/16)` = **32000** counts. Both `sfm/event_hdf5.py` and
`minimateplus/event_file_io.py` divided by 32768, scaling every geophone
sample and every derived peak down by `1 - 32000/32768` = **2.34%**.
Measured against 216 per-channel comparisons with preserved Blastware ASCII
exports: **32768 → 151/216 exact** (worst error 0.238 in/s on a 10 in/s
event); **32000 → 216/216 exact**, worst error 0.005 in/s (exactly 1 LSB —
pure quantization). The error scales with amplitude, so it was invisible on
quiet events and worst on the loud ones that matter for compliance.
The mic path is unaffected — it back-solves its own per-count factor from the
device-reported peak.
**Scope:** the scale lives in `_samples_to_float`, which every event passes
through regardless of which codec produced the samples — so this affected
**waveforms, histograms and series-4 (Thor IDF) alike**, not just waveforms.
Verified after regeneration: series-3 histogram peaks vs their ASCII reports
now sit at a median ratio of 1.0000 across 1,137 comparisons (0.9766 under
32768); series-4 peaks vs device peaks moved from a median 0.960 to 0.983
across 1,468 comparisons. The four block-framing fixes below are
waveform-only — histograms decode via `histogram_codec.decode_histogram_body`,
which is untouched.
- **Series-3 waveform codec: four block-framing cases caused silent channel
truncation.** `walk_body` hit its unknown-tag `break` mid-stream and every
channel decoded after that point came out short — typically Vert/Long/MicL,
sometimes at a third of their true length, with no error raised.
- **Wide-NN RLE `0X NN`** — the 12-bit NN encoding already handled for
`1X NN` / `2X NN` also applies to the `00 NN` RLE tag. Runs longer than
252 samples must use the wide form (e.g. `01 0c` = 268 repeats).
- **`30 NN` with NN > 0x10** — the `0 < NN <= 0x10` guard was arbitrary;
data-section `30 NN` blocks reach at least NN = 0x18. The length formula
(`NN × 1.5 + 2`) was already correct.
- **Variable-width `40 NN` segment headers** — NN is the *count of
previous-channel continuation deltas*, so the header is `2 × NN + 16`
bytes and every field after the deltas shifts by `2 × NN`. Only `40 02`
(20 bytes) was handled; `40 01` (18) and `40 03` (22) both occur.
- **Tagless segment headers** — a segment header can appear with no
`40 NN` tag at all: just the 14-byte tail
`[field2:2][len:2][channel_id:4][marker:2][anchors:4]`. This is the NN=0
case (no continuation deltas needed, so no tag and no delta bytes). It is
where the walk stopped in 7 of the 8 events still truncating after the
first three fixes.
### Changed
- **Segment channel now comes from the header's own channel-id byte** rather
than from rotation position. The field previously documented as a
"monotonic uint32 LE counter" is really `[channel][00][00][segment_index]`
with `0x46`=Tran `0x47`=Vert `0x48`=Long `0x49`=MicL — verified on
**1697 of 1697** segment headers across the ground-truth corpus with zero
disagreements. Rotation-by-position is kept only as a fallback for unknown
ids; it was fragile because a single missed or extra header (exactly what
tagless headers caused) desynced every channel after it.
- **`parse_segment_header` return shape** — now `n_prev_deltas`,
`prev_deltas`, `marker`, `anchors`, `channel`, `segment_index` in place of
the fixed-offset `anchor_bytes` / `fixed_pattern` / `tail` keys. The old
`fixed_pattern` (`02 00 00 01`) conflated the 2-byte constant marker with
the first anchor. `counter` is retained as the raw uint32 of the id field.
### Verification
Against the 75 ground-truth events (BW binary paired with its preserved
`_ASCII.TXT` export), decoding end-to-end through the production path:
| | before | after |
|---|---|---|
| exact (full length, within 1 LSB) | 37 | **72** |
| truncated | 23 | **3** |
| full length, value error > 2 LSB | 15 | **0** |
Worst remaining error among the 72: 0.0050 in/s = exactly 1 LSB.
No regressions — the byte-exact fixture suite still passes, and the full-suite
failure list is unchanged from baseline (16 pre-existing failures from
gitignored fixtures).
### Notes
- **The "DC offset" symptom is _not_ a decode bug.** Events whose geo trace
sits at a constant level instead of oscillating around zero
(dominant-axis `|mean| / peak` >> 0) reproduce *exactly* in Blastware's own
ASCII export — e.g. `BE12599/N599LQD7.8E0W` Tran reads mean +0.345,
min +0.335, max +0.355 in both. It is a known recurring hardware fault (the
operators call it an "offset"): the affected channel's baseline exceeds the
unit's own geo trigger level, so the unit retriggers continuously and floods
the ACH queue with garbage events. Store-wide it affects 2 units of 21 across
6 episodes; see `scratch/offset_candidates.csv` and the project memory notes.
- ~~**Still open:** 3 of 75 ground-truth events truncate at a segment-header
variant with a variable-width prefix.~~ **Resolved later the same day** — the
record-chain rewrite (above) showed there is no variable prefix; it was
walker drift. All 75 are now sample-count exact.
- **Still open after this release:**
- **Series-4 (Thor / Micromate) is not verified** — UM-series sits at ~48%
against device peaks with a ~1.7% systematic bias and a near-zero tail.
Thor IDFW is pinned to `decode_waveform_legacy` deliberately.
- **14 sensitive-range files** show a decoded/truth ratio of exactly 8.0
(= 10.0/1.25) — a units bug, not a codec one. Never chased.
- **`backfill_sidecars.py --force` also inserts DB rows** for store files
that have none (1,286 on the snapshot; one-time per store), and the
dry-run does not report that count before you commit to it.
- **Verification is uneven:** per-sample proof on the 11% of files with a
preserved `_ASCII.TXT`, peak-and-structure consistency on the other 89%.
---
## v0.25.0 — 2026-08-25
**reviewed_real 3-state review flag + twin review-propagation.** The
`events` table and the `/db/events` feed now carry `reviewed_real`, a
3-state review flag mutually exclusive with `false_trigger`, mirrored from
the sidecar review block — plus histogram/waveform twin review-propagation
(flagging one flags both, matched by serial + identical PVS + timestamp
window).
### Added
- **`events` column** `reviewed_real` — `INTEGER NOT NULL DEFAULT 0`, added
via the existing incremental `_migrate` ADD COLUMN pass (auto-migrates on
`SeismoDb()` construction, no manual migration). `query_events` /
`get_event` (and thus `/db/events`) return it automatically (`SELECT *`).
- **Mutual exclusivity with `false_trigger`** — setting `reviewed_real=1`
clears `false_trigger`, and vice versa, enforced on both review paths:
the sidecar review PATCH (`update_event_review`) and the quick
`PATCH /db/events/{id}/false_trigger` endpoint (`set_false_trigger`).
- **`find_twins`** — matches an event's histogram/waveform twins by serial +
identical peak-vector-sum + a timestamp window.
- **`propagate_review_to_twins`** — copies an event's `false_trigger`/
`reviewed_real` state onto its twins, wired into both the
`PATCH /db/events/{id}/sidecar` review path and the quick
`PATCH /db/events/{id}/false_trigger` path, so flagging one flags both
regardless of which endpoint made the change.
---
## v0.24.0 — 2026-08-22
**Waveform-shape metrics on events.** The `events` table and the `/db/events`
feed now carry per-event crest factor and points-near-peak, computed from the
decoded waveform samples at ingest — groundwork for Terra-View's
false-trigger detection (Phase B).
### Added
- **`events` columns** `shape_crest_factor`, `shape_near_peak_count`,
`shape_sample_count`, `shape_axis`. Added via the existing incremental
`_migrate` ADD COLUMN pass — **auto-migrates on `SeismoDb()` construction,
no manual migration**. `query_events` / `get_event` (and thus `/db/events`)
return them automatically (`SELECT *`).
- **Populated at ingest** — crest factor + near-peak-count are computed from
the decoded samples in every save path (`shape_from_h5`/
`shape_from_samples`), and `insert_events` persists them on INSERT and
UPSERT.
- **Backfill** `scripts/backfill_event_shape.py` — fills the columns for
existing events from their on-disk `.h5` waveform samples (idempotent,
UPDATE-only).
### Upgrade Notes
Run the backfill once after deploying, against the events DB + waveform store:
`python3 scripts/backfill_event_shape.py --db-path <seismo_relay.db> --store-root <waveforms/>`
Events with no decodable samples (or no waveform file) stay NULL and render
"—" downstream.
---
7-task TDD plan: shape DSP module (crest factor + points-near-peak), shape_*
columns + auto-migrate, insert_events persistence, ingest population in the
save paths, backfill script, /db/events exposure + v0.24.0 bump. Phase B
(terra-view scoring/UI/review) gets its own plan once this feed is live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDXjZCr4RqT2U3QvMDhgzf
set_false_trigger now clears reviewed_real when flagging false_trigger=True
(mirrors update_event_review's exclusivity), and the quick
PATCH /db/events/{id}/false_trigger endpoint now calls
propagate_review_to_twins after the flag write, matching the sidecar PATCH
path's try/except-with-log.warning pattern. Previously the quick path could
leave both flags set and never touched twins.
Also corrects the v0.25.0 CHANGELOG bullet (exclusivity was not actually
enforced on the quick path until this commit) and adds a caveat comment on
find_twins about rare clamped/saturated-PVS false-positive twin matches.
Two independent bugs, both found by diffing 75 production events against
their preserved Blastware ASCII exports (<store>/<serial>/<file>_ASCII.TXT).
1. Geo full scale was wrong — every geophone reading was 2.34% low.
The codec emits geo samples in 16-count units with a documented LSB of
exactly 0.005 in/s, and decoded_to_adc_counts multiplies by 16, so one
ADC count is 0.005/16 in/s and 10.000 in/s is 10.0/(0.005/16) = 32000
counts. sfm/event_hdf5.py and minimateplus/event_file_io.py both
divided by 32768 (2^15), scaling every sample and derived peak down by
1 - 32000/32768. The error scales with amplitude, so it was invisible
on quiet events and worst on the loud ones that matter for compliance.
Mic is unaffected (it back-solves its scale from the device peak).
216 per-channel comparisons: 32768 -> 151/216 exact, worst error 0.238
in/s on a 10 in/s event; 32000 -> 216/216 exact, worst 0.005 = 1 LSB.
2. walk_body silently truncated channels on four unhandled framing cases.
An unrecognised tag ends the walk and decode_waveform_v2 returns
whatever it got, so this surfaced as short channels, never an error:
- wide-NN RLE `0X NN` (runs longer than 252 samples)
- `30 NN` with NN > 0x10 (the old cap was arbitrary)
- variable-width `40 NN` headers: NN counts previous-channel
continuation deltas, so the header is 2*NN + 16 bytes; `40 01`
and `40 03` occur alongside `40 02`
- tagless segment headers: no `40 NN` tag at all, just the 14-byte
tail [field2:2][len:2][channel_id:4][marker:2][anchors:4]
Also: the header field documented as a "monotonic uint32 LE counter" is
really [channel_id][00][00][segment_index], with 0x46=Tran 0x47=Vert
0x48=Long 0x49=MicL — verified on 1697/1697 segment headers, zero
disagreements. decode_waveform_v2 now takes the channel from that field
instead of rotation position, which was fragile: one missed header
desynced every channel after it.
parse_segment_header now returns n_prev_deltas/prev_deltas/marker/
anchors/channel/segment_index; the old fixed_pattern (02 00 00 01)
conflated the 2-byte marker with the first anchor.
Ground-truth corpus, end to end through the production path:
exact 37 -> 72, truncated 23 -> 3, full-length value errors 15 -> 0.
Store-wide, 729 of 1388 series-3 waveform events decode differently and
728 gain samples; the scale fix changes float values on all of them, so
stored .h5 files need regenerating.
Still open: 3 events truncate at a header variant with a variable-width
prefix (2/4/6 bytes) before the channel id and an `01 00` marker.
Documented in docs/instantel_protocol_reference.md with byte offsets.
+20 tests. No regressions: the byte-exact fixture suite still passes and
the full-suite failure list is unchanged from baseline (16 pre-existing
failures from gitignored fixtures).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgTe8CamXAHcAmaQ6QNcog
The scale lives in _samples_to_float, which every event passes through
regardless of source codec, so waveforms, histograms and Thor IDF events
were all 2.34% low — not just waveforms. Verified after regeneration:
series-3 histogram peaks vs ASCII reports now median 1.0000 across 1137
comparisons (0.9766 under 32768); series-4 peaks vs device peaks moved
from median 0.960 to 0.983 across 1468.
The four block-framing fixes remain waveform-only; histogram_codec is
untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgTe8CamXAHcAmaQ6QNcog
274 series-3 waveform events across 6 episodes on 2 units (BE9558,
BE18438) whose dominant geo axis sits pinned at a DC offset above that
unit's own geo trigger level — the "offset" hardware fault that makes a
unit retrigger continuously and flood the ACH queue.
Detection rule: dominant-axis |mean|/peak > 0.7 AND |mean| >= 0.9 x the
unit's geo trigger level. Bare |mean|/peak is useless on quiet events —
a trace at the 0.010 in/s noise floor clears any ratio threshold.
Not a decode artifact: these reproduce exactly in Blastware's own ASCII
export. Kept as the starting point for the archive-wide analysis.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgTe8CamXAHcAmaQ6QNcog
Brings the protocol reference, CLAUDE.md and the codec RE status doc up
to date with everything confirmed in this pass.
instantel_protocol_reference.md
- Changelog row for the five findings.
- S7.6.1: scope table showing the 32000 scale correction applies to
series-3 waveform, series-3 histogram and series-4 Thor alike, with
the measured before/after ratios for each.
- S15: closed "Full channel ID mapping in SUB 5A stream" — resolved by
the segment-header channel id ([channel][00][00][segment], 0x46=Tran
0x47=Vert 0x48=Long 0x49=MicL, 1697/1697 verified). Four new open
questions: variable-prefix segment descriptors, the histogram codec
missing peak intervals (26% of channels), UM-series IDF decoding
~1000x low, and the Thor per-count LSB residual.
- NEW Appendix E — Known Device Faults. Documents the field-observed
"offset" fault: symptom, why it floods the ACH queue (pedestal
exceeds the unit's own geo trigger level), the episode table, the
detection rule that works, what the data rules out (not the
geophone, not the battery, not environmental, not condensation),
and the two remaining candidate mechanisms with the test that
separates them. Explicitly flags that it is NOT a decode artifact,
since that mistake has already been made once.
CLAUDE.md
- Body-codec section: the four framing cases and the channel-id
finding, with the corpus result.
- "What's NOT solved": replaced the stale walker-edge-cases bullet
with the four genuinely open items.
waveform_codec_re_status.md
- Scale scope table matching the protocol reference.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgTe8CamXAHcAmaQ6QNcog
Operator report: attaching a different geophone to an affected unit
makes the offset go away. That rules out the unit's analog front-end
and any stored per-channel zero constant (a constant lives in the unit
and would survive a sensor swap).
The stored data agrees — MicL, a separate transducer on its own cable,
shows no offset during either episode (|mean|/peak 0.17 and 0.02) while
the geo channels on the same unit at the same moment are pinned.
Two distinct sensor-side patterns recorded:
BE18438 Vert 0.97, Tran 0.16, Long 0.18 -> one conductor pair
BE9558 Long 0.99, Tran 0.90, Vert 0.81 -> shared return / ground
Candidate mechanisms narrowed to three, since a geophone coil is passive
and cannot generate sustained DC: galvanic corrosion at a connector or
splice (matches the ~46 mV referred to the ADC input), a leakage path to
shield, or changed coil DC resistance interacting with the amplifier's
input bias current.
Also records the confound: swapping a sensor requires a monitoring
restart, and these units run Sensor Check "Before monitoring", so the
restart re-zeros too. The swap does not cleanly separate "new sensor"
from "the restart re-zeroed it". Controls and the single best
measurement (open-circuit DC across the suspect connector) documented.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgTe8CamXAHcAmaQ6QNcog
Re-measured properly. The first pass compared h5 max against the ASCII
header PPV and reported "26% of channels miss the peak". The histogram
ASCII actually carries a full per-interval data table (Tran/Vert/Long
peak + freq + PVS per interval), which is real ground truth, so the
comparison should have been per-interval from the start.
Per-interval result, n=1196 series-3 histograms:
- decoded VALUES are right: 1031/1196 (86%) match within 1 LSB across
the overlapping prefix
- the interval COUNT is short in 1195 of 1196 files: median 1 missing,
1088 short by 1-2, 65 by 3-10, 39 by 11-100, 3 by >100 (max 205)
- decoded max falls below the device PPV in 169/1196 files (14%), not
26% — that happens when a dropped interval held the peak
So it is a termination bug in histogram_codec.decode_histogram_body,
the same family as the waveform-walker truncation fixed earlier today,
rather than mis-decoded interval values. Series-3 only; there is no
preserved series-4 ASCII in the snapshot to compare against.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgTe8CamXAHcAmaQ6QNcog
Two errors in the series-3 histogram block model, both found by diffing
against the per-interval data table in the preserved Blastware ASCII
exports (1211 files in the prod snapshot — far stronger ground truth
than the header PPV used previously).
1. The block is uniformly BIG-ENDIAN. Peaks and half-periods are uint16
BE (T_peak [5:7], T_halfperiod [7:9], V_peak [9:11], V_halfperiod
[11:13], L_peak [13:15], L_halfperiod [15:17], M_peak [17:19],
M_halfperiod [19:21]); only block_ctr [2:4] is little-endian.
The old uint8-peak model silently CLIPPED any peak above 1.275 in/s:
the final interval of BE18193/T193LQ9K.OE0H reads 8.270 in/s in BW's
export (1654 counts = 0x0676) and decoded as 0x76 = 118 = 0.590.
The byte documented as a per-channel "annotation" was never an
annotation — it is the half-period's high byte, which is exactly why
it was non-zero on the sub-Hz intervals BW renders as "<1.0".
The marker is block[4] alone. Testing [4:6] as a uint16 LE marker
forced block[5] == 0, which is what capped the peak at one byte.
2. The final block of each stream carries tail 9c 06 00 42 instead of
1e 0a 00 00, and holds arbitrary bytes at [21:23]. Rejecting it
dropped the last interval of nearly every histogram — frequently the
interval holding the event peak, so the file's PPV read low.
Verified end to end through the production path: 1211/1211 histograms
decode exactly (interval count + every per-interval peak), plus 842,442
per-interval frequency comparisons with zero mismatches. Previously
1 of 1196 files was fully correct.
decode_histogram_body_full records expose `is_terminal` in place of the
removed `annotations` tuple. +6 tests. No regressions: full-suite
failure list unchanged from baseline.
NOTE: stored histogram .h5 files need regenerating to pick this up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgTe8CamXAHcAmaQ6QNcog
backfill_event_shape.py skipped rows whose .h5 produced no shape and left
the previously stored value in place. A stale shape outlives the decode
it came from and silently feeds the false-trigger detector.
Found while re-running the backfill after the histogram codec fix: 493
rows in the prod snapshot were carrying shape metrics that no longer
matched their .h5 — e.g. BE17353/S353LDOK.XZ0H held crest_factor from a
223-sample decode while its .h5 holds a single interval. These predate
today's work (present in the pre-32000 snapshot), so this is pre-existing
behaviour rather than fallout from the codec fixes.
Now NULLs shape_crest_factor / shape_near_peak_count / shape_sample_count
/ shape_axis in that case and reports a `cleared_stale` count. Verified
on the snapshot: 493 cleared, 0 stale rows remaining, 11570 rows matching
their .h5 exactly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgTe8CamXAHcAmaQ6QNcog
Supersedes the segment-header model entirely, including the fixes made
earlier today. Found via multi-agent structural analysis of the 25 files
that stalled the walker, then verified independently.
Records are self-delimiting: off+2 is a uint16 BE length, next_record =
off + 2 + len, and the chain ends on a record whose chan_id is 0x06.
off+8 carries a 3-valued mode enum:
02 00 14-byte header, 2 anchors, then CUMULATIVE delta blocks
01 00 10-byte header, no anchors, blocks are ABSOLUTE values
00 03 10-byte header, NO TAGS AT ALL - raw 12-bit packed absolute
`40 NN` is an ordinary int16 BE data block (2*NN + 2), never a header.
Reading it as a 2*NN + 16 header is what made walks drift — the
"variable-prefix segment descriptors" reported earlier today were not a
format feature, just walker drift of exactly
4 - (old_stop - true_record_start), on all 25 affected files.
Measured on the production snapshot:
all four channels equal length 156/1388 -> 1388/1388
ASCII sample-count exact 72/75 -> 75/75
ASCII fully exact 70/75 -> 73/75
device PPV waveform (live) 1288/1306 -> 1306/1306 (mean err 0.00000)
device PPV histogram (live) 4434/4459 -> 4458/4459
Also eliminates the walker-over-read class: 24 of those 35 files were
histograms that read_blastware_file fed to the waveform codec first; the
old walker accepted them and returned garbage (one yielded 98,923
"intervals"), while the record-chain decoder returns None so they fall
through to histogram_codec.
00 03 records are DECODED, not skipped. Skipping them silently shifts
the time base of everything after them on that channel — BE9558/
K558LOF2.820W had MicL displaced by exactly 512 samples with nothing
marking the gap.
Footer detection now prefers the 0e 08 candidate whose body yields a
chain terminating on 0x06; the signature can occur inside a sample
stream. Blast radius 1 file of 1388.
The superseded model survives as decode_waveform_legacy, pinned by
micromate/idf_file.py: its Thor IDFW body-offset search trial-decodes
candidates and keeps whichever yields the most samples, so the new
decoder returning None where the old returned garbage changes that
heuristic's winner. Deferred until that search uses the record chain.
Tests: 253 passed (+11), failure list unchanged from baseline. The 9
tests pinning the superseded model are retargeted at
decode_waveform_legacy, which still implements it.
NOTE: stored .h5 files need regenerating — nearly all get longer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgTe8CamXAHcAmaQ6QNcog
backfill_sidecars.py skipped the .h5 write when a file produced no
samples, with the stated intent of not replacing it with an empty
placeholder. That silently preserved output from a superseded decoder.
After the record-chain fix, 415 histogram files stopped decoding (216 on
BE18193, 199 on BE9440) but kept .h5 files whose peaks ran up to 400x
the device-reported PPV. Those were feeding charts and the
false-trigger detector with nothing marking them. The .h5 is now
removed in that case and the run reports stale_h5_removed.
Store-wide effect, series-3, decoded peak vs device-reported PPV:
waveform 1307/1307 (100%), mean abs ratio error 0.00000
histogram 4434/4435 (100%)
Both were 99% with a tail of 18 and 25 wrong files respectively.
The 415 files are a genuine unmapped format variant, not a regression:
their bodies open `00 00 00 01 0a 00` (valid block header, marker 0a at
[4], block_ctr 256) but block[28:32] matches neither known tail, and no
stride from 8 to 64 bytes places a marker at [4] consistently. Bodies
are very large (one is 360,573 bytes). They were previously being
decoded by the WAVEFORM codec, which accepted them and returned garbage
- so the gap pre-dates today's work; the fix only exposed it. Logged as
an open question in the protocol reference.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgTe8CamXAHcAmaQ6QNcog
Sub-minute histogram intervals are packed several to a block so that
every block still covers exactly one minute of data:
interval intervals/block stride
1 minute 1 32 <- the standard big-endian block
15 s 4 92
2 s 30 612
stride = 12 + n * 20
Block = [00][segment][ctr uint16 LE][0a][00], then n x 20-byte records of
8 x uint16 LITTLE-endian values (T_peak, T_halfp, V_peak, V_halfp,
L_peak, L_halfp, M_peak, M_halfp) plus a 2-word tail whose first word is
0000 on every real interval, then a 6-byte block trailer.
The standard 32-byte block is BIG-endian; this variant is LITTLE-endian.
The tail-word check matters: a session ending mid-block leaves buffer
garbage in the remaining interval slots, which decoded as peaks
thousands of times the real value. Stride detection also requires at
least 2 records, since a 1-record block would have stride 32 and
collide with the standard block.
Recovers 415 files that decoded to nothing: 216 on BE18193 (2 s
intervals) and 199 on BE9440 (15 s). Before decoding to nothing they
were being accepted by the WAVEFORM codec, which returned garbage
peaking up to 400x the device-reported PPV.
Ground truth BE9440/K440L3AQ.T70H (5,710 intervals) matches its
Blastware ASCII export exactly: 17,130/17,130 geo peaks, 22,840/22,840
frequencies, 5,710/5,710 mic dB(L). Across all 455 affected files,
1,354/1,365 channel peaks (99.2%) match the device-reported PPV; the 11
that don't are under-reads on BE9440 where the walk stops early.
Fixture (binary + ASCII) saved under tests/fixtures/, which is
gitignored per repo practice — the ground-truth test skips when absent.
Tests: 258 passed, failure list unchanged from baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgTe8CamXAHcAmaQ6QNcog
Swept every series-3 binary with the live decoder against five
independent checks: decode exceptions, zero samples, unequal geo channel
lengths, peaks above range full scale, decoded peak vs device-reported
PPV, and waveform length vs declared record time.
1. block[22] is NOT a constant and must not be tested. Documented as
always 0x00, it carries data on loud blocks, and rejecting those threw
away the interval holding the event peak. BE18350/T350L7HR.NL0H
block 92 has block[22]=0x26 and a Tran peak of 0x0563 = 1379 counts =
6.895 in/s — exactly the device-reported PPV — while the file decoded
to 0.015 in/s. block[0]==0, block[4]==0x0A and the 4-byte tail are
six bytes of constraint, which is what keeps trailer content out.
2. Block-model dispatch now goes on signature strength rather than on
whichever decoder returns first. A multi-interval body also yields
scattered standard-tail blocks by coincidence, so "first non-empty"
handed 193 BE18193 files to the standard walker and produced peaks of
149 in/s against a 10 in/s full scale.
3. Multi-interval stride detection requires the block counter to
increment by exactly 1. Without it the detector false-positives on
ordinary standard-block bodies: they carry a header every 32 bytes,
and 192 = 12 + 20*9 and 512 = 12 + 20*25 are both multiples of 32, so
a stride "fits" while skipping 6 or 16 real blocks. That misrouted
9,082 files.
Partial-block garbage is trimmed within the final block only, stopping
at the first slot with a non-zero tail word or a geo peak above full
scale (2000 counts in 16-count units). Trimming purely from the end
left garbage stranded behind a slot that happened to have a zero tail
word; trimming on the tail word alone truncated four BE9440 files by up
to 2,800 intervals.
Result: 11,603 / 11,603 series-3 binaries clean on every check.
Ground truth unchanged: 1211/1211 histograms exact per-interval, 75/75
waveform sample counts exact (73/75 fully exact, the 2 differ by 1 LSB
on rail samples), and the multi-interval fixture still matches its BW
ASCII export on all 45,680 values.
Tests: 259 passed, failure list unchanged from baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgTe8CamXAHcAmaQ6QNcog
Two body-model rewrites, a systematic scale error affecting every
geophone reading the system ever produced, a recovered file format, and
two artifact-hygiene bugs where stale files outlived the decodes that
made them.
- geo full scale is 32000 ADC counts, not 32768 (every reading 2.34% low)
- the waveform body is a record chain, not a tag stream
- the histogram block is big-endian, with a terminal tail
- sub-minute intervals pack several per block (415 files recovered)
- three more defects found by a full-corpus sweep, each masking the next
- stale .h5 files and stale shape_* columns are now cleared, not left
All 11,603 series-3 binaries in the production snapshot pass every check.
Ground truth: 1,211/1,211 histograms exact per-interval, 75/75 waveform
sample counts exact, multi-interval fixture exact on all 45,680 values.
Also corrects a changelog note that went stale within the same day: the
"3 of 75 events still truncate" item was resolved by the record-chain
rewrite, and the remaining open items are now listed explicitly.
CLAUDE.md gains a "Where things stand" block at the top — the header had
been reading v0.21.0, four releases behind, which is the first thing you
see when picking the project back up.
Tests: 259 passed; the 16 failures are pre-existing (gitignored
fixtures) and unchanged from baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgTe8CamXAHcAmaQ6QNcog
TOOL_VERSION had been frozen at 0.21.1 for four releases despite its own
comment saying "Bump this constant and CHANGELOG.md together at release
time". It is not cosmetic: backfill_sidecars.py decides whether to
regenerate with
ver_ok = sidecar.source.tool_version >= event_file_io.TOOL_VERSION
so with the constant stuck at 0.21.1 and every sidecar stamped 0.21.1,
a backfill WITHOUT --force skipped the entire store. That is precisely
the failure the check exists to prevent, and it means every sidecar
regenerated during the 0.26.0 decode work is stamped 0.21.1 while having
been produced by 0.26.0 code.
Verified: a non-force dry-run over the snapshot now reports
written=11603 skipped(uptodate)=0, where before it would have skipped
all 11,603. Prod therefore does not need --force to pick up the decode
corrections — the version difference alone is enough.
Note the installed dist metadata reads 0.12.0, older than the constant,
so the best-effort "prefer installed metadata when newer" path correctly
defers to TOOL_VERSION.
Also bumps the README header, which still read v0.22.0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgTe8CamXAHcAmaQ6QNcog
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
v0.26.0 — 2026-08-27
Series-3 decode correctness. Two body-model rewrites, a systematic
scale error affecting every geophone reading ever produced, a recovered
file format, and two artifact-hygiene bugs where stale files outlived the
decodes that made them. All 11,603 series-3 binaries in the production
snapshot now pass every check.
Fixed
Series-3 health sweep: 11,603 / 11,603 binaries now clean on every check.
Swept every series-3 file with the live decoder against five independent
checks — decode exceptions, zero samples, unequal geo channel lengths, peaks
above range full scale, decoded peak vs the device-reported PPV, and waveform
length vs the declared record time. Three real defects surfaced and were
fixed:
block[22]is not a constant and must not be tested. It was documentedas always
0x00but carries data on loud blocks, and rejecting those threwaway the interval holding the event peak.
BE18350/T350L7HR.NL0Hblock 92 hasblock[22]=0x26and a Tran peak of0x0563= 1379 counts = 6.895 in/s — exactly the device-reported PPV —while the file as a whole decoded to 0.015 in/s.
block[0]==0x00,block[4]==0x0Aand the 4-byte tail are six bytes of constraint, which iswhat keeps trailer content out.
Block-model dispatch now goes on signature strength, not on whichever
decoder returns first. A multi-interval body also yields scattered
standard-tail blocks by coincidence; dispatching on "first non-empty"
handed 193 BE18193 files to the standard walker and produced peaks of
149 in/s against a 10 in/s full scale.
Multi-interval stride detection requires the block counter to increment
by exactly 1. Without it the detector false-positives on ordinary
standard-block bodies: those carry a header every 32 bytes, and
192 = 12 + 20×9and512 = 12 + 20×25are both multiples of 32, so astride "fits" while actually skipping 6 or 16 real blocks. That misrouted
9,082 files.
Partial-block garbage is now trimmed within the final block only, stopping at
the first slot with a non-zero tail word or a geo peak above full scale.
Trimming purely from the end left garbage stranded behind one slot that
happened to have a zero tail word; trimming on the tail word alone truncated
four BE9440 files by up to 2,800 intervals.
Sub-minute histogram intervals are packed several to a block — 415 files
recovered. The device always writes one minute of data per block, so a
shorter interval just means more intervals in a longer block:
stride = 12 + n * 20. Each 20-byte record carries 8 × uint16little-endian values — peak and half-period per channel — plus a 2-word
tail whose first word is
0000on every real interval (a session endingmid-block leaves buffer garbage in the remaining slots, which decoded as
peaks thousands of times the real value until that check was added).
The standard 32-byte block is big-endian; this variant is not.
These 415 files (216 on BE18193 at 2 s intervals, 199 on BE9440 at 15 s)
previously decoded to nothing at all — and before that were being accepted
by the waveform codec, which returned garbage peaking up to 400× the
device-reported PPV.
Ground truth
BE9440/K440L3AQ.T70H— 5,710 intervals — matches itsBlastware ASCII export on 17,130/17,130 geo peaks, 22,840/22,840
frequencies and 5,710/5,710 mic dB(L) values. Across all 455 affected
files, 1,354/1,365 (99.2%) channel peaks match the device-reported PPV;
the 11 that don't are under-reads on BE9440 where the walk stops early.
backfill_sidecars.pynow removes a stale.h5when nothing decodes.It previously skipped the write "so we don't replace whatever's there with an
empty placeholder", which silently preserved output from a superseded
decoder. After the record-chain fix, 415 histogram files stopped decoding (an
unmapped block variant on BE18193 and BE9440) but kept
.h5files whose peaksran up to 400× the device's own reported PPV — garbage feeding the charts
and the false-trigger detector with nothing marking it. Reports a
stale_h5_removedcount.The series-3 waveform body is a RECORD CHAIN, not a tag stream — this
supersedes the segment-header model, including the fixes made earlier the
same day.
Records are self-delimiting.
off+2is auint16 BElength andnext_record = off + 2 + len; the chain ends on a record whosechan_idis0x06.off+8carries a 3-valued mode enum:02 0001 0000 0340 NNis an ordinary int16 BE data block (2*NN + 2), never a segmentheader. Reading it as a
2*NN + 16header is what made walks drift — and the"variable-prefix segment descriptors" reported earlier today were not a format
feature at all, just walker drift of exactly
4 - (old_stop - true_record_start)on all 25 affected files.Measured against the production snapshot:
Mean absolute PPV ratio error on waveforms is now 0.00000. The 2 remaining
ASCII imperfections differ by exactly 1 LSB on samples sitting at the
±10.000 in/s rail.
This also eliminated the walker-over-read class. 24 of those 35 files
were histograms that
read_blastware_filefed to the waveform codec first;the old walker accepted them and returned garbage (one yielded 98,923
"intervals"), while the record-chain decoder correctly returns
Noneso theyfall through to
histogram_codec.00 03records are decoded rather than skipped. Skipping them does not merelylose samples — it silently shifts the time base of everything after them on
that channel (observed on
BE9558/K558LOF2.820W, MicL displaced by exactly512 samples with nothing marking the gap).
Footer detection now prefers whichever
0e 08candidate yields a chainterminating on
0x06, since the signature can occur inside a sample stream.Blast radius: 1 file of 1,388.
The superseded model is retained as
decode_waveform_legacyand pinned bymicromate/idf_file.py, whose Thor IDFW body-offset search trial-decodescandidate offsets and keeps whichever yields the most samples — the new
decoder correctly returns
Nonewhere the old one returned garbage, whichchanges that heuristic's winner. Switching Thor over is deferred until that
search is reworked to use the record chain directly.
Series-3 histogram block is uniformly big-endian, and the stream's final
block has its own tail — the codec was clipping large peaks and dropping the
last interval of nearly every histogram.
uint16big-endian, notuint8plus an"annotation" byte:
T_peak[5:7],T_halfperiod[7:9],V_peak[9:11], and so on. Onlyblock_ctr[2:4]is little-endian. The oldmodel silently clipped any peak above 1.275 in/s — the final interval
of
BE18193/T193LQ9K.OE0Hreads 8.270 in/s in Blastware's own export anddecoded as 0.590. The "annotation" byte was the half-period's high byte,
which is why it was non-zero exactly on the sub-Hz intervals BW renders
as
<1.0.block[4]alone. Testing[4:6]as auint16 LEmarker forced
block[5] == 0— which is what capped the peak at one bytein the first place.
9c 06 00 42instead of1e 0a 00 00, with arbitrary bytes at[21:23]. Rejecting it droppedthe final interval of nearly every histogram, and that interval is
frequently the one holding the event peak — so the file's reported PPV
came out low.
Verified against 1211 production histograms paired with their preserved
Blastware ASCII exports, which carry a full per-interval data table:
1211/1211 now decode exactly (interval count plus every per-interval
peak), and 842,442 per-interval frequency comparisons match with zero
mismatches. Before this fix: 1 of 1196.
decode_histogram_body_fullrecords now exposeis_terminalin place ofthe removed
annotationstuple.Geophone full scale is 32000 ADC counts, not 32768 — every geo reading was
2.3% low. The verified body codec emits geo samples in 16-count units whose
documented LSB is exactly 0.005 in/s, and
decoded_to_adc_countsmultipliesby 16, so one ADC count is
0.005/16in/s and Normal range (10.000 in/s) is10.0 / (0.005/16)= 32000 counts. Bothsfm/event_hdf5.pyandminimateplus/event_file_io.pydivided by 32768, scaling every geophonesample and every derived peak down by
1 - 32000/32768= 2.34%.Measured against 216 per-channel comparisons with preserved Blastware ASCII
exports: 32768 → 151/216 exact (worst error 0.238 in/s on a 10 in/s
event); 32000 → 216/216 exact, worst error 0.005 in/s (exactly 1 LSB —
pure quantization). The error scales with amplitude, so it was invisible on
quiet events and worst on the loud ones that matter for compliance.
The mic path is unaffected — it back-solves its own per-count factor from the
device-reported peak.
Scope: the scale lives in
_samples_to_float, which every event passesthrough regardless of which codec produced the samples — so this affected
waveforms, histograms and series-4 (Thor IDF) alike, not just waveforms.
Verified after regeneration: series-3 histogram peaks vs their ASCII reports
now sit at a median ratio of 1.0000 across 1,137 comparisons (0.9766 under
32768); series-4 peaks vs device peaks moved from a median 0.960 to 0.983
across 1,468 comparisons. The four block-framing fixes below are
waveform-only — histograms decode via
histogram_codec.decode_histogram_body,which is untouched.
Series-3 waveform codec: four block-framing cases caused silent channel
truncation.
walk_bodyhit its unknown-tagbreakmid-stream and everychannel decoded after that point came out short — typically Vert/Long/MicL,
sometimes at a third of their true length, with no error raised.
0X NN— the 12-bit NN encoding already handled for1X NN/2X NNalso applies to the00 NNRLE tag. Runs longer than252 samples must use the wide form (e.g.
01 0c= 268 repeats).30 NNwith NN > 0x10 — the0 < NN <= 0x10guard was arbitrary;data-section
30 NNblocks reach at least NN = 0x18. The length formula(
NN × 1.5 + 2) was already correct.40 NNsegment headers — NN is the count ofprevious-channel continuation deltas, so the header is
2 × NN + 16bytes and every field after the deltas shifts by
2 × NN. Only40 02(20 bytes) was handled;
40 01(18) and40 03(22) both occur.40 NNtag at all: just the 14-byte tail[field2:2][len:2][channel_id:4][marker:2][anchors:4]. This is the NN=0case (no continuation deltas needed, so no tag and no delta bytes). It is
where the walk stopped in 7 of the 8 events still truncating after the
first three fixes.
Changed
Segment channel now comes from the header's own channel-id byte rather
than from rotation position. The field previously documented as a
"monotonic uint32 LE counter" is really
[channel][00][00][segment_index]with
0x46=Tran0x47=Vert0x48=Long0x49=MicL — verified on1697 of 1697 segment headers across the ground-truth corpus with zero
disagreements. Rotation-by-position is kept only as a fallback for unknown
ids; it was fragile because a single missed or extra header (exactly what
tagless headers caused) desynced every channel after it.
parse_segment_headerreturn shape — nown_prev_deltas,prev_deltas,marker,anchors,channel,segment_indexin place ofthe fixed-offset
anchor_bytes/fixed_pattern/tailkeys. The oldfixed_pattern(02 00 00 01) conflated the 2-byte constant marker withthe first anchor.
counteris retained as the raw uint32 of the id field.Verification
Against the 75 ground-truth events (BW binary paired with its preserved
_ASCII.TXTexport), decoding end-to-end through the production path:Worst remaining error among the 72: 0.0050 in/s = exactly 1 LSB.
No regressions — the byte-exact fixture suite still passes, and the full-suite
failure list is unchanged from baseline (16 pre-existing failures from
gitignored fixtures).
Notes
The "DC offset" symptom is not a decode bug. Events whose geo trace
sits at a constant level instead of oscillating around zero
(dominant-axis
|mean| / peak>> 0) reproduce exactly in Blastware's ownASCII export — e.g.
BE12599/N599LQD7.8E0WTran reads mean +0.345,min +0.335, max +0.355 in both. It is a known recurring hardware fault (the
operators call it an "offset"): the affected channel's baseline exceeds the
unit's own geo trigger level, so the unit retriggers continuously and floods
the ACH queue with garbage events. Store-wide it affects 2 units of 21 across
6 episodes; see
scratch/offset_candidates.csvand the project memory notes.Still open: 3 of 75 ground-truth events truncate at a segment-headerResolved later the same day — thevariant with a variable-width prefix.
record-chain rewrite (above) showed there is no variable prefix; it was
walker drift. All 75 are now sample-count exact.
Still open after this release:
against device peaks with a ~1.7% systematic bias and a near-zero tail.
Thor IDFW is pinned to
decode_waveform_legacydeliberately.(= 10.0/1.25) — a units bug, not a codec one. Never chased.
backfill_sidecars.py --forcealso inserts DB rows for store filesthat have none (1,286 on the snapshot; one-time per store), and the
dry-run does not report that count before you commit to it.
preserved
_ASCII.TXT, peak-and-structure consistency on the other 89%.v0.25.0 — 2026-08-25
reviewed_real 3-state review flag + twin review-propagation. The
eventstable and the/db/eventsfeed now carryreviewed_real, a3-state review flag mutually exclusive with
false_trigger, mirrored fromthe sidecar review block — plus histogram/waveform twin review-propagation
(flagging one flags both, matched by serial + identical PVS + timestamp
window).
Added
eventscolumnreviewed_real—INTEGER NOT NULL DEFAULT 0, addedvia the existing incremental
_migrateADD COLUMN pass (auto-migrates onSeismoDb()construction, no manual migration).query_events/get_event(and thus/db/events) return it automatically (SELECT *).false_trigger— settingreviewed_real=1clears
false_trigger, and vice versa, enforced on both review paths:the sidecar review PATCH (
update_event_review) and the quickPATCH /db/events/{id}/false_triggerendpoint (set_false_trigger).find_twins— matches an event's histogram/waveform twins by serial +identical peak-vector-sum + a timestamp window.
propagate_review_to_twins— copies an event'sfalse_trigger/reviewed_realstate onto its twins, wired into both thePATCH /db/events/{id}/sidecarreview path and the quickPATCH /db/events/{id}/false_triggerpath, so flagging one flags bothregardless of which endpoint made the change.
v0.24.0 — 2026-08-22
Waveform-shape metrics on events. The
eventstable and the/db/eventsfeed now carry per-event crest factor and points-near-peak, computed from the
decoded waveform samples at ingest — groundwork for Terra-View's
false-trigger detection (Phase B).
Added
eventscolumnsshape_crest_factor,shape_near_peak_count,shape_sample_count,shape_axis. Added via the existing incremental_migrateADD COLUMN pass — auto-migrates onSeismoDb()construction,no manual migration.
query_events/get_event(and thus/db/events)return them automatically (
SELECT *).the decoded samples in every save path (
shape_from_h5/shape_from_samples), andinsert_eventspersists them on INSERT andUPSERT.
scripts/backfill_event_shape.py— fills the columns forexisting events from their on-disk
.h5waveform samples (idempotent,UPDATE-only).
Upgrade Notes
Run the backfill once after deploying, against the events DB + waveform store:
python3 scripts/backfill_event_shape.py --db-path <seismo_relay.db> --store-root <waveforms/>Events with no decodable samples (or no waveform file) stay NULL and render
"—" downstream.
set_false_trigger now clears reviewed_real when flagging false_trigger=True (mirrors update_event_review's exclusivity), and the quick PATCH /db/events/{id}/false_trigger endpoint now calls propagate_review_to_twins after the flag write, matching the sidecar PATCH path's try/except-with-log.warning pattern. Previously the quick path could leave both flags set and never touched twins. Also corrects the v0.25.0 CHANGELOG bullet (exclusivity was not actually enforced on the quick path until this commit) and adds a caveat comment on find_twins about rare clamped/saturated-PVS false-positive twin matches.Two independent bugs, both found by diffing 75 production events against their preserved Blastware ASCII exports (<store>/<serial>/<file>_ASCII.TXT). 1. Geo full scale was wrong — every geophone reading was 2.34% low. The codec emits geo samples in 16-count units with a documented LSB of exactly 0.005 in/s, and decoded_to_adc_counts multiplies by 16, so one ADC count is 0.005/16 in/s and 10.000 in/s is 10.0/(0.005/16) = 32000 counts. sfm/event_hdf5.py and minimateplus/event_file_io.py both divided by 32768 (2^15), scaling every sample and derived peak down by 1 - 32000/32768. The error scales with amplitude, so it was invisible on quiet events and worst on the loud ones that matter for compliance. Mic is unaffected (it back-solves its scale from the device peak). 216 per-channel comparisons: 32768 -> 151/216 exact, worst error 0.238 in/s on a 10 in/s event; 32000 -> 216/216 exact, worst 0.005 = 1 LSB. 2. walk_body silently truncated channels on four unhandled framing cases. An unrecognised tag ends the walk and decode_waveform_v2 returns whatever it got, so this surfaced as short channels, never an error: - wide-NN RLE `0X NN` (runs longer than 252 samples) - `30 NN` with NN > 0x10 (the old cap was arbitrary) - variable-width `40 NN` headers: NN counts previous-channel continuation deltas, so the header is 2*NN + 16 bytes; `40 01` and `40 03` occur alongside `40 02` - tagless segment headers: no `40 NN` tag at all, just the 14-byte tail [field2:2][len:2][channel_id:4][marker:2][anchors:4] Also: the header field documented as a "monotonic uint32 LE counter" is really [channel_id][00][00][segment_index], with 0x46=Tran 0x47=Vert 0x48=Long 0x49=MicL — verified on 1697/1697 segment headers, zero disagreements. decode_waveform_v2 now takes the channel from that field instead of rotation position, which was fragile: one missed header desynced every channel after it. parse_segment_header now returns n_prev_deltas/prev_deltas/marker/ anchors/channel/segment_index; the old fixed_pattern (02 00 00 01) conflated the 2-byte marker with the first anchor. Ground-truth corpus, end to end through the production path: exact 37 -> 72, truncated 23 -> 3, full-length value errors 15 -> 0. Store-wide, 729 of 1388 series-3 waveform events decode differently and 728 gain samples; the scale fix changes float values on all of them, so stored .h5 files need regenerating. Still open: 3 events truncate at a header variant with a variable-width prefix (2/4/6 bytes) before the channel id and an `01 00` marker. Documented in docs/instantel_protocol_reference.md with byte offsets. +20 tests. No regressions: the byte-exact fixture suite still passes and the full-suite failure list is unchanged from baseline (16 pre-existing failures from gitignored fixtures). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HgTe8CamXAHcAmaQ6QNcogBrings the protocol reference, CLAUDE.md and the codec RE status doc up to date with everything confirmed in this pass. instantel_protocol_reference.md - Changelog row for the five findings. - S7.6.1: scope table showing the 32000 scale correction applies to series-3 waveform, series-3 histogram and series-4 Thor alike, with the measured before/after ratios for each. - S15: closed "Full channel ID mapping in SUB 5A stream" — resolved by the segment-header channel id ([channel][00][00][segment], 0x46=Tran 0x47=Vert 0x48=Long 0x49=MicL, 1697/1697 verified). Four new open questions: variable-prefix segment descriptors, the histogram codec missing peak intervals (26% of channels), UM-series IDF decoding ~1000x low, and the Thor per-count LSB residual. - NEW Appendix E — Known Device Faults. Documents the field-observed "offset" fault: symptom, why it floods the ACH queue (pedestal exceeds the unit's own geo trigger level), the episode table, the detection rule that works, what the data rules out (not the geophone, not the battery, not environmental, not condensation), and the two remaining candidate mechanisms with the test that separates them. Explicitly flags that it is NOT a decode artifact, since that mistake has already been made once. CLAUDE.md - Body-codec section: the four framing cases and the channel-id finding, with the corpus result. - "What's NOT solved": replaced the stale walker-edge-cases bullet with the four genuinely open items. waveform_codec_re_status.md - Scale scope table matching the protocol reference. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HgTe8CamXAHcAmaQ6QNcogRe-measured properly. The first pass compared h5 max against the ASCII header PPV and reported "26% of channels miss the peak". The histogram ASCII actually carries a full per-interval data table (Tran/Vert/Long peak + freq + PVS per interval), which is real ground truth, so the comparison should have been per-interval from the start. Per-interval result, n=1196 series-3 histograms: - decoded VALUES are right: 1031/1196 (86%) match within 1 LSB across the overlapping prefix - the interval COUNT is short in 1195 of 1196 files: median 1 missing, 1088 short by 1-2, 65 by 3-10, 39 by 11-100, 3 by >100 (max 205) - decoded max falls below the device PPV in 169/1196 files (14%), not 26% — that happens when a dropped interval held the peak So it is a termination bug in histogram_codec.decode_histogram_body, the same family as the waveform-walker truncation fixed earlier today, rather than mis-decoded interval values. Series-3 only; there is no preserved series-4 ASCII in the snapshot to compare against. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HgTe8CamXAHcAmaQ6QNcogSub-minute histogram intervals are packed several to a block so that every block still covers exactly one minute of data: interval intervals/block stride 1 minute 1 32 <- the standard big-endian block 15 s 4 92 2 s 30 612 stride = 12 + n * 20 Block = [00][segment][ctr uint16 LE][0a][00], then n x 20-byte records of 8 x uint16 LITTLE-endian values (T_peak, T_halfp, V_peak, V_halfp, L_peak, L_halfp, M_peak, M_halfp) plus a 2-word tail whose first word is 0000 on every real interval, then a 6-byte block trailer. The standard 32-byte block is BIG-endian; this variant is LITTLE-endian. The tail-word check matters: a session ending mid-block leaves buffer garbage in the remaining interval slots, which decoded as peaks thousands of times the real value. Stride detection also requires at least 2 records, since a 1-record block would have stride 32 and collide with the standard block. Recovers 415 files that decoded to nothing: 216 on BE18193 (2 s intervals) and 199 on BE9440 (15 s). Before decoding to nothing they were being accepted by the WAVEFORM codec, which returned garbage peaking up to 400x the device-reported PPV. Ground truth BE9440/K440L3AQ.T70H (5,710 intervals) matches its Blastware ASCII export exactly: 17,130/17,130 geo peaks, 22,840/22,840 frequencies, 5,710/5,710 mic dB(L). Across all 455 affected files, 1,354/1,365 channel peaks (99.2%) match the device-reported PPV; the 11 that don't are under-reads on BE9440 where the walk stops early. Fixture (binary + ASCII) saved under tests/fixtures/, which is gitignored per repo practice — the ground-truth test skips when absent. Tests: 258 passed, failure list unchanged from baseline. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HgTe8CamXAHcAmaQ6QNcogTOOL_VERSION had been frozen at 0.21.1 for four releases despite its own comment saying "Bump this constant and CHANGELOG.md together at release time". It is not cosmetic: backfill_sidecars.py decides whether to regenerate with ver_ok = sidecar.source.tool_version >= event_file_io.TOOL_VERSION so with the constant stuck at 0.21.1 and every sidecar stamped 0.21.1, a backfill WITHOUT --force skipped the entire store. That is precisely the failure the check exists to prevent, and it means every sidecar regenerated during the 0.26.0 decode work is stamped 0.21.1 while having been produced by 0.26.0 code. Verified: a non-force dry-run over the snapshot now reports written=11603 skipped(uptodate)=0, where before it would have skipped all 11,603. Prod therefore does not need --force to pick up the decode corrections — the version difference alone is enough. Note the installed dist metadata reads 0.12.0, older than the constant, so the best-effort "prefer installed metadata when newer" path correctly defers to TOOL_VERSION. Also bumps the README header, which still read v0.22.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HgTe8CamXAHcAmaQ6QNcog