Vibecode Scrivener
track this build4 phases, 12 steps, beginner friendly0%Scrivener's solo core is compact: build a local-first long-form authoring app with fast capture, full-text search, backlinks, tags, and Markdown export. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on polish, sync, importers.
You are building a lean indie version of Scrivener. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Scrivener · indie build A manuscript binder whose hard problems are keeping every document a readable file on disk with the index rebuildable from it, importing out of a proprietary package without mangling the text, and compiling many documents into one manuscript the same way every time. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript, everything local | a manuscript should not need a network | | Documents | One Markdown file per document in a project folder, with front matter | the text outlives this app only if it is readable without it | | Index | SQLite holding order, metadata and the search index, rebuildable from the folder | a database that can be deleted and regenerated is not a hostage | | Snapshots | A per-document history folder written before every destructive operation | the fear of losing a draft is what the paid product actually sells against | | Import | An RTF reader that honours the Unicode escape rules | this is where imported manuscripts get quietly mangled | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the interface half of the application. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A Rust toolchain and the Tauri prerequisites** · free - Why: Runs the desktop shell and the file operations that have to be atomic. - Get it: rustup.rs, then the platform packages listed in the Tauri prerequisites. - Verify: cargo --version prints a version and a bare Tauri app runs - [ ] **RTF fixtures with accents, a non-Latin script and nested groups** · free - Why: The import bug that matters is invisible in English prose and obvious in any manuscript with a name in it, so the fixtures have to contain both. - Get it: Save short documents from any word processor containing accented text, a non-Latin script and nested formatting, plus a plain text reference for each. - Verify: Each fixture imports byte identically to its reference - [ ] **A generated project of a few hundred documents** · free - Why: Search timing and index rebuild cost are invisible on a ten document test project and are the first thing a real novel exposes. - Get it: Generate documents of realistic length with a script, committed as a fixture generator rather than as files. - Verify: Search and rebuild both stay inside the stated times - [ ] **Your word counting rule and snapshot retention** · free - Why: Both are stated in the README and asserted by tests, and a counting rule invented per feature is how a writer stops trusting the numbers. - Get it: Write down how hyphenated words, contractions and numerals count, and how long snapshots are kept. - Verify: Both are in the README and named in tests ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multi-device sync. - Collaborative editing. - A print layout engine. - Writing back to the original project format. - conflict-safe multi-device sync - mature importers and exporters - collaborative editing and sharing - native mobile clients If one of those is essential to you, that is the reason to keep paying for Scrivener, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Scrivener The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a long-form manuscript binder whose documents are plain files on disk to replace Scrivener. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript, everything local · a manuscript should not need a network - Documents: One Markdown file per document in a project folder, with front matter · the text outlives this app only if it is readable without it - Index: SQLite holding order, metadata and the search index, rebuildable from the folder · a database that can be deleted and regenerated is not a hostage - Snapshots: A per-document history folder written before every destructive operation · the fear of losing a draft is what the paid product actually sells against - Import: An RTF reader that honours the Unicode escape rules · this is where imported manuscripts get quietly mangled ### Data model (create this before Phase 1) - projects(path, format_version, created_at, opened_at) · the folder is the project, the row is a convenience - documents(id, project_path, rel_path, title, ordinal, parent_id, kind, include_in_compile, label, status) - document files on disk: one Markdown file per document with YAML front matter carrying the same metadata - snapshots(document_id, taken_at, reason, path, bytes) · reason is manual, pre-compile, pre-import or pre-merge - targets(project_path, scope, words, deadline) · manuscript and session goals - search_index(document_id, content) · derived, dropped and rebuilt from the folder without asking - The folder is the source of truth. Deleting the database and reopening the project must reproduce the binder exactly, which is the test that keeps it true. ### Phase 1 · A project you can read with any text editor Build: Create a project as a folder: one Markdown file per document with front matter for title, identifier, order and compile flags, plus a folder for snapshots. Build the SQLite index by scanning the folder. Add a command that deletes the index and rebuilds it, and run it in the test suite after every structural operation. Trap: Keeping the text in the database and the folder as an export target means a corrupt database is a lost novel, and the writer discovers this on the morning the file will not open. Put the words in files, keep the database derived, and prove it by deleting and rebuilding the index in the tests. Done when: Deleting the index and reopening the project reproduces the binder order, titles and compile flags exactly, asserted after every structural operation in the suite. Every document is a file whose front matter matches the indexed metadata. Renaming a document in the binder renames the file predictably and the identifier does not change. A file added to the folder outside the app appears in the binder on the next open. Do not build yet: No editor beyond plain text, no snapshots, import or compile. ### Phase 2 · Snapshots, and writes that cannot half happen Build: Write every document through a temporary file that is verified readable before it replaces the original. Take a snapshot automatically before any merge, split, import or compile that modifies text, and on demand with a name. Keep snapshots under a stated retention and let a restore create a new document rather than overwriting the current one. Trap: Restoring a snapshot over the live document is how a writer loses this morning's work while trying to recover last week's, and it happens under exactly the stress that makes people click the wrong button. Restore into a new document beside the original and let the writer choose, and never delete on their behalf. Done when: Killing the process during a save leaves the previous file intact and readable, tested by killing it mid-write. A merge, split or import takes a snapshot first, verified by the snapshot reason recorded. Restoring a snapshot creates a new document and leaves the original untouched, tested. Snapshot retention is stated, enforced, and never removes the most recent snapshot of a document. Do not build yet: No import, compile or search yet. ### Phase 3 · Getting a manuscript out of the original Build: Import from the original's project package: an XML binder file naming the documents and a data folder holding one directory per document with its text as RTF. Write an RTF reader that handles the Unicode rules properly, then compare a round trip against a reference text file for the same chapter. Trap: RTF writes a non-ASCII character as a \uN escape followed by a fallback character for old readers, and a \ucN count says how many of those fallback characters to skip, scoped to the enclosing group and defaulting to one. A reader that ignores \uc emits both, so every accented word in an imported novel gains a stray letter, and since values above thirty two thousand seven hundred and sixty seven are written as negative numbers, a reader that treats them as unsigned mangles anything beyond the basic range. Keep a stack of counts and test with accented and non-Latin fixtures. Done when: An RTF fixture containing accented characters, a non-Latin script and nested groups with differing skip counts imports to text identical to the reference file, byte for byte. A document whose escape values exceed the signed range imports correctly, tested. Import takes a snapshot of anything it overwrites and never touches the source project. The binder order and titles from the imported package match the original structure, checked against a fixture. Do not build yet: No compile, search or targets yet. ### Phase 4 · Compile: many documents, one manuscript, the same way every time Build: Compile in binder order, honouring the include flag per document, with configurable separators per level and an optional title prefix. Substitute placeholders from project metadata. Write Markdown first, then one word processor format, and compare a second compile of unchanged text byte for byte. Trap: Building the manuscript from whatever the editor currently has in memory means a compile run while one document is unsaved silently drops a paragraph, and the file sent to an editor differs from the one on screen. Flush every document to disk, snapshot, then compile from the files only. Done when: Two compiles of unchanged documents produce byte identical files, asserted in the suite. A document with its include flag off is absent from the output and its absence is listed in the compile report. Compiling with an unsaved document flushes it first, tested by leaving an edit pending. Placeholders resolve from project metadata and an unresolved placeholder fails the compile naming the document. Do not build yet: No search, targets or backups yet. ### Phase 5 · Finding things in a large project, and getting out of this one Build: Index document text for search with results showing the document and surrounding line, and keep the index rebuildable. Count words per document, per session and against a target, counting the same way everywhere and saying which way that is. Export the whole project as a folder of Markdown files plus a manifest, and back the folder up on a schedule. Trap: Counting words one way in the editor, another way in the target bar and a third way at compile leaves a writer who hit their daily goal seeing three different numbers, and they will trust none of them. Write one counting function, state its rule in the README, and call it from everywhere including the tests. Done when: Search over a project of two hundred documents returns results within the stated time, measured on a generated fixture. Deleting the search index and reopening rebuilds it without user action. Every word count in the interface comes from one function and the counting rule is stated in the README, asserted by a test over a fixture with hyphens, contractions and numerals. Project export produces a folder readable without this app and reimports to the same binder. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-device sync, which is a conflict resolution problem that deserves its own build rather than a cloud folder and hope. - Collaborative editing, which changes the file-per-document model into an operation log and a merge algorithm. - A page-perfect print layout engine, since compiling to a word processor format hands that job to software that already does it. - Writing back into the original application's project format, which risks corrupting the file you imported from. ### README must contain - That the folder of Markdown files is the project and the database is a rebuildable index - That every destructive operation snapshots first and a restore creates a new document rather than overwriting - How RTF import handles Unicode escapes and their skip counts, and which fixtures prove it - That two compiles of unchanged documents are byte identical, and why that matters when sending a manuscript - The word counting rule, stated exactly, since every number in the interface uses it ===== AGENTS.md ===== # Agent instructions · Scrivener indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, everything local, One Markdown file per document in a project folder, with front matter, SQLite holding order, metadata and the search index, rebuildable from the folder, A per-document history folder written before every destructive operation, An RTF reader that honours the Unicode escape rules. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Keeping the text in the database and the folder as an export target means a corrupt database is a lost novel, and the writer discovers this on the morning the file will not open. Put the words in files, keep the database derived, and prove it by deleting and rebuilding the index in the tests. - Restoring a snapshot over the live document is how a writer loses this morning's work while trying to recover last week's, and it happens under exactly the stress that makes people click the wrong button. Restore into a new document beside the original and let the writer choose, and never delete on their behalf. - RTF writes a non-ASCII character as a \uN escape followed by a fallback character for old readers, and a \ucN count says how many of those fallback characters to skip, scoped to the enclosing group and defaulting to one. A reader that ignores \uc emits both, so every accented word in an imported novel gains a stray letter, and since values above thirty two thousand seven hundred and sixty seven are written as negative numbers, a reader that treats them as unsigned mangles anything beyond the basic range. Keep a stack of counts and test with accented and non-Latin fixtures. - Building the manuscript from whatever the editor currently has in memory means a compile run while one document is unsaved silently drops a paragraph, and the file sent to an editor differs from the one on screen. Flush every document to disk, snapshot, then compile from the files only. ===== BUILD_PLAN.md ===== # Build plan · Scrivener A manuscript binder whose hard problems are keeping every document a readable file on disk with the index rebuildable from it, importing out of a proprietary package without mangling the text, and compiling many documents into one manuscript the same way every time. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A project you can read with any text editor Every document is a file, and the index is derived from the folder rather than the reverse. ### Steps 1. Write the text to files, always The database is an index, and an index can be thrown away. Files: `src/project/files.ts`, `src/project/index.ts` 2. Rebuild the index from the folder in tests The only check that keeps the folder authoritative as the app grows. Files: `tests/rebuild-index.test.ts` 3. Keep identifiers stable across renames Links and snapshots point at identifiers, not at filenames. Files: `src/project/identifiers.ts` ### Done when - [ ] A deleted index rebuilds to the same binder - [ ] Front matter and index metadata agree - [ ] Renaming preserves the identifier - [ ] Files added outside the app appear on open ### Watch out - Keeping the text in the database and the folder as an export target means a corrupt database is a lost novel, and the writer discovers this on the morning the file will not open. Put the words in files, keep the database derived, and prove it by deleting and rebuilding the index in the tests. ## Phase 2 · Snapshots, and writes that cannot half happen No operation can leave a document shorter than it was without a recoverable copy. ### Steps 1. Verify then replace, never write in place A truncated chapter is indistinguishable from a deleted one. Files: `src/storage/atomic.ts` 2. Snapshot before anything destructive Automatically, with the reason recorded. Files: `src/snapshots/take.ts` 3. Restore beside, never over The recovery must not be the thing that loses the work. Files: `src/snapshots/restore.ts`, `tests/restore.test.ts` ### Done when - [ ] A kill mid-save leaves the previous file readable - [ ] Destructive operations snapshot first with a reason - [ ] Restore creates a new document - [ ] Retention never removes the most recent snapshot ### Watch out - Restoring a snapshot over the live document is how a writer loses this morning's work while trying to recover last week's, and it happens under exactly the stress that makes people click the wrong button. Restore into a new document beside the original and let the writer choose, and never delete on their behalf. ## Phase 3 · Getting a manuscript out of the original Importing a real project produces the same words, not almost the same words. ### Steps 1. Keep a stack of skip counts The count is scoped to the group, and restoring it on exit is the whole trick. Files: `src/import/rtf.ts`, `src/import/rtf.test.ts` 2. Read escape values as signed Otherwise everything outside the common range is garbage. Files: `src/import/rtf.ts` 3. Test against a reference text file Eyeballing an imported chapter proves nothing. Files: `tests/fixtures/chapter.rtf`, `tests/fixtures/chapter.txt` ### Done when - [ ] Accented and non-Latin fixtures import byte identically - [ ] Escape values beyond the signed range are handled - [ ] Import never modifies the source project - [ ] Imported binder order matches the package ### Watch out - RTF writes a non-ASCII character as a \uN escape followed by a fallback character for old readers, and a \ucN count says how many of those fallback characters to skip, scoped to the enclosing group and defaulting to one. A reader that ignores \uc emits both, so every accented word in an imported novel gains a stray letter, and since values above thirty two thousand seven hundred and sixty seven are written as negative numbers, a reader that treats them as unsigned mangles anything beyond the basic range. Keep a stack of counts and test with accented and non-Latin fixtures. ## Phase 4 · Compile: many documents, one manuscript, the same way every time Compiling twice from unchanged documents produces byte identical output. ### Steps 1. Compile from files, never from memory Flush first, snapshot, then read the folder. Files: `src/compile/compile.ts` 2. Make the output deterministic Two identical inputs, two identical files, or something is reading state it should not. Files: `tests/compile-deterministic.test.ts` 3. Report what was excluded A missing chapter should be a line in a report, not a surprise. Files: `src/compile/report.ts` ### Done when - [ ] Two compiles produce byte identical output - [ ] Excluded documents appear in the report - [ ] Unsaved edits are flushed before compiling - [ ] Unresolved placeholders fail the compile ### Watch out - Building the manuscript from whatever the editor currently has in memory means a compile run while one document is unsaved silently drops a paragraph, and the file sent to an editor differs from the one on screen. Flush every document to disk, snapshot, then compile from the files only. ## Not in this build - Multi-device sync. - Collaborative editing. - A print layout engine. - Writing back to the original project format. ## After v1, if you want it - A corkboard view over the same binder data, which adds no new source of truth - Cross-document links resolved by identifier, now that identifiers survive renames - A second compile target such as an e-book format, added behind the existing ordering ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Where project folders live. Back this up, not the index. PROJECTS_DIR=~/Documents/manuscripts # Required. Derived. Safe to delete; it rebuilds from the folder. INDEX_PATH=~/.local/share/binder/index.db # Required. Never removes the most recent snapshot of a document. SNAPSHOT_RETENTION_DAYS=180 # Required. Writes through a temporary file that is verified before replacing. AUTOSAVE_SECONDS=10 # Required. Scheduled copies of the whole project folder. BACKUP_DIR=~/Documents/manuscripts-backups # Required. The budget the search test enforces at project scale. SEARCH_MAX_MS=300
You are building a lean indie version of Scrivener. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Scrivener · indie build A manuscript binder whose hard problems are keeping every document a readable file on disk with the index rebuildable from it, importing out of a proprietary package without mangling the text, and compiling many documents into one manuscript the same way every time. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript, everything local | a manuscript should not need a network | | Documents | One Markdown file per document in a project folder, with front matter | the text outlives this app only if it is readable without it | | Index | SQLite holding order, metadata and the search index, rebuildable from the folder | a database that can be deleted and regenerated is not a hostage | | Snapshots | A per-document history folder written before every destructive operation | the fear of losing a draft is what the paid product actually sells against | | Import | An RTF reader that honours the Unicode escape rules | this is where imported manuscripts get quietly mangled | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the interface half of the application. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A Rust toolchain and the Tauri prerequisites** · free - Why: Runs the desktop shell and the file operations that have to be atomic. - Get it: rustup.rs, then the platform packages listed in the Tauri prerequisites. - Verify: cargo --version prints a version and a bare Tauri app runs - [ ] **RTF fixtures with accents, a non-Latin script and nested groups** · free - Why: The import bug that matters is invisible in English prose and obvious in any manuscript with a name in it, so the fixtures have to contain both. - Get it: Save short documents from any word processor containing accented text, a non-Latin script and nested formatting, plus a plain text reference for each. - Verify: Each fixture imports byte identically to its reference - [ ] **A generated project of a few hundred documents** · free - Why: Search timing and index rebuild cost are invisible on a ten document test project and are the first thing a real novel exposes. - Get it: Generate documents of realistic length with a script, committed as a fixture generator rather than as files. - Verify: Search and rebuild both stay inside the stated times - [ ] **Your word counting rule and snapshot retention** · free - Why: Both are stated in the README and asserted by tests, and a counting rule invented per feature is how a writer stops trusting the numbers. - Get it: Write down how hyphenated words, contractions and numerals count, and how long snapshots are kept. - Verify: Both are in the README and named in tests ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multi-device sync. - Collaborative editing. - A print layout engine. - Writing back to the original project format. - conflict-safe multi-device sync - mature importers and exporters - collaborative editing and sharing - native mobile clients If one of those is essential to you, that is the reason to keep paying for Scrivener, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Scrivener The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a long-form manuscript binder whose documents are plain files on disk to replace Scrivener. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript, everything local · a manuscript should not need a network - Documents: One Markdown file per document in a project folder, with front matter · the text outlives this app only if it is readable without it - Index: SQLite holding order, metadata and the search index, rebuildable from the folder · a database that can be deleted and regenerated is not a hostage - Snapshots: A per-document history folder written before every destructive operation · the fear of losing a draft is what the paid product actually sells against - Import: An RTF reader that honours the Unicode escape rules · this is where imported manuscripts get quietly mangled ### Data model (create this before Phase 1) - projects(path, format_version, created_at, opened_at) · the folder is the project, the row is a convenience - documents(id, project_path, rel_path, title, ordinal, parent_id, kind, include_in_compile, label, status) - document files on disk: one Markdown file per document with YAML front matter carrying the same metadata - snapshots(document_id, taken_at, reason, path, bytes) · reason is manual, pre-compile, pre-import or pre-merge - targets(project_path, scope, words, deadline) · manuscript and session goals - search_index(document_id, content) · derived, dropped and rebuilt from the folder without asking - The folder is the source of truth. Deleting the database and reopening the project must reproduce the binder exactly, which is the test that keeps it true. ### Phase 1 · A project you can read with any text editor Build: Create a project as a folder: one Markdown file per document with front matter for title, identifier, order and compile flags, plus a folder for snapshots. Build the SQLite index by scanning the folder. Add a command that deletes the index and rebuilds it, and run it in the test suite after every structural operation. Trap: Keeping the text in the database and the folder as an export target means a corrupt database is a lost novel, and the writer discovers this on the morning the file will not open. Put the words in files, keep the database derived, and prove it by deleting and rebuilding the index in the tests. Done when: Deleting the index and reopening the project reproduces the binder order, titles and compile flags exactly, asserted after every structural operation in the suite. Every document is a file whose front matter matches the indexed metadata. Renaming a document in the binder renames the file predictably and the identifier does not change. A file added to the folder outside the app appears in the binder on the next open. Do not build yet: No editor beyond plain text, no snapshots, import or compile. ### Phase 2 · Snapshots, and writes that cannot half happen Build: Write every document through a temporary file that is verified readable before it replaces the original. Take a snapshot automatically before any merge, split, import or compile that modifies text, and on demand with a name. Keep snapshots under a stated retention and let a restore create a new document rather than overwriting the current one. Trap: Restoring a snapshot over the live document is how a writer loses this morning's work while trying to recover last week's, and it happens under exactly the stress that makes people click the wrong button. Restore into a new document beside the original and let the writer choose, and never delete on their behalf. Done when: Killing the process during a save leaves the previous file intact and readable, tested by killing it mid-write. A merge, split or import takes a snapshot first, verified by the snapshot reason recorded. Restoring a snapshot creates a new document and leaves the original untouched, tested. Snapshot retention is stated, enforced, and never removes the most recent snapshot of a document. Do not build yet: No import, compile or search yet. ### Phase 3 · Getting a manuscript out of the original Build: Import from the original's project package: an XML binder file naming the documents and a data folder holding one directory per document with its text as RTF. Write an RTF reader that handles the Unicode rules properly, then compare a round trip against a reference text file for the same chapter. Trap: RTF writes a non-ASCII character as a \uN escape followed by a fallback character for old readers, and a \ucN count says how many of those fallback characters to skip, scoped to the enclosing group and defaulting to one. A reader that ignores \uc emits both, so every accented word in an imported novel gains a stray letter, and since values above thirty two thousand seven hundred and sixty seven are written as negative numbers, a reader that treats them as unsigned mangles anything beyond the basic range. Keep a stack of counts and test with accented and non-Latin fixtures. Done when: An RTF fixture containing accented characters, a non-Latin script and nested groups with differing skip counts imports to text identical to the reference file, byte for byte. A document whose escape values exceed the signed range imports correctly, tested. Import takes a snapshot of anything it overwrites and never touches the source project. The binder order and titles from the imported package match the original structure, checked against a fixture. Do not build yet: No compile, search or targets yet. ### Phase 4 · Compile: many documents, one manuscript, the same way every time Build: Compile in binder order, honouring the include flag per document, with configurable separators per level and an optional title prefix. Substitute placeholders from project metadata. Write Markdown first, then one word processor format, and compare a second compile of unchanged text byte for byte. Trap: Building the manuscript from whatever the editor currently has in memory means a compile run while one document is unsaved silently drops a paragraph, and the file sent to an editor differs from the one on screen. Flush every document to disk, snapshot, then compile from the files only. Done when: Two compiles of unchanged documents produce byte identical files, asserted in the suite. A document with its include flag off is absent from the output and its absence is listed in the compile report. Compiling with an unsaved document flushes it first, tested by leaving an edit pending. Placeholders resolve from project metadata and an unresolved placeholder fails the compile naming the document. Do not build yet: No search, targets or backups yet. ### Phase 5 · Finding things in a large project, and getting out of this one Build: Index document text for search with results showing the document and surrounding line, and keep the index rebuildable. Count words per document, per session and against a target, counting the same way everywhere and saying which way that is. Export the whole project as a folder of Markdown files plus a manifest, and back the folder up on a schedule. Trap: Counting words one way in the editor, another way in the target bar and a third way at compile leaves a writer who hit their daily goal seeing three different numbers, and they will trust none of them. Write one counting function, state its rule in the README, and call it from everywhere including the tests. Done when: Search over a project of two hundred documents returns results within the stated time, measured on a generated fixture. Deleting the search index and reopening rebuilds it without user action. Every word count in the interface comes from one function and the counting rule is stated in the README, asserted by a test over a fixture with hyphens, contractions and numerals. Project export produces a folder readable without this app and reimports to the same binder. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-device sync, which is a conflict resolution problem that deserves its own build rather than a cloud folder and hope. - Collaborative editing, which changes the file-per-document model into an operation log and a merge algorithm. - A page-perfect print layout engine, since compiling to a word processor format hands that job to software that already does it. - Writing back into the original application's project format, which risks corrupting the file you imported from. ### README must contain - That the folder of Markdown files is the project and the database is a rebuildable index - That every destructive operation snapshots first and a restore creates a new document rather than overwriting - How RTF import handles Unicode escapes and their skip counts, and which fixtures prove it - That two compiles of unchanged documents are byte identical, and why that matters when sending a manuscript - The word counting rule, stated exactly, since every number in the interface uses it ===== AGENTS.md ===== # Agent instructions · Scrivener indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, everything local, One Markdown file per document in a project folder, with front matter, SQLite holding order, metadata and the search index, rebuildable from the folder, A per-document history folder written before every destructive operation, An RTF reader that honours the Unicode escape rules. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Keeping the text in the database and the folder as an export target means a corrupt database is a lost novel, and the writer discovers this on the morning the file will not open. Put the words in files, keep the database derived, and prove it by deleting and rebuilding the index in the tests. - Restoring a snapshot over the live document is how a writer loses this morning's work while trying to recover last week's, and it happens under exactly the stress that makes people click the wrong button. Restore into a new document beside the original and let the writer choose, and never delete on their behalf. - RTF writes a non-ASCII character as a \uN escape followed by a fallback character for old readers, and a \ucN count says how many of those fallback characters to skip, scoped to the enclosing group and defaulting to one. A reader that ignores \uc emits both, so every accented word in an imported novel gains a stray letter, and since values above thirty two thousand seven hundred and sixty seven are written as negative numbers, a reader that treats them as unsigned mangles anything beyond the basic range. Keep a stack of counts and test with accented and non-Latin fixtures. - Building the manuscript from whatever the editor currently has in memory means a compile run while one document is unsaved silently drops a paragraph, and the file sent to an editor differs from the one on screen. Flush every document to disk, snapshot, then compile from the files only. ===== BUILD_PLAN.md ===== # Build plan · Scrivener A manuscript binder whose hard problems are keeping every document a readable file on disk with the index rebuildable from it, importing out of a proprietary package without mangling the text, and compiling many documents into one manuscript the same way every time. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A project you can read with any text editor Every document is a file, and the index is derived from the folder rather than the reverse. ### Steps 1. Write the text to files, always The database is an index, and an index can be thrown away. Files: `src/project/files.ts`, `src/project/index.ts` 2. Rebuild the index from the folder in tests The only check that keeps the folder authoritative as the app grows. Files: `tests/rebuild-index.test.ts` 3. Keep identifiers stable across renames Links and snapshots point at identifiers, not at filenames. Files: `src/project/identifiers.ts` ### Done when - [ ] A deleted index rebuilds to the same binder - [ ] Front matter and index metadata agree - [ ] Renaming preserves the identifier - [ ] Files added outside the app appear on open ### Watch out - Keeping the text in the database and the folder as an export target means a corrupt database is a lost novel, and the writer discovers this on the morning the file will not open. Put the words in files, keep the database derived, and prove it by deleting and rebuilding the index in the tests. ## Phase 2 · Snapshots, and writes that cannot half happen No operation can leave a document shorter than it was without a recoverable copy. ### Steps 1. Verify then replace, never write in place A truncated chapter is indistinguishable from a deleted one. Files: `src/storage/atomic.ts` 2. Snapshot before anything destructive Automatically, with the reason recorded. Files: `src/snapshots/take.ts` 3. Restore beside, never over The recovery must not be the thing that loses the work. Files: `src/snapshots/restore.ts`, `tests/restore.test.ts` ### Done when - [ ] A kill mid-save leaves the previous file readable - [ ] Destructive operations snapshot first with a reason - [ ] Restore creates a new document - [ ] Retention never removes the most recent snapshot ### Watch out - Restoring a snapshot over the live document is how a writer loses this morning's work while trying to recover last week's, and it happens under exactly the stress that makes people click the wrong button. Restore into a new document beside the original and let the writer choose, and never delete on their behalf. ## Phase 3 · Getting a manuscript out of the original Importing a real project produces the same words, not almost the same words. ### Steps 1. Keep a stack of skip counts The count is scoped to the group, and restoring it on exit is the whole trick. Files: `src/import/rtf.ts`, `src/import/rtf.test.ts` 2. Read escape values as signed Otherwise everything outside the common range is garbage. Files: `src/import/rtf.ts` 3. Test against a reference text file Eyeballing an imported chapter proves nothing. Files: `tests/fixtures/chapter.rtf`, `tests/fixtures/chapter.txt` ### Done when - [ ] Accented and non-Latin fixtures import byte identically - [ ] Escape values beyond the signed range are handled - [ ] Import never modifies the source project - [ ] Imported binder order matches the package ### Watch out - RTF writes a non-ASCII character as a \uN escape followed by a fallback character for old readers, and a \ucN count says how many of those fallback characters to skip, scoped to the enclosing group and defaulting to one. A reader that ignores \uc emits both, so every accented word in an imported novel gains a stray letter, and since values above thirty two thousand seven hundred and sixty seven are written as negative numbers, a reader that treats them as unsigned mangles anything beyond the basic range. Keep a stack of counts and test with accented and non-Latin fixtures. ## Phase 4 · Compile: many documents, one manuscript, the same way every time Compiling twice from unchanged documents produces byte identical output. ### Steps 1. Compile from files, never from memory Flush first, snapshot, then read the folder. Files: `src/compile/compile.ts` 2. Make the output deterministic Two identical inputs, two identical files, or something is reading state it should not. Files: `tests/compile-deterministic.test.ts` 3. Report what was excluded A missing chapter should be a line in a report, not a surprise. Files: `src/compile/report.ts` ### Done when - [ ] Two compiles produce byte identical output - [ ] Excluded documents appear in the report - [ ] Unsaved edits are flushed before compiling - [ ] Unresolved placeholders fail the compile ### Watch out - Building the manuscript from whatever the editor currently has in memory means a compile run while one document is unsaved silently drops a paragraph, and the file sent to an editor differs from the one on screen. Flush every document to disk, snapshot, then compile from the files only. ## Not in this build - Multi-device sync. - Collaborative editing. - A print layout engine. - Writing back to the original project format. ## After v1, if you want it - A corkboard view over the same binder data, which adds no new source of truth - Cross-document links resolved by identifier, now that identifiers survive renames - A second compile target such as an e-book format, added behind the existing ordering ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Where project folders live. Back this up, not the index. PROJECTS_DIR=~/Documents/manuscripts # Required. Derived. Safe to delete; it rebuilds from the folder. INDEX_PATH=~/.local/share/binder/index.db # Required. Never removes the most recent snapshot of a document. SNAPSHOT_RETENTION_DAYS=180 # Required. Writes through a temporary file that is verified before replacing. AUTOSAVE_SECONDS=10 # Required. Scheduled copies of the whole project folder. BACKUP_DIR=~/Documents/manuscripts-backups # Required. The budget the search test enforces at project scale. SEARCH_MAX_MS=300
You are building a production product version of Scrivener. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Scrivener · product brief ## Problem Scrivener's solo core is compact: build a local-first long-form authoring app with fast capture, full-text search, backlinks, tags, and Markdown export. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on polish, sync, importers. ## Product outcome A binder where the words are files anybody can read, no operation can lose a draft without leaving a snapshot, an imported manuscript keeps its accents, and the manuscript you compile today is byte for byte the one you compiled yesterday. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - desktop development machine - Node.js 22 and Rust toolchain - local filesystem access ## Explicit non-goals for v1 - Multi-device sync. - Collaborative editing. - A print layout engine. - Writing back to the original project format. - conflict-safe multi-device sync - mature importers and exporters - collaborative editing and sharing - native mobile clients ## Success criteria - A deleted index is confirmed to rebuild the same binder - A kill mid-save is confirmed to leave the previous file readable - Accented and non-Latin RTF fixtures are confirmed to import byte identically - Two compiles of unchanged documents are confirmed byte identical - Every word count is confirmed to come from one tested function ===== BRIEF.md ===== # Build brief · Scrivener The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a long-form manuscript binder whose documents are plain files on disk to replace Scrivener. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript, everything local · a manuscript should not need a network - Documents: One Markdown file per document in a project folder, with front matter · the text outlives this app only if it is readable without it - Index: SQLite holding order, metadata and the search index, rebuildable from the folder · a database that can be deleted and regenerated is not a hostage - Snapshots: A per-document history folder written before every destructive operation · the fear of losing a draft is what the paid product actually sells against - Import: An RTF reader that honours the Unicode escape rules · this is where imported manuscripts get quietly mangled ### Data model (create this before Phase 1) - projects(path, format_version, created_at, opened_at) · the folder is the project, the row is a convenience - documents(id, project_path, rel_path, title, ordinal, parent_id, kind, include_in_compile, label, status) - document files on disk: one Markdown file per document with YAML front matter carrying the same metadata - snapshots(document_id, taken_at, reason, path, bytes) · reason is manual, pre-compile, pre-import or pre-merge - targets(project_path, scope, words, deadline) · manuscript and session goals - search_index(document_id, content) · derived, dropped and rebuilt from the folder without asking - The folder is the source of truth. Deleting the database and reopening the project must reproduce the binder exactly, which is the test that keeps it true. ### Phase 1 · A project you can read with any text editor Build: Create a project as a folder: one Markdown file per document with front matter for title, identifier, order and compile flags, plus a folder for snapshots. Build the SQLite index by scanning the folder. Add a command that deletes the index and rebuilds it, and run it in the test suite after every structural operation. Trap: Keeping the text in the database and the folder as an export target means a corrupt database is a lost novel, and the writer discovers this on the morning the file will not open. Put the words in files, keep the database derived, and prove it by deleting and rebuilding the index in the tests. Done when: Deleting the index and reopening the project reproduces the binder order, titles and compile flags exactly, asserted after every structural operation in the suite. Every document is a file whose front matter matches the indexed metadata. Renaming a document in the binder renames the file predictably and the identifier does not change. A file added to the folder outside the app appears in the binder on the next open. Do not build yet: No editor beyond plain text, no snapshots, import or compile. ### Phase 2 · Snapshots, and writes that cannot half happen Build: Write every document through a temporary file that is verified readable before it replaces the original. Take a snapshot automatically before any merge, split, import or compile that modifies text, and on demand with a name. Keep snapshots under a stated retention and let a restore create a new document rather than overwriting the current one. Trap: Restoring a snapshot over the live document is how a writer loses this morning's work while trying to recover last week's, and it happens under exactly the stress that makes people click the wrong button. Restore into a new document beside the original and let the writer choose, and never delete on their behalf. Done when: Killing the process during a save leaves the previous file intact and readable, tested by killing it mid-write. A merge, split or import takes a snapshot first, verified by the snapshot reason recorded. Restoring a snapshot creates a new document and leaves the original untouched, tested. Snapshot retention is stated, enforced, and never removes the most recent snapshot of a document. Do not build yet: No import, compile or search yet. ### Phase 3 · Getting a manuscript out of the original Build: Import from the original's project package: an XML binder file naming the documents and a data folder holding one directory per document with its text as RTF. Write an RTF reader that handles the Unicode rules properly, then compare a round trip against a reference text file for the same chapter. Trap: RTF writes a non-ASCII character as a \uN escape followed by a fallback character for old readers, and a \ucN count says how many of those fallback characters to skip, scoped to the enclosing group and defaulting to one. A reader that ignores \uc emits both, so every accented word in an imported novel gains a stray letter, and since values above thirty two thousand seven hundred and sixty seven are written as negative numbers, a reader that treats them as unsigned mangles anything beyond the basic range. Keep a stack of counts and test with accented and non-Latin fixtures. Done when: An RTF fixture containing accented characters, a non-Latin script and nested groups with differing skip counts imports to text identical to the reference file, byte for byte. A document whose escape values exceed the signed range imports correctly, tested. Import takes a snapshot of anything it overwrites and never touches the source project. The binder order and titles from the imported package match the original structure, checked against a fixture. Do not build yet: No compile, search or targets yet. ### Phase 4 · Compile: many documents, one manuscript, the same way every time Build: Compile in binder order, honouring the include flag per document, with configurable separators per level and an optional title prefix. Substitute placeholders from project metadata. Write Markdown first, then one word processor format, and compare a second compile of unchanged text byte for byte. Trap: Building the manuscript from whatever the editor currently has in memory means a compile run while one document is unsaved silently drops a paragraph, and the file sent to an editor differs from the one on screen. Flush every document to disk, snapshot, then compile from the files only. Done when: Two compiles of unchanged documents produce byte identical files, asserted in the suite. A document with its include flag off is absent from the output and its absence is listed in the compile report. Compiling with an unsaved document flushes it first, tested by leaving an edit pending. Placeholders resolve from project metadata and an unresolved placeholder fails the compile naming the document. Do not build yet: No search, targets or backups yet. ### Phase 5 · Finding things in a large project, and getting out of this one Build: Index document text for search with results showing the document and surrounding line, and keep the index rebuildable. Count words per document, per session and against a target, counting the same way everywhere and saying which way that is. Export the whole project as a folder of Markdown files plus a manifest, and back the folder up on a schedule. Trap: Counting words one way in the editor, another way in the target bar and a third way at compile leaves a writer who hit their daily goal seeing three different numbers, and they will trust none of them. Write one counting function, state its rule in the README, and call it from everywhere including the tests. Done when: Search over a project of two hundred documents returns results within the stated time, measured on a generated fixture. Deleting the search index and reopening rebuilds it without user action. Every word count in the interface comes from one function and the counting rule is stated in the README, asserted by a test over a fixture with hyphens, contractions and numerals. Project export produces a folder readable without this app and reimports to the same binder. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-device sync, which is a conflict resolution problem that deserves its own build rather than a cloud folder and hope. - Collaborative editing, which changes the file-per-document model into an operation log and a merge algorithm. - A page-perfect print layout engine, since compiling to a word processor format hands that job to software that already does it. - Writing back into the original application's project format, which risks corrupting the file you imported from. ### README must contain - That the folder of Markdown files is the project and the database is a rebuildable index - That every destructive operation snapshots first and a restore creates a new document rather than overwriting - How RTF import handles Unicode escapes and their skip counts, and which fixtures prove it - That two compiles of unchanged documents are byte identical, and why that matters when sending a manuscript - The word counting rule, stated exactly, since every number in the interface uses it ===== ARCHITECTURE.md ===== # Architecture · Scrivener ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript, everything local | a manuscript should not need a network | | Documents | One Markdown file per document in a project folder, with front matter | the text outlives this app only if it is readable without it | | Index | SQLite holding order, metadata and the search index, rebuildable from the folder | a database that can be deleted and regenerated is not a hostage | | Snapshots | A per-document history folder written before every destructive operation | the fear of losing a draft is what the paid product actually sells against | | Import | An RTF reader that honours the Unicode escape rules | this is where imported manuscripts get quietly mangled | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Project | Files on disk and stable identifiers. | Any on-disk layout, provided the index stays derivable from it. | | Snapshots | Pre-operation copies and non-destructive restore. | Any storage, provided restore never overwrites the current document. | | Import | RTF reading with correct escape handling. | Any parser that passes the accented and non-Latin fixtures. | | Compile | Deterministic ordering and output. | Any output format added behind the same ordering. | | Search | A disposable index over document text. | Any engine, provided deleting the index is harmless. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PROJECTS_DIR` · required · Where project folders live. Back this up, not the index. - `INDEX_PATH` · required · Derived. Safe to delete; it rebuilds from the folder. - `SNAPSHOT_RETENTION_DAYS` · required · Never removes the most recent snapshot of a document. - `AUTOSAVE_SECONDS` · required · Writes through a temporary file that is verified before replacing. - `BACKUP_DIR` · required · Scheduled copies of the whole project folder. - `SEARCH_MAX_MS` · required · The budget the search test enforces at project scale. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Scrivener product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri 2 with React and TypeScript, everything local, One Markdown file per document in a project folder, with front matter, SQLite holding order, metadata and the search index, rebuildable from the folder, A per-document history folder written before every destructive operation, An RTF reader that honours the Unicode escape rules. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Keeping the text in the database and the folder as an export target means a corrupt database is a lost novel, and the writer discovers this on the morning the file will not open. Put the words in files, keep the database derived, and prove it by deleting and rebuilding the index in the tests. - Restoring a snapshot over the live document is how a writer loses this morning's work while trying to recover last week's, and it happens under exactly the stress that makes people click the wrong button. Restore into a new document beside the original and let the writer choose, and never delete on their behalf. - RTF writes a non-ASCII character as a \uN escape followed by a fallback character for old readers, and a \ucN count says how many of those fallback characters to skip, scoped to the enclosing group and defaulting to one. A reader that ignores \uc emits both, so every accented word in an imported novel gains a stray letter, and since values above thirty two thousand seven hundred and sixty seven are written as negative numbers, a reader that treats them as unsigned mangles anything beyond the basic range. Keep a stack of counts and test with accented and non-Latin fixtures. - Building the manuscript from whatever the editor currently has in memory means a compile run while one document is unsaved silently drops a paragraph, and the file sent to an editor differs from the one on screen. Flush every document to disk, snapshot, then compile from the files only. - Counting words one way in the editor, another way in the target bar and a third way at compile leaves a writer who hit their daily goal seeing three different numbers, and they will trust none of them. Write one counting function, state its rule in the README, and call it from everywhere including the tests. ===== MILESTONES.md ===== # Delivery milestones · Scrivener Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A project you can read with any text editor Every document is a file, and the index is derived from the folder rather than the reverse. ### Steps 1. Write the text to files, always The database is an index, and an index can be thrown away. Files: `src/project/files.ts`, `src/project/index.ts` 2. Rebuild the index from the folder in tests The only check that keeps the folder authoritative as the app grows. Files: `tests/rebuild-index.test.ts` 3. Keep identifiers stable across renames Links and snapshots point at identifiers, not at filenames. Files: `src/project/identifiers.ts` ### Done when - [ ] A deleted index rebuilds to the same binder - [ ] Front matter and index metadata agree - [ ] Renaming preserves the identifier - [ ] Files added outside the app appear on open ### Watch out - Keeping the text in the database and the folder as an export target means a corrupt database is a lost novel, and the writer discovers this on the morning the file will not open. Put the words in files, keep the database derived, and prove it by deleting and rebuilding the index in the tests. ## M2 · Snapshots, and writes that cannot half happen No operation can leave a document shorter than it was without a recoverable copy. ### Steps 1. Verify then replace, never write in place A truncated chapter is indistinguishable from a deleted one. Files: `src/storage/atomic.ts` 2. Snapshot before anything destructive Automatically, with the reason recorded. Files: `src/snapshots/take.ts` 3. Restore beside, never over The recovery must not be the thing that loses the work. Files: `src/snapshots/restore.ts`, `tests/restore.test.ts` ### Done when - [ ] A kill mid-save leaves the previous file readable - [ ] Destructive operations snapshot first with a reason - [ ] Restore creates a new document - [ ] Retention never removes the most recent snapshot ### Watch out - Restoring a snapshot over the live document is how a writer loses this morning's work while trying to recover last week's, and it happens under exactly the stress that makes people click the wrong button. Restore into a new document beside the original and let the writer choose, and never delete on their behalf. ## M3 · Getting a manuscript out of the original Importing a real project produces the same words, not almost the same words. ### Steps 1. Keep a stack of skip counts The count is scoped to the group, and restoring it on exit is the whole trick. Files: `src/import/rtf.ts`, `src/import/rtf.test.ts` 2. Read escape values as signed Otherwise everything outside the common range is garbage. Files: `src/import/rtf.ts` 3. Test against a reference text file Eyeballing an imported chapter proves nothing. Files: `tests/fixtures/chapter.rtf`, `tests/fixtures/chapter.txt` ### Done when - [ ] Accented and non-Latin fixtures import byte identically - [ ] Escape values beyond the signed range are handled - [ ] Import never modifies the source project - [ ] Imported binder order matches the package ### Watch out - RTF writes a non-ASCII character as a \uN escape followed by a fallback character for old readers, and a \ucN count says how many of those fallback characters to skip, scoped to the enclosing group and defaulting to one. A reader that ignores \uc emits both, so every accented word in an imported novel gains a stray letter, and since values above thirty two thousand seven hundred and sixty seven are written as negative numbers, a reader that treats them as unsigned mangles anything beyond the basic range. Keep a stack of counts and test with accented and non-Latin fixtures. ## M4 · Compile: many documents, one manuscript, the same way every time Compiling twice from unchanged documents produces byte identical output. ### Steps 1. Compile from files, never from memory Flush first, snapshot, then read the folder. Files: `src/compile/compile.ts` 2. Make the output deterministic Two identical inputs, two identical files, or something is reading state it should not. Files: `tests/compile-deterministic.test.ts` 3. Report what was excluded A missing chapter should be a line in a report, not a surprise. Files: `src/compile/report.ts` ### Done when - [ ] Two compiles produce byte identical output - [ ] Excluded documents appear in the report - [ ] Unsaved edits are flushed before compiling - [ ] Unresolved placeholders fail the compile ### Watch out - Building the manuscript from whatever the editor currently has in memory means a compile run while one document is unsaved silently drops a paragraph, and the file sent to an editor differs from the one on screen. Flush every document to disk, snapshot, then compile from the files only. ## M5 · Finding things in a large project, and getting out of this one (production only) Search stays fast at novel length, word counts are honest, and export needs no special tool. ### Steps 1. Keep the search index disposable It should never be the reason a project will not open. Files: `src/search/index.ts` 2. One word counting function, called everywhere Three counts of the same chapter is worse than no count. Files: `src/stats/words.ts`, `src/stats/words.test.ts` 3. Export a folder anyone can read Plus a manifest, so it comes back in the same order. Files: `src/export/project.ts` ### Done when - [ ] Search is within the stated time at project scale - [ ] A deleted search index rebuilds silently - [ ] All word counts come from one tested function - [ ] Export reimports to the same binder ### Watch out - Counting words one way in the editor, another way in the target bar and a third way at compile leaves a writer who hit their daily goal seeing three different numbers, and they will trust none of them. Write one counting function, state its rule in the README, and call it from everywhere including the tests. ===== OPERATIONS.md ===== # Operations · Scrivener ## Backup The project folder, which is the entire manuscript including snapshots. The index is deliberately excluded so that restoring a backup cannot restore a stale binder over good files. ## Restore Restore the folder, open the project, and let the index rebuild. Then compile and compare against the last compiled output to confirm nothing was lost in the round trip. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a desktop application this is the status bar and the suite: time since the last successful autosave, snapshot count and age per document, and search time at project scale. ## Incident checklist A document that will not open is the incident. Recovery order is the newest snapshot, then the backup folder, then the temporary file from an interrupted write, which is why writes never happen in place. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A deleted index is confirmed to rebuild the same binder - [ ] A kill mid-save is confirmed to leave the previous file readable - [ ] Accented and non-Latin RTF fixtures are confirmed to import byte identically - [ ] Two compiles of unchanged documents are confirmed byte identical - [ ] Every word count is confirmed to come from one tested function ## Launch constraint Do not market omitted Scrivener capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Where project folders live. Back this up, not the index. PROJECTS_DIR=~/Documents/manuscripts # Required. Derived. Safe to delete; it rebuilds from the folder. INDEX_PATH=~/.local/share/binder/index.db # Required. Never removes the most recent snapshot of a document. SNAPSHOT_RETENTION_DAYS=180 # Required. Writes through a temporary file that is verified before replacing. AUTOSAVE_SECONDS=10 # Required. Scheduled copies of the whole project folder. BACKUP_DIR=~/Documents/manuscripts-backups # Required. The budget the search test enforces at project scale. SEARCH_MAX_MS=300
# Scrivener · indie build A manuscript binder whose hard problems are keeping every document a readable file on disk with the index rebuildable from it, importing out of a proprietary package without mangling the text, and compiling many documents into one manuscript the same way every time. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript, everything local | a manuscript should not need a network | | Documents | One Markdown file per document in a project folder, with front matter | the text outlives this app only if it is readable without it | | Index | SQLite holding order, metadata and the search index, rebuildable from the folder | a database that can be deleted and regenerated is not a hostage | | Snapshots | A per-document history folder written before every destructive operation | the fear of losing a draft is what the paid product actually sells against | | Import | An RTF reader that honours the Unicode escape rules | this is where imported manuscripts get quietly mangled | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the interface half of the application. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A Rust toolchain and the Tauri prerequisites** · free - Why: Runs the desktop shell and the file operations that have to be atomic. - Get it: rustup.rs, then the platform packages listed in the Tauri prerequisites. - Verify: cargo --version prints a version and a bare Tauri app runs - [ ] **RTF fixtures with accents, a non-Latin script and nested groups** · free - Why: The import bug that matters is invisible in English prose and obvious in any manuscript with a name in it, so the fixtures have to contain both. - Get it: Save short documents from any word processor containing accented text, a non-Latin script and nested formatting, plus a plain text reference for each. - Verify: Each fixture imports byte identically to its reference - [ ] **A generated project of a few hundred documents** · free - Why: Search timing and index rebuild cost are invisible on a ten document test project and are the first thing a real novel exposes. - Get it: Generate documents of realistic length with a script, committed as a fixture generator rather than as files. - Verify: Search and rebuild both stay inside the stated times - [ ] **Your word counting rule and snapshot retention** · free - Why: Both are stated in the README and asserted by tests, and a counting rule invented per feature is how a writer stops trusting the numbers. - Get it: Write down how hyphenated words, contractions and numerals count, and how long snapshots are kept. - Verify: Both are in the README and named in tests ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multi-device sync. - Collaborative editing. - A print layout engine. - Writing back to the original project format. - conflict-safe multi-device sync - mature importers and exporters - collaborative editing and sharing - native mobile clients If one of those is essential to you, that is the reason to keep paying for Scrivener, and the README should say so rather than pretend.
# Build brief · Scrivener The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a long-form manuscript binder whose documents are plain files on disk to replace Scrivener. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript, everything local · a manuscript should not need a network - Documents: One Markdown file per document in a project folder, with front matter · the text outlives this app only if it is readable without it - Index: SQLite holding order, metadata and the search index, rebuildable from the folder · a database that can be deleted and regenerated is not a hostage - Snapshots: A per-document history folder written before every destructive operation · the fear of losing a draft is what the paid product actually sells against - Import: An RTF reader that honours the Unicode escape rules · this is where imported manuscripts get quietly mangled ### Data model (create this before Phase 1) - projects(path, format_version, created_at, opened_at) · the folder is the project, the row is a convenience - documents(id, project_path, rel_path, title, ordinal, parent_id, kind, include_in_compile, label, status) - document files on disk: one Markdown file per document with YAML front matter carrying the same metadata - snapshots(document_id, taken_at, reason, path, bytes) · reason is manual, pre-compile, pre-import or pre-merge - targets(project_path, scope, words, deadline) · manuscript and session goals - search_index(document_id, content) · derived, dropped and rebuilt from the folder without asking - The folder is the source of truth. Deleting the database and reopening the project must reproduce the binder exactly, which is the test that keeps it true. ### Phase 1 · A project you can read with any text editor Build: Create a project as a folder: one Markdown file per document with front matter for title, identifier, order and compile flags, plus a folder for snapshots. Build the SQLite index by scanning the folder. Add a command that deletes the index and rebuilds it, and run it in the test suite after every structural operation. Trap: Keeping the text in the database and the folder as an export target means a corrupt database is a lost novel, and the writer discovers this on the morning the file will not open. Put the words in files, keep the database derived, and prove it by deleting and rebuilding the index in the tests. Done when: Deleting the index and reopening the project reproduces the binder order, titles and compile flags exactly, asserted after every structural operation in the suite. Every document is a file whose front matter matches the indexed metadata. Renaming a document in the binder renames the file predictably and the identifier does not change. A file added to the folder outside the app appears in the binder on the next open. Do not build yet: No editor beyond plain text, no snapshots, import or compile. ### Phase 2 · Snapshots, and writes that cannot half happen Build: Write every document through a temporary file that is verified readable before it replaces the original. Take a snapshot automatically before any merge, split, import or compile that modifies text, and on demand with a name. Keep snapshots under a stated retention and let a restore create a new document rather than overwriting the current one. Trap: Restoring a snapshot over the live document is how a writer loses this morning's work while trying to recover last week's, and it happens under exactly the stress that makes people click the wrong button. Restore into a new document beside the original and let the writer choose, and never delete on their behalf. Done when: Killing the process during a save leaves the previous file intact and readable, tested by killing it mid-write. A merge, split or import takes a snapshot first, verified by the snapshot reason recorded. Restoring a snapshot creates a new document and leaves the original untouched, tested. Snapshot retention is stated, enforced, and never removes the most recent snapshot of a document. Do not build yet: No import, compile or search yet. ### Phase 3 · Getting a manuscript out of the original Build: Import from the original's project package: an XML binder file naming the documents and a data folder holding one directory per document with its text as RTF. Write an RTF reader that handles the Unicode rules properly, then compare a round trip against a reference text file for the same chapter. Trap: RTF writes a non-ASCII character as a \uN escape followed by a fallback character for old readers, and a \ucN count says how many of those fallback characters to skip, scoped to the enclosing group and defaulting to one. A reader that ignores \uc emits both, so every accented word in an imported novel gains a stray letter, and since values above thirty two thousand seven hundred and sixty seven are written as negative numbers, a reader that treats them as unsigned mangles anything beyond the basic range. Keep a stack of counts and test with accented and non-Latin fixtures. Done when: An RTF fixture containing accented characters, a non-Latin script and nested groups with differing skip counts imports to text identical to the reference file, byte for byte. A document whose escape values exceed the signed range imports correctly, tested. Import takes a snapshot of anything it overwrites and never touches the source project. The binder order and titles from the imported package match the original structure, checked against a fixture. Do not build yet: No compile, search or targets yet. ### Phase 4 · Compile: many documents, one manuscript, the same way every time Build: Compile in binder order, honouring the include flag per document, with configurable separators per level and an optional title prefix. Substitute placeholders from project metadata. Write Markdown first, then one word processor format, and compare a second compile of unchanged text byte for byte. Trap: Building the manuscript from whatever the editor currently has in memory means a compile run while one document is unsaved silently drops a paragraph, and the file sent to an editor differs from the one on screen. Flush every document to disk, snapshot, then compile from the files only. Done when: Two compiles of unchanged documents produce byte identical files, asserted in the suite. A document with its include flag off is absent from the output and its absence is listed in the compile report. Compiling with an unsaved document flushes it first, tested by leaving an edit pending. Placeholders resolve from project metadata and an unresolved placeholder fails the compile naming the document. Do not build yet: No search, targets or backups yet. ### Phase 5 · Finding things in a large project, and getting out of this one Build: Index document text for search with results showing the document and surrounding line, and keep the index rebuildable. Count words per document, per session and against a target, counting the same way everywhere and saying which way that is. Export the whole project as a folder of Markdown files plus a manifest, and back the folder up on a schedule. Trap: Counting words one way in the editor, another way in the target bar and a third way at compile leaves a writer who hit their daily goal seeing three different numbers, and they will trust none of them. Write one counting function, state its rule in the README, and call it from everywhere including the tests. Done when: Search over a project of two hundred documents returns results within the stated time, measured on a generated fixture. Deleting the search index and reopening rebuilds it without user action. Every word count in the interface comes from one function and the counting rule is stated in the README, asserted by a test over a fixture with hyphens, contractions and numerals. Project export produces a folder readable without this app and reimports to the same binder. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-device sync, which is a conflict resolution problem that deserves its own build rather than a cloud folder and hope. - Collaborative editing, which changes the file-per-document model into an operation log and a merge algorithm. - A page-perfect print layout engine, since compiling to a word processor format hands that job to software that already does it. - Writing back into the original application's project format, which risks corrupting the file you imported from. ### README must contain - That the folder of Markdown files is the project and the database is a rebuildable index - That every destructive operation snapshots first and a restore creates a new document rather than overwriting - How RTF import handles Unicode escapes and their skip counts, and which fixtures prove it - That two compiles of unchanged documents are byte identical, and why that matters when sending a manuscript - The word counting rule, stated exactly, since every number in the interface uses it
# Agent instructions · Scrivener indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, everything local, One Markdown file per document in a project folder, with front matter, SQLite holding order, metadata and the search index, rebuildable from the folder, A per-document history folder written before every destructive operation, An RTF reader that honours the Unicode escape rules. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Keeping the text in the database and the folder as an export target means a corrupt database is a lost novel, and the writer discovers this on the morning the file will not open. Put the words in files, keep the database derived, and prove it by deleting and rebuilding the index in the tests. - Restoring a snapshot over the live document is how a writer loses this morning's work while trying to recover last week's, and it happens under exactly the stress that makes people click the wrong button. Restore into a new document beside the original and let the writer choose, and never delete on their behalf. - RTF writes a non-ASCII character as a \uN escape followed by a fallback character for old readers, and a \ucN count says how many of those fallback characters to skip, scoped to the enclosing group and defaulting to one. A reader that ignores \uc emits both, so every accented word in an imported novel gains a stray letter, and since values above thirty two thousand seven hundred and sixty seven are written as negative numbers, a reader that treats them as unsigned mangles anything beyond the basic range. Keep a stack of counts and test with accented and non-Latin fixtures. - Building the manuscript from whatever the editor currently has in memory means a compile run while one document is unsaved silently drops a paragraph, and the file sent to an editor differs from the one on screen. Flush every document to disk, snapshot, then compile from the files only.
# Build plan · Scrivener A manuscript binder whose hard problems are keeping every document a readable file on disk with the index rebuildable from it, importing out of a proprietary package without mangling the text, and compiling many documents into one manuscript the same way every time. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A project you can read with any text editor Every document is a file, and the index is derived from the folder rather than the reverse. ### Steps 1. Write the text to files, always The database is an index, and an index can be thrown away. Files: `src/project/files.ts`, `src/project/index.ts` 2. Rebuild the index from the folder in tests The only check that keeps the folder authoritative as the app grows. Files: `tests/rebuild-index.test.ts` 3. Keep identifiers stable across renames Links and snapshots point at identifiers, not at filenames. Files: `src/project/identifiers.ts` ### Done when - [ ] A deleted index rebuilds to the same binder - [ ] Front matter and index metadata agree - [ ] Renaming preserves the identifier - [ ] Files added outside the app appear on open ### Watch out - Keeping the text in the database and the folder as an export target means a corrupt database is a lost novel, and the writer discovers this on the morning the file will not open. Put the words in files, keep the database derived, and prove it by deleting and rebuilding the index in the tests. ## Phase 2 · Snapshots, and writes that cannot half happen No operation can leave a document shorter than it was without a recoverable copy. ### Steps 1. Verify then replace, never write in place A truncated chapter is indistinguishable from a deleted one. Files: `src/storage/atomic.ts` 2. Snapshot before anything destructive Automatically, with the reason recorded. Files: `src/snapshots/take.ts` 3. Restore beside, never over The recovery must not be the thing that loses the work. Files: `src/snapshots/restore.ts`, `tests/restore.test.ts` ### Done when - [ ] A kill mid-save leaves the previous file readable - [ ] Destructive operations snapshot first with a reason - [ ] Restore creates a new document - [ ] Retention never removes the most recent snapshot ### Watch out - Restoring a snapshot over the live document is how a writer loses this morning's work while trying to recover last week's, and it happens under exactly the stress that makes people click the wrong button. Restore into a new document beside the original and let the writer choose, and never delete on their behalf. ## Phase 3 · Getting a manuscript out of the original Importing a real project produces the same words, not almost the same words. ### Steps 1. Keep a stack of skip counts The count is scoped to the group, and restoring it on exit is the whole trick. Files: `src/import/rtf.ts`, `src/import/rtf.test.ts` 2. Read escape values as signed Otherwise everything outside the common range is garbage. Files: `src/import/rtf.ts` 3. Test against a reference text file Eyeballing an imported chapter proves nothing. Files: `tests/fixtures/chapter.rtf`, `tests/fixtures/chapter.txt` ### Done when - [ ] Accented and non-Latin fixtures import byte identically - [ ] Escape values beyond the signed range are handled - [ ] Import never modifies the source project - [ ] Imported binder order matches the package ### Watch out - RTF writes a non-ASCII character as a \uN escape followed by a fallback character for old readers, and a \ucN count says how many of those fallback characters to skip, scoped to the enclosing group and defaulting to one. A reader that ignores \uc emits both, so every accented word in an imported novel gains a stray letter, and since values above thirty two thousand seven hundred and sixty seven are written as negative numbers, a reader that treats them as unsigned mangles anything beyond the basic range. Keep a stack of counts and test with accented and non-Latin fixtures. ## Phase 4 · Compile: many documents, one manuscript, the same way every time Compiling twice from unchanged documents produces byte identical output. ### Steps 1. Compile from files, never from memory Flush first, snapshot, then read the folder. Files: `src/compile/compile.ts` 2. Make the output deterministic Two identical inputs, two identical files, or something is reading state it should not. Files: `tests/compile-deterministic.test.ts` 3. Report what was excluded A missing chapter should be a line in a report, not a surprise. Files: `src/compile/report.ts` ### Done when - [ ] Two compiles produce byte identical output - [ ] Excluded documents appear in the report - [ ] Unsaved edits are flushed before compiling - [ ] Unresolved placeholders fail the compile ### Watch out - Building the manuscript from whatever the editor currently has in memory means a compile run while one document is unsaved silently drops a paragraph, and the file sent to an editor differs from the one on screen. Flush every document to disk, snapshot, then compile from the files only. ## Not in this build - Multi-device sync. - Collaborative editing. - A print layout engine. - Writing back to the original project format. ## After v1, if you want it - A corkboard view over the same binder data, which adds no new source of truth - Cross-document links resolved by identifier, now that identifiers survive renames - A second compile target such as an e-book format, added behind the existing ordering
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Where project folders live. Back this up, not the index. PROJECTS_DIR=~/Documents/manuscripts # Required. Derived. Safe to delete; it rebuilds from the folder. INDEX_PATH=~/.local/share/binder/index.db # Required. Never removes the most recent snapshot of a document. SNAPSHOT_RETENTION_DAYS=180 # Required. Writes through a temporary file that is verified before replacing. AUTOSAVE_SECONDS=10 # Required. Scheduled copies of the whole project folder. BACKUP_DIR=~/Documents/manuscripts-backups # Required. The budget the search test enforces at project scale. SEARCH_MAX_MS=300
# Scrivener · product brief ## Problem Scrivener's solo core is compact: build a local-first long-form authoring app with fast capture, full-text search, backlinks, tags, and Markdown export. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on polish, sync, importers. ## Product outcome A binder where the words are files anybody can read, no operation can lose a draft without leaving a snapshot, an imported manuscript keeps its accents, and the manuscript you compile today is byte for byte the one you compiled yesterday. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - desktop development machine - Node.js 22 and Rust toolchain - local filesystem access ## Explicit non-goals for v1 - Multi-device sync. - Collaborative editing. - A print layout engine. - Writing back to the original project format. - conflict-safe multi-device sync - mature importers and exporters - collaborative editing and sharing - native mobile clients ## Success criteria - A deleted index is confirmed to rebuild the same binder - A kill mid-save is confirmed to leave the previous file readable - Accented and non-Latin RTF fixtures are confirmed to import byte identically - Two compiles of unchanged documents are confirmed byte identical - Every word count is confirmed to come from one tested function
# Build brief · Scrivener The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a long-form manuscript binder whose documents are plain files on disk to replace Scrivener. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript, everything local · a manuscript should not need a network - Documents: One Markdown file per document in a project folder, with front matter · the text outlives this app only if it is readable without it - Index: SQLite holding order, metadata and the search index, rebuildable from the folder · a database that can be deleted and regenerated is not a hostage - Snapshots: A per-document history folder written before every destructive operation · the fear of losing a draft is what the paid product actually sells against - Import: An RTF reader that honours the Unicode escape rules · this is where imported manuscripts get quietly mangled ### Data model (create this before Phase 1) - projects(path, format_version, created_at, opened_at) · the folder is the project, the row is a convenience - documents(id, project_path, rel_path, title, ordinal, parent_id, kind, include_in_compile, label, status) - document files on disk: one Markdown file per document with YAML front matter carrying the same metadata - snapshots(document_id, taken_at, reason, path, bytes) · reason is manual, pre-compile, pre-import or pre-merge - targets(project_path, scope, words, deadline) · manuscript and session goals - search_index(document_id, content) · derived, dropped and rebuilt from the folder without asking - The folder is the source of truth. Deleting the database and reopening the project must reproduce the binder exactly, which is the test that keeps it true. ### Phase 1 · A project you can read with any text editor Build: Create a project as a folder: one Markdown file per document with front matter for title, identifier, order and compile flags, plus a folder for snapshots. Build the SQLite index by scanning the folder. Add a command that deletes the index and rebuilds it, and run it in the test suite after every structural operation. Trap: Keeping the text in the database and the folder as an export target means a corrupt database is a lost novel, and the writer discovers this on the morning the file will not open. Put the words in files, keep the database derived, and prove it by deleting and rebuilding the index in the tests. Done when: Deleting the index and reopening the project reproduces the binder order, titles and compile flags exactly, asserted after every structural operation in the suite. Every document is a file whose front matter matches the indexed metadata. Renaming a document in the binder renames the file predictably and the identifier does not change. A file added to the folder outside the app appears in the binder on the next open. Do not build yet: No editor beyond plain text, no snapshots, import or compile. ### Phase 2 · Snapshots, and writes that cannot half happen Build: Write every document through a temporary file that is verified readable before it replaces the original. Take a snapshot automatically before any merge, split, import or compile that modifies text, and on demand with a name. Keep snapshots under a stated retention and let a restore create a new document rather than overwriting the current one. Trap: Restoring a snapshot over the live document is how a writer loses this morning's work while trying to recover last week's, and it happens under exactly the stress that makes people click the wrong button. Restore into a new document beside the original and let the writer choose, and never delete on their behalf. Done when: Killing the process during a save leaves the previous file intact and readable, tested by killing it mid-write. A merge, split or import takes a snapshot first, verified by the snapshot reason recorded. Restoring a snapshot creates a new document and leaves the original untouched, tested. Snapshot retention is stated, enforced, and never removes the most recent snapshot of a document. Do not build yet: No import, compile or search yet. ### Phase 3 · Getting a manuscript out of the original Build: Import from the original's project package: an XML binder file naming the documents and a data folder holding one directory per document with its text as RTF. Write an RTF reader that handles the Unicode rules properly, then compare a round trip against a reference text file for the same chapter. Trap: RTF writes a non-ASCII character as a \uN escape followed by a fallback character for old readers, and a \ucN count says how many of those fallback characters to skip, scoped to the enclosing group and defaulting to one. A reader that ignores \uc emits both, so every accented word in an imported novel gains a stray letter, and since values above thirty two thousand seven hundred and sixty seven are written as negative numbers, a reader that treats them as unsigned mangles anything beyond the basic range. Keep a stack of counts and test with accented and non-Latin fixtures. Done when: An RTF fixture containing accented characters, a non-Latin script and nested groups with differing skip counts imports to text identical to the reference file, byte for byte. A document whose escape values exceed the signed range imports correctly, tested. Import takes a snapshot of anything it overwrites and never touches the source project. The binder order and titles from the imported package match the original structure, checked against a fixture. Do not build yet: No compile, search or targets yet. ### Phase 4 · Compile: many documents, one manuscript, the same way every time Build: Compile in binder order, honouring the include flag per document, with configurable separators per level and an optional title prefix. Substitute placeholders from project metadata. Write Markdown first, then one word processor format, and compare a second compile of unchanged text byte for byte. Trap: Building the manuscript from whatever the editor currently has in memory means a compile run while one document is unsaved silently drops a paragraph, and the file sent to an editor differs from the one on screen. Flush every document to disk, snapshot, then compile from the files only. Done when: Two compiles of unchanged documents produce byte identical files, asserted in the suite. A document with its include flag off is absent from the output and its absence is listed in the compile report. Compiling with an unsaved document flushes it first, tested by leaving an edit pending. Placeholders resolve from project metadata and an unresolved placeholder fails the compile naming the document. Do not build yet: No search, targets or backups yet. ### Phase 5 · Finding things in a large project, and getting out of this one Build: Index document text for search with results showing the document and surrounding line, and keep the index rebuildable. Count words per document, per session and against a target, counting the same way everywhere and saying which way that is. Export the whole project as a folder of Markdown files plus a manifest, and back the folder up on a schedule. Trap: Counting words one way in the editor, another way in the target bar and a third way at compile leaves a writer who hit their daily goal seeing three different numbers, and they will trust none of them. Write one counting function, state its rule in the README, and call it from everywhere including the tests. Done when: Search over a project of two hundred documents returns results within the stated time, measured on a generated fixture. Deleting the search index and reopening rebuilds it without user action. Every word count in the interface comes from one function and the counting rule is stated in the README, asserted by a test over a fixture with hyphens, contractions and numerals. Project export produces a folder readable without this app and reimports to the same binder. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-device sync, which is a conflict resolution problem that deserves its own build rather than a cloud folder and hope. - Collaborative editing, which changes the file-per-document model into an operation log and a merge algorithm. - A page-perfect print layout engine, since compiling to a word processor format hands that job to software that already does it. - Writing back into the original application's project format, which risks corrupting the file you imported from. ### README must contain - That the folder of Markdown files is the project and the database is a rebuildable index - That every destructive operation snapshots first and a restore creates a new document rather than overwriting - How RTF import handles Unicode escapes and their skip counts, and which fixtures prove it - That two compiles of unchanged documents are byte identical, and why that matters when sending a manuscript - The word counting rule, stated exactly, since every number in the interface uses it
# Architecture · Scrivener ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript, everything local | a manuscript should not need a network | | Documents | One Markdown file per document in a project folder, with front matter | the text outlives this app only if it is readable without it | | Index | SQLite holding order, metadata and the search index, rebuildable from the folder | a database that can be deleted and regenerated is not a hostage | | Snapshots | A per-document history folder written before every destructive operation | the fear of losing a draft is what the paid product actually sells against | | Import | An RTF reader that honours the Unicode escape rules | this is where imported manuscripts get quietly mangled | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Project | Files on disk and stable identifiers. | Any on-disk layout, provided the index stays derivable from it. | | Snapshots | Pre-operation copies and non-destructive restore. | Any storage, provided restore never overwrites the current document. | | Import | RTF reading with correct escape handling. | Any parser that passes the accented and non-Latin fixtures. | | Compile | Deterministic ordering and output. | Any output format added behind the same ordering. | | Search | A disposable index over document text. | Any engine, provided deleting the index is harmless. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PROJECTS_DIR` · required · Where project folders live. Back this up, not the index. - `INDEX_PATH` · required · Derived. Safe to delete; it rebuilds from the folder. - `SNAPSHOT_RETENTION_DAYS` · required · Never removes the most recent snapshot of a document. - `AUTOSAVE_SECONDS` · required · Writes through a temporary file that is verified before replacing. - `BACKUP_DIR` · required · Scheduled copies of the whole project folder. - `SEARCH_MAX_MS` · required · The budget the search test enforces at project scale. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Scrivener product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri 2 with React and TypeScript, everything local, One Markdown file per document in a project folder, with front matter, SQLite holding order, metadata and the search index, rebuildable from the folder, A per-document history folder written before every destructive operation, An RTF reader that honours the Unicode escape rules. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Keeping the text in the database and the folder as an export target means a corrupt database is a lost novel, and the writer discovers this on the morning the file will not open. Put the words in files, keep the database derived, and prove it by deleting and rebuilding the index in the tests. - Restoring a snapshot over the live document is how a writer loses this morning's work while trying to recover last week's, and it happens under exactly the stress that makes people click the wrong button. Restore into a new document beside the original and let the writer choose, and never delete on their behalf. - RTF writes a non-ASCII character as a \uN escape followed by a fallback character for old readers, and a \ucN count says how many of those fallback characters to skip, scoped to the enclosing group and defaulting to one. A reader that ignores \uc emits both, so every accented word in an imported novel gains a stray letter, and since values above thirty two thousand seven hundred and sixty seven are written as negative numbers, a reader that treats them as unsigned mangles anything beyond the basic range. Keep a stack of counts and test with accented and non-Latin fixtures. - Building the manuscript from whatever the editor currently has in memory means a compile run while one document is unsaved silently drops a paragraph, and the file sent to an editor differs from the one on screen. Flush every document to disk, snapshot, then compile from the files only. - Counting words one way in the editor, another way in the target bar and a third way at compile leaves a writer who hit their daily goal seeing three different numbers, and they will trust none of them. Write one counting function, state its rule in the README, and call it from everywhere including the tests.
# Delivery milestones · Scrivener Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A project you can read with any text editor Every document is a file, and the index is derived from the folder rather than the reverse. ### Steps 1. Write the text to files, always The database is an index, and an index can be thrown away. Files: `src/project/files.ts`, `src/project/index.ts` 2. Rebuild the index from the folder in tests The only check that keeps the folder authoritative as the app grows. Files: `tests/rebuild-index.test.ts` 3. Keep identifiers stable across renames Links and snapshots point at identifiers, not at filenames. Files: `src/project/identifiers.ts` ### Done when - [ ] A deleted index rebuilds to the same binder - [ ] Front matter and index metadata agree - [ ] Renaming preserves the identifier - [ ] Files added outside the app appear on open ### Watch out - Keeping the text in the database and the folder as an export target means a corrupt database is a lost novel, and the writer discovers this on the morning the file will not open. Put the words in files, keep the database derived, and prove it by deleting and rebuilding the index in the tests. ## M2 · Snapshots, and writes that cannot half happen No operation can leave a document shorter than it was without a recoverable copy. ### Steps 1. Verify then replace, never write in place A truncated chapter is indistinguishable from a deleted one. Files: `src/storage/atomic.ts` 2. Snapshot before anything destructive Automatically, with the reason recorded. Files: `src/snapshots/take.ts` 3. Restore beside, never over The recovery must not be the thing that loses the work. Files: `src/snapshots/restore.ts`, `tests/restore.test.ts` ### Done when - [ ] A kill mid-save leaves the previous file readable - [ ] Destructive operations snapshot first with a reason - [ ] Restore creates a new document - [ ] Retention never removes the most recent snapshot ### Watch out - Restoring a snapshot over the live document is how a writer loses this morning's work while trying to recover last week's, and it happens under exactly the stress that makes people click the wrong button. Restore into a new document beside the original and let the writer choose, and never delete on their behalf. ## M3 · Getting a manuscript out of the original Importing a real project produces the same words, not almost the same words. ### Steps 1. Keep a stack of skip counts The count is scoped to the group, and restoring it on exit is the whole trick. Files: `src/import/rtf.ts`, `src/import/rtf.test.ts` 2. Read escape values as signed Otherwise everything outside the common range is garbage. Files: `src/import/rtf.ts` 3. Test against a reference text file Eyeballing an imported chapter proves nothing. Files: `tests/fixtures/chapter.rtf`, `tests/fixtures/chapter.txt` ### Done when - [ ] Accented and non-Latin fixtures import byte identically - [ ] Escape values beyond the signed range are handled - [ ] Import never modifies the source project - [ ] Imported binder order matches the package ### Watch out - RTF writes a non-ASCII character as a \uN escape followed by a fallback character for old readers, and a \ucN count says how many of those fallback characters to skip, scoped to the enclosing group and defaulting to one. A reader that ignores \uc emits both, so every accented word in an imported novel gains a stray letter, and since values above thirty two thousand seven hundred and sixty seven are written as negative numbers, a reader that treats them as unsigned mangles anything beyond the basic range. Keep a stack of counts and test with accented and non-Latin fixtures. ## M4 · Compile: many documents, one manuscript, the same way every time Compiling twice from unchanged documents produces byte identical output. ### Steps 1. Compile from files, never from memory Flush first, snapshot, then read the folder. Files: `src/compile/compile.ts` 2. Make the output deterministic Two identical inputs, two identical files, or something is reading state it should not. Files: `tests/compile-deterministic.test.ts` 3. Report what was excluded A missing chapter should be a line in a report, not a surprise. Files: `src/compile/report.ts` ### Done when - [ ] Two compiles produce byte identical output - [ ] Excluded documents appear in the report - [ ] Unsaved edits are flushed before compiling - [ ] Unresolved placeholders fail the compile ### Watch out - Building the manuscript from whatever the editor currently has in memory means a compile run while one document is unsaved silently drops a paragraph, and the file sent to an editor differs from the one on screen. Flush every document to disk, snapshot, then compile from the files only. ## M5 · Finding things in a large project, and getting out of this one (production only) Search stays fast at novel length, word counts are honest, and export needs no special tool. ### Steps 1. Keep the search index disposable It should never be the reason a project will not open. Files: `src/search/index.ts` 2. One word counting function, called everywhere Three counts of the same chapter is worse than no count. Files: `src/stats/words.ts`, `src/stats/words.test.ts` 3. Export a folder anyone can read Plus a manifest, so it comes back in the same order. Files: `src/export/project.ts` ### Done when - [ ] Search is within the stated time at project scale - [ ] A deleted search index rebuilds silently - [ ] All word counts come from one tested function - [ ] Export reimports to the same binder ### Watch out - Counting words one way in the editor, another way in the target bar and a third way at compile leaves a writer who hit their daily goal seeing three different numbers, and they will trust none of them. Write one counting function, state its rule in the README, and call it from everywhere including the tests.
# Operations · Scrivener ## Backup The project folder, which is the entire manuscript including snapshots. The index is deliberately excluded so that restoring a backup cannot restore a stale binder over good files. ## Restore Restore the folder, open the project, and let the index rebuild. Then compile and compare against the last compiled output to confirm nothing was lost in the round trip. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a desktop application this is the status bar and the suite: time since the last successful autosave, snapshot count and age per document, and search time at project scale. ## Incident checklist A document that will not open is the incident. Recovery order is the newest snapshot, then the backup folder, then the temporary file from an interrupted write, which is why writes never happen in place. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A deleted index is confirmed to rebuild the same binder - [ ] A kill mid-save is confirmed to leave the previous file readable - [ ] Accented and non-Latin RTF fixtures are confirmed to import byte identically - [ ] Two compiles of unchanged documents are confirmed byte identical - [ ] Every word count is confirmed to come from one tested function ## Launch constraint Do not market omitted Scrivener capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Where project folders live. Back this up, not the index. PROJECTS_DIR=~/Documents/manuscripts # Required. Derived. Safe to delete; it rebuilds from the folder. INDEX_PATH=~/.local/share/binder/index.db # Required. Never removes the most recent snapshot of a document. SNAPSHOT_RETENTION_DAYS=180 # Required. Writes through a temporary file that is verified before replacing. AUTOSAVE_SECONDS=10 # Required. Scheduled copies of the whole project folder. BACKUP_DIR=~/Documents/manuscripts-backups # Required. The budget the search test enforces at project scale. SEARCH_MAX_MS=300
$ choose a build depth, inspect the files, then open the complete pack in your agent
Scrivener: People pay for a writing surface they trust for years, plus migration tools, sync, and tiny interaction details that disappear into habit.
xconflict-safe multi-device sync
xmature importers and exporters
xcollaborative editing and sharing
xnative mobile clients
Don't feel like building it? These folks already made it free.
all 3 free alternatives to Scrivener →· no votes, no pay-to-list · just what's real
Scrivener pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| standard macos licence | custom | — | 1 macOS major-version licence; usable on multiple computers owned by the licensee or household. |
| educational macos licence | custom | — | 1 macOS major-version licence; education eligibility required. |
| standard windows licence | custom | — | 1 Windows major-version licence; usable on multiple computers owned by the licensee or household. |
| educational windows licence | custom | — | 1 Windows major-version licence; education eligibility required. |
| ios/ipados app | custom | — | 1 universal iPhone/iPad purchase. |
| macos + windows bundle | custom | — | 2 desktop OS licences: 1 macOS licence and 1 Windows licence. |
free tierno free tier; trial lasts 30 days of actual use
billingone-time perpetual licences only; no monthly or annual subscription plan
hidden costsmacOS, Windows and iOS/iPadOS are separate purchases unless the desktop bundle is bought; paid major-version upgrades may be required; Dropbox is required for supported desktop-to-iOS sync
verified 2026-08-13 · source ↗
Vibecode Scrivener
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Scrivener replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Scrivener cost?
Scrivener's pricing is usage-based or varies by plan · The official pricing URL is recorded, but a stable current amount was not safely recoverable in this pass. Do not publish a number until rechecked..
What do I lose by replacing Scrivener?
Honestly: conflict-safe multi-device sync; mature importers and exporters; collaborative editing and sharing; native mobile clients. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Scrivener?
Yes: novelWriter (A manuscript binder made of plain text, with fewer knobs than Scrivener.) Manuskript (Scrivener's broad outline, rebuilt by volunteers and left with rougher edges.) Kindling (A tiny new Scrivener-shaped app that imports the real thing and keeps the outline beside the draft.) All 3 curated free alternatives are at vibecodeit.com/scrivener/alternatives. The prompt is for when you want it exactly your way.