Commit graph pcbjam/web
Author SHA1 Message Date
014648807f cmms-embed: VITE_LIBS_SOURCE=static (not off) — install the window.kicadLibs bridge
Some checks failed
wasm-build.yml / cmms-embed: VITE_LIBS_SOURCE=static (not off) — install the window.kicadLibs bridge (push) Failing after 0s
release / meta (push) Waiting to run
release / build (push) Blocked by required conditions
release / publish-wasm (push) Blocked by required conditions
release / deploy-demo (push) Blocked by required conditions
release / deploy-editor (push) Blocked by required conditions
release / Build all tools + KiCad e2e (Ubicloud) (push) Waiting to run
wasm-build.yml / cmms-embed: VITE_LIBS_SOURCE=static (not off) — install the window.kicadLibs bridge (release) Failing after 0s
off leaves libsSource null, so boot.ts never calls installLibsProvider and
window.kicadLibs is never installed. A C++ libs-bridge call on schematic open
(park site K1, sch_io_pcbjam_lib) then hits undefined -> throws inside a
suspending import -> SuspendError -> abort. static installs the bridge (2
built-in example symbols; answers not-found gracefully otherwise), no backend.

Standalone-only change; dist/ rebuild, no wasm rebuild.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 10:13:09 +10:00
84dd40702c cmms-embed: sub-path mount support (/vendor/pcbjam/)
Serve the standalone editor under a CMMS sub-path with no change to
@pcbjam/shared or the wasm/C++ side. No-op for a root deploy (BASE_URL='/').

- main.tsx: <BrowserRouter basename={import.meta.env.BASE_URL}>
- lib/base-path.ts (new): stripBase() for raw location.pathname reads,
  hardNav() for the cross-document window.location.assign() navs (the
  process-global wasm runtime means a tool switch / File->Quit is a full
  page load, and @pcbjam/shared's projectPath() is origin-absolute)
- config.ts currentScope(): stripBase() before splitting for the scope segment
- WasmTool.tsx quit-path: stripBase() for the projects/libs segment test
- tool-navigation.ts (x2), NewFileDialog.tsx, ProjectView.tsx, quit-hook.ts:
  window.location.assign(projectPath(...)) -> hardNav(...)
- .env.production: VITE_API_BASE_URL=/pcb-project-api.php, PROJECT_SOURCE=remote,
  DOC_SOURCE=api, LIBS_SOURCE=off, WASM_ROOT=/vendor/pcbjam/wasm,
  YJS_PROVIDER=broadcastchannel; APP_URL/USER/SCOPE unset

Build: vite build --base=/vendor/pcbjam/

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 22:49:31 +10:00
Istvan Matejcsok
c421d724b0 findings(E-10..E-22): fix the defects a code review found in the E-1..E-9 work
A review of the group-E fixes found 13 further defects; ten were introduced by
those fixes, two pre-existed and were merely relocated, one is deferred.

Services / transport
  E-10  retireWorker synthesized no bg/exit frame, so sharedspice's s_bgRunning
        mirror stayed latched true after a mid-run worker death: Run stayed
        disabled and the promised fresh-worker restart was unreachable for the
        whole session. Retirement now dispatches a synthetic controlled-exit
        straight to the installed handler (never through dispatchEvt — a
        fabricated frame must not touch the credit ledger). Driving the repro
        exposed two further defects, both fixed here: a replacement worker
        trapped on pre-init engine reads, and the rerun's cm_input_path/circ hit
        that uninitialized engine before KiCad's validate() re-init (the native
        flow assumes a crashed engine survives in-process — true for the dll,
        false for a dead worker). Reads now answer their empty shapes pre-init,
        writes lazy-init, and init is idempotent per worker engine.
  E-19  dispatchEvt acked only AFTER handler(evt) returned, and the sharedspice
        client deliberately rethrows non-trap errors — so each throw leaked one
        unit of the 64-frame credit window until the stream died with a
        misattributed "transport exceeded". The ack moves to a finally in both
        service copies; the throw still propagates (the trap machinery needs it).
  E-20  the oversize-line path promises to transfer the accepted prefix, but
        with the window full that flush only DEFERS, and stopEventStream wiped
        the deferred queue — losing the diagnostics that explain the failure.
        The terminal notice now carries them as pendingEvents; both hosts
        deliver them in order, unacked (the fatal frame is outside the credit
        protocol).
  E-21  the 30s prefetch deadline discarded every model already collected and
        reported nothing. A caller-owned progress sink ships the partials and
        the omission reaches the export report. (Awaiting the aborted collection
        was rejected: an in-flight source fetch is not abortable — E-4's
        original disease.) Plus a serving-candidate memo, so a .wrl ref served
        by its .step fallback stops re-probing the miss on every export.

Scheduler
  E-14  _terminalizeNativeTrap classified by message substring, so any plain JS
        error QUOTING 'Aborted(' or 'out of bounds' permanently bricked a
        healthy instance. Now structural only: instanceof RuntimeError plus a
        duck-typed name check (verified in this build's glue that abort() throws
        a genuine RuntimeError both pre- and post-runtime-init). Module.onAbort
        now latches the gate — the authoritative notification, previously
        ignored.
  E-15  the shim half: _pumpResume gates on terminal (catching wakes already
        queued at latch time) and resolveWait refuses on terminal WITHOUT
        consuming the entry, so a frame stays visibly parked rather than
        resuming inside a trapped module.
  E-16  the E-5 handler read the realm-global scheduler at dispatch instead of
        its installing module's; also frees the per-line buffer on the non-trap
        rethrow path.
  E-11  get_vec trusted the worker's res.length over the transferred arrays.
        Observed death shape: a 4 GiB std::vector threw an unhandled
        std::length_error that exited the editor's main loop. Now clamped, with
        the buffers freed on every failure path.

Guardrails (replacing two deferred refactors: e2e→production-code injection and
collapsing the four copies of the worker-lifecycle machinery)
  E-18  the source contract asserted comment-string counts — rewording failed
        CI while moving a guard outside its #ifdef passed. It now parses the
        #ifdef regions and asserts on code.
        service-stub-parity.ts pins what the four lifecycle copies must share:
        credit-window equality parsed from source, the finally-ack, boot
        deadlines, terminal-notice consumption. The transport numbers are now
        single-sourced from the worker.
        CI actually runs the gates: the web/standalone vitest suites (which had
        NEVER run in CI), the reducer, the source contract and the parity tool —
        with a NON_PLAYWRIGHT_GATES check so deleting a step re-fails the lint.
  E-22  the e2e occ stub's 60s boot watchdog, deleted in a66e109, is restored in
        the ngspice-stub shape with a wedgeNextBoot() repro hook.

Every behavioral fix has red-then-green evidence (the reds were captured first).
E-17 (a stale RUNNING cross-stamping the next run's generation under E-6's
transport deferral) is DEFERRED with its analysis recorded — a real fix needs
run identity on the bg frames.

Test hygiene: the dwell lint now requires the mandated ": <why>" and all 47 bare
markers carry their reason; three export-report dwells became modal-lease polls;
exact-ledger assertions became relative deltas; the dead data-wx-dom-id branch,
an unused fault hook and unused receipt plumbing are gone; abort scans, wx
dialog drivers, the sim harness and the vitest FakeWorker are each one copy now.

Bumps kicad and wxwidgets to their findings-group-e tips.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 18:19:16 +02:00
Istvan Matejcsok
0f21a33216 findings(E-5,E-6,E-7,E-8,E-9): close the test-coverage gaps + kicad ifdef bump
Coverage audit found three fixes without a test that could catch their
reversion:

- E-6 ack-on-enqueue: new unit case "acks a pre-handler queued frame at
  enqueue so the transport window never starves" (ngspice-service.test.ts;
  false-green audited — fails with the ack removed, exactly one ack per
  frame, none repeated on drain).
- E-5 / E-8 wiring / E-9 / E-7 shape: new source-contract tripwire
  tests/tools/findings-e-source-contract.ts (npm run findings-e:contract) —
  the codex-thread contract style for C++/EM_JS code that cannot be
  behaviorally unit-tested without a wasm build. Asserts: the module
  identity stamp + self-disarm and ABSENCE of the install-once presence
  guard (E-5); all four completion sites use runWaitCompletion, no stub
  resolves a wait directly, token<=0 bails, the shim exports the gate
  surface (E-8); the run-generation mechanism present with its behavioral
  drops confined to the wasm build (E-7); the identity-checked destructor
  unregistration (E-9).

Also bumps kicad to the E-7 ifdef-confinement commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 18:18:28 +02:00
Istvan Matejcsok
06a46546cc findings(E-5,E-6): validation-round fixes — live e2e falsified two ported shapes
E-5: the module-identity bridge called installingModule._malloc, but this
build exposes _malloc only as a bare glue-closure export (Module._malloc is
absent) — every char/stat event entry threw TypeError, which also starved
the E-6 credit window (thrown dispatches never acked) and wedged the queued
bg-finished frame behind them. The bridge now uses the bare closure exports
(identity is still exact: the EM_JS body IS the installing module's closure;
the __ngspiceOnEvent self-disarm covers supersession).

E-6 (codex reference design corrected — its validation matrix never ran):
a FULL credit window was terminal (stopEventStream at 64 in-flight frames).
Under live e2e that killed a real simulation: bg-thread emissions proxy one
per task, so each line ships as its own frame and a normal transient outruns
a busy main thread. A full window now DEFERS into a bounded FIFO (512 events
/ 4 MiB) drained in order as acks free credit; only true overload or an
invalid ack is terminal. Retention stays bounded (8 MiB in flight + 4 MiB
deferred + 1 MiB open batch). And the service/harness mirror queue now acks
at ENQUEUE — placing a frame in the bounded pre-handler queue is taking
ownership; without that, a stream starting before the C++ handler installs
(the ngspice-probe page) starves the worker window forever.

Test updates: worker-batch reducer — new "a full credit window defers and
drains in order, never terminal" case pinning the regression; the storm case
now proves the deferred caps are the terminal edge. board-ready.ts gains the
owner-free openBoardProgrammatically (codex helper the ported occ-export
spec needs; the barrier-based waitForUiBoardReady was NOT taken).
occ-export.spec: domId is optional on this line's registry (coordinate
fallback is the supported path).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 18:18:21 +02:00
Istvan Matejcsok
c14e76651c findings(E-5,E-8,E-9): module-identity ngspice events + runWaitCompletion admission gate
E-8 (re-implemented for JSPI — the codex gate is entangled with the dropped
execution owner; under JSPI a fresh non-suspending JS→wasm entry while
another activation is suspended is structurally safe on its own stack, so
the admission boundary for worker completions is liveness + trap state, not
execution ownership):
- jspi-scheduler.js grows `terminal` (trapped instance; distinct from `dead`),
  canTouchNative(), _terminalizeNativeTrap() (WebAssembly.RuntimeError +
  cross-realm string classification), and runWaitCompletion(site, token,
  prepare, inertResult): prepare runs immediately and owns ALL native work;
  stale tokens and dead/terminal instances drop loudly without resolving
  (resolving would resume the parked frame inside the damaged module); a
  trap latches terminal; a plain JS bug resolves inertResult so the wait
  fails instead of stranding. beginWait refuses (token 0) when dead/terminal.
- all four delayed completion sites route their native work through the
  gate: 'OCC export completion' (exporter_step_stub), 'OCC model completion'
  (oce_plugin_stub — the MEMFS cache write moves inside the gate too),
  'ngspice request completion' and 'ngspice vector completion'
  (sharedspice_client — every HEAP32/HEAPF64/malloc write inside prepare,
  inertResult 1 = transport error). Every wxWasmBeginWait caller in the
  stubs bails on token <= 0.
- deliberately NOT ported from codex: ownerModule, enqueueNativeCompletion,
  executionBarrier, the byte-credit native-entry FIFO — completions are
  one-shot per wait token and stream volume is bounded at the E-6 transport
  credit window. Cross-refs logged for group M (M-2/M-6/M-8).

E-5 (re-implemented; codex shape kept, owner APIs replaced with the E-8
gate): js_ngspice_install_events binds the handler to the EXACT installing
module (handler.__pcbjamNgspiceOwnerModule stamp; presence is not identity),
re-installation replaces a foreign module's handler, a superseded handler
disarms itself, native entry goes through installingModule._malloc/
._pcbjam_ngspice_event (never lexical Module), each dispatch checks
canTouchNative() (loud drop on a dead/terminal module), and a trap on the
per-line entry latches the terminal gate.

Tests: scheduler-shim.test.ts +7 (gate happy/stale/dead/terminal/cross-realm/
js-bug/beginWait-refusal). e2e specs updated from the codex line: occ-export
decode-fault recovery (real onmessageerror transition via failDecode, J-4),
ngspice-probe direct-service coverage, eeschema-sim rewritten onto the E-7
applied-generation receipt (codex's executionBarrier await replaced with a
pendingWaits('ngspice') drain poll — the JSPI-line equivalent).

Also bumps the kicad submodule to the E-7/E-9 commit (dd5751038f7).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 18:18:21 +02:00
Istvan Matejcsok
0b8e7186d4 findings(E-1,E-2,E-3,E-6): ngspice service generations + credit-bounded event transport
Adapted from codex/asyncify-execution-owner-core 3753320. Service side mirrors
the occ-service shape (E-1 watchdogs, E-2 fail-all + boot-death fix — onerror
now rejects the in-flight boot waiter instead of stranding it, E-3
onmessageerror terminal, Blob URL revoked, per-generation evtQueue cleared on
retirement).

E-6 transport bounds (worker hunks re-applied inside the emscripten-6
em-pthread else-branch — the codex file predates that split, so this is a
re-application, not a cherry-pick):
- batch cut at 512 lines / 1 MiB exact JSON-UTF-8 bytes, measured before a
  line is retained; a single line > 1 MiB flushes the accepted prefix then
  stops the event stream terminally (never retained);
- posting gated by a 64-frame / 8 MiB unacked credit window; each frame
  carries { eventSequence, eventBytes } and is released only by an exact
  { sequence, bytes } ack; any mismatched ack is terminal;
- the service mirrors the same 64-frame / 8 MiB bound on its pre-handler
  queue, acks after handing a frame to __ngspiceOnEvent, and retires the
  generation on invalid credit; { fatal } frames retire the worker.

Tests: ngspice-service.test.ts (11, ported) — watchdogs, crash/bootError/
decode-fault settlement + recovery, out-of-order ids, sync postMessage throw,
stale-generation event drops, fatal-frame retirement. tests/tools/
ngspice-worker-batch-unit.ts (node:vm over the production worker source;
`npm run ngspice:worker-batch`) — bounded ordered chunks, byte-pressure
flush, 100k-chunk credit storm, over-limit line, exact ack lease. e2e harness
twin updated to speak the ack protocol (adds __ngspiceServiceTestHooks).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 18:18:02 +02:00
Istvan Matejcsok
3de00bb15b findings(E-1,E-2,E-3,E-4): occ-service generations/watchdogs/messageerror + abortable model prefetch
Adapted from codex/asyncify-execution-owner-core 3753320 (scheduler-free on
that branch already; one comment line re-worded for the JSPI line):

- E-1: generation-slotted WorkerSlot with a 2-min boot watchdog (armed before
  resolveWasmBase, so a hung delivery lookup expires too) and a 30-min
  per-request response watchdog; retireWorker() is the single idempotent
  funnel (fail that generation's pendings, terminate, revoke the worker Blob
  URL, clear timers/listeners).
- E-2: worker.onerror is wired for the worker's whole life and settles every
  in-flight STEP/export request; a synchronous postMessage throw settles its
  request without leaking the pending id; late frames from a retired
  generation are inert.
- E-3: worker.onmessageerror retires the generation like error does.
- E-4: collectBoardModelFiles is a pure source/IDB/network path (no editor
  MEMFS round-trip) taking an AbortSignal checked at every loop head;
  prefetchBoardModels races it against a 30 s deadline — timeout is non-fatal
  (export proceeds without models) and late results are inert.

Tests: occ-service.test.ts (7, ported) — boot/response watchdog expiry,
crash-settles-all, bootError retry, decode-fault retirement (invokes the real
onmessageerror transition, per J-4), hung-prefetch export; models-bridge.test.ts
+3 — abort inertness, zero FS access on the collect path. e2e harness twin
updated to the same generation shape (adds __occServiceTestHooks/failDecode).
False-green audited: 19 cases fail with the fixes reverted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 18:18:02 +02:00
Gergő Törcsvári
a4fab53590 sibling restage: focus-triggered repull — a dropped touched no longer strands the mirror until reload
Follow-up to the sync-delete fix: with the doc corruption gone, the remaining
in-session staleness class is the passive watch itself. It re-pulls only on
`touched` control frames, and the gateway debounces those 2s leading-edge
with NO trailing emit — a dropped frame (real across gateway/DO hops, nearly
impossible on localhost) leaves the MEMFS mirror pre-delete until an
unrelated later edit. Field symptom: "delete + sync doesn't delete, but
reloading the pcbnew tab and syncing again does".

The sync gesture always brings the pcbnew tab to the front first, so on
window focus / visibilitychange every live sibling watch now sends a manual
SyncStep1 (new gateway-only `YjsProvider.repull`), rate-limited to one per
2s; any news restages through the normal debounce.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MLPKYptXFrxHj5Gu7rhToz
2026-08-31 15:13:43 +02:00
Gergő Törcsvári
8a3fc914e5
collab: paste-collision sync-delete fix — lenient sibling restage, forced removals for deleted dirty roots, eeschema child-blob lifting
Field bug (2026-08-31): copy-pasting a symbol whose pins kept their source
uuids corrupted the sheet's ydoc on delete (pcbjam-shared: cross-parent
re-key fix, bumped here); the pcbnew tab's sibling mirror then silently
froze on the dangling refs, so "Update PCB from Schematic" with delete
enabled never removed the deleted symbols' footprints — until any later
edit resynced the sheet.

- standalone sibling-restage: render tolerantly past dangling item refs
  (docToFile onMissingItem) and console.warn on drops/failures instead of
  only the gated debug log — a frozen mirror is no longer silent.
- pcbnew_embind flushDiff: dirty roots that are already STRUCT_DELETED emit
  forced removals on both wires instead of being P-5-skipped (observed: 8
  footprints deleted on the board stayed in the board room forever); they
  are erased from the next baseline so a redo re-adds them.
- eeschema_embind blobFor: lift an unlifted child (field/pin/sheet-pin) to
  its screen root before serializing — the selection writer emits nothing
  for such a child standalone, so the entry used to reach JS as an empty
  envelope and be skipped, silently dropping the edit; residual empty blobs
  now warn instead of shipping hollow envelopes. SCH_MARKER_T (ERC
  artifacts, never file content) stays out of the snapshot and seed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MLPKYptXFrxHj5Gu7rhToz
2026-08-31 12:50:12 +02:00
Gergő Törcsvári
f3119e690f
feat(standalone): floating sheet navigator panel backed by a C++ hierarchy export (sheet-panel)
Canvas-only eeschema sessions (read-only viewers, hide-UI editors) could not
reach the wx hierarchy pane, so a viewer of a hierarchical design was stuck
on the entry sheet. Adds the React stand-in (SheetPanel) + the bridge:

- eeschema_embind: kicadSheetsGetTree() (SCHEMATIC::Hierarchy(), page-number
  ordered: instance KIID path / parent / Sheetname / file / page / depth) and
  kicadSheetsEnter(path) (validated synchronously, applied on the coroutine
  via SCH_ACTIONS::changeSheet — same route as the wx pane, history intact).
  OnSchSheetChanged pushes window.kicadCollab.onSheetsState after EVERY
  navigation, wx-driven included, so the panel is event-driven.
- kicad → 9ab93b838e: navigate actions allowlisted for read-only viewers.
- SheetPanel.tsx: LayerPanel shell conventions; rendered + menu row only when
  the hierarchy has >1 sheet instance; viewers boot it collapsed.
- tests: fixtures/demo/hier (root → Power, IO → Sub) + web/sheet-panel.spec
  (list/order/depth, navigate + collab rebind, menu toggle/close, flat
  schematic has no panel, bridge-driven navigation updates the panel);
  SheetPanel.test.ts for the wire parser.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtN6ASBvMGNbjPqY5boycg
2026-08-29 16:24:57 +02:00
Gergő Törcsvári
fab6108cbb
fix(standalone): never let the boot-time default-lib create blank the lib tables
Anonymous open of a public project (read-only viewer): boot saw no writable
lib, POSTed createLib("My Symbols"), the session gate 401'd, and the throw
escaped the listLibs try/catch — both sym/fp lib tables were seeded EMPTY for
the session (staging: /tg44/projects/arduino/... Arduino Leonardo.kicad_sch).

- ensureWritableLib(): skipped for readOnly sessions; a failed create is
  logged and swallowed, never propagated (the listed libs stay seeded).
- WasmTool passes readOnly through to boot.
- unit tests for the skip / create / already-writable / rejected-create paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtN6ASBvMGNbjPqY5boycg
2026-08-29 15:18:53 +02:00
Gergő Törcsvári
c4bdaad7a0
staging CI green-up: P-1 apply-slot wedge under JSPI, popup P-4 regression, ysync double-seed header, spec fixes
- P-1 (findings-p.spec P-1c red on CI + locally): a JS exception thrown from a
  window.kicadCollab wire callback does NOT reach the C++ catch(...) — under
  JSPI it REJECTS the promising coroutine entry, slot.done never flips and the
  apply slot is busy for the page's life. Three layers: libcontext flags the
  rejected record dead (kicad → d0afbc4bd3),
  drainApplies() reaps a !done && !CanResume() body, and every EM_ASM
  window.kicadCollab.onX(...) call is try/catch-guarded (collab_common.h,
  kicad_editor/eeschema/pl_editor embind).
- popup.spec 'dismisses on outside click': wx P-4 SetFocus delegation excluded
  for wxPopupWindow (wxwidgets → a7436d1052).
- ysync-two-tab concurrent seed: repairLayoutY + seedNonce retraction deleted
  both headers (pcbjam-shared → 8dabf2e).
- dark-mode.spec: stale light-mode reference (pre wx fa46f1822c DOM
  re-projection) replaced with the CI chromium render.
- items-wire-batch-loss.spec: precondition obsolete after P-5 — poisoned entry
  is now synthetic, plus a P-5 contract assertion (field blob == '').
- presence-ghost-peer.spec: skip early when no /parties/ socket (CI web e2e
  runs the BroadcastChannel provider); findings-p1.spec: polled click-select
  precondition.

Local: kicad-chromium findings/presence/ysync/collab 59/59, popup 7/7,
dark-mode 1/1, pcbjam-shared vitest 249/249.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvvARNMxQrq17vogn3REMU
2026-08-28 22:25:21 +02:00
Gergő Törcsvári
5ce919daa6
findings group W: pins/presence bridge never receives non-finite coords (W-1/W-4)
- comments.ts pushPins: finite filter; presence-kicad: finiteCursor() drops non-finite peer cursors.
- collab_presence_core.h: numOr() — a present JSON null no longer throws type_error.302
  across embind in setPins/setRemote/setRemoteCursors (takes effect on next wasm build).
- comments-nonfinite.test.ts: sink repro (boot + throttled live push).
- pcbjam-shared → ff8835c (finite wire schemas, resolveAnchor guard, encoded route segments).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CwF7w1pkSTh5Z8jQyiXmHm
2026-08-28 19:42:31 +02:00
Gergő Törcsvári
af07a413ee
ysync bug 07 UP side: superseded sheet switch never adopts onto the new screen + apply envelope sheet tag/guard (subsheet self-reference fix)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013MnySXALJiYsRQ5mCrxgwX
2026-08-28 18:19:29 +02:00
Gergő Törcsvári
602f5c6fed
collab: passive pull facade, sibling mirror without BoardRoom wake (load-path-rework 0004)
- GatewayDocFacade: passiveSync (Step1 on subscribe + on touched, answered by
  the gateway from at-rest state), onReset; activate() after a passive fill
  still sends act + a participant Step1.
- sibling-restage subscribes passive+passiveSync; reset drops the watch (no
  flush) and re-dials while a peer still has the sheet open.
- files-watch: upload/job hints on room-backed paths restage + announce
  onRoomBackedChanged; sheet-manager.invalidate() drops a parked room.
- kicad-binding: normalize server-serialized bodies on the editorMatchesDoc
  path (runner-seeded ydocs carry kicad-cli's serialization).
- pcbjam-shared -> 0f4d3a1 (reset control, kicadDocToYdocUpdate).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQsdqaX57xzcCWUjqP7ctV
2026-08-28 17:55:59 +02:00
Gergő Törcsvári
7a9aeb5181
presence: fix stale peer selections after delete, select-all lag, ghost-peer removal (findings group Y)
- wasm core: CORE::onDocChanged() from both collab listeners (local commit AND
  remote apply) repaints peers' shapes from the live document + re-checks the
  local selection post-settle; PresenceStart registers the bridge listener
- wasm core: cursors on their own overlay trio; shapes repaint only when the
  non-cursor snapshot changes; new kicadCollabSetRemoteCursors (cursor-only
  update, PEER.id) in both TUs + merged editor + JSPI mutator allowlist
- presence.ts: size-aware trailing throttle for cursor/viewport publishes
  (128 KB/s budget) + parsed-peers memo; presence-kicad.ts: cursor-only push
  when the shape signature is unchanged
- gateway.ts: honor the `gone` control (removeAwarenessStates)
- specs: kicad stale-after-delete gate, web ghost-peer timing (+diag), unit
  select-all budget (pcbjam-shared → 4573a7c)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AScTR39aqyrY5i3ZFHmnMn
2026-08-28 16:14:44 +02:00
Gergő Törcsvári
d069b7be80
findings Q-2: a non-target sibling fetch failure no longer aborts the open
syncProjectToMemfs rethrew the first rejected fetchBytes whatever file it
was, so one missing/unreadable sibling (a .kicad_sch body gone, a Q-1
phantom row, a transient 5xx) took the whole board open down behind
"download failed (404)". Now only the TARGET's failure rejects; siblings
are logged, counted and reported through onStatus, and KiCad reports a
missing sheet itself. kicad-runner.test.ts pinned the old contract with a
non-target file — corrected to a target; new kicad-runner.findings-q.test
gates both branches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9KFksoViNPYYs1ygkcAoQ
2026-08-28 15:15:08 +02:00
Gergő Törcsvári
97d4f9657e
read-only-viewer 0003: no lib catalog for viewers; 3D viewer from the session menu
- kicadShow3DViewer embind (pcbnew-only name, registered in the unguarded
  section so the merged kicad_editor image carries it): runOnCoroutine →
  ACTIONS::show3DViewer.
- Session menu: "3D viewer" row (pcbnew) — the only 3D entry once the wx
  chrome is hidden. Runs the deferred model prescan first.
- Read-only sessions: skip the boot-time enableRealtime scope-room socket
  (would 401 for non-members) and defer the board's 3D-model prescan until
  the viewer is opened (deferBoardModelPrescan / runDeferredModelPrescan).
- kicad submodule → read-only allowlist for the 3D viewer actions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXCntRWNn89M72EkrqMkvc
2026-08-27 15:26:30 +02:00
Gergő Törcsvári
c981e3dc75
collab: repair duplicated layout header groups after seed and on remote layout merges (ysync 0011 follow-up; shared → f14401b)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0189z2siCsezE39ugcDKNBMs
2026-08-27 10:17:23 +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
Gergő Törcsvári
767f2abfc0
libs 0019: remote lib edit — invalidate, don't auto-reload; editor copy counted + re-read
- kicadLibsInvalidate: a peer's edit only drops the lib's plugin entry + pcbnew's
  PreloadedFootprints (the cache the old reload never cleared — tree/preview/
  LoadFootprint/update-from-library kept serving the old body); the fat re-load
  now runs lazily or from Update-from-library (kicadLibsReload, which also
  clears the preloaded cache)
- usage bridges count the Footprint/Symbol Editor's open copy; update re-opens
  an unmodified copy, reports a modified one
- embind TU gets eeschema/symbol_editor on its include path; smoke probes
- kicad → 27051b46e2

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Wd1r3ewftpV1DBSEArpRa
2026-08-26 14:10:25 +02:00
Gergő Törcsvári
079522d1e7
standalone: split the WasmTool component body
useLibNotices hook (toast/stale-lib/load-badge state + window listeners +
auto-dismiss timers) and presentational children — NoticeStack, SessionMenu
(+ StaleLibsRow, FollowBanner), BootOverlay, LibLoadingOverlay, FatalOverlay,
ConsolePanel. Behavior and data-testids unchanged; WasmTool.tsx 2378 → 1680
lines. The boot effect + collab wiring stay in place.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Wd1r3ewftpV1DBSEArpRa
2026-08-25 19:52:11 +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
Gergő Törcsvári
9a19b96b6c
standalone: file-change hints — ~files watch, sibling restage, target notice (project-sync 0002)
GatewayDocFacade.onFiles + hint-only channel; files-watch router (Tier 0
echo/observed bookkeeping, Tier 1 debounced sibling restage, Tier 2 open-
target notice); WasmTool/ToolPage wiring with observed-revision seams.
Bumps pcbjam-shared (4fd6af2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Wd1r3ewftpV1DBSEArpRa
2026-08-25 13:55:01 +02:00
Gergő Törcsvári
5650e6193f
standalone: room-backed files skip the save upload (save-flow uploadPolicy)
In ydoc mode the collab room owns a document's state — items reach it at
commit time, layout heads at save time (syncLayoutFromSave). The raw CAS
PUT on Ctrl+S was only ever the file-list registration + fallback copy,
but for a file the listing already marks hasYdoc/isLive it bumps the
revision for nothing, can 409 the target into the durable save-blocked
banner, and leaves a shadow row the ydoc supersedes on every read.

registerSaveHook gains `uploadPolicy(relPath) → "upload" | "room"`;
WasmTool marks the boot listing's ydoc/live files "room" (ydoc mode only).
Files with no row yet (created sheets, the synthesized .kicad_pro) still
upload; onSavedText still runs for room-backed saves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Wd1r3ewftpV1DBSEArpRa
2026-08-25 13:23:03 +02:00
Gergő Törcsvári
b824eb007a
standalone: record CAS base revision for sync-bundle-staged files
Files staged from the project sync namespace bundle (stageViaProjectSync)
never passed through fetchFileBytes, the only place baseRevisions was set,
so their first save PUT carried expected revision 0 and 409'd against any
row ever re-saved — e.g. assigning a footprint (CvPcb → eeschema root save
also writes .kicad_pro) failed with "Save conflict … (local base 0,
server 1)".

Add ProjectSource.rememberBaseRevision + api.rememberFileBaseRevision,
DriveOptions.onStagedRevision reported by stageViaProjectSync with the
listing revision, and wire it through WasmTool from ToolPage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Wd1r3ewftpV1DBSEArpRa
2026-08-24 19:36:51 +02:00
Gergő Törcsvári
745b8f413a
version badge: display commit hashes GitHub-style (7 chars)
Staging tags are "staging-<full 40-char sha>" — shorten any embedded
full hash for display only; the corresponding-source link keeps the
full sha, release tags pass through untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011gJ3M1RpeZLeNUUj8jKC4h
2026-08-20 11:49:56 +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
Gergő Törcsvári
47bd6d1a3e
feat: lib kind identity + chunked packages3D ingest + registry model serving
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
2026-08-20 09:35:17 +02:00
Gergő Törcsvári
aeacadf603
feat: live lib edit — cmd+s accelerators, editing-context overlay, lib-set realtime (libs 0015)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166YJapMpPB8Ab7wT8Nx51t
2026-08-19 13:47:43 +02:00
Gergő Törcsvári
4e089ae455
feat(save): D group — client save lane + CAS PUT re-created from codex reference
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
2026-08-19 13:47:43 +02:00
Gergő Törcsvári
fe57caeb92
ui: style the boot-request wait like the wasm loading overlay
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
2026-08-18 21:05:42 +02:00
Gergő Törcsvári
5191a63fd8
fix: spawn CDN pthread workers via a same-origin blob trampoline
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
2026-08-18 20:13:25 +02:00
Gergő Törcsvári
3e53ac37f4
feat: cache ydoc-backed file bodies under the blob-etag validator
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
2026-08-18 16:37:44 +02:00
Gergő Törcsvári
8de7695f28
feat: load-path §5 client — gateway transport behind the YjsProvider seam
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
2026-08-18 14:02:40 +02:00
Gergő Törcsvári
ad0f215fe3
chore: bump pcbjam-shared — load-path §5 gateway wire protocol
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLSht9cadprtT2mhynawWu
2026-08-18 12:59:05 +02:00
Gergő Törcsvári
7c15f14bb8
feat: consume the boot payload (load-path-rework 0001 §6, client)
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
2026-08-18 12:17:29 +02:00
Gergő Törcsvári
314986ad36
feat: stage plain project files via the sync namespace (load-path-rework 0001 §4 full, client)
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
2026-08-18 11:45:23 +02:00
Gergő Törcsvári
f9deee60a7
fix(eeschema): warm collab rooms for the OPENED hierarchy only, not every project schematic
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
2026-08-18 11:22:28 +02:00
Gergő Törcsvári
2355e03888
deps: pcbjam-shared — registry verdict drives (re)connect resync (load-path-rework 0002 5b)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLSht9cadprtT2mhynawWu
2026-08-18 11:12:23 +02:00
Gergő Törcsvári
451833666e
feat: cutover-409 save retry (load-path-rework 0002, 3a)
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
2026-08-18 09:24:36 +02:00
Gergő Törcsvári
89b4a7b61c
feat: load-path rework steps 1-2 — project-file IDB cache + immutable CDN lib layers
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
2026-08-17 20:26:44 +02:00
Gergő Törcsvári
7d500a5f9e
fix(collab): group C session-lifecycle hardening — C-1..C-7
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
2026-08-17 19:42:08 +02:00
Gergő Törcsvári
1b99b63851
staging: bump pcbjam-shared — B-5 delete precondition client (e1df6fa)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZJ1pUePb4W47hGoLMYTw4
2026-08-17 19:24:07 +02:00
Gergő Törcsvári
1f13db0d87
test(collab): A-4 deterministic held-relay same-item conflict reducer
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
2026-08-17 15:02:01 +02:00
Gergő Törcsvári
a0bbffe5c0
fix(editor): via GetWidth layer fix + console tab/copy restore
- 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
2026-08-17 12:33:21 +02:00
Gergő Törcsvári
d10b196e47
chore: bump pcbjam-shared to 218fddb — source sequencing + PROJECT_FILE_REVISION_HEADER
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
2026-08-14 17:57:09 +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