Commit graph pcbjam/tests
Author SHA1 Message Date
Gergő Törcsvári
c80622a860
wx wasm: grid cell editors typeable (wxwidgets → d32535fefb) + gating e2e
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
2026-08-27 14:42:37 +02:00
Gergő Törcsvári
9575498c9e
cvpcb: footprint filter box drawn too high and dead to clicks (wx fa46f1822c)
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
2026-08-27 13:47:07 +02:00
Gergő Törcsvári
8fa6bae084
fix: hollow sheet rooms wiped subsheets on entry (ysync 0011) + hierarchy pane icon sizing/selection colours
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
2026-08-27 08:59:01 +02:00
Istvan Matejcsok
d7cb570f07 fix(wasm): 3D-viewer toolbar clicks hijacked by hidden main-frame DOM controls — input barrier + canvas anchoring
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>
2026-08-26 13:19:34 +02:00
Istvan Matejcsok
4b92175c4e tests: eeschema-copy-paste — satisfy the determinism guard
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>
2026-08-26 12:16:57 +02:00
Istvan Matejcsok
6034a0550d eeschema copy/paste fix: wx UTF-8 text dataobj (c1f1477) + regression specs
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>
2026-08-26 11:21:00 +02:00
Gergő Törcsvári
fcad12d251
tests: fpedit-cmd-save — drop the from-pcbnew variant (CI timeouts), keep the standalone frame
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
2026-08-26 09:05:37 +02:00
Gergő Törcsvári
866db5888c
libs 0017: sync overrides indexed + stale-lib session menu + Cmd+S DOM-focus fix + WasmTool split
- kicadLibsFootprintUsage + kicadUpdateFromLibrary embinds (result via
  pcbjam:lib-update-done — runOnCoroutine is deferred)
- standalone: stale-lib FAB triangle + session-menu Update-from-library row,
  save busy notice names the item, footprint placed-usage in the toast
- WasmTool.tsx split: module helpers → components/wasm-tool/
- specs: save-cmd-key (Meta+S, mac UA), fpedit-cmd-save (DOM-focus repro)
- wxwidgets → cdd5a5c (wxDomBlurActive)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Wd1r3ewftpV1DBSEArpRa
2026-08-25 17:58:54 +02:00
Istvan Matejcsok
daaff1f973 fix(3d): blank viewer after raytracing round-trip — owner-context FFP routing + VAO isolation (gl1)
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>
2026-08-25 11:25:34 +02:00
Gergő Törcsvári
cf28e3be82
tests(trio): settleConverged names the diverging tab + first differing line
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
2026-08-24 19:35:52 +02:00
Istvan Matejcsok
7a88399a60 fix(dialogs): centre unpositioned dialogs — repair the origin-placement regression from the display-index fix
The display-index fix (wxDisplay::GetFromWindow returning 0) disabled
DIALOG_SHIM::Show's accidental always-Centre() fallback, so every
unpositioned dialog opened at the port's literal (0,0) wxDefaultPosition.
CI caught it: the eeschema text-properties dialog opened under the
just-clicked canvas-centre point, its OK/Cancel corner swallowed the
trailing mouse event and the dialog insta-closed (run 32739091966 — the
dialog exists in exactly one trace snapshot; passes locally where the
dialog finishes opening after the click sequence).

wxwidgets bump: wasm wxDialog::Show now centres any dialog still at (0,0)
on first show — the native default-placement parity native ports get from
the window manager. Restores the exact pre-regression on-screen placement
(and thereby every dialog screenshot baseline).

New red->green test: eeschema-ui.spec.ts "text properties dialog opens
centred, not at the top-left origin" — clicks away from the dialog's
centred footprint (so no engine can reproduce the swallow), asserts the
dialog window div is viewport-centred; was at (0,0) pre-fix, (332,171)
post-fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 17:50:21 +02:00
Istvan Matejcsok
3722891d48 fix(3d): blank render + lost position after 3D viewer close/reopen (gl1 context guard)
Closing the viewer destroys its wxGLCanvas's WebGL context; reopening mints a
new one. The gl1 shim cached GL names (FFP program, stream/scratch VBOs) in
never-reset statics behind `if (!handle)` guards — in the new context every
draw died with INVALID_OPERATION and the viewer showed only the clear color
("Reload time 0.031 s" is benign: warm model caches make the rebuild fast).

contextSync() (gl1_state.cpp) now detects the context change in programSync()
— the one choke point every shim draw crosses, and a path the 2D GAL never
reaches (a first attempt checking in the glBindTexture wrap saw the GAL's
context and thrash-rebuilt the program 23x per run) — and drops the cached
names for lazy rebuild in the new context. Context identity is a monotonic id
stamped on Emscripten's per-context record: the numeric
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE is recycled, so a destroy-then-create can
return the same number and a handle comparison detects nothing.

The lost-position half is a wxwidgets wasm fix (pointer bump: GetFromWindow
reports display 0; saved geometry used to carry display=(unsigned)-1, which
LoadWindowState treats as "display not found" and re-centres the frame).

TDD (red observed before each fix, green after):
- tests/kicad/3d-viewer-reopen.spec.ts (new, own worker like the deadlock
  spec): load board, open viewer, render-gate, drag by the titlebar, close
  via the x, reopen; asserts the board re-renders (was: 1 distinct colour for
  90 s) and the window position is restored (was: re-centred to 0,0 after
  closing at 40,90). Green run logs exactly one [gl1] context-change line.
- 3d-regression harness: recreateContext() destroys the context AND swaps in
  a fresh canvas element (a browser canvas keeps its context for life, so
  same-element recreation hands back the live old context and hides the bug);
  the new 3d-webgl spec test renders redraw-mini-board-navigator before and
  after recreation and requires pixel-identical output. Parity: 47/47, zero
  drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 16:18:22 +02:00
Gergő Törcsvári
90bc4e0222
batched upload resave: --resave-batch CLI + one staging pass per bulk upload (tasks-runner 0004)
kicad_tools --resave-batch <outdir> <file>...: N resaves in one process
(one WASM init — marginal resave is ~50ms vs ~0.3s/process). Outputs in
<outdir>/<index>/, per-file "RESAVE-BATCH <index> <exit-code>" stderr
verdicts with the single-file 0/4/5 contract; exit 0 = loop completed, so
an invalid file mid-batch can't mask its neighbors.

loadSchematicHeadless now binds each schematic to ITS project via
SETTINGS_MANAGER::GetProject, never Prj(): with aSetActive=false projects
accumulate across batch entries and Prj() keeps returning the first one —
the writer then stamped a wrong/empty project name into saved
symbol-instance data. (UnloadProject is no fix: unloading the active
project immediately reloads a null "" project.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AH2iPekUGsEAYnMUD5BmAi
2026-08-24 14:46:43 +02:00
Viktor Vaczi
f419a1fedd build+perf: wasm-opt the shipped wasm, and measure real frames in CI
emcc only runs Binaryen at link -O2+ (link.py: should_run_binaryen_optimizer
returns OPT_LEVEL >= 2) and we link at -O1, so the shipped module had never seen
wasm-opt at all — it kept its entire 19.56 MB name section, ~20% of the editor
(-sJSPI sets ASYNCIFY=2, which suppresses wasm-ld's --strip-debug, leaving
wasm-opt as the only thing that would drop it). Step 8.2 runs it post-link and
in-container, so CI's cached compile phase covers it and the host post-process
stays pure-host.

Default -O2, picked by measuring every level on the same module: -O0 already
captures 27% of the raw win (it is mostly the name section), -O2 costs 23 s and
gives the best frame rate, and -O3/-O4/-Os/-Oz cost 48-132 s for at most 1.5%
more brotli — -O4 is not even smaller than -O3. Targets that already link -O2/-Oz
(occ_service, kicad_tools) are skipped by testing for the target_features
section, which emcc strips whenever it ran the optimizer itself, so there is no
hard-coded target list to drift. Feature flags come from the module's own
target_features section and so cannot diverge from the link.

The perf specs reported requestAnimationFrame ticks as "FPS". That is not a frame
rate: rAF fires on the compositor's schedule whether or not the GAL redrew, and
it read 120/s on a board where the renderer completed zero frames in six seconds.
measureInteractionFps now counts completed GAL frames — runs of draws to the
default framebuffer, exactly one per frame in every AA mode — and drives a pure
middle-drag pan after a zoom-to-fit. Mixing wheel zoom into the drive made the
result depend on where the wheel left the view: +-20% across identical repeats,
against +-2% for pan alone. The report gains a GAL fps column with a regression
flag on the 1x number; rAF is kept so historical runs stay comparable.

CI has no GPU, so its number is a software-rasteriser redraw rate — a regression
signal, not a user-facing frame rate. Method and measurements in the bench report.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-22 12:53:00 +02:00
Gergő Törcsvári
3df3468d36
viewer-panels: read-only boots panels open-collapsed; kicad-style layer rows
- Read-only sessions boot BOTH panels open as collapsed headers (a stored
  per-browser choice wins); inspector's default anchor moves to the
  top-left, layers keep the top-right stack under the FAB.
- Layer rows follow KiCad's Appearance-pane order: color swatch, eye
  toggle, layer name (name click = set active).
- read-only spec: boot-default assertions (open + collapsed + top-left
  anchor) in the lock test, chevron expansion in the panels test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011gJ3M1RpeZLeNUUj8jKC4h
2026-08-20 11:49:55 +02:00
Gergő Törcsvári
acc76f65ec
feat: viewer panels — layer selector + selection inspector (viewer-panels)
Canvas-only sessions (read-only viewers, hide-UI editors) get two
floating, draggable, closable panels standing in for the chrome-hidden
wx panes, opened from the overlay menu's View section:

- wasm/bindings: layer bridge — kicadLayersGetState/SetVisible/SetActive
  (bodies mirror the compiled-in-but-unreachable IPC handlers, applies
  on the coroutine lane, fresh state pushed to
  window.kicadCollab.onLayersState); setters join the jspi-scheduler
  mutator lane.
- standalone: LayerPanel + SelectionInspector on the comments-panel
  shell conventions (useDraggablePanel, collapse, persisted state);
  local-selection store fed from presence's onSelection in edit
  sessions and bindLocalSelectionFeed for read-only viewers (with a
  bounded post-gesture pull burst — clarify-menu selections produce no
  canvas event); pure item-summary extraction + unit tests.
- kicad submodule: read-only selection unlock (selection live for
  inspection; point editors + RMB context menus stay locked).
- tests/web: read-only spec updated — viewer click selects (or pops the
  clarify list), RMB context menu suppressed with writer positive
  control, Delete still swallowed; new viewer-panels test (eye toggle
  round-trip, active layer, panel drag, inspector rows from a real
  canvas click).

Record: docs/features/read-only-viewer/0002-viewer-panels.md (root).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011gJ3M1RpeZLeNUUj8jKC4h
2026-08-20 11:29:46 +02:00
Istvan Matejcsok
75bb0d7661 fix(tests): restore the dark-mode spec's light-mode reference PNG
tests/wizard-04-finish-headless.png is a spec-owned reference image the
dark-mode spec diffs against in-process (toolbar must ignore the
browser's dark theme) — it was never a promotable baseline, but the
baselines-to-R2 migration (6dfa6eb) swept it out of git with the rest of
the PNGs, and every e2e run since fails with ENOENT at
screenshot-compare.ts:56. Restored verbatim from 6dfa6eb^ and documented
as deliberately committed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 15:11:39 +02:00
Istvan Matejcsok
91291fde37 screenshots: sweep remaining committed-manifest wording
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 14:35:08 +02:00
Istvan Matejcsok
ec7a1b0787 screenshots: R2-hosted manifest becomes the baseline source of truth (morelli cutover)
The committed tests/screenshot-manifest.json is retired. CI now downloads
baselines/pcbjam/manifest.json (manifest v3, written only by the morelli
review app + its seed script) to the gitignored .baseline-manifest.json,
and everything downstream (pull, verify, compare) reads that copy:

- config.ts: MANIFEST_VERSION 3, MANIFEST_PATH .baseline-manifest.json,
  R2_BASELINES_MANIFEST_KEY; ManifestEntry grows opaque provenance
- r2-sync.ts: new --manifest mode (atomic fetch; no-creds skip DELETES a
  stale copy so the gate skips rather than using old baselines); --push
  gone (bytes enter the CAS only via morelli's promote)
- compare.ts: hard-skips when no manifest was fetched — a stale warm
  cache can never gate
- wasm-build.yml: fetch-manifest step before the baselines cache; cache
  key now hashes the fetched manifest; the gen-manifest --check lint gate
  goes with the committed manifest
- deleted: screenshot-manifest.json, promote.ts, changelog.ts,
  gen-manifest.ts, screenshot-changelog.yml, promote-screenshots skill
- docs (CLAUDE/README/TESTING/WHATWORKS/tools README): promote flow is
  now https://pcbjam-morelli-staging.pcbjam-staging.workers.dev

Validated locally against the real bucket: fetch-manifest (492), cold
pull 492 / warm pull cached=492, no-creds skip chain, compare gate skip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 14:35:08 +02:00
Istvan Matejcsok
4196958c10 ci: upload each run's screenshots + meta.json to R2 (runs/pcbjam/<run-id>/)
Durable per-run screenshot store for the morelli review app
(github.com/PCBJam/morelli): after the report step, CI uploads the renders
and a meta.json index (identity, branch/commit, per-shot sha256+dims,
embedded compare summary) to runs/pcbjam/<GITHUB_RUN_ID>/ — 30-day R2
lifecycle; GH artifacts remain the debugging archive. meta.json is written
last as the upload-complete marker. Uses a new optional WRITE keypair
(CI_SCREENSHOTS_S3_WRITE_*); without it the step no-ops, so secretless
callers stay green. R2Store gains putKey() for the non-CAS run keys.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 14:35:08 +02:00
Istvan Matejcsok
453df4fb9c tests: harden the R2 baseline pipeline's failure paths (review findings)
- compare gate: refuse an INCOMPLETE cache, not just an empty one — a partial
  R2 fetch (continue-on-error step) previously misreported un-fetched
  baselines as ADDED and silently disabled their removed-detection.
- post-discord: a missing report.json now posts "gate SKIPPED", never the
  false " no screenshot drift" — a disabled pipeline must look disabled.
- promote: hard-fail on a pre-migration (non-v2) manifest instead of warning;
  the warn path could commit manifest hashes never uploaded to R2.
- changelog: tolerate per-image R2 failures (skip + loud note, like the old
  git-blob null-skip) instead of aborting the whole post; missing creds warn
  + exit 0 (notification-only workflow); git-show failures are loud errors,
  no longer mistaken for "predates the migration".
- version guard: a manifest NEWER than the tooling throws everywhere instead
  of reading as a silent no-op.
- manifest ordering: locale-independent code-unit comparator in writer +
  checker (localeCompare depends on host locale; writer=dev Mac, checker=CI).
- r2-sync pull: byte-size pre-filter before hashing cached files.
- wasm-build.yml: restore-keys on the baseline cache — manifest changes now
  restore the previous tree and download only the delta.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 09:21:28 +02:00
Istvan Matejcsok
3257ddc966 docs: R2 baseline model in TESTING/README/WHATWORKS + promote-screenshots skill
The three docs still described the committed-PNG model (following them would
trip the new resurrection guard); they now document the fetch-from-R2 cache,
the manifest pin, and the credential requirements. The new /promote-screenshots
skill is the runbook for blessing a CI run's render (dry-run first, RW keypair
from tests/.env, commit only the manifest diff).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 09:21:28 +02:00
Istvan Matejcsok
6dfa6ebd15 tests: move screenshot baselines from git to R2 (content-addressed manifest v2)
Baseline PNGs (492, 35 MB) leave git: they now live in the private R2 bucket
pcbjam-ci-screenshots as immutable sha256/<hex>.png objects, pinned by the
committed screenshot-manifest.json (v2: name, engine, sha256, bytes, dims).
tests/baseline-screenshots/ becomes a gitignored cache materialized by the new
`npm run screenshots:fetch` (r2-sync.ts + aws4fetch r2-store.ts).

- promote.ts: requires the RW keypair (shell env or gitignored tests/.env),
  syncs the cache, uploads new hashes BEFORE rewriting the manifest; the
  manifest diff is the only git-visible output. --prune only edits the
  manifest — R2 objects are never deleted, old commits still resolve.
- compare.ts: skips the gate (exit 0, no report.json) when the manifest
  expects baselines but the cache is empty (secretless callers).
- changelog.ts: diffs the manifest between revs and fetches bytes from R2;
  guards against the migration commit (base manifest not v2 → skip).
- gen-manifest --check: v2 schema + resurrection guard (fails if baseline
  PNGs are ever re-committed); credential-free so every caller can gate.
- wasm-build.yml: declares optional read-only S3 secrets, caches + fetches
  baselines before the lint gate; release.yml/deploy-staging.yml pass them.
- screenshot-changelog.yml: triggers on the manifest path instead of PNGs.

All 492 objects are seeded and hash-verified in the bucket; fetch/compare
degrade to a warn-and-skip without credentials.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 09:21:28 +02:00
Gergő Törcsvári
db84293af1
tests: fix two-tab collab baseline race + build the jspi harnesses in CI
1) pcbnew/eeschema two-tab 'a local move propagates A->B' read the
   pre-move baseline AFTER kicadCollabTestMoveFirst. The move is queued
   through CallAfter + the apply coroutine, and the drain can land
   between two consecutive page.evaluate round-trips — when it does
   (~50% under CI load, reproduced locally with --repeat-each) orig
   captures the ALREADY-MOVED position and the not-toBe poll waits on
   itself. Wire tracing showed the bridge working: the moved delta
   emits, nothing reverts. Fix: baseline from kicadCollabSnapshot
   BEFORE the move. 12/12 green at --repeat-each=4 (was ~50% red).

2) jspi-firefox suites 404'd their harness modules: jspi-stack and
   jspi-coroutine build via ad-hoc build.sh (Phase 3 Makefile wiring
   TODO) which CI never ran. Wire both into build-wasm-test.sh (its
   hash is already in the testapps cache key), and add the _pt pthread
   variant that index.html?pt=1 loads but nothing built. jspi-firefox
   8/8 green locally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmR6goSk7JC17h7fkgGvHG
