Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gstack/browse-audit.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{"ts":"2026-07-14T21:39:03.322Z","cmd":"status","args":"","origin":"about:blank","durationMs":2,"status":"ok","hasCookies":false,"mode":"launched"}
{"ts":"2026-07-14T21:44:12.653Z","cmd":"goto","args":"http://localhost:5173/","origin":"http://localhost:5173/","durationMs":714,"status":"ok","hasCookies":false,"mode":"launched"}
{"ts":"2026-07-14T21:44:14.724Z","cmd":"console","args":"--errors","origin":"http://localhost:5173/","durationMs":1,"status":"ok","hasCookies":false,"mode":"launched"}
{"ts":"2026-07-14T21:44:14.780Z","cmd":"snapshot","args":"-i -o /Users/dano/work/BrainWaves/.gstack/qa-reports/screenshots/initial.png","origin":"http://localhost:5173/","durationMs":17,"status":"ok","hasCookies":false,"mode":"launched"}
6 changes: 6 additions & 0 deletions .gstack/claude-available.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"available": true,
"path": "/Users/dano/.local/bin/claude",
"install_url": "https://docs.anthropic.com/en/docs/claude-code",
"checked_at": "2026-07-14T21:40:03.299Z"
}
15 changes: 14 additions & 1 deletion .llms/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ A priority for this codebase is extensibility modularity and hackability. There
- **Testing**: Vitest
- **Linting**: ESLint + Prettier (single quotes, ES5 trailing commas)


## Domain skills

Read these before touching the matching seams (also listed in root `CLAUDE.md`):

- `.claude/skills/electron-ipc-architecture/` + `electron-ipc-channel/` — process boundary, IPC
- `.claude/skills/pyodide-mne/` — analysis worker, `pyodide://`, InstallMNE
- `.claude/skills/redux-observable-epochs/` — epics, markers, empty ERPs

## Playtest

This is an Electron app. `http://localhost:5173` is the Vite renderer **without** preload. Do not `/qa` or `/browse` it — `electronAPI` and `pyodide://` only exist in the Electron window. See root `CLAUDE.md`.

## Key Directories
- `src/main/` — Electron main process
- `src/renderer/` — React renderer process
Expand All @@ -39,7 +52,7 @@ npm run package # Build + package for current platform

## Conventions
- Use TypeScript; avoid `any` unless strictly necessary
- Redux state changes go through RTK slices or typed actions via `typesafe-actions`
- Redux state changes go through RTK `createAction` / `createReducer`. `typesafe-actions` remains only for `ActionType` unions — do not add more of it.
- Side effects belong in RxJS epics (`redux-observable`)
- Do not commit secrets or device credentials
- Keep Electron main/renderer separation strict — use preload IPC bridges
Expand Down
28 changes: 26 additions & 2 deletions .llms/learnings.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Device/LSL connectivity is integration-tested with the native layers fully mocke
- Renderer drivers mock `window.electronAPI` (capture the `onLSLInletData`/`onLSLInletDisconnected` handlers to "push" inlet epochs) and `@neurosity/sdk`. The Neurosity mock's `Neurosity` must be `new`-able — `neurosity.ts` does `new Neurosity(...)` — so define a plain `function Neurosity(){ return client }` inside `vi.hoisted` (arrow fns aren't constructable) and `vi.mock('@neurosity/sdk', () => ({ Neurosity: h.Neurosity }))`.
- `lslBridge.ts` probes availability at module load via a top-level `isLSLAvailable()` promise; to test the gate, set `window.electronAPI.isLSLAvailable` before `vi.resetModules()` + dynamic `await import('../lslBridge')`, then flush a microtask.

**CI**: `npm run device-integration` (script targets `src/main/lsl src/renderer/utils/eeg`) runs as its own `.github/workflows/integration.yml` job with `npm ci --ignore-scripts` — since every native module is mocked, it skips the slow Pyodide/MNE postinstall and needs no liblsl. The full cross-OS suite still runs these too via `npm test` in `test.yml` (whose `Test` step runs before the lint step).
**CI**: `npm run device-integration` (script targets `src/main/lsl src/renderer/utils/eeg`) runs as its own `.github/workflows/device.yml` job with `npm ci --ignore-scripts` — since every native module is mocked, it skips the slow Pyodide/MNE postinstall and needs no liblsl. The full cross-OS suite still runs these too via `npm test` in `test.yml` (whose `Test` step runs before the lint step).

## Styling System (post Phase 4 migration)

Expand Down Expand Up @@ -120,7 +120,7 @@ The CDN version is derived from `node_modules/pyodide/package.json` — **not**

