Skip to content

#268 — Atomic (transactional) plot save: commitPlotSave / reconcilePlotSave#658

Merged
IanMayo merged 18 commits into
mainfrom
claude/eloquent-fermi-bzLml
Jun 2, 2026
Merged

#268 — Atomic (transactional) plot save: commitPlotSave / reconcilePlotSave#658
IanMayo merged 18 commits into
mainfrom
claude/eloquent-fermi-bzLml

Conversation

@IanMayo

@IanMayo IanMayo commented Jun 1, 2026

Copy link
Copy Markdown
Member

Implements backlog item #268 — make a plot save all-or-nothing. Spec, plan, and tasks were authored earlier on this branch; this PR now carries the full implementation + evidence.

The problem

A "Save" is several writes — features.geojson, the STAC item.json, and the thumbnail PNGs. They landed independently and "Plot saved" appeared before the last of them committed. The feature-collection write was a raw fs.writeFileSync that bypassed the StacWriter boundary. So a failure or interruption mid-save could leave a half-updated plot (new tracks + stale thumbnail, a torn feature file, or a "saved" plot that never fully wrote).

The approach (ADR-039)

One host-agnostic boundary operation, commitPlotSave, commits the whole save unit atomically, plus reconcilePlotSave, called before the read on open, to heal an interrupted save. Enforced at the shared @debrief/stac-writer boundary so neither host can regress it (FR-009), and the FC write moves back onto the boundary (FR-004).

  • Desktop (filesystem): a write-ahead intent journal is the commit point — stage temps → atomically write .save-journal.json → apply renames → clear. Reconcile rolls back before the journal exists (discard temps, keep originals = last-good) and forward after it (finish the renames = new version). Every interruption point resolves to one coherent plot.
  • Browser (IndexedDB): one multi-store readwrite transaction (items + payloads) — atomic for free; a killed tab discards uncommitted work. The only gap was two writes in two transactions; now they share one. Reconcile is a clean no-op.
  • Honest reporting: markClean() + "Plot saved" moved to after the commit resolves; a rejected commit shows a failure, keeps the plot dirty, and leaves the previous version intact.
  • Durability target is deliberate: atomicity/coherence, not power-loss durability of the newest save — reconcile only ever chooses a coherent version, never repairs a torn one. No fsync machinery, no new dependencies.

What changed

Area Change
shared/stac-writer/src/interface.ts +commitPlotSave / +reconcilePlotSave + DTOs; thumbnails is Pick<WritePlotThumbnailPairInput,…>; featureCollection reuses RawGeoJSONFeatureCollection
apps/vscode/src/services/stacWriterFs.ts FS commitPlotSave (stage→journal→apply→clear) + reconcilePlotSave (roll back/forward, idempotent)
apps/vscode/src/services/saveJournal.ts FS-only SaveJournal type + typed parseSaveJournal() (no any)
apps/vscode/src/commands/saveSession.ts route the save through commitPlotSave; success only after commit
apps/vscode/src/commands/openPlot.ts + reconcileOnOpen.ts reconcile before the read; non-blocking recovery notice
apps/web-shell/src/services/stacWriterIdb.ts IDB commitPlotSave (one transaction) + reconcilePlotSave (clean)
apps/web-shell/src/mocks/stacService.ts createStandaloneItem + getPlotData route through commit / reconcile
docs/project_notes/decisions.md ADR-039

Scene-capture's eager features.geojson write is a documented, deliberate deferral (best-effort single-file write, superseded by Save Session) — see ADR-039.

Evidence (specs/268-save-atomicity/evidence/)

  • fault-injection-matrix.md — the credibility artifact: 8 filesystem + 4 IndexedDB interruption points each driven to a coherent outcome (SC-001/002/005).
  • test-summary.md, usage-example.md, and the feature post media/shipped-post.md.

Tests: 32 new (31 Vitest unit/integration + 1 web-shell Playwright smoke) + a mutation-verified compile-time Pick guard, all green. The IndexedDB save is proven to use exactly one transaction; "Plot saved" fires for 0% of saves that did not fully commit (SC-003).