2026-08-14 21:46:24 +02:00
Viktor Vaczi
e652810de7 bench: jetson FPS battery — both backends saturate at 80.9 MB scale
Parameterizes the throttle-sweep FPS battery (fpsBattery helper) and adds a
jetson-agx-thor run: asyncify raf 2.1-5.3 / distinct 0.2-0.8 vs JSPI raf
1.9-5.0 / distinct 0.1-0.7 — indistinguishable. Suspension overhead is a
per-event-loop-turn cost; at hundreds of ms of GAL work per frame it stops
discriminating. vme-wren (~27 MB) remains the discriminating size class.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 12:07:18 +02:00
Viktor Vaczi
da299ed6f9 bench: JSPI vs asyncify A/B — harness + results
Adds pcbnew-large-perf.spec.ts (PERF_LARGE-gated: repeated cold loads,
vme-wren/jetson opens, rAF + distinct-glcanvas-frame FPS under throttle,
wasm/JS heap checkpoints), fetchIntoMemfs + openAndWait/sampleMemory/
measureFpsDetailed perf-utils, dual 9.99+10.0 config seeding in pcbnew.html
so foreign-branch builds boot wizard-free, and the full benchmark report +
raw data under docs/features/async/migration-evidence/.

Headlines: wasm 94 vs 113 MB raw (18.6 vs 36.7 MB gzip), post-link tail
1.6 s/49 MB vs 63 s/6.1 GB per build, cold load −40 %, 27.7 MB board open
−45 %, real redraws +68 % at 4× throttle, boot heap −31 %.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 11:53:06 +02:00
Viktor Vaczi
9c475a804e jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.

Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
  could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
  (WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
  WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.

Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
  workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
  of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
  scaffolding (existed to parallelize the deleted wasm-opt phase; the
  postprocess is a seconds-long node script and now runs inline),
  build-monitor's dead asyncify rows, sched-context orphan build
  output, dead .gitignore entries, the .jspi-assets spike dir (the two
  wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
  called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
  main_stack_runner.h + 5 includes, the always-null context-sleep weak
  hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
  noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
  WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
  (gerber-demo keeps it: it loads the deployed CDN release, which
  predates emscripten 6 — noted inline).

Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).

Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).

Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).

Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).

Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
Viktor Vaczi
3ee174e9b4 tests: un-skip sweep — 26 tests revived on the JSPI build, failures re-gated with fresh evidence
Empirical pass over every skip/fixme whose premise the JSPI migration
could have changed. Revived (verified green):

