Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ cd margin
npm run verify
```

This runs 35 unit and API smoke tests, type-checks the client and server, and produces the Vite production bundle.
This runs 41 unit and API smoke tests, type-checks the client and server, and produces the Vite production bundle.

## Production deployment

Expand Down
29 changes: 29 additions & 0 deletions docs/architecture/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Architecture overview

Margin is a local-first Markdown writing application with a React and CodeMirror client and an Express boundary for structured OpenAI operations.

## Authored and companion state

Markdown is the authored artifact. Findings, discussions, reviews, sources, corrections, proposals, and generated assets are companion state. Companion state may help interpret or revise a draft, but it never becomes prose without an explicit writer action.

The durable client architecture is organized around document identity:

```text
DocumentRecord
├── Markdown working snapshot
└── DocumentCompanionState
├── findings and discussions
├── sources and research
├── document review and brief
├── argument corrections
├── revision proposals
└── portable asset metadata
```

IndexedDB stores document records and companion state independently by document ID. A short initialization boundary loads the active session before mounting the editor. Switching documents remounts document-scoped UI state from the selected session, preventing ephemeral state from leaking between drafts.

## Direction

Subsequent releases add shared TextAnchors, claims and evidence, a local retrieval library, a controlled Revision Pass, editorial decision memory, and a content-addressed job scheduler. Each domain is extracted when its implementation issue is active rather than through a single large rewrite.

See [the roadmap](../roadmap.md) and [persistence architecture](persistence.md).
54 changes: 54 additions & 0 deletions docs/architecture/persistence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Persistence architecture

## Goals

- Every document has stable identity independent of title.
- Companion state is isolated per document and survives switching and reload.
- Legacy `margin.workspace.v1` data migrates without losing prose or editorial state.
- Markdown Save and Export semantics remain unchanged.
- Corrupt companion data degrades to empty safe state without discarding the document.

## Stores

The `margin.editorial.v1` IndexedDB database contains:

| Store | Key | Purpose |
| --- | --- | --- |
| `documents` | `id` | Identity, file metadata, content/fingerprint hashes, timestamps, and a local Markdown working snapshot. |
| `documentState` | `documentId` | Findings, discussions, sources, reviews, corrections, proposals, and assets. |
| `settings` | `key` | Active document identity and migration markers. |

The persistence domain lives under `margin/src/persistence/`. It exposes document/session operations rather than IndexedDB primitives to React.

## Why the record contains a working snapshot

The roadmap’s initial `DocumentRecord` sketch did not include Markdown content. Browser file handles are not reliably recoverable across reloads, and new unsaved documents have no external file at all. Margin therefore stores a local working snapshot on the document record.

This snapshot is recovery state, not a proprietary authored format. **Save Markdown** and package export continue to produce ordinary Markdown and assets; the companion database never silently writes model output into prose.

## Identity and recovery

- New documents receive UUIDs immediately.
- Built-in examples use stable reserved IDs so their companion state can be revisited.
- Open file handles map to document IDs for the current browser session.
- After reload, an exact fingerprint of normalized content plus filename may recover a record.
- A same-named file with different content is a new document. Margin does not guess or merge aggressively.
- Renaming changes metadata and fingerprint but never document ID.

## Migration

On first initialization:

1. Read `margin.workspace.v1` if it is valid.
2. Create one legacy-origin document record and companion record.
3. Mark it active and set `legacyWorkspaceV1Migrated=complete`.
4. Preserve the localStorage value unchanged for rollback during the 0.2 release line.
5. Stop writing legacy localStorage.

Malformed legacy data falls back to the starter document. Malformed IndexedDB companion fields are individually normalized to safe defaults.

## Save lifecycle

Document and companion records are written together in one IndexedDB transaction after a short debounce. Switching first flushes the current session, then activates the target. File Save additionally associates the selected file handle with the current document ID and refreshes record metadata.

Transient selections, open shelves, hover state, in-flight jobs, and provider cooldown remain ephemeral.
2 changes: 1 addition & 1 deletion docs/build-week-submission.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ Live research posed a different problem. Web work takes time, and the finished r
- In-draft editorial tasks that run in the background and return reviewable diffs with source provenance when needed.
- Tables, charts, diagrams, editorial images, and genuinely interactive widgets with portable fallbacks.
- Reviewable issue discussions and explicit applied-change diffs.
- A server-only OpenAI boundary, deterministic fallback, 35 automated checks, production build, and public deployment.
- A server-only OpenAI boundary, deterministic fallback, 41 automated checks, production build, and public deployment.

### What we learned

Expand Down
24 changes: 24 additions & 0 deletions docs/findings/2026-08-27-browser-file-handle-recovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Browser file handles are not document persistence

## Symptom

A document opened from disk can retain a writable file handle during one browser session, but Margin cannot rely on that object to reconstruct the active draft after reload. New unsaved drafts have no handle at all.

## Root cause

The File System Access API exposes useful handles, but their durable availability and permissions vary by browser and session. A `DocumentRecord` containing metadata alone cannot satisfy “reload and recover the active document.”

## Fix

Store a normalized Markdown working snapshot on each local `DocumentRecord`. Keep a weak in-session handle-to-document association for identity and direct saves. After reload, use an exact filename-plus-content fingerprint only as conservative recovery; ambiguous or changed files become new records.

## Regression tests added

- Reload restores the active document and its independent companion state.
- Same filename with different content does not merge.
- A known in-session file handle preserves identity while accepting newer file content.
- Renaming updates metadata without changing document ID.

## Broader design implication

External file access and local editorial continuity are different responsibilities. Margin needs a local working snapshot for continuity while keeping Markdown Save/Export as the portable authored artifact. Future sync work should preserve this separation rather than treating a browser handle as durable identity.
24 changes: 24 additions & 0 deletions docs/releases/0.2-durable-foundation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Margin 0.2 — Durable Foundation

Status: in progress

## PR 1: per-document persistence

- Added IndexedDB stores for documents, companion state, and settings.
- Added durable UUID identity for new/opened documents and stable identities for included examples.
- Added conservative fingerprint recovery for reopened files.
- Added a recent-documents section for returning to locally remembered drafts.
- Migrated the legacy global workspace once while retaining rollback data.
- Added an asynchronous initialization boundary and per-document editor remount.
- Preserved Markdown Save/Export behavior.

### Compatibility note

Document records contain a local Markdown working snapshot because browser file handles cannot reliably restore an editor after reload. Markdown remains the portable authored artifact; the snapshot is local recovery state.

## Remaining release work

- Shared durable TextAnchor passage identity ([#3](https://github.com/kbaxter/margin-app/issues/3)).
- CodeMirror mapping and migration of findings/argument claims ([#4](https://github.com/kbaxter/margin-app/issues/4)).

0.2 is not complete until the manual A/B document and anchor durability gate passes.
39 changes: 39 additions & 0 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Margin roadmap

Margin is evolving from a strong single-document writing surface into a durable editorial system whose understanding compounds across sessions and documents. This file is the canonical, versioned roadmap; GitHub issue [#1](https://github.com/kbaxter/margin-app/issues/1) tracks execution.

## Product invariants

1. Markdown remains the authored artifact. Companion state never silently becomes prose.
2. The writer remains in control. AI edits require review and acceptance.
3. Margin says fewer, better things.
4. Anchors are the shared passage-identity primitive.
5. Durability precedes intelligence.
6. Sources and evidence retain provenance; URLs, locators, and quotations are never invented.
7. Markdown and normal assets remain useful outside Margin.
8. Interaction stays tied to a document, passage, finding, claim, or review—not a global chat sidebar.
9. Model boundaries use structured outputs and validation.
10. Every pull request leaves `npm run verify` passing.

## Releases

| Release | Issues | Outcome |
| --- | --- | --- |
| 0.2 Durable Foundation | [#2](https://github.com/kbaxter/margin-app/issues/2)–[#4](https://github.com/kbaxter/margin-app/issues/4) | Documents retain independent editorial state; passage identity survives normal editing and reopen. |
| 0.3 Claims & Evidence | [#5](https://github.com/kbaxter/margin-app/issues/5)–[#7](https://github.com/kbaxter/margin-app/issues/7) | Margin tracks consequential claims, checks evidence, and notices stale support. |
| 0.4 Margin Library | [#8](https://github.com/kbaxter/margin-app/issues/8)–[#9](https://github.com/kbaxter/margin-app/issues/9) | Project and corpus context is searchable; Connect finds meaningful prior relationships. |
| 0.5 Revision Intelligence | [#10](https://github.com/kbaxter/margin-app/issues/10)–[#11](https://github.com/kbaxter/margin-app/issues/11) | Review becomes a controlled Revision Pass and learns from editorial decisions. |
| 0.6 Execution Layer | [#12](https://github.com/kbaxter/margin-app/issues/12) | Background model work is scheduled, cached, cancelled, and invalidated coherently. |

## Delivery rules

- One implementation issue per branch and normally one issue per pull request.
- Architecture documentation changes with the code.
- Domains are extracted incrementally as touched; no preliminary rewrite of `App.tsx` or `server/index.ts`.
- Focused domain tests accompany behavior changes.
- Before merge, run `npm ci` and `npm run verify`, then manually exercise changed UI behavior.
- Meaningful implementation discoveries belong in `docs/findings/`.

## Definition of success

A writer’s documents, claims, sources, disagreements, corrections, and accepted or rejected editorial decisions accumulate into a durable local editorial system. Margin remembers what a claim meant, what supported it, when support became stale, where related ideas appeared, and how the writer responds to intervention—while Markdown remains ordinary portable prose and no AI edit becomes authored text without the writer’s choice.
18 changes: 18 additions & 0 deletions margin/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions margin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@lezer/highlight": "^1.2.1",
"dotenv": "^16.6.1",
"express": "^5.1.0",
"idb": "^8.0.3",
"lucide-react": "^0.468.0",
"openai": "^5.20.1",
"react": "^19.1.1",
Expand All @@ -37,6 +38,7 @@
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^4.7.0",
"concurrently": "^9.2.1",
"fake-indexeddb": "^6.2.5",
"tsx": "^4.20.5",
"typescript": "~5.8.3",
"vite": "^6.1.0"
Expand Down
Loading
Loading