Full-suite regression (same SHA, all green): @debrief/stac-writer 22, debrief-vscode 841 (+1 pre-existing skip), @debrief/web-shell 135 Vitest; pnpm -r typecheck + vscode tsc --noEmit + ESLint + ruff + pyright clean. No Python changed. (One timing-flaky shared/schemas perf-budget test passes on re-run.)

Constitution

Serves Article I.3 (silent partial save → explicit success/failure), Article IV.2/IV.4 (FC write back onto the boundary; one implementation per host), and Article IV.5 (boundary save-unit type structurally derived with a compile-time guard). No schema change. PASS.

Cloud session: branch pinned to claude/eloquent-fermi-bzLml; spec dir resolved via gitignored .specify/.active-feature.

https://claude.ai/code/session_01Q6WZYZRVLVmHdwpWLSNV5x

The `.debrief-session` sidecar was retired by #249/spec-261, so item
#268 ("VS Code save atomicity") no longer describes the code it cites:
saveSession.ts now performs two write phases (features.geojson +
thumbnail/STAC assets), not three, and the old line references
(163-208) and "FC<->sidecar pair" framing are dead.

Rewrite the row to reflect the current two-write flow with correct line
numbers, keep it as a parked transactional-save tech-debt item (the
non-atomic save risk survives the sidecar's removal), and note the
cross-host StacWriter angle that keeps complexity medium-to-high.
Trimmed the estimate to ~5-7 dev-days and bumped the Updated date.

https://claude.ai/code/session_01Q6WZYZRVLVmHdwpWLSNV5x
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

📋 Backlog Navigator

Review this PR's BACKLOG.md edits interactively:

👉 Open in Backlog Navigator

Updated for 850b864 — the navigator reads BACKLOG.md live from this PR's head branch, so no rebuild is needed.

@IanMayo IanMayo temporarily deployed to debrief-preview-pr-658 June 1, 2026 12:42 Inactive
Author the feature spec for backlog item #268 (rescoped in the previous
commit). Captures the real gap after the spec-261 sidecar retirement:
a save is several independent writes (feature collection + STAC item +
thumbnail assets), the feature-collection write bypasses the shared
persistence boundary with a non-atomic raw file write, and there is no
way to commit the writes as one unit — so a failure or interruption
mid-save can leave a partial / internally-inconsistent plot.

Spec defines all-or-nothing save semantics (US1), honest success/failure
reporting with safe retry (US2), and coherent recovery after an
uncatchable interruption (US3), enforced at the shared writer boundary so
both hosts benefit. Commit mechanism (staging+atomic-move vs last-good
recovery file) is deliberately left to /speckit.plan.

Also link the spec from BACKLOG #268 and move its status approved ->
specified (preserves the ID <-> spec-dir invariant). Spec dir is pinned
via .specify/.active-feature for this cloud session (branch stays
claude/eloquent-fermi-bzLml).

https://claude.ai/code/session_01Q6WZYZRVLVmHdwpWLSNV5x
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

📋 Spec Navigator

Review this PR's specs interactively:
👉 Open in Spec Navigator
Raw spec directories changed in this PR:

@IanMayo IanMayo temporarily deployed to debrief-preview-pr-658 June 1, 2026 12:53 Inactive
@IanMayo IanMayo changed the title docs(backlog): rescope #268 after spec-261 sidecar retirement #268 — rescope after spec-261 sidecar retirement + add Atomic Plot Save spec Jun 1, 2026
Record the /speckit.clarify Session 2026-06-01 decisions in the spec and
tighten the affected requirements:

- Host scope = BOTH hosts (desktop FS + browser storage), enforced at the
  shared persistence boundary (FR-009 already mandated this; now confirmed).
- Interrupted-save recovery = automatically restore the last good version
  + non-blocking notice, no analyst prompt (FR-008) — keeps this non-UI.
