Repository navigation
Land Epoch Reviewer stack, remove stale docs/dead code, gate the release pipeline - #235
Merged
Merged
Conversation
The "Clean Data" flow told Pyodide to `raw_epochs.save("<host OS path>")`,
but the worker's filesystem is MEMFS (in-memory) and cannot reach host
disk — so the cleaned `-epo.fif` never landed on disk and the Analyze
screen's `readWorkspaceCleanedEEGData` picker found nothing in a fresh
workspace. This repairs that flow with a MEMFS→host write-back bridge,
the one slice the epoch-review plan (§6d / Open Question 8) declares
resolved and standalone.
Chain: Python saves to MEMFS `/tmp/<subject>-cleaned-epo.fif` → the
worker reads the bytes and posts them back zero-copy (transferable
ArrayBuffer) on a new `savedEpochs` dataKey → `pyodideMessageEpic`
writes them to `Data/<subject>/EEG/<subject>-cleaned-epo.fif` via a new
`fs:writeCleanedEpochs` IPC channel that mirrors `fs:storePyodideImagePng`.
- src/main/index.ts: new `fs:writeCleanedEpochs` handler (binary
renderer→disk write)
- src/preload/index.ts + electron.d.ts: `writeCleanedEpochs` bridge + type
- webworker.js: `readFileAfter` read-back path posts MEMFS bytes as a
transferable
- webworker/index.ts: `saveEpochs(worker, subject)` now saves to MEMFS
(overwrite=True) and requests read-back
- pyodideEpics.ts: route `savedEpochs` → `writeCleanedEpochs`, with a
success/error toast
Output path is bit-identical to what the Analyze picker globs (`epo.fif`
suffix). Also tempers docs/epoch-review-ui-plan.md against source.
typecheck: clean · eslint: clean · build: green
Resolves the forge-blocking open questions and writes a forge-ready Phase 0/1 execution plan into docs/epoch-review-ui-plan.md §0: - OQ1 rendering → Canvas 2D + DOM/SVG overlay, thin drawEpochs boundary, viewport windowing; WebGL a later swap. - OQ2 transport → extend the dataKey pattern (epochArrays, applyRejection); defer the runPython RPC (matches TODOS). - OQ6 live ERP preview → in scope for v1 (Phase 1), computed CLIENT-SIDE over the Phase-0 buffer (pure computeErp mean, zero worker round-trips) — Flavor 1 (while-cleaning) only; real-time-during-experiment is a separate future non-Python effort. - Shared condition-color palette built in Phase 0 (used by Phase-0 render, so not dead code). - Narrow Pyodide buffer-path fidelity test added in Phase 0. - Delivery split into two sequential PRs. Also updates the standing Pyodide-fidelity TODO to note the narrow test.
First half of the interactive epoch-review UI (docs/epoch-review-ui-plan.md
§0/§9). Read-only this phase — proves the data path and the Canvas 2D
rendering choice; click-to-reject, apply, and live ERP are Phase 1.
Data path (all new, extends the proven dataKey pattern — no runPython RPC):
Load Dataset → loadEpochsEpic → requestEpochArrays('raw_epochs')
→ worker get_epochs_arrays() writes a float32 buffer to MEMFS + returns meta
→ worker posts { buffer, results, dataKey:'epochArrays' } zero-copy transferable
→ pyodideMessageEpic → SetEpochArrays → reducer → CleanComponent → EpochReviewer
- utils.py: get_epochs_arrays(epochs, out_path) → float32 C-order
[epoch][channel][time] buffer + metadata (Marker/stim channel excluded via
pick_types(eeg=True)); plot_topo gains a palette param; condition-color
fallback DRYed to one _DEFAULT_CONDITION_PALETTE constant.
- webworker.js: readFileAfter read-back unified to post { buffer, results }
(savedEpochs write-back from PR #222 migrated to the same field).
- conditionPalette.ts: canonical condition palette, single source for the
Python matplotlib plots (JSON-injected) and the React reviewer.
- EpochReviewer.tsx: Canvas 2D traces (epochs side-by-side, channels stacked)
+ DOM overlay (labels, dividers), viewport windowing (first 8 epochs),
spike-preserving min/max downsampling, condition coloring, empty state, DPR
scaling. Pure decode/downsample helpers in epochArrays.ts.
Tests: native pytest (get_epochs_arrays buffer decodes to get_data(), Marker
excluded, byte length, event codes) + vitest (decode/downsample/color-index).
typecheck 0 · eslint 0 errors · vitest 37/37 · native pytest 10/10 · build green
Second half of the epoch-review UI (docs/epoch-review-ui-plan.md §0/§9) —
turns the static Phase 0 viewer into a working, teach-as-you-go cleaner.
Interaction (EpochReviewer, Canvas 2D + DOM overlay, OQ1):
- Click an epoch column to toggle it rejected (greyed trace + wash + ✕).
- Prev/Next navigation through epoch windows; column→ABSOLUTE index math is
offset-aware so the right epochs are dropped after paging.
- Amplitude gain control (clamped, traces can't bleed across lanes).
Live ERP preview (OQ6, client-side — the teaching payoff):
- LiveErpPane averages NON-rejected epochs per condition via the pure
meanTrace() over the Phase-0 buffer and redraws on every toggle. ZERO worker
round-trips (OQ2=2A, no runPython RPC); empty selection → flat line, no NaN.
Apply at "Clean Data":
- apply_rejection(epochs, drop_indices, bad_channels) in utils.py (epochs.drop
+ info['bads']), native-tested BIT-IDENTICAL to a manual MNE drop/bads.
- CleanEpochs carries {dropIndices, badChannels}; cleanEpochsEpic posts
apply_rejection (trailing ';' suppresses the Epochs PyProxy) → saves via the
existing write-back bridge (PR #222) → re-fetches arrays + stats. Local
selection clears after commit (fresh 0-based indices next round).
Bad-channel UI is Phase 2 — apply_rejection takes the param but the UI wires
badChannels: [] for now.
typecheck 0 · eslint 0 errors · vitest 44/44 · native pytest 14/14 · build green
Auto-flag = dev-gated one-click button + expandable threshold settings; suggestions computed in Python (peak-to-peak), pre-marked but overridable. Bad-channel flagging enabled on all devices with a warning Dialog on the 2nd bad channel of a 4-ch dataset. Condition legends use codeToLabel.
…bels
Full-parity phase (docs/epoch-review-ui-plan.md §0/§9).
Bad-channel flagging (OQ5):
- Click a channel label to toggle it bad (struck-through red label + red lane
wash across all epochs). Flows into the Phase-1 apply path:
CleanEpochs({dropIndices, badChannels}) → apply_rejection sets info['bads'].
- shadcn Dialog warns when marking a 2nd bad channel on a 4-channel (Muse)
dataset — informational, user can proceed.
Auto-flag (OQ4 + New-A, Python/MNE):
- Dev-gated "Auto-flag artifacts" button (AUTO_FLAG_EXPERIMENTS set, keyed by
the EXPERIMENTS enum) + expandable peak-to-peak threshold settings.
- suggest_rejections(epochs, threshold_uv) in utils.py (peak-to-peak per epoch,
Marker excluded; advisory — drops nothing), native-tested. New
'suggestedRejections' dataKey (fire-and-forget, no runPython RPC) →
SetSuggestedRejections → CleanComponent PRE-MARKS suggested epochs (additive
union, fully overridable) and shows reasons. Actual drop still via
apply_rejection, so the saved .fif stays MNE-exact.
Condition legend (New-B):
- EpochReviewer + LiveErpPane legends use human-readable labels via
buildMarkerRegistry(stimuli).codeToLabel (fallback to "Condition {code}").
typecheck 0 · eslint 0 errors · vitest 44/44 · native pytest 17/17 · build green
Phase 3 (onboarding layer, OQ3 open — route via office-hours/ceo-review) and Phase 4 (N-channel/WebGL, a11y, keyboard, perf; OQ7 open) added under Deferred. Three non-blocking Phase 2 review notes (bad-channel display exclude='bads', additive auto-flag re-merge, threshold min guard) added under tech debt.
- Strip process-narrating / Phase-history comments (EpochReviewer top block, pyodideActions, pyodideEpics, webworker index, EpochReviewer color/legend); keep only definition-level intent comments. - Invert auto-flag registration: replace the central AUTO_FLAG_EXPERIMENTS set with an opt-out hideAutoFlagEpochs field on ExperimentParameters (default on), colocated with the experiment definition. - Memoize buildMarkerRegistry by stimuli reference so the code->label map isn't rebuilt on every CleanComponent render. - Record the repo's comment-style preference in .llms/learnings.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019uiGzUcGN7j6qDWrfH5MCr
Lets any contributor drive the live BrainWaves Electron app over CDP (agent-browser connect 9333) for QA/dogfooding, instead of a headless browser that whitescreens without the preload bridge. SKILL.md self- documents install (npm i -g agent-browser); skills-lock.json pins the source + content hash. See .llms/learnings.md for the launch/attach loop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GaZP9vefeQV4CvNAm5Vv2Y
…very epoch load_data built the MNE RawArray straight from the µV CSV values without the µV→V scale MNE expects, so peak-to-peak came back ~1e6× inflated (tens of millions of "µV") and suggest_rejections flagged all epochs → blank ERPs. - Scale eeg rows ×1e-6 in load_data, keyed on ch_type=='eeg' (stim/Marker row untouched; marker codes stay numeric). - Emit µV (×1e6) at the two display boundaries — the epoch-viewer buffer (get_epochs_arrays) and the ERP plot (already labeled uV) — so the viewer and plot show the same numbers as before while MNE-internal units are now correct. - Regression guard: assert normal ptp lands 1–1000µV (not ~1e7). - Fix test_detects_injected_artifact: it added a DC offset (ptp-neutral) and only "passed" because the units bug flagged everything; now injects a real single-sample spike. QA plan 6e (T1). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTrEip1cj1QLTU48qr9jLw
openWorkspaceDir passed a relative "BrainWaves_Workspaces/<title>" to shell.showItemInFolder, which silently no-ops on non-absolute paths — the Home screen "Go to Folder" button did nothing. Add a shell:openWorkspaceDir handler that resolves the absolute path with getWorkspaceDir and opens it via shell.openPath; point the renderer at it. QA plan 9a (T4). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTrEip1cj1QLTU48qr9jLw
isEEGEnabled defaulted to false — nonsensical for an EEG app, and a run would
silently record behavior only with no warning. Flip the default to true, and
guard handleStartExperiment: if EEG is disabled, or enabled but no device is
connected, confirm ("...records responses but no brain data. Continue anyway?")
before starting, reusing the existing showMessageBox pattern.
QA plan 7 (T3).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTrEip1cj1QLTU48qr9jLw
Muse is Bluetooth — there is no USB dongle. Drop the COMPUTER_CONNECTABILITY "Insert the USB Receiver" step so "Turn your headset on" → Next goes straight to searching for devices. QA plan 8 (T6). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTrEip1cj1QLTU48qr9jLw
…TIMULUS_n buildMarkerRegistry now derives display labels from the experiment's own stimulus.condition, so Clean counts/legend and the ERP read "Face"/"House" instead of the generic STIMULUS_1/2. Numeric codes are untouched (they still drive the CSV Marker column and MNE event_id — labels are display-only). Falls back to STIMULUS_n when a stimulus has no condition, and refuses to collapse two codes under one label (would silently merge conditions in analysis). The Clean legend and Python ERP legend pick the names up automatically (both read off the registry / epochs.event_id). QA plan 6b (T7). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTrEip1cj1QLTU48qr9jLw
The savedEpochs branch wrote whatever buffer arrived and only caught write errors. An empty/dropped buffer wrote nothing and errored nothing, leaving the Analyze picker mysteriously empty. Guard: if the buffer is missing or zero-length, surface an error toast instead of failing silently. QA plan 9b (T5). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTrEip1cj1QLTU48qr9jLw
Cleaning with all epochs flagged produced an empty, unanalyzable dataset (and
previously wrote a degenerate .fif with no error) — the source of the empty
Analyze picker after auto-flag over-rejected. handleCleanData now confirms
("This will reject all N epochs, leaving nothing to analyze") before dropping
when the reject set covers every epoch.
Note: this file also carries in-progress epoch-reviewer Phase 2 Clean-screen
changes that were already staged in the working tree; they are committed here
because they could not be separated non-interactively. The T2 guard is the
handleCleanData method + its wiring.
QA plan 6e/9b (T2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTrEip1cj1QLTU48qr9jLw
- 6a: the ⚙ auto-flag settings button now uses Button size="icon" (h-9 w-9),
matching the height of the adjacent Clean Data / Auto-flag buttons.
- 6g: replace the raw µV number input with a range slider labeled by effect
("More flags ← → Fewer flags"), showing the current µV inline and via
aria-valuetext. Bounds come from getPtpThresholdPreset(device) — a single
validated range today, with a per-device seam for later (no fabricated
per-hardware ceilings). Only meaningful now that units are fixed (T1).
QA plan 6a/6g.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTrEip1cj1QLTU48qr9jLw
When a run finished, RunComponent silently re-rendered the identical pre-run
landing — no confirmation it recorded, and the only way forward (top nav) was
undiscoverable. Track hasFinished and, once a run ends, show a completion panel
("Recording complete") with a primary "Clean your data →" link to the Clean
screen and a "Run again" secondary. Closes the Collect dead-end.
QA plan 5.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTrEip1cj1QLTU48qr9jLw
- 3: replace the Emotiv saline-soak help tip with device-agnostic guidance
(clean bare skin, sweep hair) that also explains sensor settling; rename the
HELP_STEP.SIGNAL_SALINE step to SIGNAL_SETTLING. Mount the HelpButton +
HelpSidebar on the Explore screen (previously Collect-only).
- 1 (lightweight): show an amber role="status" settling banner for the first
~45s after connecting ("a red, jumpy signal is normal while sensors make
contact… watch it turn green"), auto-dismissed by a timer and manually
closeable. No change to the signal-quality math — the honest red state still
surfaces after the window. Full amber per-dot settling state (DF2) deferred to
a live design-review pass.
QA plan 3 + 1 (lightweight).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTrEip1cj1QLTU48qr9jLw
…dedupe wrapper
Concrete markup fixes for the pre-run landing (QA item 4), short of the full
two-column DF1 redesign (deferred to a live design-review):
- The "mystery empty bar" was a `w-full` button holding only a pencil emoji;
now a small ghost icon button beside the title.
- Collapse the redundant nested h-screen/gradient wrapper (the outer container
already provides it) into a centered flex.
- Wrap the subject/group/session summary in a compact shadcn Card, right-size
the Run Experiment button (no longer w-full), and drop the <h1>{title}</h1>
that duplicated the nav breadcrumb.
QA plan 4 (markup subset; DF1 live-signal column deferred).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTrEip1cj1QLTU48qr9jLw
When no cleaned data exists, the "Select Clean Datasets" picker rendered an
empty box with no guidance — the confusing blank Analyze screen from the QA
walkthrough. Show a warm message ("No cleaned data yet — clean a recording
first…") with a "Go to Clean →" link instead.
QA plan 9b / T11 (Analyze empty state).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTrEip1cj1QLTU48qr9jLw
Checks off T1–T7, 6a/6g, #3/#4-markup/#5, lightweight #1, and the Analyze empty state (commits beb1620..376740f), and documents why #2/#6c (profile-first), #4-DF1 + DF2-full (visual/design-review), and #6d (Phase-3 product) are deferred. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTrEip1cj1QLTU48qr9jLw
- Refactored the web worker's message handling to ensure asynchronous commands are processed in sequence, preventing overlap and ensuring Python globals are available before subsequent commands. - Introduced a new function `load_clean_epochs` in `utils.py` to load and concatenate cleaned .fif epoch files from MEMFS paths, improving data handling. - Updated the `loadCleanedEpochs` function in `index.ts` to utilize the new `load_clean_epochs` function for better clarity and functionality. - Adjusted message posting logic to prevent sending non-structured-cloneable MNE objects back to the main thread. QA plan updates included for the new functionality.
Epoch reviewer Phase 2: bad-channel flagging, auto-flag suggestions, condition labels
The epoch-reviewer stack was based on the pyodide-analysis-fix branch and each PR merged into its parent rather than main, so only docs/epoch-review-ui-plan.md reached main when #220 landed. This brings in Phases 0-2 plus the UX and analysis fixes that rode along. - Phase 0: epoch arrays shipped to a Canvas renderer (#223) - Phase 1: interactive rejection, live ERP, apply/save (#224) - Phase 2: bad-channel flagging, auto-flag suggestions, condition labels (#225) - Clean->Analyze round-trip: persist cleaned epochs to host disk (#222) - fix(analysis): scale Muse uV->V on load so auto-flag stops rejecting every epoch - fix(experiment): default EEG on + warn before a run with no brain data - fix(connect): remove Emotiv-era USB receiver step from Muse connect flow - feat(analysis): show real condition names instead of STIMULUS_n
Sweep of debris left after the epoch-reviewer stack landed. Dead code (zero callers, verified post-merge): - plotTestPlot — debug scaffolding that rendered a four-point zigzag titled "Test Plot" into the real plotKey:'topo' UI slot. Its only caller was swapped for plotTopoMap in #220. - cleanEpochsPlot — rendered a matplotlib epochs figure in WASM then immediately plt.close()'d it; never routed to the UI. The Epoch Reviewer replaced its last caller. - parseSingleQuoteJSON — obsoleted by the worker's toJs() dict_converter. Docs: - Delete docs/lsl-implementation-plan.md. LSL shipped in #204/#221; every file in its "New files to create" inventory exists. Durable knowledge already lives in .llms/learnings.md; remaining per-platform checks are in TODOS.md. - device-connectivity.md said "currently: Muse only" — false since the EEGDriver registry (Muse, Neurosity, LSL). Corrected and pointed at the driver contract; the Muse-specific flow diagrams are retained as such. - epoch-review-ui-plan.md still called itself "Preliminary ... not yet an implementation spec" after Phases 0-2 merged. Status updated. Stale known-issues (fixed, but still instructing readers to ignore errors in two files — which would mask real regressions): - Drop the "Pre-existing TypeScript errors" entry from .llms/learnings.md and TODOS.md. experimentEpics.ts now carries explicit eslint-disable + typed workarounds, routes.tsx is clean v6, and CI's typecheck passes. - deviceActions.ts: drop a "(Phase 3)" reference to the deleted LSL plan.
The Release workflow has never run — no tag has ever been pushed — and as
written a tag published whatever compiled: no test, lint, or typecheck ran on
the release path, and both workflows used 'npm install', so the tree CI passed
on was not necessarily the tree that shipped.
Release workflow:
- New 'verify' job (test + typecheck + lint) that the publish matrix depends
on. Nothing builds or uploads until it is green.
- Split 'package-ci' into build -> verify artifacts -> publish, so a build with
a missing Pyodide payload cannot be published.
- npm ci, and fail-fast so a broken tag uploads as few half-good artifacts as
possible across the three OS legs.
test.yml: npm ci, plus the artifact check after Build.
package.json: publish.releaseType 'draft'. electron-updater is wired to this
repo, so a tag would otherwise push an update to installed clients the moment
it built. Draft means artifacts are uploaded but a human publishes.
New tests:
- tests/pyodideAssets.test.ts — static guard on the contract between
extraResources ('to': pyodide) and the main process protocol handler
(path.join(process.resourcesPath, 'pyodide')), plus the dev-side path and the
directory InstallMNE writes into. These live in three files with no shared
constant; a mismatch is invisible in dev and broke prod completely once (see
.llms/learnings.md). Needs no build, so it runs in the default suite.
- tests/build.test.ts — post-build presence checks: the three bundles, the
Pyodide runtime files, and every wheel named in the package manifest. Runs
via 'npm run test:build' under its own config and is excluded from the
default run, so a missing build is a loud ordering error, never a silent skip.
TODOS: record the two things this still does not cover — an actual packaged-app
launch smoke test, and the first release dry-run.
…e code docs + vendored skill - Delete docs/faces-houses-qa-plan.md (603 L) and docs/epoch-review-ui-plan.md. A manual QA script and a plan for work that just shipped; nothing reads either. Phase 3/4 already live in TODOS.md as their own bullets. - Delete the accidentally-vendored agent-browser skill (.agents/, skills-lock, and the .claude symlink) — a checked-in symlink is a Windows-clone hazard. path contract: detect -> impossible - New src/shared/pyodideAssets.ts exports PYODIDE_SOURCE_DIR and PYODIDE_RESOURCE_DIR; src/main/index.ts imports them instead of repeating literals. Deletes tests/pyodideAssets.test.ts, which regex-scraped main's source to compare a folder name. package.json is JSON and can't import, so that one pairing is asserted in build.check.ts. - Rename build.test.ts -> build.check.ts: the default glob never picks it up, so vitest.build.config.ts and the vitest.config exclude both go. - Drop the packageJson.main check (out/main/index.js already covers it). yagni - PtpThresholdPreset + getPtpThresholdPreset(_device) ignored its parameter and returned a literal -> PTP_THRESHOLD const. DEFAULT_PTP_THRESHOLD_UV folds in. - hideAutoFlagEpochs: no experiment sets it; renderReview's showAutoFlag param was always true. - EpochArraysMeta.sfreq/.drop_log and SuggestedRejection.channel/.peak_uv had zero readers outside fixtures. Dropped from utils.py too. - EpochReviewer amp +/- gain: each lane already autoscales, so gain only bought clipping. - EEGExplorationComponent settling banner: HelpSidebar says the same thing and this PR adds the Help entry point to that screen. native + stdlib - role=button/tabIndex/onKeyDown -> <button type="button"> in both places. - times.findIndex((t) => t >= 0) for the t=0 line. - conditionIndexForCode -> Math.max(0, codes.indexOf(code)) at three call sites. - Channel-count warning uses window.electronAPI.showMessageBox, matching handleCleanData, instead of a Radix Dialog plus a state field. shrink - Dropped downsampleMinMax's width >= length branch (only tests reached it), meanTrace's empty guard (LiveErpPane already skips empty groups), conditionPalette's double-modulo, utils.py's single-file short-circuit, and webworker's dataKey === 'epochArrays' special-case. - palette is now required in plot_topo/plot_conditions; both call sites inject it, so the copied _DEFAULT_CONDITION_PALETTE is gone. - LiveErpPane derives includedCount; EpochReviewer reuses visibleCount and hoists uniqueSortedCodes above the effect. - cleanEpochsEpic is tap + map, not async returning a value to map back. - Deleted experimentReducer.test.ts (a literal asserted against a literal). test_suggest_rejections no longer asserts on the dropped keys. The uV->V regression guard now computes peak-to-peak from the epochs directly, so it tests the data rather than the reporting payload.
CI caught this: the guard was removed but its test remained, so the empty case divided by zero and returned NaN. The sole caller (LiveErpPane) already `continue`s on empty condition groups, so both the guard and the test covered an unreachable path. Deleted the test and documented the non-empty precondition on meanTrace instead.
- EpochReviewer: the divider loop still referenced `count`, which the visibleCount dedupe removed. - CleanComponent: the suggestion <li> key still read s.channel, dropped from SuggestedRejection. Key is index + position now. - Remove two comments left behind by the deleted gain state and dialog state.
…an include CI: `vitest run tests/build.check.ts` found no test files. Positional args filter against `include`, and build.check.ts deliberately doesn't match the default *.test.ts glob, so nothing matched. Kept the review's substance — the filename alone keeps it out of `npm test`, so there is no `exclude` in vitest.config.ts and no two-file coordination. The build config now only supplies the include.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four commits. The first recovers work that was stranded; the rest clean up after it, close the gap that made it possible to miss, and act on review.
Net: +2,320 / −1,011 across 42 files.
1. Land the Epoch Reviewer stack —
387fac2PRs #222–#225 all report MERGED, but each merged into its parent branch, not main. Only the bottom of the stack (
pyodide-analysis-fix, #220) was retargeted; when it landed, the children were never re-pointed. The result: main got a plan document and nothing else, while the implementation sat onepoch-reviewer-phase1, 24 commits ahead and invisible.Clean merge, no conflicts — the stack never touched
package.json/package-lock.json, so main's dependabot bumps didn't collide.Plus a fix cluster that matters independently of the feature:
beb16207fdccfbc1cb40d69396e4STIMULUS_n68a410b2. Remove stale docs and dead code —
a97bd04Dead code, all verified zero-caller after the merge:
plotTestPlot— debug scaffolding that rendered a four-point zigzag titled "Test Plot" into the realplotKey: 'topo'UI slot. Its only caller was swapped forplotTopoMapin Fix Pyodide analysis pipeline (harvest #194: dataKey routing, MEMFS, MNE 1.x) #220.cleanEpochsPlot— rendered a matplotlib epochs figure in WASM then immediatelyplt.close()d it, never routed to the UI. Became dead in this merge — the Epoch Reviewer replaced its last caller, as the plan predicted.parseSingleQuoteJSON— obsoleted by the worker'stoJs()dict_converter.Docs: deleted
docs/lsl-implementation-plan.md(477 L — LSL shipped in #204/#221; every file in its "New files to create" inventory exists). Correcteddevice-connectivity.md, which opened with "currently: Muse only" — false since theEEGDriverregistry.Also dropped the "Pre-existing TypeScript errors (do not treat as regressions)" entry from
.llms/learnings.mdandTODOS.md. Those errors are fixed, and a standing instruction to ignore type errors in two files is a permanently disabled alarm.3. Gate the release pipeline —
f3f432cThe Release workflow has never executed. No tag has ever been pushed. As written, a tag ran
npm install+package-ciand published whatever compiled — no test, lint, or typecheck on the release path.verifyjob (test + typecheck + lint) that the publish matrixneeds:. Nothing builds or uploads until it's green.package-ciinto build → verify artifacts → publish, so a build with a missing Pyodide payload can't be published.npm ciin both workflows, so the tree CI passed on is the tree that ships.fail-fast: trueto limit half-good uploads across the three OS legs.publish.releaseType: "draft".electron-updateris wired to this repo, so a tag would otherwise push an update to installed clients the moment it built. Draft means artifacts upload but a human publishes.4. Address review —
2b339c7−1,490 net. Every item from the review was taken; two were taken further than proposed.
Docs + the vendored skill (−950). Deleted
faces-houses-qa-plan.md(a manual QA script nothing reads),epoch-review-ui-plan.md(a plan for work in this diff; Phase 3/4 already live inTODOS.md), and the accidentally-vendoredagent-browserskill including its checked-in symlink.The path contract: detect → make impossible. The review was right that regex-scraping
src/main/index.tsto compare a folder name is brittle. Newsrc/shared/pyodideAssets.tsexportsPYODIDE_SOURCE_DIR/PYODIDE_RESOURCE_DIR;src/main/index.tsimports them, andtests/pyodideAssets.test.tsis deleted. One assertion survives inbuild.check.ts:package.jsonis JSON and cannot import, soextraResources↔ constants is the one pairing a shared symbol can't enforce.Test harness collapsed.
build.test.ts→build.check.ts— the default glob never matches it, sovitest.build.config.tsand thevitest.configexclude both go. Two files coordinating to hide one test, replaced by a filename.YAGNI.
getPtpThresholdPreset(_device)ignored its parameter and returned a literal →PTP_THRESHOLD.hideAutoFlagEpochs(no experiment sets it) and the always-trueshowAutoFlagparam.EpochArraysMeta.sfreq/.drop_logandSuggestedRejection.channel/.peak_uv— zero readers outside fixtures, dropped fromutils.pytoo. The amp ± gain control (lanes already autoscale). The settling banner (HelpSidebarsays the same thing, and this PR adds the Help entry point to that screen).Native + stdlib.
role="button"/tabIndex/onKeyDown→<button type="button">in both places.times.findIndex((t) => t >= 0).conditionIndexForCode→Math.max(0, codes.indexOf(code))inline. The channel-count warning now usesshowMessageBox, matchinghandleCleanData, instead of a Radix Dialog plus a state field.Shrink.
downsampleMinMax'swidth >= lengthbranch,meanTrace's empty guard, the double-modulo inconditionPalette,utils.py's single-file short-circuit, and thedataKey === 'epochArrays'special-case in the worker.paletteis now required inplot_topo/plot_conditions, so the copied_DEFAULT_CONDITION_PALETTEis gone.One deviation worth flagging. The review asked to drop
peak_uvfromutils.py, buttest_ptp_in_physiological_range— the µV→V regression guard the review explicitly kept — read its value. Rather than keep the field or lose the guard, the test now computes peak-to-peak from the epochs directly. It tests the data instead of the reporting payload, which is what it was always trying to do.Test plan
Authored on a machine with no Node, no
node_modules, and no MNE — CI is the first execution. The previous push already answered the biggest unknowns: the ubuntu leg passed, which meansnpm cisucceeded (lockfile in sync), the merged stack passes against main's dependabot bumps, andtest:buildran clean after Build.This push needs re-verification of:
npm test— fixture edits inEpochReviewer.test.tsx/epochArrays.test.ts, and the removedconditionIndexForCode/downsampleMinMaxcasesnpm run typecheck— thePTP_THRESHOLDswap, dropped interface fields, and therenderReviewsignature changenpm run lint— two<div role="button">→<button>conversionsnpm run build+npm run test:build— the renamed check and new shared constantstest_suggest_rejections.pyrewritten;palettenow required inplot_topo/plot_conditionsNot covered by CI:
.fifround-trips to Analyzev1.0.0-rc.1dry-run — the release path is still unproven end to endStill open
Recorded in
TODOS.md, not addressed here:build.check.tsproves the payload is on disk; nothing proves the app starts. That's the gap that would catch a prod-only Pyodide regression automatically.