off leaves libsSource null, so boot.ts never calls installLibsProvider and
window.kicadLibs is never installed. A C++ libs-bridge call on schematic open
(park site K1, sch_io_pcbjam_lib) then hits undefined -> throws inside a
suspending import -> SuspendError -> abort. static installs the bridge (2
built-in example symbols; answers not-found gracefully otherwise), no backend.
Standalone-only change; dist/ rebuild, no wasm rebuild.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Serve the standalone editor under a CMMS sub-path with no change to
@pcbjam/shared or the wasm/C++ side. No-op for a root deploy (BASE_URL='/').
- main.tsx: <BrowserRouter basename={import.meta.env.BASE_URL}>
- lib/base-path.ts (new): stripBase() for raw location.pathname reads,
hardNav() for the cross-document window.location.assign() navs (the
process-global wasm runtime means a tool switch / File->Quit is a full
page load, and @pcbjam/shared's projectPath() is origin-absolute)
- config.ts currentScope(): stripBase() before splitting for the scope segment
- WasmTool.tsx quit-path: stripBase() for the projects/libs segment test
- tool-navigation.ts (x2), NewFileDialog.tsx, ProjectView.tsx, quit-hook.ts:
window.location.assign(projectPath(...)) -> hardNav(...)
- .env.production: VITE_API_BASE_URL=/pcb-project-api.php, PROJECT_SOURCE=remote,
DOC_SOURCE=api, LIBS_SOURCE=off, WASM_ROOT=/vendor/pcbjam/wasm,
YJS_PROVIDER=broadcastchannel; APP_URL/USER/SCOPE unset
Build: vite build --base=/vendor/pcbjam/
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The findings-E branch tightened lint:determinism to require
"documented interaction dwell: <why>" on every dwell marker; these six
specs landed on staging after that branch was cut, so the two only met
at merge time and the first staging CI run went red. Comment-only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A review of the group-E fixes found 13 further defects; ten were introduced by
those fixes, two pre-existed and were merely relocated, one is deferred.
Services / transport
E-10 retireWorker synthesized no bg/exit frame, so sharedspice's s_bgRunning
mirror stayed latched true after a mid-run worker death: Run stayed
disabled and the promised fresh-worker restart was unreachable for the
whole session. Retirement now dispatches a synthetic controlled-exit
straight to the installed handler (never through dispatchEvt — a
fabricated frame must not touch the credit ledger). Driving the repro
exposed two further defects, both fixed here: a replacement worker
trapped on pre-init engine reads, and the rerun's cm_input_path/circ hit
that uninitialized engine before KiCad's validate() re-init (the native
flow assumes a crashed engine survives in-process — true for the dll,
false for a dead worker). Reads now answer their empty shapes pre-init,
writes lazy-init, and init is idempotent per worker engine.
E-19 dispatchEvt acked only AFTER handler(evt) returned, and the sharedspice
client deliberately rethrows non-trap errors — so each throw leaked one
unit of the 64-frame credit window until the stream died with a
misattributed "transport exceeded". The ack moves to a finally in both
service copies; the throw still propagates (the trap machinery needs it).
E-20 the oversize-line path promises to transfer the accepted prefix, but
with the window full that flush only DEFERS, and stopEventStream wiped
the deferred queue — losing the diagnostics that explain the failure.
The terminal notice now carries them as pendingEvents; both hosts
deliver them in order, unacked (the fatal frame is outside the credit
protocol).
E-21 the 30s prefetch deadline discarded every model already collected and
reported nothing. A caller-owned progress sink ships the partials and
the omission reaches the export report. (Awaiting the aborted collection
was rejected: an in-flight source fetch is not abortable — E-4's
original disease.) Plus a serving-candidate memo, so a .wrl ref served
by its .step fallback stops re-probing the miss on every export.
Scheduler
E-14 _terminalizeNativeTrap classified by message substring, so any plain JS
error QUOTING 'Aborted(' or 'out of bounds' permanently bricked a
healthy instance. Now structural only: instanceof RuntimeError plus a
duck-typed name check (verified in this build's glue that abort() throws
a genuine RuntimeError both pre- and post-runtime-init). Module.onAbort
now latches the gate — the authoritative notification, previously
ignored.
E-15 the shim half: _pumpResume gates on terminal (catching wakes already
queued at latch time) and resolveWait refuses on terminal WITHOUT
consuming the entry, so a frame stays visibly parked rather than
resuming inside a trapped module.
E-16 the E-5 handler read the realm-global scheduler at dispatch instead of
its installing module's; also frees the per-line buffer on the non-trap
rethrow path.
E-11 get_vec trusted the worker's res.length over the transferred arrays.
Observed death shape: a 4 GiB std::vector threw an unhandled
std::length_error that exited the editor's main loop. Now clamped, with
the buffers freed on every failure path.
Guardrails (replacing two deferred refactors: e2e→production-code injection and
collapsing the four copies of the worker-lifecycle machinery)
E-18 the source contract asserted comment-string counts — rewording failed
CI while moving a guard outside its #ifdef passed. It now parses the
#ifdef regions and asserts on code.
service-stub-parity.ts pins what the four lifecycle copies must share:
credit-window equality parsed from source, the finally-ack, boot
deadlines, terminal-notice consumption. The transport numbers are now
single-sourced from the worker.
CI actually runs the gates: the web/standalone vitest suites (which had
NEVER run in CI), the reducer, the source contract and the parity tool —
with a NON_PLAYWRIGHT_GATES check so deleting a step re-fails the lint.
E-22 the e2e occ stub's 60s boot watchdog, deleted in a66e109, is restored in
the ngspice-stub shape with a wedgeNextBoot() repro hook.
Every behavioral fix has red-then-green evidence (the reds were captured first).
E-17 (a stale RUNNING cross-stamping the next run's generation under E-6's
transport deferral) is DEFERRED with its analysis recorded — a real fix needs
run identity on the bg frames.
Test hygiene: the dwell lint now requires the mandated ": <why>" and all 47 bare
markers carry their reason; three export-report dwells became modal-lease polls;
exact-ledger assertions became relative deltas; the dead data-wx-dom-id branch,
an unused fault hook and unused receipt plumbing are gone; abort scans, wx
dialog drivers, the sim harness and the vitest FakeWorker are each one copy now.
Bumps kicad and wxwidgets to their findings-group-e tips.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Coverage audit found three fixes without a test that could catch their
reversion:
- E-6 ack-on-enqueue: new unit case "acks a pre-handler queued frame at
enqueue so the transport window never starves" (ngspice-service.test.ts;
false-green audited — fails with the ack removed, exactly one ack per
frame, none repeated on drain).
- E-5 / E-8 wiring / E-9 / E-7 shape: new source-contract tripwire
tests/tools/findings-e-source-contract.ts (npm run findings-e:contract) —
the codex-thread contract style for C++/EM_JS code that cannot be
behaviorally unit-tested without a wasm build. Asserts: the module
identity stamp + self-disarm and ABSENCE of the install-once presence
guard (E-5); all four completion sites use runWaitCompletion, no stub
resolves a wait directly, token<=0 bails, the shim exports the gate
surface (E-8); the run-generation mechanism present with its behavioral
drops confined to the wasm build (E-7); the identity-checked destructor
unregistration (E-9).
Also bumps kicad to the E-7 ifdef-confinement commit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
E-5: the module-identity bridge called installingModule._malloc, but this
build exposes _malloc only as a bare glue-closure export (Module._malloc is
absent) — every char/stat event entry threw TypeError, which also starved
the E-6 credit window (thrown dispatches never acked) and wedged the queued
bg-finished frame behind them. The bridge now uses the bare closure exports
(identity is still exact: the EM_JS body IS the installing module's closure;
the __ngspiceOnEvent self-disarm covers supersession).
E-6 (codex reference design corrected — its validation matrix never ran):
a FULL credit window was terminal (stopEventStream at 64 in-flight frames).
Under live e2e that killed a real simulation: bg-thread emissions proxy one
per task, so each line ships as its own frame and a normal transient outruns
a busy main thread. A full window now DEFERS into a bounded FIFO (512 events
/ 4 MiB) drained in order as acks free credit; only true overload or an
invalid ack is terminal. Retention stays bounded (8 MiB in flight + 4 MiB
deferred + 1 MiB open batch). And the service/harness mirror queue now acks
at ENQUEUE — placing a frame in the bounded pre-handler queue is taking
ownership; without that, a stream starting before the C++ handler installs
(the ngspice-probe page) starves the worker window forever.
Test updates: worker-batch reducer — new "a full credit window defers and
drains in order, never terminal" case pinning the regression; the storm case
now proves the deferred caps are the terminal edge. board-ready.ts gains the
owner-free openBoardProgrammatically (codex helper the ported occ-export
spec needs; the barrier-based waitForUiBoardReady was NOT taken).
occ-export.spec: domId is optional on this line's registry (coordinate
fallback is the supported path).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
E-8 (re-implemented for JSPI — the codex gate is entangled with the dropped
execution owner; under JSPI a fresh non-suspending JS→wasm entry while
another activation is suspended is structurally safe on its own stack, so
the admission boundary for worker completions is liveness + trap state, not
execution ownership):
- jspi-scheduler.js grows `terminal` (trapped instance; distinct from `dead`),
canTouchNative(), _terminalizeNativeTrap() (WebAssembly.RuntimeError +
cross-realm string classification), and runWaitCompletion(site, token,
prepare, inertResult): prepare runs immediately and owns ALL native work;
stale tokens and dead/terminal instances drop loudly without resolving
(resolving would resume the parked frame inside the damaged module); a
trap latches terminal; a plain JS bug resolves inertResult so the wait
fails instead of stranding. beginWait refuses (token 0) when dead/terminal.
- all four delayed completion sites route their native work through the
gate: 'OCC export completion' (exporter_step_stub), 'OCC model completion'
(oce_plugin_stub — the MEMFS cache write moves inside the gate too),
'ngspice request completion' and 'ngspice vector completion'
(sharedspice_client — every HEAP32/HEAPF64/malloc write inside prepare,
inertResult 1 = transport error). Every wxWasmBeginWait caller in the
stubs bails on token <= 0.
- deliberately NOT ported from codex: ownerModule, enqueueNativeCompletion,
executionBarrier, the byte-credit native-entry FIFO — completions are
one-shot per wait token and stream volume is bounded at the E-6 transport
credit window. Cross-refs logged for group M (M-2/M-6/M-8).
E-5 (re-implemented; codex shape kept, owner APIs replaced with the E-8
gate): js_ngspice_install_events binds the handler to the EXACT installing
module (handler.__pcbjamNgspiceOwnerModule stamp; presence is not identity),
re-installation replaces a foreign module's handler, a superseded handler
disarms itself, native entry goes through installingModule._malloc/
._pcbjam_ngspice_event (never lexical Module), each dispatch checks
canTouchNative() (loud drop on a dead/terminal module), and a trap on the
per-line entry latches the terminal gate.
Tests: scheduler-shim.test.ts +7 (gate happy/stale/dead/terminal/cross-realm/
js-bug/beginWait-refusal). e2e specs updated from the codex line: occ-export
decode-fault recovery (real onmessageerror transition via failDecode, J-4),
ngspice-probe direct-service coverage, eeschema-sim rewritten onto the E-7
applied-generation receipt (codex's executionBarrier await replaced with a
pendingWaits('ngspice') drain poll — the JSPI-line equivalent).
Also bumps the kicad submodule to the E-7/E-9 commit (dd5751038f7).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adapted from codex/asyncify-execution-owner-core 3753320. Service side mirrors
the occ-service shape (E-1 watchdogs, E-2 fail-all + boot-death fix — onerror
now rejects the in-flight boot waiter instead of stranding it, E-3
onmessageerror terminal, Blob URL revoked, per-generation evtQueue cleared on
retirement).
E-6 transport bounds (worker hunks re-applied inside the emscripten-6
em-pthread else-branch — the codex file predates that split, so this is a
re-application, not a cherry-pick):
- batch cut at 512 lines / 1 MiB exact JSON-UTF-8 bytes, measured before a
line is retained; a single line > 1 MiB flushes the accepted prefix then
stops the event stream terminally (never retained);
- posting gated by a 64-frame / 8 MiB unacked credit window; each frame
carries { eventSequence, eventBytes } and is released only by an exact
{ sequence, bytes } ack; any mismatched ack is terminal;
- the service mirrors the same 64-frame / 8 MiB bound on its pre-handler
queue, acks after handing a frame to __ngspiceOnEvent, and retires the
generation on invalid credit; { fatal } frames retire the worker.
Tests: ngspice-service.test.ts (11, ported) — watchdogs, crash/bootError/
decode-fault settlement + recovery, out-of-order ids, sync postMessage throw,
stale-generation event drops, fatal-frame retirement. tests/tools/
ngspice-worker-batch-unit.ts (node:vm over the production worker source;
`npm run ngspice:worker-batch`) — bounded ordered chunks, byte-pressure
flush, 100k-chunk credit storm, over-limit line, exact ack lease. e2e harness
twin updated to speak the ack protocol (adds __ngspiceServiceTestHooks).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adapted from codex/asyncify-execution-owner-core 3753320 (scheduler-free on
that branch already; one comment line re-worded for the JSPI line):
- E-1: generation-slotted WorkerSlot with a 2-min boot watchdog (armed before
resolveWasmBase, so a hung delivery lookup expires too) and a 30-min
per-request response watchdog; retireWorker() is the single idempotent
funnel (fail that generation's pendings, terminate, revoke the worker Blob
URL, clear timers/listeners).
- E-2: worker.onerror is wired for the worker's whole life and settles every
in-flight STEP/export request; a synchronous postMessage throw settles its
request without leaking the pending id; late frames from a retired
generation are inert.
- E-3: worker.onmessageerror retires the generation like error does.
- E-4: collectBoardModelFiles is a pure source/IDB/network path (no editor
MEMFS round-trip) taking an AbortSignal checked at every loop head;
prefetchBoardModels races it against a 30 s deadline — timeout is non-fatal
(export proceeds without models) and late results are inert.
Tests: occ-service.test.ts (7, ported) — boot/response watchdog expiry,
crash-settles-all, bootError retry, decode-fault retirement (invokes the real
onmessageerror transition, per J-4), hung-prefetch export; models-bridge.test.ts
+3 — abort inertness, zero FS access on the collect path. e2e harness twin
updated to the same generation shape (adds __occServiceTestHooks/failDecode).
False-green audited: 19 cases fail with the fixes reverted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up to the sync-delete fix: with the doc corruption gone, the remaining
in-session staleness class is the passive watch itself. It re-pulls only on
`touched` control frames, and the gateway debounces those 2s leading-edge
with NO trailing emit — a dropped frame (real across gateway/DO hops, nearly
impossible on localhost) leaves the MEMFS mirror pre-delete until an
unrelated later edit. Field symptom: "delete + sync doesn't delete, but
reloading the pcbnew tab and syncing again does".
The sync gesture always brings the pcbnew tab to the front first, so on
window focus / visibilitychange every live sibling watch now sends a manual
SyncStep1 (new gateway-only `YjsProvider.repull`), rate-limited to one per
2s; any news restages through the normal debounce.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MLPKYptXFrxHj5Gu7rhToz
download.savannah.gnu.org 502'd through all retries and took out the
2026-08-31 staging CI run. Try savannah, then its mirror pool alias, then
SourceForge — every candidate must hash to FREETYPE_SHA256, so a mirror can
only supply the byte-identical tarball.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MLPKYptXFrxHj5Gu7rhToz
Field bug (2026-08-31): copy-pasting a symbol whose pins kept their source
uuids corrupted the sheet's ydoc on delete (pcbjam-shared: cross-parent
re-key fix, bumped here); the pcbnew tab's sibling mirror then silently
froze on the dangling refs, so "Update PCB from Schematic" with delete
enabled never removed the deleted symbols' footprints — until any later
edit resynced the sheet.
- standalone sibling-restage: render tolerantly past dangling item refs
(docToFile onMissingItem) and console.warn on drops/failures instead of
only the gated debug log — a frozen mirror is no longer silent.
- pcbnew_embind flushDiff: dirty roots that are already STRUCT_DELETED emit
forced removals on both wires instead of being P-5-skipped (observed: 8
footprints deleted on the board stayed in the board room forever); they
are erased from the next baseline so a redo re-adds them.
- eeschema_embind blobFor: lift an unlifted child (field/pin/sheet-pin) to
its screen root before serializing — the selection writer emits nothing
for such a child standalone, so the entry used to reach JS as an empty
envelope and be skipped, silently dropping the edit; residual empty blobs
now warn instead of shipping hollow envelopes. SCH_MARKER_T (ERC
artifacts, never file content) stays out of the snapshot and seed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MLPKYptXFrxHj5Gu7rhToz
Canvas-only eeschema sessions (read-only viewers, hide-UI editors) could not
reach the wx hierarchy pane, so a viewer of a hierarchical design was stuck
on the entry sheet. Adds the React stand-in (SheetPanel) + the bridge:
- eeschema_embind: kicadSheetsGetTree() (SCHEMATIC::Hierarchy(), page-number
ordered: instance KIID path / parent / Sheetname / file / page / depth) and
kicadSheetsEnter(path) (validated synchronously, applied on the coroutine
via SCH_ACTIONS::changeSheet — same route as the wx pane, history intact).
OnSchSheetChanged pushes window.kicadCollab.onSheetsState after EVERY
navigation, wx-driven included, so the panel is event-driven.
- kicad → 9ab93b838e: navigate actions allowlisted for read-only viewers.
- SheetPanel.tsx: LayerPanel shell conventions; rendered + menu row only when
the hierarchy has >1 sheet instance; viewers boot it collapsed.
- tests: fixtures/demo/hier (root → Power, IO → Sub) + web/sheet-panel.spec
(list/order/depth, navigate + collab rebind, menu toggle/close, flat
schematic has no panel, bridge-driven navigation updates the panel);
SheetPanel.test.ts for the wire parser.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtN6ASBvMGNbjPqY5boycg
Anonymous open of a public project (read-only viewer): boot saw no writable
lib, POSTed createLib("My Symbols"), the session gate 401'd, and the throw
escaped the listLibs try/catch — both sym/fp lib tables were seeded EMPTY for
the session (staging: /tg44/projects/arduino/... Arduino Leonardo.kicad_sch).
- ensureWritableLib(): skipped for readOnly sessions; a failed create is
logged and swallowed, never propagated (the listed libs stay seeded).
- WasmTool passes readOnly through to boot.
- unit tests for the skip / create / already-writable / rejected-create paths.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtN6ASBvMGNbjPqY5boycg
CI web legs (both engines, run 33212413159) selected the footprint but 'r'
never rotated it; the keyboard chain is gated on the kicad harness variant
(green on CI), so an unexercisable precondition here is a skip with a PROBE
line, not a P-1 failure.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvvARNMxQrq17vogn3REMU
CI firefox read 0.228 header diff against the Chromium reference while its own
dark and light renders were pixel-identical — pure Firefox-vs-Chromium AA, not
a theme leak. Add the firefox light render as wizard-04-finish-headless-firefox.png
and pick the reference by browserName.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvvARNMxQrq17vogn3REMU
On a saturated runner the canvas lags the viewport past the click budget
(local full web sweep: 'click-select of the first footprint' timed out); the
spec gates the rotate → flushDiff → fit chain, not hit-testing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvvARNMxQrq17vogn3REMU
- P-1 (findings-p.spec P-1c red on CI + locally): a JS exception thrown from a
window.kicadCollab wire callback does NOT reach the C++ catch(...) — under
JSPI it REJECTS the promising coroutine entry, slot.done never flips and the
apply slot is busy for the page's life. Three layers: libcontext flags the
rejected record dead (kicad → d0afbc4bd3),
drainApplies() reaps a !done && !CanResume() body, and every EM_ASM
window.kicadCollab.onX(...) call is try/catch-guarded (collab_common.h,
kicad_editor/eeschema/pl_editor embind).
- popup.spec 'dismisses on outside click': wx P-4 SetFocus delegation excluded
for wxPopupWindow (wxwidgets → a7436d1052).
- ysync-two-tab concurrent seed: repairLayoutY + seedNonce retraction deleted
both headers (pcbjam-shared → 8dabf2e).
- dark-mode.spec: stale light-mode reference (pre wx fa46f1822c DOM
re-projection) replaced with the CI chromium render.
- items-wire-batch-loss.spec: precondition obsolete after P-5 — poisoned entry
is now synthetic, plus a P-5 contract assertion (field blob == '').
- presence-ghost-peer.spec: skip early when no /parties/ socket (CI web e2e
runs the BroadcastChannel provider); findings-p1.spec: polled click-select
precondition.
Local: kicad-chromium findings/presence/ysync/collab 59/59, popup 7/7,
dark-mode 1/1, pcbjam-shared vitest 249/249.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvvARNMxQrq17vogn3REMU
lint:determinism failed the staging deploy (run 33200086355) on 5 lines in
the presence specs committed 8/28: overlay-repaint dwells before settledShot,
the best-effort #canvas focus, and the sampling cadences of the two explicit
presence-eviction poll loops. All are intentional; annotate them the way
3d-viewer-*.spec.ts does so the guard accepts them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvvARNMxQrq17vogn3REMU
security-audit-v3 #15. download_file already had a verify branch; no caller
used it and every *_SHA256 in versions.sh was a commented placeholder, so a
tampered mirror tarball flowed straight into configure/make and the shipped
WASM.
- versions.sh: 13 pins (cross-checked against Homebrew/Buildroot/nixpkgs/
FreeBSD/vcpkg/boost.org/curl PGP; glm .zip is TOFU), boost/curl/libgit2
versions moved beside their pins.
- all 13 download_file call sites pass "${NAME_SHA256}".
- download_file refuses an empty or malformed pin (PCBJAM_ALLOW_UNPINNED=1
to bootstrap a new dep); file_sha256 prefers sha256sum, falls back to shasum.
- scripts/deps/check-pins.sh: static 3-arg check + offline file:// enforcement
test; runs in wasm-build.yml before the deps cache, on cache hits too.
Expect one cold --build-deps run: the deps-cache key hashes versions.sh.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GcsgJZ77bhZatLAVU8R84H
- GatewayDocFacade: passiveSync (Step1 on subscribe + on touched, answered by
the gateway from at-rest state), onReset; activate() after a passive fill
still sends act + a participant Step1.
- sibling-restage subscribes passive+passiveSync; reset drops the watch (no
flush) and re-dials while a peer still has the sheet open.
- files-watch: upload/job hints on room-backed paths restage + announce
onRoomBackedChanged; sheet-manager.invalidate() drops a parked room.
- kicad-binding: normalize server-serialized bodies on the editorMatchesDoc
path (runner-seeded ydocs carry kicad-cli's serialization).
- pcbjam-shared -> 0f4d3a1 (reset control, kicadDocToYdocUpdate).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQsdqaX57xzcCWUjqP7ctV
- wasm core: CORE::onDocChanged() from both collab listeners (local commit AND
remote apply) repaints peers' shapes from the live document + re-checks the
local selection post-settle; PresenceStart registers the bridge listener
- wasm core: cursors on their own overlay trio; shapes repaint only when the
non-cursor snapshot changes; new kicadCollabSetRemoteCursors (cursor-only
update, PEER.id) in both TUs + merged editor + JSPI mutator allowlist
- presence.ts: size-aware trailing throttle for cursor/viewport publishes
(128 KB/s budget) + parsed-peers memo; presence-kicad.ts: cursor-only push
when the shape signature is unchanged
- gateway.ts: honor the `gone` control (removeAwarenessStates)
- specs: kicad stale-after-delete gate, web ghost-peer timing (+diag), unit
select-all budget (pcbjam-shared → 4573a7c)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AScTR39aqyrY5i3ZFHmnMn
syncProjectToMemfs rethrew the first rejected fetchBytes whatever file it
was, so one missing/unreadable sibling (a .kicad_sch body gone, a Q-1
phantom row, a transient 5xx) took the whole board open down behind
"download failed (404)". Now only the TARGET's failure rejects; siblings
are logged, counted and reported through onStatus, and KiCad reports a
missing sheet itself. kicad-runner.test.ts pinned the old contract with a
non-target file — corrected to a target; new kicad-runner.findings-q.test
gates both branches.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9KFksoViNPYYs1ygkcAoQ
- R-9: tests/kicad/project-sync.spec.ts gains an effect-asserting test —
stages a self-contained footprint lib (ecc83 R_Axial as SyncFixture.pretty
+ absolute-uri fp-lib-table via a new stageAndOpen beforeOpen hook), presses
Update PCB, and asserts the board's footprint references through
kicadCollabSnapshot/kicadCollabTestItemBlob (R777, then R888 after a
schematic rewrite + re-sync). Red-first: the dialog report said "Add R777"
while the board stayed empty when the button click never landed.
- R-2: tests/kicad/via-snapshot-assert.spec.ts — two-via board, exact widths
+ no PCB_VIA::GetWidth wx assert line in the console.
- R-4: tests/web/console-copy.spec.ts — Ctrl/Cmd+C over a console selection
fires `copy` (guard stops the keydown before wx); canvas pointerdown
collapses the selection. ControlOrMeta: headless engines use the HOST copy
accelerator regardless of the device UA.
- R-3: workers/cdn gets a vitest harness (package.json + lockfile) and
test/index.test.ts with a workerd-like stub bucket (always-defined range):
plain GET is 200/no Content-Range, Range → 206, HEAD/304/404/405/OPTIONS.
Mutation-verified (pre-1ea35f7 gating → 2 reds). CI step added after the
corpus lint; node_modules gitignored.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015j8uFzSjwRrAeJ85QWVLQa
- P-5: blobForItem returns "" for PCB_FIELD_T (the file writer cannot format a
field standalone → hollow (kicad_pcb …) envelope); liftBlob skips empty blobs
and STRUCT_DELETED roots with a console breadcrumb; the scalar added path and
the seed never attach an empty blob.
- P-1 (not reproduced, structural): the runOnCoroutine apply body runs in
try/catch(...) so an unwinding body still marks the slot done instead of
wedging fit/pan/flushDiff/remote applies forever; new probe
kicadCollabTestApplyQueueState() → {busy, queued}.
- P-4: wxwidgets 9666a743bc (TLW SetFocus delegates to a child,
menubar never takes focus); probe kicadTestFocusWindow() (wx FindFocus).
- Specs: tests/kicad/findings-p.spec.ts (P-1 ×3, P-2 probes as fixme, P-3 ×2,
P-4, P-5), tests/web/findings-p1.spec.ts (P-1 on the real standalone).
P-2 verdict: stock KiCad candidate priority, text scales normally; P-3 not
reproducible — see docs/features/findings/groups/P-editor-tools-embind-interaction.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WmnMPkjugyGqe4LvUsBB14
- tests/kicad/footprint-chooser-confirm.spec.ts (O-2): typed filter + Enter
confirms the chooser and a footprint is placed.
- tests/kicad/infobar-dismiss.spec.ts (O-3): real click on the older-version
infobar close glyph dismisses it; GAL rect shift logged.
- uipolish app/spec: rounded-neg-radius (O-1 guard), enable-propagation (O-3),
dom-nav-keys Enter/ArrowDown → CHAR_HOOK with TEXT_ENTER once (O-2).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3cyLvFWbfNdCNW7x7C2A
- kicadShow3DViewer embind (pcbnew-only name, registered in the unguarded
section so the merged kicad_editor image carries it): runOnCoroutine →
ACTIONS::show3DViewer.
- Session menu: "3D viewer" row (pcbnew) — the only 3D entry once the wx
chrome is hidden. Runs the deferred model prescan first.
- Read-only sessions: skip the boot-time enableRealtime scope-room socket
(would 401 for non-members) and defer the board's 3D-model prescan until
the viewer is opened (deferBoardModelPrescan / runDeferredModelPrescan).
- kicad submodule → read-only allowlist for the 3D viewer actions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXCntRWNn89M72EkrqMkvc
tests/kicad/grid-editors-typing.spec.ts: Symbol Properties fields grid —
types into the canvas-drawn STC Value editor and the wxComboCtrl-based
Footprint editor, commits via OK, saves and reads both values back.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKyjp6N4BeYb1qZVJemPFN
wx submodule bump: DOM controls re-projected when a frame's client origin
moves (menubar attached after toolbars). Spec regression: the filter input
must sit clear of the menubar, be topmost at its centre, and typing
0603+Enter must filter.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ws9hayc7nLHHMdZhwcjUGX
Collab: a save-all's layout-only sync into a never-entered sheet room left a
doc with layout but zero items; the first entry adopted it and removed every
item on screen. seed() now file-seeds a hollow doc, syncLayoutFromSave skips
never-seeded empty rooms, and the ydoc-boot / sibling-restage paths fall back
to the file. Tree: new tree-hier standalone app + e2e spec covering the
wxwidgets bundle-size and unfocused-selection fixes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0189z2siCsezE39ugcDKNBMs
- kicadLibsInvalidate: a peer's edit only drops the lib's plugin entry + pcbnew's
PreloadedFootprints (the cache the old reload never cleared — tree/preview/
LoadFootprint/update-from-library kept serving the old body); the fat re-load
now runs lazily or from Update-from-library (kicadLibsReload, which also
clears the preloaded cache)
- usage bridges count the Footprint/Symbol Editor's open copy; update re-opens
an unmodified copy, reports a modified one
- embind TU gets eeschema/symbol_editor on its include path; smoke probes
- kicad → 27051b46e2
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Wd1r3ewftpV1DBSEArpRa
wxwidgets bump: main-window DOM controls join the input barrier when a
secondary window overlaps them; secondary windows/GL canvases anchored to the
.window.toplevel border→outline.
Tests (TDD, red on the old wx.js): standalone secondary-frame app (main-frame
wxChoice under a secondary wxFrame's AUI toolbar) + e2e/secondary-frame-input
spec (fall-through hit-testing, click delivery, barrier follows drags), and
kicad/3d-viewer-toolbar-hijack spec (viewer at top-left over pcbnew's combos —
the user-reported repro). modal.spec border assertion updated to the outline
ring.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI's lint:determinism rejects blind waitForTimeout. Convert the chromium
clipboard-content read to expect.poll (the write lands after a JSPI
suspension) and annotate the remaining waits as documented interaction
dwells — select-all/paste/commit resolve inside the wx tool framework with
no page-observable, and the test's real gate is the FS-save poll.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cmd+C/Cmd+V in the wasm eeschema pasted a stray "(" SCH_TEXT — the stroke
font renders it as a small blue arc — instead of the copied symbol, and any
copy reached navigator.clipboard as a single character. Root cause and fix
live in wxwidgets (90bbad29822: UTF-8 wxDF_UNICODETEXT + the port's missing
GetAllFormats key function; c1f14775ba3: empty-read cache fallback + no
browser pre-clear on copy). This bump carries them plus the guards:
- tests/kicad/eeschema-copy-paste.spec.ts: chromium asserts the full
multi-form (lib_symbols …)(symbol …) blob reaches navigator.clipboard;
both engines assert copy → paste → save yields a second symbol and no
stray (text …) item (firefox exercises the m_textCache fallback path).
The kicad-chromium project now grants clipboard-read/write. NB the kicad
projects' device UAs claim Windows, so specs must send plain Control+…
— ControlOrMeta resolves to Meta on a mac host, which the "Windows" app
ignores, and the bare key fires eeschema hotkeys instead.
- tests/e2e/textdataobj.spec.ts + Makefile.wasm target `textdataobj`:
drives the wx-repo harness app (wxwidgets/tests/wasm/textdataobj_test.cpp)
and asserts the SUITE-DONE failure counters plus the full-string browser
round-trip, sentinel-seeded so a failed write cannot read as stale success.
- tests/e2e/clipboard.spec.ts: the Copy test now asserts the full input
text reaches the clipboard (previously truncated to "S").
Verified locally: all specs red on the old wx, green after; wx-chromium
320/320, kicad-chromium 121/121 (collab specs env-gated).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same FOOTPRINT_EDIT_FRAME and the same wx-layer focus rule; opening a second
editor from a board session never became ready on CI (Chromium 180 s, Firefox
footprint load >60 s).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Wd1r3ewftpV1DBSEArpRa
In ydoc mode the collab room owns a document's state — items reach it at
commit time, layout heads at save time (syncLayoutFromSave). The raw CAS
PUT on Ctrl+S was only ever the file-list registration + fallback copy,
but for a file the listing already marks hasYdoc/isLive it bumps the
revision for nothing, can 409 the target into the durable save-blocked
banner, and leaves a shadow row the ydoc supersedes on every read.
registerSaveHook gains `uploadPolicy(relPath) → "upload" | "room"`;
WasmTool marks the boot listing's ydoc/live files "room" (ydoc mode only).
Files with no row yet (created sheets, the synthesized .kicad_pro) still
upload; onSavedText still runs for room-backed saves.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Wd1r3ewftpV1DBSEArpRa
The staging editor was built with neither --models-tag (no models on the
staging CDN) nor VITE_MODELS_SOURCE, so modelsSourceConfig() returned null,
no model3d handler was installed and every 3D ensure answered "(unserved)"
despite the chunked packages3D ingest being complete on the staging registry.
- build-editor.mjs: --models-source <cdn|registry> → VITE_MODELS_SOURCE
- deploy-staging.yml: editor builds with --models-source registry
- release.yml: prod editor builds with --models-source registry (libs/0016 §6
step 4); MODELS_TAG now only feeds the demo build, which has no closed API.
Prod rollout order: split-lib-kinds + packages3D ingest on prod BEFORE the
next release tag — prod's registry currently has 0 model3d libs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1iwtBFvQ8CFg966bqSZh2
The blit's one-time setup leaked its quad-VBO GL_ARRAY_BUFFER binding
(not VAO state); the raytracing->OpenGL switch-back reload then captured
client-memory gl*Pointer addresses as offsets into that 48-byte buffer and
baked them into the recorded display lists — every frame INVALID_OPERATION,
viewer blank until canvas recreation. Found via a bounded routed-draw
diagnostic on the live repro; verified by driving two full real-Chrome
round-trips (back-to-OpenGL frame hashes byte-identical to pre-toggle).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Switching OpenGL → raytracing → OpenGL could leave the viewer showing only the
background gradient, with mid-session "[gl1] WebGL context changed" thrash and
INVALID_OPERATION storms on BOTH WebGL contexts. Traced mechanism: the shim's
FFP draw routing keyed on one process-global client-array flag; an interrupted
fixed-function window (MODEL_3D::BeginDrawMulti loops, or the switch-back
reload re-recording display lists with the GL context lock released across
JSPI suspensions) left it set, after which the raytracer blit's and the 2D
GAL's glDrawArrays were routed through the FFP pipeline — and one misrouted
draw permanently repointed the VICTIM's own VAO attributes at shim buffers
(the blit's attribute 0 collides with ATTR_POSITION), so both stayed
broken/blank even after the flag cleared.
Shim fixes (wasm/gl1):
- Owner-context routing gate: __wrap_glDrawArrays/Elements route into the FFP
pipeline only under the shim's owner context (adopted at the first FFP
client-state mutation or programSync in a context); foreign-context draws
always pass through — the 2D GAL can never be misrouted and the context
guard can never thrash.
- VAO isolation (ScopedDefaultVAO): draw executors do their attribute setup on
VAO 0 and restore the caller's binding — a misrouted draw can no longer
corrupt the caller.
- contextSync() resets the whole client-array mirror on a context change
(enables/pointers/VBO names all described the dead context).
kicad pointer bump (d6e3dc1a87a): blit preamble disables the four client
arrays (same-context firewall) + DoRePaint hidden-parent early return now
clears m_is_currently_painting like its six siblings (a standalone
sufficient cause of a permanently blank viewer).
TDD (each observed red before its fix, green after; harness = authoritative):
- T1 VAO corruption, T2 foreign-context routing + guard thrash, T3 stale
client-state surviving context recreation — tests/e2e/3d-webgl.spec.ts over
new harness choreography (appQuad/ffpMakeStale/createSecondContext/
quadDrawFresh). Parity stays 47/47 with zero drift.
- tests/kicad/3d-viewer-engine-toggle.spec.ts (new, CI-skipped like the
deadlock spec): real round-trip happy-path gate — board re-renders, zero
[gl1] lines, zero INVALID_OPERATION. (The raytraced image itself never
displays on the wasm build — the pre-existing inert-toggle KNOWN ISSUE in
3d-viewer-deadlock.spec.ts, out of scope here; the engine switch and the
poisoning reload path run regardless.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Files staged from the project sync namespace bundle (stageViaProjectSync)
never passed through fetchFileBytes, the only place baseRevisions was set,
so their first save PUT carried expected revision 0 and 409'd against any
row ever re-saved — e.g. assigning a footprint (CvPcb → eeschema root save
also writes .kicad_pro) failed with "Save conflict … (local base 0,
server 1)".
Add ProjectSource.rememberBaseRevision + api.rememberFileBaseRevision,
DriveOptions.onStagedRevision reported by stageViaProjectSync with the
listing revision, and wire it through WasmTool from ToolPage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Wd1r3ewftpV1DBSEArpRa
A bare expect(false).toBe(true) after the 90s poll hid WHAT diverged in the
drift-trio S4 same-item races (CI 2026-08-24 ×2, local under 15-worker
contention 2/21). The failure now reports each tab that differs from A and
the first differing model line, so the next divergence self-documents
(memory: s4-value-race-divergence — never re-quarantine without the diff).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AH2iPekUGsEAYnMUD5BmAi