llm-ingestion-okf/docs/2026-09-08-rangeringsbom-sammensatte-ord.md
Kjell Tore Guttormsen c3b645bccf fix(consume): the compound-word miss is a degenerate signal's tie-break, behind a flag
The consumer's question about `vann- og frostsikring` in a subsea tunnel
delivered 0 of the 16 concepts covering it, best of them at fused rank 14.
Reproduced with the denominator, then decomposed per signal before anything
was built.

It is not a matcher miss. `normalise("vann- og frostsikring")` already returns
`('vann', 'frostsikring')` on HEAD, the prefix rule already bridges the
inflections, and the best covering concept already answers 7 of 7 question
tokens -- more than any delivered one. A tokeniser rule had nothing to widen.

It is the fusion, but not a weight. RRF ranks every concept in every signal,
including a signal that scored them all the same, and the declared
`(-score, concept_id)` tie-break then orders that group by id. On N500 the
document prior has TWO distinct values over 270 concepts, so the third signal
contributed alphabetical UUID order spread from 1/61 to 1/329 -- enough to put
a concept leading the body signal behind concepts sharing only `tunnel` and
`vann`.

`--tie-shared-rank` lets concepts a signal scores equally share that group's
first rank. The miss closes: best covering 14 -> 3, 2 of 16 delivered. OFF BY
DEFAULT, by the order's own rule: the three requirement lookups hold at rank 1
and the K2 digest holds, but hit@8 over the six published questions falls 5 of
6 to 4 of 6. Decomposed rather than guessed -- K2's prior is coarse (6 values
over 39 documents) rather than degenerate, and one gold sat early in its tie
group. That benefit was never a measurement, but it is a published row.

`--withheld-titles` gives each withheld entry the concept's title, so a reader
can see WHAT was withheld without reading the bundle. 11 lines of code; the
bytes are why it is off. It grows an N500 payload 37.9 % and takes the
629-concept K2 bundle's BOOKKEEPING to 122 704 B -- past the 120 000-byte limit
itself -- which would falsify the breaking point published in the tracked
`skills/okf-consume/SKILL.md` on the day it shipped.

Defaults measured, not asserted: six payload digests built from a frozen
`ff79cfa` (`git archive`, `__file__` checked) and from this tree with both
flags omitted are 6 of 6 identical, and `okf_skill.py` output is identical
apart from the paths each copy writes about itself. Contract checker exit 0 on
eight payloads, both values.

One known-positive did not reproduce and is reported rather than matched: the
order's S7 literal `2ae46f68`/169 573 B is stale by three excerpt-form commits;
HEAD measures `c759a657`/171 614 B.

Suite 1388 -> 1397. Report: docs/2026-09-08-rangeringsbom-sammensatte-ord.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 18:53:18 +02:00

14 KiB
Raw Blame History

The compound-word ranking miss: a degenerate signal, not a matcher, 2026-09-08

A consumer asked the N500 bundle about vann- og frostsikring in a subsea tunnel and got eight excerpts, none of which covered the subject; the bundle covers it on 16 concepts. The consumer behaved as the contract asks — it answered [sourced-not-sufficient] and named its denominator — so the miss is the ranker's (docs/2026-09-08-claude-code-skill-vilkaarlig-bundle.md § 5).

The order proposed two sites for one rule: the tokeniser, or the fusion. The measurement chose for us, and it chose neither of the two mechanisms the order named. The tokeniser already resolves the compound, and the fusion's defect is not a weight — it is the tie-break in a signal that separates nothing.


0. What IS measured, and what is NOT

Measured. The miss reproduced with a command and a denominator; the three per-signal ranks of the covering concepts; the document prior's distinctness on two corpora; the rule's effect on four cases; every known-negative the order names, both values; six default payload digests against a frozen HEAD; the contract checker on eight payloads; the skill generator's output before and after; the full suite.

Known-positive, run first. Every figure this session builds on was reproduced on this machine before any after-column was read:

# known-positive source measured here
1 files matching frostsikring C1 § 5 17, case-insensitively, index.md among them
2 files matching vann- og frostsikring C1 § 5 10
3 files matching undersjøisk C1 § 5 13
4 the miss itself: covering concepts in the cut C1 § 5 0
5 hit@8 over the six published questions 2026-09-07 § hit@k 5 of 6, ranks 1,1,1,1,1,
6 the three requirement lookups, rank of 8 O2c § 3 1, 1, 1
7 K2 bundle, whole tree Arm C § "did not move" 1108 files, 9cd74519…
8 suite before the session STATE (round 110) 1388 passed

One known-positive did NOT reproduce, and the deviation is the order's own literal. The order names the S7 control payload as 2ae46f68…, 169 573 B. Measured on frozen ff79cfa (git archive into a scratch tree, __file__ asserted), that command produces c759a657…, 171 614 B. The literal is not wrong, it is stale: it was published at 116d3e1, and a37d5ce, 17c49fc and c95d189 each widened the excerpt form afterwards. No document restates it. The row is therefore measured as "byte-identical against HEAD", which is what it exists to prove, and the stale literal is reported rather than quietly matched.

