Commit graph

298 commits

Author SHA1 Message Date
81d7194e20 docs: measurement prose names no document by its numbers — green
Section totals, a page count and build sizes identified one reference
document and one generated HTML corpus to anyone who knows them. They are
written as N, M, an offset of N or a share in docstrings, comments, help
text, reports, README, CLAUDE.md and the changelog; no measured relation
changes. Sums that let N be worked back are written in words. The
`okf quality --fasit` bar is 999/1000 (99.9 %), just under the known-good
arm's measured share, and every build measured keeps its verdict.

Red with the local list widened to those numbers: 52 tracked files. Green: 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 14:52:03 +02:00
ea8590db82 test(quality): the boundary bar is a round share that keeps every verdict — red
The bar was the known-good arm's own measured fraction, which names the
document it was read off. The test asks for 999/1000 and for the verdict
of every recorded build to stand: the known-good arm passes; an older build
at 99.7 %, the known-bad arm at 41.6 % and five of six fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 14:52:03 +02:00
88cf67f12e test(fixtures): the STS fixtures and fixture codes are fictitious
Three STS fixtures still carried the section titles and labels of one real
reference document, and three identifiers were copies of its codes with a
letter or a word swapped. They now describe an invented kitchen counter and
cookbook series: the titles, labels and descriptions of sts-identity.xml,
sts-inherit.xml and sts-empty-label.xml, the P350/P351 document codes, the
99-0001 delivery prefix and chapter 7 of the image and accounting corpora.
Generated fixtures are regenerated and the witness inventory's per-document
totals are identical before and after; only names and text move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 14:52:03 +02:00
5359394a4f test(hygiene): a code term is found beside an underscore or a letter
A term bounded by `\b` cannot see a code written as `X_...` or
`test_x...s`, because `_` is a word character. The local list's code and
word terms now count only letters and digits as a neighbour. Red with the
tightened list: 3 tracked files. The three lines are rewritten: green, 0.

The tracked side gains a synthetic known-positive for that shape and a
known-negative list of ordinary words and codes that stand next to a term's
shape, so a list grown too wide fails in the test and not on a later file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 14:52:02 +02:00
9d1f4b14ed test(fixtures): replace sector-specific example material with generic, fictitious examples — green
Every fixture, test document, tool example and document now uses an invented
kitchen-and-baking handbook series, written in this repository. The package's
behaviour is unchanged; src/ changes are comments and help text only.

- Generated fixtures are regenerated from their generators. Their structural
  counts are identical before and after: elements, images, rows, cells,
  headings, bookmarks and the witness inventory's per-document totals. The
  image-inbox and accounting documents are renamed kapittel-84-*.
- tools/okf_accounting_gate.py: the two options that named one real corpus
  each are replaced by a generic, repeatable --corpus PATH with no default.
  Row 5 compares the PDF pair alone. Gate verdict unchanged: RED rows 2, 3, 6.
- tools/okf_witness.py: the STS JSON reader for one publisher's delivery is
  removed, along with its three twins and five tests. The mutation harness
  loses W09.
- docs/: 13 dated reports that documented runs on a retired reference corpus
  are removed, and 40 are neutralized. Dead links are removed, and no new
  dangling path is introduced.
- The synthetic MCP-gate corpus and the residual probe words are neutral.

Valgt: keep the `okf quality --fasit` bar value (the measured fraction, one corpus) and
rewrite only its provenance, because the verdict stays unchanged and the
number names nothing.

Term check with the local list: 0 of 411 tracked files, 0 file names, 0 of
27 binary fixtures. Suite after git add: 2457 passed, 1 skipped. The base
tree had 2460 passed and 2 skipped; five tests went with the JSON reader and
four were added by the term check. ruff, ruff format and mypy --strict src/
are clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 14:52:02 +02:00
1e7345a401
test(hygiene): no tracked file may carry a term from the local exclusion list — red
Sector-specific example material is to be replaced by generic, fictitious
examples; this test keeps it replaced. Every tracked file's path, text and the
text a reader gets out of a binary fixture (zip members, inflated PDF streams)
is matched against a term list kept in a git-ignored local file
(`*.local.txt`). Without the file the check is skipped with a message, never
passed; the scanner's known-positive runs on a synthetic term either way. An
empty exception list is tested as a list.

Red on af2d1d3 with the list in place: 161 of 426 tracked files, 11 file
names, 12 of 27 binary fixtures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 12:32:39 +02:00
af2d1d350e
test(retrieval-gate): register the hold-out set for row 5 with ~/ paths
Same set, same sha256, same threshold and `threshold_written_at`, the same
`bundle_ref` (measured again on the durable bundle and identical), `readings`
empty. Only the two paths move from this machine's absolute form to `~/...`,
and `registered_by` names the order that did it. No share was read: the set
is absent on this machine, and row 5 stops before reading it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 10:24:41 +02:00
9787b7fc93
test(retrieval-gate): row 5 must name no machine and measure the bundle it registered — red
The hold-out registration is committed to a public repository, so its paths
must be written under the home directory (`~/...`) and expanded when read; an
absolute path is a NO with its reason. And the bundle it names is the
registered bundle only while its tree measures the registered `bundle_ref`.

Red on ef76a24: an absolute path is followed, `~/` is never expanded, no
line compares the bundle's ref, an absent path is not named. Existing row-5
tests now write `~/` registrations under a `home` fixture and count twelve
checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 10:22:47 +02:00
ef76a24d72
test(propose-segments): the arm-attribution test reads --help without colour
Python 3.14's argparse colours `--help` when FORCE_COLOR is set, and a
background session inherits FORCE_COLOR=3. An escape code then stands
before ` --outline-run`, no option chunk starts with the flag, and
test_each_arm_flag_carries_its_attribution_inside_its_own_option_chunk
fails -- also on v1.1.0. The test now sets PYTHON_COLORS=0, the variable
_colorize.can_colorize reads first, so it outranks FORCE_COLOR and
NO_COLOR alike; what --help says is unchanged and src/ is untouched.

Chosen over stripping ANSI codes before comparing, because that would
restate Python's escape grammar in the test.

Measured, full suite: before the fix FORCE_COLOR=3 gives 1 failed / 2449
passed / 1 skipped, and this is the only colour-dependent test. After
git add: 2450 passed / 1 skipped both with FORCE_COLOR=3 and without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 10:10:15 +02:00
8ba32a0278
test(retrieval-gate): register the hold-out set for row 5 again, at a durable bundle path
The registration row 5 reads, in a commit of its own and after the
commit that lets row 5 read the set's own schema. It replaces the one
on okf-holdout-registrering (7b0ce41), which is left unmerged: that
registration pointed at a bundle in a job's temporary directory, which
disappears with the job.

The set is the one written blind in claude-code-llm-wiki (order
4350316446, commit 33735b4), pinned by sha256. The bundle is the same
bytes row 8 measures (v1.1.0, --gate none), copied to
~/okf/gate-bundles/claude-code-wiki-7141ab3f with its ref measured
identical to the source (sha256-tree:7141ab3f...) and diff -r empty.
Threshold 0.8 questions answered over questions asked, written
2026-09-23T06:56:47Z with no reading taken; readings stay empty.

No gate code, no src/, no MUTANT_BAR moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 08:59:31 +02:00
3aff2ca0aa
test(retrieval-gate): row 5 must read a hold-out set in the schema it declares — red
The registered hold-out set is written in a consumer's own question-set
schema, the one row 8 already reads, and row 5 reads every set with the
gate's synthetic reader: "not a question set this gate can read:
'set_id'", a NO no set in that schema can turn.

Three tests over an invented set in that schema and the synthetic
bundle: 4 of 5 clears 0.8 and falls under 0.9 through the same code
path; an unknown schema is a NO that names the schema; and the row
prints the share and never which hold-out question missed, with the
share itself as the known-positive in the same run. All three fail on
cb0c10b with the GateUsage above.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 08:37:00 +02:00
cb0c10b421 fix(retrieval-gate): the judge reads a passage as its exact reconstruction — M15 felled, row 7 15 of 15
Row 8's judge required every delivered text to be the whole concept's bytes
(the row-6 identity), which is older than v1.1 C1: an excerpt `as_passage` cut
to its answering passage was class e even when its span carried the citation.
PM's re-measurement of e503f6a found that the dominant miss class of row 8 on
the real set.

