fix(structure): a derived reference needs a cue, not just a number shape

STRUCTURED_V1 derived `references` from every number-shaped token in a body.
A consumer measured 12 false references out of 12 on their corpus, ten of them
version numbers in titles read as document numbers. Measured again here before
changing anything, on two corpora, through `derive_document_structure` itself:

  corpus A  a consumer's normative bundles   2 561 docs -> 2 838 subjects
  corpus B  this repository's own docs/         28 docs ->   559 subjects

The reported class reproduces, and two larger ones they did not report turn up:
702 of corpus A's subjects (24.7 %) are hexadecimal fragments of UUIDs read out
of `id="..."` attributes in embedded MathML, and corpus B lifts tokens out of
escape sequences in quoted source (`\n60` -> `N60`), licence identifiers
(`AGPL-3.0` -> `3.0`) and package pins.

A derived subject is now a bundle-local link target, or a number immediately
preceded by a cue from a closed set. Positive rather than a blacklist because
the data forces it: `V221` is a genuine document number in corpus A and
`V0.3.0` is a software version in corpus B, and they are the same token shape,
so only the words in front of them can tell them apart. The cue matches at a
word boundary (a Norwegian compound ending in `-klasse` otherwise satisfies the
cue `se`, which admitted 86 class designations) and the window is NFC-normalised
so a cue survives a decomposed filesystem. Fragment-only and brace-carrying link
targets go too: neither can name a concept, so neither is a pending pointer.

After: 2 838 -> 1 279 (A) and 559 -> 72 (B). Hand-classified against the
occurrence that actually passed the gate: 30 of 30 sampled genuine on A, 60 of
60 on B. Residual known falses: 9 of B's 72, all illustrative link targets in
prose about link syntax.

The prefix-resolution rule stays open, per the order's condition: of 2 589
documents, 2 562 carry a number and 0 of those are dotted, so unique-prefix
match has no data here to be defended against.

Cost stated rather than hidden: a corpus phrasing cross-references outside this
vocabulary derives nothing and must declare `references`. A missing reference is
visible to the reader; a false one is not.

Record: docs/plan/references-cue-rule.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ATyA5Lx53N1rKzwMnuMoem
This commit is contained in:
Kjell Tore Guttormsen 2026-08-29 09:26:57 +02:00
commit 2827be0ece
4 changed files with 277 additions and 6 deletions

View file

@ -0,0 +1,120 @@
# What a derived `references` subject is
Record of the decision taken 2026-08-29 on order `…507632913`, and of the
measurement it rests on. Supersedes the reference half of
`docs/plan/structure-derivation.md`; nothing else in that document moves.
## The defect
`STRUCTURED_V1` derived `references` by taking every number-shaped token in a
document body. A consumer measured it on their own corpus and reported 12 false
references out of 12, ten of them version numbers carried in titles and read as
document numbers.
The order was not to inherit that figure. It was measured again here, on two
corpora this repository can reach, before anything was changed.
## Measured before the change
Both figures come from `derive_document_structure` itself, run over every
unique document (deduplicated by sha256) and counting the subjects it returns
with `references` marked derived.
| corpus | documents | documents with derived `references` | subjects |
|---|---|---|---|
| A — a consumer's normative bundle set, six bundles, Norwegian | 2 561 | 1 010 | 2 838 |
| B — this repository's own `docs/`, English and Norwegian technical prose | 28 | 27 | 559 |
What those subjects were, classified against the position the scanner actually
lifted each one from:
- **Corpus A.** 702 of 2 838 (24.7 %) were hexadecimal fragments of UUIDs read
out of `id="…"` attribute values inside embedded HTML and MathML. The rest of
the false population was unit symbols (`kN/m2` reduced to `M2`), table row
and class labels, and formula numbers.
- **Corpus B.** Dominated by software versions (`v0.3.0`, `3.14.0`, `0.11.10`,
`1.28.2`), a licence identifier (`AGPL-3.0``3.0`), a container format
version (`%PDF-1.6``1.6`), a hash algorithm name (`SHA256`), and tokens
lifted out of two-character escape sequences inside quoted source (`\n60`
`N60`).
The consumer's reported class reproduces. Two classes they did not report —
identifier fragments inside markup, and escape-sequence artefacts — are larger.
## The decision
**A derived reference subject is one of exactly two things: a bundle-local
markdown link target, or a number immediately preceded by a reference cue from
a closed set.** Everything else number-shaped in body prose is not a reference.
The rule is positive rather than a blacklist, and that is forced by the data,
not chosen for taste: corpus A proves `V221` and `V240` are genuine document
numbers, corpus B proves `V0.3.0` is a software version, and the two are the
same token shape. Nothing structural separates them. Only the words in front of
them do, so only a positive cue can be defended.
The cue must match at a word boundary. Without that guard a Norwegian compound
ending in `-klasse` satisfies the cue `se`, which admitted 86 class
designations on corpus A. The window is normalised to NFC first, so a cue
carrying a Norwegian letter matches whether the file arrived composed or
decomposed.
Two link targets are also excluded, on the same ground rather than a new one: a
fragment-only target (`#anchor`) points inside the document it appears in, and
a target carrying a brace is a template placeholder from prose *about* links.
Neither can name a concept — `reduce_to_id_grammar` emits lowercase
alphanumerics and hyphens — so neither is a pointer awaiting resolution. It is
a claim that cannot come true.
## Measured after the change
Same script, same corpora, `HEAD` against the working tree.
| corpus | subjects before | subjects after | dropped |
|---|---|---|---|
| A | 2 838 | 1 279 | 1 559 (54.9 %) |
| B | 559 | 72 | 487 (87.1 %) |
Precision was checked by hand against the occurrence that actually passed the
gate, not the subject's first occurrence in the file — reading the first one
mis-attributes context and was a real error in an earlier pass of this
measurement.
- Corpus A: 30 subjects drawn at random from 1 279. **30 of 30 genuine.**
- Corpus B: all 60 cue-gated subjects. **60 of 60 genuine.**
**Residual known false positives: 9 of corpus B's 72**, all in the link
channel, all illustrative targets in prose about link syntax (`link`, `target`,
`/sti`, `subdir/`). They are indistinguishable from real relative paths without
guessing at path semantics, so they stay. Corpus A's link channel is empty, so
this residue is not visible there at all.
## The cost, stated rather than hidden
A corpus whose cross-references are phrased outside this vocabulary — a third
language, a house style — derives **nothing**, and must declare `references`
itself. That is the intended failure: a missing reference is visible to the
reader, a false one is not.
Two recall losses inside the corpora measured, both accepted: a reference whose
cue is a preposition (`i henhold til …` where the noun is absent) and one
written as a prefixed compound (`N-V240`) are dropped.
## What was deliberately not done
**The prefix-resolution rule (`N130` in a body not finding a document numbered
`N130.2`) stays open.** The order allowed it in the same pass only if the data
bore it. It does not: of 2 589 documents across both corpora, 2 562 carry a
number and **0** of those numbers are dotted, so a unique-prefix match has
nothing here to be defended against. Closing two things badly is worse than
closing one well.
## Adjacent defect found, not fixed — needs its own order
The line-oriented frontmatter parser lets a **nested key overwrite a top-level
key of the same name**. A `sources:` block whose entry carries its own `title:`
replaces the document's `title`, and the `number` derived from that title
follows it. This is the same family as the known block-list finding but strictly
worse: that one drops a value silently, this one *substitutes* one. It changes
`title`, `number` and `parent`, so it is outside this order's scope, and it is
why corpus A's numbers above are what they are.

View file

@ -1,5 +1,10 @@
# Structure derivation: numbering, hierarchy, cross-references, supersession
> **Superseded in one place, 2026-08-29 (order `…507632913`).** What makes a
> body mention a `references` subject is now the cue rule measured in
> `docs/plan/references-cue-rule.md`, not "every number mention". Everything
> else below stands.
Answers order `20260826T124341Z-181848459` (from `.claude`, 2026-08-26). The
premise it waited on — the Door B / Door A capability gap — was closed in
`dc9ea59`: the answer was title derivation, not a door change and not a profile
@ -53,7 +58,7 @@ nothing derived can still use everything declared.
| | leading number token of filename, then of title | **derived** |
| `parent` | arithmetic on `number` (drop last dotted part) | inherits `number`'s |
| `references` | producer's `references` key | declared |
| | bundle-local link targets + number mentions in the body | **derived** |
| | bundle-local link targets + cued number mentions in the body | **derived** |
| `supersedes` | producer's `supersedes` key | declared |
| | same number + ordered `version` | **derived** |
| `version`, `status`, `date` | producer only | declared |