Vibecode Genei
track this build4 phases, 12 steps, beginner friendly0%The visible research summarization loop is buildable, but a credible replacement needs more than the first screen. Genei earns its keep through data, import reliability, so expect a weekend or multi-day build and a narrower personal scope.
You are building a lean indie version of Genei. 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 ===== # Genei · indie build A personal research library whose hard problems are pulling readable text out of documents designed for print, getting bibliographic metadata from a source rather than a guess, and making every generated sentence traceable to a passage. Estimated effort: **weekend**. 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 | | --- | --- | --- | | Runtime | TypeScript on Node 22 with SQLite, run locally | a personal corpus with no reason to leave the machine | | Originals | Kept byte for byte on disk, hashed, and never the thing that gets rewritten | the stored document is the evidence for every claim above it | | Extraction | Layout-aware text with character offsets preserved per page | a quote without an offset cannot be checked | | Metadata | Fetched from a bibliographic service and stored per field with its source | the difference between a citation and a guess is provenance | | Summaries | Extractive first, generated second, and always span-anchored | an unanchored sentence is dropped before it reaches the screen | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the importer, the index and the interface locally. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **Twenty documents you already have, including awkward ones** · free - Why: Every extraction bug in this category is a layout bug, and a folder of clean single-column articles will pass tests that a scanned appendix or a two-column paper fails. - Get it: Your own PDFs and EPUBs, including at least one two-column paper, one scan and one long book. - Verify: All twenty import and the two-column one extracts in reading order - [ ] **A contact address for the metadata service** · free - Why: The bibliographic API asks callers to identify themselves and offers better service to those who do, and it costs nothing to comply. - Get it: An address you monitor, sent as a mailto parameter or in the User-Agent as the service documents. - Verify: A lookup succeeds and your address appears in the request - [ ] **A reference manager to import into** · free - Why: The citation export is only credible if a real reference manager reads it, and format bugs are invisible until you try. - Get it: Any mainstream open-source reference manager. - Verify: Exported BibTeX and CSL-JSON both import with fields intact - [ ] **A model key, if you want generated summaries** (optional) · pennies per document, capped by your configured ceiling - Why: The generated summary path is optional by design, and the extractive path has to work without it, so this is genuinely not required to finish the build. - Get it: Any provider whose API returns structured output, so spans can be part of the contract. - Verify: A summarisation returns sentences with offsets that resolve ## 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: - Licensed scholarly metadata and citation graphs. - Logged-in automation against publisher sites. - Paywall circumvention of any kind. - Team libraries and shared annotation. - licensed scholarly metadata - publisher-specific import reliability - citation graph scale - team libraries and institutional access If one of those is essential to you, that is the reason to keep paying for Genei, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Genei 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 research library that keeps the sources you fed it, gets their metadata right, and never produces a sentence it cannot point at to replace Genei. 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. ### Rule zero Two rules, and the first is the whole difference between a research tool and a liability. Nothing is asserted that cannot be pointed at. Every sentence in a generated summary carries a reference to a character range in a stored document, shown in the interface and clickable, and a sentence the model produces without one is dropped rather than displayed. The same rule applies twice as hard to citations: a reference list is assembled from metadata fields you fetched and stored, never from a model's memory, because a plausible citation to a paper that does not exist is the single most damaging output this category produces. Second, import only what you can legitimately reach. Files you already have, open access copies located through a service that publishes how to use it, and pages an anonymous visitor can read. No borrowed institutional session, no bypassing a paywall, and no logged-in browser automation against a publisher, because the terms you would be breaking are ones you personally agreed to. ### Stack (fixed, do not substitute) - Runtime: TypeScript on Node 22 with SQLite, run locally · a personal corpus with no reason to leave the machine - Originals: Kept byte for byte on disk, hashed, and never the thing that gets rewritten · the stored document is the evidence for every claim above it - Extraction: Layout-aware text with character offsets preserved per page · a quote without an offset cannot be checked - Metadata: Fetched from a bibliographic service and stored per field with its source · the difference between a citation and a guess is provenance - Summaries: Extractive first, generated second, and always span-anchored · an unanchored sentence is dropped before it reaches the screen ### Data model (create this before Phase 1) - documents(id, path, sha256, kind, imported_at, title, page_count, text, extractor, extractor_version) - pages(document_id, page_number, start_offset, end_offset) · so a character offset resolves to a page number a reader can turn to - works(document_id, doi, isbn, title, container, year, publisher, url, open_access_url) · the bibliographic identity, once found - work_fields(document_id, field, value, source, fetched_at) · every field records where it came from, including manual - notes(id, document_id, start_offset, end_offset, body, created_at) · your own writing, anchored like everything else - claims(id, document_id, summary_id, sentence, start_offset, end_offset, method) · method is extracted or generated, and a generated claim without offsets is never stored - A summary is a list of claims, each pointing at a character range in documents.text, which the interface renders as a highlight on the stored document. ### Phase 1 · Keeping the document and knowing where you are in it Build: Import PDFs, EPUBs, HTML and plain text. Store the original unchanged and hashed. Extract text preserving reading order, and record the character range each page occupies. Handle two-column layouts, running headers and ligatures, and record the extractor and its version on the document. Trap: Naive PDF extraction reads a two-column paper across the columns, so sentences interleave and every quote you later pull is half from one column and half from the other, which looks like the model hallucinating when it is actually your importer. Extract with layout awareness, check a two-column paper by eye during this phase, and store the page offsets so any quote can be checked against the printed page. Done when: A two-column paper extracts in reading order, verified against the printed pages. Any character offset resolves to a page number and the interface can highlight that range on the stored document. The original file is byte-identical to what was imported, checked by hash. Every document records its extractor and version so a later improvement can requeue exactly the affected documents. Do not build yet: No metadata lookup, search, summarising or citation export. ### Phase 2 · Finding out what the thing actually is Build: Extract any DOI from the document, then look the work up through a bibliographic API, using its documented courtesy convention: Crossref's REST API offers a faster pool to callers who supply a contact address, as a mailto query parameter or in the User-Agent, and asks you to back off on a 429 (checked against their current guidance). Store every field with its source and fetch time. Where nothing is found, leave fields empty and let the user type them, marked as manual. Trap: Filling in missing authors or a year by asking a language model produces a citation that looks right, passes review, and is wrong, and the person who finds out is a reviewer rather than you. Leave the field empty, mark it as needing attention, and make the citation export refuse to emit a reference with an unsourced required field. Done when: Every stored bibliographic field has a source of either the service name or manual, enforced by a not-null constraint. No code path writes a bibliographic field from a model's output, verified by review and asserted by a test over the write paths. A 429 from the metadata service backs off and retries with a bounded delay rather than failing the import. A document with no identifier imports successfully with empty fields marked as needing attention. Do not build yet: No search, summarising or export yet. ### Phase 3 · Finding the passage you remember reading Build: Full-text search over extracted text with results shown as passages around the match, each carrying its document, page and offsets. Filter by author, year, container and tag. Searching returns the passage, not just the document, because the passage is what you were looking for. Trap: Returning a list of matching documents rather than matching passages means every search ends with you opening a ninety page PDF and using the reader's own find function, which is the workflow the tool was supposed to replace. Return ranked passages with their offsets, and make clicking one open the document scrolled to the highlight. Done when: A search returns passages with document, page and offsets, and clicking one opens the highlighted range. Metadata filters combine with the text query and both narrow the same result set. Search runs entirely locally with no outbound request, asserted with the network disabled. Search over a corpus of at least two thousand documents returns within a time you have measured and stated. Do not build yet: No summarising, citation export or cost controls yet. ### Phase 4 · Summaries that point at the page Build: Produce an extractive summary first: the highest-ranked sentences with their offsets, which needs no model at all. Then, optionally, a generated summary in which each sentence must be emitted with the offsets it was derived from; verify each one resolves to real text before storing, and drop the ones that do not. Show the model name and the estimated cost before any paid call. Trap: Asking a model for a summary and then trying to attach citations afterwards by searching for similar text produces confident-looking anchors on sentences that were invented, because near-enough matching always finds something. Require the spans as part of the generation contract, verify that the quoted characters actually appear at those offsets, and discard any sentence that fails rather than repairing it. Done when: Every stored generated claim has offsets whose text exists at that range in the document, asserted on write. A claim returned without offsets or with offsets that do not resolve is dropped and the drop is counted and shown. The extractive summary works with no model configured and no network, tested. A paid summarisation shows the model name and an estimated cost before running and respects a configured ceiling. Do not build yet: No citation export, backups or cost history yet. ### Phase 5 · Getting citations out, and getting everything out Build: Export references as BibTeX and CSL-JSON built only from stored fields, refusing any reference missing a required field and naming what is missing. Export the whole library as originals plus a manifest of metadata, notes and claims with their offsets. Cap monthly model spend and record every call. Back up the database and exercise the restore in the suite. Trap: An export that silently omits a missing year or invents a container title produces a bibliography that compiles and is wrong in a way nobody notices until a copy editor does. Refuse the reference, list which fields are missing and for which document, and make fixing them a visible task in the interface rather than an error in a log. Done when: An export containing a work with a missing required field fails with that document and field named, and no placeholder is emitted. Exported BibTeX and CSL-JSON both import into a mainstream reference manager, checked by doing it. The library export contains the original files, and importing it into an empty install restores documents, notes and claims with working offsets. Model spend is capped monthly and every call is recorded with its cost. Do not build yet: Nothing after this. ### Out of scope (and why) - Licensed scholarly metadata and citation graphs, which are the paid datasets behind the products in this category and cannot be reproduced from open sources. - Logged-in browser automation against publisher sites, which breaks terms you personally accepted and is how accounts and institutional access get revoked. - Any paywall circumvention, including proxies and shadow libraries, whatever the argument for it. - Team libraries and shared annotation, which need identity, permissions and a server you then operate. - A chat interface over the whole corpus, which makes unanchored answers easy to produce and hard to check, against the rule above. ### README must contain - That every generated sentence carries verified offsets into a stored document, and unanchored sentences are dropped rather than shown - That bibliographic fields come from a service or from you, each recorded with its source, and never from a model - That citation export refuses a reference with a missing required field instead of filling it in - The courtesy convention used with the metadata service, including the contact address sent and the backoff on rate limiting - The measured extraction and search times on a corpus of at least two thousand documents ===== AGENTS.md ===== # Agent instructions · Genei indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript on Node 22 with SQLite, run locally, Kept byte for byte on disk, hashed, and never the thing that gets rewritten, Layout-aware text with character offsets preserved per page, Fetched from a bibliographic service and stored per field with its source, Extractive first, generated second, and always span-anchored. 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 - Naive PDF extraction reads a two-column paper across the columns, so sentences interleave and every quote you later pull is half from one column and half from the other, which looks like the model hallucinating when it is actually your importer. Extract with layout awareness, check a two-column paper by eye during this phase, and store the page offsets so any quote can be checked against the printed page. - Filling in missing authors or a year by asking a language model produces a citation that looks right, passes review, and is wrong, and the person who finds out is a reviewer rather than you. Leave the field empty, mark it as needing attention, and make the citation export refuse to emit a reference with an unsourced required field. - Returning a list of matching documents rather than matching passages means every search ends with you opening a ninety page PDF and using the reader's own find function, which is the workflow the tool was supposed to replace. Return ranked passages with their offsets, and make clicking one open the document scrolled to the highlight. - Asking a model for a summary and then trying to attach citations afterwards by searching for similar text produces confident-looking anchors on sentences that were invented, because near-enough matching always finds something. Require the spans as part of the generation contract, verify that the quoted characters actually appear at those offsets, and discard any sentence that fails rather than repairing it. ===== BUILD_PLAN.md ===== # Build plan · Genei A personal research library whose hard problems are pulling readable text out of documents designed for print, getting bibliographic metadata from a source rather than a guess, and making every generated sentence traceable to a passage. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Keeping the document and knowing where you are in it Text comes out with offsets that map back to a page and a highlight. ### Steps 1. Extract with layout awareness and check a two-column paper The failure that gets blamed on the model later. Files: `src/extract/pdf.ts`, `tests/two-column.test.ts` 2. Record page ranges as character offsets An offset that maps to a page number is a checkable quote. Files: `src/extract/pages.ts`, `src/db/schema.sql` 3. Keep the original untouched and hashed It is the evidence behind everything the app later says. Files: `src/store/originals.ts` ### Done when - [ ] A two-column paper extracts in reading order - [ ] An offset resolves to a page and highlights correctly - [ ] The stored original is byte-identical to the import - [ ] Extractor and version are recorded per document ### Watch out - Naive PDF extraction reads a two-column paper across the columns, so sentences interleave and every quote you later pull is half from one column and half from the other, which looks like the model hallucinating when it is actually your importer. Extract with layout awareness, check a two-column paper by eye during this phase, and store the page offsets so any quote can be checked against the printed page. ## Phase 2 · Finding out what the thing actually is Bibliographic fields come from a service or from you, and each one says which. ### Steps 1. Look up by identifier and record the source per field Provenance per field is what makes a citation defensible. Files: `src/metadata/crossref.ts`, `src/db/schema.sql` 2. Send a contact address and honour 429 with backoff The documented courtesy, and it gets you the faster pool. Files: `src/metadata/client.ts`, `tests/backoff.test.ts` 3. Forbid model output in bibliographic fields A test over the write paths, not a promise in a comment. Files: `tests/no-generated-metadata.test.ts` ### Done when - [ ] Every bibliographic field records its source - [ ] No model output can reach a bibliographic field - [ ] A 429 backs off within a bounded delay - [ ] A document with no identifier still imports ### Watch out - Filling in missing authors or a year by asking a language model produces a citation that looks right, passes review, and is wrong, and the person who finds out is a reviewer rather than you. Leave the field empty, mark it as needing attention, and make the citation export refuse to emit a reference with an unsourced required field. ## Phase 3 · Finding the passage you remember reading Search covers the whole corpus, returns the passage in context, and filters on metadata. ### Steps 1. Return passages with offsets, not document hits The passage is the thing being searched for. Files: `src/search/passages.ts` 2. Combine metadata filters with the text query Author and year are how researchers actually narrow. Files: `src/search/filters.ts` 3. Assert search makes no network request A local corpus should stay local. Files: `tests/offline-search.test.ts` ### Done when - [ ] Results are passages with document, page and offsets - [ ] Metadata filters narrow the text results - [ ] Search issues no outbound request - [ ] Search time on two thousand documents is measured and stated ### Watch out - Returning a list of matching documents rather than matching passages means every search ends with you opening a ninety page PDF and using the reader's own find function, which is the workflow the tool was supposed to replace. Return ranked passages with their offsets, and make clicking one open the document scrolled to the highlight. ## Phase 4 · Summaries that point at the page Every sentence shown is either a quotation or a generated sentence with a span behind it. ### Steps 1. Ship extractive summarising before anything generated It works offline, costs nothing, and sets the anchoring contract. Files: `src/summary/extractive.ts` 2. Verify every generated span against the stored text The characters are there or the sentence does not ship. Files: `src/summary/verify.ts`, `tests/span-verify.test.ts` 3. Show model and estimated cost before a paid call A corpus-wide summarisation is a real invoice. Files: `src/summary/estimate.ts` ### Done when - [ ] Every generated claim resolves to real text at its offsets - [ ] Unanchored claims are dropped and counted - [ ] Extractive summarising works offline with no model - [ ] Paid runs show a cost estimate and respect a ceiling ### Watch out - Asking a model for a summary and then trying to attach citations afterwards by searching for similar text produces confident-looking anchors on sentences that were invented, because near-enough matching always finds something. Require the spans as part of the generation contract, verify that the quoted characters actually appear at those offsets, and discard any sentence that fails rather than repairing it. ## Not in this build - Licensed scholarly metadata and citation graphs. - Logged-in automation against publisher sites. - Paywall circumvention of any kind. - Team libraries and shared annotation. ## After v1, if you want it - Optical character recognition for scans, offered per document from the failure list - A second metadata service as a fallback, recorded with its own source name per field - Cross-document claim search, once enough summaries exist for it to be worth ranking ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Original files, byte for byte as imported. LIBRARY_DIR=./data/library # Required. Documents, metadata, notes and claims with their offsets. DATABASE_PATH=./data/library.db # Required. Sent with metadata lookups as the service's documented courtesy convention. CROSSREF_MAILTO=you@example.com # Required. Bounded backoff on rate limiting. Never an unlimited retry loop. METADATA_MAX_RETRIES=4 # Required. Set to none for extractive summaries only, which is the default and works offline. SUMMARY_MODEL=none # Optional · secret. Only when a generated summary model is configured. SUMMARY_API_KEY=sk-...redacted # Optional. Model spend per month. Reaching it refuses further paid calls. MONTHLY_SPEND_CEILING=10.00
You are building a lean indie version of Genei. 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 ===== # Genei · indie build A personal research library whose hard problems are pulling readable text out of documents designed for print, getting bibliographic metadata from a source rather than a guess, and making every generated sentence traceable to a passage. Estimated effort: **weekend**. 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 | | --- | --- | --- | | Runtime | TypeScript on Node 22 with SQLite, run locally | a personal corpus with no reason to leave the machine | | Originals | Kept byte for byte on disk, hashed, and never the thing that gets rewritten | the stored document is the evidence for every claim above it | | Extraction | Layout-aware text with character offsets preserved per page | a quote without an offset cannot be checked | | Metadata | Fetched from a bibliographic service and stored per field with its source | the difference between a citation and a guess is provenance | | Summaries | Extractive first, generated second, and always span-anchored | an unanchored sentence is dropped before it reaches the screen | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the importer, the index and the interface locally. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **Twenty documents you already have, including awkward ones** · free - Why: Every extraction bug in this category is a layout bug, and a folder of clean single-column articles will pass tests that a scanned appendix or a two-column paper fails. - Get it: Your own PDFs and EPUBs, including at least one two-column paper, one scan and one long book. - Verify: All twenty import and the two-column one extracts in reading order - [ ] **A contact address for the metadata service** · free - Why: The bibliographic API asks callers to identify themselves and offers better service to those who do, and it costs nothing to comply. - Get it: An address you monitor, sent as a mailto parameter or in the User-Agent as the service documents. - Verify: A lookup succeeds and your address appears in the request - [ ] **A reference manager to import into** · free - Why: The citation export is only credible if a real reference manager reads it, and format bugs are invisible until you try. - Get it: Any mainstream open-source reference manager. - Verify: Exported BibTeX and CSL-JSON both import with fields intact - [ ] **A model key, if you want generated summaries** (optional) · pennies per document, capped by your configured ceiling - Why: The generated summary path is optional by design, and the extractive path has to work without it, so this is genuinely not required to finish the build. - Get it: Any provider whose API returns structured output, so spans can be part of the contract. - Verify: A summarisation returns sentences with offsets that resolve ## 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: - Licensed scholarly metadata and citation graphs. - Logged-in automation against publisher sites. - Paywall circumvention of any kind. - Team libraries and shared annotation. - licensed scholarly metadata - publisher-specific import reliability - citation graph scale - team libraries and institutional access If one of those is essential to you, that is the reason to keep paying for Genei, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Genei 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 research library that keeps the sources you fed it, gets their metadata right, and never produces a sentence it cannot point at to replace Genei. 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. ### Rule zero Two rules, and the first is the whole difference between a research tool and a liability. Nothing is asserted that cannot be pointed at. Every sentence in a generated summary carries a reference to a character range in a stored document, shown in the interface and clickable, and a sentence the model produces without one is dropped rather than displayed. The same rule applies twice as hard to citations: a reference list is assembled from metadata fields you fetched and stored, never from a model's memory, because a plausible citation to a paper that does not exist is the single most damaging output this category produces. Second, import only what you can legitimately reach. Files you already have, open access copies located through a service that publishes how to use it, and pages an anonymous visitor can read. No borrowed institutional session, no bypassing a paywall, and no logged-in browser automation against a publisher, because the terms you would be breaking are ones you personally agreed to. ### Stack (fixed, do not substitute) - Runtime: TypeScript on Node 22 with SQLite, run locally · a personal corpus with no reason to leave the machine - Originals: Kept byte for byte on disk, hashed, and never the thing that gets rewritten · the stored document is the evidence for every claim above it - Extraction: Layout-aware text with character offsets preserved per page · a quote without an offset cannot be checked - Metadata: Fetched from a bibliographic service and stored per field with its source · the difference between a citation and a guess is provenance - Summaries: Extractive first, generated second, and always span-anchored · an unanchored sentence is dropped before it reaches the screen ### Data model (create this before Phase 1) - documents(id, path, sha256, kind, imported_at, title, page_count, text, extractor, extractor_version) - pages(document_id, page_number, start_offset, end_offset) · so a character offset resolves to a page number a reader can turn to - works(document_id, doi, isbn, title, container, year, publisher, url, open_access_url) · the bibliographic identity, once found - work_fields(document_id, field, value, source, fetched_at) · every field records where it came from, including manual - notes(id, document_id, start_offset, end_offset, body, created_at) · your own writing, anchored like everything else - claims(id, document_id, summary_id, sentence, start_offset, end_offset, method) · method is extracted or generated, and a generated claim without offsets is never stored - A summary is a list of claims, each pointing at a character range in documents.text, which the interface renders as a highlight on the stored document. ### Phase 1 · Keeping the document and knowing where you are in it Build: Import PDFs, EPUBs, HTML and plain text. Store the original unchanged and hashed. Extract text preserving reading order, and record the character range each page occupies. Handle two-column layouts, running headers and ligatures, and record the extractor and its version on the document. Trap: Naive PDF extraction reads a two-column paper across the columns, so sentences interleave and every quote you later pull is half from one column and half from the other, which looks like the model hallucinating when it is actually your importer. Extract with layout awareness, check a two-column paper by eye during this phase, and store the page offsets so any quote can be checked against the printed page. Done when: A two-column paper extracts in reading order, verified against the printed pages. Any character offset resolves to a page number and the interface can highlight that range on the stored document. The original file is byte-identical to what was imported, checked by hash. Every document records its extractor and version so a later improvement can requeue exactly the affected documents. Do not build yet: No metadata lookup, search, summarising or citation export. ### Phase 2 · Finding out what the thing actually is Build: Extract any DOI from the document, then look the work up through a bibliographic API, using its documented courtesy convention: Crossref's REST API offers a faster pool to callers who supply a contact address, as a mailto query parameter or in the User-Agent, and asks you to back off on a 429 (checked against their current guidance). Store every field with its source and fetch time. Where nothing is found, leave fields empty and let the user type them, marked as manual. Trap: Filling in missing authors or a year by asking a language model produces a citation that looks right, passes review, and is wrong, and the person who finds out is a reviewer rather than you. Leave the field empty, mark it as needing attention, and make the citation export refuse to emit a reference with an unsourced required field. Done when: Every stored bibliographic field has a source of either the service name or manual, enforced by a not-null constraint. No code path writes a bibliographic field from a model's output, verified by review and asserted by a test over the write paths. A 429 from the metadata service backs off and retries with a bounded delay rather than failing the import. A document with no identifier imports successfully with empty fields marked as needing attention. Do not build yet: No search, summarising or export yet. ### Phase 3 · Finding the passage you remember reading Build: Full-text search over extracted text with results shown as passages around the match, each carrying its document, page and offsets. Filter by author, year, container and tag. Searching returns the passage, not just the document, because the passage is what you were looking for. Trap: Returning a list of matching documents rather than matching passages means every search ends with you opening a ninety page PDF and using the reader's own find function, which is the workflow the tool was supposed to replace. Return ranked passages with their offsets, and make clicking one open the document scrolled to the highlight. Done when: A search returns passages with document, page and offsets, and clicking one opens the highlighted range. Metadata filters combine with the text query and both narrow the same result set. Search runs entirely locally with no outbound request, asserted with the network disabled. Search over a corpus of at least two thousand documents returns within a time you have measured and stated. Do not build yet: No summarising, citation export or cost controls yet. ### Phase 4 · Summaries that point at the page Build: Produce an extractive summary first: the highest-ranked sentences with their offsets, which needs no model at all. Then, optionally, a generated summary in which each sentence must be emitted with the offsets it was derived from; verify each one resolves to real text before storing, and drop the ones that do not. Show the model name and the estimated cost before any paid call. Trap: Asking a model for a summary and then trying to attach citations afterwards by searching for similar text produces confident-looking anchors on sentences that were invented, because near-enough matching always finds something. Require the spans as part of the generation contract, verify that the quoted characters actually appear at those offsets, and discard any sentence that fails rather than repairing it. Done when: Every stored generated claim has offsets whose text exists at that range in the document, asserted on write. A claim returned without offsets or with offsets that do not resolve is dropped and the drop is counted and shown. The extractive summary works with no model configured and no network, tested. A paid summarisation shows the model name and an estimated cost before running and respects a configured ceiling. Do not build yet: No citation export, backups or cost history yet. ### Phase 5 · Getting citations out, and getting everything out Build: Export references as BibTeX and CSL-JSON built only from stored fields, refusing any reference missing a required field and naming what is missing. Export the whole library as originals plus a manifest of metadata, notes and claims with their offsets. Cap monthly model spend and record every call. Back up the database and exercise the restore in the suite. Trap: An export that silently omits a missing year or invents a container title produces a bibliography that compiles and is wrong in a way nobody notices until a copy editor does. Refuse the reference, list which fields are missing and for which document, and make fixing them a visible task in the interface rather than an error in a log. Done when: An export containing a work with a missing required field fails with that document and field named, and no placeholder is emitted. Exported BibTeX and CSL-JSON both import into a mainstream reference manager, checked by doing it. The library export contains the original files, and importing it into an empty install restores documents, notes and claims with working offsets. Model spend is capped monthly and every call is recorded with its cost. Do not build yet: Nothing after this. ### Out of scope (and why) - Licensed scholarly metadata and citation graphs, which are the paid datasets behind the products in this category and cannot be reproduced from open sources. - Logged-in browser automation against publisher sites, which breaks terms you personally accepted and is how accounts and institutional access get revoked. - Any paywall circumvention, including proxies and shadow libraries, whatever the argument for it. - Team libraries and shared annotation, which need identity, permissions and a server you then operate. - A chat interface over the whole corpus, which makes unanchored answers easy to produce and hard to check, against the rule above. ### README must contain - That every generated sentence carries verified offsets into a stored document, and unanchored sentences are dropped rather than shown - That bibliographic fields come from a service or from you, each recorded with its source, and never from a model - That citation export refuses a reference with a missing required field instead of filling it in - The courtesy convention used with the metadata service, including the contact address sent and the backoff on rate limiting - The measured extraction and search times on a corpus of at least two thousand documents ===== AGENTS.md ===== # Agent instructions · Genei indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript on Node 22 with SQLite, run locally, Kept byte for byte on disk, hashed, and never the thing that gets rewritten, Layout-aware text with character offsets preserved per page, Fetched from a bibliographic service and stored per field with its source, Extractive first, generated second, and always span-anchored. 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 - Naive PDF extraction reads a two-column paper across the columns, so sentences interleave and every quote you later pull is half from one column and half from the other, which looks like the model hallucinating when it is actually your importer. Extract with layout awareness, check a two-column paper by eye during this phase, and store the page offsets so any quote can be checked against the printed page. - Filling in missing authors or a year by asking a language model produces a citation that looks right, passes review, and is wrong, and the person who finds out is a reviewer rather than you. Leave the field empty, mark it as needing attention, and make the citation export refuse to emit a reference with an unsourced required field. - Returning a list of matching documents rather than matching passages means every search ends with you opening a ninety page PDF and using the reader's own find function, which is the workflow the tool was supposed to replace. Return ranked passages with their offsets, and make clicking one open the document scrolled to the highlight. - Asking a model for a summary and then trying to attach citations afterwards by searching for similar text produces confident-looking anchors on sentences that were invented, because near-enough matching always finds something. Require the spans as part of the generation contract, verify that the quoted characters actually appear at those offsets, and discard any sentence that fails rather than repairing it. ===== BUILD_PLAN.md ===== # Build plan · Genei A personal research library whose hard problems are pulling readable text out of documents designed for print, getting bibliographic metadata from a source rather than a guess, and making every generated sentence traceable to a passage. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Keeping the document and knowing where you are in it Text comes out with offsets that map back to a page and a highlight. ### Steps 1. Extract with layout awareness and check a two-column paper The failure that gets blamed on the model later. Files: `src/extract/pdf.ts`, `tests/two-column.test.ts` 2. Record page ranges as character offsets An offset that maps to a page number is a checkable quote. Files: `src/extract/pages.ts`, `src/db/schema.sql` 3. Keep the original untouched and hashed It is the evidence behind everything the app later says. Files: `src/store/originals.ts` ### Done when - [ ] A two-column paper extracts in reading order - [ ] An offset resolves to a page and highlights correctly - [ ] The stored original is byte-identical to the import - [ ] Extractor and version are recorded per document ### Watch out - Naive PDF extraction reads a two-column paper across the columns, so sentences interleave and every quote you later pull is half from one column and half from the other, which looks like the model hallucinating when it is actually your importer. Extract with layout awareness, check a two-column paper by eye during this phase, and store the page offsets so any quote can be checked against the printed page. ## Phase 2 · Finding out what the thing actually is Bibliographic fields come from a service or from you, and each one says which. ### Steps 1. Look up by identifier and record the source per field Provenance per field is what makes a citation defensible. Files: `src/metadata/crossref.ts`, `src/db/schema.sql` 2. Send a contact address and honour 429 with backoff The documented courtesy, and it gets you the faster pool. Files: `src/metadata/client.ts`, `tests/backoff.test.ts` 3. Forbid model output in bibliographic fields A test over the write paths, not a promise in a comment. Files: `tests/no-generated-metadata.test.ts` ### Done when - [ ] Every bibliographic field records its source - [ ] No model output can reach a bibliographic field - [ ] A 429 backs off within a bounded delay - [ ] A document with no identifier still imports ### Watch out - Filling in missing authors or a year by asking a language model produces a citation that looks right, passes review, and is wrong, and the person who finds out is a reviewer rather than you. Leave the field empty, mark it as needing attention, and make the citation export refuse to emit a reference with an unsourced required field. ## Phase 3 · Finding the passage you remember reading Search covers the whole corpus, returns the passage in context, and filters on metadata. ### Steps 1. Return passages with offsets, not document hits The passage is the thing being searched for. Files: `src/search/passages.ts` 2. Combine metadata filters with the text query Author and year are how researchers actually narrow. Files: `src/search/filters.ts` 3. Assert search makes no network request A local corpus should stay local. Files: `tests/offline-search.test.ts` ### Done when - [ ] Results are passages with document, page and offsets - [ ] Metadata filters narrow the text results - [ ] Search issues no outbound request - [ ] Search time on two thousand documents is measured and stated ### Watch out - Returning a list of matching documents rather than matching passages means every search ends with you opening a ninety page PDF and using the reader's own find function, which is the workflow the tool was supposed to replace. Return ranked passages with their offsets, and make clicking one open the document scrolled to the highlight. ## Phase 4 · Summaries that point at the page Every sentence shown is either a quotation or a generated sentence with a span behind it. ### Steps 1. Ship extractive summarising before anything generated It works offline, costs nothing, and sets the anchoring contract. Files: `src/summary/extractive.ts` 2. Verify every generated span against the stored text The characters are there or the sentence does not ship. Files: `src/summary/verify.ts`, `tests/span-verify.test.ts` 3. Show model and estimated cost before a paid call A corpus-wide summarisation is a real invoice. Files: `src/summary/estimate.ts` ### Done when - [ ] Every generated claim resolves to real text at its offsets - [ ] Unanchored claims are dropped and counted - [ ] Extractive summarising works offline with no model - [ ] Paid runs show a cost estimate and respect a ceiling ### Watch out - Asking a model for a summary and then trying to attach citations afterwards by searching for similar text produces confident-looking anchors on sentences that were invented, because near-enough matching always finds something. Require the spans as part of the generation contract, verify that the quoted characters actually appear at those offsets, and discard any sentence that fails rather than repairing it. ## Not in this build - Licensed scholarly metadata and citation graphs. - Logged-in automation against publisher sites. - Paywall circumvention of any kind. - Team libraries and shared annotation. ## After v1, if you want it - Optical character recognition for scans, offered per document from the failure list - A second metadata service as a fallback, recorded with its own source name per field - Cross-document claim search, once enough summaries exist for it to be worth ranking ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Original files, byte for byte as imported. LIBRARY_DIR=./data/library # Required. Documents, metadata, notes and claims with their offsets. DATABASE_PATH=./data/library.db # Required. Sent with metadata lookups as the service's documented courtesy convention. CROSSREF_MAILTO=you@example.com # Required. Bounded backoff on rate limiting. Never an unlimited retry loop. METADATA_MAX_RETRIES=4 # Required. Set to none for extractive summaries only, which is the default and works offline. SUMMARY_MODEL=none # Optional · secret. Only when a generated summary model is configured. SUMMARY_API_KEY=sk-...redacted # Optional. Model spend per month. Reaching it refuses further paid calls. MONTHLY_SPEND_CEILING=10.00
You are building a production product version of Genei. 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 ===== # Genei · product brief ## Problem The visible research summarization loop is buildable, but a credible replacement needs more than the first screen. Genei earns its keep through data, import reliability, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A library that keeps your documents intact, tells you where every bibliographic field came from, refuses to print a reference it cannot support, and shows you the exact passage behind every sentence of a summary. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node.js 22 - browser automation for user-authorized imports - optional OpenAI API key stored in .env ## Explicit non-goals for v1 - Licensed scholarly metadata and citation graphs. - Logged-in automation against publisher sites. - Paywall circumvention of any kind. - Team libraries and shared annotation. - licensed scholarly metadata - publisher-specific import reliability - citation graph scale - team libraries and institutional access ## Success criteria - A two-column paper is confirmed to extract in reading order - No model output is confirmed able to reach a bibliographic field - Every generated claim is confirmed to resolve to real text - A missing required field is confirmed to block a reference - A library export is confirmed to reimport with working offsets ===== BRIEF.md ===== # Build brief · Genei 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 research library that keeps the sources you fed it, gets their metadata right, and never produces a sentence it cannot point at to replace Genei. 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. ### Rule zero Two rules, and the first is the whole difference between a research tool and a liability. Nothing is asserted that cannot be pointed at. Every sentence in a generated summary carries a reference to a character range in a stored document, shown in the interface and clickable, and a sentence the model produces without one is dropped rather than displayed. The same rule applies twice as hard to citations: a reference list is assembled from metadata fields you fetched and stored, never from a model's memory, because a plausible citation to a paper that does not exist is the single most damaging output this category produces. Second, import only what you can legitimately reach. Files you already have, open access copies located through a service that publishes how to use it, and pages an anonymous visitor can read. No borrowed institutional session, no bypassing a paywall, and no logged-in browser automation against a publisher, because the terms you would be breaking are ones you personally agreed to. ### Stack (fixed, do not substitute) - Runtime: TypeScript on Node 22 with SQLite, run locally · a personal corpus with no reason to leave the machine - Originals: Kept byte for byte on disk, hashed, and never the thing that gets rewritten · the stored document is the evidence for every claim above it - Extraction: Layout-aware text with character offsets preserved per page · a quote without an offset cannot be checked - Metadata: Fetched from a bibliographic service and stored per field with its source · the difference between a citation and a guess is provenance - Summaries: Extractive first, generated second, and always span-anchored · an unanchored sentence is dropped before it reaches the screen ### Data model (create this before Phase 1) - documents(id, path, sha256, kind, imported_at, title, page_count, text, extractor, extractor_version) - pages(document_id, page_number, start_offset, end_offset) · so a character offset resolves to a page number a reader can turn to - works(document_id, doi, isbn, title, container, year, publisher, url, open_access_url) · the bibliographic identity, once found - work_fields(document_id, field, value, source, fetched_at) · every field records where it came from, including manual - notes(id, document_id, start_offset, end_offset, body, created_at) · your own writing, anchored like everything else - claims(id, document_id, summary_id, sentence, start_offset, end_offset, method) · method is extracted or generated, and a generated claim without offsets is never stored - A summary is a list of claims, each pointing at a character range in documents.text, which the interface renders as a highlight on the stored document. ### Phase 1 · Keeping the document and knowing where you are in it Build: Import PDFs, EPUBs, HTML and plain text. Store the original unchanged and hashed. Extract text preserving reading order, and record the character range each page occupies. Handle two-column layouts, running headers and ligatures, and record the extractor and its version on the document. Trap: Naive PDF extraction reads a two-column paper across the columns, so sentences interleave and every quote you later pull is half from one column and half from the other, which looks like the model hallucinating when it is actually your importer. Extract with layout awareness, check a two-column paper by eye during this phase, and store the page offsets so any quote can be checked against the printed page. Done when: A two-column paper extracts in reading order, verified against the printed pages. Any character offset resolves to a page number and the interface can highlight that range on the stored document. The original file is byte-identical to what was imported, checked by hash. Every document records its extractor and version so a later improvement can requeue exactly the affected documents. Do not build yet: No metadata lookup, search, summarising or citation export. ### Phase 2 · Finding out what the thing actually is Build: Extract any DOI from the document, then look the work up through a bibliographic API, using its documented courtesy convention: Crossref's REST API offers a faster pool to callers who supply a contact address, as a mailto query parameter or in the User-Agent, and asks you to back off on a 429 (checked against their current guidance). Store every field with its source and fetch time. Where nothing is found, leave fields empty and let the user type them, marked as manual. Trap: Filling in missing authors or a year by asking a language model produces a citation that looks right, passes review, and is wrong, and the person who finds out is a reviewer rather than you. Leave the field empty, mark it as needing attention, and make the citation export refuse to emit a reference with an unsourced required field. Done when: Every stored bibliographic field has a source of either the service name or manual, enforced by a not-null constraint. No code path writes a bibliographic field from a model's output, verified by review and asserted by a test over the write paths. A 429 from the metadata service backs off and retries with a bounded delay rather than failing the import. A document with no identifier imports successfully with empty fields marked as needing attention. Do not build yet: No search, summarising or export yet. ### Phase 3 · Finding the passage you remember reading Build: Full-text search over extracted text with results shown as passages around the match, each carrying its document, page and offsets. Filter by author, year, container and tag. Searching returns the passage, not just the document, because the passage is what you were looking for. Trap: Returning a list of matching documents rather than matching passages means every search ends with you opening a ninety page PDF and using the reader's own find function, which is the workflow the tool was supposed to replace. Return ranked passages with their offsets, and make clicking one open the document scrolled to the highlight. Done when: A search returns passages with document, page and offsets, and clicking one opens the highlighted range. Metadata filters combine with the text query and both narrow the same result set. Search runs entirely locally with no outbound request, asserted with the network disabled. Search over a corpus of at least two thousand documents returns within a time you have measured and stated. Do not build yet: No summarising, citation export or cost controls yet. ### Phase 4 · Summaries that point at the page Build: Produce an extractive summary first: the highest-ranked sentences with their offsets, which needs no model at all. Then, optionally, a generated summary in which each sentence must be emitted with the offsets it was derived from; verify each one resolves to real text before storing, and drop the ones that do not. Show the model name and the estimated cost before any paid call. Trap: Asking a model for a summary and then trying to attach citations afterwards by searching for similar text produces confident-looking anchors on sentences that were invented, because near-enough matching always finds something. Require the spans as part of the generation contract, verify that the quoted characters actually appear at those offsets, and discard any sentence that fails rather than repairing it. Done when: Every stored generated claim has offsets whose text exists at that range in the document, asserted on write. A claim returned without offsets or with offsets that do not resolve is dropped and the drop is counted and shown. The extractive summary works with no model configured and no network, tested. A paid summarisation shows the model name and an estimated cost before running and respects a configured ceiling. Do not build yet: No citation export, backups or cost history yet. ### Phase 5 · Getting citations out, and getting everything out Build: Export references as BibTeX and CSL-JSON built only from stored fields, refusing any reference missing a required field and naming what is missing. Export the whole library as originals plus a manifest of metadata, notes and claims with their offsets. Cap monthly model spend and record every call. Back up the database and exercise the restore in the suite. Trap: An export that silently omits a missing year or invents a container title produces a bibliography that compiles and is wrong in a way nobody notices until a copy editor does. Refuse the reference, list which fields are missing and for which document, and make fixing them a visible task in the interface rather than an error in a log. Done when: An export containing a work with a missing required field fails with that document and field named, and no placeholder is emitted. Exported BibTeX and CSL-JSON both import into a mainstream reference manager, checked by doing it. The library export contains the original files, and importing it into an empty install restores documents, notes and claims with working offsets. Model spend is capped monthly and every call is recorded with its cost. Do not build yet: Nothing after this. ### Out of scope (and why) - Licensed scholarly metadata and citation graphs, which are the paid datasets behind the products in this category and cannot be reproduced from open sources. - Logged-in browser automation against publisher sites, which breaks terms you personally accepted and is how accounts and institutional access get revoked. - Any paywall circumvention, including proxies and shadow libraries, whatever the argument for it. - Team libraries and shared annotation, which need identity, permissions and a server you then operate. - A chat interface over the whole corpus, which makes unanchored answers easy to produce and hard to check, against the rule above. ### README must contain - That every generated sentence carries verified offsets into a stored document, and unanchored sentences are dropped rather than shown - That bibliographic fields come from a service or from you, each recorded with its source, and never from a model - That citation export refuses a reference with a missing required field instead of filling it in - The courtesy convention used with the metadata service, including the contact address sent and the backoff on rate limiting - The measured extraction and search times on a corpus of at least two thousand documents ===== ARCHITECTURE.md ===== # Architecture · Genei ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript on Node 22 with SQLite, run locally | a personal corpus with no reason to leave the machine | | Originals | Kept byte for byte on disk, hashed, and never the thing that gets rewritten | the stored document is the evidence for every claim above it | | Extraction | Layout-aware text with character offsets preserved per page | a quote without an offset cannot be checked | | Metadata | Fetched from a bibliographic service and stored per field with its source | the difference between a citation and a guess is provenance | | Summaries | Extractive first, generated second, and always span-anchored | an unanchored sentence is dropped before it reaches the screen | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Import | Originals, layout-aware extraction and page offsets. | Any extractor, provided offsets still resolve to pages. | | Metadata | Lookup, provenance per field and backoff. | Any bibliographic service, provided the source is recorded per field. | | Search | Local passage search and metadata filters. | Any index, provided results stay passages with offsets. | | Summary | Extractive first, verified spans always. | Any model, provided spans are part of the contract and verified on write. | | Export | BibTeX, CSL-JSON and the full library manifest. | Any format, provided a missing required field still refuses. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `LIBRARY_DIR` · required · Original files, byte for byte as imported. - `DATABASE_PATH` · required · Documents, metadata, notes and claims with their offsets. - `CROSSREF_MAILTO` · required · Sent with metadata lookups as the service's documented courtesy convention. - `METADATA_MAX_RETRIES` · required · Bounded backoff on rate limiting. Never an unlimited retry loop. - `SUMMARY_MODEL` · required · Set to none for extractive summaries only, which is the default and works offline. - `SUMMARY_API_KEY` · optional, secret · Only when a generated summary model is configured. - `MONTHLY_SPEND_CEILING` · optional · Model spend per month. Reaching it refuses further paid calls. ## 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 · Genei product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript on Node 22 with SQLite, run locally, Kept byte for byte on disk, hashed, and never the thing that gets rewritten, Layout-aware text with character offsets preserved per page, Fetched from a bibliographic service and stored per field with its source, Extractive first, generated second, and always span-anchored. - 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 - Naive PDF extraction reads a two-column paper across the columns, so sentences interleave and every quote you later pull is half from one column and half from the other, which looks like the model hallucinating when it is actually your importer. Extract with layout awareness, check a two-column paper by eye during this phase, and store the page offsets so any quote can be checked against the printed page. - Filling in missing authors or a year by asking a language model produces a citation that looks right, passes review, and is wrong, and the person who finds out is a reviewer rather than you. Leave the field empty, mark it as needing attention, and make the citation export refuse to emit a reference with an unsourced required field. - Returning a list of matching documents rather than matching passages means every search ends with you opening a ninety page PDF and using the reader's own find function, which is the workflow the tool was supposed to replace. Return ranked passages with their offsets, and make clicking one open the document scrolled to the highlight. - Asking a model for a summary and then trying to attach citations afterwards by searching for similar text produces confident-looking anchors on sentences that were invented, because near-enough matching always finds something. Require the spans as part of the generation contract, verify that the quoted characters actually appear at those offsets, and discard any sentence that fails rather than repairing it. - An export that silently omits a missing year or invents a container title produces a bibliography that compiles and is wrong in a way nobody notices until a copy editor does. Refuse the reference, list which fields are missing and for which document, and make fixing them a visible task in the interface rather than an error in a log. ===== MILESTONES.md ===== # Delivery milestones · Genei Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Keeping the document and knowing where you are in it Text comes out with offsets that map back to a page and a highlight. ### Steps 1. Extract with layout awareness and check a two-column paper The failure that gets blamed on the model later. Files: `src/extract/pdf.ts`, `tests/two-column.test.ts` 2. Record page ranges as character offsets An offset that maps to a page number is a checkable quote. Files: `src/extract/pages.ts`, `src/db/schema.sql` 3. Keep the original untouched and hashed It is the evidence behind everything the app later says. Files: `src/store/originals.ts` ### Done when - [ ] A two-column paper extracts in reading order - [ ] An offset resolves to a page and highlights correctly - [ ] The stored original is byte-identical to the import - [ ] Extractor and version are recorded per document ### Watch out - Naive PDF extraction reads a two-column paper across the columns, so sentences interleave and every quote you later pull is half from one column and half from the other, which looks like the model hallucinating when it is actually your importer. Extract with layout awareness, check a two-column paper by eye during this phase, and store the page offsets so any quote can be checked against the printed page. ## M2 · Finding out what the thing actually is Bibliographic fields come from a service or from you, and each one says which. ### Steps 1. Look up by identifier and record the source per field Provenance per field is what makes a citation defensible. Files: `src/metadata/crossref.ts`, `src/db/schema.sql` 2. Send a contact address and honour 429 with backoff The documented courtesy, and it gets you the faster pool. Files: `src/metadata/client.ts`, `tests/backoff.test.ts` 3. Forbid model output in bibliographic fields A test over the write paths, not a promise in a comment. Files: `tests/no-generated-metadata.test.ts` ### Done when - [ ] Every bibliographic field records its source - [ ] No model output can reach a bibliographic field - [ ] A 429 backs off within a bounded delay - [ ] A document with no identifier still imports ### Watch out - Filling in missing authors or a year by asking a language model produces a citation that looks right, passes review, and is wrong, and the person who finds out is a reviewer rather than you. Leave the field empty, mark it as needing attention, and make the citation export refuse to emit a reference with an unsourced required field. ## M3 · Finding the passage you remember reading Search covers the whole corpus, returns the passage in context, and filters on metadata. ### Steps 1. Return passages with offsets, not document hits The passage is the thing being searched for. Files: `src/search/passages.ts` 2. Combine metadata filters with the text query Author and year are how researchers actually narrow. Files: `src/search/filters.ts` 3. Assert search makes no network request A local corpus should stay local. Files: `tests/offline-search.test.ts` ### Done when - [ ] Results are passages with document, page and offsets - [ ] Metadata filters narrow the text results - [ ] Search issues no outbound request - [ ] Search time on two thousand documents is measured and stated ### Watch out - Returning a list of matching documents rather than matching passages means every search ends with you opening a ninety page PDF and using the reader's own find function, which is the workflow the tool was supposed to replace. Return ranked passages with their offsets, and make clicking one open the document scrolled to the highlight. ## M4 · Summaries that point at the page Every sentence shown is either a quotation or a generated sentence with a span behind it. ### Steps 1. Ship extractive summarising before anything generated It works offline, costs nothing, and sets the anchoring contract. Files: `src/summary/extractive.ts` 2. Verify every generated span against the stored text The characters are there or the sentence does not ship. Files: `src/summary/verify.ts`, `tests/span-verify.test.ts` 3. Show model and estimated cost before a paid call A corpus-wide summarisation is a real invoice. Files: `src/summary/estimate.ts` ### Done when - [ ] Every generated claim resolves to real text at its offsets - [ ] Unanchored claims are dropped and counted - [ ] Extractive summarising works offline with no model - [ ] Paid runs show a cost estimate and respect a ceiling ### Watch out - Asking a model for a summary and then trying to attach citations afterwards by searching for similar text produces confident-looking anchors on sentences that were invented, because near-enough matching always finds something. Require the spans as part of the generation contract, verify that the quoted characters actually appear at those offsets, and discard any sentence that fails rather than repairing it. ## M5 · Getting citations out, and getting everything out (production only) References export in formats a reference manager reads, and the library is portable. ### Steps 1. Build references only from stored fields Refuse rather than fill. This is the rule with teeth. Files: `src/export/bibtex.ts`, `src/export/csl.ts`, `tests/missing-field.test.ts` 2. Export originals plus a manifest and reimport it An export that does not reimport is a filename, not an exit. Files: `src/export/library.ts`, `tests/roundtrip.test.ts` 3. Record every model call with its cost and cap the month The cost page is the only one that gets read later. Files: `src/cost/ledger.ts` ### Done when - [ ] A missing required field blocks the reference and is named - [ ] BibTeX and CSL-JSON import into a reference manager - [ ] A library export reimports with working offsets - [ ] Monthly spend is capped and every call is recorded ### Watch out - An export that silently omits a missing year or invents a container title produces a bibliography that compiles and is wrong in a way nobody notices until a copy editor does. Refuse the reference, list which fields are missing and for which document, and make fixing them a visible task in the interface rather than an error in a log. ===== OPERATIONS.md ===== # Operations · Genei ## Backup The originals directory and the database together: the originals are irreplaceable if they were your only copy, and the database holds notes and claims that cannot be regenerated. Back up both, and verify a restore reproduces working offsets rather than assuming it will. ## Restore Restore both directories, then verify a sample of claims still resolve to the text at their offsets. If extraction has changed version since the backup, requeue extraction by version rather than reimporting, so notes keep their anchors. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Extraction failures, documents with fields still needing attention, dropped unanchored claims per summary, and model spend against the monthly ceiling. A rising drop count means the summarisation contract is being violated and the output should not be trusted. ## Incident checklist A wrong citation means a field was entered manually or a service returned something unexpected, and work_fields records which and when. A quote that does not match the page means extraction reordered the text, which the two-column test in Phase 1 exists to catch. 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 two-column paper is confirmed to extract in reading order - [ ] No model output is confirmed able to reach a bibliographic field - [ ] Every generated claim is confirmed to resolve to real text - [ ] A missing required field is confirmed to block a reference - [ ] A library export is confirmed to reimport with working offsets ## Launch constraint Do not market omitted Genei 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. Original files, byte for byte as imported. LIBRARY_DIR=./data/library # Required. Documents, metadata, notes and claims with their offsets. DATABASE_PATH=./data/library.db # Required. Sent with metadata lookups as the service's documented courtesy convention. CROSSREF_MAILTO=you@example.com # Required. Bounded backoff on rate limiting. Never an unlimited retry loop. METADATA_MAX_RETRIES=4 # Required. Set to none for extractive summaries only, which is the default and works offline. SUMMARY_MODEL=none # Optional · secret. Only when a generated summary model is configured. SUMMARY_API_KEY=sk-...redacted # Optional. Model spend per month. Reaching it refuses further paid calls. MONTHLY_SPEND_CEILING=10.00
# Genei · indie build A personal research library whose hard problems are pulling readable text out of documents designed for print, getting bibliographic metadata from a source rather than a guess, and making every generated sentence traceable to a passage. Estimated effort: **weekend**. 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 | | --- | --- | --- | | Runtime | TypeScript on Node 22 with SQLite, run locally | a personal corpus with no reason to leave the machine | | Originals | Kept byte for byte on disk, hashed, and never the thing that gets rewritten | the stored document is the evidence for every claim above it | | Extraction | Layout-aware text with character offsets preserved per page | a quote without an offset cannot be checked | | Metadata | Fetched from a bibliographic service and stored per field with its source | the difference between a citation and a guess is provenance | | Summaries | Extractive first, generated second, and always span-anchored | an unanchored sentence is dropped before it reaches the screen | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the importer, the index and the interface locally. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **Twenty documents you already have, including awkward ones** · free - Why: Every extraction bug in this category is a layout bug, and a folder of clean single-column articles will pass tests that a scanned appendix or a two-column paper fails. - Get it: Your own PDFs and EPUBs, including at least one two-column paper, one scan and one long book. - Verify: All twenty import and the two-column one extracts in reading order - [ ] **A contact address for the metadata service** · free - Why: The bibliographic API asks callers to identify themselves and offers better service to those who do, and it costs nothing to comply. - Get it: An address you monitor, sent as a mailto parameter or in the User-Agent as the service documents. - Verify: A lookup succeeds and your address appears in the request - [ ] **A reference manager to import into** · free - Why: The citation export is only credible if a real reference manager reads it, and format bugs are invisible until you try. - Get it: Any mainstream open-source reference manager. - Verify: Exported BibTeX and CSL-JSON both import with fields intact - [ ] **A model key, if you want generated summaries** (optional) · pennies per document, capped by your configured ceiling - Why: The generated summary path is optional by design, and the extractive path has to work without it, so this is genuinely not required to finish the build. - Get it: Any provider whose API returns structured output, so spans can be part of the contract. - Verify: A summarisation returns sentences with offsets that resolve ## 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: - Licensed scholarly metadata and citation graphs. - Logged-in automation against publisher sites. - Paywall circumvention of any kind. - Team libraries and shared annotation. - licensed scholarly metadata - publisher-specific import reliability - citation graph scale - team libraries and institutional access If one of those is essential to you, that is the reason to keep paying for Genei, and the README should say so rather than pretend.
# Build brief · Genei 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 research library that keeps the sources you fed it, gets their metadata right, and never produces a sentence it cannot point at to replace Genei. 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. ### Rule zero Two rules, and the first is the whole difference between a research tool and a liability. Nothing is asserted that cannot be pointed at. Every sentence in a generated summary carries a reference to a character range in a stored document, shown in the interface and clickable, and a sentence the model produces without one is dropped rather than displayed. The same rule applies twice as hard to citations: a reference list is assembled from metadata fields you fetched and stored, never from a model's memory, because a plausible citation to a paper that does not exist is the single most damaging output this category produces. Second, import only what you can legitimately reach. Files you already have, open access copies located through a service that publishes how to use it, and pages an anonymous visitor can read. No borrowed institutional session, no bypassing a paywall, and no logged-in browser automation against a publisher, because the terms you would be breaking are ones you personally agreed to. ### Stack (fixed, do not substitute) - Runtime: TypeScript on Node 22 with SQLite, run locally · a personal corpus with no reason to leave the machine - Originals: Kept byte for byte on disk, hashed, and never the thing that gets rewritten · the stored document is the evidence for every claim above it - Extraction: Layout-aware text with character offsets preserved per page · a quote without an offset cannot be checked - Metadata: Fetched from a bibliographic service and stored per field with its source · the difference between a citation and a guess is provenance - Summaries: Extractive first, generated second, and always span-anchored · an unanchored sentence is dropped before it reaches the screen ### Data model (create this before Phase 1) - documents(id, path, sha256, kind, imported_at, title, page_count, text, extractor, extractor_version) - pages(document_id, page_number, start_offset, end_offset) · so a character offset resolves to a page number a reader can turn to - works(document_id, doi, isbn, title, container, year, publisher, url, open_access_url) · the bibliographic identity, once found - work_fields(document_id, field, value, source, fetched_at) · every field records where it came from, including manual - notes(id, document_id, start_offset, end_offset, body, created_at) · your own writing, anchored like everything else - claims(id, document_id, summary_id, sentence, start_offset, end_offset, method) · method is extracted or generated, and a generated claim without offsets is never stored - A summary is a list of claims, each pointing at a character range in documents.text, which the interface renders as a highlight on the stored document. ### Phase 1 · Keeping the document and knowing where you are in it Build: Import PDFs, EPUBs, HTML and plain text. Store the original unchanged and hashed. Extract text preserving reading order, and record the character range each page occupies. Handle two-column layouts, running headers and ligatures, and record the extractor and its version on the document. Trap: Naive PDF extraction reads a two-column paper across the columns, so sentences interleave and every quote you later pull is half from one column and half from the other, which looks like the model hallucinating when it is actually your importer. Extract with layout awareness, check a two-column paper by eye during this phase, and store the page offsets so any quote can be checked against the printed page. Done when: A two-column paper extracts in reading order, verified against the printed pages. Any character offset resolves to a page number and the interface can highlight that range on the stored document. The original file is byte-identical to what was imported, checked by hash. Every document records its extractor and version so a later improvement can requeue exactly the affected documents. Do not build yet: No metadata lookup, search, summarising or citation export. ### Phase 2 · Finding out what the thing actually is Build: Extract any DOI from the document, then look the work up through a bibliographic API, using its documented courtesy convention: Crossref's REST API offers a faster pool to callers who supply a contact address, as a mailto query parameter or in the User-Agent, and asks you to back off on a 429 (checked against their current guidance). Store every field with its source and fetch time. Where nothing is found, leave fields empty and let the user type them, marked as manual. Trap: Filling in missing authors or a year by asking a language model produces a citation that looks right, passes review, and is wrong, and the person who finds out is a reviewer rather than you. Leave the field empty, mark it as needing attention, and make the citation export refuse to emit a reference with an unsourced required field. Done when: Every stored bibliographic field has a source of either the service name or manual, enforced by a not-null constraint. No code path writes a bibliographic field from a model's output, verified by review and asserted by a test over the write paths. A 429 from the metadata service backs off and retries with a bounded delay rather than failing the import. A document with no identifier imports successfully with empty fields marked as needing attention. Do not build yet: No search, summarising or export yet. ### Phase 3 · Finding the passage you remember reading Build: Full-text search over extracted text with results shown as passages around the match, each carrying its document, page and offsets. Filter by author, year, container and tag. Searching returns the passage, not just the document, because the passage is what you were looking for. Trap: Returning a list of matching documents rather than matching passages means every search ends with you opening a ninety page PDF and using the reader's own find function, which is the workflow the tool was supposed to replace. Return ranked passages with their offsets, and make clicking one open the document scrolled to the highlight. Done when: A search returns passages with document, page and offsets, and clicking one opens the highlighted range. Metadata filters combine with the text query and both narrow the same result set. Search runs entirely locally with no outbound request, asserted with the network disabled. Search over a corpus of at least two thousand documents returns within a time you have measured and stated. Do not build yet: No summarising, citation export or cost controls yet. ### Phase 4 · Summaries that point at the page Build: Produce an extractive summary first: the highest-ranked sentences with their offsets, which needs no model at all. Then, optionally, a generated summary in which each sentence must be emitted with the offsets it was derived from; verify each one resolves to real text before storing, and drop the ones that do not. Show the model name and the estimated cost before any paid call. Trap: Asking a model for a summary and then trying to attach citations afterwards by searching for similar text produces confident-looking anchors on sentences that were invented, because near-enough matching always finds something. Require the spans as part of the generation contract, verify that the quoted characters actually appear at those offsets, and discard any sentence that fails rather than repairing it. Done when: Every stored generated claim has offsets whose text exists at that range in the document, asserted on write. A claim returned without offsets or with offsets that do not resolve is dropped and the drop is counted and shown. The extractive summary works with no model configured and no network, tested. A paid summarisation shows the model name and an estimated cost before running and respects a configured ceiling. Do not build yet: No citation export, backups or cost history yet. ### Phase 5 · Getting citations out, and getting everything out Build: Export references as BibTeX and CSL-JSON built only from stored fields, refusing any reference missing a required field and naming what is missing. Export the whole library as originals plus a manifest of metadata, notes and claims with their offsets. Cap monthly model spend and record every call. Back up the database and exercise the restore in the suite. Trap: An export that silently omits a missing year or invents a container title produces a bibliography that compiles and is wrong in a way nobody notices until a copy editor does. Refuse the reference, list which fields are missing and for which document, and make fixing them a visible task in the interface rather than an error in a log. Done when: An export containing a work with a missing required field fails with that document and field named, and no placeholder is emitted. Exported BibTeX and CSL-JSON both import into a mainstream reference manager, checked by doing it. The library export contains the original files, and importing it into an empty install restores documents, notes and claims with working offsets. Model spend is capped monthly and every call is recorded with its cost. Do not build yet: Nothing after this. ### Out of scope (and why) - Licensed scholarly metadata and citation graphs, which are the paid datasets behind the products in this category and cannot be reproduced from open sources. - Logged-in browser automation against publisher sites, which breaks terms you personally accepted and is how accounts and institutional access get revoked. - Any paywall circumvention, including proxies and shadow libraries, whatever the argument for it. - Team libraries and shared annotation, which need identity, permissions and a server you then operate. - A chat interface over the whole corpus, which makes unanchored answers easy to produce and hard to check, against the rule above. ### README must contain - That every generated sentence carries verified offsets into a stored document, and unanchored sentences are dropped rather than shown - That bibliographic fields come from a service or from you, each recorded with its source, and never from a model - That citation export refuses a reference with a missing required field instead of filling it in - The courtesy convention used with the metadata service, including the contact address sent and the backoff on rate limiting - The measured extraction and search times on a corpus of at least two thousand documents
# Agent instructions · Genei indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript on Node 22 with SQLite, run locally, Kept byte for byte on disk, hashed, and never the thing that gets rewritten, Layout-aware text with character offsets preserved per page, Fetched from a bibliographic service and stored per field with its source, Extractive first, generated second, and always span-anchored. 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 - Naive PDF extraction reads a two-column paper across the columns, so sentences interleave and every quote you later pull is half from one column and half from the other, which looks like the model hallucinating when it is actually your importer. Extract with layout awareness, check a two-column paper by eye during this phase, and store the page offsets so any quote can be checked against the printed page. - Filling in missing authors or a year by asking a language model produces a citation that looks right, passes review, and is wrong, and the person who finds out is a reviewer rather than you. Leave the field empty, mark it as needing attention, and make the citation export refuse to emit a reference with an unsourced required field. - Returning a list of matching documents rather than matching passages means every search ends with you opening a ninety page PDF and using the reader's own find function, which is the workflow the tool was supposed to replace. Return ranked passages with their offsets, and make clicking one open the document scrolled to the highlight. - Asking a model for a summary and then trying to attach citations afterwards by searching for similar text produces confident-looking anchors on sentences that were invented, because near-enough matching always finds something. Require the spans as part of the generation contract, verify that the quoted characters actually appear at those offsets, and discard any sentence that fails rather than repairing it.
# Build plan · Genei A personal research library whose hard problems are pulling readable text out of documents designed for print, getting bibliographic metadata from a source rather than a guess, and making every generated sentence traceable to a passage. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Keeping the document and knowing where you are in it Text comes out with offsets that map back to a page and a highlight. ### Steps 1. Extract with layout awareness and check a two-column paper The failure that gets blamed on the model later. Files: `src/extract/pdf.ts`, `tests/two-column.test.ts` 2. Record page ranges as character offsets An offset that maps to a page number is a checkable quote. Files: `src/extract/pages.ts`, `src/db/schema.sql` 3. Keep the original untouched and hashed It is the evidence behind everything the app later says. Files: `src/store/originals.ts` ### Done when - [ ] A two-column paper extracts in reading order - [ ] An offset resolves to a page and highlights correctly - [ ] The stored original is byte-identical to the import - [ ] Extractor and version are recorded per document ### Watch out - Naive PDF extraction reads a two-column paper across the columns, so sentences interleave and every quote you later pull is half from one column and half from the other, which looks like the model hallucinating when it is actually your importer. Extract with layout awareness, check a two-column paper by eye during this phase, and store the page offsets so any quote can be checked against the printed page. ## Phase 2 · Finding out what the thing actually is Bibliographic fields come from a service or from you, and each one says which. ### Steps 1. Look up by identifier and record the source per field Provenance per field is what makes a citation defensible. Files: `src/metadata/crossref.ts`, `src/db/schema.sql` 2. Send a contact address and honour 429 with backoff The documented courtesy, and it gets you the faster pool. Files: `src/metadata/client.ts`, `tests/backoff.test.ts` 3. Forbid model output in bibliographic fields A test over the write paths, not a promise in a comment. Files: `tests/no-generated-metadata.test.ts` ### Done when - [ ] Every bibliographic field records its source - [ ] No model output can reach a bibliographic field - [ ] A 429 backs off within a bounded delay - [ ] A document with no identifier still imports ### Watch out - Filling in missing authors or a year by asking a language model produces a citation that looks right, passes review, and is wrong, and the person who finds out is a reviewer rather than you. Leave the field empty, mark it as needing attention, and make the citation export refuse to emit a reference with an unsourced required field. ## Phase 3 · Finding the passage you remember reading Search covers the whole corpus, returns the passage in context, and filters on metadata. ### Steps 1. Return passages with offsets, not document hits The passage is the thing being searched for. Files: `src/search/passages.ts` 2. Combine metadata filters with the text query Author and year are how researchers actually narrow. Files: `src/search/filters.ts` 3. Assert search makes no network request A local corpus should stay local. Files: `tests/offline-search.test.ts` ### Done when - [ ] Results are passages with document, page and offsets - [ ] Metadata filters narrow the text results - [ ] Search issues no outbound request - [ ] Search time on two thousand documents is measured and stated ### Watch out - Returning a list of matching documents rather than matching passages means every search ends with you opening a ninety page PDF and using the reader's own find function, which is the workflow the tool was supposed to replace. Return ranked passages with their offsets, and make clicking one open the document scrolled to the highlight. ## Phase 4 · Summaries that point at the page Every sentence shown is either a quotation or a generated sentence with a span behind it. ### Steps 1. Ship extractive summarising before anything generated It works offline, costs nothing, and sets the anchoring contract. Files: `src/summary/extractive.ts` 2. Verify every generated span against the stored text The characters are there or the sentence does not ship. Files: `src/summary/verify.ts`, `tests/span-verify.test.ts` 3. Show model and estimated cost before a paid call A corpus-wide summarisation is a real invoice. Files: `src/summary/estimate.ts` ### Done when - [ ] Every generated claim resolves to real text at its offsets - [ ] Unanchored claims are dropped and counted - [ ] Extractive summarising works offline with no model - [ ] Paid runs show a cost estimate and respect a ceiling ### Watch out - Asking a model for a summary and then trying to attach citations afterwards by searching for similar text produces confident-looking anchors on sentences that were invented, because near-enough matching always finds something. Require the spans as part of the generation contract, verify that the quoted characters actually appear at those offsets, and discard any sentence that fails rather than repairing it. ## Not in this build - Licensed scholarly metadata and citation graphs. - Logged-in automation against publisher sites. - Paywall circumvention of any kind. - Team libraries and shared annotation. ## After v1, if you want it - Optical character recognition for scans, offered per document from the failure list - A second metadata service as a fallback, recorded with its own source name per field - Cross-document claim search, once enough summaries exist for it to be worth ranking
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Original files, byte for byte as imported. LIBRARY_DIR=./data/library # Required. Documents, metadata, notes and claims with their offsets. DATABASE_PATH=./data/library.db # Required. Sent with metadata lookups as the service's documented courtesy convention. CROSSREF_MAILTO=you@example.com # Required. Bounded backoff on rate limiting. Never an unlimited retry loop. METADATA_MAX_RETRIES=4 # Required. Set to none for extractive summaries only, which is the default and works offline. SUMMARY_MODEL=none # Optional · secret. Only when a generated summary model is configured. SUMMARY_API_KEY=sk-...redacted # Optional. Model spend per month. Reaching it refuses further paid calls. MONTHLY_SPEND_CEILING=10.00
# Genei · product brief ## Problem The visible research summarization loop is buildable, but a credible replacement needs more than the first screen. Genei earns its keep through data, import reliability, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A library that keeps your documents intact, tells you where every bibliographic field came from, refuses to print a reference it cannot support, and shows you the exact passage behind every sentence of a summary. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node.js 22 - browser automation for user-authorized imports - optional OpenAI API key stored in .env ## Explicit non-goals for v1 - Licensed scholarly metadata and citation graphs. - Logged-in automation against publisher sites. - Paywall circumvention of any kind. - Team libraries and shared annotation. - licensed scholarly metadata - publisher-specific import reliability - citation graph scale - team libraries and institutional access ## Success criteria - A two-column paper is confirmed to extract in reading order - No model output is confirmed able to reach a bibliographic field - Every generated claim is confirmed to resolve to real text - A missing required field is confirmed to block a reference - A library export is confirmed to reimport with working offsets
# Build brief · Genei 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 research library that keeps the sources you fed it, gets their metadata right, and never produces a sentence it cannot point at to replace Genei. 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. ### Rule zero Two rules, and the first is the whole difference between a research tool and a liability. Nothing is asserted that cannot be pointed at. Every sentence in a generated summary carries a reference to a character range in a stored document, shown in the interface and clickable, and a sentence the model produces without one is dropped rather than displayed. The same rule applies twice as hard to citations: a reference list is assembled from metadata fields you fetched and stored, never from a model's memory, because a plausible citation to a paper that does not exist is the single most damaging output this category produces. Second, import only what you can legitimately reach. Files you already have, open access copies located through a service that publishes how to use it, and pages an anonymous visitor can read. No borrowed institutional session, no bypassing a paywall, and no logged-in browser automation against a publisher, because the terms you would be breaking are ones you personally agreed to. ### Stack (fixed, do not substitute) - Runtime: TypeScript on Node 22 with SQLite, run locally · a personal corpus with no reason to leave the machine - Originals: Kept byte for byte on disk, hashed, and never the thing that gets rewritten · the stored document is the evidence for every claim above it - Extraction: Layout-aware text with character offsets preserved per page · a quote without an offset cannot be checked - Metadata: Fetched from a bibliographic service and stored per field with its source · the difference between a citation and a guess is provenance - Summaries: Extractive first, generated second, and always span-anchored · an unanchored sentence is dropped before it reaches the screen ### Data model (create this before Phase 1) - documents(id, path, sha256, kind, imported_at, title, page_count, text, extractor, extractor_version) - pages(document_id, page_number, start_offset, end_offset) · so a character offset resolves to a page number a reader can turn to - works(document_id, doi, isbn, title, container, year, publisher, url, open_access_url) · the bibliographic identity, once found - work_fields(document_id, field, value, source, fetched_at) · every field records where it came from, including manual - notes(id, document_id, start_offset, end_offset, body, created_at) · your own writing, anchored like everything else - claims(id, document_id, summary_id, sentence, start_offset, end_offset, method) · method is extracted or generated, and a generated claim without offsets is never stored - A summary is a list of claims, each pointing at a character range in documents.text, which the interface renders as a highlight on the stored document. ### Phase 1 · Keeping the document and knowing where you are in it Build: Import PDFs, EPUBs, HTML and plain text. Store the original unchanged and hashed. Extract text preserving reading order, and record the character range each page occupies. Handle two-column layouts, running headers and ligatures, and record the extractor and its version on the document. Trap: Naive PDF extraction reads a two-column paper across the columns, so sentences interleave and every quote you later pull is half from one column and half from the other, which looks like the model hallucinating when it is actually your importer. Extract with layout awareness, check a two-column paper by eye during this phase, and store the page offsets so any quote can be checked against the printed page. Done when: A two-column paper extracts in reading order, verified against the printed pages. Any character offset resolves to a page number and the interface can highlight that range on the stored document. The original file is byte-identical to what was imported, checked by hash. Every document records its extractor and version so a later improvement can requeue exactly the affected documents. Do not build yet: No metadata lookup, search, summarising or citation export. ### Phase 2 · Finding out what the thing actually is Build: Extract any DOI from the document, then look the work up through a bibliographic API, using its documented courtesy convention: Crossref's REST API offers a faster pool to callers who supply a contact address, as a mailto query parameter or in the User-Agent, and asks you to back off on a 429 (checked against their current guidance). Store every field with its source and fetch time. Where nothing is found, leave fields empty and let the user type them, marked as manual. Trap: Filling in missing authors or a year by asking a language model produces a citation that looks right, passes review, and is wrong, and the person who finds out is a reviewer rather than you. Leave the field empty, mark it as needing attention, and make the citation export refuse to emit a reference with an unsourced required field. Done when: Every stored bibliographic field has a source of either the service name or manual, enforced by a not-null constraint. No code path writes a bibliographic field from a model's output, verified by review and asserted by a test over the write paths. A 429 from the metadata service backs off and retries with a bounded delay rather than failing the import. A document with no identifier imports successfully with empty fields marked as needing attention. Do not build yet: No search, summarising or export yet. ### Phase 3 · Finding the passage you remember reading Build: Full-text search over extracted text with results shown as passages around the match, each carrying its document, page and offsets. Filter by author, year, container and tag. Searching returns the passage, not just the document, because the passage is what you were looking for. Trap: Returning a list of matching documents rather than matching passages means every search ends with you opening a ninety page PDF and using the reader's own find function, which is the workflow the tool was supposed to replace. Return ranked passages with their offsets, and make clicking one open the document scrolled to the highlight. Done when: A search returns passages with document, page and offsets, and clicking one opens the highlighted range. Metadata filters combine with the text query and both narrow the same result set. Search runs entirely locally with no outbound request, asserted with the network disabled. Search over a corpus of at least two thousand documents returns within a time you have measured and stated. Do not build yet: No summarising, citation export or cost controls yet. ### Phase 4 · Summaries that point at the page Build: Produce an extractive summary first: the highest-ranked sentences with their offsets, which needs no model at all. Then, optionally, a generated summary in which each sentence must be emitted with the offsets it was derived from; verify each one resolves to real text before storing, and drop the ones that do not. Show the model name and the estimated cost before any paid call. Trap: Asking a model for a summary and then trying to attach citations afterwards by searching for similar text produces confident-looking anchors on sentences that were invented, because near-enough matching always finds something. Require the spans as part of the generation contract, verify that the quoted characters actually appear at those offsets, and discard any sentence that fails rather than repairing it. Done when: Every stored generated claim has offsets whose text exists at that range in the document, asserted on write. A claim returned without offsets or with offsets that do not resolve is dropped and the drop is counted and shown. The extractive summary works with no model configured and no network, tested. A paid summarisation shows the model name and an estimated cost before running and respects a configured ceiling. Do not build yet: No citation export, backups or cost history yet. ### Phase 5 · Getting citations out, and getting everything out Build: Export references as BibTeX and CSL-JSON built only from stored fields, refusing any reference missing a required field and naming what is missing. Export the whole library as originals plus a manifest of metadata, notes and claims with their offsets. Cap monthly model spend and record every call. Back up the database and exercise the restore in the suite. Trap: An export that silently omits a missing year or invents a container title produces a bibliography that compiles and is wrong in a way nobody notices until a copy editor does. Refuse the reference, list which fields are missing and for which document, and make fixing them a visible task in the interface rather than an error in a log. Done when: An export containing a work with a missing required field fails with that document and field named, and no placeholder is emitted. Exported BibTeX and CSL-JSON both import into a mainstream reference manager, checked by doing it. The library export contains the original files, and importing it into an empty install restores documents, notes and claims with working offsets. Model spend is capped monthly and every call is recorded with its cost. Do not build yet: Nothing after this. ### Out of scope (and why) - Licensed scholarly metadata and citation graphs, which are the paid datasets behind the products in this category and cannot be reproduced from open sources. - Logged-in browser automation against publisher sites, which breaks terms you personally accepted and is how accounts and institutional access get revoked. - Any paywall circumvention, including proxies and shadow libraries, whatever the argument for it. - Team libraries and shared annotation, which need identity, permissions and a server you then operate. - A chat interface over the whole corpus, which makes unanchored answers easy to produce and hard to check, against the rule above. ### README must contain - That every generated sentence carries verified offsets into a stored document, and unanchored sentences are dropped rather than shown - That bibliographic fields come from a service or from you, each recorded with its source, and never from a model - That citation export refuses a reference with a missing required field instead of filling it in - The courtesy convention used with the metadata service, including the contact address sent and the backoff on rate limiting - The measured extraction and search times on a corpus of at least two thousand documents
# Architecture · Genei ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript on Node 22 with SQLite, run locally | a personal corpus with no reason to leave the machine | | Originals | Kept byte for byte on disk, hashed, and never the thing that gets rewritten | the stored document is the evidence for every claim above it | | Extraction | Layout-aware text with character offsets preserved per page | a quote without an offset cannot be checked | | Metadata | Fetched from a bibliographic service and stored per field with its source | the difference between a citation and a guess is provenance | | Summaries | Extractive first, generated second, and always span-anchored | an unanchored sentence is dropped before it reaches the screen | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Import | Originals, layout-aware extraction and page offsets. | Any extractor, provided offsets still resolve to pages. | | Metadata | Lookup, provenance per field and backoff. | Any bibliographic service, provided the source is recorded per field. | | Search | Local passage search and metadata filters. | Any index, provided results stay passages with offsets. | | Summary | Extractive first, verified spans always. | Any model, provided spans are part of the contract and verified on write. | | Export | BibTeX, CSL-JSON and the full library manifest. | Any format, provided a missing required field still refuses. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `LIBRARY_DIR` · required · Original files, byte for byte as imported. - `DATABASE_PATH` · required · Documents, metadata, notes and claims with their offsets. - `CROSSREF_MAILTO` · required · Sent with metadata lookups as the service's documented courtesy convention. - `METADATA_MAX_RETRIES` · required · Bounded backoff on rate limiting. Never an unlimited retry loop. - `SUMMARY_MODEL` · required · Set to none for extractive summaries only, which is the default and works offline. - `SUMMARY_API_KEY` · optional, secret · Only when a generated summary model is configured. - `MONTHLY_SPEND_CEILING` · optional · Model spend per month. Reaching it refuses further paid calls. ## 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 · Genei product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript on Node 22 with SQLite, run locally, Kept byte for byte on disk, hashed, and never the thing that gets rewritten, Layout-aware text with character offsets preserved per page, Fetched from a bibliographic service and stored per field with its source, Extractive first, generated second, and always span-anchored. - 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 - Naive PDF extraction reads a two-column paper across the columns, so sentences interleave and every quote you later pull is half from one column and half from the other, which looks like the model hallucinating when it is actually your importer. Extract with layout awareness, check a two-column paper by eye during this phase, and store the page offsets so any quote can be checked against the printed page. - Filling in missing authors or a year by asking a language model produces a citation that looks right, passes review, and is wrong, and the person who finds out is a reviewer rather than you. Leave the field empty, mark it as needing attention, and make the citation export refuse to emit a reference with an unsourced required field. - Returning a list of matching documents rather than matching passages means every search ends with you opening a ninety page PDF and using the reader's own find function, which is the workflow the tool was supposed to replace. Return ranked passages with their offsets, and make clicking one open the document scrolled to the highlight. - Asking a model for a summary and then trying to attach citations afterwards by searching for similar text produces confident-looking anchors on sentences that were invented, because near-enough matching always finds something. Require the spans as part of the generation contract, verify that the quoted characters actually appear at those offsets, and discard any sentence that fails rather than repairing it. - An export that silently omits a missing year or invents a container title produces a bibliography that compiles and is wrong in a way nobody notices until a copy editor does. Refuse the reference, list which fields are missing and for which document, and make fixing them a visible task in the interface rather than an error in a log.
# Delivery milestones · Genei Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Keeping the document and knowing where you are in it Text comes out with offsets that map back to a page and a highlight. ### Steps 1. Extract with layout awareness and check a two-column paper The failure that gets blamed on the model later. Files: `src/extract/pdf.ts`, `tests/two-column.test.ts` 2. Record page ranges as character offsets An offset that maps to a page number is a checkable quote. Files: `src/extract/pages.ts`, `src/db/schema.sql` 3. Keep the original untouched and hashed It is the evidence behind everything the app later says. Files: `src/store/originals.ts` ### Done when - [ ] A two-column paper extracts in reading order - [ ] An offset resolves to a page and highlights correctly - [ ] The stored original is byte-identical to the import - [ ] Extractor and version are recorded per document ### Watch out - Naive PDF extraction reads a two-column paper across the columns, so sentences interleave and every quote you later pull is half from one column and half from the other, which looks like the model hallucinating when it is actually your importer. Extract with layout awareness, check a two-column paper by eye during this phase, and store the page offsets so any quote can be checked against the printed page. ## M2 · Finding out what the thing actually is Bibliographic fields come from a service or from you, and each one says which. ### Steps 1. Look up by identifier and record the source per field Provenance per field is what makes a citation defensible. Files: `src/metadata/crossref.ts`, `src/db/schema.sql` 2. Send a contact address and honour 429 with backoff The documented courtesy, and it gets you the faster pool. Files: `src/metadata/client.ts`, `tests/backoff.test.ts` 3. Forbid model output in bibliographic fields A test over the write paths, not a promise in a comment. Files: `tests/no-generated-metadata.test.ts` ### Done when - [ ] Every bibliographic field records its source - [ ] No model output can reach a bibliographic field - [ ] A 429 backs off within a bounded delay - [ ] A document with no identifier still imports ### Watch out - Filling in missing authors or a year by asking a language model produces a citation that looks right, passes review, and is wrong, and the person who finds out is a reviewer rather than you. Leave the field empty, mark it as needing attention, and make the citation export refuse to emit a reference with an unsourced required field. ## M3 · Finding the passage you remember reading Search covers the whole corpus, returns the passage in context, and filters on metadata. ### Steps 1. Return passages with offsets, not document hits The passage is the thing being searched for. Files: `src/search/passages.ts` 2. Combine metadata filters with the text query Author and year are how researchers actually narrow. Files: `src/search/filters.ts` 3. Assert search makes no network request A local corpus should stay local. Files: `tests/offline-search.test.ts` ### Done when - [ ] Results are passages with document, page and offsets - [ ] Metadata filters narrow the text results - [ ] Search issues no outbound request - [ ] Search time on two thousand documents is measured and stated ### Watch out - Returning a list of matching documents rather than matching passages means every search ends with you opening a ninety page PDF and using the reader's own find function, which is the workflow the tool was supposed to replace. Return ranked passages with their offsets, and make clicking one open the document scrolled to the highlight. ## M4 · Summaries that point at the page Every sentence shown is either a quotation or a generated sentence with a span behind it. ### Steps 1. Ship extractive summarising before anything generated It works offline, costs nothing, and sets the anchoring contract. Files: `src/summary/extractive.ts` 2. Verify every generated span against the stored text The characters are there or the sentence does not ship. Files: `src/summary/verify.ts`, `tests/span-verify.test.ts` 3. Show model and estimated cost before a paid call A corpus-wide summarisation is a real invoice. Files: `src/summary/estimate.ts` ### Done when - [ ] Every generated claim resolves to real text at its offsets - [ ] Unanchored claims are dropped and counted - [ ] Extractive summarising works offline with no model - [ ] Paid runs show a cost estimate and respect a ceiling ### Watch out - Asking a model for a summary and then trying to attach citations afterwards by searching for similar text produces confident-looking anchors on sentences that were invented, because near-enough matching always finds something. Require the spans as part of the generation contract, verify that the quoted characters actually appear at those offsets, and discard any sentence that fails rather than repairing it. ## M5 · Getting citations out, and getting everything out (production only) References export in formats a reference manager reads, and the library is portable. ### Steps 1. Build references only from stored fields Refuse rather than fill. This is the rule with teeth. Files: `src/export/bibtex.ts`, `src/export/csl.ts`, `tests/missing-field.test.ts` 2. Export originals plus a manifest and reimport it An export that does not reimport is a filename, not an exit. Files: `src/export/library.ts`, `tests/roundtrip.test.ts` 3. Record every model call with its cost and cap the month The cost page is the only one that gets read later. Files: `src/cost/ledger.ts` ### Done when - [ ] A missing required field blocks the reference and is named - [ ] BibTeX and CSL-JSON import into a reference manager - [ ] A library export reimports with working offsets - [ ] Monthly spend is capped and every call is recorded ### Watch out - An export that silently omits a missing year or invents a container title produces a bibliography that compiles and is wrong in a way nobody notices until a copy editor does. Refuse the reference, list which fields are missing and for which document, and make fixing them a visible task in the interface rather than an error in a log.
# Operations · Genei ## Backup The originals directory and the database together: the originals are irreplaceable if they were your only copy, and the database holds notes and claims that cannot be regenerated. Back up both, and verify a restore reproduces working offsets rather than assuming it will. ## Restore Restore both directories, then verify a sample of claims still resolve to the text at their offsets. If extraction has changed version since the backup, requeue extraction by version rather than reimporting, so notes keep their anchors. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Extraction failures, documents with fields still needing attention, dropped unanchored claims per summary, and model spend against the monthly ceiling. A rising drop count means the summarisation contract is being violated and the output should not be trusted. ## Incident checklist A wrong citation means a field was entered manually or a service returned something unexpected, and work_fields records which and when. A quote that does not match the page means extraction reordered the text, which the two-column test in Phase 1 exists to catch. 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 two-column paper is confirmed to extract in reading order - [ ] No model output is confirmed able to reach a bibliographic field - [ ] Every generated claim is confirmed to resolve to real text - [ ] A missing required field is confirmed to block a reference - [ ] A library export is confirmed to reimport with working offsets ## Launch constraint Do not market omitted Genei 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. Original files, byte for byte as imported. LIBRARY_DIR=./data/library # Required. Documents, metadata, notes and claims with their offsets. DATABASE_PATH=./data/library.db # Required. Sent with metadata lookups as the service's documented courtesy convention. CROSSREF_MAILTO=you@example.com # Required. Bounded backoff on rate limiting. Never an unlimited retry loop. METADATA_MAX_RETRIES=4 # Required. Set to none for extractive summaries only, which is the default and works offline. SUMMARY_MODEL=none # Optional · secret. Only when a generated summary model is configured. SUMMARY_API_KEY=sk-...redacted # Optional. Model spend per month. Reaching it refuses further paid calls. MONTHLY_SPEND_CEILING=10.00
$ choose a build depth, inspect the files, then open the complete pack in your agent
Genei: Researchers pay for correct metadata, resilient importers, citation coverage, and workflows that survive publisher and browser changes.
xlicensed scholarly metadata
xpublisher-specific import reliability
xcitation graph scale
xteam libraries and institutional access
Don't feel like building it? These folks already made it free.
OOpen NotebookA self-hosted research notebook for sources, summaries and chat, with Docker attached.open source↗no votes, no pay-to-list · just what's real
Genei pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| professional basic | $13.51/user | $10.81/user | Up to 5 GB of uploaded files; unlimited projects and resources. |
| professional pro | $40.56/user | $33.80/user | Unlimited uploaded-file storage, projects and resources. |
| academic basic | $6.75/user | $5.40/user | Up to 5 GB of uploaded files; unlimited projects and resources; academic eligibility required. |
| academic pro | $27.04/user | $21.63/user | Unlimited uploaded-file storage, projects and resources; academic eligibility required. |
free tierno free tier; 14-day trial
billingmonthly + annual; prices are published in GBP
hidden costsacademic rates require eligibility; currency conversion, taxes and card charges can change the final USD amount
verified 2026-08-13 · source ↗
Vibecode Genei
Kinda. The core of Genei is buildable in a weekend with the prompt on this page, but there are real gaps: licensed scholarly metadata, publisher-specific import reliability. Read the honest list above before committing.
How much does Genei cost?
Genei'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 Genei?
Honestly: licensed scholarly metadata; publisher-specific import reliability; citation graph scale; team libraries and institutional access. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Genei?
Yes: Open Notebook (A self-hosted research notebook for sources, summaries and chat, with Docker attached.) NotebookLM (Sources become cited answers, summaries and notes; the free limits are unusually humane.) The prompt is for when you want it exactly your way.