`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>
`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>
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>
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>
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>
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>
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>
C5b. The answer form's example of a short source reference named a real
publication. It now names an invented one, "Brukerhåndbok § 4.2", in the
template and in the skill generated from it. Older history is not rewritten.
Suite on a clean tree after `git add`: 2397 passed, 2 skipped, 4 xfailed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
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>
`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>
`eval/soek/` is where the search gate's question sets are placed by hand. The
sets themselves are `.gitignore`d rather than committed, and that is this
repository's existing rule rather than a new one: `okf_retrieval_gate.py`
already states that a gold set names documents in someone's corpus, so it
arrives as a path and is never a constant here.
Measured before it was decided: the sets carry coordination ids and an
absolute path under a home directory, both of which this repository's own
frames forbid in a tracked file. A set that is absent is reported
`IKKE KJOERT -- fixture mangler` and counted RED, never as 0 hits and never as
a quiet pass, so a machine without them cannot read silence as a green row.
The README states the SHAPE each set must have and nothing about where any of
them came from or what any of them contains.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`CLAUDE.md` is the file every session here reads before it reads code, so a
stale claim in it is not documentation debt -- it is an instruction. Three were
made false by the commits above and are corrected rather than left standing:
- `okf project` "owns NO flag that moves a bundle's bytes" -- it owns exactly
one now, `--gate`, and the skill directory no longer carries the bundle id.
- `okf skill --generic` as a candidate -- the generic form is the DEFAULT, with
`--for-bundle` as the opt-in, and the paragraph now carries why the candidate
won rather than describing a decision that has already been taken.
- `--withheld-titles` as a live flag -- it is retired with the flat list it
existed to name, and the paragraph carries the measurement that retired it
plus the shape and the revision bump that replaced it.
No code and no test moves; this is the invariants file catching up with the
tree it describes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
**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>
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>
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>
`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>
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>
Operator rule, 2026-09-20, for every repository: our code does not start Claude
Code. `tools/okf_adjudicate.py` did, with `subprocess.run` on a hardcoded
`/Users/.../claude` -- a path to one binary on one machine, in a public
repository.
Removed with its test file. References: **17 -> 3**, all three in the
CHANGELOG, and that is a deliberate deviation stated in the removal entry
itself: two of them describe what the tool did under the versions that shipped
it, and a changelog that edits its own past is not a record. The three K3/K4/K5
reports that used it now say so in the past tense, with the removal noted where
the tool was named.
It is the only such place, measured rather than assumed: `subprocess.*claude|
CLAUDE_CLI|claude", *"-` over `src` and `tools` gives **0** here and **3** on
`df5a118`. Every other subprocess in `tools/` starts `git`, `pytest` through
`sys.executable`, a PDF utility, or the coordination script `okf_watch.py`
owns -- none of them a model.
`okf build --gate` and the guard are untouched; nothing that judges CONTENT
moved. What this removes is a tool that asked a model to judge a proposal, and
the only measurement that ever rested on it is a 2026-09-02 baseline, which is
reported and dated.
2318 passed, 2 skipped (2334 before, minus this tool's 18 tests, plus 2 new).
`ruff check` and `ruff format --check` clean over 159 files, `mypy --strict
src/` clean over 25. Wheel built and installed into a clean venv: `okf --help`
exits 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Correction to `23ef6bb`: its `9 -> 6` is true of the 74 files the span touched
when it was measured, and the span has grown since -- re-measured over the
finished span's 79 files, the same pattern is **11 -> 7**, the one extra pair
being `tools/okf_adjudicate.py` (gone with this commit) and
`tests/test_okf_skill.py`'s own known-positive for the absolute-path check.
A public repository should not carry the layout of the machine it was written
on, and it should not carry the identifiers of a queue nobody outside can see.
Six places in material this span added or changed did.
Neutral text, same facts: a private queue path in the EOL report, a private
note path in two docstrings and one report header, and four order identifiers
opening four reports. Where a sentence lost its source it says `internal
measurement note` -- the measurement is still named, the path is not.
One of them was output, not prose: row 9 of the retrieval gate printed a
`~/corpora/...` path to stdout on every run. It says the bundles exist on the
machine the row was written against; the row already takes `--k2 SET SHA
BUNDLE` from anyone who holds them.
Measured with the patterns as given, and both validated against a
known-positive on `df5a118` first -- an unvalidated pattern reporting zero is
not a measurement:
* `~/\.claude|/Users/|coord/.*/orders` over the 74 files the span touches:
**9 -> 6**. The six left are deliberate: two README lines telling a reader to
install a skill into their OWN `~/.claude/skills`, one prose line arguing
that `/Users/...` is not more portable than a relative path, and three lines
of `tests/test_retrieval_gate.py`'s own known-positive for this exact check,
which removing would disarm it.
* `[0-9]{8}T[0-9]{6}Z-[0-9]+` over the 38 files the span ADDED: **4 -> 0**.
A note on the method, because the first run of the first grep reported 0 and
was wrong: zsh does not word-split an unquoted parameter, so
`git grep -- $FILES` passed 74 names as one argument and matched no file at
all. Both numbers above come from `$(git diff --name-only ...)`, which zsh does
split, and the known-positive is what made the false zero visible.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
`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>
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>
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>
Found by the clean-extraction run, not by inspection:
`test_the_changelog_heads_with_the_packaged_version` requires the first
`## [x]` heading to be the packaged version with a date, and 0.10.1 is the
version being prepared and not yet tagged. The content is unchanged; the two
subsections say when they were written.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
`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>
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>
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>
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>
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>
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>
REAL_SET_PINS states what each set IS -- questions, fasit entries, controls
and sha256 -- so a one-question file in the right shape is refused
(`check_real_pin`, exit 2) instead of reading `1 of 1 | 3 of 3 | GREEN`.
Three counts rather than one: the digest says the bytes are the pinned
bytes, the counts are what a reader can check against the source.
Every set's line now carries `measured against <key> = <path> | bundle_id
<id> | ref <sha256-tree:...>`, for the bundles the run actually used and
SS 3.3's two halves both printed -- three builds on this machine share one
bundle_id at three refs.
Stated limit: the table lives in the file a capability session edits, as
SYNTHETIC_SETS and SPECS_SHA256 do. It raises the cost of the attack and
does not remove it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
The round's report, and the two published sentences it makes false.
CLAUDE.md said a delta and an end-of-line escape both state a skip every
decoder agrees on. True of the delta, false of the end-of-line, and the
paragraph now says what was measured instead: 703 of 22 620 swept streams
carried here and refused by Pillow, 1 492 drawn differently, 0 and 32 after
the two clauses, and the third clause that would give 0 and 0 priced at 15 of
the 25 real files.
The accounting-gate section gains the two rests about what a CONSUMER is
told: the published door contract now names the keys the gate reads, and a
bundle built without the door says why a converted image cannot be proved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The catalogue goes 45 to 47 and the runner stays `killed N of N`, exit 0.
`P8` is PM's own survivor from `44ad845`: the cursor rule one row too lenient
(`height - 1` -> `height - 2`), which survived 51 tests because no arm in
`CURSOR_CASES` stopped a row early. `P13` removes the clause this round added,
so the round leaves a mutant behind and not only a test. Both are judged by
`tests/test_asset_viewable.py`, which is why the per-mutant suite added in the
previous round was worth having.
CHANGELOG under 0.10.1, no new version and no tag.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Green: 117 of 117 in the suite that was 3 red.
The published `--accounting` sketch gains the four keys the door has been
writing and the contract did not name -- `conversions`, with its `from`/`to`
pair, plus `normalised_soft_hyphen`, `unaccounted` and `double_booked` -- and
a paragraph saying what a door built WITHOUT `conversions` costs the
consumer who built it: every converted image claimed-and-not-found, 19 of 50
on R761. The two new tests hold the sketch against both sides, what the gate
looks up and what the door serialises, so neither drifting again is silent.
N5, and the choice: SAY IT OUT LOUD rather than only in a docstring. A
docstring is read by whoever edits this file; the reader who is misled is
reading a REPORT, where a count of claimed-and-not-found images appeared with
no cause beside it. `_tally` now names the missing ledger when, and only
when, something was claimed and not found -- two known-negatives hold that
condition. `asset_holds` gets the sentence as well, since it costs nothing.
mypy --strict on this file: 11 errors before, 11 after (pre-existing, this
file is not in the `src/` gate).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Green: 23 of 23 in the suite that was 3 red, and the sweep's carried-here-
refused-there count goes 703 of 22 620 to 0, the drawn-differently count
1 492 to 32.
Two clauses, both refusing with the code the path already uses:
* an end-of-line escape at column 0. It closes no row, so the row it moves
the cursor over is a row the stream never wrote -- PM's four-EOL frame,
carried with 32 of 32 pixels never decoded while Pillow refuses the file.
* a delta whose horizontal offset would leave the row. The format puts that
offset inside the line; this reader keeps the cursor past the row end and a
flat decoder rolls it into the next row, which is two pictures from one
stream.
CHOSEN OVER PM's RECOMMENDATION, and the recommendation was measured first:
refusing only a stream that painted nothing leaves 512 streams carried here
and refused there, and 1 171 drawn differently. It would have narrowed this
class for the third round running instead of closing it.
WHAT IS NOT CLOSED IS STATED, in the docstring and in the test: 32 of 22 620
streams are still drawn differently, every one of them a run or absolute
block that overruns its row. Refusing those gives 0 and 0 -- and costs 15 of
the 25 real RLE8 files, which would drop 15 figures and move a pinned
bundle's bytes. Measured on the corpus first: over 11 441 files scanned
across the four raw deliveries and the K2 reference corpus, the 25 BMPs on
this machine use an end-of-line at column 0 in 0 of 25 and a delta in 0 of
25, and 25 of 25 still decode to Pillow's pixels exactly (3 117 220 pixels
compared, after the change).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
README's row table and the `--real` paragraph, CLAUDE.md's gate entry,
CHANGELOG under 0.10.1 (no new version, no tag), and the full report.
The report carries the eight attacks before and after, the row table before
and after, row 8's three-set measurement with its two granularities apart, the
mutation table as properties rather than as code lines, and § 6 -- the three
things this round does NOT close: row 3 can still be made green by an honest
change that costs elsewhere (the gate is the unit, not the row), the judge's
parser is still `consume`'s and is bounded by measurement rather than removed,
and `SPECS_SHA256` digests the corpus's fields rather than the bundle's bytes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
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>
PM's J3. `MUTANT_ROSTER` carries the thirteen labels and `MUTANT_COUNT` their
number, both written apart from `MUTANTS`, and `row_seven` refuses to run
unless the labels it was handed ARE that roster, in order, with no duplicate.
The bar is taken from the roster's length, not from `len(mutants)`.
Why a pin and not a share: the bar is a percentage, so a longer list is a
lower bar per survivor. Seven copies of `M03 k = 1` took the row to 18 of 20
GREEN with the same two survivors -- nothing new felled, the bar lowered.
Lengthening the list honestly now costs three edits in three places, each
readable as what it is; a duplicate label is refused outright, because two
copies of one mutation are one mutation whatever the roster says.
Row 7 is unchanged on the shipped list: 11 of 13, bar 12 of 13, RED, the same
two survivors with the same measured notes. 56 passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PM's J3: seven duplicate `M03 k = 1` mutants appended to `MUTANTS` took row 7
from `11 of 13 RED` to `18 of 20 GREEN` with the SAME two survivors. The bar
is `MUTANT_BAR` of `len(mutants)`, and `MUTANTS` is a module constant in the
file a capability session edits -- a gate whose pass mark is a list the
candidate writes.
Reproduced here at 18 of 20 exactly. 1 red on an assertion about behaviour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PM's J1. Three of row 5's ten checks now read the repository's history instead
of the registration's own fields:
- the registration is COMMITTED and unmodified against HEAD -- a file written
in a working tree is not a pre-registration;
- the commit that ADDED it is not itself a change to the ranking -- a
threshold and the change it is supposed to bind, in one commit, is neither;
- a change to the ranking landed AFTER it. This is the one that cannot be
self-attested: it is satisfied only in the order a pre-registration actually
happens, and it is red today because neither half has happened.
The three come from `git_provenance`, a `Provenance` the row receives the way
it already receives a path, so both directions are driven from the test and
the default is the real git. Outside a git tree, or with no git on PATH, the
fields come back in their REFUSING form with the reason printed -- unknown is
not unknown-and-therefore-fine.
WHAT GIT CANNOT PROVE IS STATED IN THE ROW, not implied: history shows ORDER,
never that nobody read the number before writing the threshold -- a number can
be read from an uncommitted tree. Order is what these three checks are.
Row 5 is unchanged where it matters today: `0 of 1 RED`, no registration. The
two existing direction tests move 7 -> 10 checks and a third arm is added that
drives each git check red on its own. 54 passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PM's J1: two files written by the session under test came back `7 of 7 GREEN`.
Every one of row 5's checks reads a field the registration owns. "Written by
another session than the ranking change" is `bool()` of a string the file
sets; "no reading predates the threshold" reads a `readings` list from the
same file. The order's point -- report-only without a pre-written threshold is
not a protection -- is not met by a protection that is itself a claim.
1 red on an assertion about behaviour (GREEN for a registration this test
wrote seconds earlier), and it names what a session cannot write about
itself: that git already carried the file before the ranking moved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>