- 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
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
Standalone side of the collision fixes + registry 3D serving:
- remote list + boot preload map the backend's collision-safe mount
nickname into LibInfo.name — one string everywhere KiCad-facing, so
lib tables never mount duplicate names (first-match-wins shadowing)
- registryModelsSource beside cdnModelsSource: model3d origin libs as
sparse sync layers (boot-preloaded index/stacks, IDB per version);
VITE_MODELS_SOURCE=registry opts in, CDN stays default
- bumps web/pcbjam-shared (libSchema.nickname)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3C6837qtdFU84xWhFekxd
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>
Staging deploy builds now upload their renders to runs/pcbjam/<run-id>/
like ci-ubicloud does (secrets: inherit there), so render changes on the
staging branch can be reviewed and promoted in morelli without a manual
ci-ubicloud dispatch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
The standalone's save path was a bare multipart POST: no per-path lane, no
revision tracking, 'Saved ✓' on any 2xx (findings D-1/D-2/D-3 client half —
the 8e3a886 server CAS + smoke oracle landed without it). Re-created fresh:
- save-flow.ts: full persistence lane — per-path active+pending snapshots,
capacity admission before the byte copy, committed-ONLY promotion,
conflict/unknown outcomes install a durable path block, status generations,
SaveHookHandle.stop() aborts transports (D-9 teardown superseding the
interim unregister). +17 unit tests (codex suite, green unmodified).
- project-source.ts: D-1 two-map revision tracking (baseRevisions = model
ancestry and the only legal write precondition; observedRevisions =
metadata), seeded from listing rows, download headers, and cache hits;
uploadFileBytes is now the CAS PUT with x-pcbjam-file-revision and
409 / pre-publish / ambiguous-outcome classification; refreshFileRevision.
- api.ts / idb-project-store / HomePage local sources / ToolPage /
NewFileDialog / persistCreatedSheet: SaveOutcome contract threaded through.
Validated end-to-end by apps/tests editor-save-lane.spec.ts against the live
stack (revision 1 learned on load, PUT base 1 → 200 → server revision 2).
Standalone units 365/365, tsc clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZJ1pUePb4W47hGoLMYTw4
- 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>
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>
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>
The pre-boot state was a white page with bare 'loading…' text; now it's the
same dark full-viewport centered spinner + mono status as WasmTool's boot
overlay, so the boot wait, the download screen and the editor read as one
continuous load with no white flash.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLSht9cadprtT2mhynawWu
Closes the doc-23 §7 KNOWN GAP that killed the editor wherever the wasm is
CDN-served (staging/prod platform): emscripten 6 spawns pthread workers from
_scriptName — the glue's absolute CDN URL — and new Worker(<cross-origin>) is
a SecurityError, observed on staging as 'Failed to construct Worker' right
after instantiation and an editor that never renders. No runtime hook exists
post-mainScriptUrlOrBlob, so wrap window.Worker and redirect EXACTLY the
glue-URL construction to the (formerly dormant) pthreadWorkerScript blob that
importScripts() the glue — blob workers inherit the page origin, and the CDN
already sends the CORP/ACAO the page's COEP requires. ?trace= now reaches
pthread realms through the same blob, same-origin included.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLSht9cadprtT2mhynawWu
fileCacheValidator: hasYdoc && !isLive rows now validate as
y<YDOC_CONVERT_EPOCH>:<ydocTag> (revision-0 collab-only rows included); live
rows and untagged older backends stay uncacheable. The remote source caches
the CONVERTED KiCad text — a warm load skips the download and the measured
~2s-class ydoc→s-expr conversion — and the unconvertible-ydoc plain fallback
is cached under the same tag, ending the stale-ydoc double-fetch. A ydoc
response under a revision-form validator (room appeared mid-listing) stays
uncached, preserving the old race guard exactly.
Measured (Arduino Leonardo, dev stack): cold 52 file GETs → warm reload 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLSht9cadprtT2mhynawWu
One websocket per project (GatewayConnection, refcounted per endpoint+project,
single reconnect ladder) with per-doc facades: a mini y-websocket client over
varint-channel-tagged frames (y-protocols sync + awareness, own Awareness per
facade so skeleton presence stays per-room). connectProvider's partykit kind
routes every parseable board/presence room through the gateway; the per-room
dial remains only for unparseable operator rooms.
Laziness contract (0001 §5 amendment): sheet-manager warms the hierarchy with
PASSIVE subscriptions — no SyncStep1, no BoardRoom wake; touched hints drive
the parked dirty flag; doSwitch awaits the new provider.activate() sync
barrier before bind/seed; syncLayoutFromSave activates before writing; a stray
local write into a passive doc auto-activates. suberr (invalid-file 409,
presence 403) surfaces as CollabSubRejectedError — terminal in the switch
retry ladder like SexprVersionError, ending the old blind re-dial.
Verified: standalone 346 unit tests green (10 new facade tests incl. the
no-doc-frame passive pin); browser on the dev stack: pcbnew demo = 1 gateway
socket / 0 board-rooms with live collab, eeschema Arduino Leonardo = 2 sockets
total, sub table presence+root active / 3 children passive. tests/web: no
regressions (4 failures reproduced identically without this diff —
pre-existing on the JSPI line; 1 parallel-load flake passes alone).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLSht9cadprtT2mhynawWu
useProjectBoot: the tool page tries the ONE composed boot round-trip first —
its project half is the getProject shape — and falls back to the active
source's getProject on any miss (older backend, local-store slug, static
gallery), so downstream behavior without a payload is exactly pre-boot.
With a payload: seedSessionIdentity makes the /api/me flight a resolved
no-op; the synced libs source is constructed with the preloaded listing
(kind-filtered client-side by kindCounts — the same org/mirror-always +
libHasKind rule the server applies) and the batch-resolved stacks, and every
INTERNAL listing (syncState, presync, enableRealtime name-mapping) rides the
preload too; the fresh project-sync digest threads into staging so a warm
restage affirms with zero HTTP.
Measured warm load (Arduino repo-as-project): total API traffic = 1 boot
request + the ydoc-backed per-file set — /api/me, /libs, sync-stacks and
/sync/manifest all at zero.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLSht9cadprtT2mhynawWu
stageViaProjectSync: plain uploaded files (revision>0, not ydoc-backed, not
the target) stage from a one-layer static SyncStack over the backend's
project sync surface — ONE bundle GET cold, a manifest diff warm, bodies
IDB-mirrored like a library. The target keeps its room-materialized wrapper,
ydoc-backed files keep the negotiated per-file fetch, a namespace miss (a
write raced the listing) falls back per-file, and ANY namespace failure
(older backend, decode error) falls back wholesale — exactly the previous
behavior. Demo/local sources pass no config and are untouched.
Measured (Arduino repo-as-project, 108 files = 76 plain + 32 ydoc-backed):
cold 76 requests → 1 bundle; warm 1 manifest GET, 0 bodies.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLSht9cadprtT2mhynawWu
connectAll warmed a room for every .kicad_sch in the project — a
repo-as-project upload (N boards × sheets) opened dozens of sockets for
schematics the wasm never loads. Only the opened root plus its transitive
(property "Sheetfile" …) closure is in memory, so only those need rooms:
no in-memory copy, no divergence risk, no clobber, and C++ sheet navigation
can only reach hierarchy members anyway (same reasoning as pcbnew's
directory-scoped sibling restage).
resolveSheetHierarchy: regex closure over staged MEMFS text, refs resolved
against the referencing sheet's directory (../ and ${KIPRJMOD}/ handled),
non-project refs ignored, unreadable sheets kept warmed but unexpanded, and
an unscopable root (fileless boot) falls back to all project sheets —
over-warming costs sockets, under-warming would cost collab. In-editor
"Add Sheet" children keep their created-hook warm-up.
Verified on the Arduino repo-as-project (27 schematics): 26 board-room
sockets → 5 (root + 3 hierarchy children + presence).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLSht9cadprtT2mhynawWu
saveItemBody retries ONCE after a SyncRoomMovedError: invalidates the cached
batch-resolved descriptor (onStackMoved clears batchedStacks), closes the stale
stack, re-resolves, and retries the write against the room the fresh descriptor
names. A persistent refusal fails after the single retry. Bumps pcbjam-shared
for the registry wire types + typed error + mux replay.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLSht9cadprtT2mhynawWu
Step 1: warm platform loads stop re-downloading every project file. New
project-file-cache.ts (raw IDB, best-effort) keyed by the listing's
revision:updatedAt validator — the pair, not revision alone, because the resave
equivalent-body swap keeps revision but touches updatedAt. Listing row threaded
through fetchFileBytes(slug, path, meta?); ydoc-backed and revision-0 files are
never cached; prune runs on every fresh listing.
Step 2: cdnLibsSource marks its tag-pinned static layers immutable — warm demo
loads skip all ~155 per-lib manifest GETs (bumps pcbjam-shared for the
LayerDescriptor.immutable flag).
docs/features/load-path-rework/0001 steps 1-2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLSht9cadprtT2mhynawWu
C-1: pending-session ownership in WasmTool (registered before adoption,
destroyed on every non-adoption exit incl. unmount/late-handoff guards);
sheet-manager destroyed flag refuses post-destroy connects.
C-2: attachKicadCollab destroys a partially-attached binding on seed throw.
C-3: connectKicadDoc gains a 30s whole-path deadline + abort signal covering
provider import/construction/initial sync, with partial cleanup on every
loss path (incl. late-resolving construction).
C-4: sheet switch clears host presence/comments/follow/drift callbacks
BEFORE the new room connects (onActiveChange(null) pre-connect).
C-5: switchTo rethrows SexprVersionError terminally (no retry timer, queue
unpoisoned); boot surfaces it, nav hook degrades per-sheet.
C-6: sibling-restage failed dial retries on 1s→30s backoff instead of
latching forever; roster churn still never re-dials.
C-7: terminal-error promote() now tears down every ws-driven collab ingress
(shared teardownCollab) — no ticket storm under the fatal overlay; UP apply
observer gains the symmetric try/catch + clean-stack re-surface.
Tests: index.test.ts (new, 8), sheet-manager +3, sibling-restage +2;
standalone units 135/135, tsc clean; ysync-two-tab + eeschema-subschema
7/7 kicad-chromium on the rebuilt bundle.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZJ1pUePb4W47hGoLMYTw4
Unit-tier rebuild of drift-trio S4/S4b (findings A-4): two bound editors over
a held Y relay, conflicts released in fixed order, asserting Y.Doc convergence
FIRST then native projections against the doc. Pinned clientIDs make the LWW
winner deterministic; reverse-order release must converge byte-identically.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZJ1pUePb4W47hGoLMYTw4
Second half of the staging Firefox boot failure: the bucket stores
artifacts brotli-compressed (publish-wasm --compress br) with
Content-Encoding in the object metadata, but the worker returned them
with the default encodeBody:'automatic' — the runtime drops a user-set
Content-Encoding and re-negotiates, and on workers.dev that served the
raw brotli bytes with NO encoding header. The editor <script> tags
loaded binary garbage: Firefox fires onload and silently executes
nothing, so wxElementRegistry/Module never appeared even after the 206
fix. encodeBody:'manual' passes the header+body through untouched
(verified against local workerd with a br-encoded seeded object).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmR6goSk7JC17h7fkgGvHG
The previous fix only stopped passing range options for range-less
GETs, but current workerd reports a DEFINED full-span object.range even
for a plain get() with empty options — so every response still went out
206 + Content-Range and Firefox still refused to execute the editor
glue. Verified against local workerd (wrangler dev + seeded R2): the
206 branch must key off the REQUEST having asked for a range.
Plain GET now 200 with full body; Range requests still get proper 206s.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmR6goSk7JC17h7fkgGvHG
- itemToJson: pass PADSTACK::ALL_LAYERS for vias — the layerless virtual
PCB_VIA::GetWidth() is an assert trap since the padstack refactor, and the
collab baseline/snapshot serializer hit it once per via per snapshot
(big-board load = assert storm). Values were already correct; wire format
unchanged (applyChanged's layerless SetWidth writes the same slot).
- console: closed state is a content-width bottom-left tab again (version
badge + app bottom edge visible); opened footer panel unchanged.
- console: partial-selection copy works — wx's window-level keydown handler
preventDefaults Ctrl/Cmd+C, so a capture-phase guard stops propagation to wx
when the selection lives in the console; canvas mousedown collapses stale
log selections so they can't steal the editor's own Ctrl+C.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HPtPBVLKQzaXTkirYgcVay
env.BUCKET.get(key, { range: request.headers }) makes R2 report a
DEFINED object.range (full span) even when the request carried no Range
header, so every plain GET went out as 206 + Content-Range. Chrome
executes a 206 <script>; Firefox fires onload but refuses to EXECUTE
the script — on hosts with no masking edge cache (staging workers.dev)
the injected editor glue never ran: no [KICAD_STATUS] logs, heap 0MB,
'runtime did not initialize (no FS) in 90s' on every Firefox boot.
Prod's cdn.pcbjam.com zone cache normalizes cached hits to 200, which
is why only staging surfaced it; cold prod objects share the bug.
Pass the range option only when the request actually sent a Range.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmR6goSk7JC17h7fkgGvHG
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
The jspi migration removed binaryen from .gitmodules (no asyncify
post-link pass) but left the gitlink behind, so every submodule
command errors with 'no submodule mapping found for binaryen'.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmR6goSk7JC17h7fkgGvHG
The private repo's content-addressed files route imports
PROJECT_FILE_REVISION_HEADER from @pcbjam/contract, which re-exports
@pcbjam/shared; the previous pin (22217c9) predates that export by one
commit and breaks the closed server's watch build.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmR6goSk7JC17h7fkgGvHG
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
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
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
mimalloc's mi_atomic_yield() is sleep(0) on wasm, reached from malloc's slow
path under cross-thread delayed-free contention. Under JSPI a yield there
suspends the activation INSIDE the allocator — any other activation that runs
next can re-enter mimalloc mid-operation. ms==0 now busy-returns like stock
emscripten. (The removelist half of the staging fix is obsolete here — no
asyncify, no removelist. The mimalloc-storm harness + spec arrive with the
staging merge.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDeBaKKhQztd8KiVtHuyXr
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
sourcetrail/: Sourcetrail 2021.4.19 indexing pipeline for the wasm port —
compile-db transform (rsp expansion, PCH strip, libc++-11 pinning, path
rewrites), removelist-candidate analysis over the indexed call graph, README
with regen steps and the hard-won tricks (relative --project-file hang,
SDK-header poisoning). Heavy artifacts (.srctrldb, compile dbs, libcxx
headers) stay untracked via the folder's .gitignore.
docs/asyncify-allocator-suspend/plan.md: verified plan for the nanosleep-shim
zero-duration guard (mimalloc mi_atomic_yield=sleep(0) can suspend malloc on
the main thread), the red/green contention test, and the adversarially
verified removelist additions with measured payoff (80.9k -> 60.0k
instrumented functions). Planned, not yet executed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2NHhbzuEHqP2JmcrSD96D