# Addressed Notes reads

**Specification · addressed snapshot reads**

This page defines the read contract and its acceptance criteria. It is the design
reference; the [Notes guide](/notes/) and distributed agent skills describe how to
use it. Guides may use different explanations, but must preserve these semantics.

## The two read modes

| Request | Meaning | Result |
| --- | --- | --- |
| `read NOTE` | Observe current state | Complete source, content hash, write revision |
| `read NOTE --at REV` | Retrieve retained state | The same snapshot every time, while retained and accessible |
| `read NOTE --since HASH` | Compare prior content with current content | Unified line diff, base hash, result hash and revision |

`--toc`, `--section` and `--tag` select a snapshot view. They never change the
meaning of `--since`. `--linger` delivers an initial current snapshot and then
line diffs through its existing SSE transport.

## Actions the representation must support

| Assistant action | Required information |
| --- | --- |
| Orient within an article | Full heading outline with levels and source-derived section IDs |
| Read a section or subsection | Semantic nested section subtree, including child sections |
| Read one paragraph or heading | Exact element ID, source slice, global string and line ranges |
| Generate an edit | Original source spelling, code-point offsets and the original write revision |
| Verify an edit | Read the revision in the acknowledged write receipt |
| Continue during collaboration | Keep the draft baseline separate from newer streamed observations |

## Normative snapshot contract

There are two read modes. `read` returns the current snapshot; `--at REVISION`
returns the retained snapshot identified by that opaque revision. `--since HASH`
returns a unified line diff against the current source. A hash is a content
reference, not a write revision. Snapshot views do not change differential reads.
Explicit `--format inline-dff` remains available for character diffs; patch input
continues to default to `inline-dff`.

```bash
# NOTE_ID identifies a note you can read. Plain reads show hash and revision.
dreamlake notes read "$NOTE_ID"
dreamlake notes read "$NOTE_ID" --view html
# Copy REVISION from the read receipt; keep it with the draft it describes.
dreamlake notes read "$NOTE_ID" --at "$REVISION" --toc
dreamlake notes read "$NOTE_ID" --at "$REVISION" --section s1.1
dreamlake notes read "$NOTE_ID" --at "$REVISION" --tag s1.1.p1
# HASH is the content hash from that receipt. This returns line diffs.
dreamlake notes read "$NOTE_ID" --since "$HASH"
```

TOC, section and tag selectors return mapped HTML without needing `--view html`.
They are mutually exclusive and cannot combine with `--since`. `--tag` is an exact
generated element ID, not an arbitrary CSS selector. A paragraph ID selects one
paragraph; a section's slug ID selects the entire nested section. `--section`
accepts a numeric section index or its content-derived ID. `--at` cannot combine
with `--if-match`: the former retrieves history, the latter requires the current
revision to match. Missing snapshots or elements return 404. Every read, including
cached historical reads, rechecks current note permissions.

### Elements, identifiers and ranges

Snapshot HTML uses real nested `section` elements and `h1`–`h6` headings:

```html
<section id="section-plan" data-index="s1" data-char="0:90" data-lines="1:11">
  <h1 id="s1.h" data-index="s1" data-char="0:7" data-lines="1:1">Plan</h1>
  <p id="s1.p1" data-char="8:25" data-lines="3:3">We build robots.</p>
  <section id="section-training" data-index="s1.1" data-char="26:56" data-lines="5:8">
    <h2 id="s1.1.h" data-index="s1.1" data-char="26:38" data-lines="5:5">Training</h2>
    <p id="s1.1.p1" data-char="39:55" data-lines="7:7">Train 😀 safely.</p>
  </section>
</section>
```

This abbreviated example omits mapping spans and later section content. Source
ranges count Unicode code points, start at zero, and exclude the end. Line ranges
are one-based and include both endpoints. Ranges refer to original source,
including Markdown syntax and line terminators, never rendered text. Section
ranges include subsections. Heading ranges cover only the heading. Paragraphs
are numbered within their nearest section; lists, quotes, code and tables do not
advance the prose counter. Preamble paragraphs use `s0.pN`. Skipped heading levels
add a nesting level, without invented empty headings; sibling numbering remains
unique when heading levels change. IDs and ranges are local to one revision.

Section slugs follow the existing editor convention: NFC normalization, lowercase,
remove characters other than Unicode letters/numbers, whitespace, `_` and `-`,
then collapse whitespace to hyphens. Prefix with `section-`; an empty slug uses
`untitled`. Duplicate slugs get `-1`, `-2`, etc., in document order. No truncation
is applied. Reverse lookup uses the element's exact source range, not reversal of
the lossy slug. Headings use `sN.h`; paragraphs use `sN.pN`.

### List items and Markdown hints

List items use section-local IDs in document order: `s1.li1`, `s1.li2`,
and so on. Nested items get their own IDs; a parent item's range includes its
nested list. Ordered and task-list items use the same rule. Markers, indentation
and line endings belong to the original source range. Lists do not advance the
paragraph counter. Preamble items use `s0.liN`.

```html
<li id="s1.li1" data-char="11:29" data-lines="3:4">
  😀 One
  <ul><li id="s1.li2" data-char="20:29" data-lines="4:4">Two</li></ul>
</li>
```

Use `read NOTE --tag s1.li2` to select a single item. For Markdown notes,
`--view markdown` prints the original selected Markdown with generated address
comments before headings, paragraphs and list items. It uses the same validated
HTML snapshot and supports `--at`, `--toc`, `--section`, or `--tag`:

```bash
dreamlake notes read "$NOTE_ID" --at "$REVISION" --section s1 --view markdown
```

```markdown
<!-- s1.li1 chars=11:29 lines=3:4 -->
- 😀 One
<!-- s1.li2 chars=20:29 lines=4:4 -->
  - Two
```

This is a reading view, not canonical source. Its header identifies the revision
and hash; ranges always refer to original source, excluding generated hints.
Do not write the annotated output back as article content. Default source reads
stay exact and unannotated. `--since` and `--linger` retain their existing source
contracts; they cannot combine with the annotated view. HTML-source notes use
`--view html` instead.

### Scoped source and edit commands

A scoped root retains the whole document's `data-hash` and `data-revision`, and
adds `data-scope`, `data-source-start`, `data-source-end` and `data-source-hash`.
Its `data-source` contains only the selected source slice, not the entire note.
For a mapped element, subtract `data-source-start` when slicing this local source;
retain absolute offsets in edit commands. A TOC has empty root source, with exact
heading source attached to each heading's `data-source`. It does not imply the
assistant read omitted section bodies. Full HTML keeps the existing full-source
contract. The CLI checks the returned scope, revision and source hash before
printing. Never write the HTML or a partial source slice as the complete note.

For the fixture paragraph above, a directly generated patch is:

```text
@@ chars 39:55 @@
~ [-Train 😀 safely.\n-]{+Train safely and quickly.\n+}
```

```bash
# edit.dff contains the patch prepared against REVISION, with exact old text.
dreamlake notes patch "$NOTE_ID" --file edit.dff --base-revision "$REVISION" --exact
# Copy NEXT_REVISION from the write receipt; read that exact acknowledged state.
dreamlake notes read "$NOTE_ID" --at "$NEXT_REVISION" --tag s1.1.p1
```

Exact mode refuses concurrent changes with 412. Without `--exact`, the existing
native RTC merge mode uses the original baseline identities. A later read or
streamed revision must never silently replace the baseline of a prepared draft.

### Linger and presence

`--linger` retains its existing contract: an initial source snapshot, then
SSE-driven, debounced line diffs. It does not stream HTML or scoped fragments.
Read a focused view in a separate command using a revision emitted by linger:
`read NOTE --at REVISION --tag s1.1.p1`. Pinned reads do not overwrite the live
presence selection with historical offsets. Live scoped reads report their
actual range; a TOC reports no whole-document passage highlight. Selection events
carry their own content hash and must not be interpreted against a different
source. `--linger` rejects `--at`, `--toc`, `--tag`, sections and HTML before joining.

### Server retrieval

On the server, a pinned read performs permission checks and a direct retained
baseline lookup, without initializing or reading RTC and without uploading new
snapshots. Rendering shares a bounded content cache (16 entries, 8 MiB estimated
payload budget); a cold read still loads and parses the complete source once.
Current reads retain the existing coherent RTC observation and persistence path.
No claim of constant-time cold reads or eliminated live-read storage costs is made.

## Acceptance evidence

All tests below run against isolated fixtures. The HTTP harness launches the real
CLI and the real Fastify routes on localhost, with fixture storage and RTC
authority. It is not production or a live hosted RTC test.

| Requirement | Verified behavior |
| --- | --- |
| Efficient retrieval | Pinned read bypasses RTC initialization, RTC observation and snapshot uploads. Scope rendering shares a bounded cache. |
| Repeated reads are consistent | Outline and paragraph read at the same revision remain identical despite an intervening edit. Old snapshots remain readable after a later write. |
| Editing is direct | Paragraph offsets and exact source generate a patch accepted by the existing edit endpoint; unrelated source stays byte-for-byte unchanged. |
| Stale writes fail | Exact-mode edit against an intervening change returns 412, without applying the draft. |
| Read scopes are clear | Whole article, TOC, nested section and one paragraph are separately verified; scoped reads do not include unrelated paragraph source. |
| Linger remains compatible | Real CLI receives snapshot B and line diff B→C; `--at B --tag …` still returns B. SIGINT sends leave. |
| Authorization persists | Revoked note access prevents historical retrieval, even with a valid retained token. |
| Offset integrity | Emoji, CRLF, formatting, skipped heading levels, duplicate titles and repeated projections are tested. |

### Measured example

A local fixture containing 100 sections and 400 paragraphs produced these results:

| View | Response size | Local rendering time |
| --- | --- | --- |
| Entire article | 586 KB | 39 ms, cold |
| TOC | 35.6 KB | 1.8 ms, warm median |
| One section | 6.9 KB | 1.8 ms, warm median |
| One paragraph | 2.4 KB | 1.7 ms, warm median |

Source size: 237 KB. Warm measurements use 20 iterations. Timings exclude network,
permission checks, and object-storage latency; they are not a service SLA.
A cold read still loads and parses the complete source. Current reads retain the
existing RTC observation and persistence cost. Historical reads load and validate
a retained baseline by direct object key, without listing history.

### Reproduce

From a workspace checkout containing `dreamlake-server`, set `CLI_SOURCE` to the
companion CLI checkout and `ARTIFACTS` to an output directory:

```bash
NOTES_SCOPED_CLI="$CLI_SOURCE" NOTES_READ_ARTIFACTS="$ARTIFACTS" \
  pnpm -C dreamlake-server exec vitest run --project unit \
  src/services/notesV2Routes.test.ts src/services/notesReadBaseline.test.ts src/lib/noteHtml
pnpm -C dreamlake-server exec tsx scripts/benchmark-notes-read.ts "$ARTIFACTS"
```

The harness emits article, outline, section and paragraph HTML; the generated
patch; the write receipt and exact readback; the linger event stream; and a JSON
report. The benchmark emits `benchmark.json`. No real notes are modified.
