From a7d846aa335fcc09f2a16c1e619588ab01910dcb Mon Sep 17 00:00:00 2001 From: Julien Calixte Date: Sun, 28 Jun 2026 23:52:27 +0200 Subject: [PATCH] docs: add language glossary, QFD design, and ADRs Capture the walk-with-me + qfd session: CONTEXT.md ubiquitous-language glossary, DESIGN.md goal-driven cascade (Beauty / Pride & Peace / public voice), and ADRs for Note identity (Path) and the two reference modes (Live vs Snapshot). --- CONTEXT.md | 125 ++++++++ DESIGN.md | 419 +++++++++++++++++++++++++ docs/adr/0001-note-identity-is-path.md | 15 + docs/adr/0002-two-reference-modes.md | 20 ++ 4 files changed, 579 insertions(+) create mode 100644 CONTEXT.md create mode 100644 DESIGN.md create mode 100644 docs/adr/0001-note-identity-is-path.md create mode 100644 docs/adr/0002-two-reference-modes.md diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..962ffe0 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,125 @@ +# Remanso + +Remanso displays markdown **Notes** from GitHub repositories with Zettelkasten-style backlinks, and publishes/reads decentralized public notes over ATProto. This glossary is the shared vocabulary for the domain — it must appear verbatim in code, tests, commits, and docs. + +**Naming convention:** Themed river/Amazonian names (after *Remanso*, a still backwater) are used for **user-facing surfaces and places** (e.g. **Igarapé**). **Core technical concepts** keep their standard Zettelkasten/Obsidian names (**Note, Backlink, Path, Card, Draft, Fleeting**) for discoverability. + +## Language + +### Core note model + +**Note**: +A single markdown document in a repository, identified by its **Path**. Editing a Note does not change its identity. +_Avoid_: "file" when you mean the semantic document. + +**Path**: +A Note's stable identity — its location within the repo (e.g. `ideas/zettel.md`). What authors link to and what survives edits. + +**SHA**: +The git blob hash of a Note's content — a *content version*, not the Note's identity. Changes on every edit. Legitimately used as the address of a **Snapshot reference** (an immutable shared version). +_Avoid_: treating SHA as the Note's *identity* (identity is **Path**); but SHA *is* the right address for an immutable snapshot. + +**File** / **RepoFile**: +The repo artifact carrying a Note's metadata (`path`, `sha`, `size`, …) as returned by the GitHub API. A File is how a Note appears in a repo listing, not a separate domain entity. + +### Note kinds + +A Note's kind is inferred from its folder, not stored as a field. + +**Fleeting Note**: +A transient, quick-capture Note you later distill into a permanent one (Zettelkasten sense). Stored under the `inbox` / `_inbox` folder. +_Avoid_: "Inbox Note" for the concept — `inbox` is only the storage folder convention; the domain term is Fleeting Note. + +**Draft Note**: +A work-in-progress Note, stored under the `drafts` / `_drafts` folder. + +**Task**: +An actionable item in `todo.txt` (todo.txt format); may recur. Completing a Task archives it to `done.txt`, and for a recurring Task schedules the next occurrence. A Task is not a Note. +_Avoid_: "Todo Note" — Tasks live in a single todo.txt file, not as Notes. + +### Reading & navigation + +**Igarapé**: +The primary reading surface — the channel you travel as you read a Note and follow its backlinks (Amazonian/Tupi: a narrow navigable waterway through the forest). Any kind of Note appears in the Igarapé. Named to extend the river metaphor set by *Remanso* (a still backwater). +_Avoid_: "Flux" (dropped — internal jargon, no domain meaning). + +**Index**: +A flat listing of every Note in a repo (path, sha, size). A catalogue view, unrelated to time or edits. +_Avoid_: "History" / "Historic Notes" for this — it is an index, not a chronological record. + +**History**: +The chronological log of repos the user has recently visited (powers the "last visited" navigation). +_Avoid_: using "history" for the Note **Index**, or for edit/version tracking (no such feature exists; there is no "edit history"). + +**Backlink**: +An inbound reference to a Note — another Note that links to it. The Zettelkasten/Obsidian standard term (kept plain per the naming convention). + +**Stacked Notes**: +The navigation pattern in the **Igarapé** where following a Backlink opens the target Note alongside the current one, accumulating a horizontal stack you can read across. + +**Live reference**: +A reference to a Note **by Path** that resolves to its *current* content (e.g. a Backlink you follow in your own repo). Reflects edits. + +**Snapshot reference**: +A reference to a Note **by SHA** that resolves to that *exact, immutable* version (e.g. a shared/bookmarked stack link). Content-addressed; deliberately unaffected by later edits, so what was shared cannot change underneath a reader. Not a fragility — an integrity feature. + +### Spaced repetition + +**Card**: +A flashcard reviewed via spaced repetition — its content (front, back, references) and its review schedule together form one Card. Content lives in `_cards/` markdown; the schedule lives in the local DB (an implementation split, not two domain concepts). +_Avoid_: using "Card" to mean only the content, or only the schedule. The pair (`Repetition` in code) **is** the Card. + +**Level**: +A Card's mastery level (1–8); higher levels space reviews further apart. + +**Need-review**: +A Card the user has manually flagged to resurface for review, independent of its scheduled date. + +### Decentralized publishing (ATProto) + +**Published Note**: +A Note published to the decentralized ATProto network as a `space.remanso.note` record, addressed by **DID** + **rkey** (not by Path). A distinct entity created by publishing a Note; carries its own snapshot (title, images, content, theme, language). +_Avoid_: "Public Note" — reserve "public" for a Note in a public GitHub repo (see below). + +**Publish**: +The act of making a Note public as a **Published Note**, via a single low-friction gesture — suffixing the file `*.pub.md` in the IDE — rather than an in-app form. "Lower every wall to make your voice public." + +**DID**: +The decentralized identifier of a Published Note's **Author** (e.g. `did:plc:…`). Standard ATProto term. + +**rkey**: +The record key identifying one Published Note within an Author's PDS repo. Standard ATProto term. + +**Author**: +The publisher of a Published Note — a `handle` plus a **PDS**, resolved from a DID. + +**PDS**: +Personal Data Server — where an Author's ATProto records (and image blobs) are hosted. + +## Relationships + +- A **Note** is identified by exactly one **Path**. +- A **Note** has one current **SHA**, which changes on every edit. +- A **File** is the repo-listing view of a **Note** (it carries the Note's Path and current SHA). +- A **Note** can be published as a **Published Note**, which is then addressed by **DID** + **rkey** instead of Path. +- A **Published Note** has exactly one **Author**; an **Author** is identified by a **DID** and hosted on a **PDS**. +- Following a **Backlink** in the **Igarapé** produces **Stacked Notes**. + +## Example dialogue + +> **Dev:** "When I follow a **Backlink** in the **Igarapé**, do I navigate by **Path** or **SHA**?" +> **Maintainer:** "The identity is the **Path** — that's what the link author wrote. The **SHA** is just the current content version; we happen to use it as a runtime handle, but if it ever disagrees with the Path, the Path wins." +> +> **Dev:** "And if I publish this **Note**?" +> **Maintainer:** "It becomes a **Published Note** — a separate ATProto record addressed by **DID** + **rkey**, not by Path. The repo **Note** stays as it is." + +## Flagged ambiguities + +- **Identity = Path; two reference modes (refined 2026-06-28)** — see [ADR-0001](docs/adr/0001-note-identity-is-path.md). A Note's *identity* is its **Path**, so **Live references** (Backlinks in your repo) must resolve to current content. But a **Snapshot reference** (a shared `?stackedNotes=sha` link) deliberately pins a **SHA** for immutability — that is an integrity feature, not a fragility. The earlier framing of "SHA-as-handle = fragility" was too absolute; ADR-0001 is to be amended to record both modes. +- `useNotes()` / `useFolderNotes()` are named "notes" but return `RepoFile[]`. Per this glossary they return **Files** (the repo-listing view), not `Note` objects — a naming drift to reconcile in code. +- `FluxNote` / `FluxNoteView` should be renamed to **Igarapé** (`Igarape` / `IgarapeView`) in code — "Flux" is dropped jargon. +- `HistoricNotes` view (route `/history`) is the **Index**, not history — rename to `NoteIndex` and move off the `/history` path. +- `CLAUDE.md` describes `modules/history/` as "Edit history tracking" — incorrect; it is the visited-repos **History**. Correct the doc; there is no edit-history feature. +- Card cluster: `Repetition` (the content+schedule pair) **is** the **Card** — rename `Repetition` → `Card`. The content-only struct and `RepetitionCard` (schedule) become named sub-parts (e.g. `CardContent`, `ReviewSchedule`). The hook already calls the pairs `cards` — make the types agree. +- ATProto cluster: rename `PublicNote` / `PublicNoteRecord` / `PublicNoteListItem` → `PublishedNote*`, and move `PublicNoteListItem` out of `Note.ts` into the `atproto` module (it is not a repo-Note concept). +- "Public" is overloaded: a Note in a **public GitHub repo** (auth-free access) vs the ATProto entity (now **Published Note**). Resolved — "public" refers only to repo visibility. diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..245bc41 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,419 @@ +# Remanso — Design (QFD) + +Goal-driven design for what Remanso is *for*: a **beautiful, primarily read-only** viewer for notes authored in the IDE (git push) and read as a Matuschak-style stacked-notes web — built to make notes feel **good-looking**, to evoke **pride and peace** (including reading *gracefully on a flaky mobile/metro connection*), and to **lower every wall** to making your voice public. Relies on [CONTEXT.md](CONTEXT.md) for vocabulary (Note, Path, SHA, Backlink, Igarapé, Stacked Notes, Live/Snapshot reference, Published Note, Publish), [ADR-0001](docs/adr/0001-note-identity-is-path.md) (Note identity = Path) and [ADR-0002](docs/adr/0002-two-reference-modes.md) (Live vs Snapshot reference). The earlier "Path↔SHA resolution layer" is not a goal in itself — it folds in under Peace (live references that never break) and integrity (immutable Snapshot references). + +Strength weights used in matrices: **9** strong, **3** medium, **1** weak, blank none. + +--- + +## House of Quality + +```tikz +\usetikzlibrary{arrows.meta, positioning, shapes.geometric, shapes.misc, calc, fit, backgrounds} + +% Toggles +\newif\ifqfdshowroof \qfdshowrooftrue +\newif\ifqfdshowbasement \qfdshowbasementtrue +\newif\ifqfdshowcompetitive \qfdshowcompetitivetrue +\newif\ifqfdshowlegend \qfdshowlegendtrue +\newif\ifqfdshowimportance \qfdshowimportancetrue +\newif\ifqfdshowcorrlegend \qfdshowcorrlegendtrue +\newif\ifqfdshowevallegend \qfdshowevallegendtrue +\newif\ifqfdshowtitle \qfdshowtitletrue + +% Dimensions +\def\qfdNW{5} +\def\qfdNH{5} +\def\qfdWhatW{4.0} +\def\qfdImpW{0.9} +\def\qfdCmpW{3} +\def\qfdHdrH{2.6} +\def\qfdBasementN{4} + +% Titles & labels +\def\qfdWhatsTitle{Customer needs} +\def\qfdImpTitle{Imp.\ \%} +\def\qfdPerceptionTitle{Comparative evaluation} +\def\qfdPoorLabel{poor} +\def\qfdExcellentLabel{excellent} +\def\qfdAltOneLabel{Our product} +\def\qfdAltTwoLabel{Competitor A} +\def\qfdAltThreeLabel{Competitor B} +\def\qfdRelTitle{Relation} +\def\qfdCorrTitle{Correlation} +\def\qfdEvalTitle{Evaluation} + +\def\qfdProjectTitle{} +\def\qfdConcept{} + +\tikzset{ + qfdthin/.style ={line width=0.35pt}, + qfdmed/.style ={line width=0.7pt}, + qfdstrong/.style={circle, draw, fill=black, minimum size=7pt, inner sep=0pt}, + qfdmod/.style ={circle, draw, minimum size=7pt, inner sep=0pt, line width=0.8pt}, + qfdweak/.style ={regular polygon, regular polygon sides=3, draw, minimum size=8.5pt, inner sep=0pt, line width=0.7pt}, + qfdrel/.is choice, + qfdrel/S/.style={qfdstrong}, + qfdrel/M/.style={qfdmod}, + qfdrel/W/.style={qfdweak}, + qfdalt1mk/.style={circle, draw, fill=black, minimum size=6pt, inner sep=0pt, line width=1pt}, + qfdalt1ln/.style={line width=1.2pt}, + qfdalt2mk/.style={regular polygon, regular polygon sides=3, draw, fill=black, minimum size=6pt, inner sep=0pt, line width=0.7pt}, + qfdalt2ln/.style={line width=0.7pt, dashed}, + qfdalt3mk/.style={rectangle, draw, fill=black, minimum size=5pt, inner sep=0pt, line width=0.7pt}, + qfdalt3ln/.style={line width=0.7pt, dotted}, +} + +\newcommand{\qfdDrawGrid}{% + \foreach \c in {1,...,\qfdNHm} \draw[qfdthin] (\c, 0) -- (\c, -\qfdNW); + \foreach \r in {1,...,\qfdNWm} \draw[qfdthin] (0, -\r) -- (\qfdNH, -\r); + \foreach \r in {1,...,\qfdNWm} \draw[qfdthin] (\qfdLeftEdge, -\r) -- (0, -\r); + \ifqfdshowroof + \foreach \c in {1,...,\qfdNHm} \draw[qfdthin] (\c, 0) -- (\c, \qfdHdrH); + \fi + \ifqfdshowcompetitive + \foreach \r in {1,...,\qfdNWm} \draw[qfdthin] (\qfdNH, -\r) -- (\qfdNH+\qfdCmpW, -\r); + \fi + \ifqfdshowbasement + \foreach \r in {1,...,\qfdBasementN} \draw[qfdthin] (0, -\qfdNW-\r) -- (\qfdNH, -\qfdNW-\r); + \foreach \c in {1,...,\qfdNHm} \draw[qfdthin] (\c, -\qfdNW) -- (\c, -\qfdNW-\qfdBasementN); + \fi +} + +\newcommand{\qfdDrawRoof}{% + \ifqfdshowroof + \foreach \k in {1,...,\qfdNHm} {% + \pgfmathsetmacro{\rx}{(\k+\qfdNH)/2} + \pgfmathsetmacro{\ry}{\qfdHdrH + (\qfdNH-\k)/2} + \pgfmathsetmacro{\lx}{\k/2} + \pgfmathsetmacro{\ly}{\qfdHdrH + \k/2} + \draw[qfdthin] (\k, \qfdHdrH) -- (\rx, \ry); + \draw[qfdthin] (\k, \qfdHdrH) -- (\lx, \ly); + }% + \draw[qfdmed] (0, \qfdHdrH) -- (\qfdNH/2, \qfdApexY) -- (\qfdNH, \qfdHdrH); + \foreach \i in {1,...,\qfdNH} + \foreach \k in {1,...,\qfdNH} {% + \pgfmathtruncatemacro{\jj}{\i+\k} + \ifnum\jj>\qfdNH\relax\else + \pgfmathsetmacro{\xx}{\i + \k/2 - 0.5} + \pgfmathsetmacro{\yy}{\qfdHdrH + \k/2} + \coordinate (C-\i-\jj) at (\xx, \yy); + \fi + }% + \fi +} + +\newcommand{\qfdDrawScale}{% + \ifqfdshowcompetitive + \foreach \tk in {0,1,2,3,4,5} {% + \pgfmathsetmacro{\tx}{\qfdNH + (\tk+0.5)*\qfdCmpW/6} + \node[anchor=south, font=\scriptsize] at (\tx, 0.02) {\tk}; + }% + \node[anchor=south, font=\scriptsize\bfseries, align=center] at ({\qfdNH + \qfdCmpW/2}, 0.7) {\qfdPerceptionTitle}; + \node[anchor=north, font=\scriptsize\itshape] at ({\qfdNH + 0.45}, -\qfdNW) {\qfdPoorLabel}; + \node[anchor=north, font=\scriptsize\itshape] at ({\qfdNH + \qfdCmpW - 0.45}, -\qfdNW) {\qfdExcellentLabel}; + \fi +} + +\newcommand{\qfdDrawZoneTitles}{% + \ifqfdshowimportance + \node[rotate=90, anchor=west, font=\footnotesize\bfseries] at ({-\qfdImpW/2}, 0.12) {\qfdImpTitle}; + \fi + \node[font=\scriptsize\bfseries, align=center, text width=\qfdWhatW cm] at ({\qfdLeftEdge + \qfdWhatW/2}, {\ifqfdshowroof \qfdHdrH/2 \else 0.6 \fi}) {\qfdWhatsTitle}; +} + +\newcommand{\qfdDrawTitle}{% + \ifqfdshowtitle + \ifx\qfdProjectTitle\empty\else + \pgfmathsetmacro{\qfdTitleX}{\qfdNH/2} + \pgfmathsetmacro{\qfdTitleY}{\ifqfdshowroof \qfdApexY \else \qfdHdrH \fi + 0.9} + \pgfmathsetmacro{\qfdSubW}{\qfdNH + 2} + \node[anchor=south, font=\large\bfseries, align=center] at (\qfdTitleX, \qfdTitleY) {\qfdProjectTitle}; + \ifx\qfdConcept\empty\else + \node[anchor=north, font=\footnotesize\itshape, align=center, text width=\qfdSubW cm] at (\qfdTitleX, {\qfdTitleY - 0.1}) {\qfdConcept}; + \fi + \fi + \fi +} + +\newcommand{\qfdDrawFrames}{% + \begin{scope}[qfdmed] + \draw (\qfdLeftEdge, 0) rectangle (\qfdNH, -\qfdNW); + \ifqfdshowimportance \draw (-\qfdImpW, 0) -- (-\qfdImpW, -\qfdNW); \fi + \draw (0, 0) -- (0, -\qfdNW); + \ifqfdshowroof \draw (0, 0) rectangle (\qfdNH, \qfdHdrH); \fi + \ifqfdshowbasement \draw (0, -\qfdNW) rectangle (\qfdNH, -\qfdNW-\qfdBasementN); \fi + \ifqfdshowcompetitive \draw (\qfdNH, 0) rectangle (\qfdNH+\qfdCmpW, -\qfdNW); \fi + \end{scope} +} + +\newcommand{\qfdDrawLegend}{% + \ifqfdshowlegend + \pgfmathsetmacro{\qfdLegX}{\qfdNH + \ifqfdshowcompetitive \qfdCmpW + 0.7 \else 0.7 \fi} + \pgfmathsetmacro{\qfdLegBottom}{-2.05 \ifqfdshowroof \ifqfdshowcorrlegend - 2.55 \fi \fi \ifqfdshowcompetitive \ifqfdshowevallegend - 2.20 \fi \fi} + \pgfmathsetmacro{\qfdLegY}{\qfdHdrH - 0.4} + \begin{scope}[shift={(\qfdLegX, \qfdLegY)}] + \draw[qfdmed, rounded corners=2pt] (-0.15, 0.4) rectangle (4.5, \qfdLegBottom); + \node[anchor=west, font=\footnotesize\bfseries] at (0, 0.1) {\qfdRelTitle}; + \draw[qfdthin] (0, -0.15) -- (4.35, -0.15); + \node[qfdstrong] at (0.22, -0.5) {}; \node[anchor=west] at (0.5, -0.5) {Strong (9)}; + \node[qfdmod] at (0.22, -0.95) {}; \node[anchor=west] at (0.5, -0.95) {Medium (3)}; + \node[qfdweak] at (0.22, -1.4) {}; \node[anchor=west] at (0.5, -1.4) {Weak (1)}; + \ifqfdshowroof \ifqfdshowcorrlegend + \node[anchor=west, font=\footnotesize\bfseries] at (0, -2.10) {\qfdCorrTitle}; + \draw[qfdthin] (0, -2.35) -- (4.35, -2.35); + \node[anchor=west] at (0, -2.70) {{$+\!+$}\quad very positive}; + \node[anchor=west] at (0, -3.05) {{$+$\phantom{$+$}}\quad positive}; + \node[anchor=west] at (0, -3.40) {{$-$\phantom{$-$}}\quad negative}; + \node[anchor=west] at (0, -3.75) {{$-\!-$}\quad very negative}; + \fi \fi + \ifqfdshowcompetitive \ifqfdshowevallegend + \pgfmathsetmacro{\qfdEvalTop}{-2.10 \ifqfdshowroof\ifqfdshowcorrlegend - 2.55 \fi\fi} + \node[anchor=west, font=\footnotesize\bfseries] at (0, \qfdEvalTop) {\qfdEvalTitle}; + \pgfmathsetmacro{\qfdEvalSep}{\qfdEvalTop - 0.25} + \draw[qfdthin] (0, \qfdEvalSep) -- (4.35, \qfdEvalSep); + \pgfmathsetmacro{\qfdLegA}{\qfdEvalTop - 0.55} + \draw[qfdalt1ln] (0.05, \qfdLegA) -- (0.45, \qfdLegA); \node[qfdalt1mk] at (0.25, \qfdLegA) {}; \node[anchor=west, font=\bfseries] at (0.55, \qfdLegA) {\qfdAltOneLabel}; + \pgfmathsetmacro{\qfdLegB}{\qfdEvalTop - 0.95} + \draw[qfdalt2ln] (0.05, \qfdLegB) -- (0.45, \qfdLegB); \node[qfdalt2mk] at (0.25, \qfdLegB) {}; \node[anchor=west] at (0.55, \qfdLegB) {\qfdAltTwoLabel}; + \pgfmathsetmacro{\qfdLegC}{\qfdEvalTop - 1.35} + \draw[qfdalt3ln] (0.05, \qfdLegC) -- (0.45, \qfdLegC); \node[qfdalt3mk] at (0.25, \qfdLegC) {}; \node[anchor=west] at (0.55, \qfdLegC) {\qfdAltThreeLabel}; + \fi \fi + \end{scope} + \fi +} + +\newenvironment{qfdhouse}{% + \begin{tikzpicture}[x=1cm, y=1cm, font=\scriptsize, line cap=round, line join=round] + \ifqfdshowimportance + \pgfmathsetmacro{\qfdLeftEdge}{-\qfdWhatW-\qfdImpW} + \else + \pgfmathsetmacro{\qfdLeftEdge}{-\qfdWhatW} + \fi + \pgfmathsetmacro{\qfdApexY}{\qfdHdrH + \qfdNH/2} + \pgfmathtruncatemacro{\qfdNHm}{\qfdNH - 1} + \pgfmathtruncatemacro{\qfdNWm}{\qfdNW - 1} + \qfdDrawGrid + \qfdDrawRoof + \qfdDrawScale + \qfdDrawZoneTitles + \qfdDrawTitle +}{% + \qfdDrawFrames + \qfdDrawLegend + \end{tikzpicture}% +} + +\begin{document} + +\def\qfdNW{3} +\def\qfdNH{8} +\def\qfdWhatW{4.5} +\def\qfdHdrH{3.6} +\def\qfdImpTitle{Wt} +\def\qfdWhatsTitle{Goals} +\def\qfdProjectTitle{Remanso} +\def\qfdConcept{A \textbf{beautiful}, read-only notes web for \textbf{pride} and \textbf{peace}, with one-gesture \textbf{public} publishing.} +\qfdshowcompetitivefalse + +\begin{qfdhouse} + \pgfmathsetmacro{\qfdWhatTextW}{\qfdWhatW - 0.2} + \foreach \r/\t in {1/{G1 Beauty}, 2/{G2 Pride \& Peace (graceful on mobile)}, 3/{G3 Effortless public voice}} + \node[anchor=west, font=\scriptsize, text width=\qfdWhatTextW cm, align=left] at ({\qfdLeftEdge + 0.1}, {-\r + 0.5}) {\t}; + \foreach \r/\imp in {1/9, 2/10, 3/8} + \node[font=\scriptsize] at ({-\qfdImpW/2}, {-\r + 0.5}) {\imp}; + + \foreach \c/\t in {1/{F1 Render fidelity}, 2/{F2 Typography}, 3/{F3 No broken refs}, 4/{F4 Snapshot integrity}, 5/{F5 Offline-resilient}, 6/{F6 Calm surface}, 7/{F7 Reveal web}, 8/{F8 1-gesture publish}} + \node[rotate=90, anchor=west, font=\scriptsize] at ({\c - 0.5}, 0.15) {\t}; + + % Relations — G1 (row 1) + \node[qfdrel/S] at ({1-0.5},{-1+0.5}){}; \node[qfdrel/S] at ({2-0.5},{-1+0.5}){}; + \node[qfdrel/M] at ({3-0.5},{-1+0.5}){}; \node[qfdrel/W] at ({4-0.5},{-1+0.5}){}; + \node[qfdrel/W] at ({5-0.5},{-1+0.5}){}; \node[qfdrel/M] at ({6-0.5},{-1+0.5}){}; + \node[qfdrel/W] at ({7-0.5},{-1+0.5}){}; \node[qfdrel/W] at ({8-0.5},{-1+0.5}){}; + % G2 (row 2) + \node[qfdrel/M] at ({1-0.5},{-2+0.5}){}; \node[qfdrel/M] at ({2-0.5},{-2+0.5}){}; + \node[qfdrel/S] at ({3-0.5},{-2+0.5}){}; \node[qfdrel/S] at ({4-0.5},{-2+0.5}){}; + \node[qfdrel/S] at ({5-0.5},{-2+0.5}){}; \node[qfdrel/S] at ({6-0.5},{-2+0.5}){}; + \node[qfdrel/S] at ({7-0.5},{-2+0.5}){}; \node[qfdrel/M] at ({8-0.5},{-2+0.5}){}; + % G3 (row 3) + \node[qfdrel/M] at ({1-0.5},{-3+0.5}){}; \node[qfdrel/M] at ({2-0.5},{-3+0.5}){}; + \node[qfdrel/W] at ({3-0.5},{-3+0.5}){}; \node[qfdrel/W] at ({4-0.5},{-3+0.5}){}; + \node[qfdrel/W] at ({5-0.5},{-3+0.5}){}; \node[qfdrel/W] at ({6-0.5},{-3+0.5}){}; + \node[qfdrel/M] at ({7-0.5},{-3+0.5}){}; \node[qfdrel/S] at ({8-0.5},{-3+0.5}){}; + + % Roof correlations + \node[font=\scriptsize] at (C-1-2) {$+$}; + \node[font=\scriptsize] at (C-1-5) {$-\!-$}; + \node[font=\scriptsize] at (C-3-4) {$-$}; + \node[font=\scriptsize] at (C-3-5) {$+\!+$}; + \node[font=\scriptsize] at (C-2-6) {$-$}; + \node[font=\scriptsize] at (C-6-7) {$-$}; + \node[font=\scriptsize] at (C-2-8) {$+$}; + + % Basement: target / difficulty / abs / rel + \foreach \c/\tgt/\diff/\abs/\rel in {1/{all}/3/135/14, 2/{taste}/2/135/14, 3/{0 brk}/3/125/13, 4/{100\%}/3/107/11, 5/{offln}/4/107/11, 6/{calm}/2/125/13, 7/{on-dmd}/2/123/13, 8/{1-gest}/3/111/12} { + \node[font=\scriptsize] at ({\c - 0.5}, {-\qfdNW - 0.5}) {\tgt}; + \node[font=\scriptsize] at ({\c - 0.5}, {-\qfdNW - 1.5}) {\diff}; + \node[font=\scriptsize] at ({\c - 0.5}, {-\qfdNW - 2.5}) {\abs}; + \node[font=\scriptsize\bfseries] at ({\c - 0.5}, {-\qfdNW - 3.5}) {\rel}; + } +\end{qfdhouse} +\end{document} +``` + +Basement rows per function: **target / difficulty (1–5) / absolute weight / relative weight %**. + +--- + +## 1. Goals — the WHATs + +| ID | Goal | Weight | Source | +|-----|---------------------------------------------------------------------------------------|:------:|--------| +| G1 | **Beauty** — notes render gorgeously; the page is something you want to look at | 9 | Author intent (this session) | +| G2 | **Pride & Peace** — calm, trustworthy, quietly proud: nothing breaks, nothing misleads, nothing clutters; reads gracefully on a flaky mobile/metro connection | 10 | Author intent (this session) | +| G3 | **Effortless public voice** — lower *every* wall to making your voice public: one gesture (`.pub.md`), open decentralized rails, no forms | 8 | Author intent (this session) | + +## 2. Functions — the HOWs + +| ID | Function | Dir | Target (now) | Target (future) | +|-----|-------------------------------------------------------------------------|:---:|--------------|-----------------| +| F1 | **Render rich content faithfully** (math, code, diagrams, images, alerts, checkboxes) — for repo Notes *and* Published Notes | ↑ | every supported type renders correctly; failures degrade gracefully (never raw error dumps) | — | +| F2 | **Present beautiful typography & layout** (fonts, width, prose, theme, lightbox); Published Notes carry their own theme/fonts/language | ↑ | tasteful defaults + per-repo/per-note tuning | — | +| F3 | **Never show a broken live reference** (backlinks/images/notes resolve to current, or degrade gracefully) | ↓ | 0 visible broken refs in normal use; graceful placeholder on flaky network | 0 incl. edge cases | +| F4 | **Preserve Snapshot references immutably** (a shared SHA link renders the exact shared version; never silently substitutes current content) | → | 100% snapshot fidelity; on uncached miss, latest-cached + disclosure banner | — | +| F5 | **Serve instantly, offline & resilient on mobile/flaky networks** (PouchDB cache-first; no spinner anxiety in the metro) | ↓ | full offline read; cached render fast | cached render ≤ ~100 ms; survives mid-read connection loss | +| F6 | **Keep the surface calm & uncluttered** (reading-first, settings tucked away, the Remanso stillness) | → | minimal chrome; no nags/noise | — | +| F7 | **Reveal the web of connections** (backlinks + stacked notes) — *calm by default, on demand* | ↑ | every Note shows inbound links; stacking fluid; nothing flooded | — | +| F8 | **Publish in one gesture to open rails** (`.pub.md` → ATProto Published Note; no in-app form; decentralized, author-owned) | ↓ | publish = rename + push (1 gesture), 0 in-app steps | — | + +## 3. Cascade — Goals → Functions → How → Components + +- **G1 Beauty** _W:9_ + - **F1** Render rich content faithfully _Dir↑_ + - **How**: markdown-it pipeline with plugins; each renderer wrapped so a parse failure yields a styled fallback, not a stack trace + - **Component**: C1 `useMarkdown` + plugins (KaTeX, Shikiji, Mermaid, TikZ, GitHub alerts, checkboxes) + - **F2** Beautiful typography & layout _Dir↑_ + - **How**: DaisyUI theme + `@tailwindcss/typography` prose + per-repo `UserSettings`; Published Notes apply their stored theme/fonts + - **Component**: C2 Theme & typography system (themes, fonts/width, image lightbox) +- **G2 Pride & Peace** _W:10_ + - **F3** Never show a broken live reference _Dir↓ Target 0 broken_ + - **How**: a single bidirectional Path↔SHA index; resolve Live references by Path; fix the in-stack edit bug; graceful placeholder when a target is genuinely missing + - **Component**: C3 Reference resolver (Path↔SHA index over `store.files`) + - **F4** Preserve Snapshot references immutably _Dir→_ — see [ADR-0002](docs/adr/0002-two-reference-modes.md) + - **How**: keep SHA-addressed stack URLs; render the pinned version; on cache miss show honest "unavailable", never substitute current + - **Component**: C4 Snapshot pinning + unavailable state + - **F5** Serve instantly, offline & resilient _Dir↓ Target offline read_ + - **How**: cache-first reads from PouchDB via the worker; render from cache before/without network; tolerate mid-read connection loss + - **Component**: C5 PouchDB cache + `DataApi` worker; C9 Freshness/pull + - **F6** Keep the surface calm & uncluttered _Dir→_ + - **How**: reading-first Igarapé; settings/chrome tucked away; no nags + - **Component**: C6 Igarapé surface + calm chrome + - **F7** Reveal the web of connections — calm by default _Dir↑_ + - **How**: backlinks present but quiet; a Note opens into the stack only when its Backlink is summoned (progressive disclosure) + - **Component**: C7 Backlinks + Stacked Notes (reveal-on-demand) +- **G3 Effortless public voice** _W:8_ + - **F8** Publish in one gesture to open rails _Dir↓ friction_ + - **How**: `.pub.md` suffix → external/CI publish to ATProto as a Published Note; in-app surfaces read those records, styled like private notes + - **Component**: C8 Publish pipeline + ATProto read plumbing (`getUrl`, `withATProtoImages`, `Public*`→`Published*` views) + +## 4. House — Goals × Functions + +Cells: link strength (9/3/1/blank). Importance row = Σ(weight × strength). + +| | F1 | F2 | F3 | F4 | F5 | F6 | F7 | F8 | +|-----------------------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| G1 Beauty (9) | 9 | 9 | 3 | 1 | 1 | 3 | 1 | 1 | +| G2 Pride & Peace (10) | 3 | 3 | 9 | 9 | 9 | 9 | 9 | 3 | +| G3 Public voice (8) | 3 | 3 | 1 | 1 | 1 | 1 | 3 | 9 | +| **Σ** | 135 | 135 | 125 | 107 | 107 | 125 | 123 | 111 | + +**Top engineering priorities:** F1/F2 (rendering beauty) lead on raw Σ because all three goals touch them; F3 and F6 follow on the strength of Pride & Peace. **Caveat the matrix hides:** the *primary read context is mobile on a flaky metro connection*, which makes F3 (graceful) + F5 (offline-resilient) the **reliability spine to watch hardest** — a broken note in the metro is far more peace-destroying than its mid-pack Σ suggests. Treat F3+F5 as critical despite F5's rank (see §7). + +## 5. Roof — Function × Function tradeoffs + +`◎` strong reinforce · `○` mild reinforce · `×` mild conflict · `⊗` strong conflict. + +| | F1 | F2 | F3 | F4 | F5 | F6 | F7 | F8 | +|--------|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:| +| **F1** | — | ○ | | | ⊗ | | | | +| **F2** | | — | | | | × | | ○ | +| **F3** | | | — | × | ◎ | | | | +| **F4** | | | | — | | | | | +| **F5** | | | | | — | | | | +| **F6** | | | | | | — | × | | +| **F7** | | | | | | | — | | +| **F8** | | | | | | | | — | + +**Conflicts that actually shape the design:** +- **F1 ⊗ F5** (rich rendering vs instant/offline-on-mobile) — heavy renderers fight load speed and battery. Mitigation: lazy-load renderers, cache rendered HTML, render-from-cache first (T2). +- **F3 × F4** (never-broken vs immutable snapshot) — resolved by [ADR-0002](docs/adr/0002-two-reference-modes.md): pre-cache aggressively; on a true miss, show latest-cached + a disclosure banner — integrity via *disclosure, not refusal*, so the metro never dead-ends (T3). +- **F6 × F7** (calm vs the web) — resolved: **calm by default, reveal on demand** (T1). +- **F3 ◎ F5** (graceful refs reinforce offline resilience) — cache-first resolution makes both true at once; design them together. +- **F2 × F6** (customization vs clutter) — keep settings tucked away, defaults tasteful. + +## 6. Components & Function → Component map + +| ID | Component | ADR | +|-----|------------------------------------------------------------|----------| +| C1 | `useMarkdown` + markdown-it plugins (math/code/diagrams/…) | — | +| C2 | Theme & typography system (themes, fonts/width, lightbox) | — | +| C3 | Reference resolver — Path↔SHA index over `store.files` | ADR-0001 | +| C4 | Snapshot pinning + "unavailable" state | ADR-0002 | +| C5 | PouchDB cache + `DataApi` worker (cache-first reads) | — | +| C6 | Igarapé reading surface + calm chrome | — | +| C7 | Backlinks + Stacked Notes (reveal-on-demand) | — | +| C8 | Publish pipeline + ATProto read plumbing | — | +| C9 | Freshness / pull (external-commit detection) | ADR-0001 | + +| | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | +|-----|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:| +| F1 | 9 | 3 | | | 3 | | | 1 | | +| F2 | 1 | 9 | | | | 3 | | 1 | | +| F3 | | | 9 | 1 | 3 | | 3 | | 9 | +| F4 | | | 1 | 9 | 3 | | | | | +| F5 | | | 3 | 3 | 9 | | | | 3 | +| F6 | | 3 | | | | 9 | 3 | | | +| F7 | | | 3 | | | 3 | 9 | | | +| F8 | | 1 | | | | | | 9 | | + +## 7. Critical performance budget + +| Rank | Function | Target | Watched on | If we miss it | +|------|----------|--------|------------|---------------| +| 1 | F3 (never broken) | 0 visible broken live refs | manual mobile run + a "broken-ref" dev assertion; fix the StackedNote `saveCacheNote` path bug | render a calm placeholder ("note unavailable"), never a stack trace or empty column | +| 2 | F5 (offline/mobile) | full offline read; survive mid-read connection loss | DevTools offline/slow-3G throttling on the stacked-notes flow | cache-first always; if uncached + offline, honest "not downloaded yet" placeholder | +| 3 | F1 (render fidelity) | all content types render; failures degrade | snapshot-render a fixture note (math/code/mermaid/tikz/alerts) | per-block styled fallback; the rest of the note still renders | +| 4 | F4 (snapshot integrity) | 100% pinned-version fidelity | test: edit a Note, re-open an old `?stackedNotes=sha` link | show pinned version; on a true miss, latest-cached + disclosure banner — never *silent* substitution (ADR-0002) | +| 5 | F8 (publish friction) | 1 gesture, 0 in-app steps | end-to-end: `.pub.md` push → appears in `/notes` | surface the failure plainly; do not require an in-app fallback form (would re-raise the wall) | + +## 8. Tradeoffs — Got / Paid / ADR + +| ID | Tradeoff | Got | Paid | ADR | +|-----|----------|-----|------|-----| +| T1 | Calm-by-default over web-forward (F6 > F7 prominence) | Stillness; the Remanso peace | Connections less immediately visible (mitigated by reveal-on-demand) | — | +| T2 | Lazy-load + cache rendered HTML (F1 ⊗ F5) | Fidelity *and* instant/offline render | Cache complexity; possible stale-render window | — | +| T3 | Integrity via disclosure, not refusal (F3 + F4 in the unavailable edge) | No silent rewrite *and* no metro dead-end: latest-cached + a banner | A banner caveat instead of a guaranteed exact view when uncached | [ADR-0002](docs/adr/0002-two-reference-modes.md) | +| T4 | Filename-suffix publishing (`.pub.md`) over in-app publish UI (F8) | Near-zero friction; publishing is a gesture, not a feature | Less in-app preview/control; coupling to a naming convention | — | + +### Tensions being watched (unresolved by design) + +- **Rename/move changes a Note's Path → its Live references break.** Under ADR-0001 that's a new identity; we accept it for now. **Trigger to revisit:** if reorganizing notes becomes common enough that broken backlinks hurt Peace, add git-rename following. +- **F1 ⊗ F5 residual cost on low-end mobile.** Even lazy + cached, very heavy notes (large TikZ/Mermaid) may stutter in the metro. **Trigger to revisit:** if real mobile runs show jank, pre-render heavy blocks to images at publish/cache time. + +## 9. Inconsistencies spotted and fixed + +- **In-stack edit bug:** `StackedNote.performSave()` called `saveCacheNote()` without `path`, so `store.files` never got the new SHA after an in-Igarapé edit (`FleetingNotes` passed it correctly). Broke F3. **Fixed** — `StackedNote.vue` now passes `path`. +- **Asymmetric file index:** `addFile` deduped by SHA only, `registerUploadedFile` by PATH — so an edit (same path, new SHA) left a stale duplicate entry. **Fixed** — `addFile` now dedups by SHA *and* path (a path is unique), with a regression test in `userRepo.store.spec.ts`. (Full bidirectional index C3 still the longer-term direction.) +- **ADR-0001 over-absolute:** it called SHA-keyed references "a fragility". **Refined** by ADR-0002 into two intentional modes (Live/Snapshot). +- **`CLAUDE.md` "edit history tracking"** mislabels the visited-repos History; no edit-history feature exists. (Flagged in CONTEXT.md.) +- **Naming drift carried from the language session:** `Flux`→`Igarapé`, `HistoricNotes`→`Index`, `Repetition`→`Card`, `PublicNote*`→`PublishedNote*`, `useNotes()` returns Files not Notes. (Tracked in CONTEXT.md "Flagged ambiguities".) + +--- + +## How to keep this honest + +- When a new ADR lands → add its components to §6 and re-score affected rows. +- When a spike / measurement returns numbers → update §7 `Target` / `Watched on`. +- WHATs change rarely; HOWs change with each release; matrices are recomputed when either side changes. +- If a section becomes empty after edits, delete it — empty sections lie. diff --git a/docs/adr/0001-note-identity-is-path.md b/docs/adr/0001-note-identity-is-path.md new file mode 100644 index 0000000..8828cfd --- /dev/null +++ b/docs/adr/0001-note-identity-is-path.md @@ -0,0 +1,15 @@ +# Note identity is Path, not SHA + +A **Note**'s stable identity is its **Path** (its location in the repo, e.g. `ideas/zettel.md`), not its content **SHA**. We decided this because authors link to Notes by path (markdown `[[…]]` resolved via `resolvePath()`) and expect those links to survive edits — whereas a Note's SHA changes on every edit. + +This is surprising in the current code, which keys **Backlinks** (`Backlink { sha }`) and the stacked-notes URL (`?stackedNotes=sha1;sha2`) by SHA. Those are runtime *handles* that must resolve *through* the Path; if a SHA-handle ever disagrees with the Path, the Path wins. + +## Considered options + +- **Path as identity (chosen).** Survives edits; matches how authors write links. Cost: needs a resolution layer mapping Path → current SHA for lookup/navigation, and the SHA-keyed code is a known fragility to reconcile. +- **SHA as identity (rejected).** Matches the current code literally and needs no resolution layer, but every edit produces a new identity — backlinks and open-note URLs would break on edit. Unacceptable for a notes app where editing is routine. + +## Consequences + +- Backlinks and stacked-note navigation should ultimately be expressed in terms of Path; SHA-keying stays only as an internal lookup handle. +- Caching by SHA remains valid (it is content-addressed versioning), but Note identity must never be conflated with it. diff --git a/docs/adr/0002-two-reference-modes.md b/docs/adr/0002-two-reference-modes.md new file mode 100644 index 0000000..0a8d10b --- /dev/null +++ b/docs/adr/0002-two-reference-modes.md @@ -0,0 +1,20 @@ +--- +status: accepted (refines ADR-0001) +--- + +# Two reference modes: Live (Path) and Snapshot (SHA) + +A reference to a Note resolves in one of two deliberate modes. A **Live reference** (by **Path**) resolves to the Note's *current* content — this is a Backlink you follow in your own repo, and it reflects edits (Note identity = Path, per ADR-0001). A **Snapshot reference** (by **SHA**) resolves to that *exact, immutable* version — this is a shared or bookmarked stack link (`?stackedNotes=sha`), content-addressed so what was shared cannot change underneath a reader. + +This refines ADR-0001, which framed SHA-keyed references as "a fragility." That framing was too absolute: SHA-pinning is the **correct** tool for a Snapshot reference. Pinning is an *integrity* feature — it guarantees a shared view can't be silently rewritten, so no reader is misled and no author is misrepresented. + +## Considered options + +- **Two explicit modes (chosen).** Live=Path, Snapshot=SHA, each with a clear purpose. Cost: the resolver must support both, and the UI must make the mode legible (you should know whether you're reading "now" or "as shared"). +- **Single mode, Path only (rejected).** Simpler, but a shared link would always re-resolve to current content — destroying the integrity guarantee that makes sharing safe. +- **Single mode, SHA only (rejected by ADR-0001).** In-repo navigation would break on every edit. + +## Consequences + +- The system **pre-caches** aggressively (content is cached by both SHA and Path on every fetch / freshness pull) so the pinned version is usually present, including offline. +- When a Snapshot reference's pinned content is genuinely unavailable (never-fetched + offline — common on a flaky mobile connection), the system **falls back to the most up-to-date cached version and shows a banner** disclosing "this is the latest available, not the exact shared version." Integrity is preserved by **disclosure, not refusal**: the reader is never silently shown different content, but is also never dead-ended on the metro. (This chooses graceful continuity over a hard "unavailable" stop, given mobile is the primary read context.)