**WebAgg backend does not work in web workers** — WebAgg tries to access `js.document` to inject CSS/JS into the DOM on first import, which throws `ImportError: cannot import name 'document' from 'js'` in a worker context. Use `agg` instead. Set it via `os.environ["MPLBACKEND"] = "agg"` before any matplotlib import. `fig.savefig()` works with `agg` and is the correct way to get plot images back to the renderer.

**Plot result routing pattern** — `worker.postMessage()` is fire-and-forget (returns `undefined`). Plot epics should use `tap()` to fire the worker message and `mergeMap(() => EMPTY)` to emit nothing. Results come back asynchronously on the worker `message` event. Add a `plotKey` field to each worker message; the worker echoes it back; `pyodideMessageEpic` switches on `plotKey` to dispatch `SetTopoPlot`/`SetPSDPlot`/`SetERPPlot` with a `{ 'image/png': base64string }` MIME bundle. `PyodidePlotWidget` renders this via `@nteract/transforms`.
**Plot result routing pattern** — `worker.postMessage()` is fire-and-forget (returns `undefined`). Plot epics should use `tap()` to fire the worker message and `mergeMap(() => EMPTY)` to emit nothing. Results come back asynchronously on the worker `message` event. Add a `plotKey` field to each worker message; the worker echoes it back; `pyodideMessageEpic` switches on `plotKey` to dispatch `SetTopoPlot`/`SetPSDPlot`/`SetERPPlot` with an SVG string wrapped as `{ 'image/svg+xml': svg }`. `PyodidePlotWidget` renders that as a data-URI `<img>` (no `@nteract/transforms`).

## Lab.js 23.x API: `hooks` replaces `messageHandlers`

Expand Down Expand Up @@ -231,3 +231,27 @@ fails on a `pyodide://` URL instead.
Experiments emit `this.data.correct_response` as a real boolean (`true`/`false`) and `response_given` as `'yes'`/`'no'` (see `src/renderer/utils/labjs/functions.ts`). But all consumers in `src/renderer/utils/behavior/compute.js` read data **after** it's been written to CSV and re-parsed, so every value is a **string**. That's why existing code gates on `row.correct_response === 'true'` and `row.response_given === 'yes'`, and parses numbers with `parseFloat`.

Trap: a new metric written naively (`row.correct_response === true`, or arithmetic on an unparsed string) will silently return `false`/`0`/`NaN` for post-CSV data — and may *work* on pre-CSV in-memory data, so it passes a quick test and fails in production. Always compare against the string `'true'`/`'yes'` and `parseFloat` before doing math.

## Playtest the Electron window, never Vite `:5173`

`npm run dev` serves the renderer at `http://localhost:5173` *and* opens Electron.
Chrome / gstack `/browse` / `/qa` against that URL has no preload: `LSLStatusListener`
throws and Pyodide cannot fetch `pyodide://host/...`. Evidence:
`.gstack/browse-console.log` (2026-07-14). Drive the Electron window (planned:
`--remote-debugging-port` + OMP CDP). Root `CLAUDE.md` bans `/qa` on Vite.

## Custom experiments are a V1 P0 restore, not a delete

The 2017–2020 app had a working custom-experiment builder (CHANGELOG 0.11–0.13).
HEAD still has the files (`CustomDesignComponent`, `StimuliRow`/`StimuliDesignColumn`,
`experiments/custom/`) but the bank has no Custom card, `getExperimentFromType`
falls through to Faces/Houses, and CONDITIONS/TRIALS are stubbed. Recover from
git history; do not delete the stub. See `TODOS.md`.

## LSL inlet `injectMarker` is intentional

`EEGDriver.injectMarker` is required for Muse/Neurosity (CSV + ERP). LSL inlet
is a separate mode: the external recorder owns markers, so `injectMarker()`
no-ops. First-party runs still `sendMarker()` to the LSL *outlet* from
`RunComponent`. Do not "fix" the inlet no-op.

20 changes: 20 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
@.llms/CLAUDE.md
@.llms/learnings.md

## Skill routing

When the user's request matches an available skill, invoke it via the Skill tool. When in doubt, invoke the skill.

### BrainWaves domain skills (read these first)

These live in `.claude/skills/` and are the source of truth for the hard seams.

- Anything that crosses main / preload / renderer (IPC, `electronAPI`, native modules, Bluetooth, FS, dialogs) → `electron-ipc-architecture`, then `electron-ipc-channel` when adding/editing a channel
- Pyodide, MNE, `webworker/`, `InstallMNE.mjs`, plot/data routing, prod-only analysis failures → `pyodide-mne`
- Epics, live EEG, markers, empty ERPs, `buildMarkerRegistry` → `redux-observable-epochs`

### This is an Electron app, not a website

`npm run dev` starts electron-vite, which also serves the renderer at `http://localhost:5173`. That URL is **not** the app.