- Firefox wasm-budget guards RETIRED (drift-trio, drift-trio-fuzz,
  drift-trio-scenarios, ysync-two-tab, ysync-libsymbols): the JSPI
  build (~half the asyncify size) fits three editor tabs inside
  Firefox 153's per-process budget — +20 firefox collab tests.
- roundtrip 'pcbnew preserves items through a yjs round trip': the
  asyncify-fragile envelope parse it waited on is gone — both engines.
- drift-trio-scenarios S4/S4b: converge now (was KNOWN ~5-8%).
- pcbnew-collab + eeschema-collab 'a local move propagates A→B'.
- web eeschema-fp-selector, read-only-editor's fixme'd writer-stream
  test, footprint-browse-remote read path (chromium; firefox gated:
  FootprintEnumerate rows never appear in 60s — slow wasm tier
  suspected).

Still broken, re-gated with re-verified reasons:

- 3d-viewer raytracer engine toggle: still inert, both engines.
- maximize display geometry: wxDisplay reports 0x0 in the harness.
- web editor WRITE bridge (symbol/footprint × remote/spike): wedges at
  the New Symbol/Footprint dialog step on both engines — the
  web-e2e-rot 01 gap stands for writes.

Verify runs: kicad+wx touched files 93 passed / 0 failed / 7 skipped
(intended gates); web touched files 7 passed / 0 failed / 9 skipped.
lint:determinism + lint:ci-coverage green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-13 18:43:36 +02:00
Viktor Vaczi
db819850ee jspi: fix the dead-tools ownership bug, emscripten-6 fallout, and green the full suite on Playwright 1.62
Live-app fix (Place Footprints / routing dead in Chrome): submodule
bumps carry the coroutine ownership fix (kicad 012d95ecb4) and the
handler-exception survival fix (wxwidgets 1b5f0e31f4).

Emscripten-6 fallout:
- occ/ngspice worker wrappers: mainScriptUrlOrBlob was removed
  upstream; pthread children re-run the wrapper blob, so an em-pthread
  realm now importScripts the glue and gets out of the way (before:
  recursive service boots, pool never fills, silent 180s boot hangs —
  every occ spec and ngspice bg_run).
