4 flakes in 6 CI runs on 2026-08-01, all with clean traces: no guard beacons,
A demonstrably landed (the 208cb67 sequencing gate passed), B just never got
the one-shot rect applied within 30s while two wasm instances starved the CI
box. Awareness re-broadcasts state periodically, so a 90s condition-poll
converges on re-delivery; local runs stay fast (converge in <2s).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
kicadTestFiberParkStartSecond/PokeSecond: a second coroutine started while
the first body is asyncify-parked reproduces the misattributed jump that
launders the parked fiber past the C++ guard (the v0.1.21 prod bypass).
Spec scenario 2 stages it and asserts the JS stale-rewind guard quarantines
the laundered resume (exactly one fiber-resume-refused beacon), the parked
body completes undisturbed, and both coroutines finish cleanly.
Doc: async/16 rounds 2 + WSOD section.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
All three prod crash cascades (v0.1.19–21) ended white for the same reason:
the final trap lands inside a child's EFFECT (an embind call reached through
a react-query subscription), React unmounts the entire root, and the fatal
overlay + console panel die with the tree they were built to survive.
- WasmErrorBoundary: crash-capable children live inside it; the fatal screen
and the console panel live OUTSIDE and keep rendering. WasmTool state
(logs included) survives a descendant render/effect throw.
- The fatal screen is now an actual blue screen (:( + solid blue), and every
promotion path (window error, unhandled rejection, worker error, boundary)
auto-opens the console — the log is the only account of what was loading.
- tests/web/fatal-overlay.spec.ts pins the contract: a terminal uncaught
error after boot ⇒ visible fatal overlay + open console with the [fatal]
record. Green locally (23.9s).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
"B never tracked the second viewport move" ate three v0.1.21 release
attempts (2026-08-01) while the same build passed 10/10 locally, including
under 4-worker contention. The T2 block asserted B's convergence without
first confirming A's second fit LANDED — the T1 block has that sequencing
point, T2 didn't — so on a starved CI box any A-side fit delay was
misreported as a B-side tracking failure, and B's 30s window burned while A
was still moving. Mirror the T1 pattern: poll A onto its target, then start
B's clock.
Verified 4/4 green locally (web-chromium, 2 workers). Reminder that cost 20
minutes tonight: web/standalone/.env poisons local web e2e (untitled/title
mismatches) — stash it before local runs (standalone-env-e2e-poison).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
Companion to kicad f0ce20ef64 (libcontext swap_suspended guard), which this
pins. The v0.1.20 diagnostics decoded the crash that survived v0.1.13–19:
TOOL_MANAGER Resume()s a coroutine whose body is asyncify-parked inside
handleSleep, the swap rewinds the stale fiber suspension, and the runtime is
poisoned. Full chain of evidence in docs/features/async/16-fiber-resume-guard.md
(+ round-3 addendum in 15-timer-park-repro.md).
- wasm/bindings/fiber_park.h + kicadTestFiberPark{Start,Prime,Poke,State}
exports (pcbnew + merged kicad_editor): stages Call→yield→legitimate
resume→sleep park→mid-park Resume, the exact prod state machine. The
first yield matters: it primes a real (then stale) suspension, matching
long-lived tool loops rather than a first-slice park.
- tests/kicad/fiber-resume-park.spec.ts: asserts the healthy contract on
polled state only (embind returns across fiber swaps are unwind
placeholders). RED on the unguarded build — fiber/sleep buffer
cross-restores, a jump-ghost beacon, the parked body zombified. GREEN with
the guard: mid-park poke refused ([collab-fcontext] jump-refused beacon),
park completes, post-yield resume works, no trap signatures.
- Regression sweep green: timer-park-repro, collab-load-fuzz, load-pcb,
pcbnew-collab, collab-undo, eeschema-collab (19 passed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
kicadTestArmTimerPark(delayMs, parkMs): a one-shot wxTimer whose Notify()
emscripten_sleep()s, entering through the exact GAL-refresh-timer path
(emscripten_async_call → TimerCallbackFunc::Run → dispatch guard → Notify) —
the fresh-entry-that-parks the prod board-load trap family needs. Pollable
kicadTestTimerParkState(); inert unless armed. Registered beside
kicadTestSetOpenPark in pcbnew + the merged kicad_editor image.
tests/kicad/timer-park-repro.spec.ts drives four escalating cycles (park
only, 2× + fiber hammering, + 256MB heap growth mid-park) and asserts the
runtime survives every rewind AND that the [wx-asyncify] diagnostics observed
the window — engagement is asserted, so a run where the lever never created
the overlap cannot pass vacuously.
Result so far (docs/features/async/15-timer-park-repro.md): GREEN through
both rounds — genuine double-parks, live currData cross-restores, fiber
swaps, and mid-park heap growth are all handled by the shim + runtime. The
prod trap needs an ingredient this window still lacks (ranked in the doc);
the spec stays as the regression gate for whatever the eventual fix is.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
The onItems handler let unwrapWireItem's throw unwind through embind into the
C++ emitter — a bare pageerror, the whole batch lost, and flushDiff already
rebaselined so the dropped items could never be re-sent. Field-seen case:
Update PCB from Schematic emitted 67 changed entries, one an item-less board
envelope (pcbnew writes nothing for a standalone footprint field); all 67 were
dropped and two new footprints existed only on the syncing tab.
The conversion now skips un-resolvable entries per-entry (shared items-wire
fix), every conversion site warns via warnSkip, and the handler body is
wrapped so nothing escapes into the C++ caller again. The tests/web spec
drives the real serializer's empty envelope through onItems and holds that a
good entry batched with it still reaches the peer (proven red before the fix).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019daWLdW5xrRhjUvCUWoSAe
- kicadOpenFile now holds wxWasmDispatchGuard (open_gate.h). It enters through
embind, so the interlock read "nothing parked" for the whole load and wx timers
dispatched into the half-built board — the residual prod "index out of bounds"
that survived the settle gate.
- new wasm/bindings/gerbview_embind.cpp (the bundle had no embind surface at all):
kicadOpenFile / kicadOpenFiles / kicadOpenFileBusy. Clicking one gerber opens the
whole fabrication set in its folder, since a lone layer is not a useful view.
- cross-app presence rejoins in the boot fan-out (network-only; the wasm-bound half
still waits for the open to settle) — it had been pushed behind the board load.
- tests: gerber-set selection units + a gerbview multi-file open e2e.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0137pGo8W7asomGUTRMB7RzM
Self-asserting wx app (tests/apps/standalone/uipolish) with 8 checks:
clip-clear / clip-empty / clip-box (DC clip box reaches the canvas — the
collapsed wire-properties-panel bug), blit-origin (wxBufferedDC device
origin), mask-alpha (ConvertToImage carries wxMask — infobar close button),
scaled-dims (physical size for scaled bitmaps), checkbox-floor (selection-
filter density) and statbmp-best (bundle logical size — layer-panel icons).
The spec runs a default-DPR pass plus a deviceScaleFactor:2 pass that also
asserts the statbmp <img> ships the 32px asset at 16 CSS px.
Bumps wxwidgets for the CloneGDIRefData SOURCE_NONE fix the @2x pass
surfaced (empty statbmp data URL on hi-DPI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mh188aysvgeaTRYh6syztQ
The WRL→STEP migration notice auto-dismisses after 10 s
(pcb_edit_frame.cpp ShowMessageFor); when the timer fired between the
pre-hide GL-box baseline and the post-restore comparison, the canvas
shifted by the bar's height and the ±3 px restore-exactness check could
never pass (CI 30289317464, web-firefox — pure timing, the wx dark
form-control bump on that run was innocent). Gate the baseline on the
notice being HIDDEN — its DOM node outlives the dismiss, so absence
never happens.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y32etYBCKV6t1qDoLoGmgF
kicad bump: re-apply only the nav hunk of 3dcfea5e45 (the 7/20 backout
92f18ef4ed was aimed at the WebGL GAL recovery/flush but took the nav
with it, breaking web/tool-switch.spec.ts on both engines since).
eeschema-sim spec: the Run tool's ENABLE(!simRunning) is a wxUpdateUIEvent
condition the WASM port only re-evaluates on input events — after a run
finishes the toolbar can hold its stale "running" state past the 60s
poll (CI 29846684031: run finished, Run still disabled). Nudge the mouse
inside the poll so the condition re-evaluates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APzCH3oxjLrZk6Nxepvczz
Symbolized (HOIST_KEEP_NAMES=1): the trap is on the asyncify REWIND re-entering
the fiber — stack-local COROUTINE+body were destroyed when Call() returned
early on a park, so the rewind called through freed objects (latent UB in the
ORIGINAL fire-and-forget runOnFiber too). Heap-pinned FiberSlot + explicit
done flag + fiber-tail re-drain. Layer 2 (rewind interplay) still open —
fuzz stays fixme'd; pageerror stacks now captured in fuzz artifacts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5cAM9M6q34n5X4dbrfVvi
runOnFiber now runs bodies strictly one-at-a-time through a park-safe FIFO
(collab_common.h): the per-body fire-and-forget coroutine interleaved under
load — an asyncify park inside commit.Push let the event loop start the next
body, so a local commit and a remote apply ran interleaved on shared state
(s_applyingRemote is one global), silently losing applies on the actively-
editing receiver (fuzz finding #10a; B now fuzzes clean; 39-test suite green).
kicadCollabFiberBusy embind probe (merged + standalone registrations): a
bare-embind-stack scratch save during a parked fiber mis-dispatches (table
index OOB) — trio.ts modelText/drift and production drift-detect now defer
while fiber work is in flight (#10b hardening; the trap's root cause is still
open and needs a symbolized stack — fuzz stays fixme'd).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5cAM9M6q34n5X4dbrfVvi
drift-trio-fuzz.spec.ts: mulberry32-seeded weighted concurrent actions on A+B
(C observes), K=40, marker-synced oracle sweep every 10 steps, per-tab console
capture, and a replayable failure artifact (seed + action log + drift/saves/
renders/consoles) dumped to logs/kicad/drift-trio-fuzz/. DRIFT_FUZZ_SEED
replays a sequence exactly.
First catch (0008 §10 #10, OPEN): the actively-editing receiver's C++ apply
silently drops ops under sustained bidirectional load — JS dispatched every
apply, the editor never applied some; pure observer and emitter stay clean;
one run hit a wasm 'memory access out of bounds' on the observer. Timing-
dependent, so the fuzz tests are test.fixme'd (an expected-fail would flake
CI) and run manually via DRIFT_FUZZ_SEED until the fix lands (phase E).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5cAM9M6q34n5X4dbrfVvi
drift-trio-scenarios.spec.ts: disjoint ping-pong, same-item interleave (no
settle between bursts), conflict pairs (move-vs-delete / value-vs-value /
move-vs-move — winner is CRDT policy, asserted only as convergence + drift
silence), undo storm, 12-edit burst churn, late-joiner adopt, and Ctrl+S mid
peer burst; per-tool adapters, marker-waits before every sweep (finding #7).
S4 exposed finding #9: mutation hooks resolved item pointers at call time and
committed later on the fiber — a remote remove in between frees the pointer
(doApplyItems) and the commit resurrects the deleted item. Phase-B mutation
hooks now re-resolve by uuid ON the fiber; a vanished item makes the mutation
lose silently. applyDeltaToY's concurrent update/delete verified coherent
(full resurrect or full remove) — no shared change needed. trio.ts: TabSet
oracles + exported startV2 for duo/late-join composition.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5cAM9M6q34n5X4dbrfVvi
17 kicadCollabTest* action primitives (eeschema: wire/junction/no-connect/
label/symbol/move/mirror/duplicate; pcbnew: track/via/text/zone/flip/
footprint-field/lock/move/duplicate), each a real SCH_/BOARD_COMMIT on the
fiber so the listener → flushDiff emit runs as for UI edits; tool-unique
names, merged-image safe. Fixes surfaced by the catalogs (0008 §10 #4–#8):
eeschema adds SetParent before staging (Push silently skips listener
notifications for unparented items), and pcbnew blobForItem now Formats
non-footprints with the FILE writer + wrapInBoardEnvelope — SaveSelection's
transfer copy cleared the locked flag, so (locked yes) never reached the doc.
drift-trio.spec.ts gains full A/B-alternating catalogs with per-step landed
gate + oracle sweep. Bumps kicad for the Duplicate child-uuid re-roll.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5cAM9M6q34n5X4dbrfVvi
Comments showed the author's SLUG — which doubles as their personal scope
— instead of their name, because WasmTool passed presenceUser().id where
.name already existed.
- denormalize authorName/authorEmail onto each comment message + thread at
write time, so a comment still reads correctly when its author is
offline, renamed, or gone. `author` stays the SLUG: colorFor() and the
"is this mine?" ownership checks compare it. Legacy comments fall back.
- session-identity keeps the email it was already fetching and discarding.
Overlay menu: restyle + reorganise into labelled sections (People /
Document / Comments / View) over one shared row shape, so sections can't
drift apart again.
- comments: four bare icons -> labelled rows (Add comment / Show list with
count / Hide pins). The nested expand toggle is gone: the section IS the
group, so it was a collapsible inside a collapsible.
- presence: facepile of initials -> one row per person with an explicit
Follow/Stop. The separate "Following X" banner is deleted; that state now
lives on the person's own row, so there is nothing to keep in sync.
- SourceChip gains a `muted` tone for known-dark surfaces; its solid
variant is untouched for the light project pages that share it.
Fixes a regression the redesign introduced: the taller panel (z-50) covered
the comment popover (z-40), and since a popover can be opened FROM the
menu's thread list, the menu swallowed clicks on the popover it had just
spawned — delete was unreachable. Popover now z-[60].
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016p9kjdGBdcpwUSjJ3q5xg2
The Y.Doc is the source of truth for the FILE, but both live wires wrote
KiCad's CLIPBOARD dialect — a lossy, paste-oriented format. Every
difference was permanent, unfixable drift.
- pcbnew: serialize footprint blobs with CTL_FOR_BOARD, not CLIPBOARD_IO's
CTL_FOR_CLIPBOARD, which emitted (version)(generator)(generator_version)
inside every (footprint …). Keep (locked yes).
- eeschema: aForClipboard=false — clipboard mode collapsed every symbol's
(instances … (path "/sheet")) to (path "").
- Re-supply (version) at PARSE time only (withFootprintVersion): the token
is invalid file content but load-bearing on decode — without it the
parser starts at m_requiredVersion=0 and stamps (hide yes) on every
mandatory field.
- FOOTPRINT copy ctor: restore mandatory-field uuids (EDA_ITEM::operator=
keeps the target's const m_Uuid, so Clone() rerolled all four).
- drift: classify order-only diffs as `reordered` — y-sexpr v2 reorders
legitimately; excluded from counts, report-worthiness and dedupe hashes.
Migration 0017.
- fpedit from eeschema: AsyncLoad()+BlockUntilLoaded() in initLibraryTree —
FACE_PCB starts lazily there and never preloaded its libraries.
Guards: wire-vs-file round-trip tests (pcbnew + eeschema),
fpedit-from-eeschema (verified red without the fix), symedit-from-eeschema.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016p9kjdGBdcpwUSjJ3q5xg2
The ~1/9 wasm trap on CvPcb open ("index out of bounds" / "indirect call to
null" in a footprint AsyncLoad pool worker, then eeschema aborting on the
broken future — and the eeschema-fp-selector "CI-only" trap family, which was
never llvmpipe-specific) was wxString's UTF-8 build mutating SHARED strings on
read-only access from concurrent pool workers: every iterator ctor/dtor
spliced an intrusive list inside the string object, and torn splices wrote
through dead node pointers into other threads' stack frames. Second defect:
the UTF-8 position cache returned stale offsets when another thread's string
died and its address was reused.
Fixed in the wxwidgets fork (per-thread iterator registry + position cache
disabled under Emscripten) — kicad is untouched and AsyncLoad keeps its full
multi-worker fan-out. Falsified along the way (all perturbation masks, not
fixes): serializing the items, mimalloc vs dlmalloc, pthread stack size,
ASYNCIFY_STACK_SIZE, private-copy EnumFromStr, hot-path logging.
New red-first standalone app tests/apps/standalone/wxstring-mt (+ spec
coroutine-wxstring-mt.spec.ts, wx-chromium + coroutine-firefox): shared-string
compares alternating with wide-literal conversions reproduce the exact editor
trap signatures on the unfixed wx and run 4.7M rounds clean on the fixed one;
the pos-cache address-reuse dance corrupts on the first reuse before and
survives 673 after; two guard modes keep the iterator fix-up feature honest
(incl. an anti-elision liveness check — balanced register/unregister pairs in
tight loops can legally be optimized away, so a naive red test tests nothing).
Verification: wx+coroutine suites 382 passed; in-app AsyncLoad hammer 3x1000
rounds clean (baseline died <10); eeschema-assign-footprints spec 20/20
firefox + 20/20 chromium on the final build; lint:determinism clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rb9jsqtHsC3tHTaJ45244j
The spec's boot gates (canvas, registry, title) are all wx-side and go
live seconds before WasmTool drops its opaque boot overlay (ready only
flips after the collab seed + waitForWxUi). The Tools click landed on
the overlay, the popup never opened, and the menu-item wait timed out —
flaky on CI (the 1s title poll sometimes samples after overlay unmount),
100% locally. Add the inset-0.z-30 count-0 guard the chrome-toggle /
tool-switch / quit-* specs already use.
Verified: web-chromium 5/5, web-firefox 8/9 (was 0/5, 0/1). The one
remaining firefox failure is a distinct pre-existing wasm trap on the
CvPcb-open path (EVT.MENU dispatch → "index out of bounds" in a worker),
same signature as the eeschema-fp-selector CI trap — now known to
reproduce on macOS real GL too. Left as a TODO(cvpcb-open-trap) in the
spec; needs a debug-symbol repro.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rb9jsqtHsC3tHTaJ45244j
The eeschema symbol-chooser footprint-selector test was marked
test.fail() on CI for a software-GL wasm "index out of bounds" trap.
That trap no longer fires (SwiftShader retired + serial-inline footprint
load fix), so the test passes on both engines and the expected-fail
marker was flipping the run red ("expected to fail, but passed").
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The three page.waitForTimeout calls in eeschema-assign-footprints.spec.ts
are legitimate interaction dwells (two poll intervals that pump the wx
event loop inside asserting loops, one first-paint settle before a
screenshot), but were unannotated, so the determinism guard failed CI.
Mark each with a `dwell:` note per tests/TESTING.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Board Setup -> "Import Settings from Another Board..." -> browse opens three
nested wx modals; a click inside the top file dialog leaked to the Import
Settings dialog beneath it (its checkbox toggled). New Playwright e2e repro
asserts the click no longer reaches the shadowed modal, with a sanity phase
proving it does toggle when Import Settings is the topmost modal. Lives in
tests/kicad/ so the kicad-firefox/kicad-chromium projects run it on both
engines automatically (no routing list; lint-ci-coverage satisfied).
Bumps wxwidgets to the JS-layer input barrier fix (build/wasm/wx.js) that
stops the click leaking through the shadowed dialog's live DOM controls.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Squash of experiment/ff-big-modules vs main.
Big-module routing removed: native-EH shrank kicad_editor below
SpiderMonkey's x86-64 code budget (runs 29355049705/29356152413 green on
stock Firefox), so BIG_MODULE_SPECS routing and the baseline-only-JIT
crutch are gone — kicad-firefox and kicad-chromium both run the full
suite, with the module compiled the way real users' browsers compile it.
Per-engine screenshots end to end: stableShot/shotPath write
test-results/<engine>/<name>.png; baselines move to
baseline-screenshots/{chromium,firefox}/ and the whole tools/screenshots
pipeline (compare/promote/manifest/spec-map/changelog/Discord) keys on
<engine>/<name>. Previously Firefox and Chromium renders of one spec
overwrote each other and Firefox renders were never actually gated.
Seeded from CI run 29421380806 (92 new firefox baselines, +24 chromium
web-suite shots); manifest generated from the baseline tree.
One merged playwright.config.ts (kicad/asyncify/coroutine/perf as
projects); ~25 dead npm scripts dropped. The web suite is gated in CI for
the first time ever (4 rotted specs fixed, 5 broken lib-bridge specs
triaged as fixme in docs/features/web-e2e-rot/); cheap lint step after
npm ci; last 26 blind-sleep violations fixed.
SwiftShader retired: CI Chromium renders WebGL on ANGLE → Mesa llvmpipe
(--use-gl=angle --use-angle=gl --ignore-gpu-blocklist; the blocklist flag
is mandatory — llvmpipe is blocklisted and WebGL is silently unavailable
without it) in BOTH configs. Under WORKERS=4 congestion SwiftShader
transiently failed the first post-board-load draw and the recovery
cascade ended in a silent permanent Cairo fallback — that engine flip was
the "~1.2% changedRatio both directions" occ-export baseline flake.
Validated 160/160 across two 80-repeat rigs; full analysis in
docs/features/wx-parity-bugs/occ-export-context-eviction.md. Chromium
baselines shift slightly on llvmpipe — promote once from the first green
run. Deflakes the new coverage exposed: presence baselines settle before
capture; presence fixtures declare current file formats; perf gets its
own outputDir so CI evidence survives; occ-export settles the board paint
before the export dialog; menu-item waits (waitForRenderedByLabel before
clickMenuItem) in 4 specs + the TESTING.md rule.
Web suite runs the PROD build, in parallel: webServer becomes backend
`start` + the standalone's e2e:preview (build-preview.mjs: link-wasm →
stash the public/wasm symlink aside during vite build, build-demo.mjs's
move — then vite preview as the persistent server). The wasm middleware
serves /wasm/* in preview and emits COOP/COEP/CORP itself (a pthread
worker script's own response must carry COEP or Chrome kills it with
ERR_BLOCKED_BY_RESPONSE). VITE_* flags bake at build time;
VITE_ALLOW_USER_OVERRIDE joins turbo globalEnv. fullyParallel + default
workers: 5.2m → 1.4m. Determinism fixes the parallel run exposed:
shared-page specs become serial groups; locks.spec grabs alice's exact
item via the new kicadCollabTestSelectByUuid hook (cross-tab "first
footprint" order is not a ysync invariant); quit specs poll page.url()
(quit supersedes its own navigation — NS_BINDING_ABORTED on Firefox).
Suite: 51 passed / 12 skipped / 0 failed in 1.6m.
CI-coverage gate (lint:ci-coverage): every tests/**/*.spec.ts must be
reachable from the npm scripts the workflows invoke — scraped from
.github/workflows/, resolved through package.json, coverage asked from
playwright --list itself. Rules: uncovered-spec + orphan-project (with a
documented LOCAL_ONLY_PROJECTS allowlist). Gating next to
lint:determinism; 138 spec files / 13 projects accounted for.
Product fixes kept from the investigations (reachable on real GPUs too):
wx 7799fd1be5 — paint flags clear before dispatch + Invalidate always
propagates; kicad 3dcfea5e45 — SwiftShader pass-boundary flush +
per-instance font texture + first-frame GL-error drain (GAL recovery
recovers instead of falling back to Cairo) + the user-facing eeschema
switch navigates again under __EMSCRIPTEN__ (project-sync's
FaceRegistered gate had rerouted it into the hidden sync player; caught
by the newly-gated web suite).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eUxiPApHgGiu9NFyQfhAq
Quit used to mimic the Back button (history.back() whenever a referrer
existed), but every in-app entry and every tool switch is a hard
location.assign that pushes a history entry — so after schematic ⇄ pcb
switches, one step back is another editor, not the page the user left from.
Quit now navigates to the project overview explicitly (projectPath; "/" for
lib editors), which also covers deep links uniformly.
New VITE_QUIT_ORIGIN ("" ⇒ same-origin) lets the backed editor deployment
exit to the management app's project page instead of the standalone's own;
build-editor.mjs sets it (override with --app-origin).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tools → "Switch to PCB Editor" is a hard location.assign that stacks a second
editor history entry, so a Back-style quit lands in the previous editor
instead of leaving for the project overview. Red until the quit hook stops
unwinding history.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- defaultKicadPro() template + synthesizeProjectFile() writes a minimal
<stem>.kicad_pro into MEMFS when a board/schematic opens without one, so KiCad
runs a real project (self-heals legacy fileless projects).
- NewFileDialog creates the .kicad_pro companion alongside a new board/schematic.
- sibling-restage.ts: pcbnew sessions subscribe to sibling .kicad_sch collab rooms
as invisible observers and re-stage MEMFS on update (restageFile extracted from
syncProjectToMemfs), so a sync reads live schematic data, not the boot snapshot.
- tests/kicad/project-sync.spec.ts (2 e2e) + sibling-restage.test.ts (5 unit).
- Bump kicad pointer (SCH-from-PCB sync gate + hidden player + .kicad_pro save hook).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdqhM7j71XQn96hcHkabDu
One circular FAB (peer-count badge) replaces the top-right overlay row:
draggable anywhere (pointer capture, 4px click-vs-drag threshold — the
comment-pin pattern), position persisted in localStorage, panel opens
toward screen center and stacks composable sections — roster, source chip,
view-only pill, follow row, comments, chrome toggle. Renders at z-50 above
everything (decided: covers wx dialogs; trivially dismissed via click-away/
Esc/drag). CommentLayer portals its bar + list panel into the menu's
comments slot; pins/popovers/composer/click-catcher stay canvas-anchored.
The FAB is the chrome-hidden survivor. e2e specs updated with an
openOverlayMenu helper (panel is closed by default, click-away closes it).
NOTE: Playwright suites updated but not executed here (no local WASM
artifacts) — verify on CI / next session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A42xfPFNdfsUt9eowkC9eM
presenceUser()/userSlug() now resolve the authenticated user: a plain
/api/me fetch (GPL no-link rule — no closed contract import) races the WASM
boot and is awaited before presence/comments bind. `?user=`/`?libowner=`
overrides are gated behind VITE_ALLOW_USER_OVERRIDE=1 (dev script + e2e
harnesses set it; prod builds never do). Anonymous/example-backend sessions
keep the pre-auth slug fallback. Fixes prod's empty presence (everyone was
"local-user", deduped as own tabs) and comment authorship in one move.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A42xfPFNdfsUt9eowkC9eM
Tools → "Switch to PCB Editor" in a project with no .kicad_pcb (e.g. created
from a lone schematic) did nothing: the nav hook found no file for the target
tool and returned false. Now a session that can persist (ToolPage passes the
new createFile prop) writes the templated counterpart via
createProjectFileIfMissing — no download fallback, and it re-checks existence
so a collaborator's file is never clobbered — then navigates to it, matching
native KiCad (pcbnew opens a new board at the derived path). Sessions that
can't persist (read-only viewers, scratch/local-folder) keep the logged no-op.
Also latch the quit dispatcher off before every deliberate tool-switch
navigation (markDeliberateNavigation): the wx port's UnloadCallback runs on
BEFOREUNLOAD and closes the top frame the moment the navigation starts, so
the quit hook history.back()'d over the in-flight navigation — the pagehide
latch is too late (it only fires at commit time).
e2e: new tests/web/tool-switch-missing-file.spec.ts reproduces the flow via a
browser-local (IDB) project created from the home page; playwright-web config
gains VITE_LOCAL_PROJECTS=idb and derives STANDALONE_PORT/CORS_ORIGIN from
WEB_APP_URL (runs the suite past a squatted :3048); both vars declared in
web/turbo.json globalEnv (turbo strict-env strips undeclared vars). Includes
the previously-uncommitted tool-switch spec repairs (URL grammar + z-30
boot-overlay wait).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
T4: kicad-binding.test.ts — revertNonce → DOC_REVERTED_EVENT dispatch via a
window stub (once per nonce; stale marker on open silent; observer gone
after destroy). T6: tests/tools/cli-contract.ts (npm run tools:contract,
skip-when-unbuilt): resave version bump + relint clean for board/schematic/
hierarchy/footprint (the CTL_FOR_LIBRARY version-header regression), exit
codes 2/4/1 — the exact contract run-tools-job.ts keys off. 15 checks
green. Also bumps pcbjam-shared (T3 unit tests).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HLua64PCVwkQ1hpWdaf1Gm
The quit hook only history.back()ed on a SAME-ORIGIN referrer, so the
primary closed-deploy entry — app.pcbjam.com deep-linking into
editor.pcbjam.com (cross-origin) — fell through to the editor's own
project view instead of returning to the app. Quit is Back-button
semantics (3a2255c): any non-empty referrer with in-tab history now
goes back; deep links / fresh tabs (no referrer) keep the project-page
fallback. Verified live against the closed stack: :3047 → editor →
quit lands back on :3047; a direct editor URL still falls back to the
editor's project overview.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
720cff5 added occ-export-models.spec.ts (boots the ~190MB merged pcbnew
module, prefetches 17 model bodies, runs a 13MB OCC STEP export in the
occ_service worker — twice, serial) but never listed it in BIG_MODULE_SPECS,
so on CI it ran on the firefox project: headed under Xvfb with the
baseline-only WASM JIT and llvmpipe. That CPU storm ran concurrently with
the chromium-ci pool and starved the SwiftShader 3D-viewer opens past their
180s cap — 3d-viewer.spec:299 (edge-resize) timed out at openThreeDViewer in
runs 29076702564 (main) and 29090433643 (PR), and 29076702564 also dropped
occ-export.spec:105's menu click. In both red runs the last console line
before the 180s silence is the misrouted spec's export.
Add it to BIG_MODULE_SPECS like every other pcbnew.html-booting spec: it now
runs on chromium-ci (V8, headless SwiftShader) and firefox ignores it
(verified with CI=1 playwright --list).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VYxyBRJnKKExFcA5CRPXjM
The occ_service export worker has its own MEMFS — the editor's lazily-fetched
lib models were invisible there, so every export was a bare board (54
"Could not add 3D model" warnings on pic_programmer, 2 STEP products).
- models-bridge: collectBoardModelFiles(boardText) — scan refs, ensure via
the 0004 sparse source (IDB/R2, wrl->step fallback), read staged bytes
back, dedupe by real staged path.
- occ-service.ts: attach the collected models to every export request
(best-effort — prefetch failure still exports, misses reported by the
exporter); transfer the body buffers.
- occ-worker.js (shared app/harness): pass req.models through to occExport.
- occ_service_main.cpp: occExport(board, params, models) stages each entry
under PCBJAM_3D::MODELS_MEMFS_ROOT (path-sanitized) for the exporter's
staged-model probe (kicad 83645275ac), removed again after the export.
- tests: harness occ stub mirrors the prefetch against the page kicadLibs
provider + captures report/productCount; new occ-export-models.spec.ts
guards the delivery (green companion pins preconditions; guard asserts 0
missing lib models + component PRODUCTs). pic_programmer: 17/17 staged,
87 products @ 13.3 MB (was 2 @ 402 KB). models-bridge unit tests 13/13.
Known remainder (0007 step 4): project-local ${KIPRJMOD} refs still drop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UjpnviP3ZDxTM1Ap63Sqv
emitViewportIfChanged deduped on scale+center only, so a canvas SIZE change
(boot layout settling after the bind-time seed, window resize) never re-pushed
{w,h} to JS. CommentLayer's worldToScreen maps through h/2, so every DOM pin
hit target (and its hover ring) sat vertically offset from its GAL dot by
exactly delta-h/2 css-px until the next pan/zoom finally passed the dedupe.
Size now participates in the dedupe and wxEVT_SIZE re-pushes post-layout
(CallAfter, after the GAL's own onSize). New regression e2e
comments-viewport-resize.spec.ts asserts the DOM pin re-aligns with GAL truth
(fresh kicadCollabGetViewport) across a window resize with no pan/zoom.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x3h5AzkWDbHmCkeVYAuzU
Follow-user: click a peer's roster avatar to mirror their viewport until
local input breaks it.
- collab_presence_core.h: CORE::fitViewport(cx, cy, halfW, halfH) — fit the
leader's world rect with CONTAIN semantics (zoom derived from the
follower's own canvas via the ToScreen ratio; GetScale is the zoom, not
px/IU). Exported as kicadCollabFitViewport from both editor TUs + the
merged dispatcher.
- presence-kicad.ts: publish the visible world rect (viewportRect) into
awareness, 100 ms trailing throttle; guarded for pre-0008 handles.
- follow-user.ts: createFollow — follows an awareness CLIENT (a tab, not a
user); applies leader rect changes via FitViewport, dedupes unchanged
republishes; break-on-interact compares local onViewport echoes against
the last applied rect (2% rel tolerance, echo-grace before the first fit
lands); unfollows on leader-left; pauses on eeschema sheet mismatch.
- PresenceRoster: avatars are follow toggles (ring on the followed peer);
WasmTool renders the "Following <name> — move to stop" banner.
- tests: 7 controller units (85/85 collab), fitViewport round-trip e2e in
both kicad presence specs (20/20), two-tab tests/web/follow.spec.ts
(converge → track → wheel-zoom breaks → subsequent moves ignored).
Chip depth-layer fix (user-reported): name chips washed out inside
low-alpha selection fills — chip rects shared the shapes overlay's single
depth, and same-depth fragments drawn LATER lose the depth test, so an
earlier-painted fill rejected the chip's pixels. Now three layers via the
fork's VIEW_OVERLAY::SetDepthOffset: text (0) < chips + pin dots (1) <
selection shapes (2). drawLabel/drawCursor/drawSelectionBox take the chip
overlay explicitly; comment-pin dots move to the chip layer too (the 0005
"drawn last so pins sit above" comment had the rule backwards). Verified
with a chip-inside-30%-fill pixel repro + the full presence suite.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG