llm-ingestion-okf/docs/2026-09-08-eksakt-oppslag.md
Kjell Tore Guttormsen a37d5ced38 fix(consume): match an identifier by equality, deliver the concept a question names
A question naming a requirement number now delivers that requirement at rank 1
on all three vegnormal bundles (was 96, 9, 35 of 446, 1 133, 270). Two
mechanisms, both measured, both default because no published figure moves.

The matcher: `tokens_match` compared four leading characters, so the unique
identifier `3.3.1-13` read as 135 of 446 common and the rarity weight ranked a
common adjective above the number naming the document. An identifier now
matches by equality alone; df falls to 1/1/1. Words keep the prefix rule, which
was measured for Norwegian compounds. Equality has no floor either, so a
three-character identifier stops matching nothing at all -- measured, `9.2`
reached 0 concepts while sitting verbatim in one title.

The lookup: a question carrying an identifier that sits verbatim in a concept's
title or id is answered by a partition over the fusion's output, not by a
fourth signal. The form was chosen by measurement -- a fourth RRF signal was
simulated first and put the gold at rank 26 / 15 / 19, none of them delivered,
because RRF consumes ranks only and one signal contributes at most 1/(RRF_K+1).
No frontmatter key list is declared: of 1 846 concepts carrying `req_number`,
1 846 also carry that identifier in the title.

The matcher alone is NOT a monotone win (N200 9 -> 26, because that gold's body
cross-references a neighbouring number that the prefix rule counted as a hit on
the question's). Only the partition delivers; the table is in the record.

Consumer corpus: every named control byte-identical against a frozen
`git archive` copy of 116d3e1 -- four payload digests, eight candidate ranks,
six hit@8 rows, both known-negatives. One document that was withheld at
position 621 of 621 is now delivered at rank 1, on a corpus with no
requirement-number grammar at all.

13 tests (12 red before the rules existed), 7 mutations, 7 red. 1 320 passed.
Consumption-side only; no bundle ref moves.
Record: docs/2026-09-08-eksakt-oppslag.md

Co-Authored-By: Claude <claude-opus-5>
2026-09-08 13:27:59 +02:00

18 KiB

The exact lookup: naming a document is not searching for it, 2026-09-08

Two sessions made a requirement number visible to the consumption pre-pass and then made it worth more than a common word, and neither delivered the document the question named. The first left the gold below_k on all three bundles (docs/2026-09-08-kravnummer-tokenisering.md § 8); the second delivered one of three and decomposed the other two into a matcher decision and a fusion decision, with no measurement behind either (docs/2026-09-08-sjeldenhetsvekt.md § 3, § 4).

This session takes both decisions with numbers. The result is that a question naming a requirement number now delivers that requirement at rank 1 on 3 of 3 bundles, and that every published figure on the consumer corpus is byte-identical.


0. What IS measured, and what is NOT

Measured. Eleven known-positive figures reproduced before anything was changed; document frequency under the matcher and under equality on three bundles; the gold's fused rank, verdict, payload digest and byte count before, after the matcher alone, and after both mechanisms; the three spellings of one identifier; every named control on the consumer corpus against a frozen git archive copy of 116d3e1; a control question on that corpus that does carry an identifier, before and after; hit@8 over the six published questions and both known-negatives; the cost of each new pass; seven mutations.

Known-positive, run first. Every figure this session builds on was reproduced on this machine, with the committed tool, on a clean tree at 116d3e1, before any after-column was read:

# known-positive source measured here
1 gold fused rank per bundle, weight off previous session § 2 96, 9, 35
2 gold fused rank per bundle, weight on previous session § 2 103, 8, 35
3 df under the matcher vs under equality previous session § 3 135 / 17 / 5 vs 1 / 1 / 1
4 considered / withheld / delivered consumer § 3 446/438/8, 1133/1125/8, 270/262/8
5 gold position in withheld consumer § 3.1 119 of 438, 204 of 1125, 66 of 262
6 payload sha256, weight off previous session § 2 49e9c718…, adc2eac7…, 21c5bbfe…
7 payload sha256, weight on previous session § 2 ba09a56a…, 403ee989…, 1758f6ca…
8 consumer corpus, the good question's candidate rank, four rows previous session § 5 1 / 1 / 1 / 1
9 the priced sheet's candidate rank, four rows previous session § 5, § 7 251 / 10 / 78 / 2
10 the mandate question's default payload previous session § 7 169 583 B
11 hit@8 over the six published questions 2026-09-07 § hit@k 5 of 6, five of them at rank 1

NOT measured, named rather than glossed:

  • o200k_base token counts. No o200k encoder is installed on this machine and this repository's single-runtime-dependency rule does not admit one to produce a caption. Figures below are in BUDGET_UNIT — utf-8 bytes of emitted JSON — plus the payload sha256, which is exact where a byte count is only indicative.
  • The rebuilt consumer bundle of 56c1205 is not on disk. Its content identity (sha256-tree:2f82fcfe…) names a tree this machine no longer holds, and neither does the bundle the two S7 byte figures in the order come from (2ae46f68… 169 573 B and 8aaf8558… 172 588 B). Those two figures are therefore not reproduced here. The control corpus used instead is the bundle delivered 2026-09-03 (sha256-tree:9a4e5561…), on which rows 8, 9 and 10 of the table above reproduce the published values exactly. This costs the decision nothing: what the decision needs is byte-identity before and after, and that is measured on one and the same bundle, against a frozen copy of the committed code rather than against a remembered number.
  • Whether a live model answers better. This measures a ranking. "Delivered" is not "read", and that measurement belongs to the consumer.

1. Mechanism (a): an identifier matches by equality, and by nothing else

tokens_match compared four leading characters, for a reason that was measured and still holds: Norwegian compounds do not match token-exactly, and vare|ne and vare|mottak share a stem. A requirement number has no stem. Four leading characters of 3.3.1-13 are four leading characters of every requirement in section 3.3, so a unique identifier read as 135 of 446 common — and the rarity weight then faithfully ranked a common adjective as more informative than the number naming the document.

The rule, whole:

if is_identifier(left) or is_identifier(right):
    return left == right

is_identifier is _IDENTIFIER_RE.fullmatch, the pattern the tokeniser already uses to keep an identifier whole. No new constant, no token class declared anywhere, and words are untouched.

Red first. tokens_match('3.3.1-13', '3.3.1-14') was True and is now False; ('3.3.1-13', '3.3.1-13') was and is True. The known-positive ran in the same file and passed before and after: brannsikring/brannvern, prisene/prissammenstilling, varene/varemottak all still match, and brann/bygg still does not.

df after, same instrument as before:

bundle identifier df before df after df exact concepts
N100 3.3.1-13 135 1 1 446
N200 2.9.2-12 17 1 1 1 133
N500 10.2-2 5 1 1 270

A short identifier stops being invisible. MIN_SHARED_PREFIX made a three-character identifier match nothing, not even itself: measured on the 629-concept consumer bundle, 9.2 reached 0 concepts under the matcher while sitting verbatim in one title. Equality has no floor, so it now reaches that one. The floor stays where it was measured — for words.

What the matcher alone bought, and what it cost. This is the honest column, and it is not all in one direction:

bundle gold fused rank, before after (a) alone with --rarity-weight, before → after
N100 96 53 103 → 45
N200 9 26 8 → 23
N500 35 37 35 → 36

N200 got worse, and the reason is measured rather than guessed. That gold's body carries 88 identifiers, three of them occurrences of 2.9.2-1 — a cross-reference to the neighbouring requirement. Under the prefix rule that cross-reference answered the question's 2.9.2-12, so the gold was scoring a hit on its own body that it had not earned. Removing a false positive that happened to help is still removing a false positive; the number is here so that nobody has to take that on faith. On its own, mechanism (a) delivers 0 of 3.


2. Mechanism (b): the form was chosen by a measurement, not by a preference

A question carrying an identifier that sits verbatim in a concept's title or id is a lookup: the reader already knows which document they want and is spelling its number. The order named two candidate forms and invited a third.

Form (i), a fourth signal inside the RRF fusion, was simulated first on the same three bundles, on top of mechanism (a), with the lookup as a 0/1 signal:

bundle gold rank with a fourth RRF signal of
N100 26 446
N200 15 1 133
N500 19 270

None of them delivered, and the reason is the one the previous session already wrote down: RRF consumes ranks only, so any single signal contributes at most 1/(RRF_K + 1) however certain it is. A concept the question names cannot outbid three signals that merely describe it. Form (i) is refuted by its own numbers, not by taste.

Form (ii) ships: a partition applied to the fusion's output. Concepts whose title or id bears the question's identifier are moved to the front, keeping the order the fusion gave them; everything else keeps its order too. Nothing is dropped, nothing is scored twice, and the fused ranks themselves are untouched.

named = set(lookup_hits(concepts, question))
if named:
    ranked_ids = [k for k in ranked_ids if k in named] + [k for k in ranked_ids if k not in named]

It reads the text the title-and-id signal reads, and declares no key list. The order allowed a frontmatter key set (req_number, identifier, …). It is not declared, because a measurement decided instead: of the 1 846 concepts across the three bundles that carry a req_number, the identifier in that key is also in the title on 1 846 of them, and on 0 does the key carry an identifier the title lacks. A key list would have bought nothing and would have been a constant no measurement asked for. The consumer corpus has no such key at all, and is served anyway (§ 4).

The known-negative is structural, not lucky. lookup_hits returns the empty tuple whenever the question carries no identifier, and every one of the ten published control questions on the consumer corpus carries zero identifiers under _IDENTIFIER_RE — the six hit@8 questions, both known-negatives, the good question and the mandate-shaped one. Verified with normalise on each question before any payload was built.

Red first, on a synthetic 300-concept corpus where every concept carries the word a standards corpus repeats on every page and the neighbours are numbered so that they share the gold's first four characters: the named concept comes back at rank 1; two concepts bearing the same number come back as the top two; a question with no identifier leaves the order untouched; an identifier no concept bears changes nothing and empties nothing.


3. After: three bundles, one instrument, before and after

Instrument, unchanged from the previous session and from § 0 above: the gold's position in the fused ranked list, plus the payload's own verdict from build_payload. Command:

python3 tools/okf_consume.py <bundle> \
  --question "Hva krever Krav <n> i <normal>? Gjengi det sentrale vilkåret." \
  [--rarity-weight]
N100:2023 N200:2024 N500:2024
concepts 446 1 133 270
gold fused rank, before 96 9 35
gold fused rank, after 1 1 1
gold verdict, before below_k below_k below_k
gold verdict, after delivered, rank 1 delivered, rank 1 delivered, rank 1
the same with --rarity-weight 1, delivered 1, delivered 1, delivered
payload bytes, before → after 61 566 → 58 409 142 861 → 146 145 40 626 → 39 193
payload sha256, after 5bf6ab78… af5829cd… 9a9e7860…
okf_contract_check exit 0, 14 rules, 0 findings idem idem
lookup hits found 1 1 1

3 of 3, at rank 1. The order's target was rank ≤ 8 on 3 of 3.

The three spellings are one lookup. For each bundle, the question was asked with an ASCII hyphen, an en dash and an em dash. All three deliver the same eight concepts in the same order, with the gold at rank 1 — the payload digests differ only because the question string is echoed in the payload, so the excerpt list is compared instead.

Cost. The lookup is one pass over the concepts already in memory: 0.011 s over 446, 0.024 s over 1 133, 0.006 s over 270. The df pass the rarity weight uses, re-measured under the new matcher: 0.055 s, 0.227 s, 0.064 s. Both are inside the order's one-second bound on the largest bundle by a factor of four or better, and a question carrying no identifier does not tokenise the corpus at all.


4. The consumer corpus: nothing moves, and one unreachable thing becomes reachable

Every named control was measured twice — once from a git archive copy of 116d3e1 extracted to a scratch tree, and once from this working tree — on the same bundle, with PYTHONPATH and __file__ both checked so the frozen run could not import the working copy.

control before after
good question, candidate rank 1 of 233 1 of 233
good question with the cost vocabulary 1 of 248 1 of 248
the same two with --rarity-weight 1 / 1 1 / 1
priced sheet, mandate question, vocabulary off 251 of 271 251 of 271
priced sheet, --cost-vocabulary --k 12 10 of 280 10 of 280
the same two with --rarity-weight 78 / 2 78 / 2
mandate question, default payload 78fec2ef…, 169 583 B byte-identical
mandate question, --cost-vocabulary --k 12 --limit 160000 1f11e56b…, 240 021 B byte-identical
mandate question, --cost-vocabulary --k 12 777986a5…, 172 246 B byte-identical
good question, default payload f7ff803e…, 165 109 B byte-identical
hit@8 over the six published questions 5 of 6 5 of 6, every rank, byte count and spend identical
both known-negatives 8 delivered each byte-identical

Every figure in that table is identical — the four payload digests and their byte counts, the eight candidate ranks, all six hit@8 rows with their spends, and both known-negatives. No consumer's bytes move because these two changes exist — which is what makes them safe to ship on by default rather than behind a flag.

Generality, on a corpus with no requirement numbers at all. The consumer bundle has no req_number key and no requirement grammar; its documents are numbered in their titles instead (… 9.2 …). One control question was written that names such a number. Before, that document was withheld below_k at position 621 of 621 — dead last, because a three-character identifier matched nothing under MIN_SHARED_PREFIX, not even itself. After, it is delivered at rank 1. The mechanism is not a vegnormal mechanism.


5. The decision, made by the number

The order's rule: (a) is a defect fix and ships as default if every control on the consumer corpus stands byte-identical; (b) ships as default if the known-negative holds.

mechanism condition measured decision
(a) identifier matches by equality all consumer controls byte-identical every control identical default, no flag
(b) the lookup partition byte-identical for a question with no identifier structural: 0 identifiers in 10 of 10 control questions; every payload identical default, no flag

Nothing fell, so no flag is added and nothing has to be named as a loss. --rarity-weight is untouched and stays off; with it on, the three lookups also deliver at rank 1, so the two mechanisms do not fight.


6. What did NOT close

  • . and - are not interchangeable in a lookup. _DASH_TO_HYPHEN folds every dash spelling to one, so em dash, en dash and hyphen are one lookup — but a question spelling 1.10 does not find a document whose id spells it 1-10, and this library's own id grammar produces exactly that hyphenated form from a dotted source. Measured and left open: the fix is a canonicalisation inside the lookup, and it needs its own before/after.
  • A bundle whose identifiers live only in frontmatter is not served. Today that set is empty (0 of 1 846), which is why no key list was declared. If a consumer arrives with one, this is the rule that has to grow.
  • An identifier that appears only in a concept's BODY is not a lookup. It is a search, and it ranks like one.
  • The matcher alone made two of three lookups worse (§ 1). Only the partition delivers. Anyone lifting one half without the other should read that table first.
  • The two S7 byte figures from the O1 bundle are not reproduced, because neither that bundle nor the rebuilt one is on this machine (§ 0).
  • "Delivered" is not "read". All three golds are now in the payload at rank
    1. Whether a model then answers the engineer's question is the consumer's measurement, and this document does not claim it.

7. Tests, mutations, gates

Thirteen new tests. Twelve were written before either rule existed and eleven of them failed; the twelfth is the known-positive that had to pass from the start (the Norwegian compounds), so the red was the rules' absence and not a broken fixture. The thirteenth was written afterwards, against a mutation that survived, and is red against it. One fixture defect was found by the red run itself: the neighbours were numbered so that one of them was the gold's number, which made the df 2 rather than 1.

Seven mutations, seven red. Three survived the first pass and none was banded blind:

  • M2 (is_identifier matches a substring instead of the whole token) survived because no token normalise emits can tell the two apart — the generic split keeps neither . nor -, so a split token can never contain an identifier. The commitment is real even where no output distinguishes it, so it is now asserted on the function.
  • M4 (the early return for a question with no identifier) survived because an empty identifier set intersects to nothing anyway. The guard is a cost commitment, not a semantic one, so it is now measured as one: a question without an identifier must not tokenise the corpus at all.
  • M6 (the lifted concepts re-sorted by id instead of keeping the fused order) survived because the fixture's two holders tied. The fixture was corrected so byte order and fused order disagree on purpose.
gate result
pytest -q 1 320 passed, 1 skipped (1 307 on this branch before, +13)
ruff check src tests tools clean
ruff format --check . clean, 88 files
mypy --strict src/ clean, 17 files
okf_contract_check on the three lookup payloads exit 0, 14 rules, 0 findings
git diff --stat tools/okf_consume.py, tests/test_okf_consume.py

No rebuild. The change is consumption-side only, confirmed by git diff --stat: no production module under src/ is touched, so every bundle ref in flight is unchanged.