- `window.electronAPI` and the `pyodide://` protocol exist only inside the Electron window (preload + main).
- Opening `:5173` in Chrome / `/qa` crashes `LSLStatusListener` and fails Pyodide init (`Failed to fetch … pyodide://host/pyodide/pyodide.asm.js`).
- **Never** `/qa`, `/qa-only`, or `/browse` against `localhost:5173`.
- Playtest the Electron window. A dedicated `electron-playtest` skill (CDP attach via `--remote-debugging-port`) is the planned harness — see `TODOS.md`. Until that exists: `npm run dev` and drive the desktop app, not the Vite URL.
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ If you find a bug, or have a suggestion on how to improve the project, just fill

If you're interested in using BrainWaves as a basis for your own work to streamline EEG experimentation or psychological data collection, we'd love to hear from you. Send an email to [dano@neurotechx.com](mailto:dano@neurotechx.com) or create an issue and we'll be in touch.

All project management for BrainWaves occurs through issues on Github (via Zenhub). If you want to see what we need help with, or what is on our [roadmap](ROADMAP.md), check out the issues on this repository.
Current work lives in [`TODOS.md`](TODOS.md) and the strategic layer in [`ROADMAP.md`](ROADMAP.md). Agent/developer conventions are in [`CLAUDE.md`](CLAUDE.md) and [`.llms/`](.llms/). GitHub issues are still welcome.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ go. This step disappears once the project is signed and notarized.

The rest of this README is for developers building from source.

- **Node.js** >= 18
- **npm** >= 9
- **Node.js** `^20.19.0 || >=22.12.0` (see `package.json` `engines`)
- **npm** >= 8
- No Python installation required — EEG analysis runs via [Pyodide](https://pyodide.org) (Python compiled to WebAssembly), which is downloaded automatically on first `npm install`.

> **Note:** `npm install` downloads ~300 MB of Pyodide WASM files on first run. This is expected and only happens once.
Expand Down
43 changes: 27 additions & 16 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,40 @@
# Roadmap

## Overview
Summer 2026: BrainWaves revival for high school. Execution detail lives in `TODOS.md` — this file is the strategic layer. If the two disagree, `TODOS.md` wins.

Summer 2026: Brainwaves revival for High School
## V1 / V1.1 — classroom MVP

## LSL
Ship a signed-off Muse classroom loop: Design → Collect → Clean → Analyze, including **custom experiments** (P0; they worked in the 2017–2020 app and must work again).

- [x] Cut out emotiv SDK garbage
- [ ] Update existing muse-js Muse connectivity. Emit connected devices on LSL stream
- [ ] Add Neurosity SDK to app. Emit connected devices on LSL stream
- [ ] Add option for users to connect external LSL streams, providing support for any theoretical EEG device
- [ ] Update stimulus markers from experiments to LSL stream
- [ ] Use LSL internally to handle data recording
- [ ] QA LSL experiment functionality, ensuring support on both Mac and Win, and easy installation
- [x] Cut Emotiv SDK
- [x] Muse + Neurosity first-party drivers (`EEGDriver` registry)
- [x] LSL outlets for connected first-party devices (epochs + stimulus markers)
- [x] External LSL inlet in ConnectModal (when liblsl is available)
- [ ] Restore custom-experiment authoring (see TODOS — P0)
- [ ] QA built-in + custom experiments on Muse hardware
- [ ] First release dry-run (`v1.0.0-rc.1`) + packaged-app smoke
- [ ] Cross-platform LSL packaging verification (macOS x64, Windows, Linux)

## Lesson Content
CSV is still the system of record. Using LSL *internally* for recording is not a V1 goal.

- [ ] Get lesson content from Steve Azeka from Brainwaves classes
- [ ] Add lesson content to Brainwaves app
## V1.5 — visual polish

## Lab.js
Epoch-reviewer onboarding (plain language, guided mode). See TODOS "Next".

- Type lab.js data (pending lab.js TypeScript library update)
- Remove jspsych and refactor lab.js usage
## V2 — lesson content

- [ ] Neuro content from Steve Azeka (2017 classroom material)
- [ ] Data-science content from Teon Brooks
- [ ] In-app lesson surface (static markdown first)

## Later

- Muse S Athena (Gen 3)
- Neurosity polish (only if a partner classroom owns Crowns)
- Type lab.js / strip leftover jsPsych strings (users never see this)
- Lesson surface beyond markdown (block-based / notebooks)

## Deliberate dual systems (not debt)

- Live EEG in a `<webview>` (thread isolation) vs canvas epoch reviewer (high-frequency interactive) vs Pyodide SVG (static MNE/matplotlib).
- BLE `EEGDriver` (Muse / Neurosity) vs LSL inlet (external recorder). Inlet `injectMarker` is a **no-op on purpose** — the external recorder owns markers. First-party runs still `injectMarker` locally and `sendMarker` to the LSL outlet.
Loading
Loading