test(consume): hit@8 over six questions against a random-ranker baseline
hit@8 = 5 of 6, every hit at rank 1, against a chance baseline of 1.35 of 6 over a denominator of 629 concepts per question. Wall time 0.51-0.56 s per question; spent 17 970 - 74 838 bytes against a 120 000 limit. Two things this measurement did NOT establish, both in the report: - BOTH known-negative controls FAILED. A question the bundle has no answer to still returns eight excerpts, because no natural Norwegian question is lexically disjoint from a 629-concept corpus under a four-character shared-prefix rule -- measured per token, the interrogative `hvor` reaches 40 concepts, `brukes` 83. So `no_lexical_match` works per concept and not as a whole-question gate: an empty excerpt list is evidence of absence, a full one is not evidence of presence. The fix is named (rarity weighting) and NOT built, because this step's fence freezes the instrument before it is measured. - The question texts were written during execution, after the ranker existed. The plan recorded the gold documents' SIZE profile -- its per-row baselines sum to 1.35 and the sizes used here reproduce that exactly, which is an independent check that this is the set the plan profiled -- but it recorded no question texts, and three of six gold documents could not be pinned uniquely from the sizes. Not a blind evaluation, and the report says so. The scorer is a tool, not a script in a document: `tools/okf_consume_measure.py` takes the gold set as an INPUT because it is tracked in a public repository and an answer key names a consumer's documents. hit_rank, both chance baselines and the document-size census are unit-tested; the corpus run is a measurement. Public-file rule, checked with a pattern DERIVED from the corpus's own 39 document names rather than hand-picked, and shown able to find first (67 hits on the bundle's own index): zero corpus document names in any tracked file in this repository. One leak was found and removed on the way -- a corpus concept name in a code comment and a hardcoded corpus path in a test. Suite run after git add: 1230 passed, mypy --strict clean on 27 files, ruff clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
51735fa7a8
commit
d7751c0b9a
6 changed files with 598 additions and 39 deletions
185
docs/2026-09-07-okf-konsumskill-maaling.md
Normal file
185
docs/2026-09-07-okf-konsumskill-maaling.md
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
# The consumption pre-pass, measured — hit@8 on a 629-concept bundle
|
||||
|
||||
**Date:** 2026-09-07 · **Instrument:** `tools/okf_consume.py` at `51735fa`,
|
||||
scored by `tools/okf_consume_measure.py` · **Corpus:** a 629-concept, 39-document
|
||||
OKF v0.2 segmented bundle built by this library's own doors on 2026-09-03.
|
||||
|
||||
This is the first time `docs/consumption-contract.md` has been run against a
|
||||
bundle. The contract has existed since S5 with no instrument, and a rule with no
|
||||
instrument is a rule nobody can be shown to have broken. What follows is that
|
||||
instrument's first numbers, with the two things a first number needs: the
|
||||
denominator it was measured over, and the chance baseline it should be read
|
||||
against.
|
||||
|
||||
**Read the headline with its granularity attached.** The order this work came
|
||||
from asked for "≥ 5 spørsmål der **fasit-konseptet** er kjent" — gold at
|
||||
**concept** granularity. The operator answered the open question on it
|
||||
(2026-09-07 04:33Z) with option (a): gold is the top-level **document** the
|
||||
question names, taken from the corpus's own document titles and never from
|
||||
anything the ranker computes. **Five of the six rows below are therefore
|
||||
DOCUMENT-granularity, which is a weaker claim than the order asked for.** One row
|
||||
— the price form — has a gold document holding exactly one concept, so document
|
||||
and concept granularity coincide there, and it is the only concept-level result
|
||||
in this report.
|
||||
|
||||
## The result
|
||||
|
||||
| figure | value |
|
||||
|---|---|
|
||||
| questions | 6 positive + 2 known-negative controls |
|
||||
| **hit@8** | **5 of 6**, every hit at **rank 1** |
|
||||
| expected by chance | **1.35 of 6** |
|
||||
| denominator | **629 concepts** per question, every one accounted for |
|
||||
| `k` | 8 |
|
||||
| known-negative controls | **2 of 2 FAILED** — see below |
|
||||
| `spent` per question | 17 970 – 74 838 bytes (median 20 182), limit 120 000 |
|
||||
| whole payload per question | 109 951 – 165 109 bytes |
|
||||
| wall time per question | 0.51 – 0.56 s (629 concepts, 1.95 MB of concept text) |
|
||||
|
||||
Five hits at rank 1 against an expectation of 1.35 is a real signal and a small
|
||||
one: **n = 6**. Nothing here supports a claim about the ranker's behaviour on a
|
||||
seventh question, and this report makes none.
|
||||
|
||||
### Per-row, with the baseline each row must be read against
|
||||
|
||||
The per-question gold ids are **not in this file**. They name documents in a
|
||||
consumer's corpus, and this repository keeps corpus paths and document titles out
|
||||
of tracked files; they live in the local project directory beside the run, and go
|
||||
to `portfolio-optimiser` by coord. What is publishable is the shape:
|
||||
|
||||
| row | gold concepts | hit@8 | rank | chance, analytic | chance, empirical |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 — **the concept-level row** | 1 | ✅ | 1 | 0.013 | 0.013 |
|
||||
| 2 | 49 | ✅ | 1 | 0.479 | 0.483 |
|
||||
| 3 | 20 | ✅ | 1 | 0.229 | 0.231 |
|
||||
| 4 | 43 | ✅ | 1 | 0.434 | 0.438 |
|
||||
| 5 | 11 | ✅ | 1 | 0.132 | 0.134 |
|
||||
| 6 | 5 | ❌ | — | 0.062 | 0.063 |
|
||||
|
||||
**Why every row carries its own baseline.** A document-prefix hit is *easier for
|
||||
a large gold document*: with gold sets spanning 1 to 49 concepts, a raw count of
|
||||
4/6 could be produced by a ranker that had learned nothing. Row 2 alone is 48 %
|
||||
likely by chance; row 1 is 1.3 %. The two hardest rows (1 and 6, at 0.013 and
|
||||
0.062) are one hit and one miss.
|
||||
|
||||
The empirical baseline is 20 000 draws at seed 20260907. It agrees with the
|
||||
analytic form to within about half a percentage point — **not** to three decimal
|
||||
places, and this report does not claim it does.
|
||||
|
||||
**The miss, diagnosed rather than absorbed.** The gold's best concept ranked
|
||||
**11 overall**, just outside `k = 8`, and comes back at rank 11 for `k = 12` and
|
||||
`k = 16`. Cause: the profile matches two tokens when they share four leading
|
||||
characters, so a compound question term matches two much larger sibling
|
||||
documents in the same subject area, and those two fill all eight slots. The
|
||||
document prior does not rescue it (0.333 against 0.433 and 0.415). This is a
|
||||
near-miss at the reported `k`, not an absence — and it is the shape of failure to
|
||||
expect from this ranker: crowding by large same-prefix neighbours.
|
||||
|
||||
## The controls that failed, stated first rather than last
|
||||
|
||||
The order required a known-negative: a question with no answer in the bundle
|
||||
must produce an empty or "not found" payload, never a confident guess. **Both
|
||||
known-negative questions produced eight excerpts.** The control failed.
|
||||
|
||||
| control | tokens reaching nothing | delivered |
|
||||
|---|---|---|
|
||||
| the question chosen as planned | 2 of 6 | 8 |
|
||||
| the most lexically disjoint question found | 4 of 6 | 8 |
|
||||
|
||||
**Why, measured rather than guessed.** The pre-pass has a per-concept relevance
|
||||
rule (`no_lexical_match`) that withholds a concept the question does not reach,
|
||||
and it works: it fires on 600-odd concepts per run. It does **not** work as a
|
||||
whole-question gate, because no natural Norwegian question is lexically disjoint
|
||||
from a 629-concept corpus under a four-character shared-prefix rule. Measured
|
||||
per token: the interrogative `hvor` reaches 40 concepts, `mange` 17, `brukes` 83,
|
||||
`etablert` 38. Four candidate questions were probed and none reached zero on
|
||||
every token. The first control's content words did genuinely match — one of them
|
||||
reached 17 concepts describing welded seams in floor covering, which is a correct
|
||||
lexical answer and a useless topical one.
|
||||
|
||||
**Consequence for anyone using this, stated in the skill as well as here: an
|
||||
empty `excerpts` list is evidence of absence; a full one is not evidence of
|
||||
presence.** When the delivered excerpts do not answer the question, the right
|
||||
output is `[sourced-not-sufficient]` plus a statement that the cut found nothing
|
||||
responsive — not eight answers.
|
||||
|
||||
**What would fix it, named and not built.** Rarity weighting: a token matching a
|
||||
large fraction of the corpus carries little information, and requiring a
|
||||
delivered concept to match at least one *informative* token would make the gate
|
||||
work. Smoothed IDF is on the plan's own list of mechanisms worth lifting and
|
||||
this build does not implement it. It is not attempted here because this step's
|
||||
scope fence freezes the instrument before it is measured — tuning a ranker
|
||||
against the questions it is being scored on produces a number about the tuning.
|
||||
|
||||
## Honesty limits
|
||||
|
||||
Six, and the first three matter most.
|
||||
|
||||
1. **n = 6.** Six questions is a small sample and the table says so on every
|
||||
reading. The chance baseline is what keeps it from being meaningless, not
|
||||
what makes it large.
|
||||
2. **The question texts were written after the ranker existed.** The plan fixed
|
||||
the gold documents' *size profile* before any ranker existed — its recorded
|
||||
per-row baselines sum to 1.35, and the sizes used here reproduce that total
|
||||
exactly, which is an independent check that this is the gold set the plan
|
||||
profiled. But the plan recorded no question **texts**, so those were written
|
||||
during execution. Three of the six gold documents also could not be pinned
|
||||
uniquely from the recorded sizes and were chosen among equal-sized
|
||||
candidates. The gold documents themselves came from the corpus's own titles,
|
||||
per the operator's answer, and not from anything the ranker computes — but
|
||||
this is not a blind evaluation and must not be quoted as one.
|
||||
3. **One ranking change was made with a gold answer visible.** The stage-one
|
||||
document prior originally *summed* lexical overlap across a document's units,
|
||||
which measures document size: measured on this corpus, a 79-concept document
|
||||
scored 6.0 (0.076 each) against the price document's 2.0 over 1 concept, and
|
||||
the price gold fell outside the top 8. The prior is now a density. The fix is
|
||||
justified by the scoring function's own arithmetic rather than by the answer
|
||||
— a prior that grows with size is measuring size — but it was found by running
|
||||
row 1, and the ranker is therefore not blind to that row.
|
||||
4. **Five of six rows are DOCUMENT granularity**, not the concept granularity
|
||||
the order asked for. Row 1 is the exception and is labelled throughout.
|
||||
5. **Two of the three states this contract makes consumers distinguish have
|
||||
denominator zero in this corpus.** `adjudicated` never occurs (all 618 present
|
||||
values are `proposed`; 11 concepts carry no key and read as `unknown`), and
|
||||
`verified` is absent on all 629 — anchored and unanchored — so every excerpt
|
||||
is `unverified`. `type: verdict` is likewise absent on all 629, which makes
|
||||
the § 9.1 exclusion **vacuous here**. All three are exercised only against a
|
||||
synthetic fixture, and the skill states that rather than implying coverage.
|
||||
6. **The contract checker is a lint, not the acceptance.** Measured: it returns
|
||||
0 findings on a payload declaring `considered=0, withheld=0, delivered=0`
|
||||
paired with the *unfilled* template. A green checker means the payload carries
|
||||
what a claim would have to rest on — not that the claim is sound. The real
|
||||
gates are the negative controls, the partition assertion, the placeholder scan
|
||||
and the digest test, and none of those is something the checker performs.
|
||||
|
||||
## What the contract's own gates reported
|
||||
|
||||
| gate | result |
|
||||
|---|---|
|
||||
| SS 5.2, `considered == withheld + delivered` | closes on every run, by construction |
|
||||
| SS 5.3, every withheld concept names its rule | 621 of 621 on the row-1 run, from a closed set of six |
|
||||
| SS 7.4, the instrument reproduced a known-positive before reporting | 10 349 expected, 10 349 measured, plus a second route (`wc -c` = 10 060, delta 289) |
|
||||
| SS 7.2, `spent` is the **delivered set** | 74 838 spent against a 120 000 limit on row 1 — where a whole-payload reading would have put 165 109 against the same limit and refused |
|
||||
| SS 3.3, the ref is a content identity, not a declared version | `sha256-tree:` over the reachable tree; `--ref` asserts and never overrides |
|
||||
| SS 9.2, no directory enumerated | the index walk reaches 629 concepts, exactly the set the forbidden method finds |
|
||||
| contract check on a real payload | `conformant: 14 rules over 8 excerpts and 621 withheld entries, 0 findings`, exit 0 |
|
||||
| negative control: broken identity | `NOT conformant`, 2 findings, exit 1 |
|
||||
| negative control: missing payload | exit 2 |
|
||||
|
||||
## Reproduce
|
||||
|
||||
```sh
|
||||
# One payload, and the checker on it.
|
||||
.venv/bin/python tools/okf_consume.py <bundle> \
|
||||
--question "Hvordan skal prisene fylles ut?" --out payload.json
|
||||
.venv/bin/python tools/okf_contract_check.py \
|
||||
--skill skills/okf-consume/SKILL.md --payload payload.json
|
||||
|
||||
# The whole table. The gold set is an INPUT and is not in this repository.
|
||||
.venv/bin/python tools/okf_consume_measure.py <bundle> --questions <gold-set.json>
|
||||
```
|
||||
|
||||
The gold-set file's shape is in `tools/okf_consume_measure.py`'s module
|
||||
docstring. Determinism is asserted by the suite, not by inspection: two runs of
|
||||
the same `(bundle, question, k, limit)` produce byte-identical output, and the
|
||||
module reaches no clock.
|
||||
Loading…
Add table
Add a link
Reference in a new issue