manni:citations:1.0.0-proposal.4
The question it answers: what do this page’s claims rest on? A common vocabulary for pinning a sentence to the source lines it was written from. Any drift check can implement it. Hash the cited lines the same way, compare against the pin, and report the sentence that stopped being true.
One page-level key, citations. The citation- prefix is reserved, so any
citation-* key at the page root is rejected.
| Key | Type | Location | Notes |
|---|---|---|---|
citations |
non-empty list of entries | external |
One entry per claim or pin. A page with no citations omits the key |
The Location column is the x-manni-location mark the key carries in
manni:citations:1.0.0-proposal.4. page means the value belongs in the
document’s own metadata and is meant to reach delivered output. external
means it belongs in the collection’s external-metadata manifest. A key is
page when a third-party or user-owned agent fetching the page would act on
it. Citations are CI’s: a drift check reads them, and an agent reading the
page does not. The fields inside an entry carry no mark of their own. See
field location for how the marks are
used.
An entry is a closed object with one required block, source. The optional
claim block is the other end, and both ends carry a line range and a hash:
| Field | Type | Notes |
|---|---|---|
id |
kebab-case, unique per page | What a body marker names, and what a finding is reported under. Required when a marker names the entry |
claim.lines |
L or "L1-L2" |
Lines of the page body, counted after the frontmatter, so editing the frontmatter never moves a claim. Required unless a marker names the entry |
claim.integrity |
sha256- and 64 hex digits |
The claimed lines hashed under the one rule. Always plain, because the page is public |
source.file |
source file | A repo-root-relative posix path. In place of the path, ~ and at least 82 base64url characters is an encrypted source: the path, encrypted under the family encryption key |
source.lines |
L or "L1-L2" |
File lines, 1-based and inclusive. Absent, the whole file is pinned. Readable even when file is encrypted |
source.integrity |
sha256- or hmac-sha256-, and 64 hex digits |
The pin over the cited lines. hmac-sha256- exactly when file is encrypted |
source.commit-sha |
7 to 64 hex digits | The commit the source pin was taken at. Tooling writes the full hash |
quote |
boolean, default false | The claim is a fenced block that reproduces the source lines |
An entry with no claim and no marker is a bare pin, checked source-side
only. The source.file grammar rejects an absolute path, a drive letter, a
backslash, a . or .. segment, an empty segment and a URL. So a source that
cannot be resolved fails at the schema. Spaces and dots inside a segment are
fine: docs/release notes/v1.2.md is a file. That the end line is not before
the start line is a rule on implementations, since a pattern cannot compare
two numbers.
The hashing rule is stated once, in the schema’s source.integrity
description, and applies to both ends. Decode as UTF-8, drop one leading
byte-order mark, and turn CRLF into LF. Split on LF and discard the empty
element a trailing LF leaves. Take the cited lines, join them with LF and no
trailing LF, and keep trailing whitespace. A plain file and every claim are
pinned sha256- over that text. An encrypted file is pinned hmac-sha256-,
the HMAC-SHA256 of that text under a key derived from the family encryption
key.
Proposal 0045
states the construction of both the ciphertext and the keyed pin.
Markers
Section titled “Markers”A citation can be anchored from the body by a marker. A marker is one comment
naming an entry by id, written in the format’s own comment syntax. It anchors
the rest of its own line when that carries text, else the paragraph that
follows it. With quote: true it anchors the fenced block that follows it. The payload is an id
and nothing else. The entry itself lives in the frontmatter or a sidecar
manifest, never in the body.
| Format | Forms |
|---|---|
| markdown, mdx | <!-- cite ID -->, {/* cite ID */}, [comment]: # (cite ID) |
| html, xml | <!-- cite ID --> |
| asciidoc | // (cite ID) |
| rst | .. (cite ID) |
A marker naming no entry is a finding, and so are two markers naming the same id. Metadata validation sees the entries only; an implementer of the drift check reads the markers itself.
Examples
Section titled “Examples”One sentence on body line 3, pinned to one source line:
citations: - id: fetch-timeout claim: lines: 3 integrity: sha256-c41f09aa8ba1a04a3d5a0a0dbb1d9c1e8e9e26a1c5f0b2dd4e9a0c17f4f19aab source: file: src/limits.ts lines: 2 integrity: sha256-78af1d3321f9cbb177a7e4c958e39be56fd14cb93c1e441778bc4232e0fe4b1f commit-sha: 3f9c2a1e7b0d4c5a6f8e9d0b1a2c3d4e5f607182A bare pin says the page rests on a whole file:
citations: - source: file: src/limits.ts integrity: sha256-…A marker anchors an entry to a paragraph, in markdown and in MDX:
<!-- cite retries -->Retries default to 3, and the count is not configurable.{/* cite retries */}Retries default to 3, and the count is not configurable.An encrypted entry, citing a private source from a public page. The ciphertext replaces the path, and the pin is keyed by the same family encryption key. So the page carries neither the path nor a verifier for the line:
citations: - id: retries quote: true source: file: "~AQ…" lines: 1-3 integrity: hmac-sha256-…Design decisions
Section titled “Design decisions”- A pin is a record of the past. The family’s fourth principle says a
derivable fact lies, and a hash of lines that sit right there looks
derivable. It is not. The pin says what the lines were when the sentence was
written; the check is the comparison between that record and the file now.
Recompute it on every run and there is nothing to compare against. It is the
same object as evals’
generated-assertion-hashand theintegritypins on a config’s schema entries. integrity, with an algorithm prefix. Named after what it is rather than how it is computed, as the config’s schema pins and Subresource Integrity are. So a second algorithm can arrive without a second field. Hex rather than base64, because the family’s other hash is hex.- One normalization rule, up front. CRLF and a byte-order mark are not changes. A check that runs on every push cannot afford a page of false findings on one platform. Trailing whitespace is kept, because a trailing space is a change.
- An encrypted path, under the family’s key. A checkout with the family encryption key and the files can decrypt the path and read the lines, so it needs no second secret. The encryption is deterministic, so one path under one key always gives one ciphertext. A public site therefore does reveal how often a private file is cited; that is recorded as accepted.
- A keyed pin for an encrypted source. A plain hash over a short private
line is a verifier a reader can run against guesses. A keyed pin closes
that, at the cost that only a checkout with the key can check the pin. It
reads
hmac-sha256-, so the prefix says what the value is. - The claim is pinned, not copied. A claim is a line range and a hash, the same shape as the source end. A pinned range cannot be ambiguous on the page it was taken from, and a copied sentence could be.
- Entries in one place, markers as anchors. An entry lives in the frontmatter or a sidecar manifest, and a marker only names it. So a source is never written into the body, and a public page never carries a private path twice.
- A closed entry and a guarded prefix. The entry rejects unknown fields,
so
line: 2besidefilefails instead of being ignored. The page root stays open for sibling vocabularies, and thecitation-prefix is reserved so a misspelledcitation-comitfails the way a closed container would make it.
What reviewers should poke at
Section titled “What reviewers should poke at”Three questions. Is one hash function in the integrity pattern too tight for
a draft, given that adding one is a schema revision? Should the ciphertext differ
from page to page? The cost is a drift check that no longer recognises one
private file across two pages. And are the parenthesised marker
forms worth keeping?