# Noteback — Design Spec **Date:** 2026-06-03 **Status:** Approved for planning **Repo:** `noteback/` (open source; license is the author's choice — MIT recommended for max adoption) --- ## 1. Overview Noteback is a zero-backend Chrome extension for reviewing **local AI-generated HTML documents** (specs, plans, design docs). You highlight a passage, attach a comment anchored to that exact quote, and then either: 1. **Copy the feedback as Markdown** to paste back to an AI or a person, or 2. **Save the document as a self-contained "feedback canvas"** — a single HTML file with your highlights and comments baked in, that the recipient can open, read, *add their own comments to (no extension required)*, and re-share. An AI handed that file can act on the feedback directly. It runs entirely locally. No server, no account, no telemetry. ### The workflow it serves > An AI generates an HTML doc → you open it locally → you annotate it → you send the feedback back to the AI (next prompt) **or** to the human who shared it (who will likely paste it into *their* AI). --- ## 2. Goals & non-goals ### Goals (the two co-equal pillars) - **G1 — Frictionless browser overlay.** Annotating a local HTML doc should feel as natural as commenting in Google Docs: select text, type a note, done. Zero setup beyond installing the extension. - **G2 — Best-in-class feedback export.** The output must be excellent for *both* audiences it can travel to: a human reviewer and an AI model. This is what separates Noteback from generic web annotators. ### Non-goals (v1) - Not a general "annotate any website" tool (the annotation space is saturated; we win the local-AI-doc niche). - Not a PDF annotator. - No cloud sync, accounts, or collaboration server. - Not a replacement for Plannotator's local-URL flow — Noteback is the *zero-setup, works-on-any-open-tab* alternative. --- ## 3. Positioning & naming - **Brand:** **Noteback** — "note it → send it back." Verified clear on the Chrome Web Store and GitHub (only adjacent note-takers exist; nobody owns the word). *To-do before launch: confirm npm package name + a domain are also free.* - **Discoverability strategy:** the generic word "annotate" is unwinnable (Web Highlights, Glasp, Hypothesis, etc.). Instead, own the wide-open long-tail: *"feedback for AI / annotate AI docs / review LLM specs / copy feedback to Claude."* - **Web Store listing title:** `Noteback — Annotate AI Docs & Copy/Share Feedback for Claude, ChatGPT & co.` - **GitHub topics:** `chrome-extension`, `annotation`, `ai`, `llm`, `claude`, `chatgpt`, `feedback`, `code-review`, `manifest-v3`, `html`. - Do **not** put "Claude"/"ChatGPT" in the brand name (trademarks); naming them in the *description* is fine and good for search. --- ## 4. Architecture Chrome **Manifest V3**, fully client-side. ### 4.1 The key architectural decision: one portable runtime, two modes The annotation engine (selection → popover → highlight painting → comment list → serialization) is built **once** as a self-contained module, and runs in two modes: | Mode | Host | State store | Used by | |------|------|-------------|---------| | **Extension mode** | Injected as a content script into local pages | `chrome.storage.local` | The original author (has the extension) | | **Embedded mode** | Inlined as a ``. 4. A one-line **guiding HTML comment** at the top so any AI handed the file knows what it is: `` 5. The inlined **portable runtime** + `InFileStateAdapter`. **AI consumption:** an AI can act on the canvas whether it reads the visible annotated text or parses the JSON state block — both are present and consistent. ### 8.3 Re-sharing the canvas — self-modification mechanics A saved HTML file **cannot silently overwrite itself** (browser security boundary). Re-sharing therefore works in tiers, feature-detected at runtime: - **Baseline (always available, incl. `file://`): "Download with my comments."** The runtime serializes current state into a fresh HTML blob and triggers a download (suggested filename = original). Non-destructive — each round is its own versioned file. - **Enhancement (secure contexts, e.g. `localhost`/`https`): in-place "Save."** If `'showSaveFilePicker' in window`, offer a real overwrite via the File System Access API (requires a user gesture + one-time permission prompt + the user pointing the picker at the file). Falls back to download if unavailable or declined. **Not** relied upon for the primary `file://` workflow. **Asymmetry to remember:** the original author (with the extension) never touches this — their edits autosave to `chrome.storage.local`. The download/save flow exists only for *extension-less recipients*, which is exactly the canvas's audience. --- ## 9. Onboarding & permissions UX - On first run / first local-doc visit, detect whether **"Allow access to file URLs"** is enabled. If not, show a one-time card with the exact steps to enable it (with a deep link to the extension's details page where possible). - `localhost`/`127.0.0.1` are **opt-in** (off by default): the popup's per-type toggle enables them, so a dev server doesn't get the overlay unasked-for. The onboarding card points users serving docs locally to that toggle. --- ## 10. Privacy & open-source posture - 100% local: no network calls, no analytics, no accounts. State lives in `chrome.storage.local` and inside saved files only. - This is both a privacy selling point and a fast Web Store review (minimal permissions, no remote code). - Repo includes README (with the keyword-rich description), LICENSE, and a short CONTRIBUTING; runtime is framework-light to keep the embedded canvas small. --- ## 11. MVP scope vs. future ### In v1 (MVP) - Extension injects on `file://` + `localhost`; selection → comment; persistent highlights; sidebar. - Robust text-quote anchoring with an "unanchored" fallback group. - **Copy as Markdown.** - **Save as fully-interactive HTML feedback canvas** (download baseline + feature-detected in-place save). - Onboarding for "Allow access to file URLs." ### Explicitly out of scope for v1 (clean fast-follows) - **Author attribution / multi-round threads** — distinguishing whose comment is whose, a color per author, "reply" to a comment. Data model already reserves `author`. *Likely first fast-follow.* - Image/diagram pin comments (click-to-pin). - "Any web page" mode (broaden host permissions). - Custom export templates + AI-instruction preamble toggle + "group under heading." - Severity/type tags (nit / question / change-request). - Multi-doc dashboard, cloud sync. --- ## 12. Testing approach - **Runtime unit tests** (mode-agnostic): anchoring (find/re-find/orphan), state serialization round-trip, Markdown export formatting. - **Adapter tests:** `ChromeStorageAdapter` and `InFileStateAdapter` satisfy the same contract. - **Canvas integration test:** export a canvas, re-open the produced HTML in a bare page (no extension), assert highlights render and a new comment can be added + re-serialized. - **Manual smoke matrix:** `file://` doc, `localhost` doc, large doc, doc with duplicate phrases (occurrence index), regenerated doc (orphans). --- ## 13. Open questions (to resolve during planning) - Markdown default: confirmed clean/neutral format above; revisit whether to include an optional heading-context line by default. - Canvas size budget: target max inlined runtime size (keep the self-contained file lean). - Build tooling: plain JS modules vs. a light bundler (needed to inline the runtime into the canvas template) — decide in the plan. --- ## 14. Snapshot-based document history (2026-06-06) **Status:** implemented (PR #4). **Supersedes** the per-fragment "earlier feedback" model and refines §5 (data model) and §8.3 (re-sharing): a document now owns an ordered list of whole-document **versions**, and the canvas and extension share one history engine. The enforced runtime contract for this lives in `CONTRACTS.md` §8; the non-obvious gotchas live in `CLAUDE.md`. This section records the *design decisions and rationale* so they aren't lost when the planning artifacts are removed. ### 14.1 The problem — two stories, one of them blank There used to be two unrelated persistence stories, and only one had any history: | | Extension | Embedded canvas | |---|---|---| | Identity | full URL (`location.href`) | content hash + normalized URL | | Backend | `chrome.storage.local` | in-file JSON + `localStorage` | | History | **none at all** | yes — but stored as padded fragments | The embedded "earlier feedback" stored padded *sections* around each selection, re-highlighted across text nodes with whitespace-loose matching, then trimmed to a byte cap. That subsystem (`snapshot.js`, ~311 lines) is where several shipped bugs lived: empty captures, first-mark-only unions, a paint-before-persist trap. Meanwhile the extension — where the author's real iterate-and-re-comment loop happens — had no history at all. ### 14.2 Mental model — one timeline of versions Collapse "current comments" and "earlier feedback" into a single concept: **a document owns an ordered list of versions, and each version is a complete, openable document with its comments attached.** "Current" is just the newest node, not a separate category. The per-version key is the **content hash**, or a minted id when the content is too short to hash (`h0:`). A history entry is therefore the *same kind of thing* as the live document — a whole doc you open and read in context, not a clipped fragment that must be reconstructed. That equivalence is what removes the complexity. ### 14.3 Identity — baked when we own the HTML, URL-mapped when we don't Identity is one concept with two acquisition paths: - **`wrap` / "Save as canvas"** bakes a stable `data-noteback-doc-id` onto `#noteback-doc-root`. It travels with the file, survives renames/moves, and `wrap` **preserves** it on re-export (precedence: explicit `--id` → the id baked in the `-o` target → the id baked in the input → mint). - **Extension on a page it didn't author** looks up (or mints) a doc-id in a `chrome.storage` `nb:url:` → doc-id map. A baked id always **wins** over a URL lookup, so landing on a wrapped canvas chains its history correctly; saving a bare page as a canvas is where a URL-keyed page first gets a baked id minted into the output. ### 14.4 One sidebar, every stage of the doc's life A single panel adapts to where you are: - **Working state** — live comments pinned at top; every earlier version that received feedback is a timeline node below, newest first. Each past row offers **peek** (click the row — renders that version in context), **open** (checks it out as a full canvas tab), and **copy feedback** (that version's markdown). - **Collapse rule (keep the panel calm):** the **History** group (the label the user sees) is **hidden at 0** earlier versions, shown **inline at exactly 1**, and at **2+** the most-recent earlier version stays inline while the rest tuck under a "+N older versions" disclosure. Last round of feedback is always zero-click; deeper history is one click; the current draft is never buried. - **Layout:** the live comments (or the "No notes yet" empty state) own the scrollable space; the History timeline **docks at the bottom** — a bounded, self-scrolling band above the action buttons — so it never pushes the current draft's notes out of view. - **History travels with the file (opt-in).** History normally lives only in the browser's `localStorage`, so a saved canvas opened on another machine shows comments but an empty timeline. The Save menu gains **"HTML · with comments and history"** (shown only when there IS history) which embeds the doc's full history — every version record + gzipped snapshot — into a `#noteback-history` JSON block in the file. On reopen the embedded runtime seeds `localStorage` from that block (**only keys not already present** — never clobber newer local data), so the timeline rehydrates from the file itself. The block is excluded from snapshots, clean copies, and plain "with comments" saves, so it never nests or recurses. - **Inline view** opens the snapshot as a read-only side panel beside the sidebar — NOT a centered modal, NOT a new tab. The panel (`.nb-hist-view`) fills the document area left of the 360px sidebar so the live timeline stays visible. The snapshot **fills the panel** as a column-flex `