Skip to content

feat(eeg): add content-addressed session event references - #44

Draft
aurascoper wants to merge 3 commits into
mainfrom
feat/session-event-references
Draft

feat(eeg): add content-addressed session event references#44
aurascoper wants to merge 3 commits into
mainfrom
feat/session-event-references

Conversation

@aurascoper

@aurascoper aurascoper commented Jul 28, 2026

Copy link
Copy Markdown
Owner
head:  a514dc5                     # merge-forward onto post-D0-R main
base:  main@859feaa                # the #43 merge commit
files: 5   (4 added, 1 amended)

Updated onto post-D0-R main

#43 is merged (859feaa), so the prerequisite is satisfied. This branch was
merged forward rather than rebased, so the reviewed commits stay as reviewed.

The effective diff is 5 filesREADME.md never appeared here (this branch
amends Recordings/README.md), and D0-R's 11 files are now upstream rather than
in this diff.

The new ADR gate on this tree

D0-R's reference-integrity checker now runs against this branch. It passes, and
this branch adds no ADR reference of its own:

python3 Tests/eval/test_adr_references.py    Ran 16 tests — OK
python3 Scripts/check_adr_references.py --mode all
                                            PASS (1 warning)

That one warning is the historical ADR-009+ recommendation in
docs/reviews/, surfaced by advisory mode and deliberately preserved — not
introduced by this PR
, and not an error. Hard mode is clean.

Re-verified post-merge

python3 Tests/eval/test_session_events.py    30/30 pass
python3 Tests/eval/test_adr_references.py    16/16 pass
python3 Scripts/check_adr_references.py      PASS
swift build                                  Build complete
git diff --check                             clean

What this is

A content-addressed reference/index artifact — not a copied-signal dataset and not a label set. Each observation binds to recording_sha256 + sample range + params_sha256, so later work has something reproducible to disagree about.

It carries no signal. Hold the recording and the window is re-derivable and observed recomputes to equality; without it the log says nothing. That indirection is why one artifact serves a spectral routine today and an encoder later — storing a latent would bind the log to whichever model produced it and expire with it.

What it does not claim

Detection is gated behind SLEEP_CYCLE_DESIGN.md §21 ("5+ clean sessions through the toolkit" — currently 0). Efficacy is gated behind the D8 pre-registration (decision_registry.md entry 8, "non-negotiable"). This is upstream of both and shortens neither — but every session run for the §21 gate now yields a structured event log for free.

The validator enforces that rather than the docs asking:

contains_signal              false
science_status               pipeline_only
label_status                 heuristic_observation
live_control                 false
promotion_status             not_eligible
clean_session_gate_credited  false

clean_session_gate_credited: false is the important one — running an extractor over a recording credits nothing toward §21. Whether a session was clean is a judgement about the recording, not about whether a script parsed it.

A suppressed detector is not a clean channel

The correction that came from real data. artifact_burst compares a channel to its own baseline. When that baseline is already pathological the comparison is vacuous — nothing exceeds burst_sigma × a rail — so a saturated AF7 emitted zero bursts. An absent record is ambiguous between clean, no transient, and never eligible.

session-events-manifest.json states it instead:

"AF7": {"detector_status": "suppressed", "suppressed_reason": "channel_saturated"},
"TP9": {"detector_status": "eligible",   "suppressed_reason": null}

The validator rejects a manifest omitting any channel, because a missing entry restores the ambiguity.

Review gates

gate where
half-open [start, start+count), non-empty, within recording_sample_count declared in the manifest; validated
event_id pins schema, recording, kind, range, params, observation — never a path test_same_bytes_at_a_different_path_give_identical_references
one-sample edit changes recording_sha256 and invalidates references test_editing_one_sample_invalidates_every_reference
canonical param serialisation, order-independent test_params_digest_is_order_independent
saturated/dead channel cannot read as a clean detector test_a_saturated_channel_is_reported_suppressed_not_clean
malformed rows, non-finite values, missing columns, sample-rate disagreement fail closed four *_fails_closed tests
Goertzel demeaned and Hann-windowed, coherent gain divided out goertzel_power
overlap and dedup documented docs/architecture/session-events.md