NOT measured.

  • Whether a live model answers better. This measures a ranking. Delivered is not read.
  • o200k_base token counts. No encoder is installed and the single-dependency rule does not admit one. Every figure is in BUDGET_UNIT plus sha256.
  • Whether the rule helps a third corpus class. Two were measured (the three N-bundles and K2). A corpus whose document prior is neither degenerate nor coarse is unmeasured, and the flag's default reflects that.

1. The reproduction, with the command and the numbers

The order asks for the question C1 § 5 used, quoted. It is not in that document — § 5 describes the question in prose ("about water and frost protection in a subsea tunnel") and quotes no line. The question below is the order's own wording, and it is marked as such rather than presented as C1's:

python3 tools/okf_consume.py ~/repos/vegnormal-okf/build/ferdig/n500-2024 \
  --question "Hvilke krav gjelder vann- og frostsikring i undersjøisk tunnel?" \
  --out /tmp/o4-baseline.json

270 concepts, 8 delivered, 262 withheld. 0 of the 16 covering concepts — the concepts whose title or body says frostsikring, case-insensitively — is in the cut. The best of them fuses to rank 14.

The eight delivered, and the three covering concepts nearest the cut, with the per-signal ranks that decide the fusion:

fused body title+id doc prior lexical title
delivered 1 1 17 6 4 6 Krav 4.3—1 Tunnelportaler
delivered 2 2 24 1 50 7 Krav 3.2—2 Utslipp av vann fra tunnel
delivered 3 3 138 5 1 4 Krav 5.3.6—1 Fjernstyrte bommer
covering 1 14 6 33 130 7 Krav 8.4.2—1 Frostisolering med PE-skum eller XPS
covering 2 39 34 30 121 6 Krav 9.3.2—2 Fundament, omfylling og frostsikring
covering 3 44 25 101 63 5 Krav 1.1—2 Generelle bestemmelser

The best covering concept answers 7 of 7 question tokens — more than any delivered one — and leads the body signal at rank 6. It is not invisible to the matcher. It is behind on one signal: the document prior, at rank 130.