`passage_span` accepts a passage only when the delivered text IS what the
bundle's bytes rebuild: the span is body[start:end] byte for byte in the
DELIVERED body (the offsets land a frontmatter's length off in the file), `of`
is that body's length, `[...]` stands exactly where text is left out, and the
one other line allowed is a heading line of the body above the span, or a
prefix of one. The citation is read in the span alone. A passage that is not
its reconstruction is class e with its own detail. A quote only in the heading
or the markers is class d.

Chose to check that the heading is one of the body's own heading lines above
the span, and not to re-derive WHICH heading `as_passage` picks, because the
guarantee is "the bundle's bytes"; the nearest-heading rule is the product's
presentation and a judge that copies it agrees by construction.

M15 (a passage carrying one sentence the file does not) is row 7's fifteenth
mutant. No synthetic concept was long enough to be cut, so a new fixture
delivers one (DELIVERY, ~6 600 characters, set set-passage.json); the corpus
pin moved with it. MUTANT_BAR unchanged, src/ untouched.

Gate, synthetic: rows 1 and 6 13/13 -> 14/14, row 7 14/14 -> 15 of 15 GREEN
(M15 -> row 1, 6), rows 2/3/4 unchanged, GATE RED: rows 5, 8. Row 8 rerun on
the real set: no class e miss remains (its numbers are kept out of this public
history, per the wiki directive). Suite after git add,
FORCE_COLOR unset: 2450 passed, 1 skipped (+6). ruff, format, mypy --strict
clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-22 23:53:09 +02:00
70c388d419 test(retrieval-gate): row 8's judge must read a passage as a passage — red
Since v1.1 C1 a concept over PASSAGE_CHARS is delivered as its answering
passage (heading, [...], span, [...]), and the judge still demands the whole
body (the row-6 identity), so every such delivery is class e even when the span
carries the citation. PM's re-measurement of e503f6a found this to be the
dominant miss class of row 8 on the real set.

Pinned before the judge moves, on an invented one-concept bundle:
- a passage whose span carries the quote is a HIT; the offsets count in the
  DELIVERED body, and a test asserts that reading them in the concept file
  misses (trap 1);
- the same passage with one invented sentence, in the span or between the
  marker line and the span, is class e, named (trap 2: "the span occurs
  somewhere in the text" would count it);
- a quote the passage carries only in its heading or its markers is class d,
  beside the known-positive in the same run;
- row 7 carries M15, passage cheating, as its fifteenth mutant.

Fails on e503f6a: five assertion failures and one ValueError (M15 does not
exist yet); no collection error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-22 23:53:05 +02:00
e503f6abd7
test(retrieval-gate): three fixtures make M06, M07 and M10 decide a delivery — row 7 14 of 14
Row 7 left three mutants standing after v1.1 (11 of 14, bar 13): the passage
signal reading no body, the field signal weighing no title and no path, and a
flattened fusion (bm25.RRF_K = 10 000). Each switches off a mechanism the
default ranking runs, and each moved 0 ranks, because every synthetic concept
was short and opened with its own title -- none of the three ever decided a
delivery here.

Three bundles, one per mechanism, and one pinned set (set-mechanisms.json):

- PASSAGE: a long concept answered in one window of its body, against ten
  short concepts whose titles carry the question's words. Rank 1; with no
  body windows it falls out of k (class b).
- PATH: a concept named by its path alone, against ten decoys denser in the
  body. Rank 4 at k = 6; with no title/path weight it falls out of k.
- FUSION: gold 1st in the passage signal and 21st in the field signal, one
  decoy 10th and 11th (rank sum 19 < 20). At RRF_K = 60 the gold leads, and
  it keeps the lead through K = 180 (measured in steps of 10); flattened, the
  decoy wins k = 1.

Chose separate fixtures over one combined one because each fixture's comment
can then name the single mechanism it pins. Verified per question against all
14 mutants: no previously felled mutant is lost. src/ is untouched, MUTANT_BAR
and the roster are unchanged, the corpus pin moved with the corpus.

Gate: rows 1 and 6 go 10/10 -> 13/13, row 7 11/14 -> 14/14 GREEN, rows 2/3/4
unchanged, GATE RED: rows 5, 8. Suite: 2444 passed, 1 skipped (+3), measured
with FORCE_COLOR unset -- with FORCE_COLOR=3 in the environment Python 3.14
colours argparse help and test_each_arm_flag_carries_its_attribution fails,
independently of this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-22 22:41:01 +02:00
09d7e6ee08
test(retrieval-gate): row 7 must fell M06, M07 and M10 — red
The three mutants that survive v1.1 each switch off a mechanism the default
BM25 ranking runs: the passage signal's body windows, the field signal's
title and path weight, and the rank-fusion constant. A gate that cannot see
them removed cannot see them break. One test per survivor, driven through
the gate's own `deterministic_rows` on the pinned sets and corpus: it fails
today with an AssertionError (no row got worse), and the same test passes
for M04, which the gate already fells.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-22 22:19:12 +02:00
21f9241712 feat(check): the checker and the contract read a folder's reply
`okf check --payload` takes the reply to one call over a folder as well
as a single payload: every bundle's payload is held to all 19 rules on
its own, a finding is named with its bundle, one every payload carries
alike is reported once, an answer labelled with a bundle its payload
does not describe is `answer_misattributed`, and a reply with no answer
is `payload_invalid`. No rule is added, and a single payload's report is
unchanged. Contract SS 2.5.4 names the folder run and SS 8.11 fixes the
reply; the known-positive moves to 24 620 / delta 592.

The skill text follows: the working method's steps 1 and 4 name the
folder, and the generic skill says to use the server's tools first where
they are registered, with the skill as the supplement. The folder is an
instruction in both generators, never a path: the bundle's parent
written absolute named this checkout, and the test holding generated
commands to no repository path fell on it.

v1.1 order F, part F4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 10:50:14 +02:00
570496470b docs(project): the server is the standard way in, the skill the supplement
`okf project`'s closing text and the README's first screen now say it in
that order: register `okf mcp --root` once, and it answers from every
project and reaches subagents; the skill beside the bundle is for someone
who would rather register nothing; neither is made again when a bundle is
rebuilt. Two tests hold the order in both places. The README's stale
`<id>-consume` skill path is corrected to `okf-consume-any`.

v1.1 order F, part F3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 10:36:52 +02:00
718c064279 feat(consume): one call over a folder asks every bundle under it
`okf consume <folder>` is the server's `okf_ask` with no bundle named,
byte for byte: no ranking of its own, every excerpt carrying its bundle
id. `--bundle-id` asks one bundle under the folder. A flag that acts on
one bundle's cut is refused by name over a folder rather than dropped,
because the server takes none of them. A bundle path reads as before.

v1.1 order F, part F2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 10:29:10 +02:00
df83c65e32 feat(card): a folder shows every bundle under it, as the server does
`okf card <folder>` prints `okf_list` and `okf_describe` with no bundle
named, joined by `mcp_server.overview`, and computes nothing of its own:
one source, two doors. Chosen over a separate `--root` flag because the
skill's first step must be the same command whether it was pointed at a
bundle or at a folder of them; the rule that decides is discovery's own
(a directory carrying an `index.md` is a bundle). A bundle path prints its
card as before.

v1.1 order F, part F1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 10:28:32 +02:00
3d149f955a test(gates): retire the K2 track; re-measure the retrieval gate's premises for BM25
Operator decision 2026-09-21: the test track built on material tied to the
operator's employer (K2) is retired -- not re-measured, not frozen. Public
tests and gates run on invented material.

Retrieval gate:
- The four FUSION_PREMISE xfails are gone and pass through their INPUTS: the
  synthetic MISS, LOOKUP and QUOTA bundles were re-measured for BM25 (the
  miss fasit no longer shares the rare word `maa`; lookup and quota decoys
  carry the question's words so each partition and the quota decide their
  own fixture). SPECS_SHA256 moved with them. Rows 2 and 3 green again.
- Row 7's mutants M04, M06, M07, M08, M10 now patch `bm25`, the code the
  default runs. Three survive with 0 ranks moved (passage body, title
  weight, bm25.RRF_K), each with its mechanism printed. M07 was not forced:
  every synthetic body carries its title as a heading.
- Row 9 (K2) removed; row 8 requires `wiki-20` alone, the `r761` and
  `vegnormal` adapters are gone. Chose the broad reading of "K2" because the
  operator decision defines it as employer-tied material and the order's
  grep includes `vegnormal`.

Also removed: tests/test_default_bundle_pin.py, the K2 arms of
test_okf_consume, the four real-arm tests of test_quality, the R761 soft
hyphen test, the N101/N200 delivery tests and okf_accounting_gate's default
real corpus (and H5's guard, which only existed for those defaults). Two
fixtures carrying road-standard identifiers are rewritten with invented ones.

Gate after: 1 10/10, 2 7/7, 3 5/5, 4 6/6, 5 0/1, 6 10/10, 7 11/14,
8 NOT RUN -> GATE RED: rows 5, 7, 8. Suite 2423 passed, 1 skipped,
0 xfailed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 10:05:43 +02:00
977040f575 feat(check): the contract and the checker state the payload's v1.1 forms
C6. `docs/consumption-contract.md` now says what the pre-pass emits:

- SS 2.5 point 2: sub-questions are written in the bundle's terms and given to
  the pre-pass in ONE run where it takes several;
- SS 8's example is `okf-consumption/2` as shipped: `withheld` is the
  `{total, by_rule, nearest, complete}` mapping, `coverage` carries
  `absent_terms` and `weak`, an excerpt carries `text`, `text_sha256`,
  `passage` and `own_title`; point 1 closes the identity on `withheld.total`;
- point 7 keeps the falsified verdicts on record and states the one reading
  carried since v1.1, `weak`, with its rule: a word held in no form, or
  nothing delivered -- whether a word exists at all, which does not move with
  question style or corpus size the way the two shares did;
- point 8 the passage (`{start, end, of}`, `text_sha256` over the passage,
  `sha256` still the file's), point 9 several sub-questions in one run (one
  cut, `questions`, `subquestions`, per-sub-question coverage), point 10 a
  title inherited from the heading above, with `own_title`;
- SS 10 names this library's default ranking (BM25) and `--ranking fusion`.

`okf check` holds the two new forms a reader acts on: `passage_malformed`
(not whole numbers with 0 <= start < end <= of) and `subquestions_unindexed`
(not distinct indices into `questions`, or indices in a payload listing none).
17 -> 19 rules; the two tests that pin the published count move with it.
Each rule is held against a real payload (0 findings) and against that
payload broken six ways.

Editing the contract moved the SS 7.4 known-positive, measured once after the
edit: 19 837 -> 23 672 encoded, 19 358 -> 23 092 raw, delta 479 -> 580. The
example payload and `skills/okf-consume` are regenerated by the published
recipe and `okf check` reports 19 rules, 0 findings on them.

Suite on a clean tree after `git add`: 2443 passed, 2 skipped, 4 xfailed.
ruff, ruff format, mypy --strict clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 08:47:46 +02:00
da6faf8776 feat(mcp): the bundle's map, and a working method that reads it first
C5. `bundlemap.build_map` lists a bundle in its own words: one line per
source document -- its name, then the titles of its concepts in document
order -- and documents whose names differ only in their numbers (a changelog
per release, a note per week) as ONE line: the name with every number as `#`,
the count, the first and last by natural order, and the titles across the
series that are words. `SERIES_MIN` = 5, at most `TITLES_PER_LINE` = 24 titles
a line, the lines capped at `MAP_MAX_BYTES` = 48 000 together with
`lines_truncated` counting the rest. Derived on every call, never stored.

The card (`okf card`, `okf_describe`) carries it as `map` and no longer
carries `source_files`: that list named every document a second time with no
series collapsed, a quarter of the reply on a large bundle, for names the map
already carries. Chose removal over keeping both because the describe reply
has to fit a client's tool-reply limit and the map says more.

The working method now reads: take the map first (`okf card`, or
`okf_describe`), write two to four sub-questions in its words, and send them
in ONE call (`--question` repeated, or `okf_ask` `questions`). Changed in the
skill template, the generated `skills/okf-consume`, and the server
instructions (held under the 2 KB a client keeps). The regeneration recipe for
`skills/okf-consume` gains `--for-bundle`: since v1.1 the generator writes the
generic skill by default, so the recipe as published produced the other file.

A test holds a four-sub-question `okf_ask` over concepts far over the passage
size under 50 000 bytes of reply text (25 000 tokens at a pessimistic two
bytes a token). The real-collection measurements are kept in local state.

Suite on a clean tree after `git add`: 2429 passed, 2 skipped, 4 xfailed.
ruff, ruff format, mypy --strict clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 08:37:12 +02:00
f7cd84c5e6 feat(consume): a table fragment is read under the heading it stands under
C3, the remaining half. The proposer names a table block with no heading of
its own after the line it starts on (`Tabell linje <n>`). A position, not a
name: searched, it matched no question, and shown, it told a reader nothing
about what the table is.

`consume.inherit_table_titles` reads such a concept under the title of the
nearest concept ABOVE it in the same source document -- in reading order, the
heading the table stands under -- ordered by `source_offset`, else
`source_lines`, one key per document and never mixed. Applied where the bundle
is loaded, so the ranking, the excerpt and the near misses all see the same
title; the excerpt keeps the file's own as `own_title`, so the name shown is
never mistaken for the one in the file. A table with nothing named above it,
or a document whose concepts do not all carry one position key, is left as it
is.

Chose to do it in the reading and not in the build: no bundle byte moves, no
collection has to be rebuilt, and the proposer's goldens stay pinned.
`consume.MECHANICAL_TITLE` is held against the proposer's own output by a test.

Suite on a clean tree after `git add`: 2418 passed, 2 skipped, 4 xfailed.
ruff, ruff format, mypy --strict clean. The search gate's table for the
previous commit is kept in local state.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 08:26:20 +02:00
80aac93b8f feat(consume): several sub-questions in one call, merged by the product
C2. `consume.build_multi_payload` takes two or more questions, reads the
bundle ONCE (`bm25.prepare` builds the index a question does not depend on),
ranks and cuts each sub-question exactly as `build_payload` would alone, and
interleaves the deliveries: first excerpt of each sub-question in turn, then
the second, a concept already taken skipped, cut at the same `k` and `limit`
one question gets -- so asking four times does not buy a payload four times
the size.

Chose round-robin, not a merge by score, because two questions' BM25 totals
are not on one scale: a merge by score would let the wordiest sub-question
take every place. It is the rule the search gate measured with before the
product had it, moved unchanged.

Shape, and only for two or more questions (one question is `build_payload`'s
payload byte for byte):

- `questions` replaces `question`;
- every excerpt carries `subquestions`, the indices of every sub-question
  whose own delivery named it, the one whose text (passage) it carries first;
- `coverage` holds one block per sub-question (the single shape plus its
  `question`, `unanswered_in_payload` read against what the reader receives),
  `weak_subquestions`, and `weak` true only when EVERY sub-question is weak;
- `withheld` is every concept the merge did not deliver: `below_k` where a
  sub-question delivered it and the merge's cut did not, otherwise the rule
  of the sub-question that ranked it best. `nearest` walks the rankings in the
  delivery's turn order. The contract checker accepts it with 0 findings.

`okf consume --question A --question B` and `okf_ask` with `questions` (both
forms at once is `question_ambiguous`) reach it. A reservation or a fusion
widening acts on ONE cut and is refused with several questions
(`subquestions_flag_conflict`).

The search gate's series (e) and (f) now ask ONE call with every
sub-question; the gate's own merge is gone. Sets and thresholds untouched.
The gate's table for this commit is kept in local state.

Suite on a clean tree after `git add`: 2411 passed, 2 skipped, 4 xfailed.
ruff, ruff format, mypy --strict clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 08:04:29 +02:00
ab6e24aa22 feat(consume): the payload says when the bundle looks like it does not cover the question
C4. `coverage` gains two keys, and every key it had keeps its bytes:

- `absent_terms`: the question's words the bundle holds in NO form -- not as
  written, and not through a relative it uses (`bm25.query_groups`, the same
  bridge the ranking reads through);
- `weak`: true when one such word exists or nothing was delivered.

A reading with its rule in the open, never a verdict about the bundle. It is
computed for both rankings (`bm25.absent_terms` serves the fusion). The
retrieval gate's `marked` -- the one reading both gates share -- reads `weak`
beside its own bar, never instead of it; the known-negative that strips the
payload's words now strips both readings.

Words that only FRAME a question are stopwords in both languages (`how
often`, `hvor ofte`, `hva står i`, `what does it say`), and so are the
Norwegian function words spelled without their letters (`naar`, `paa`), the way
ASCII-only text writes them. Read as topic words they would be "absent" from
any collection that never uses them, which is what the synthetic sets showed
on three answered questions before the list was extended.

The working method says what to do with it, in one sentence each: the skill
template's step 3 and the MCP server's instructions (1 253 bytes, under the
2 048 a client keeps) -- rephrase in the bundle's words, and if it stays weak,
say the bundle does not cover the question.

The search gate's table for this commit is kept in local state.

Suite on a clean tree after `git add`: 2397 passed, 2 skipped, 4 xfailed.
ruff, ruff format, mypy --strict clean. Retrieval gate unchanged at the rows
the previous commit left red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 07:12:43 +02:00
735468f600 feat(consume): BM25 ranking by passage and title, a large concept delivered as its passage
C1. `okf consume` and MCP's `okf_ask` now rank with BM25 (`bm25.py`) instead
of the three-signal fusion. Two signals, fused by reciprocal rank:

- passage: every body cut into 500-character windows every 250, a concept
  scored by its BEST window -- a narrow question is answered in one place;
- field: title three times, the id path and source name twice, then the body
  -- a broad question is answered by what a section is called.

The document prior and the rarity weight are gone from the default: the first
favoured big documents full of common words, the second gave its largest
weight to a word the collection does not hold. Under BM25 such a word weighs
exactly zero. A signal that scores a concept zero adds nothing to it, and ties
share a rank, so alphabetical order lifts nothing either.

Three rules carried over from the fusion, each with its own test, because the
suite showed what BM25 alone lost:
- a directory every concept shares is not read (K3-20's defect, one signal on);
- a number a section is known by (`4.2`, `10.2-2`) is kept as one token, or
  a question naming a section by its number matches nothing in it;
- a question word the collection does NOT hold is read as the collection's
  words it shares a leading word with (`consume.tokens_match`) -- Norwegian
  inflection and compounding -- at that word's idf, never at its own.

The lookup and title-covered partitions are shared with the fusion
(`_partitioned`). `ranking="fusion"` / `--ranking fusion` keeps the old order
reachable; `--cost-vocabulary` and `--rarity-weight` widen only the fusion and
are refused with the default (`ranking_flag_conflict`) rather than ignored.

C3. A concept longer than `PASSAGE_CHARS` (4 000) is delivered as the span
around its best window, snapped to whole lines, under the nearest heading
above it, with `[...]` where text was left out. `passage: {start, end, of}`
says so, `text_sha256` covers what was delivered, and `sha256` stays the
file's, so the whole can be fetched by `concept_id`. 4 000 because eight
excerpts of it stay far under a tool response's limit even with several
sub-questions merged, while a 500-character window keeps 3 500 characters of
surroundings. The budget pays for the passage, not the file.

Tests moved with the default, each stated rather than silenced:
- fusion-mechanism tests (cost vocabulary, rarity weight, reservation, shared
  rank, the reference-bundle pins) ask for `ranking="fusion"`, the order they
  were measured on; the BM25 reading of the reference bundle is a separate
  measurement, kept in local state;
- the retrieval gate still measures the shipped default. Row 1 holds. Four of
  its premises were built against the fusion (a concept forced below k that
  BM25 now delivers, a quota that no longer decides, mutants patching fusion
  code) and are `xfail(strict=True)` until the fixtures are re-measured;
- the shipped example payload is regenerated; the shipped skill is unchanged.

README's Consume section and CLAUDE.md state the new default and that the
flags described after it belong to the fusion.

The search gate's table for this commit is kept in local state: the question
sets belong to a consumer whose content does not go on a public mirror.

Suite on a clean tree after `git add`: 2390 passed, 2 skipped, 4 xfailed.
ruff, ruff format, mypy --strict clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 06:22:56 +02:00
cf21449ddb docs: remove what this repository published about a consumer's corpus
Operator decision 2026-09-21: nothing from that consumer's collection goes out
on the public remote. The NAME stays where it is already published -- it is a
consumer of this library, named as such, and removing it would mean rewriting
published history, which this repository does not do. What goes is everything
that describes their CONTENT.

Removed across README, CLAUDE.md, CHANGELOG, four dated reports, the
consumption contract, three source modules and three test modules: their
corpus's document and page counts, the concept count of a bundle built from
it, the byte figures of a payload built from it, the question and fasit counts
and recorded score of their evaluation set, a bundle id with two content refs,
an order id naming them, and a path into their repository.

Kept, because the argument survives without the corpus: RATIOS and
percentages. A ratio is the finding -- a withheld list that is 65.5 % of a
payload is a defect at any corpus size -- and it discloses nothing about how
large anyone's collection is. Where a claim lost its denominator it now SAYS
so rather than quietly reading as unmeasured: the gate-refusal limitation in
the README states that the corpus and its counts are deliberately withheld and
points the reader at their own build, which is the number that binds them
anyway.

One integrity pin is kept and named here rather than left to be found: the
retrieval gate still pins that set by sha256, because the pin is what refuses
a self-written file in the right shape, and a checksum discloses nothing about
what it checksums. Its recorded SCORE is gone -- that was their figure about
their own corpus, and the row now says so instead of restating it.

The known-positive constants move with the contract document, as they must.
Suite green, 2372 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 04:28:15 +02:00
245ff64c3d test(soek): a red search gate measuring what the asker RECEIVES, one exit code
`tools/okf_soek_gate.py` runs the question sets through the same
`consume.build_payload` path `okf consume` and MCP's `okf_ask` use, at the
shipped k and limit, and counts on the DELIVERED excerpts rather than on an
internal ranking -- a rank nobody receives is not an answer.

Written RED: it is the finish line a later round has to make green, and a
gate that is green on the day it is written has measured nothing. Thresholds
are named constants carrying their reason in a comment. Exit 0 only when every
row holds, 1 with the full table otherwise, 2 when the collection cannot be
found -- never 0 hits against nothing. Two runs print identical bytes.

The sets and the collection are both INPUTS: nothing about anyone's corpus is
committed, and no row names a document, a question or a quote.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 04:03:11 +02:00
e3408435d0 feat(mcp): the server carries the working method, and describe fans out
**A subagent inherits its session's MCP tools; it does not inherit its skills.**
So the method A3 put in the skill reaches the main thread and no arm running
below it, and the one place every caller sees is the server's own
`instructions` and its tool descriptions. Both are truncated by Claude Code at
2 KB, and truncation is worse than rejection here -- a reader gets the first
half of a method and no sign the rest existed -- so what travels is the SHORT
form and the long one stays in the skill, which has no such cap. A test holds
it under the limit WITH a control, so the assertion is a measurement and not a
tautology.

`okf_describe` without `bundle_id` now describes every served bundle, where it
refused and `okf_ask` in the same position fanned out. The tool a caller is
told to read FIRST was the one requiring a name it did not have yet, and a
tool that refuses the call its sibling accepts is a shape a client must be
told out of band -- the configuration this server exists to remove. The named
call's shape is byte-unchanged, and so is every one-to-one server's: the
fan-out replaces an ERROR, so no caller's bytes move.

`okf project`'s closing lines and the README's first screen carry the one line
the USER runs to register the server on user scope, verified against Claude
Code's own MCP documentation (`claude mcp add [options] <name> -- <command>`).
Nothing here starts Claude Code, and the line says whose it is. Measured:
a project bundle at `<root>/<project>/.okf/<id>` is depth 3, inside
`MAX_DISCOVERY_DEPTH`, so a `--root` server finds what `okf project` wrote --
a test builds one and discovers it rather than reasoning about the walk.

A5, and it was free: `okf skill` without `--out` now refuses in the same
`refused (<code>)` form as every other refusal in this chain. The exit code
does not move -- 2 was already right, "the run did not happen" -- what was
wrong was that a caller parsing our form got argparse's line on the one flag
everybody forgets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 00:03:59 +02:00
30edd3f5d8 feat(skill): one generic skill by default, carrying a working method and an answer form
The operator built a 2313-concept bundle from one project's own documentation,
asked it a question in his own words, and judged the result unusable. The
generated skill was an audit contract: all its discipline sat on the accounting
-- markings, denominators, budget lines, source pointers -- and none of it on
understanding the question, searching again, or writing one coherent answer.
Two sentences actively forbade the second of those.

**The two forbidding sentences are gone and their replacements are tested from
both sides.** "Do not go looking for context the pre-pass deliberately
withheld" read as "one run per question", and no wording of the operator's
question put the right document inside a single run's cut -- so a rule against
a second run was a rule against finding it at all. "Not something to retry with
a narrower question" generalised a budget-refusal case into the same ban.
SS 2.2 of the contract said the first of them, so the contract moved with the
skill rather than being left to disagree with it: a second pre-pass run with
other terms, and a fetch of a concept the payload NAMED, are reachable; SS 9's
two real boundaries -- directory enumeration, the verdict layer -- are not.

**Two new sections, and the checker requires them.** `## Working method`: read
the bundle's map, put the question into the bundle's own words, split a broad
question into 2-4 sub-questions, search per sub-question, read what lay just
outside the cut and search again with its words, same method across several
bundles, then assemble ONE answer ordered by sub-question, saying which source
holds and what is not covered. `## Answer form`: the questioner's language,
plain prose, no `below_k`, no digests, no budget lines, no denominators; short
textbook-style references (document + section, plus bundle where several were
read); and the audit trail written only when the questioner asks for it or
into a document that travels without the skill. `REQUIRED_SECTIONS` follows the
template and the contract's new SS 2.5 and SS 2.6 -- never the other way round.

**The generic skill becomes what `okf skill` and `okf project` write.** A
per-bundle skill's numbers go stale the moment its bundle is rebuilt, one copy
per consuming project, and a project with two bundles installs two
near-identical skills; the generic form carries no bundle's numbers and names
`okf card` for them. `--for-bundle` is the opt-in for the instantiated copy,
which still refuses out loud on a stale pairing -- safe to keep, not enough to
keep default. `rule_bundle_identity` learned to tell a generic skill from an
unfilled template by the frontmatter name the generator writes, so the template
still fails for the opposite reason: it declares no identity because it is
unfinished.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 23:53:34 +02:00
e3169ec50c feat(consume): the withheld set is counts plus names, not one entry per concept
Measured 2026-09-20 on a 2313-concept bundle of one project's own
documentation: `withheld` held 2 305 entries = 186 440 B of compact JSON =
**65.5 % of the 284 850-byte payload**, and not one of those bytes counted
against the budget the same payload reports (`spent` was 45 192). A reader was
handed 239 658 bytes the budget line did not know about, to learn 2 305 concept
ids with nothing beside them -- the title being exactly what `--withheld-titles`
existed to buy, and which was off because buying it for 2 305 entries cost
another 37.9 %.

`withheld` is now a mapping: `total` (equal to `denominators.withheld`, so
SS 5.2's identity is unmoved and closes on the NUMBERS), `by_rule` (the same
total decomposed over the closed rule set, so "what kind of drop" is answerable
without the list), `nearest` (the best-ranked drops BY NAME, with title and
source document, so a reader who sees a near miss can ask for it) and
`complete`. The near misses are read off the ranking, not off `cut`'s output:
`cut` sorts by id so the partition is comparable, and that order says nothing
about which concept a reader might want next.

Same question, same bundle, after: **52 421 bytes, 18.4 % of the old file**.
The whole list stays reachable behind `--withheld-full`, and the two
instruments that classify EVERY miss by its rule -- the retrieval gate and
`okf_consume_measure` -- now ask for it explicitly and assert `complete`
rather than assuming it. `--withheld-nearest N` sets the cap (default 20,
which is `k` plus the next twelve). `--withheld-titles` is retired: a flag
whose only remaining effect would be to STRIP the title from a list the caller
asked for in full names no decision worth two shapes for one list.

`CONTRACT_REVISION` moves to `okf-consumption/2`, because a consumer indexing
the old key as a list would otherwise break silently. Three checker rules move
with it, and one of them is the interesting case: `parent_unfollowable` used
`excerpts` + `withheld` as the bundle's own denominator, which a truncated
block is not -- so that clause now runs only where the payload SAYS it is
complete, stated in SS 8.6 rather than left as a silence, with the other two
clauses (shape, self-reference) running either way. `Report` carries both
denominators, because a report claiming it examined 2 305 entries it never saw
is the same defect one level up.

The generated skill's "breaking point" section goes with it: it extrapolated a
concept count from the cost of ONE withheld entry, and there is no such slope
any more. It now states what this bundle's bookkeeping cost and that the block
is bounded by the cap rather than by the bundle -- an extrapolation from a
slope the code no longer has would be a measurement of the previous revision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 23:32:22 +02:00
1c97e57212 feat(project): --gate reaches the build, and the bundle says which one
`project.create` called `build()` with five keyword arguments and no `gate=`,
so `okf project` screened by the package default and nothing anywhere said
that was a choice rather than the only option. The gate is not a segmentation
rule -- it is a screen about whether a document may be persisted at all -- so
it is the one flag this command owns that may move a bundle's bytes, and the
module docstring, the README paragraph and the test file's own claim are
corrected rather than left standing beside the new flag.

The default is `okf build`'s default, so an unflagged `okf project` is the
bytes it always was; the byte-equality invariant against `okf build` holds
unchanged. An unknown gate name still raises `gate_invalid` rather than
falling back -- a fallback reproduces the defect with an extra step.

Tests first, all three red: the gate name read back out of the bundle's own
`log.md` (not out of the call), the flag parsed by this command's parser, and
the unknown name refused.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 23:08:50 +02:00
404bed26e2
feat(build): name the documents the gate refused whole, and close as 1.0.0
Measured 2026-09-20 on an official documentation corpus of 594 sources built
with the shipped default gate `guard-trusted-source`: 17 sources were refused
OUTRIGHT -- `fail_secure` 3, `quarantine_review` 14 -- and 16 of them were
among 197 official documentation pages, the pages on hooks, skills,
permissions, errors, env-vars and authentication among them. The summary said
only `fail_secure`: 3/594. Three of the four facts a reader needs were missing:
the COUNT of documents the gate dropped (the existing `rejected (coded)` line
sums gate refusals and extraction failures, two failures with two different
remedies), the NAMES, and the way out. Rebuilt with `--gate none`, all 17 went
through untouched, so the refusal is the gate and not the readers.

`okf build` now prints a `Documents the gate refused WHOLE` section directly
under the denominator, carrying all four: the count with its denominator, the
names capped at ten with the rest in the bundle's `log.md`, the codes, and
`--gate none` for a source you vouch for yourself. The same fact goes to stderr
in one line, built from the same field, because `okf build > report.txt` is an
ordinary thing to do. `log.md` gains one bullet naming every refused document,
uncapped.

The exit code deliberately does not move. The build is valid -- every refusal
is coded, the conservation identity holds, and the bundle is a true record of
what the gate allowed. What was wrong was the silence.

A run the gate refused nothing from is byte-identical in both places, which is
the known-negative in the new suite: no bundle this repository ships was built
with a gate refusal, so this cannot have moved a byte measured here.

Also, and measuring nothing new:

- README gains `Known limitations` high up -- the gate's refusals and the way
  out, the absent ceiling on what one run pays for images (a 70 KB PDF with 16
  images under the declared limit reached 851 MB peak RSS; RLIMIT_AS is not
  enforceable on this platform, so the 512 MiB per-link budget is the whole
  bound), the three gates of this repository that are RED today (retrieval
  5/7/8/9, MCP 2, accounting 2/3/6 -- all three re-run on this commit), what
  the content accounting does not count, and the rough edges nothing is
  planned for.
- The two `pip install` lines under "Install in detail" install `[extract]`.
  The first screen does; those two did not, so the two recipes produced
  different installations and the detailed one reports `resolved converter
  path: unresolved (extractor_extra_missing)`.
- Version `1.0.0`, synced across pyproject, `__version__`, `uv.lock`, the four
  README install lines, the install prose, the current-tag entry and the
  CHANGELOG, where the two "after the 0.10.1 notes were written, untagged"
  sections are folded in. It adds no capability over `v0.10.1`; what it adds
  is that the tool says what it does not do.

Suite: 2325 passed, 2 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 21:44:49 +02:00
d300338e4d
docs(front-page): the gate numbers the gate actually prints, and a breaking point that was measured
Four claims on the front page were false on this commit, and one of them was a
number no division ever produced.

**The retrieval gate.** README reported it RED on rows 3, 4, 5, 7, 8 and 9,
with row 3 at 2 of 5 and row 4 at 3 of 6. Run on this commit it is RED on rows
5, 7, 8 and 9, with row 3 at 5 of 5 and row 4 at 6 of 6: `f81683e` made a
withheld concept carry the rule that actually decided it, and `05cb190` gave
the payload a `coverage` block, and neither updated the table. Row 8 is `0 of 3
| NOT RUN` on the default run and was published as `44 of 64 questions`, which
is what it scores the day all three private sets are handed to it -- now
labelled with the day and the machine rather than printed as a row. The same
four figures were stale in `CLAUDE.md`.

**The breaking point in a generated skill.** `int(LIMIT / per_withheld) if
per_withheld else 0` printed `At roughly 0 concepts the bookkeeping alone
reaches the 120000-byte limit` whenever the generation run withheld nothing --
the absence of a measurement, rendered as one, and read as a bundle that breaks
before it holds anything. A run with no withheld entry has no slope to
extrapolate from, so the sentence is withheld with its reason. The shipped
`skills/okf-consume/SKILL.md` is generated with the question its
`references/README.md` names, withholds nothing, and carried exactly that `0`;
it is regenerated. Two arms in the test, because one would pass on an empty
set: the bundles that withhold something must still state a positive figure.

The sentence for that arm also stopped saying `**4 bytes** for 3 concepts`
where the 4 bytes were the cost of 0 withheld entries. It is now `for N of M
concepts`, which moves two generated skills' line counts and therefore the
published comparison: 280 of 312 and 310 -> 281 of 313 and 311, re-measured,
with the 62 differing lines unchanged.

**Four tools.** A single-bundle server exposes three: `okf_list` is absent
where there is nothing to list. README's table already said so in a cell; the
heading and the CHANGELOG did not.

**What `--accounting` accounts for.** The account is over the element classes
each format's vocabulary names, verified against `accounting._READERS` rather
than against the report: a file whose suffix has no reader is accounted at file
level only, `.docx` reads `document.xml` and `footnotes.xml` (so headers,
footers, endnotes and comments are outside), `.pptx` reads the slides (so
speaker notes are outside), `.xlsx` reads the worksheets (so cell comments are
outside and a cell contributes its cached value, never its formula), and `.rtf`
skips its header and footer groups. A hidden slide or sheet IS counted -- it
lives in the same part as a visible one. Nothing is built for this; the list is
what `0 unaccounted` does not claim.

Gates re-run on the commit: retrieval `GATE RED: rows 5, 7, 8, 9` (exit 1),
MCP `GATE RED: rows 2` (exit 1), both matching what is now written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 15:40:20 +02:00
bf697bfcad
fix(consume): every read path into a bundle is contained, not just okf_fetch
`okf_fetch` resolved a concept through `connectors.safe_resolve` from the day
the server was written. The other three ways into the same bytes did not.
`okf consume` and `okf_ask` reach `consume.build_payload`, `okf_describe`
reaches `mcp_server.card`, and both built the concept path by joining the
index's own name onto the bundle root. `consume._join` refuses a `..` segment
and an absolute target, but it is a STRING rule over the index text, and a
symlink is a fact about the filesystem that reading that text cannot see: the
index could name `lekkasje.md`, that name could be a link to a file outside the
bundle, and the file came back in the answer.

Measured before the fix, on a bundle carrying one honest concept and one
escaping link: 8 of 11 new rows red, the 3 green ones being `okf_fetch` on the
same two links and the known-positive that the clean bundle still answers. So
the suite was not red for an unrelated reason, and the fix is not "refuse every
bundle holding a link".

One place, not three copies: `consume.resolve_in_bundle` makes the check and
`consume.read_path_in_bundle` adds the file's presence. Every reader here goes
through them -- the index walk, the ref, the document prior, the payload, the
card, `okf_fetch`, and the three outside `consume` (`skill`, `quality`,
`project`) that joined the same way.

Two more failure modes in the same check, because they are the same question:

* A NAMED PIPE is not a regular file. `read_text` on one blocks for as long as
  nobody writes to it, which on a server is the whole process; the red row for
  it ran 60 s to a subprocess deadline and now returns in under a second.
* A DEAD INDEX LINK raised `FileNotFoundError`, and the broad handler in
  `handle` wrote `{error}` into the refusal -- the SERVER's absolute path,
  handed to whoever asked, over one index entry naming a file nobody wrote.
  It is `concept_unreadable` now, naming the concept and not the machine.

The returned path is the JOINED one, never the resolved one: `read_concept`
derives a concept id by taking the read path relative to the bundle root, and
once containment holds the two are the same bytes.

2334 passed, 2 skipped (was 2323 + 2). `mypy --strict src/` clean over 25 files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 15:20:38 +02:00
df5a1183c9
feat(mcp): serve OKF bundles over MCP in two shapes, plus the generic skill
The eval was written RED at `5f1772e` with no server in the tree. This is the
capability it was written against.

`okf mcp --bundle <dir>` serves exactly one bundle, whose tools take no bundle
argument. `okf mcp --root <dir>` (repeatable) serves every bundle under the
roots and knows NONE of them by name. Four tools -- `okf_list`,
`okf_describe`, `okf_ask`, `okf_fetch` -- each carrying its reason in the
description a client actually reads.

Gate today: 1 (7/7) - 2 (83/181) - 3 (4/4) - 4 (9/9) - 5 (3/3) - 6 (6/6),
`GATE RED: rows 2`, exit 1.

THE PROTOCOL IS STDLIB, AND THAT IS THE PACKAGING INVARIANT KEPT RATHER THAN
A TASTE. An MCP SDK would be this package's second runtime dependency on the
DEFAULT install path, for four JSON-RPC methods and a newline framing, and
`test_the_only_runtime_dependency_is_the_security_boundary` pins that list
literally. Chosen hand-written because the surface needed is `initialize`,
`notifications/initialized`, `tools/list` and `tools/call`; `uv.lock` is
untouched.

NOTHING IS CACHED ACROSS CALLS, and row 3 is why. Every call re-walks the
roots and recomputes `bundle_ref`, so a bundle added, removed or rebuilt while
the process runs is seen by the next call with no restart, no configuration
edit and no code change -- 9 of 9 discovery checks over three bundles written
while the server was serving. The cost is paid per call and is published
rather than hidden: 0.75 s for the identity of a 2 756-concept bundle, 5.6 s
for one ask, 4 min 13 s for row 2's full run over four bundles.

CONTAINMENT IS TWO INDEPENDENT CHECKS: the bundle's own index must name the
concept, AND `connectors.safe_resolve` must place it inside the bundle. A
mutant removing either one alone still refuses -- with a DIFFERENT code, which
row 6 asserts by name -- and one removing both is killed. Row 6 declares a
code set per case because its first run had the 10 MB concept refused as
`concept_unknown`: the fixture had not named the file in the index, so the
size ceiling never ran and the row was green for a reason unrelated to the
attack.

`okf card <bundle>` and `okf skill --generic` are the one-to-many skill
candidate. The card is DERIVED on every run and never written into the bundle:
storing it would move the bytes of all six `examples/*/expected-bundle` trees
(23 files compared byte-for-byte) and of the pinned reference bundle, to keep
something recomputable in under a second, and a stored card is one more
artefact that can disagree with what is beside it. Measured here rather than
taken from the order: two per-bundle skills are identical on 280 of 312 and
310 lines; the 62 that differ are identity, concept count, the
conditional-field table, the whole-bundle cost and the breaking point. The
generic skill carries none of them, and `render_generic()` takes no argument,
so there is no bundle it could have read.

Row 2 decomposes into three numbers and the middle one is the finding: 99 of
181 (bundle, anchor) pairs are present in the bundles at all, 83 of those 99
were reached, and 0 of 83 were met by `okf_fetch` on the anchor as a concept
id. The set's anchors and this library's concept ids are different
vocabularies, so every pair met was met through the ranker -- 83 is a FLOOR on
the ceiling, never the ceiling.

13 mutants in a scratch copy, never in the working tree: 12 killed, 1 survived
with its mechanism printed, 0 errors, control green first. Suite 2323 passed,
2 skipped. The architecture choice between the two shapes is the OPERATOR's;
these rows are its input. Report: docs/2026-09-20-mcp-to-varianter.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 10:25:55 +02:00
5f1772e832
test(mcp-gate): the eval for an MCP surface over OKF bundles, written RED
No capability lands in this commit. `tools/okf_mcp_gate.py` measures six
rows against a server that does not exist, and the reason is the
interpreter's own words rather than this file's opinion: `python -c 'import
llm_ingestion_okf.mcp_server'` raises ModuleNotFoundError, printed in row 1.

Today: 1 (0/7) - 2 (0/0) - 3 (1/4) - 4 (0/9) - 5 (0/3) - 6 (0/6),
`GATE RED: rows 1, 2, 3, 4, 5, 6`, exit 1.

Row 3 is not 0, and that is a measurement rather than a slip. The order
predicted 0 of N on every row; the incumbent one-to-one skill already
refuses out loud when its bundle is rebuilt, because `okf check`'s
`bundle_mismatch` rule exists and compares the declared ref against the
payload's. One of the four artefact classes therefore passes the loudness
clause today -- at the cost the row also prints: one artefact to recreate
and one manual step per consuming project.

The server is a SUBPROCESS and never an import. Every row speaks
newline-delimited JSON-RPC over stdin/stdout starting at `initialize`, with
a client written narrowly here: a client built from the server's own framing
helpers would agree with the server by construction. Nothing in the gate
ranks, serves or writes a bundle.

Denominators are pinned in the gate and recounted a second time in the
tests. `REQUIRED_TOOLS` (7 across two variants), `DRILL_ARTEFACTS` (4),
`DISCOVERY_BUNDLES x DISCOVERY_CHECKS` (9), `CROSS_CHECKS` (3) and
`HOSTILE_CASES` (6) do not come from the run -- a row counting what the
server happened to offer goes green by offering less, which is the defect
rows 2 and 3 of the retrieval gate were caught on.

Row 2's denominator DOES come from the frozen graded question set, at run
time, and the set is never committed here: `--sett` plus `--frys`, with the
sha256 verified against the freeze file and a freeze version below the
ordered one refused as a usage error, not measured. Without them the row is
`0 of 0` with the reason stated. Its machinery carries a known-positive over
the synthetic corpus this file invents, and a green test proves those quotes
really are in the fixture on disk without asking any server -- so a 0 there
means the surface could not reach text that is demonstrably present.

Six tests are red, every one of them on an assert about behaviour: 0 of 7
tools answered, 1 of 4 artefacts loud, 0 of 9 discovery checks, 0 of 3
cross-bundle checks, 0 of 6 hostile cases, and `find_spec` returning None
rather than an ImportError. Eight instrument tests are green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 09:51:06 +02:00
c5b0dc6d9a
test(retrieval-gate): the pin needs the bytes, the threshold needs a range, and an empty hold-out clears nothing
Three guards for the code the two step-0 commits added, each written
against a mutation that would otherwise survive: a wiki-shaped file
carrying the pinned 20 questions and 29 fasit entries but invented content
(so the counts alone are a one-line forgery), a threshold of 80 or 1.5
(a share or a typo, and guessing is not this row's job), and a hold-out set
of no questions -- the shape every row-5 test used until today, which would
have made the new comparison vacuous the moment it was added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 08:45:27 +02:00
05cb19087a
feat(consume): the payload says what of the question it reached, and row 4 reads it
`coverage` carries three lists: the terms the pre-pass read the question as,
the terms no concept in the bundle answers, and the terms no delivered
excerpt answers. Without it a reader holding eight excerpts cannot tell a
bundle that ANSWERED its question from one that merely ranked something --
the two payloads have the same shape.

FACTS, AND NO VERDICT, which is a measurement and not caution. Two readings
were built and both falsified over 81 questions (16 synthetic, 65 across the
three real sets, 2026-09-20): the share of question terms a delivered
excerpt answers separates the synthetic controls at 0.33 against 0.50 and
REVERSES on real data (covered questions down to 0.27, one genuinely
uncovered question at 0.71); the share of a bundle tying the best lexical
match is ~0.00 for every real question either way. Question style dominates
the first, corpus size the second.

The one bar this repository declares is the gate's: `UNANSWERED_BAR = 2/3`
over `unanswered_in_bundle`, swept and collapsing at both ends -- at 0.50
eleven real covered questions are marked, at 0.70 the row falls to 5 of 6,
at 2/3 the row is 6 of 6 and 0 of 65 real questions are marked. The margin
is thin (0.6087 against 0.6667) and is published that way, together with
what it does not catch: r761-sk2's own known-negative sits at 0.2857.

Row 4: 3 of 6 RED -> 6 of 6 GREEN, with the 10 answered synthetic questions
held unmarked as the known-negative. The contract's SS 8 gains point 7, the
consumption skill is told to read the block, and the SS 7.4 known-positive
moves with the document (14 721/375 -> 16 389/417). Suite 2292 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 08:44:06 +02:00
90394c383d
test(consume, retrieval-gate): the payload must say what of the question it covered, and row 4 must see it
Red first, on behaviour: three asserts about what the payload carries and
what row 4 scores, none on an import or an attribute.

Measured 2026-09-19: N3, N4 and N5 come back with 8, 8 and 1 excerpts and
nothing that says they are weak, so `marked = nothing delivered` reads
three uncovered questions as answered ones. The second gate test is the
known-negative that a marking firing on everything would fail: the 10
questions the three synthetic sets DO answer must stay unmarked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 08:25:38 +02:00
f81683ea54
fix(consume): a withheld concept carries the rule that actually decided it
The quota filters the WHOLE candidate list, so every over-quota candidate
came back `source_quota_exceeded` -- including the ones the RANK had
already put outside k, which the quota only reached because it ran first.

`_fates_without_quota` asks the same cut what would have become of each
candidate with no quota, and the drop keeps THAT rule; only a candidate the
quota-off cut would have delivered is named as the quota's. The packer is
lifted into `_pack` and used by both, so the quota-off fate is decided by
the code the run itself uses and never by a second implementation.

The retrieval gate's row 3 goes 2 of 5 RED to 5 of 5 GREEN. Row 7 is
unchanged at 12 of 14; M01 and M02 lose their row-3 credit, which was the
lying label moving and not the ranking. Suite: 2288 passed, 0 failed; no
committed payload moves a byte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 08:24:26 +02:00
6b62ecea34
test(consume): a drop the rank had already made must not be named as the quota's
Red first, on behaviour: the withheld set names c4 and c5 as
`source_quota_exceeded` where the same cut without the quota withholds them
`below_k`. The test measures the truth itself, off a second cut, and
carries a known-positive so the assertion is not vacuous.

Measured by PM on 25 real misses 2026-09-17: 13 were labelled by the quota
and decided by the rank.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 08:11:57 +02:00
735fb237e4
feat(retrieval-gate): row 5 reads the threshold as a number and compares it with the hold-out
Two checks replace `bool(threshold)`: `_as_share` parses it as a share in
[0, 1] -- so `report-only; any number is acceptable for v1` is a NO, and so
is `80` -- and `_hold_out_verdict` RUNS the registered set against the
registered bundle and prints `answered of asked = share against threshold`.

A registration naming an absent set, an unreadable bundle or an empty set
is a NO with its reason, never an exception and never a silent pass. The
row is 11 checks; it stays RED today because no registration exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 08:11:03 +02:00
7e73257f30
test(retrieval-gate): row 5 must read the threshold as a number and compare it with the hold-out
Red first, on behaviour: both assertions are about the checks the row
prints, neither is an import or an attribute error.

Measured by PM on 23588e5: `bool(threshold)` was the whole check, so the
threshold `report-only; any number is acceptable for v1` read as `a
threshold is written: yes`. A threshold that is never compared with a
number cannot fell anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 08:07:27 +02:00
3dc2491083
test(retrieval-gate): row 8 must name the bundle it measured, and the three real sets must be pinned
Red first, on behaviour: the identity assertion fails on the rendered
details, and the two pin assertions fail with DID NOT RAISE. Neither dies
on an import or an attribute.

PM measured both on 23588e5: one pinned r761 set gives 4 of 7 on three
bundles and 0 of 7 on a fourth while the session recorded 7 of 7, from a
bundle the output never names; and three self-written one-question files
plus a self-written bundle read `3 of 3 GREEN`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 08:01:42 +02:00
7e5248aa84
test(accounting-gate): the published door contract is behind the gate, and a doorless bundle is silent
RED, 3 of 117 on assert about behaviour, 0 on import.

Two rests from PM's checkpoint on 44ad845, both about what a reader OUTSIDE
this repository is told.

The contract sketch in the gate's own module docstring is what a consumer
implements the `--accounting` door from. It does not name `conversions`,
which the gate now DEPENDS on, nor `normalised_soft_hyphen`, `unaccounted`
or `double_booked`, which the door has written for longer. A consumer
following it writes a ledger this gate reads as "nothing was converted", and
every converted image in their bundle is reported claimed-and-not-found.

The first test measures what the gate READS rather than what its source
mentions: the ledger is handed to `_declared_conversions` as a mapping that
records every lookup at any depth, so the assertion is about lookups and not
about grep. The second measures the other direction, from the DOOR's own
serialisation, so the fix cannot be "publish everything": contract and
output must be the same set.

The third is N5. Without the door there is no ledger, so a converted image
cannot be proved carried and is counted claimed-and-not-found -- the reading
the ledger round removed, back again for every reader who builds without the
flag, and stated nowhere. Asserted through `row3`, not through a signature
that does not exist yet, so the red is behaviour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 22:51:54 +02:00
7cf758dab7
test(assets): the RLE8 rule is short of the independent decoder, in 703 of 22 620 streams
RED, 3 of 23 on assert about behaviour, 0 on import.

PM's checkpoint on 44ad845 found that four end-of-line escapes and an
end-of-bitmap carry an 8x4 frame with 32 of 32 pixels never decoded, while
Pillow -- the independent decoder this file names -- refuses the same bytes.
The sentence the last round wrote into `assets.py`, that a delta and an
end-of-line both leave pixels every decoder agrees on, is false for the
end-of-line half.

Measured here before anything is fixed, and the class is wider than the one
construction: over every opcode sequence of length 1 to 4 on a 4x3 frame
(22 620 streams), 703 are carried by this package and refused by Pillow, and
1 492 more are carried by both and drawn DIFFERENTLY. PM's recommendation on
its own -- refuse a stream that painted nothing -- leaves 512 and 1 171 of
those, so it would narrow the class a third time rather than close it.

Four new arms in CURSOR_CASES, one per clause the table could not see:
the ROW clause (a stream stopping one row early with that row complete, which
is PM's P8 mutant `height - 1` -> `height - 2`, a mutant that survived 51
tests), and three end-of-line constructions that reach the end of the frame
without closing a row. The sweep is the guard the table cannot be: a
hand-picked table holds the shapes its author imagined, and this defect has
now been closed one shape at a time twice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 22:46:13 +02:00
149836c722
test(retrieval-gate): the headline test could not see the sum it forbids
X2 of this round's own mutation run SURVIVED: put the headline back to
`quoted + concept` over `quoted_units + concept_units` and all 64 tests stayed
green. The reason is the fixture, not the row -- three sets of one fasit entry
each make "questions answered" and "units hit" the same number, so the test
agreed with the mutant by construction. The same shape as round 158's X5, and
the same lesson as `konstruert-rad-kan-vaere-fasitradens-speil`.

One set now carries TWO fasit entries, which is the only shape where the two
readings differ: the headline must be 3 of 3 questions where the sum would be
4 of 4. `_hitting_set` takes `entries` and says why in its docstring.

64 passed. Mutation run in a scratch copy of HEAD, twelve mutants plus a
control that must survive: killed 11 of 12 before this commit, and X2 is the
twelfth.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 22:01:57 +02:00
1020a0ee0b
test(retrieval-gate): the eighth attack stays as a regression guard
PM ran eight cheating attacks and four were already refused. Three of those
four already had a test (a tampered sha is exit 2, a moved set is refused, a
tuned corpus now meets a pin as well as row 2's forced classes); the fourth --
a cut that delivers nothing, which came back as an error from `consume` rather
than as a quiet row -- did not. The table is 8 of 8 now, not 4 of 4.

64 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 21:48:46 +02:00
714aafbff2
fix(retrieval-gate): the seven small findings -- a corpus pin, a K2 input, an observed term
PM's checkpoint left seven small findings beside the four bearing ones. Six
are closed here (the seventh, running row 8 against the real sets, follows).

G9 -- THE CONFIRMATION TERM IS OBSERVED NOW. `hit = bool(hit_ids) and
bool(confirmed)` survived all 46 tests, because every mutation of the text
empties `hit_ids` one step earlier. The shape that reaches it is a delivery
that still CARRIES the citation and is no longer the concept file's bytes:
`M14` is that mutation and it is FELLED (row 7 goes 11 of 13 to 12 of 14, bar
12 of 13 to 13 of 14, still RED, the same two survivors), and a test drives it
with its known-positive in the same test. No production line changed: the term
was always observable, it was unobserved.

AND THAT MEASURES THE JUDGE'S INDEPENDENCE RATHER THAN ASSERTING IT. The judge
does read the bundle through `consume.read_concept` and `delivered_text` --
PM's finding -- but the index is warmed BEFORE the first mutation, so the two
sides do not move together. Measured both ways: index warmed first, every unit
is a miss with `confirmed False`; index built UNDER the same patch, every unit
is a hit. The gate never builds one under a mutation. Stated in `LIMITS` with
that measurement, rather than closed by re-implementing a normalisation rule
this repository already owns once.

SPECS -- the synthetic corpus is pinned like the sets (`SPECS_SHA256` over
`specs_digest`). PM's corpus tuning was caught by row 2's forced classes and
not by a pin, and a more careful tuning was left standing.

ROW 9 TAKES AN INPUT. `--k2 SET SHA256 BUNDLE` reads a gold set in this gate's
own set shape; `K2_QUESTIONS` stays the denominator whatever the file carries,
and a set of another size is refused (exit 2) as another set wearing this
one's name. Without a set the row stays RED and not NOT RUN -- ITS denominator
is known, six recorded questions, so the absence is measured; row 8's is not
known until the sets arrive. Both fail the gate identically. This is a
deliberate divergence from the order's parenthetical, stated here and in the
row.

MYPY. `mypy --strict` on this file goes 8 errors to 0, the four in
`read_real_set` among them (`questions = []` against a name inferred
`tuple[Question, ...]`) -- the adapters that meet the real sets.

63 passed. The verdict is unchanged: GATE RED: rows 3, 4, 5, 7, 8, 9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 21:24:40 +02:00