- Reliability target = atomicity with a coherent fallback, NOT guaranteed
  power-loss durability of the newest save (FR-005 reworded off "durably
  committed"; FR-007 clarified; removed the contradicting US2 wording).

Also: add the ## Clarifications section, update the checklist notes to
mark the two flagged assumptions resolved, and move BACKLOG #268 status
specified -> clarified. Commit mechanism stays deferred to /speckit.plan.

https://claude.ai/code/session_01Q6WZYZRVLVmHdwpWLSNV5x
@IanMayo IanMayo temporarily deployed to debrief-preview-pr-658 June 1, 2026 17:11 Inactive
/speckit.plan output for 268-save-atomicity. Resolves the deferred
commit-mechanism decision and designs the multi-write transaction at the
shared StacWriter boundary.

Artifacts:
- plan.md         — summary, technical context, constitution check (PASS,
                    no violations), source-file map, media=none
- research.md     — grounding (current save/load/writer call sites) +
                    8 decisions; FS write-ahead intent journal as the
                    atomic commit point, web-shell single IDB transaction
- data-model.md   — CommitPlotSaveInput/Result, SaveJournal, ReconcileResult
                    (boundary DTOs derived via Pick per Article IV.5)
- contracts/      — stac-writer-commit.ts (type contract, C1-C5 behaviour)
                    + save-flow.md (save + reconcile-on-open sequence)
- quickstart.md   — how to verify (fault-injection acceptance tests)
- evidence/opening-context.md — cached blog opener (mermaid Hook)

Design: add commitPlotSave + reconcilePlotSave to StacWriter; FS adaptor
stages temps -> writes journal (commit point) -> applies renames -> clears,
reconcile rolls back before the journal / forward after; web-shell wraps
the save in one multi-store transaction. saveSession reports success only
after commit; openPlot reconciles before the read. No new dependencies;
no schema change. BACKLOG #268 clarified -> planned.

https://claude.ai/code/session_01Q6WZYZRVLVmHdwpWLSNV5x
@IanMayo IanMayo temporarily deployed to debrief-preview-pr-658 June 1, 2026 17:25 Inactive
/speckit.tasks output for 268-save-atomicity. 31 tasks across 6 phases:

- Phase 1 Setup (2)        — ADR for the save-journal decision; fault-injection helper
- Phase 2 Foundation (4)   — extend StacWriter (commitPlotSave/reconcilePlotSave +
                             DTOs), SaveJournal type, stub doubles, Pick-derivation guard
- Phase 3 US1/P1 (8)       — commitPlotSave atomicity on both adaptors + route both
                             save call sites through it (the MVP)
- Phase 4 US2/P2 (3)       — honest reporting: success only after commit, retryable failure
- Phase 5 US3/P3 (7)       — reconcilePlotSave + reconcile-before-read on open + notice
- Phase 6 Polish (7)       — full gate, web-shell smoke, evidence (test-summary,
                             usage-example, fault-injection-matrix), blog post, PR

Tests-first per story (contracts C1-C5 <-> SC-001..005). Final task T031 runs
/speckit.pr. BACKLOG #268 planned -> tasked.

https://claude.ai/code/session_01Q6WZYZRVLVmHdwpWLSNV5x
@IanMayo IanMayo temporarily deployed to debrief-preview-pr-658 June 1, 2026 17:38 Inactive
claude added 8 commits June 1, 2026 19:57
…ave (#268)

Phase 1-2 (Setup + Foundation):
- ADR-039: write-ahead intent journal as the FS commit point; one IDB
  transaction on the browser host
- Add commitPlotSave/reconcilePlotSave + DTOs to the StacWriter interface
  (thumbnails derived via Pick; featureCollection reuses the generated
  RawGeoJSONFeatureCollection) and export them from the package index
- Add the FS-only SaveJournal type + typed parseSaveJournal() validator
- Add a shared Proxy-based fault-injection writer wrapper for the
  saveSession/open integration tests (auto-covers new write methods)
- Add a compile-time Pick-derivation guard (mutation-verified)
… it (#268 US1)

Phase 3 (User Story 1 — a failed save never corrupts my plot):
- FS adaptor commitPlotSave: stage temps → write intent journal (commit
  point) → apply renames → clear; pre-commit failure rolls back (originals
  byte-identical, temps cleaned); apply-phase failure leaves the journal for
  roll-forward on open. + buildItemWithThumbnails / applyJournalRenames helpers.
- IDB adaptor commitPlotSave: one readwrite transaction over items+payloads
  (+meta); resolves existing/bundled/minimal item; aborts roll back cleanly.
  reconcilePlotSave returns clean (IDB never exposes partial state).
- VS Code saveSession now commits FC + thumbnails through commitPlotSave in
  one unit (raw fs.writeFileSync of features.geojson gone — FR-004); success
  reported only after the commit resolves; capture failure is non-fatal.
- web-shell createStandaloneItem routes its item+payload write through
  commitPlotSave (closes the two-transaction gap).
- Re-export RawGeoJSONFeatureCollection from @debrief/stac-writer.
- Tests: FS commit (5), IDB commit (4), saveSession routing (4), saveSession
  integration via fault-injection helper (2). All green; typecheck clean.
Phase 4 (honest reporting):
- saveSession reporting-order test proves markClean + 'Plot saved' fire only
  after commitPlotSave resolves; a rejected commit shows failure, keeps dirty,
  shows no success (SC-003).
Phase 5 (coherent plot after an interrupted save):
- FS reconcilePlotSave: journal present → roll forward (apply pending renames
  idempotently, drop journal); stray temps + no journal → roll back to
  last-good; clean → no-op; malformed journal → safe roll-back. + listSaveTemps.
- IDB reconcilePlotSave: clean no-op (IndexedDB transactions are atomic).
- VS Code open path runs reconcileBeforeOpen (extracted module) before the
  read; non-blocking warning on recovery.
- web-shell getPlotData reconciles before the read (no-op, symmetric).
- Tests: FS reconcile (6), IDB reconcile (3), open-path integration (4). Green;
  both apps typecheck clean.
@IanMayo IanMayo temporarily deployed to debrief-preview-pr-658 June 1, 2026 20:56 Inactive
@IanMayo IanMayo changed the title #268 — rescope after spec-261 sidecar retirement + add Atomic Plot Save spec #268 — Atomic (transactional) plot save: commitPlotSave / reconcilePlotSave Jun 1, 2026
@IanMayo IanMayo temporarily deployed to debrief-preview-pr-658 June 1, 2026 21:00 Inactive
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

🚀 Preview Deployments

Code Server (Full VS Code Extension)

🖥️ Open Code Server

Browser-based VS Code with the Debrief extension and sample data pre-installed.

Web Shell (Standalone App)

📱 Open Web Shell

Use this for Playwright testing and demos - runs outside Storybook.

Storybook (Component Library)

📚 Open Storybook

Browse all components, stories, and documentation.

Briefing Renderer (Air-gapped Storyboard Player — spec #264)

🎬 Open Briefing Renderer

Standalone SPA that plays a Storyboard from a file://-origin zip. Loaded here on HTTPS as a static demo with the dev-fixture Storyboard (Channel Crossing — Demo Briefing).


All Links
Environment Code Server Web Shell Storybook Briefing Renderer
This PR Open IDE Open App Open Storybook Open Briefing
Main branch Open App Open Storybook Open Briefing

Updated on commit 5deb637

@IanMayo IanMayo temporarily deployed to debrief-preview-pr-658 June 1, 2026 21:01 Inactive
@IanMayo IanMayo temporarily deployed to debrief-preview-pr-658 June 1, 2026 21:03 Inactive
…-bzLml

# Conflicts:
#	apps/vscode/src/commands/saveSession.ts
@IanMayo IanMayo temporarily deployed to debrief-preview-pr-658 June 2, 2026 05:33 Inactive
@IanMayo IanMayo merged commit 3793a19 into main Jun 2, 2026
13 checks passed
@IanMayo IanMayo deleted the claude/eloquent-fermi-bzLml branch June 2, 2026 05:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants