Design goals
Portable
No absolute paths. A sidecar checked into a repo works on any machine and any clone.
Verifiable
Content fingerprints (SHA-256, byte size) let a consumer detect that the source or PDF changed since mapping ran — without re-running the matcher.
Layered
Each item separates what came from the PDF (immutable), what the matcher computed (a cache), and what the user owns (review state).
Evolvable
A top-level integer version and a $schema URL, so consumers can branch on format and validate.
Top-level shape
A session is metadata + two file references + an array of items.
{
"$schema": "https://volland.github.io/eddie-doc/schema/review-v2.schema.json",
"version": 2,
"producer": { "name": "eddie-doc", "version": "0.1.5" },
"createdAt": "2026-07-03T07:15:17.937Z",
"updatedAt": "2026-07-12T09:02:00.000Z",
"source": { "path": "chapter-01.adoc", "sha256": "…", "bytes": 4213 },
"pdf": { "path": "chapter-01.annotated.pdf", "sha256": "…", "annotationCount": 5 },
"items": [ /* … */ ]
}
| Field | Req | Meaning |
|---|---|---|
$schema | – | Format identifier; also the canonical schema URL. |
version | ✓ | On-disk format version. 2 for this spec. |
producer | – | Tool that wrote the file (name, optional version). |
createdAt / updatedAt | ✓ | ISO-8601 timestamps. |
source | ✓ | The .adoc input — a file reference. |
pdf | ✓ | The annotated PDF input, plus annotationCount. |
items | ✓ | The review items. |
File references
Both source and pdf are file references:
| Field | Meaning |
|---|---|
path | Relative to the sidecar's own directory, always with POSIX (/) separators, even on Windows. |
sha256 | Hex SHA-256 of the file's bytes at the time mapping last ran. Optional — a migrated v1 session has none until it re-maps. |
bytes | Size at map time. pdf additionally carries annotationCount, a cheap staleness signal. |
Items
Each item is a PDF annotation, its match into source, and its review state — in three separate blocks so they evolve and diff independently.
{
"id": "p2-highlight-48-719",
"annotation": {
"kind": "highlight",
"author": "Editor",
"comment": "Strong framing — keep this.",
"anchoredText": "…relationships at the center of the model.",
"markedText": "relationships at the center of the model",
"geometry": { "page": 2, "unit": "pt", "origin": "bottom-left",
"rect": [48.24, 718.58, 547.04, 748.64] }
},
"match": { "startLine": 6, "endLine": 7, "score": 0.887, "method": "fuzzy" },
"state": { "resolved": false }
}
id
A stable identifier that survives re-import so review state can be reattached. Producers should prefer the PDF's own annotation id (NM) when available, since geometry-derived ids change if the PDF is re-exported at a different position.
annotation — from the PDF (immutable)
| Field | Req | Meaning |
|---|---|---|
kind | ✓ | highlight, strikeout, underline, comment, insert, replace, other. |
author | – | Annotation author, if the PDF records it. |
comment | – | The editor's comment body. |
anchoredText | – | Text physically under the markup. Over-captures the whole line for robust matching. |
markedText | – | The tightly-bounded text inside the markup quads — used for character-precise delete/replace. |
beforeText | – | For insert (caret) marks: text left of the caret on the same line, to place the insertion at the exact offset. |
geometry | ✓ | Position in the PDF — page (1-based), unit: "pt", origin: "bottom-left", rect: [x0,y0,x1,y1]. Optional reserved quadPoints. |
match — computed (a cache, null when unmatched)
The matcher's best mapping onto the source. It is recomputable — running Re-map rebuilds it — so a consumer must treat it as a cache, not ground truth.
| Field | Req | Meaning |
|---|---|---|
startLine / endLine | ✓ | 0-based, inclusive, line range in the source. |
score | ✓ | 0–1 similarity of the matched span. |
method | – | "fuzzy" (token/bigram) or "semantic" (embedding fallback). |
sourceExcerpt | – | A snapshot of the matched source text for display/debug only. Goes stale the moment the line is edited — never authoritative. |
state — user-owned (the only hand-edited block)
| Field | Req | Meaning |
|---|---|---|
resolved | ✓ | The reviewer has handled this item. |
confirmed | – | The user vouched for the link (hand-picked, or accepted a low-confidence/semantic auto-match). Keeps it out of Needs review. |
manualLine | – | 0-based line the user manually linked to; overrides match. |
note | – | Free-form reviewer note. |
match.startLine,
state.manualLine). Editors that display 1-based line numbers add 1 for presentation.
The effective line of an item is manualLine when set, otherwise
match.startLine, otherwise unmatched.
Validate a sidecar
Any JSON Schema (draft 2020-12) validator works.
npx ajv-cli validate --spec=draft2020 --strict=false \
-s schema/review-v2.schema.json -d path/to/chapter-01.review.json
Or reference the published schema directly from your tooling at
https://volland.github.io/eddie-doc/schema/review-v2.schema.json.
Compatibility & migration
- Reading: a consumer branches on
version. Eddie Doc reads both1and2. Unknown top-level fields are ignored, not rejected. - v1 → v2: version 1 was a flat shape with absolute paths and item fields at the top level. Eddie Doc migrates it transparently on load; the next write upgrades the file to v2. No user action required.
- Forward changes: additive fields do not bump
version. A breaking change bumps it to3.
Relationship to the W3C Web Annotation model
This format is intentionally close to the
W3C Web Annotation Data Model:
an item is an annotation whose body is annotation.comment and whose
target is the PDF (via geometry) plus the source (via match/state).
The mapping is mechanical — anchoredText → TextQuoteSelector,
match.startLine → TextPositionSelector,
geometry → a quad FragmentSelector — leaving a clean path to emit
standards-compliant JSON-LD in a future version without reworking the data captured here.