- Makefile.wasm: -sASYNCIFY frankenlinks on the no-wx coroutine repro
  targets ported to -sJSPI (the JSPI-only libcontext crashed at first
  yield under them); mainloop/gl repro pages drive their tick through a
  promising export (emscripten_set_main_loop callbacks cannot suspend);
  retired inject-dyncall-shims lines removed (targets were unbuildable
  since Phase 8); $stringToNewUTF8 force-included (the EM_ASM value
  bridge aborted the runtime on the first decoded exception).
- fiber-park levers: neither embind shape can drive suspending levers
  (plain throws on strict-JSPI Firefox; emscripten::async() re-executes
  its invoker on settle) — kept sync for manual Chromium probing, spec
  coverage moved to the jspi-coroutine harness (18 cases).

Suite work:
- Playwright 1.61.1 -> 1.62.1 (Firefox 153: JSPI on by default).
- fiber-resume-park.spec retired -> coroutine-lifecycle.spec: census
  gate over boot / board load / chooser open / cancel (deterministically
  red on the pre-fix build).
- Blind asyncify-era pins re-keyed: quasimodal-strand + wait-beacons
  beacon regexes, footprint-chooser-close liveness -> wx parking-timer
  heartbeat (scheduler counters idle flat on Firefox).
- occ/ngspice test providers: 60s boot timeout + worker error
  surfacing (a worker death used to be a silent 180s timeout).
- Harness pages: stale 9.99 config dir -> 10.0 (library_manager wxCHECK
  noise, chooser had no libraries).
- gal-webgl harness: missing artifacts rebuilt (boost/glm extracted to
  the host sysroot), PgmOrNull stub added for the rebased GAL.
- jspi-scheduler: clean-shutdown console line restored (app-quit
  contract), quarantine never yanks SP from a live window.

Gates: test:e2e 699 passed / 0 failed (wx-chromium, kicad-firefox,
kicad-chromium, jspi-firefox, coroutine-firefox); web ff/cr/mobile 71
passed; lint:ci-coverage 166, lint:determinism 163, screenshots
manifest 492 current, corpus 7/7, tools:contract green. Offline
screenshot baselines show expected mass drift from the engine bump —
re-baseline (screenshots:noise -> promote) is a follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-13 17:41:28 +02:00
Viktor Vaczi
e14faeca8b jspi: retire the asyncify pipeline — knob, post-link tail, binaryen hooks
Phase 8 in the parent repo. Deleted: asyncify-scheduler.js, apply-asyncify.sh,
apply-finalize.sh, inject-dyncall-shims.sh, asyncify-imports/removelist.txt,
the wasm-opt/finalize stub pair, scripts/binaryen-hoist-pass/ (the fork stays
a dormant submodule; removal is a follow-up), bench/wasm-opt-bench.sh (README
marked historical), wasm/shims/context_sleep.cpp, and the sched-context
harness app + Makefile targets.

PCBJAM_ASYNC_BACKEND is gone: build-wx-wasm.sh hardcodes the jspi stamp
(still force-cleans pre-migration trees), build-kicad-target.sh gives editors
the JSPI link surface and the CLIs nothing (they pin ASYNCIFY=0), the stub
dance is replaced by an unconditional .real-restore, build-wasm-test.sh lost
its whole post-link loop, docker/build.sh's postprocess is the ENV shim only,
and Makefile.wasm links every app JSPI with the scheduler shim as a tracked
prerequisite. pcbjam_async_policy.h keys on __EMSCRIPTEN__.

jspi-scheduler.js: wxWasmMainLoopPump dropped from the wrap census (the
export died with the D5 detach); inert [TRACE] instrumentation removed.

CI: wasm-build.yml rewritten for the single-cache pipeline (one output cache
keyed on compile inputs; post-processed bytes cached after the shim);
opt_level input removed from both callers. wasm-cache-hash.mjs inputs now
cover patch-env-shim.mjs + jspi-scheduler.js + jspi-exports.txt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDeBaKKhQztd8KiVtHuyXr
2026-08-13 08:39:12 +02:00
Viktor Vaczi
3f09a46ff5 jspi: migration phases 0-7 — build knob, scheduler shim, test successor suite
Toolchain: emsdk 6.0.6 (versions.sh; cache-hash keys on it). Build knob
PCBJAM_ASYNC_BACKEND=jspi|asyncify: build-kicad-target.sh links editors with
-sJSPI + -sJSPI_EXPORTS=@scripts/common/jspi-exports.txt + --pre-js
jspi-scheduler.js (no DYNCALLS, no post-link asyncify pipeline); wx build
stamps the backend and forces clean on flip or unknown provenance;
docker/build.sh passes the knob, seeds the emscripten ports cache from the
volume every launch, jspi postprocess = patch-env-shim only.

scripts/common/shims/jspi-scheduler.js: the JSPI successor scheduler —
token-wait registry, resume turnstile (one armed resume between engine
re-entries, SP swaps only at microtask boundaries), green-region spill
stacks (16-aligned tops), S1 embind mutator FIFO lane + parker wraps, S6
shutdown, libctx integration hooks (suspend/end/quarantine + g_current
arm/clear), SuspendError attributor, lost-wake + stuck-window watchdogs,
__wxWaitDump observability.

Embind: PARKER registrations get emscripten::async() under PCBJAM_JSPI
(wasm/bindings/pcbjam_async_policy.h). nanosleep yields route via the shim.

Tests: tests/asyncify -> tests/jspi successor suite (jspi-stack red/green
shadow-stack battery, jspi-coroutine MiniCoro harness, suspend-races
semantic scenarios + __wxWaitDump books coherence); projects jspi-firefox/
jspi-chrome (asyncify-webkit retired — no JSPI in WebKit); unconditional
Firefox JSPI pref; guard-beacons -> wait-beacons (+wxScheduler/libctxJspi
families); Makefile.wasm links test apps against JSPI with the shim as a
tracked link prerequisite.

