Files
paperspec/spec/06-anchors.md
T
alexmandClaude a09d158e16 feat: minted keys, VRF seeds, RFC 9162 proofs, history roots, tag URIs and a transparency log
Keys (spec/02 §2.4), the rule paperstack settled on:
- A work key is a Scholar-style citation key, a hyphen and the first four
  Crockford base32 characters of SHA-256("scienting-key/1:" + identity),
  where the identity is the first of doi, arxiv, isbn, pmid, pmcid and
  openalex in same_as, else the folded title, year and family name. A
  collision lengthens the later key by two characters; a key is minted
  once and never recomputed. A document key adds .si, .v<seq> or .<type>
  for versions other than the version of record.
- Every fixture key is now minted (hale2026harmonic-damb,
  okafor2025solvent-acth, writer2027fixed-ynr7.v3, tiny2026tiny-j4qm), so
  every node and record ID was re-minted and the docs rewritten to match.
  The validator checks the document key against the work key and version
  (doc.key.work) and the hash part against the identifier (doc.key.suffix).
  vectors/keys.jsonl pins the cases paperstack's tests pin.

Review:
- Spot-check seeds are ECVRF-EDWARDS25519-SHA512-TAI proofs (RFC 9381),
  implemented in tools/oracle/vrf.py in plain Python and reproduced
  against the RFC's Example 16; the sample ranks by the VRF output. The
  earlier Ed25519 seed could be ground by a dishonest checker.
- RFC 9162 inclusion and consistency proofs (crypto.inclusion_path,
  verify_inclusion, consistency_proof, verify_consistency) with
  scienting-proof/1, vectors/proofs.jsonl and two fixture records.
- document.json carries `history`, the root over the corrections, with
  the empty-tree hash for none (spec/11 §11.5, rule doc.history).
- A provisional transparency log (spec/23): log entries, signed-note
  checkpoints with key hashes as transparency logs share them, the rule
  that a logged revocation beats a self-asserted time, and vectors.
- Reserved signature schemes cose and ml-dsa-65 (spec/08 §8.4).

Identifiers and text:
- urn:scienting: becomes tag:scient.ing,2026: (RFC 4151), since the URN
  namespace was never registered.
- Unicode 16.0 is pinned for normalization and categories (spec/01 §1.4);
  the checker refuses another version.

Checking:
- check.py: keys, proofs, VRF and checkpoint vectors; the proof and
  checkpoint records; a check that every ID and hash the prose quotes
  exists in the fixtures or vectors; three more broken packages
  (history-wrong, key-suffix, key-work).
- stress.py: key, VRF, proof and checkpoint properties.
- Docs: a keys section in the concepts and FAQ, rationale entries for
  keys, VRF, proofs, tag URIs and the Unicode pin, a "Looking ahead"
  section, media-type registration and OCI distribution notes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-14 23:21:57 -04:00

7.8 KiB
Raw Blame History

6. Addresses and anchors

6.1 Addresses

An address names a node, optionally pinned to a hash and narrowed to a span:

address  = [store "/"] key "#" node ["@" pin] ["[" start ":" end "]"]
store    = lowalnum *(lowalnum / "." / "_" / "-")
key      = workkey *("." lowalnum *(lowalnum / "-"))   ; a document key (2.4)
workkey  = LOWER *lowalnum "-" 4*51BASE32
node     = lowalnum *(lowalnum / "." / "_" / "-")      ; a node ID or an alias
pin      = 6HEXLOWER                ; the first six digits of a node_hash
start    = "0" / (%x31-39 *DIGIT)
end      = "0" / (%x31-39 *DIGIT)
lowalnum = LOWER / DIGIT
LOWER    = %x61-7A
BASE32   = DIGIT / %x61-68 / %x6A-6B / %x6D-6E / %x70-74 / %x76-7A   ; Crockford, lowercase

start is not greater than end, and both are code-point offsets into the plain projection of the node in the pinned state.

Example Names
cornell1995second-saqk#k3m9q2a A sentence, whatever its current text.
cornell1995second-saqk#k3m9q2a@4f1a0c That sentence as it read when its node_hash began 4f1a0c.
cornell1995second-saqk#k3m9q2a@4f1a0c[12:57] Code points 12 to 57 of its plain projection in that state.
smithlab/cornell1995second#k3m9q2a The same sentence in the smithlab store.

On the web the node ID is the fragment: …/smithlab/cornell1995second#k3m9q2a lands on the sentence. addresses.jsonl gives valid and invalid addresses and how each parses.

6.2 The anchor

An anchor, scienting-anchor/1, is the stored form of a pinned address, with enough context to survive change. It is what extracts, annotations, dataset values and draft citations use.

Property Required Holds
format Yes scienting-anchor/1.
store No The store.
doc Yes The document key.
node Yes The node ID, or a legacy alias.
kind No, RECOMMENDED The node's kind when the anchor was made.
own_hash, node_hash Yes The node's values when the anchor was made.
span No [start, end] in code points; present for span anchors.
quote With a span, or on a leaf {exact, prefix?, suffix?}, or {exact_hash, length, prefix?, suffix?} for quotations longer than 200 code points.
context On a cell {table_label, row_path, column_path}: the table's label and the normalized plain projections of the row's and column's header cells, outermost first.
leaves On a container without content [[id, match_hash], …]: every leaf below the node, in document order.
rev Yes The document's revision when the anchor was made.
keyed No The hashing scheme, when the anchor leaves the team (6.3).

exact is the substring of the stored plain projection at the span, or the whole projection for a leaf anchor. prefix and suffix are up to 32 code points before and after it, omitted when empty. exact_hash is SHA-256 of the normalized quotation, and length its length in code points.

Revisions are counted, not timed, because anchors are made on different machines and clocks disagree.

6.3 Keyed anchors

When an anchor leaves the team, in a public layer or on a work whose text is withheld, it carries keyed, and:

  1. own_hash and node_hash are computed under the key;
  2. quote becomes {mac, length, prefix_mac?, suffix_mac?}, each MAC over the normalized text; and
  3. context becomes {table_label_mac, row_path_mac, column_path_mac} in the same way.

Such an anchor can be verified, and its span shown, only over a copy of the paper the reader holds.

6.4 Four kinds of anchor

Kind Points at Judged by
Span Part of a leaf's text: the anchor has a span. The quote, then node_hash.
Leaf A whole sentence, cell, reference, authors, keywords, equation, equation-line, code or algorithm. node_hash.
Container with content A section, figure, panel, table, theorem or footnote. own_hash, with node_hash reported.
Container without content Any other node. Its ID and its leaves.

6.5 Resolution

To resolve an anchor against a document's current state (its nodes, its ID table and its corrections):

Follow the ID. If the anchor's node is live, use it. Otherwise follow the ID table: an alias leads to its node, a retirement to its successor, repeatedly, until a live node is reached, a deletion ends the chain, or an ID is unknown or repeats. The anchor is retired when the chain passed a retirement; an alias alone does not retire it. The kind is the anchor's kind, else the live node's, else the retired entry's node_kind.

Find the quote. The quote's hits in a text are the positions where exact occurs, or, for a hashed quote, where a window of length code points has that hash. When there is more than one hit and the anchor has a prefix or suffix, keep the hits preceded by the prefix and followed by the suffix.

Judge, by kind:

Kind Current Changed below Moved Reattached Otherwise
Span or leaf Not retired, and node_hash equal. Never. Retired, and the successor's text has exactly one hit (a span) or at least one (a leaf); a span takes the new position. Not retired, changed, and exactly one hit, with the new position; or a changed node whose only changes since rev were relabel corrections, and whose own content with the earliest of those corrections' from label has the anchored own_hash. Stale.
Container with content Not retired, and own_hash and node_hash equal. Not retired, own_hash equal, node_hash not. Retired, and the successor's own_hash equals the anchored one. Not retired, and only relabel corrections changed it, judged as for leaves. Stale.
Container without content Live and node_hash equal. Live and node_hash not equal. Retired, and the successor's leaves hold at least half of the anchored leaves, each matched by ID or else by match_hash. Never. Stale.

The cell rule. A cell anchor whose ID led to no live node (a deletion, or an unknown ID), or whose successor fails the leaf rule, is reattached when exactly one cell in the document has the anchored context and exactly one hit of the quote. This finds a value again even in a table with multi-level headers or row labels repeated across parameter sets.

The result is {result, node, node_hash?, span?, drift?, via?, reason?}: the outcome, the live node and its node_hash, the new span, the drift of a changed-below result, the IDs followed, and a reason for stale and relabel results. A moved or reattached anchor is re-pinned to the live node, and the move is recorded.

Checks are stricter than readers: an extract is a span or leaf anchor and never accepts changed-below, because a check is about exact words. resolve.jsonl resolves seventeen anchors made at revision 0 against fixtures/packages/hale2026harmonic-damb at revision 12, with every outcome.

6.6 Drift

changed-below must not last forever. For a container c and an anchor made at revision r:

  1. The corrections that count are those with index greater than r whose path or to_path contains c and whose touched list is not empty.
  2. count is their number, and T the union of their touched lists.
  3. L is the set of leaves below c (including c itself if it is one) in the current state.
  4. share is |T| divided by |L ∪ T|, written as a decimal with four places, rounded half up.
  5. level is none when count is 0, high when share is greater than the threshold, and low otherwise.

The threshold is a quarter by default; a project may set another (20). A reader shows a high drift as needing review. A change to a section's heading changes its own_hash, so a rename is never mistaken for drift.