Vibecode Fabric
track this build4 phases, 12 steps, beginner friendly0%The visible AI file workspace loop is buildable, but a credible replacement needs more than the first screen. Fabric earns its keep through polish, sync, importers, so expect a weekend or multi-day build and a narrower personal scope.
You are building a lean indie version of Fabric. 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 ===== # Fabric · indie build A local file workspace whose hard problems are keeping the originals untouched so the index is always disposable, getting text out of formats that resist it, and making search find the thing you half remember rather than returning nothing. 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 | a desktop app with filesystem access and a small binary | | Index | SQLite with FTS5, stored beside the library but deletable | the index is derived data and must be rebuildable from the files alone | | Originals | Left in place, never moved or renamed by the app | the day you stop using this, your files should be exactly where you put them | | Extraction | One module per format, run in a worker, failures recorded not swallowed | a PDF that yields no text is a fact the user needs to see | | Embeddings | Optional, local or key-provided, added only after keyword search works | semantic search on top of broken extraction finds nothing either | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **The Rust toolchain and Node.js 22** · free - Why: Tauri needs both, and the extraction workers live on the Rust side where the time and memory ceilings are enforceable. - Get it: rustup.rs for Rust, nodejs.org or nvm for Node. - Verify: cargo --version and node --version both print, and a Tauri dev build launches - [ ] **A real folder of your own files** · free - Why: Every hard part here is a format question, and a folder of sample text files will pass tests that your actual PDFs, scans and exports fail. - Get it: Point the app at a folder you genuinely use, on a copy first if you are nervous. - Verify: The scan finds at least a few hundred files across at least four formats - [ ] **One scanned PDF with no text layer** · free - Why: The empty-extraction case is the single most common silent failure in this category and it needs a real fixture rather than a synthetic one. - Get it: Scan a page, or find one in your own archive. - Verify: Extraction yields zero characters and the file appears in the failure list - [ ] **A library of at least fifty thousand files for the scale test** · free - Why: Scan, rebuild and search times are the claims this build makes, and they are only credible measured at a size that hurts. - Get it: Your real library if it is large enough, otherwise generate one from public documents. - Verify: A full rebuild completes and the time is recorded - [ ] **The FTS5 tokenizer choice** · free - Why: It decides whether search can match inside a word and what the minimum query length is, and changing it later means a full reindex. - Get it: Look at what you actually store, read the tokenizer documentation, and write down the choice with the reason. - Verify: The choice is in the schema and the README and is covered by a substring test ## 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 and sharing. - Native mobile clients. - Automatic filing that moves files. - collaborative editing and sharing - native mobile clients - conflict-safe multi-device sync - mature importers and exporters If one of those is essential to you, that is the reason to keep paying for Fabric, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Fabric 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 local workspace that indexes everything you drop into it and leaves the files where you can find them without it to replace Fabric. 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 · a desktop app with filesystem access and a small binary - Index: SQLite with FTS5, stored beside the library but deletable · the index is derived data and must be rebuildable from the files alone - Originals: Left in place, never moved or renamed by the app · the day you stop using this, your files should be exactly where you put them - Extraction: One module per format, run in a worker, failures recorded not swallowed · a PDF that yields no text is a fact the user needs to see - Embeddings: Optional, local or key-provided, added only after keyword search works · semantic search on top of broken extraction finds nothing either ### Data model (create this before Phase 1) - files(id, path, size, mtime, content_sha256, first_seen_at, last_seen_at, missing_since) · path is a reference, never a copy - documents(file_id, kind, text, extracted_at, extractor, extractor_version, char_count) · the text pulled out, with what pulled it - extraction_failures(file_id, at, extractor, reason, retryable) · visible in the interface, not only in a log - fts(document rowid, text) · an FTS5 table whose tokenizer choice is recorded in the schema and in the README - notes(id, file_id, body, created_at, updated_at) · your own writing, which is the one thing here that is not derived - tags(file_id, tag, source, confidence) · source is manual or suggested, and a suggested tag never becomes manual by itself - embeddings(document_id, model, dim, vector) · present only if you enabled them, and rebuildable like everything else ### Phase 1 · Files stay files Build: Watch one or more folders, record path, size, modification time and content hash, and never move, rename or rewrite an original. Detect a file that moved by its hash rather than treating it as a deletion and an addition. Handle a file that disappears by marking it missing rather than deleting the notes attached to it. Trap: An app that takes ownership of files by copying them into its own store makes the store the only real copy, and then a bug in the store is a bug in your documents. Keep the originals where the user put them, treat the database as a cache, and add a rebuild command in this phase so the claim is tested from the start. Done when: Deleting the index database and rebuilding produces the same file list and the same notes attached to the same files, tested end to end. Moving a file on disk is detected by hash and keeps its notes and tags rather than orphaning them. A file that disappears is marked missing and its notes survive. No code path writes to, renames or moves a file in a watched folder, verified by review and by a read-only mount test. Do not build yet: No extraction, search, tags or embeddings. ### Phase 2 · Getting the text out, and admitting when you cannot Build: One extractor per format for the types you actually keep, run in a worker with a time limit and a memory ceiling. Record the extractor and its version on every document. A failure writes a row the interface shows, with a reason a person can act on and a retry button. Trap: A scanned PDF with no text layer extracts to an empty string, and unless you treat that as a failure it silently becomes a file that search will never find, which the user discovers months later while looking for the one document they needed. Treat a zero-length extraction from a format that should have text as a failure with a stated reason, and offer optical recognition as an explicit, opt-in, per-file action. Done when: A scanned PDF appears in the failure list with a reason rather than as an indexed empty document, tested with a real scan. An extractor that hangs is killed at the time limit and recorded as a failure rather than blocking the queue, tested with a deliberately slow fixture. Every document records which extractor and version produced it, so a later improvement can requeue exactly the affected files. Retrying a failure from the interface re-runs only that file. Do not build yet: No search, tags or embeddings yet. ### Phase 3 · Search that finds the thing you half remember Build: Build the FTS5 index and choose the tokenizer on purpose. The default splits on word boundaries and will not match inside a word, so a search for part of a filename or an identifier returns nothing; the trigram tokenizer matches substrings and also makes LIKE and GLOB indexed, but substrings shorter than three characters match no rows at all (checked against the current documentation). Decide, write the choice in the README, and handle the short-query case in the interface rather than returning an empty list. Trap: Accepting the default tokenizer and then wondering why searching for part of a product code finds nothing is the most common search complaint in an app like this, and the second is a trigram index that returns nothing for a two-character query without saying why. Pick the tokenizer for the content you actually store, and when a query is shorter than the tokenizer's floor, say so in the interface instead of showing zero results. Done when: A substring search inside a word returns the document under the chosen tokenizer, or the interface states plainly that substring search is unavailable, with the choice recorded in the README. A query shorter than the tokenizer's minimum shows an explanatory message rather than an empty result list. Results are ranked and the matched text is shown in context. Search over a library of at least ten thousand documents returns in a time you have measured and stated. Do not build yet: No embeddings, tags or export yet. ### Phase 4 · Suggestions that never move anything Build: Add suggested tags and optional semantic search as a second ranker over keyword results. Every suggestion is stored with source set to suggested and a confidence, is visibly distinct from a tag you wrote, and is accepted or dismissed by you. If a model is used, the model name and the per-run cost estimate are shown before it runs. Trap: An app that files documents into folders on its own guess is delightful for a week and then you cannot find a contract because it decided the contract was a receipt, and by then you have stopped remembering where you put things. Tags are additive metadata, never a move, and a suggested tag stays labelled as suggested until you accept it. Done when: No suggestion path writes to the filesystem or changes a file's path, verified by review and a filesystem-write assertion in the test. Suggested and manual tags are stored and displayed differently, and a suggested tag never becomes manual without an explicit action. With embeddings disabled the app is fully usable and the interface says what is unavailable. A run using a paid model shows an estimated cost first and respects a configured ceiling. Do not build yet: No export, backup or scale work yet. ### Phase 5 · Leaving, rebuilding and surviving a big library Build: Export notes and tags as Markdown beside their files or as one archive, with the file path and hash in the front matter. Back up the database and document the restore. Requeue extraction by extractor version. Measure scan, extraction and search on a library of at least fifty thousand files and state the numbers in the README. Trap: Exporting notes without recording which file each one belonged to produces a folder of orphaned text that is worse than no export, because it looks like a migration path until you try to use it. Put the original path and the content hash in the exported front matter, and write an import that reattaches notes by hash so a rebuilt library keeps them. Done when: Exported notes carry the original path and content hash, and importing them into a rebuilt index reattaches every note to the right file by hash, tested end to end. A full rebuild on a library of at least fifty thousand files completes and the time is recorded in the README. Requeueing by extractor version reprocesses exactly the documents produced by older versions. Backup and restore are documented and the restore is exercised by a test rather than described. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-device sync, which is a conflict resolution problem rather than a storage one and is most of what the subscription buys. - Collaborative editing and sharing, which needs identity, permissions and a server you then have to operate. - Native mobile clients, which is a second application with its own extraction story rather than a port. - Automatic filing that moves files, because a wrong guess about where a document belongs is a document you cannot find. - A hosted model that reads your whole library by default; anything that leaves the machine is opt in and shows its cost. ### README must contain - That originals are never moved, renamed or written, and that the index can be deleted and rebuilt - Which FTS5 tokenizer was chosen, what substring behaviour that gives, and the minimum query length it implies - That an empty extraction is treated as a failure and appears in a visible list with a reason - That suggested tags are additive metadata, are labelled as suggestions, and never move a file - The measured scan, extraction and search times on a library of at least fifty thousand files ===== AGENTS.md ===== # Agent instructions · Fabric indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, SQLite with FTS5, stored beside the library but deletable, Left in place, never moved or renamed by the app, One module per format, run in a worker, failures recorded not swallowed, Optional, local or key-provided, added only after keyword search works. 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 - An app that takes ownership of files by copying them into its own store makes the store the only real copy, and then a bug in the store is a bug in your documents. Keep the originals where the user put them, treat the database as a cache, and add a rebuild command in this phase so the claim is tested from the start. - A scanned PDF with no text layer extracts to an empty string, and unless you treat that as a failure it silently becomes a file that search will never find, which the user discovers months later while looking for the one document they needed. Treat a zero-length extraction from a format that should have text as a failure with a stated reason, and offer optical recognition as an explicit, opt-in, per-file action. - Accepting the default tokenizer and then wondering why searching for part of a product code finds nothing is the most common search complaint in an app like this, and the second is a trigram index that returns nothing for a two-character query without saying why. Pick the tokenizer for the content you actually store, and when a query is shorter than the tokenizer's floor, say so in the interface instead of showing zero results. - An app that files documents into folders on its own guess is delightful for a week and then you cannot find a contract because it decided the contract was a receipt, and by then you have stopped remembering where you put things. Tags are additive metadata, never a move, and a suggested tag stays labelled as suggested until you accept it. ===== BUILD_PLAN.md ===== # Build plan · Fabric A local file workspace whose hard problems are keeping the originals untouched so the index is always disposable, getting text out of formats that resist it, and making search find the thing you half remember rather than returning nothing. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Files stay files The library is a folder you already understand, and the index can be deleted without losing anything. ### Steps 1. Index by content hash, reference by path A hash survives a rename; a path does not. Files: `src-tauri/src/index/scan.rs`, `src/db/schema.sql` 2. Write the rebuild command now, not later It is the test that the index is really disposable. Files: `src-tauri/src/index/rebuild.rs`, `tests/rebuild.test.ts` 3. Mark missing files rather than deleting rows An unplugged drive is not a decision to delete your notes. Files: `src-tauri/src/index/scan.rs` ### Done when - [ ] Deleting and rebuilding the index restores notes and tags - [ ] A moved file keeps its notes - [ ] A missing file is marked, not removed - [ ] Nothing in a watched folder is ever written by the app ### Watch out - An app that takes ownership of files by copying them into its own store makes the store the only real copy, and then a bug in the store is a bug in your documents. Keep the originals where the user put them, treat the database as a cache, and add a rebuild command in this phase so the claim is tested from the start. ## Phase 2 · Getting the text out, and admitting when you cannot Every file either has extracted text or a visible reason why it does not. ### Steps 1. Treat an empty extraction as a failure Silently indexing nothing is the worst available outcome. Files: `src-tauri/src/extract/mod.rs`, `tests/empty-extraction.test.ts` 2. Run extractors with a time and memory ceiling One malformed file should not stop the queue. Files: `src-tauri/src/extract/worker.rs` 3. Stamp the extractor and version on every document So a fixed extractor can requeue exactly its own failures. Files: `src/db/schema.sql` ### Done when - [ ] An empty extraction lands in the failure list with a reason - [ ] A hanging extractor is killed and recorded - [ ] Every document names its extractor and version - [ ] Retry re-runs one file only ### Watch out - A scanned PDF with no text layer extracts to an empty string, and unless you treat that as a failure it silently becomes a file that search will never find, which the user discovers months later while looking for the one document they needed. Treat a zero-length extraction from a format that should have text as a failure with a stated reason, and offer optical recognition as an explicit, opt-in, per-file action. ## Phase 3 · Search that finds the thing you half remember Keyword search returns the right document for partial words, and the tokenizer choice is deliberate. ### Steps 1. Choose the tokenizer deliberately and record it This single decision determines what search can ever find. Files: `src/db/schema.sql`, `docs/search.md` 2. Handle queries below the tokenizer's floor An empty list reads as a missing document. Files: `src/search/query.ts`, `tests/short-query.test.ts` 3. Measure on a real-sized library Ten documents tells you nothing about ten thousand. Files: `tests/search-perf.test.ts` ### Done when - [ ] Substring behaviour matches the documented choice - [ ] A too-short query explains itself - [ ] Results are ranked and show matched context - [ ] Search time on ten thousand documents is measured and stated ### Watch out - Accepting the default tokenizer and then wondering why searching for part of a product code finds nothing is the most common search complaint in an app like this, and the second is a trigram index that returns nothing for a two-character query without saying why. Pick the tokenizer for the content you actually store, and when a query is shorter than the tokenizer's floor, say so in the interface instead of showing zero results. ## Phase 4 · Suggestions that never move anything Automatic tags help you find things and never reorganise your library behind your back. ### Steps 1. Keep suggestion source and confidence on every tag The distinction is what stops guesses becoming facts. Files: `src/db/schema.sql`, `src/tags/suggest.ts` 2. Rank semantically only on top of keyword results A second ranker is useful; a replacement is unpredictable. Files: `src/search/rerank.ts` 3. Show cost before any paid run and cap it A library-wide embedding pass is a real invoice. Files: `src/embeddings/estimate.ts` ### Done when - [ ] No suggestion path writes to the filesystem - [ ] Suggested and manual tags stay distinguishable - [ ] The app works fully with embeddings disabled - [ ] Paid runs show an estimate and respect a ceiling ### Watch out - An app that files documents into folders on its own guess is delightful for a week and then you cannot find a contract because it decided the contract was a receipt, and by then you have stopped remembering where you put things. Tags are additive metadata, never a move, and a suggested tag stays labelled as suggested until you accept it. ## Not in this build - Multi-device sync. - Collaborative editing and sharing. - Native mobile clients. - Automatic filing that moves files. ## After v1, if you want it - Optical character recognition as an opt-in action on the files already in the failure list - Saved searches, which need nothing new in the index and a small amount in the interface - A second library root with its own index, once rebuild times at fifty thousand files are known ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Watched folders. Read only as far as this app is concerned. LIBRARY_PATHS=/home/you/Documents,/home/you/Archive # Required. Derived data. Deleting it should cost only rebuild time. INDEX_PATH=~/.local/share/fabric/index.db # Required. Per file. Exceeding it records a failure rather than blocking the queue. EXTRACT_TIMEOUT_SECONDS=60 # Required. Per worker. A malformed file should not take the app down. EXTRACT_MEMORY_MB=512 # Required. Recorded here and in the README because it decides what search can find. FTS_TOKENIZER=trigram # Required. Off by default. The app is fully usable without it. EMBEDDINGS_ENABLED=false # Optional · secret. Only if you enabled a hosted model. Never required for keyword search. EMBEDDINGS_API_KEY=sk-...redacted # Optional. Maximum estimated spend for one indexing pass before it refuses to start. EMBEDDINGS_COST_CEILING=5.00
You are building a lean indie version of Fabric. 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 ===== # Fabric · indie build A local file workspace whose hard problems are keeping the originals untouched so the index is always disposable, getting text out of formats that resist it, and making search find the thing you half remember rather than returning nothing. 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 | a desktop app with filesystem access and a small binary | | Index | SQLite with FTS5, stored beside the library but deletable | the index is derived data and must be rebuildable from the files alone | | Originals | Left in place, never moved or renamed by the app | the day you stop using this, your files should be exactly where you put them | | Extraction | One module per format, run in a worker, failures recorded not swallowed | a PDF that yields no text is a fact the user needs to see | | Embeddings | Optional, local or key-provided, added only after keyword search works | semantic search on top of broken extraction finds nothing either | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **The Rust toolchain and Node.js 22** · free - Why: Tauri needs both, and the extraction workers live on the Rust side where the time and memory ceilings are enforceable. - Get it: rustup.rs for Rust, nodejs.org or nvm for Node. - Verify: cargo --version and node --version both print, and a Tauri dev build launches - [ ] **A real folder of your own files** · free - Why: Every hard part here is a format question, and a folder of sample text files will pass tests that your actual PDFs, scans and exports fail. - Get it: Point the app at a folder you genuinely use, on a copy first if you are nervous. - Verify: The scan finds at least a few hundred files across at least four formats - [ ] **One scanned PDF with no text layer** · free - Why: The empty-extraction case is the single most common silent failure in this category and it needs a real fixture rather than a synthetic one. - Get it: Scan a page, or find one in your own archive. - Verify: Extraction yields zero characters and the file appears in the failure list - [ ] **A library of at least fifty thousand files for the scale test** · free - Why: Scan, rebuild and search times are the claims this build makes, and they are only credible measured at a size that hurts. - Get it: Your real library if it is large enough, otherwise generate one from public documents. - Verify: A full rebuild completes and the time is recorded - [ ] **The FTS5 tokenizer choice** · free - Why: It decides whether search can match inside a word and what the minimum query length is, and changing it later means a full reindex. - Get it: Look at what you actually store, read the tokenizer documentation, and write down the choice with the reason. - Verify: The choice is in the schema and the README and is covered by a substring test ## 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 and sharing. - Native mobile clients. - Automatic filing that moves files. - collaborative editing and sharing - native mobile clients - conflict-safe multi-device sync - mature importers and exporters If one of those is essential to you, that is the reason to keep paying for Fabric, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Fabric 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 local workspace that indexes everything you drop into it and leaves the files where you can find them without it to replace Fabric. 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 · a desktop app with filesystem access and a small binary - Index: SQLite with FTS5, stored beside the library but deletable · the index is derived data and must be rebuildable from the files alone - Originals: Left in place, never moved or renamed by the app · the day you stop using this, your files should be exactly where you put them - Extraction: One module per format, run in a worker, failures recorded not swallowed · a PDF that yields no text is a fact the user needs to see - Embeddings: Optional, local or key-provided, added only after keyword search works · semantic search on top of broken extraction finds nothing either ### Data model (create this before Phase 1) - files(id, path, size, mtime, content_sha256, first_seen_at, last_seen_at, missing_since) · path is a reference, never a copy - documents(file_id, kind, text, extracted_at, extractor, extractor_version, char_count) · the text pulled out, with what pulled it - extraction_failures(file_id, at, extractor, reason, retryable) · visible in the interface, not only in a log - fts(document rowid, text) · an FTS5 table whose tokenizer choice is recorded in the schema and in the README - notes(id, file_id, body, created_at, updated_at) · your own writing, which is the one thing here that is not derived - tags(file_id, tag, source, confidence) · source is manual or suggested, and a suggested tag never becomes manual by itself - embeddings(document_id, model, dim, vector) · present only if you enabled them, and rebuildable like everything else ### Phase 1 · Files stay files Build: Watch one or more folders, record path, size, modification time and content hash, and never move, rename or rewrite an original. Detect a file that moved by its hash rather than treating it as a deletion and an addition. Handle a file that disappears by marking it missing rather than deleting the notes attached to it. Trap: An app that takes ownership of files by copying them into its own store makes the store the only real copy, and then a bug in the store is a bug in your documents. Keep the originals where the user put them, treat the database as a cache, and add a rebuild command in this phase so the claim is tested from the start. Done when: Deleting the index database and rebuilding produces the same file list and the same notes attached to the same files, tested end to end. Moving a file on disk is detected by hash and keeps its notes and tags rather than orphaning them. A file that disappears is marked missing and its notes survive. No code path writes to, renames or moves a file in a watched folder, verified by review and by a read-only mount test. Do not build yet: No extraction, search, tags or embeddings. ### Phase 2 · Getting the text out, and admitting when you cannot Build: One extractor per format for the types you actually keep, run in a worker with a time limit and a memory ceiling. Record the extractor and its version on every document. A failure writes a row the interface shows, with a reason a person can act on and a retry button. Trap: A scanned PDF with no text layer extracts to an empty string, and unless you treat that as a failure it silently becomes a file that search will never find, which the user discovers months later while looking for the one document they needed. Treat a zero-length extraction from a format that should have text as a failure with a stated reason, and offer optical recognition as an explicit, opt-in, per-file action. Done when: A scanned PDF appears in the failure list with a reason rather than as an indexed empty document, tested with a real scan. An extractor that hangs is killed at the time limit and recorded as a failure rather than blocking the queue, tested with a deliberately slow fixture. Every document records which extractor and version produced it, so a later improvement can requeue exactly the affected files. Retrying a failure from the interface re-runs only that file. Do not build yet: No search, tags or embeddings yet. ### Phase 3 · Search that finds the thing you half remember Build: Build the FTS5 index and choose the tokenizer on purpose. The default splits on word boundaries and will not match inside a word, so a search for part of a filename or an identifier returns nothing; the trigram tokenizer matches substrings and also makes LIKE and GLOB indexed, but substrings shorter than three characters match no rows at all (checked against the current documentation). Decide, write the choice in the README, and handle the short-query case in the interface rather than returning an empty list. Trap: Accepting the default tokenizer and then wondering why searching for part of a product code finds nothing is the most common search complaint in an app like this, and the second is a trigram index that returns nothing for a two-character query without saying why. Pick the tokenizer for the content you actually store, and when a query is shorter than the tokenizer's floor, say so in the interface instead of showing zero results. Done when: A substring search inside a word returns the document under the chosen tokenizer, or the interface states plainly that substring search is unavailable, with the choice recorded in the README. A query shorter than the tokenizer's minimum shows an explanatory message rather than an empty result list. Results are ranked and the matched text is shown in context. Search over a library of at least ten thousand documents returns in a time you have measured and stated. Do not build yet: No embeddings, tags or export yet. ### Phase 4 · Suggestions that never move anything Build: Add suggested tags and optional semantic search as a second ranker over keyword results. Every suggestion is stored with source set to suggested and a confidence, is visibly distinct from a tag you wrote, and is accepted or dismissed by you. If a model is used, the model name and the per-run cost estimate are shown before it runs. Trap: An app that files documents into folders on its own guess is delightful for a week and then you cannot find a contract because it decided the contract was a receipt, and by then you have stopped remembering where you put things. Tags are additive metadata, never a move, and a suggested tag stays labelled as suggested until you accept it. Done when: No suggestion path writes to the filesystem or changes a file's path, verified by review and a filesystem-write assertion in the test. Suggested and manual tags are stored and displayed differently, and a suggested tag never becomes manual without an explicit action. With embeddings disabled the app is fully usable and the interface says what is unavailable. A run using a paid model shows an estimated cost first and respects a configured ceiling. Do not build yet: No export, backup or scale work yet. ### Phase 5 · Leaving, rebuilding and surviving a big library Build: Export notes and tags as Markdown beside their files or as one archive, with the file path and hash in the front matter. Back up the database and document the restore. Requeue extraction by extractor version. Measure scan, extraction and search on a library of at least fifty thousand files and state the numbers in the README. Trap: Exporting notes without recording which file each one belonged to produces a folder of orphaned text that is worse than no export, because it looks like a migration path until you try to use it. Put the original path and the content hash in the exported front matter, and write an import that reattaches notes by hash so a rebuilt library keeps them. Done when: Exported notes carry the original path and content hash, and importing them into a rebuilt index reattaches every note to the right file by hash, tested end to end. A full rebuild on a library of at least fifty thousand files completes and the time is recorded in the README. Requeueing by extractor version reprocesses exactly the documents produced by older versions. Backup and restore are documented and the restore is exercised by a test rather than described. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-device sync, which is a conflict resolution problem rather than a storage one and is most of what the subscription buys. - Collaborative editing and sharing, which needs identity, permissions and a server you then have to operate. - Native mobile clients, which is a second application with its own extraction story rather than a port. - Automatic filing that moves files, because a wrong guess about where a document belongs is a document you cannot find. - A hosted model that reads your whole library by default; anything that leaves the machine is opt in and shows its cost. ### README must contain - That originals are never moved, renamed or written, and that the index can be deleted and rebuilt - Which FTS5 tokenizer was chosen, what substring behaviour that gives, and the minimum query length it implies - That an empty extraction is treated as a failure and appears in a visible list with a reason - That suggested tags are additive metadata, are labelled as suggestions, and never move a file - The measured scan, extraction and search times on a library of at least fifty thousand files ===== AGENTS.md ===== # Agent instructions · Fabric indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, SQLite with FTS5, stored beside the library but deletable, Left in place, never moved or renamed by the app, One module per format, run in a worker, failures recorded not swallowed, Optional, local or key-provided, added only after keyword search works. 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 - An app that takes ownership of files by copying them into its own store makes the store the only real copy, and then a bug in the store is a bug in your documents. Keep the originals where the user put them, treat the database as a cache, and add a rebuild command in this phase so the claim is tested from the start. - A scanned PDF with no text layer extracts to an empty string, and unless you treat that as a failure it silently becomes a file that search will never find, which the user discovers months later while looking for the one document they needed. Treat a zero-length extraction from a format that should have text as a failure with a stated reason, and offer optical recognition as an explicit, opt-in, per-file action. - Accepting the default tokenizer and then wondering why searching for part of a product code finds nothing is the most common search complaint in an app like this, and the second is a trigram index that returns nothing for a two-character query without saying why. Pick the tokenizer for the content you actually store, and when a query is shorter than the tokenizer's floor, say so in the interface instead of showing zero results. - An app that files documents into folders on its own guess is delightful for a week and then you cannot find a contract because it decided the contract was a receipt, and by then you have stopped remembering where you put things. Tags are additive metadata, never a move, and a suggested tag stays labelled as suggested until you accept it. ===== BUILD_PLAN.md ===== # Build plan · Fabric A local file workspace whose hard problems are keeping the originals untouched so the index is always disposable, getting text out of formats that resist it, and making search find the thing you half remember rather than returning nothing. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Files stay files The library is a folder you already understand, and the index can be deleted without losing anything. ### Steps 1. Index by content hash, reference by path A hash survives a rename; a path does not. Files: `src-tauri/src/index/scan.rs`, `src/db/schema.sql` 2. Write the rebuild command now, not later It is the test that the index is really disposable. Files: `src-tauri/src/index/rebuild.rs`, `tests/rebuild.test.ts` 3. Mark missing files rather than deleting rows An unplugged drive is not a decision to delete your notes. Files: `src-tauri/src/index/scan.rs` ### Done when - [ ] Deleting and rebuilding the index restores notes and tags - [ ] A moved file keeps its notes - [ ] A missing file is marked, not removed - [ ] Nothing in a watched folder is ever written by the app ### Watch out - An app that takes ownership of files by copying them into its own store makes the store the only real copy, and then a bug in the store is a bug in your documents. Keep the originals where the user put them, treat the database as a cache, and add a rebuild command in this phase so the claim is tested from the start. ## Phase 2 · Getting the text out, and admitting when you cannot Every file either has extracted text or a visible reason why it does not. ### Steps 1. Treat an empty extraction as a failure Silently indexing nothing is the worst available outcome. Files: `src-tauri/src/extract/mod.rs`, `tests/empty-extraction.test.ts` 2. Run extractors with a time and memory ceiling One malformed file should not stop the queue. Files: `src-tauri/src/extract/worker.rs` 3. Stamp the extractor and version on every document So a fixed extractor can requeue exactly its own failures. Files: `src/db/schema.sql` ### Done when - [ ] An empty extraction lands in the failure list with a reason - [ ] A hanging extractor is killed and recorded - [ ] Every document names its extractor and version - [ ] Retry re-runs one file only ### Watch out - A scanned PDF with no text layer extracts to an empty string, and unless you treat that as a failure it silently becomes a file that search will never find, which the user discovers months later while looking for the one document they needed. Treat a zero-length extraction from a format that should have text as a failure with a stated reason, and offer optical recognition as an explicit, opt-in, per-file action. ## Phase 3 · Search that finds the thing you half remember Keyword search returns the right document for partial words, and the tokenizer choice is deliberate. ### Steps 1. Choose the tokenizer deliberately and record it This single decision determines what search can ever find. Files: `src/db/schema.sql`, `docs/search.md` 2. Handle queries below the tokenizer's floor An empty list reads as a missing document. Files: `src/search/query.ts`, `tests/short-query.test.ts` 3. Measure on a real-sized library Ten documents tells you nothing about ten thousand. Files: `tests/search-perf.test.ts` ### Done when - [ ] Substring behaviour matches the documented choice - [ ] A too-short query explains itself - [ ] Results are ranked and show matched context - [ ] Search time on ten thousand documents is measured and stated ### Watch out - Accepting the default tokenizer and then wondering why searching for part of a product code finds nothing is the most common search complaint in an app like this, and the second is a trigram index that returns nothing for a two-character query without saying why. Pick the tokenizer for the content you actually store, and when a query is shorter than the tokenizer's floor, say so in the interface instead of showing zero results. ## Phase 4 · Suggestions that never move anything Automatic tags help you find things and never reorganise your library behind your back. ### Steps 1. Keep suggestion source and confidence on every tag The distinction is what stops guesses becoming facts. Files: `src/db/schema.sql`, `src/tags/suggest.ts` 2. Rank semantically only on top of keyword results A second ranker is useful; a replacement is unpredictable. Files: `src/search/rerank.ts` 3. Show cost before any paid run and cap it A library-wide embedding pass is a real invoice. Files: `src/embeddings/estimate.ts` ### Done when - [ ] No suggestion path writes to the filesystem - [ ] Suggested and manual tags stay distinguishable - [ ] The app works fully with embeddings disabled - [ ] Paid runs show an estimate and respect a ceiling ### Watch out - An app that files documents into folders on its own guess is delightful for a week and then you cannot find a contract because it decided the contract was a receipt, and by then you have stopped remembering where you put things. Tags are additive metadata, never a move, and a suggested tag stays labelled as suggested until you accept it. ## Not in this build - Multi-device sync. - Collaborative editing and sharing. - Native mobile clients. - Automatic filing that moves files. ## After v1, if you want it - Optical character recognition as an opt-in action on the files already in the failure list - Saved searches, which need nothing new in the index and a small amount in the interface - A second library root with its own index, once rebuild times at fifty thousand files are known ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Watched folders. Read only as far as this app is concerned. LIBRARY_PATHS=/home/you/Documents,/home/you/Archive # Required. Derived data. Deleting it should cost only rebuild time. INDEX_PATH=~/.local/share/fabric/index.db # Required. Per file. Exceeding it records a failure rather than blocking the queue. EXTRACT_TIMEOUT_SECONDS=60 # Required. Per worker. A malformed file should not take the app down. EXTRACT_MEMORY_MB=512 # Required. Recorded here and in the README because it decides what search can find. FTS_TOKENIZER=trigram # Required. Off by default. The app is fully usable without it. EMBEDDINGS_ENABLED=false # Optional · secret. Only if you enabled a hosted model. Never required for keyword search. EMBEDDINGS_API_KEY=sk-...redacted # Optional. Maximum estimated spend for one indexing pass before it refuses to start. EMBEDDINGS_COST_CEILING=5.00
You are building a production product version of Fabric. 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 ===== # Fabric · product brief ## Problem The visible AI file workspace loop is buildable, but a credible replacement needs more than the first screen. Fabric earns its keep through polish, sync, importers, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A workspace that leaves your files exactly where you put them, tells you which ones it could not read instead of quietly indexing nothing, finds documents by the fragment you remember, and can be deleted entirely without taking anything with it. ## 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 and sharing. - Native mobile clients. - Automatic filing that moves files. - collaborative editing and sharing - native mobile clients - conflict-safe multi-device sync - mature importers and exporters ## Success criteria - A rebuild from scratch is confirmed to restore notes and tags - An empty extraction is confirmed to land in the failure list - The documented substring behaviour is confirmed by test - No suggestion path is confirmed to write to the filesystem - Exported notes are confirmed to reattach by hash ===== BRIEF.md ===== # Build brief · Fabric 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 local workspace that indexes everything you drop into it and leaves the files where you can find them without it to replace Fabric. 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 · a desktop app with filesystem access and a small binary - Index: SQLite with FTS5, stored beside the library but deletable · the index is derived data and must be rebuildable from the files alone - Originals: Left in place, never moved or renamed by the app · the day you stop using this, your files should be exactly where you put them - Extraction: One module per format, run in a worker, failures recorded not swallowed · a PDF that yields no text is a fact the user needs to see - Embeddings: Optional, local or key-provided, added only after keyword search works · semantic search on top of broken extraction finds nothing either ### Data model (create this before Phase 1) - files(id, path, size, mtime, content_sha256, first_seen_at, last_seen_at, missing_since) · path is a reference, never a copy - documents(file_id, kind, text, extracted_at, extractor, extractor_version, char_count) · the text pulled out, with what pulled it - extraction_failures(file_id, at, extractor, reason, retryable) · visible in the interface, not only in a log - fts(document rowid, text) · an FTS5 table whose tokenizer choice is recorded in the schema and in the README - notes(id, file_id, body, created_at, updated_at) · your own writing, which is the one thing here that is not derived - tags(file_id, tag, source, confidence) · source is manual or suggested, and a suggested tag never becomes manual by itself - embeddings(document_id, model, dim, vector) · present only if you enabled them, and rebuildable like everything else ### Phase 1 · Files stay files Build: Watch one or more folders, record path, size, modification time and content hash, and never move, rename or rewrite an original. Detect a file that moved by its hash rather than treating it as a deletion and an addition. Handle a file that disappears by marking it missing rather than deleting the notes attached to it. Trap: An app that takes ownership of files by copying them into its own store makes the store the only real copy, and then a bug in the store is a bug in your documents. Keep the originals where the user put them, treat the database as a cache, and add a rebuild command in this phase so the claim is tested from the start. Done when: Deleting the index database and rebuilding produces the same file list and the same notes attached to the same files, tested end to end. Moving a file on disk is detected by hash and keeps its notes and tags rather than orphaning them. A file that disappears is marked missing and its notes survive. No code path writes to, renames or moves a file in a watched folder, verified by review and by a read-only mount test. Do not build yet: No extraction, search, tags or embeddings. ### Phase 2 · Getting the text out, and admitting when you cannot Build: One extractor per format for the types you actually keep, run in a worker with a time limit and a memory ceiling. Record the extractor and its version on every document. A failure writes a row the interface shows, with a reason a person can act on and a retry button. Trap: A scanned PDF with no text layer extracts to an empty string, and unless you treat that as a failure it silently becomes a file that search will never find, which the user discovers months later while looking for the one document they needed. Treat a zero-length extraction from a format that should have text as a failure with a stated reason, and offer optical recognition as an explicit, opt-in, per-file action. Done when: A scanned PDF appears in the failure list with a reason rather than as an indexed empty document, tested with a real scan. An extractor that hangs is killed at the time limit and recorded as a failure rather than blocking the queue, tested with a deliberately slow fixture. Every document records which extractor and version produced it, so a later improvement can requeue exactly the affected files. Retrying a failure from the interface re-runs only that file. Do not build yet: No search, tags or embeddings yet. ### Phase 3 · Search that finds the thing you half remember Build: Build the FTS5 index and choose the tokenizer on purpose. The default splits on word boundaries and will not match inside a word, so a search for part of a filename or an identifier returns nothing; the trigram tokenizer matches substrings and also makes LIKE and GLOB indexed, but substrings shorter than three characters match no rows at all (checked against the current documentation). Decide, write the choice in the README, and handle the short-query case in the interface rather than returning an empty list. Trap: Accepting the default tokenizer and then wondering why searching for part of a product code finds nothing is the most common search complaint in an app like this, and the second is a trigram index that returns nothing for a two-character query without saying why. Pick the tokenizer for the content you actually store, and when a query is shorter than the tokenizer's floor, say so in the interface instead of showing zero results. Done when: A substring search inside a word returns the document under the chosen tokenizer, or the interface states plainly that substring search is unavailable, with the choice recorded in the README. A query shorter than the tokenizer's minimum shows an explanatory message rather than an empty result list. Results are ranked and the matched text is shown in context. Search over a library of at least ten thousand documents returns in a time you have measured and stated. Do not build yet: No embeddings, tags or export yet. ### Phase 4 · Suggestions that never move anything Build: Add suggested tags and optional semantic search as a second ranker over keyword results. Every suggestion is stored with source set to suggested and a confidence, is visibly distinct from a tag you wrote, and is accepted or dismissed by you. If a model is used, the model name and the per-run cost estimate are shown before it runs. Trap: An app that files documents into folders on its own guess is delightful for a week and then you cannot find a contract because it decided the contract was a receipt, and by then you have stopped remembering where you put things. Tags are additive metadata, never a move, and a suggested tag stays labelled as suggested until you accept it. Done when: No suggestion path writes to the filesystem or changes a file's path, verified by review and a filesystem-write assertion in the test. Suggested and manual tags are stored and displayed differently, and a suggested tag never becomes manual without an explicit action. With embeddings disabled the app is fully usable and the interface says what is unavailable. A run using a paid model shows an estimated cost first and respects a configured ceiling. Do not build yet: No export, backup or scale work yet. ### Phase 5 · Leaving, rebuilding and surviving a big library Build: Export notes and tags as Markdown beside their files or as one archive, with the file path and hash in the front matter. Back up the database and document the restore. Requeue extraction by extractor version. Measure scan, extraction and search on a library of at least fifty thousand files and state the numbers in the README. Trap: Exporting notes without recording which file each one belonged to produces a folder of orphaned text that is worse than no export, because it looks like a migration path until you try to use it. Put the original path and the content hash in the exported front matter, and write an import that reattaches notes by hash so a rebuilt library keeps them. Done when: Exported notes carry the original path and content hash, and importing them into a rebuilt index reattaches every note to the right file by hash, tested end to end. A full rebuild on a library of at least fifty thousand files completes and the time is recorded in the README. Requeueing by extractor version reprocesses exactly the documents produced by older versions. Backup and restore are documented and the restore is exercised by a test rather than described. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-device sync, which is a conflict resolution problem rather than a storage one and is most of what the subscription buys. - Collaborative editing and sharing, which needs identity, permissions and a server you then have to operate. - Native mobile clients, which is a second application with its own extraction story rather than a port. - Automatic filing that moves files, because a wrong guess about where a document belongs is a document you cannot find. - A hosted model that reads your whole library by default; anything that leaves the machine is opt in and shows its cost. ### README must contain - That originals are never moved, renamed or written, and that the index can be deleted and rebuilt - Which FTS5 tokenizer was chosen, what substring behaviour that gives, and the minimum query length it implies - That an empty extraction is treated as a failure and appears in a visible list with a reason - That suggested tags are additive metadata, are labelled as suggestions, and never move a file - The measured scan, extraction and search times on a library of at least fifty thousand files ===== ARCHITECTURE.md ===== # Architecture · Fabric ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript | a desktop app with filesystem access and a small binary | | Index | SQLite with FTS5, stored beside the library but deletable | the index is derived data and must be rebuildable from the files alone | | Originals | Left in place, never moved or renamed by the app | the day you stop using this, your files should be exactly where you put them | | Extraction | One module per format, run in a worker, failures recorded not swallowed | a PDF that yields no text is a fact the user needs to see | | Embeddings | Optional, local or key-provided, added only after keyword search works | semantic search on top of broken extraction finds nothing either | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Scanner | Paths, hashes, moves and missing files. | Any watcher, provided it never writes into a watched folder. | | Extractors | Per-format text with ceilings and recorded failures. | Any extractor per format, provided its name and version are stamped on the document. | | Search | The FTS5 index and the tokenizer contract. | Any engine, provided the substring behaviour is documented and tested. | | Suggestions | Tags with source and confidence. | Any model, provided nothing it produces can move a file. | | Export | Notes and tags with path and hash. | Any format, provided reattachment by hash still works. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `LIBRARY_PATHS` · required · Watched folders. Read only as far as this app is concerned. - `INDEX_PATH` · required · Derived data. Deleting it should cost only rebuild time. - `EXTRACT_TIMEOUT_SECONDS` · required · Per file. Exceeding it records a failure rather than blocking the queue. - `EXTRACT_MEMORY_MB` · required · Per worker. A malformed file should not take the app down. - `FTS_TOKENIZER` · required · Recorded here and in the README because it decides what search can find. - `EMBEDDINGS_ENABLED` · required · Off by default. The app is fully usable without it. - `EMBEDDINGS_API_KEY` · optional, secret · Only if you enabled a hosted model. Never required for keyword search. - `EMBEDDINGS_COST_CEILING` · optional · Maximum estimated spend for one indexing pass before it refuses to start. ## 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 · Fabric product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri 2 with React and TypeScript, SQLite with FTS5, stored beside the library but deletable, Left in place, never moved or renamed by the app, One module per format, run in a worker, failures recorded not swallowed, Optional, local or key-provided, added only after keyword search works. - 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 - An app that takes ownership of files by copying them into its own store makes the store the only real copy, and then a bug in the store is a bug in your documents. Keep the originals where the user put them, treat the database as a cache, and add a rebuild command in this phase so the claim is tested from the start. - A scanned PDF with no text layer extracts to an empty string, and unless you treat that as a failure it silently becomes a file that search will never find, which the user discovers months later while looking for the one document they needed. Treat a zero-length extraction from a format that should have text as a failure with a stated reason, and offer optical recognition as an explicit, opt-in, per-file action. - Accepting the default tokenizer and then wondering why searching for part of a product code finds nothing is the most common search complaint in an app like this, and the second is a trigram index that returns nothing for a two-character query without saying why. Pick the tokenizer for the content you actually store, and when a query is shorter than the tokenizer's floor, say so in the interface instead of showing zero results. - An app that files documents into folders on its own guess is delightful for a week and then you cannot find a contract because it decided the contract was a receipt, and by then you have stopped remembering where you put things. Tags are additive metadata, never a move, and a suggested tag stays labelled as suggested until you accept it. - Exporting notes without recording which file each one belonged to produces a folder of orphaned text that is worse than no export, because it looks like a migration path until you try to use it. Put the original path and the content hash in the exported front matter, and write an import that reattaches notes by hash so a rebuilt library keeps them. ===== MILESTONES.md ===== # Delivery milestones · Fabric Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Files stay files The library is a folder you already understand, and the index can be deleted without losing anything. ### Steps 1. Index by content hash, reference by path A hash survives a rename; a path does not. Files: `src-tauri/src/index/scan.rs`, `src/db/schema.sql` 2. Write the rebuild command now, not later It is the test that the index is really disposable. Files: `src-tauri/src/index/rebuild.rs`, `tests/rebuild.test.ts` 3. Mark missing files rather than deleting rows An unplugged drive is not a decision to delete your notes. Files: `src-tauri/src/index/scan.rs` ### Done when - [ ] Deleting and rebuilding the index restores notes and tags - [ ] A moved file keeps its notes - [ ] A missing file is marked, not removed - [ ] Nothing in a watched folder is ever written by the app ### Watch out - An app that takes ownership of files by copying them into its own store makes the store the only real copy, and then a bug in the store is a bug in your documents. Keep the originals where the user put them, treat the database as a cache, and add a rebuild command in this phase so the claim is tested from the start. ## M2 · Getting the text out, and admitting when you cannot Every file either has extracted text or a visible reason why it does not. ### Steps 1. Treat an empty extraction as a failure Silently indexing nothing is the worst available outcome. Files: `src-tauri/src/extract/mod.rs`, `tests/empty-extraction.test.ts` 2. Run extractors with a time and memory ceiling One malformed file should not stop the queue. Files: `src-tauri/src/extract/worker.rs` 3. Stamp the extractor and version on every document So a fixed extractor can requeue exactly its own failures. Files: `src/db/schema.sql` ### Done when - [ ] An empty extraction lands in the failure list with a reason - [ ] A hanging extractor is killed and recorded - [ ] Every document names its extractor and version - [ ] Retry re-runs one file only ### Watch out - A scanned PDF with no text layer extracts to an empty string, and unless you treat that as a failure it silently becomes a file that search will never find, which the user discovers months later while looking for the one document they needed. Treat a zero-length extraction from a format that should have text as a failure with a stated reason, and offer optical recognition as an explicit, opt-in, per-file action. ## M3 · Search that finds the thing you half remember Keyword search returns the right document for partial words, and the tokenizer choice is deliberate. ### Steps 1. Choose the tokenizer deliberately and record it This single decision determines what search can ever find. Files: `src/db/schema.sql`, `docs/search.md` 2. Handle queries below the tokenizer's floor An empty list reads as a missing document. Files: `src/search/query.ts`, `tests/short-query.test.ts` 3. Measure on a real-sized library Ten documents tells you nothing about ten thousand. Files: `tests/search-perf.test.ts` ### Done when - [ ] Substring behaviour matches the documented choice - [ ] A too-short query explains itself - [ ] Results are ranked and show matched context - [ ] Search time on ten thousand documents is measured and stated ### Watch out - Accepting the default tokenizer and then wondering why searching for part of a product code finds nothing is the most common search complaint in an app like this, and the second is a trigram index that returns nothing for a two-character query without saying why. Pick the tokenizer for the content you actually store, and when a query is shorter than the tokenizer's floor, say so in the interface instead of showing zero results. ## M4 · Suggestions that never move anything Automatic tags help you find things and never reorganise your library behind your back. ### Steps 1. Keep suggestion source and confidence on every tag The distinction is what stops guesses becoming facts. Files: `src/db/schema.sql`, `src/tags/suggest.ts` 2. Rank semantically only on top of keyword results A second ranker is useful; a replacement is unpredictable. Files: `src/search/rerank.ts` 3. Show cost before any paid run and cap it A library-wide embedding pass is a real invoice. Files: `src/embeddings/estimate.ts` ### Done when - [ ] No suggestion path writes to the filesystem - [ ] Suggested and manual tags stay distinguishable - [ ] The app works fully with embeddings disabled - [ ] Paid runs show an estimate and respect a ceiling ### Watch out - An app that files documents into folders on its own guess is delightful for a week and then you cannot find a contract because it decided the contract was a receipt, and by then you have stopped remembering where you put things. Tags are additive metadata, never a move, and a suggested tag stays labelled as suggested until you accept it. ## M5 · Leaving, rebuilding and surviving a big library (production only) Everything you added is exportable, the index is genuinely disposable, and the app is usable at real size. ### Steps 1. Export with path and hash in the front matter Without the hash the export is a pile of loose text. Files: `src/export/markdown.ts`, `tests/export-roundtrip.test.ts` 2. Requeue extraction by extractor version An improved extractor should not mean a full reindex. Files: `src-tauri/src/extract/requeue.rs` 3. Measure at fifty thousand files and publish the numbers Every claim about speed here is a number or it is nothing. Files: `tests/scale.test.ts`, `README.md` ### Done when - [ ] Exported notes reattach by hash after a rebuild - [ ] A full rebuild at fifty thousand files completes and is timed - [ ] Requeueing by extractor version selects the right documents - [ ] Restore is exercised in the test suite ### Watch out - Exporting notes without recording which file each one belonged to produces a folder of orphaned text that is worse than no export, because it looks like a migration path until you try to use it. Put the original path and the content hash in the exported front matter, and write an import that reattaches notes by hash so a rebuilt library keeps them. ===== OPERATIONS.md ===== # Operations · Fabric ## Backup Back up your files the way you already do; they are the real data. Back up the index only to save rebuild time, and back up notes separately because they are the one thing in the database that cannot be regenerated from disk. ## Restore Restore the files, then rebuild the index and reattach notes by content hash. The rebuild test from Phase 1 is what makes this a procedure rather than a hope. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a desktop app the signals are in the interface: the extraction failure count, the missing file count, the age of the last full scan, and the rebuild time trend as the library grows. ## Incident checklist A document you cannot find is almost always an extraction failure or a tokenizer limitation, and both are visible: the failure list names the file, and the search screen states the minimum query length. A corrupted index is a rebuild, not a loss. 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 rebuild from scratch is confirmed to restore notes and tags - [ ] An empty extraction is confirmed to land in the failure list - [ ] The documented substring behaviour is confirmed by test - [ ] No suggestion path is confirmed to write to the filesystem - [ ] Exported notes are confirmed to reattach by hash ## Launch constraint Do not market omitted Fabric 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. Watched folders. Read only as far as this app is concerned. LIBRARY_PATHS=/home/you/Documents,/home/you/Archive # Required. Derived data. Deleting it should cost only rebuild time. INDEX_PATH=~/.local/share/fabric/index.db # Required. Per file. Exceeding it records a failure rather than blocking the queue. EXTRACT_TIMEOUT_SECONDS=60 # Required. Per worker. A malformed file should not take the app down. EXTRACT_MEMORY_MB=512 # Required. Recorded here and in the README because it decides what search can find. FTS_TOKENIZER=trigram # Required. Off by default. The app is fully usable without it. EMBEDDINGS_ENABLED=false # Optional · secret. Only if you enabled a hosted model. Never required for keyword search. EMBEDDINGS_API_KEY=sk-...redacted # Optional. Maximum estimated spend for one indexing pass before it refuses to start. EMBEDDINGS_COST_CEILING=5.00
# Fabric · indie build A local file workspace whose hard problems are keeping the originals untouched so the index is always disposable, getting text out of formats that resist it, and making search find the thing you half remember rather than returning nothing. 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 | a desktop app with filesystem access and a small binary | | Index | SQLite with FTS5, stored beside the library but deletable | the index is derived data and must be rebuildable from the files alone | | Originals | Left in place, never moved or renamed by the app | the day you stop using this, your files should be exactly where you put them | | Extraction | One module per format, run in a worker, failures recorded not swallowed | a PDF that yields no text is a fact the user needs to see | | Embeddings | Optional, local or key-provided, added only after keyword search works | semantic search on top of broken extraction finds nothing either | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **The Rust toolchain and Node.js 22** · free - Why: Tauri needs both, and the extraction workers live on the Rust side where the time and memory ceilings are enforceable. - Get it: rustup.rs for Rust, nodejs.org or nvm for Node. - Verify: cargo --version and node --version both print, and a Tauri dev build launches - [ ] **A real folder of your own files** · free - Why: Every hard part here is a format question, and a folder of sample text files will pass tests that your actual PDFs, scans and exports fail. - Get it: Point the app at a folder you genuinely use, on a copy first if you are nervous. - Verify: The scan finds at least a few hundred files across at least four formats - [ ] **One scanned PDF with no text layer** · free - Why: The empty-extraction case is the single most common silent failure in this category and it needs a real fixture rather than a synthetic one. - Get it: Scan a page, or find one in your own archive. - Verify: Extraction yields zero characters and the file appears in the failure list - [ ] **A library of at least fifty thousand files for the scale test** · free - Why: Scan, rebuild and search times are the claims this build makes, and they are only credible measured at a size that hurts. - Get it: Your real library if it is large enough, otherwise generate one from public documents. - Verify: A full rebuild completes and the time is recorded - [ ] **The FTS5 tokenizer choice** · free - Why: It decides whether search can match inside a word and what the minimum query length is, and changing it later means a full reindex. - Get it: Look at what you actually store, read the tokenizer documentation, and write down the choice with the reason. - Verify: The choice is in the schema and the README and is covered by a substring test ## 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 and sharing. - Native mobile clients. - Automatic filing that moves files. - collaborative editing and sharing - native mobile clients - conflict-safe multi-device sync - mature importers and exporters If one of those is essential to you, that is the reason to keep paying for Fabric, and the README should say so rather than pretend.
# Build brief · Fabric 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 local workspace that indexes everything you drop into it and leaves the files where you can find them without it to replace Fabric. 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 · a desktop app with filesystem access and a small binary - Index: SQLite with FTS5, stored beside the library but deletable · the index is derived data and must be rebuildable from the files alone - Originals: Left in place, never moved or renamed by the app · the day you stop using this, your files should be exactly where you put them - Extraction: One module per format, run in a worker, failures recorded not swallowed · a PDF that yields no text is a fact the user needs to see - Embeddings: Optional, local or key-provided, added only after keyword search works · semantic search on top of broken extraction finds nothing either ### Data model (create this before Phase 1) - files(id, path, size, mtime, content_sha256, first_seen_at, last_seen_at, missing_since) · path is a reference, never a copy - documents(file_id, kind, text, extracted_at, extractor, extractor_version, char_count) · the text pulled out, with what pulled it - extraction_failures(file_id, at, extractor, reason, retryable) · visible in the interface, not only in a log - fts(document rowid, text) · an FTS5 table whose tokenizer choice is recorded in the schema and in the README - notes(id, file_id, body, created_at, updated_at) · your own writing, which is the one thing here that is not derived - tags(file_id, tag, source, confidence) · source is manual or suggested, and a suggested tag never becomes manual by itself - embeddings(document_id, model, dim, vector) · present only if you enabled them, and rebuildable like everything else ### Phase 1 · Files stay files Build: Watch one or more folders, record path, size, modification time and content hash, and never move, rename or rewrite an original. Detect a file that moved by its hash rather than treating it as a deletion and an addition. Handle a file that disappears by marking it missing rather than deleting the notes attached to it. Trap: An app that takes ownership of files by copying them into its own store makes the store the only real copy, and then a bug in the store is a bug in your documents. Keep the originals where the user put them, treat the database as a cache, and add a rebuild command in this phase so the claim is tested from the start. Done when: Deleting the index database and rebuilding produces the same file list and the same notes attached to the same files, tested end to end. Moving a file on disk is detected by hash and keeps its notes and tags rather than orphaning them. A file that disappears is marked missing and its notes survive. No code path writes to, renames or moves a file in a watched folder, verified by review and by a read-only mount test. Do not build yet: No extraction, search, tags or embeddings. ### Phase 2 · Getting the text out, and admitting when you cannot Build: One extractor per format for the types you actually keep, run in a worker with a time limit and a memory ceiling. Record the extractor and its version on every document. A failure writes a row the interface shows, with a reason a person can act on and a retry button. Trap: A scanned PDF with no text layer extracts to an empty string, and unless you treat that as a failure it silently becomes a file that search will never find, which the user discovers months later while looking for the one document they needed. Treat a zero-length extraction from a format that should have text as a failure with a stated reason, and offer optical recognition as an explicit, opt-in, per-file action. Done when: A scanned PDF appears in the failure list with a reason rather than as an indexed empty document, tested with a real scan. An extractor that hangs is killed at the time limit and recorded as a failure rather than blocking the queue, tested with a deliberately slow fixture. Every document records which extractor and version produced it, so a later improvement can requeue exactly the affected files. Retrying a failure from the interface re-runs only that file. Do not build yet: No search, tags or embeddings yet. ### Phase 3 · Search that finds the thing you half remember Build: Build the FTS5 index and choose the tokenizer on purpose. The default splits on word boundaries and will not match inside a word, so a search for part of a filename or an identifier returns nothing; the trigram tokenizer matches substrings and also makes LIKE and GLOB indexed, but substrings shorter than three characters match no rows at all (checked against the current documentation). Decide, write the choice in the README, and handle the short-query case in the interface rather than returning an empty list. Trap: Accepting the default tokenizer and then wondering why searching for part of a product code finds nothing is the most common search complaint in an app like this, and the second is a trigram index that returns nothing for a two-character query without saying why. Pick the tokenizer for the content you actually store, and when a query is shorter than the tokenizer's floor, say so in the interface instead of showing zero results. Done when: A substring search inside a word returns the document under the chosen tokenizer, or the interface states plainly that substring search is unavailable, with the choice recorded in the README. A query shorter than the tokenizer's minimum shows an explanatory message rather than an empty result list. Results are ranked and the matched text is shown in context. Search over a library of at least ten thousand documents returns in a time you have measured and stated. Do not build yet: No embeddings, tags or export yet. ### Phase 4 · Suggestions that never move anything Build: Add suggested tags and optional semantic search as a second ranker over keyword results. Every suggestion is stored with source set to suggested and a confidence, is visibly distinct from a tag you wrote, and is accepted or dismissed by you. If a model is used, the model name and the per-run cost estimate are shown before it runs. Trap: An app that files documents into folders on its own guess is delightful for a week and then you cannot find a contract because it decided the contract was a receipt, and by then you have stopped remembering where you put things. Tags are additive metadata, never a move, and a suggested tag stays labelled as suggested until you accept it. Done when: No suggestion path writes to the filesystem or changes a file's path, verified by review and a filesystem-write assertion in the test. Suggested and manual tags are stored and displayed differently, and a suggested tag never becomes manual without an explicit action. With embeddings disabled the app is fully usable and the interface says what is unavailable. A run using a paid model shows an estimated cost first and respects a configured ceiling. Do not build yet: No export, backup or scale work yet. ### Phase 5 · Leaving, rebuilding and surviving a big library Build: Export notes and tags as Markdown beside their files or as one archive, with the file path and hash in the front matter. Back up the database and document the restore. Requeue extraction by extractor version. Measure scan, extraction and search on a library of at least fifty thousand files and state the numbers in the README. Trap: Exporting notes without recording which file each one belonged to produces a folder of orphaned text that is worse than no export, because it looks like a migration path until you try to use it. Put the original path and the content hash in the exported front matter, and write an import that reattaches notes by hash so a rebuilt library keeps them. Done when: Exported notes carry the original path and content hash, and importing them into a rebuilt index reattaches every note to the right file by hash, tested end to end. A full rebuild on a library of at least fifty thousand files completes and the time is recorded in the README. Requeueing by extractor version reprocesses exactly the documents produced by older versions. Backup and restore are documented and the restore is exercised by a test rather than described. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-device sync, which is a conflict resolution problem rather than a storage one and is most of what the subscription buys. - Collaborative editing and sharing, which needs identity, permissions and a server you then have to operate. - Native mobile clients, which is a second application with its own extraction story rather than a port. - Automatic filing that moves files, because a wrong guess about where a document belongs is a document you cannot find. - A hosted model that reads your whole library by default; anything that leaves the machine is opt in and shows its cost. ### README must contain - That originals are never moved, renamed or written, and that the index can be deleted and rebuilt - Which FTS5 tokenizer was chosen, what substring behaviour that gives, and the minimum query length it implies - That an empty extraction is treated as a failure and appears in a visible list with a reason - That suggested tags are additive metadata, are labelled as suggestions, and never move a file - The measured scan, extraction and search times on a library of at least fifty thousand files
# Agent instructions · Fabric indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, SQLite with FTS5, stored beside the library but deletable, Left in place, never moved or renamed by the app, One module per format, run in a worker, failures recorded not swallowed, Optional, local or key-provided, added only after keyword search works. 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 - An app that takes ownership of files by copying them into its own store makes the store the only real copy, and then a bug in the store is a bug in your documents. Keep the originals where the user put them, treat the database as a cache, and add a rebuild command in this phase so the claim is tested from the start. - A scanned PDF with no text layer extracts to an empty string, and unless you treat that as a failure it silently becomes a file that search will never find, which the user discovers months later while looking for the one document they needed. Treat a zero-length extraction from a format that should have text as a failure with a stated reason, and offer optical recognition as an explicit, opt-in, per-file action. - Accepting the default tokenizer and then wondering why searching for part of a product code finds nothing is the most common search complaint in an app like this, and the second is a trigram index that returns nothing for a two-character query without saying why. Pick the tokenizer for the content you actually store, and when a query is shorter than the tokenizer's floor, say so in the interface instead of showing zero results. - An app that files documents into folders on its own guess is delightful for a week and then you cannot find a contract because it decided the contract was a receipt, and by then you have stopped remembering where you put things. Tags are additive metadata, never a move, and a suggested tag stays labelled as suggested until you accept it.
# Build plan · Fabric A local file workspace whose hard problems are keeping the originals untouched so the index is always disposable, getting text out of formats that resist it, and making search find the thing you half remember rather than returning nothing. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Files stay files The library is a folder you already understand, and the index can be deleted without losing anything. ### Steps 1. Index by content hash, reference by path A hash survives a rename; a path does not. Files: `src-tauri/src/index/scan.rs`, `src/db/schema.sql` 2. Write the rebuild command now, not later It is the test that the index is really disposable. Files: `src-tauri/src/index/rebuild.rs`, `tests/rebuild.test.ts` 3. Mark missing files rather than deleting rows An unplugged drive is not a decision to delete your notes. Files: `src-tauri/src/index/scan.rs` ### Done when - [ ] Deleting and rebuilding the index restores notes and tags - [ ] A moved file keeps its notes - [ ] A missing file is marked, not removed - [ ] Nothing in a watched folder is ever written by the app ### Watch out - An app that takes ownership of files by copying them into its own store makes the store the only real copy, and then a bug in the store is a bug in your documents. Keep the originals where the user put them, treat the database as a cache, and add a rebuild command in this phase so the claim is tested from the start. ## Phase 2 · Getting the text out, and admitting when you cannot Every file either has extracted text or a visible reason why it does not. ### Steps 1. Treat an empty extraction as a failure Silently indexing nothing is the worst available outcome. Files: `src-tauri/src/extract/mod.rs`, `tests/empty-extraction.test.ts` 2. Run extractors with a time and memory ceiling One malformed file should not stop the queue. Files: `src-tauri/src/extract/worker.rs` 3. Stamp the extractor and version on every document So a fixed extractor can requeue exactly its own failures. Files: `src/db/schema.sql` ### Done when - [ ] An empty extraction lands in the failure list with a reason - [ ] A hanging extractor is killed and recorded - [ ] Every document names its extractor and version - [ ] Retry re-runs one file only ### Watch out - A scanned PDF with no text layer extracts to an empty string, and unless you treat that as a failure it silently becomes a file that search will never find, which the user discovers months later while looking for the one document they needed. Treat a zero-length extraction from a format that should have text as a failure with a stated reason, and offer optical recognition as an explicit, opt-in, per-file action. ## Phase 3 · Search that finds the thing you half remember Keyword search returns the right document for partial words, and the tokenizer choice is deliberate. ### Steps 1. Choose the tokenizer deliberately and record it This single decision determines what search can ever find. Files: `src/db/schema.sql`, `docs/search.md` 2. Handle queries below the tokenizer's floor An empty list reads as a missing document. Files: `src/search/query.ts`, `tests/short-query.test.ts` 3. Measure on a real-sized library Ten documents tells you nothing about ten thousand. Files: `tests/search-perf.test.ts` ### Done when - [ ] Substring behaviour matches the documented choice - [ ] A too-short query explains itself - [ ] Results are ranked and show matched context - [ ] Search time on ten thousand documents is measured and stated ### Watch out - Accepting the default tokenizer and then wondering why searching for part of a product code finds nothing is the most common search complaint in an app like this, and the second is a trigram index that returns nothing for a two-character query without saying why. Pick the tokenizer for the content you actually store, and when a query is shorter than the tokenizer's floor, say so in the interface instead of showing zero results. ## Phase 4 · Suggestions that never move anything Automatic tags help you find things and never reorganise your library behind your back. ### Steps 1. Keep suggestion source and confidence on every tag The distinction is what stops guesses becoming facts. Files: `src/db/schema.sql`, `src/tags/suggest.ts` 2. Rank semantically only on top of keyword results A second ranker is useful; a replacement is unpredictable. Files: `src/search/rerank.ts` 3. Show cost before any paid run and cap it A library-wide embedding pass is a real invoice. Files: `src/embeddings/estimate.ts` ### Done when - [ ] No suggestion path writes to the filesystem - [ ] Suggested and manual tags stay distinguishable - [ ] The app works fully with embeddings disabled - [ ] Paid runs show an estimate and respect a ceiling ### Watch out - An app that files documents into folders on its own guess is delightful for a week and then you cannot find a contract because it decided the contract was a receipt, and by then you have stopped remembering where you put things. Tags are additive metadata, never a move, and a suggested tag stays labelled as suggested until you accept it. ## Not in this build - Multi-device sync. - Collaborative editing and sharing. - Native mobile clients. - Automatic filing that moves files. ## After v1, if you want it - Optical character recognition as an opt-in action on the files already in the failure list - Saved searches, which need nothing new in the index and a small amount in the interface - A second library root with its own index, once rebuild times at fifty thousand files are known
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Watched folders. Read only as far as this app is concerned. LIBRARY_PATHS=/home/you/Documents,/home/you/Archive # Required. Derived data. Deleting it should cost only rebuild time. INDEX_PATH=~/.local/share/fabric/index.db # Required. Per file. Exceeding it records a failure rather than blocking the queue. EXTRACT_TIMEOUT_SECONDS=60 # Required. Per worker. A malformed file should not take the app down. EXTRACT_MEMORY_MB=512 # Required. Recorded here and in the README because it decides what search can find. FTS_TOKENIZER=trigram # Required. Off by default. The app is fully usable without it. EMBEDDINGS_ENABLED=false # Optional · secret. Only if you enabled a hosted model. Never required for keyword search. EMBEDDINGS_API_KEY=sk-...redacted # Optional. Maximum estimated spend for one indexing pass before it refuses to start. EMBEDDINGS_COST_CEILING=5.00
# Fabric · product brief ## Problem The visible AI file workspace loop is buildable, but a credible replacement needs more than the first screen. Fabric earns its keep through polish, sync, importers, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A workspace that leaves your files exactly where you put them, tells you which ones it could not read instead of quietly indexing nothing, finds documents by the fragment you remember, and can be deleted entirely without taking anything with it. ## 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 and sharing. - Native mobile clients. - Automatic filing that moves files. - collaborative editing and sharing - native mobile clients - conflict-safe multi-device sync - mature importers and exporters ## Success criteria - A rebuild from scratch is confirmed to restore notes and tags - An empty extraction is confirmed to land in the failure list - The documented substring behaviour is confirmed by test - No suggestion path is confirmed to write to the filesystem - Exported notes are confirmed to reattach by hash
# Build brief · Fabric 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 local workspace that indexes everything you drop into it and leaves the files where you can find them without it to replace Fabric. 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 · a desktop app with filesystem access and a small binary - Index: SQLite with FTS5, stored beside the library but deletable · the index is derived data and must be rebuildable from the files alone - Originals: Left in place, never moved or renamed by the app · the day you stop using this, your files should be exactly where you put them - Extraction: One module per format, run in a worker, failures recorded not swallowed · a PDF that yields no text is a fact the user needs to see - Embeddings: Optional, local or key-provided, added only after keyword search works · semantic search on top of broken extraction finds nothing either ### Data model (create this before Phase 1) - files(id, path, size, mtime, content_sha256, first_seen_at, last_seen_at, missing_since) · path is a reference, never a copy - documents(file_id, kind, text, extracted_at, extractor, extractor_version, char_count) · the text pulled out, with what pulled it - extraction_failures(file_id, at, extractor, reason, retryable) · visible in the interface, not only in a log - fts(document rowid, text) · an FTS5 table whose tokenizer choice is recorded in the schema and in the README - notes(id, file_id, body, created_at, updated_at) · your own writing, which is the one thing here that is not derived - tags(file_id, tag, source, confidence) · source is manual or suggested, and a suggested tag never becomes manual by itself - embeddings(document_id, model, dim, vector) · present only if you enabled them, and rebuildable like everything else ### Phase 1 · Files stay files Build: Watch one or more folders, record path, size, modification time and content hash, and never move, rename or rewrite an original. Detect a file that moved by its hash rather than treating it as a deletion and an addition. Handle a file that disappears by marking it missing rather than deleting the notes attached to it. Trap: An app that takes ownership of files by copying them into its own store makes the store the only real copy, and then a bug in the store is a bug in your documents. Keep the originals where the user put them, treat the database as a cache, and add a rebuild command in this phase so the claim is tested from the start. Done when: Deleting the index database and rebuilding produces the same file list and the same notes attached to the same files, tested end to end. Moving a file on disk is detected by hash and keeps its notes and tags rather than orphaning them. A file that disappears is marked missing and its notes survive. No code path writes to, renames or moves a file in a watched folder, verified by review and by a read-only mount test. Do not build yet: No extraction, search, tags or embeddings. ### Phase 2 · Getting the text out, and admitting when you cannot Build: One extractor per format for the types you actually keep, run in a worker with a time limit and a memory ceiling. Record the extractor and its version on every document. A failure writes a row the interface shows, with a reason a person can act on and a retry button. Trap: A scanned PDF with no text layer extracts to an empty string, and unless you treat that as a failure it silently becomes a file that search will never find, which the user discovers months later while looking for the one document they needed. Treat a zero-length extraction from a format that should have text as a failure with a stated reason, and offer optical recognition as an explicit, opt-in, per-file action. Done when: A scanned PDF appears in the failure list with a reason rather than as an indexed empty document, tested with a real scan. An extractor that hangs is killed at the time limit and recorded as a failure rather than blocking the queue, tested with a deliberately slow fixture. Every document records which extractor and version produced it, so a later improvement can requeue exactly the affected files. Retrying a failure from the interface re-runs only that file. Do not build yet: No search, tags or embeddings yet. ### Phase 3 · Search that finds the thing you half remember Build: Build the FTS5 index and choose the tokenizer on purpose. The default splits on word boundaries and will not match inside a word, so a search for part of a filename or an identifier returns nothing; the trigram tokenizer matches substrings and also makes LIKE and GLOB indexed, but substrings shorter than three characters match no rows at all (checked against the current documentation). Decide, write the choice in the README, and handle the short-query case in the interface rather than returning an empty list. Trap: Accepting the default tokenizer and then wondering why searching for part of a product code finds nothing is the most common search complaint in an app like this, and the second is a trigram index that returns nothing for a two-character query without saying why. Pick the tokenizer for the content you actually store, and when a query is shorter than the tokenizer's floor, say so in the interface instead of showing zero results. Done when: A substring search inside a word returns the document under the chosen tokenizer, or the interface states plainly that substring search is unavailable, with the choice recorded in the README. A query shorter than the tokenizer's minimum shows an explanatory message rather than an empty result list. Results are ranked and the matched text is shown in context. Search over a library of at least ten thousand documents returns in a time you have measured and stated. Do not build yet: No embeddings, tags or export yet. ### Phase 4 · Suggestions that never move anything Build: Add suggested tags and optional semantic search as a second ranker over keyword results. Every suggestion is stored with source set to suggested and a confidence, is visibly distinct from a tag you wrote, and is accepted or dismissed by you. If a model is used, the model name and the per-run cost estimate are shown before it runs. Trap: An app that files documents into folders on its own guess is delightful for a week and then you cannot find a contract because it decided the contract was a receipt, and by then you have stopped remembering where you put things. Tags are additive metadata, never a move, and a suggested tag stays labelled as suggested until you accept it. Done when: No suggestion path writes to the filesystem or changes a file's path, verified by review and a filesystem-write assertion in the test. Suggested and manual tags are stored and displayed differently, and a suggested tag never becomes manual without an explicit action. With embeddings disabled the app is fully usable and the interface says what is unavailable. A run using a paid model shows an estimated cost first and respects a configured ceiling. Do not build yet: No export, backup or scale work yet. ### Phase 5 · Leaving, rebuilding and surviving a big library Build: Export notes and tags as Markdown beside their files or as one archive, with the file path and hash in the front matter. Back up the database and document the restore. Requeue extraction by extractor version. Measure scan, extraction and search on a library of at least fifty thousand files and state the numbers in the README. Trap: Exporting notes without recording which file each one belonged to produces a folder of orphaned text that is worse than no export, because it looks like a migration path until you try to use it. Put the original path and the content hash in the exported front matter, and write an import that reattaches notes by hash so a rebuilt library keeps them. Done when: Exported notes carry the original path and content hash, and importing them into a rebuilt index reattaches every note to the right file by hash, tested end to end. A full rebuild on a library of at least fifty thousand files completes and the time is recorded in the README. Requeueing by extractor version reprocesses exactly the documents produced by older versions. Backup and restore are documented and the restore is exercised by a test rather than described. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-device sync, which is a conflict resolution problem rather than a storage one and is most of what the subscription buys. - Collaborative editing and sharing, which needs identity, permissions and a server you then have to operate. - Native mobile clients, which is a second application with its own extraction story rather than a port. - Automatic filing that moves files, because a wrong guess about where a document belongs is a document you cannot find. - A hosted model that reads your whole library by default; anything that leaves the machine is opt in and shows its cost. ### README must contain - That originals are never moved, renamed or written, and that the index can be deleted and rebuilt - Which FTS5 tokenizer was chosen, what substring behaviour that gives, and the minimum query length it implies - That an empty extraction is treated as a failure and appears in a visible list with a reason - That suggested tags are additive metadata, are labelled as suggestions, and never move a file - The measured scan, extraction and search times on a library of at least fifty thousand files
# Architecture · Fabric ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript | a desktop app with filesystem access and a small binary | | Index | SQLite with FTS5, stored beside the library but deletable | the index is derived data and must be rebuildable from the files alone | | Originals | Left in place, never moved or renamed by the app | the day you stop using this, your files should be exactly where you put them | | Extraction | One module per format, run in a worker, failures recorded not swallowed | a PDF that yields no text is a fact the user needs to see | | Embeddings | Optional, local or key-provided, added only after keyword search works | semantic search on top of broken extraction finds nothing either | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Scanner | Paths, hashes, moves and missing files. | Any watcher, provided it never writes into a watched folder. | | Extractors | Per-format text with ceilings and recorded failures. | Any extractor per format, provided its name and version are stamped on the document. | | Search | The FTS5 index and the tokenizer contract. | Any engine, provided the substring behaviour is documented and tested. | | Suggestions | Tags with source and confidence. | Any model, provided nothing it produces can move a file. | | Export | Notes and tags with path and hash. | Any format, provided reattachment by hash still works. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `LIBRARY_PATHS` · required · Watched folders. Read only as far as this app is concerned. - `INDEX_PATH` · required · Derived data. Deleting it should cost only rebuild time. - `EXTRACT_TIMEOUT_SECONDS` · required · Per file. Exceeding it records a failure rather than blocking the queue. - `EXTRACT_MEMORY_MB` · required · Per worker. A malformed file should not take the app down. - `FTS_TOKENIZER` · required · Recorded here and in the README because it decides what search can find. - `EMBEDDINGS_ENABLED` · required · Off by default. The app is fully usable without it. - `EMBEDDINGS_API_KEY` · optional, secret · Only if you enabled a hosted model. Never required for keyword search. - `EMBEDDINGS_COST_CEILING` · optional · Maximum estimated spend for one indexing pass before it refuses to start. ## 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 · Fabric product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri 2 with React and TypeScript, SQLite with FTS5, stored beside the library but deletable, Left in place, never moved or renamed by the app, One module per format, run in a worker, failures recorded not swallowed, Optional, local or key-provided, added only after keyword search works. - 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 - An app that takes ownership of files by copying them into its own store makes the store the only real copy, and then a bug in the store is a bug in your documents. Keep the originals where the user put them, treat the database as a cache, and add a rebuild command in this phase so the claim is tested from the start. - A scanned PDF with no text layer extracts to an empty string, and unless you treat that as a failure it silently becomes a file that search will never find, which the user discovers months later while looking for the one document they needed. Treat a zero-length extraction from a format that should have text as a failure with a stated reason, and offer optical recognition as an explicit, opt-in, per-file action. - Accepting the default tokenizer and then wondering why searching for part of a product code finds nothing is the most common search complaint in an app like this, and the second is a trigram index that returns nothing for a two-character query without saying why. Pick the tokenizer for the content you actually store, and when a query is shorter than the tokenizer's floor, say so in the interface instead of showing zero results. - An app that files documents into folders on its own guess is delightful for a week and then you cannot find a contract because it decided the contract was a receipt, and by then you have stopped remembering where you put things. Tags are additive metadata, never a move, and a suggested tag stays labelled as suggested until you accept it. - Exporting notes without recording which file each one belonged to produces a folder of orphaned text that is worse than no export, because it looks like a migration path until you try to use it. Put the original path and the content hash in the exported front matter, and write an import that reattaches notes by hash so a rebuilt library keeps them.
# Delivery milestones · Fabric Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Files stay files The library is a folder you already understand, and the index can be deleted without losing anything. ### Steps 1. Index by content hash, reference by path A hash survives a rename; a path does not. Files: `src-tauri/src/index/scan.rs`, `src/db/schema.sql` 2. Write the rebuild command now, not later It is the test that the index is really disposable. Files: `src-tauri/src/index/rebuild.rs`, `tests/rebuild.test.ts` 3. Mark missing files rather than deleting rows An unplugged drive is not a decision to delete your notes. Files: `src-tauri/src/index/scan.rs` ### Done when - [ ] Deleting and rebuilding the index restores notes and tags - [ ] A moved file keeps its notes - [ ] A missing file is marked, not removed - [ ] Nothing in a watched folder is ever written by the app ### Watch out - An app that takes ownership of files by copying them into its own store makes the store the only real copy, and then a bug in the store is a bug in your documents. Keep the originals where the user put them, treat the database as a cache, and add a rebuild command in this phase so the claim is tested from the start. ## M2 · Getting the text out, and admitting when you cannot Every file either has extracted text or a visible reason why it does not. ### Steps 1. Treat an empty extraction as a failure Silently indexing nothing is the worst available outcome. Files: `src-tauri/src/extract/mod.rs`, `tests/empty-extraction.test.ts` 2. Run extractors with a time and memory ceiling One malformed file should not stop the queue. Files: `src-tauri/src/extract/worker.rs` 3. Stamp the extractor and version on every document So a fixed extractor can requeue exactly its own failures. Files: `src/db/schema.sql` ### Done when - [ ] An empty extraction lands in the failure list with a reason - [ ] A hanging extractor is killed and recorded - [ ] Every document names its extractor and version - [ ] Retry re-runs one file only ### Watch out - A scanned PDF with no text layer extracts to an empty string, and unless you treat that as a failure it silently becomes a file that search will never find, which the user discovers months later while looking for the one document they needed. Treat a zero-length extraction from a format that should have text as a failure with a stated reason, and offer optical recognition as an explicit, opt-in, per-file action. ## M3 · Search that finds the thing you half remember Keyword search returns the right document for partial words, and the tokenizer choice is deliberate. ### Steps 1. Choose the tokenizer deliberately and record it This single decision determines what search can ever find. Files: `src/db/schema.sql`, `docs/search.md` 2. Handle queries below the tokenizer's floor An empty list reads as a missing document. Files: `src/search/query.ts`, `tests/short-query.test.ts` 3. Measure on a real-sized library Ten documents tells you nothing about ten thousand. Files: `tests/search-perf.test.ts` ### Done when - [ ] Substring behaviour matches the documented choice - [ ] A too-short query explains itself - [ ] Results are ranked and show matched context - [ ] Search time on ten thousand documents is measured and stated ### Watch out - Accepting the default tokenizer and then wondering why searching for part of a product code finds nothing is the most common search complaint in an app like this, and the second is a trigram index that returns nothing for a two-character query without saying why. Pick the tokenizer for the content you actually store, and when a query is shorter than the tokenizer's floor, say so in the interface instead of showing zero results. ## M4 · Suggestions that never move anything Automatic tags help you find things and never reorganise your library behind your back. ### Steps 1. Keep suggestion source and confidence on every tag The distinction is what stops guesses becoming facts. Files: `src/db/schema.sql`, `src/tags/suggest.ts` 2. Rank semantically only on top of keyword results A second ranker is useful; a replacement is unpredictable. Files: `src/search/rerank.ts` 3. Show cost before any paid run and cap it A library-wide embedding pass is a real invoice. Files: `src/embeddings/estimate.ts` ### Done when - [ ] No suggestion path writes to the filesystem - [ ] Suggested and manual tags stay distinguishable - [ ] The app works fully with embeddings disabled - [ ] Paid runs show an estimate and respect a ceiling ### Watch out - An app that files documents into folders on its own guess is delightful for a week and then you cannot find a contract because it decided the contract was a receipt, and by then you have stopped remembering where you put things. Tags are additive metadata, never a move, and a suggested tag stays labelled as suggested until you accept it. ## M5 · Leaving, rebuilding and surviving a big library (production only) Everything you added is exportable, the index is genuinely disposable, and the app is usable at real size. ### Steps 1. Export with path and hash in the front matter Without the hash the export is a pile of loose text. Files: `src/export/markdown.ts`, `tests/export-roundtrip.test.ts` 2. Requeue extraction by extractor version An improved extractor should not mean a full reindex. Files: `src-tauri/src/extract/requeue.rs` 3. Measure at fifty thousand files and publish the numbers Every claim about speed here is a number or it is nothing. Files: `tests/scale.test.ts`, `README.md` ### Done when - [ ] Exported notes reattach by hash after a rebuild - [ ] A full rebuild at fifty thousand files completes and is timed - [ ] Requeueing by extractor version selects the right documents - [ ] Restore is exercised in the test suite ### Watch out - Exporting notes without recording which file each one belonged to produces a folder of orphaned text that is worse than no export, because it looks like a migration path until you try to use it. Put the original path and the content hash in the exported front matter, and write an import that reattaches notes by hash so a rebuilt library keeps them.
# Operations · Fabric ## Backup Back up your files the way you already do; they are the real data. Back up the index only to save rebuild time, and back up notes separately because they are the one thing in the database that cannot be regenerated from disk. ## Restore Restore the files, then rebuild the index and reattach notes by content hash. The rebuild test from Phase 1 is what makes this a procedure rather than a hope. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a desktop app the signals are in the interface: the extraction failure count, the missing file count, the age of the last full scan, and the rebuild time trend as the library grows. ## Incident checklist A document you cannot find is almost always an extraction failure or a tokenizer limitation, and both are visible: the failure list names the file, and the search screen states the minimum query length. A corrupted index is a rebuild, not a loss. 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 rebuild from scratch is confirmed to restore notes and tags - [ ] An empty extraction is confirmed to land in the failure list - [ ] The documented substring behaviour is confirmed by test - [ ] No suggestion path is confirmed to write to the filesystem - [ ] Exported notes are confirmed to reattach by hash ## Launch constraint Do not market omitted Fabric 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. Watched folders. Read only as far as this app is concerned. LIBRARY_PATHS=/home/you/Documents,/home/you/Archive # Required. Derived data. Deleting it should cost only rebuild time. INDEX_PATH=~/.local/share/fabric/index.db # Required. Per file. Exceeding it records a failure rather than blocking the queue. EXTRACT_TIMEOUT_SECONDS=60 # Required. Per worker. A malformed file should not take the app down. EXTRACT_MEMORY_MB=512 # Required. Recorded here and in the README because it decides what search can find. FTS_TOKENIZER=trigram # Required. Off by default. The app is fully usable without it. EMBEDDINGS_ENABLED=false # Optional · secret. Only if you enabled a hosted model. Never required for keyword search. EMBEDDINGS_API_KEY=sk-...redacted # Optional. Maximum estimated spend for one indexing pass before it refuses to start. EMBEDDINGS_COST_CEILING=5.00
$ choose a build depth, inspect the files, then open the complete pack in your agent
Fabric: People pay for a writing surface they trust for years, plus migration tools, sync, and tiny interaction details that disappear into habit.
xcollaborative editing and sharing
xnative mobile clients
xconflict-safe multi-device sync
xmature importers and exporters
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Fabric pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/user | $0/user | Core notes, canvas, bookmarks/files, AI assistant, search and spaces; storage and AI are capped but current official pages publish no numeric amounts |
| plus | — | $8/user | Unlimited items; 50 GB storage; 50 MB/file; 100 spaces; 5 AIs; light AI usage; 4 voice/meeting recordings/day |
| pro | — | $18/user | 250 GB storage; 5 GB/file; unlimited spaces; 10 AIs; moderate AI use; assistant web search; up to 10 scheduled AI jobs |
| max | — | $54/user | 1 TB storage; 5 GB/file; unlimited spaces; 50 AIs; heavy AI use; up to 50 scheduled jobs |
| teams | custom | — | 2 TB+ storage; 5 GB/file; 50+ AIs; 50+ AI jobs; pooled heavy usage, centralized billing and admin usage stats |
free tierFree plan exists with core features; storage and AI usage are capped, but no numeric free allowances are published on the current official pages
billingmonthly + annual; annual page prices are $8/$18/$54 per month equivalent; exact monthly prices were not exposed; no refunds
hidden costsAI usage can be topped up manually or automatically, creating variable spend; spending limits are optional. AI allowances are described only as light/moderate/heavy, not as numeric credits. No refunds after billing.
verified 2026-08-13 · source ↗
Vibecode Fabric
Kinda. The core of Fabric is buildable in a weekend with the prompt on this page, but there are real gaps: collaborative editing and sharing, native mobile clients. Read the honest list above before committing.
How much does Fabric cost?
Fabric'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 Fabric?
Honestly: collaborative editing and sharing; native mobile clients; conflict-safe multi-device sync; mature importers and exporters. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Fabric?
Yes: Karakeep (Links, notes, images and PDFs get searchable, preserved and optionally auto-tagged.) Atomic (Notes and web clips become a semantic graph; arbitrary files are not its strongest suit.) The prompt is for when you want it exactly your way.