Web: WasmTool setRo await + __wxWaitDump forensics, open-flow contained
promise, scheduler-shim.test.ts retargeted (8 green).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDeBaKKhQztd8KiVtHuyXr
2026-08-13 07:06:24 +02:00
Gergő Törcsvári
80199f42f4
Phase F: restore the doc-19 bounce (footprint chooser dead-app) + regression spec
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Td4ujboGuAw26jvbDQzehj
2026-08-10 10:14:21 +02:00
Gergő Törcsvári
1d8237bc08
Phase F fix: open-lane token was lost across the dispatch-context swap — mint it in JS; repro spec added
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Td4ujboGuAw26jvbDQzehj
2026-08-10 10:14:21 +02:00
Gergő Törcsvári
8ed39a72a0
THE FLIP: levers re-pinned to the post-migration invariant; all gates green incl. blue-screen repro 5/5
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Td4ujboGuAw26jvbDQzehj
2026-08-10 10:14:20 +02:00
Gergő Törcsvári
7dfd4f1834
docs 22 D-on probe + app-quit teardown spec; bump wx (alignment) + kicad (root identity)
The D-on probe (wxWASM_STAR_DISPATCH=1 on top of D5) is measured and
recorded in doc 22 §10: the week's 'environmental cliff' was adopted-fiber-
stack alignment (trap 1, fixed in wx - retires the D5 entry's findings 2-3
with a mechanism); with it fixed D5+D reached 388 passed; the remaining
reds decompose into three named Phase B gaps (wake ordering on a running
context, finished-coroutine transfer livelock, invocation-aware root
routing), each with a deterministic repro suite. D parks again behind the
switch until Phase B owns coroutine lifetimes.

app-quit.spec.ts + a wx_test_quit hook in minimal_test drive a real
File->Quit-shaped exit through D5's detached teardown (loop exits on its
context, S6 latch 'clean', OnExit + wxUninitialize there) - green in the
D-on probe, closing the D5 teardown-gate open item.

