124 lines
7.8 KiB
Markdown
124 lines
7.8 KiB
Markdown
# 6. Addresses and anchors
|
||
|
||
## 6.1 Addresses
|
||
|
||
An address names a node, optionally pinned to a hash and narrowed to a span:
|
||
|
||
```abnf
|
||
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](#63-keyed-anchors)). |
|
||
|
||
`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](20-projects.md)).
|
||
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.
|