2. Why it is not a matcher miss (the order's alternative A, falsified)

Three measurements, each of which alone rules the tokeniser out as the site:

  1. The compound is already resolved. normalise("vann- og frostsikring") returns ('vann', 'frostsikring') on HEAD. The hyphen splits, og falls below MIN_TOKEN_LENGTH. There is no coordination left for a rule to undo.
  2. The prefix rule already bridges the inflections. tokens_match('frostsikring', 'frostsikringen'), ('vann', 'vannsikring') and ('undersjøisk', 'undersjøiske') are all True at MIN_SHARED_PREFIX = 4.
  3. The covering concept already scores the maximum. Its lexical overlap is 7 of 7 question tokens. A rule widening the tokeniser cannot raise 7.

Recorded as a test rather than as a memory: tests/test_tie_shared_rank.py::test_the_hyphen_and_og_coordination_is_already_resolved_by_the_tokeniser, green on HEAD and labelled a characterisation.

One thing the same measurement DID surface, and it is not fixed here: the prefix rule over-matches in the other direction. undersjøisk matches under, so its document frequency is 159 of 270 — a term that should be one of the rarest in the question reads as one of the commonest. That belongs to the rarity weight (--rarity-weight, already shipped and already off), and this session was allowed one rule.


3. What it is: a signal that separates nothing still orders everything

RRF consumes ranks only, so concept_scores produces a rank for every concept in every signal — including a signal that gave them all the same score. The declared tie-break, (-score, concept_id), then orders that group by concept_id, and the fusion reads the result as though it were a measurement.

On N500 the concept ids are UUIDs and the document prior is degenerate:

corpus documents distinct prior values concepts sharing one value
N500 2 2 269 of 270
K2 39 6

So on N500 the third signal contributed alphabetical UUID order, spread from 1/61 down to 1/329. That spread is worth more than several positions on the two signals that did the measuring, and it is what put a concept answering 7 of 7 tokens behind concepts sharing only tunnel and vann.

The rule. tie_shared_rank: every concept a signal scores equally takes that score group's first position, so a signal that separates nothing contributes an identical constant to every concept and orders none of them. It is general — no word list, no corpus constant, no new number except the group boundary the scores themselves define.

The first position rather than the middle, and that was measured too: the midpoint variant put the best covering concept at rank 5 where the first puts it at 3, and neither changed the three lookups. First is kept because under it a non-separating signal contributes an identical constant, which is the whole claim; the midpoint still varies with the size of the group a concept lands in.


4. The known-negative table, both values

Flag --tie-shared-rank, off and on. Every row measured in one run, /tmp/o4/kn.py.

row off on verdict
N100 lookup Krav 3.3.1-13, rank of 8 1 1 holds
N200 lookup Krav 2.9.2-12, rank of 8 1 1 holds
N500 lookup Krav 10.2-2, rank of 8 1 1 holds
hit@8 over the six published questions 5 of 6 4 of 6 FALLS
— per-question rank 1,1,1,1,1, 1,,1,1,1, row 2 lost
S7 control question, default command c759a657… 171 614 B 5f4cd665… 145 228 B moves (on purpose)
K2 bundle, whole tree 9cd74519… 1108 files 9cd74519… 1108 files holds
okf_contract_check, 8 payloads exit 0 exit 0 holds
the O4 miss: best covering rank 14 3 closes
the O4 miss: covering concepts in the cut 0 of 16 2 of 16 closes

The two concepts the rule delivers are Krav 8.4.2—1 Frostisolering med PE-skum eller XPS and Krav 9.3.2—2 Fundament, omfylling og frostsikring for ledninger.

Default stays OFF, by the order's own rule: it requires every row to hold and the miss to close. The miss closes; the hit@8 row does not.

Why row 2 falls, decomposed rather than guessed

row gold concepts best fused, off best fused, on
1 1 1 1
2 49 1 28
3 20 1 1
4 43 1 1
5 11 1 1
6 5 11 (not delivered) 91 (not delivered)

K2's document prior is not degenerate: 6 distinct values over 39 documents. There the tie-break groups are large but the signal does separate, and row 2's gold happened to sit early in its group — that is, the noise was, on that one question, pointing the right way. Removing the noise removed a benefit that was never a measurement. That is an honest reason to keep the rule available and an equally honest reason not to make it the default: a rule that is right in principle and loses a published row is a caller's choice, not a library's.

Row 6 was already the known 1-of-6 shortfall and is not delivered either way.


5. The default did not move, measured against a frozen HEAD

Six payloads built from git archive ff79cfa into /tmp/o4/head (with __file__ asserted to be under that tree, so an editable install cannot be read by accident) and from this working tree with both new flags omitted:

payload digest
N100 lookup a2a91d8b99157a0c, 58 082 B
N200 lookup 0c828a5f8c432864, 144 932 B
N500 lookup ad2b35a556278553, 41 364 B
N500 O4 miss d4a75828c147d9e9, 47 056 B
S7 control c759a657dbb41d71, 171 614 B
K2 price question f17c8725ee6bbe1a, 167 360 B

6 of 6 identical. No consumer's bytes move because this session happened.

tools/okf_skill.py likewise: SKILL.md and references/example-payload.json are byte-identical apart from the absolute paths each copy writes about itself.


6. withheld names what was dropped — behind a flag, and here is why

C1 § 5's second observation: a withheld entry carries concept_id and rule and no title, so a reader told 262 concepts were withheld cannot tell what without reading the bundle, which § 2.2 forbids.

The code is cheap — 11 lines, well inside the order's 20-line bar. The bytes are not, and that is what moved the decision:

without title with title
N500 payload 41 364 B 57 023 B (+37.9 %)
N100 payload 58 082 B 88 251 B
N200 payload 144 932 B 215 148 B
K2 bookkeeping (629 concepts, everything not an excerpt) ~75 KB 122 704 B

The last row is the one that decided it. skills/okf-consume/SKILL.md — a tracked file — publishes the breaking point as "at 629 concepts it is ~75 KB … at roughly 8 000 concepts the withheld list alone approaches the 120 000-byte limit". With titles on by default that sentence becomes false immediately: at 629 concepts the bookkeeping is already past the limit. The generated skill's own figure moved from a 1206-concept ceiling to 762.

So --withheld-titles, off by default. On, every one of N500's 262 withheld entries carries its title; off, the entries are {concept_id, rule} exactly as before, and the eight contract-checked payloads all exit 0 either way.


7. What did NOT close

  • 14 of the 16 covering concepts are still outside the cut with the flag on. Rank 3 is not rank 1, and k = 8 over 270 concepts is a narrow door.
  • The over-matching prefix. undersjøisk reads as 159-of-270 common. One rule per session; this was not it.
  • hit@8 row 6 — unchanged, not delivered either way, both before and after.
  • The default. Two flags shipped this session and both are off. Whether either should be on is in the session's decision block, not decided here.

8. Tests and gates

New file tests/test_tie_shared_rank.py, six tests: two characterisations (green on HEAD, marked as such), and four that were red on HEAD — the ranking difference (8 vs 1 on a synthetic bundle, two different numbers rather than one predicate two branches share), the byte-identity of the default, the delivered set under the flag, and the CLI defaults.

Three tests added to tests/test_okf_consume.py for --withheld-titles: the titled form, the untitled default (set(entry) == {"concept_id", "rule"}), and the byte cost. One existing assertion was widened from a whole-dict equality to the two fields it is actually about, so an unrelated widening of the withheld form cannot fail a cost-vocabulary test.

gate before after
pytest -q 1388 passed 1397 passed
ruff check src tests tools clean clean
ruff format --check clean clean (92 files)
mypy --strict src 17 files, clean 17 files, clean