Landing state (D off): coroutine + coroutine-pthread + app-quit green;
nested red at case 3 in the pre-existing doc-19 wake-window family
(cases 1-2, yesterday's blockers, now pass); races wakeup_during_transition
stays layout-sensitive. Both are the class the C+B+E flip removes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBjomQfKyRa3jBdeAKpmTw
2026-08-10 10:14:18 +02:00
Gergő Törcsvári
258cb37487
star-flip: harness scenarios for star-mode transfers (doc 22 Phase B)
star_transfer_call_is_synchronous stages exactly the TOOL_MANAGER shape -
Call() then read Running() on the next line - and pins that a parked caller
resumed by the scheduler is indistinguishable from a synchronous return.
star_transfer_chain pins a three-deep nested call (root -> A -> B and back) in
strict order, with a parked star context proved undisturbed throughout.

A rule learned the hard way here: a fiber entry must NEVER return (emscripten
ends the program if it does), so these bodies park forever once their script
is done, like libcontext's trampoline loops. Getting it wrong wedges the
battery with no output at all.

The fiberRunning assertion flips to the stronger Phase B invariant: once the
pump is quiescent NO fiber is on the CPU, the scheduler is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EarUW9DS1c1sSW4ZNrkGQS
2026-08-10 10:14:17 +02:00
Gergő Törcsvári
586fbadfe9
design-b Phase A: absorb libcontext's wasm backend (doc 22)
Bumps wxwidgets (fiber lane in sched_context.h) and kicad (libcontext's wasm
backend becomes an adapter over it), so there is now ONE party performing
every emscripten_fiber_swap and recording who is on the CPU. Behaviour is
preserved throughout - libcontext still decides, the registry observes and
beacons any disagreement - which is the de-risking step doc 20's D2 never had.

Harness + spec gain three fiber-lane scenarios: a fresh fiber enters at its
entry and a swap suspends the swapper (fiber_roundtrip), releasing a suspended
fiber is legal while a stale id refuses instead of use-after-free
(fiber_release_suspended), and symmetric swaps leave a parked star context
undisturbed (fiber_and_star_coexist). The spec asserts the lane's counters,
including the tripwire that must stay zero: fiberNonEnterableSwaps.

Doc 22 gains the Phase A work log: the gate (kicad suite 139/1, the 1 being the
pre-existing occ-probe glb matrix; wx 346/1/3; batteries 48/48; zero tripwires
anywhere), the four bugs the tripwires caught and the single rule behind three
of them, the grace ring and the 33-coroutine measurement Phase B needs from it,
and the process traps this run paid for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EarUW9DS1c1sSW4ZNrkGQS
2026-08-10 10:14:17 +02:00
Gergő Törcsvári
48da418f34
design-b D3: fix the doc-19 hang — quasi-modals off the coroutine stack
THE BUG IS FIXED. tests/kicad/quasimodal-strand.spec.ts flips from a
test.fail() pin to a plain green regression test: 3/3 runs
closed=true dialogs=0 refused-resumes=0 (was closed=false dialogs=1
refused-resumes=1 on every run).

Mechanism: a quasi-modal's nested event loop parked on the TOOL
COROUTINE's stack, which suspends the fiber's body where the fiber layer
cannot see it — so the stale-fiber guard quarantined the fiber and then
refused its own resume, the dispatch guard was never released, and every
click after that was deferred forever. Bouncing the nested loop onto the
main stack leaves the coroutine suspended the legitimate way (a recorded
fiber swap), so nothing is quarantined and nothing is refused.

Layering, so this is not a pile of WASM ifdefs in KiCad:
- wx (3d37db3bf1) owns the POLICY and the hook; it must not know what a
  coroutine is.
- wasm/bindings/main_stack_runner.h is the only place that may know both
  sides: it finds the frame's TOOL_MANAGER and bounces via RunMainStack.
  Header-only and self-installing, so no build-script change; included by
  every editor's binding TU.
- KiCad gets ONE ifdef-free method (2c777efede), needed only because
  TOOL_STATE is opaque outside TOOL_MANAGER. libcontext and dialog_shim
  are untouched — an earlier draft edited both and was reverted.

This also reframes the remaining plan: the doc-19 class is closed WITHOUT
migrating tool coroutines onto scheduler contexts. Note it does not make
the wait a context yield — waits still park in place, just never on a
coroutine stack.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-10 10:14:17 +02:00
Gergő Törcsvári
6e80827cec
design-b D3 prep: pin fiber-on-context nesting and the foreign-stack refusal
Two scenarios, deliberately separate because they answer different
questions that D3 depends on:

- fiber_nests_in_context: can a libcontext-style fiber run nested inside
  a scheduler context at all? YES. Worth pinning explicitly — an earlier
  version of this scenario trapped `unreachable` and briefly looked like
  proof that nesting was impossible, which would have been a serious
  wrong turn for the plan. The real cause was the test's own bug: the
  host-side emscripten_fiber_t was zero-initialised, so the swap unwound
  into a null asyncify buffer. Nesting works; the scenario now proves it.
- foreign_stack_refused: a yield from that nested fiber is REFUSED, and
  the host context and scheduler both survive it.

The spec asserts foreignStackRefusals == 1, so the refusal path cannot
silently stop being exercised.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-10 10:14:16 +02:00
Gergő Törcsvári
7dbc9fb36e
design-b D1b/c: context harness + the bounded-and-measured memory gate
Doc 20 §6 D1, second slice: the dedicated test app D1 calls for, plus
the doc 20 §7 risk-1 gate.

tests/apps/standalone/sched-context/ drives the primitives with no wx
linked (a failure can only be the contexts layer) and assertions ON —
unlike the races harness, an emscripten "cannot start an async
operation" state here IS the bug, not a tolerated state. Nine
scenarios, each pinning one invariant; the load-bearing ones:
- parked_does_not_block: a parked context does not stop others running.
  This is doc 19's freeze made unrepresentable — there a parked activity
  held a global interlock and the UI died; here "parked" simply means
  "not runnable", and three workers run past it before it resumes.
- one_transition_in_flight: a context calling drain() gets a no-op, with
  a second ready context queued so a buggy nested drain would actually
  run something and be caught.
- async_wake: a real macrotask hop (setTimeout → mark_ready → drain),
  the shape every production bridge has.
- deep_park_sizing: parks 64 live frames deep to measure what a park
  actually costs.

The memory gate asserts the ceiling (peak live contexts, peak bytes),
that nothing leaked (live=0, bytes=0, created==finished), that nothing
was left mid-transition, and that refusals occurred (zero would mean the
illegal-operation scenarios stopped provoking).

Measured here: ~34 B/frame, 2200 B for a 64-frame park, 1 MB peak for 4
concurrent contexts. Recorded with the caveat that the harness's frames
carry three locals each, so this is a FLOOR — real bridges save far more
per frame (libcontext runs 512 K after a 64 K buffer silently
overflowed). The apparatus and its units are validated; the sizing
DECISION needs deep-park numbers from real bridges at D3/D4.

playwright.config: asyncify-firefox now matches every spec in ./asyncify
instead of only asyncify-races* — a new harness there is covered by
construction rather than by remembering to widen the pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-10 10:14:16 +02:00
Gergő Törcsvári
73a239e0be
design-b D0: land the doc-19 strand as a deterministic red spec
Doc 20 D0 second deliverable (doc 21 §4). tests/kicad/quasimodal-strand.spec.ts
reproduces the Symbol Properties hang on demand, in two parts:

- "staging" (GREEN): double-click the fixture symbol → Symbol Properties
  opens → arm the parking timer (wasm/bindings/timer_park.h) WHILE the
  dialog is up. Because the opener's fiber park is open-ended, a timer
  firing now necessarily parks on top of it — the overlap is structural,
  not a won race. Asserts the dialog opened, the timer fired, its OK
  button is hittable, and the shim beaconed concurrent contexts. Keeps
  the red pin from rotting into vacuity, and fails loudly on its own.
- "doc-19 red" (test.fail()): clicks OK and asserts the desired end state
  — dialog closes, zero fiber-resume-refused beacons, wait books balanced
  (no unresolved nested/modal wait). Goes green at D3, when Playwright
  will report "expected to fail but passed" and the marker comes off.

Verified 6/6 consecutive full-file runs, identical outcome each time:
closed=false dialogs=1 refused-resumes=1 — the doc-19 mechanism exactly
(quarantined fiber's legitimate resume refused, dialog never closes).

Deliberately NOT asserted in staging: the timer park COMPLETING (whether
a park survives the aliasing is the disease under test) and sawParked (a
100ms sampler can miss a short park) — both are reported, not gated.

Retires tests/kicad/dialog-deadlock-probe.spec.ts: the 8/4 throwaway probe
that established the mechanism. This spec supersedes it and, unlike it, is
deterministic (the probe's 3 blind waitForTimeouts were the only
determinism-lint violations in the tree; the guard is now clean).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-10 10:14:15 +02:00
Gergő Törcsvári
d519ac89c0
design-b D-1 gate: battery green scheduler-only; startModal import scrub + work log
Gate results (single battery, scheduler-only): wx app battery +
asyncify + coroutine 363 passed / 3 skipped / 0 failed; full kicad
suite 138 passed / 30 skipped / 1 failed — the one failure is the
pre-existing local occ-probe glb case (predates D-1, unrelated).

Post-gate scrubs: vestigial 'startModal' removed from ASYNCIFY_IMPORTS
(tests/apps/Makefile.wasm) and env.startModal from
scripts/common/asyncify-imports.txt (the import no longer exists in any
wasm; both lists are boundary supersets so behavior is identical).
Stale comment pointers to the deleted legacy modal machinery updated.
Doc 20: D-1 work log added, status flipped to IN PROGRESS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-10 10:14:15 +02:00
Gergő Törcsvári
4906b9fb56
design-b D-1b: delete the legacy handlesleep runtime from the injector
Doc 20 D-1, second slice: the injector now injects asyncify-scheduler.js
unconditionally — the WX_SCHEDULER=0 legacy opt-out, the
SHIM_DISABLE_HANDLESLEEP ablation skip, and the
SHIM_DISABLE_TRAMPOLINE_HEAL ablation skip are gone, and
scripts/common/shims/handlesleep.js is deleted (the scheduler subsumed
its capture/restore, fiber guard, and trampoline-heal duties in S2).
Comment-only scrubs point the remaining references at the scheduler
shim. .ci-cache-epoch bumped (shim/injector behavior changed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-10 10:14:14 +02:00
Gergő Törcsvári
e7b8c2725b
design-b D-1a: retire the legacy-shim ablation builds + redundancy pins
Doc 20 D-1 (legacy runtime deletion), first slice: races_test_noheal /
races_test_nosleepfix pinned behavior of the legacy handlesleep shim,
which is being deleted — the pins now assert properties of a runtime
that no longer exists. Drops the two Makefile.wasm link+inject variants
(SHIM_DISABLE_TRAMPOLINE_HEAL / SHIM_DISABLE_HANDLESLEEP), the
shim-redundancy pin specs in asyncify-races.spec.ts, and resolves the
tests/README.md open task. The green battery still runs every scenario
against the scheduler glue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-10 10:14:14 +02:00
Gergő Törcsvári
d6ca125cac
docs: diagnose the Symbol Properties hang (stranded tool fiber)
Reproduced live on the dev platform: the tool fiber running the
quasi-modal parks mid-body, is quarantined by the stale-fiber guard,
and its resume is REFUSED — so it never releases the dispatch guard.
Interlock held forever => clicks deferred and never drained, timer
delivery frozen; the titlebar X works because it is ungated.
Includes the captured frozen state, what is ruled out (clicks do reach
wx; no I/O in flight), and ranked fix directions. Regression vs
pre-existing still undetermined — needs a real WX_SCHEDULER=0 build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfxKn5utcntBSnxz4ZnYKs
2026-08-10 10:14:14 +02:00
Gergő Törcsvári
603e260885
mailbox S2: scheduler core — deferred wakes + N1 single-writer tripwire
asyncify-scheduler.js REPLACES handlesleep.js on WX_SCHEDULER=1 builds
(injector either-or): ports capture/restore, fiber consume-once/
quarantine guard, wake-window flags, recorder, trampoline heal — and
adds deferred wakes (a wake mid-transition queues and drains from a
clean macrotask) plus the N1 currData accessor (pure-JS writes need
scheduler authorization; strict mode throws; meta-tested). Gates:
races 9/9 with NO legacy shim (subsumption), coroutine 39/39,
wx-chromium 30/30, kicad trio 3/3 on the C-lane build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfxKn5utcntBSnxz4ZnYKs
2026-08-10 10:14:13 +02:00
Gergő Törcsvári
af1eb5fc58
mailbox S1 complete: embind lane, wheel lane, dual-contract specs
Shim embind lane wraps the doc-18 production mutators at the Module
boundary (busy-window calls queue + deliver post-settle; time-boxed
unkillable pump). N2 un-fixme'd and green; collab-load-fuzz carries the
variant contract (drop on legacy, deliver-in-order on scheduler, capped
hammer on the scheduler lane); timer-park's timerRetry silence tripwire
arms on shim+export and is green on the C-lane kicad build. Bump
wxwidgets for the wheel lane. CI both-EH matrix deliberately deferred.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfxKn5utcntBSnxz4ZnYKs
2026-08-10 10:14:13 +02:00
Gergő Törcsvári
94ae4a8a41
mailbox S0: dual-glue flag, beacon counters, N2 red spec
Doc 17 step S0 scaffolding: WX_SCHEDULER=1 injector path with an
observation-only asyncify-scheduler.js skeleton (legacy shim stays
authoritative until S2), guard-beacon extraction with occurrence
recovery for rate-limited beacons, and the fixme'd N2 ordering spec
(add-then-move probe; un-fixme at S1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfxKn5utcntBSnxz4ZnYKs
2026-08-10 10:14:12 +02:00
Gergő Törcsvári
1b84b77c8d
test(drift): quarantine the whole S4 conflict class — three distinct steps diverge
move-vs-move diverged on CI right after value-vs-value was extracted: across
this week's runs, THREE different same-item conflict steps have each failed
to converge (settleConverged 90s, byte equality never reached). This is a
conflict-resolution class bug, not a per-step flake — same evidence profile
as before (no guard beacons, reproduces locally, not poll timing). Whole S4
test fixme'd alongside S4b; the sequenced-edit scenarios (S1–S3, S5–S8) have
never diverged and remain active. Tracking: memory s4-value-race-divergence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 23:12:31 +02:00
Gergő Törcsvári
45f305b20b
test(drift): quarantine S4 value-vs-value — genuine ~5-8% divergence, own hunt
Two clients racing setValue genuinely diverge in ~5-8% of runs:
settleConverged times out at 90s with the trio never reaching byte equality.
Not timing (windows already widened, reproduces locally at single-worker) and
not the asyncify guards (zero beacons in failing runs) — a real CRDT/apply
race this harness exists to catch, gating unrelated releases in the meantime.
Extracted into its own test.fixme (S4b) with the full original body;
move-vs-delete and move-vs-move stay active in S4. Tracking notes: memory
s4-value-race-divergence — next steps are capturing both tabs' modelText diff
at timeout and bisecting the value-apply path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ac/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 22:18:49 +02:00