Events may overlap — the stride is shorter than the window — and are not deduplicated: two kinds firing on one window are two observations of it.

Evidence

Tests/eval/test_session_events.py     30/30 pass
real recording (78,228 samples)       750 events, contract-valid, AF7 suppressed
grep gate                             0 forbidden names in emitted output
swift build                           unaffected — no Swift target added

The 750-event run is pipeline validation — the extractor runs, is deterministic, and replays. It is not evidence that 750 heuristic observations are individually correct.

Standard library only; the system interpreter on a clean runner has no numpy. Fixtures are synthesised in the test rather than read from Recordings/, since no eeg.csv is committed and a test depending on one would skip vacuously.

aurascoper and others added 3 commits July 28, 2026 10:58
A dialogue turn this morning asked whether to wire a sleep detector and validate
an intervention. Both are gated, and the gates are countable:

    detector      SLEEP_CYCLE_DESIGN.md §21 — "5+ clean sessions through the
                  toolkit, no false readings"; currently 0 overnight sessions
    intervention  decision_registry.md entry 8 — the loop is "engineering
                  scaffolding, not a validated intervention"; efficacy requires
                  the D8 pre-registration, "non-negotiable"

The zero-throughput watchdog that turn named as the missing piece is already
built twice over — HealthSnapshot raises `eeg-zero-throughput` live, and
overnight-review detects it post-hoc against a CAPTURE_FAILED.json abort.

So this builds the thing upstream of both gates: `nc-eeg-session-event-v0`, one
record per observation, carrying the SHA-256 of the `eeg.csv` it points into
plus a sample range. It carries no signal. Hold the recording and the window is
re-derivable and `observed` recomputes to equality; without it the log says
nothing. That indirection is why one artifact serves a spectral routine today
and an encoder later — storing a latent would bind the log to whichever model
produced it, and expire with it.

**Observations only.** No stage, no efficacy, no REM (the montage has no chin
EMG — §291 R1). `session_event_contract.py` rejects a record asserting any of
them at any nesting depth, rather than leaving it to review.

Four kinds, all recomputable: clock gaps, per-channel health, band excursions
against an in-session baseline, artifact bursts. Baselines are the median over
the opening windows of the same session — a cross-session baseline would import
another night's headset fit.

`artifact_burst` is per-channel, and that was learned by running it. A
cross-channel envelope let a saturated AF7 (~900 µV against ~20 µV elsewhere)
trip the burst detector on 302 of 305 seconds, restating a fault
`channel_health_change` already reported. Per-channel thresholds drop it to 170
real transients on TP9/TP10/AF8 and zero on AF7, whose own baseline is already
saturated.

Determinism is a contract: same file, byte-identical output. `event_id` is
content-derived, `params_sha256` travels with every record so two logs are
comparable only if the rules match, and a one-sample edit changes
`recording_sha256` — invalidating references rather than silently re-pointing
them at different signal.

Standard library only. Band power via Goertzel over each band's integer bins —
O(n) per bin where a hand-rolled DFT would be O(n²) per window — because the
system interpreter on a clean runner has no numpy.

    Tests/eval/test_session_events.py     20/20 pass
    real recording (78,228 samples)       753 events, contract-valid
    grep gate                             0 forbidden names in emitted output
    swift build                           unaffected — no Swift target added

Fixtures are synthesised in the test rather than read from Recordings/: no
eeg.csv is committed, so a test depending on one would skip vacuously.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
D0-R (#43) merged as 859feaa, bringing the ADR registry repair and the
reference-integrity gate to main. Merged rather than rebased so the reviewed
commits stay as reviewed.

This branch adds no ADR reference, so the new gate should pass on it unchanged —
verified below rather than assumed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant