mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-11 13:28:57 +00:00
9cdd580e85
PLAN-2903 item 3. `markCaughtUp` clears `pendingResync` — the "this workspace still owes a replay" ask. Its `scopeEpoch` guard answered "has a resync landed since I looked", which is not the question: a response computed against the PRE-snapshot cursor is stale once a resync pins the cursor, however late it arrives and whether or not anything is still in flight when it does. Two loops reach it from opposite sides — one whose response returns while the resync is still running, one whose response returns after it settled — and the second is why an in-flight check alone is insufficient: it asks at clear time about a staleness decided at request time. So a reconcile loop captures `resyncSeq` when its request STARTS and hands that token back. `resyncSeq` bumps when a resync settles, which makes any capture taken during one mismatch afterwards; the in-flight check covers the capture taken before the settle bump has happened. The refusal is scoped to the stale verdict, not the caller or the workspace — the next poll captures fresh and clears at once. A SEPARATE COUNTER, not `scopeEpoch` bumped twice, which was the first shape and was wrong. `scopeEpoch` is also the fence `upsert` uses to reject an optimistic write authorised under a superseded scope, and a settle bump there rejected writes issued AFTER the new snapshot was already installed — a user's create returning successfully and never reaching the store, with the field editing it never seeing its own result. One counter for two questions looked like thrift and was an overload. Every comment that pointed reconcile loops at `scopeEpoch` moved with the split. The clear lives in one helper both doors call. `bootstrap`'s reconcile loop had its own copy, so every condition added to the public entry point silently did not reach it — the fourth time in this plan that a rule landed at one door and not its sibling. WHAT THIS DELIBERATELY DOES NOT DO. An earlier version also refused to clear while `durableSnapshotCommitted` was false. That is a wedge: `pendingResyncFor` gates a destructive decision (TASK-2099 / PLAN-2095 DR-2), and a persistently refused replace or a failing IndexedDB `open()` would make the flag un-clearable for the session. It buys no repair either — the refused case is repaired through the epoch channel, on the next hydrate or the next response whose epoch disagrees. Nothing repairs the durable cache at the instant of the refusal, and the comments say so. PLAN-2903 item 3's "nothing retries" was true when written and stopped being true at TASK-2906; that premise correction is on the plan's trail. Item 4 needs no change: IDEA-2898's joined branch already writes the told epoch durably and TASK-2906 made that write conditional. Pinned by the existing wiring test rather than re-tested. Also here, same window: a delta that cannot vouch for the epoch passes `undefined` — CARRY THE STORED EPOCH — rather than the explicit `null` TASK-2906 shipped, because the caller cannot vouch for the span between a resync installing its snapshot and `persistReplace` resolving, and a null committing after that replace would clobber the epoch it had just recorded. Carrying keeps the durable epoch agreeing with the durable ROWS. An explicit `null` still means "no baseline" and still reaches disk, with a test to pin it, and it reuses the meta row already read for the cursor gate. `markCaughtUp`'s parameter is the reconcile token now, not the scope epoch. Both production call sites and the TASK-2099 tests move with it; `scopeEpochFor` keeps its own meaning, and its remaining consumers are all mutation fences. KNOWN LIMIT, recorded in the code and filed as IDEA-2913 rather than folded in: the token is per-state and restarts at zero, so a `reset()` between a loop's capture and its response hands the REPLACEMENT state a matching token by coincidence. `resetGenerationFor` is the value that survives a reset and the loops do not capture it. This predates the unit — the scope epoch the token replaced had the same gap — so closing it is a deliberate unit, not a rider on one whose property is about ordering within a single state. INSTRUMENTS. The mutation matrix is the evidence, not a count against `main`: most new tests call an accessor `main` does not have, so they would fail there for the wrong reason and that number would be worth nothing. Nine mutants, eight killed — the in-flight guard dropped, the token guard dropped, no settle bump, a settle bump on `scopeEpoch` as well (the fenced-write regression), bootstrap clearing directly again, `durableEpochFor` nulling instead of carrying, no clear at the RAM install point, and `persistDelta` ignoring the carry-over. A non-compiling negative control scores BUILD-FAIL. Of the two IDB tests one fails against `main` behaviourally and one is a guard that passes on both. The ninth mutant — bootstrap reading a fresh token at its clear rather than the captured one — IS EQUIVALENT, correcting what an earlier revision of this message asserted. Once `caughtUp` is set there is no further await before the clear, and any resync encountered on the way makes the loop `continue`, so the two readings cannot differ without an artificial yield. Three attempts to build a discriminating test failed for that reason, and the right conclusion was the one the attempts kept implying rather than the one I had written down. Two earlier mutants also survived first runs and were answered with tests rather than equivalence arguments; both arguments reached for turned out to be wrong. One test in this file passed for the wrong reason — the iteration cap, not the guard — until a mutant exposed it. Claude-Session: https://claude.ai/code/session_01Xk9M5UVPdc84xL5E1mZkm8
Pad Web UI
SvelteKit 2 + Svelte 5 frontend for Pad, compiled to static files and embedded into the Go binary.
Development
npm install
npm run dev # Dev server at localhost:5173 (proxies API to localhost:7777)
npm run build # Production build to build/
npm run check # Type checking with svelte-check
When developing, run the Go backend separately with make dev from the project root.
Building for Production
Do not build in isolation. Always use make build from the project root — this builds the web frontend, then compiles the Go binary with the build output embedded via //go:embed.
Stack
- Svelte 5 with runes (
$state,$derived,$effect) - SvelteKit 2 with
adapter-static(SPA mode) - Tiptap block editor with markdown round-trip
- svelte-dnd-action for drag-and-drop in board/list views
- SSE for real-time updates
- TypeScript throughout
Structure
src/
routes/ SvelteKit pages
+layout.svelte App shell (sidebar + main)
+page.svelte Landing/redirect
[workspace]/
+page.svelte Dashboard (collections, phases, activity)
+layout.svelte SSE connection per workspace
[collection]/
+page.svelte Collection view (board/list)
[collection]/[item]/
+page.svelte Item detail + editor
conventions/ Purpose-built conventions page
playbooks/ Purpose-built playbooks page
settings/ Workspace settings
lib/
api/client.ts HTTP API client
components/
layout/ Sidebar, navigation
editor/ Tiptap editor, raw markdown editor
fields/ FieldEditor, relation picker
items/ ItemCard, ItemDetail
collections/ BoardView, ListView
common/ StatusBadge, badges, modals
search/ CommandPalette
stores/ Svelte 5 reactive stores
workspace.svelte.ts Workspace state
collections.svelte.ts Collection + item state
ui.svelte.ts Sidebar, mobile state
types/index.ts TypeScript types and constants
app.css Global styles and design tokens