The reading direction existed only for someone standing in a clone. `consume`, `contract_check` and `skill` moved from `tools/` into the package and are reachable as `okf consume`, `okf check` and `okf skill`; `okf project` is new and does the whole thing in one command. The red measurement: a consumption skill generated from a checkout carried 4 lines naming that checkout by absolute path, 2 of them the commands the skill tells a reader to run. It now names `okf consume` and `okf check`, and a test asserts this repository appears in it nowhere, with a known-positive so the zero is a measurement rather than a search that could not find. The `tools/` files stay as ALIASES, not re-exports: a re-export binds copies of the names into a second module object, so a caller patching one patches a binding the implementation never reads. Two tests that monkeypatch okf_consume went green again only under the alias. Every published reproduction block runs unchanged. The template and docs/consumption-contract.md (the section 7.4 known-positive) are force-included into the wheel from the file they are authored in, so both travel with the commands that cannot run without them and there is still one authored copy of each. Step 0, before any of it: okf build's default gained Arm E (--table-grid), with --no-table-grid as its opt-out. The default moved to D plus F earlier the same day on Arm F's published 5 of 12 -- a figure measured with Arm E ON. Without it the fold has no joined table to fold, and the shipped default scored 2 of 12 with docx 0 of 3. Measured on the operator's folder: 30 md / 15 concepts on the new default against 43 / 28 without Arm E. Install measurement from a fresh uv tool install, empty folder, this repository nowhere on PYTHONPATH: 5 documents in, 15 concepts out, 0 references to tools/ in the generated skill, okf check conformant (15 rules, 0 findings). Deviation stated rather than hidden: the order asked that tests/test_okf_consume.py be left untouched. Two assertions in it read a PATH, which is the one thing this work changes. Both were moved and the second made stronger -- it now asserts every command the README recipe names is a subcommand the CLI registers, which a file existing on disk never proved. Suite 1414 -> 1427. ruff clean, mypy --strict clean over 21 files. Record: docs/2026-09-08-o5-okf-project.md Co-Authored-By: Claude <claude-opus-5>
8.3 KiB
O5: one command from a folder to a bundle you can ask a question of
2026-09-08. Session 113, order 20260908T184152Z-1340419920, on top of
efa92ce (K3 round 3).
This is the session that made the reading direction installable. Everything here already worked from a clone; none of it worked for anyone else.
1. The red measurement
The consumption skill generator was run against a bundle and the result grepped for paths:
python3 tools/okf_skill.py ~/okf-test/v1-bundle --out /tmp/o5-red/skill
grep -c "^/" /tmp/o5-red/skill/SKILL.md # absolute paths
4 lines, of which two are the commands the skill tells a reader to run:
17: The contract this skill is held to is `<HOME>/repos/llm-ingestion-okf/docs/consumption-contract.md`.
25: python3 <HOME>/repos/llm-ingestion-okf/tools/okf_consume.py \
26: <HOME>/okf-test/v1-bundle \
40: python3 <HOME>/repos/llm-ingestion-okf/tools/okf_contract_check.py \
Line 26 is the caller's own data and belongs there. The other three name a checkout. A skill carrying them cannot be moved, shared, or run by anyone without that clone at that exact path — which is every reader except the one who generated it.
2. Step 0: the default moved again, because the number behind it moved
Round 3 set okf build's flagless default to --outline-run 3 --unit-fold
(Arm D plus Arm F) on Arm F's published "5 of 12". That figure had been
measured with --table-grid ON. Without Arm E, Arm F's table clause — fold a
table back into the heading that introduces it — has no joined table to fold,
because a grid table is then not one block but one block per rule line.
Arm E therefore joined the default, with --no-table-grid as its opt-out.
Measured here on the operator's five-document folder, at the same stamp:
| build | .md files |
of which concepts | index.md |
log.md |
|---|---|---|---|---|
| new default (D+E+F) | 30 | 15 | 14 | 1 |
--no-table-grid (D+F, round 3's) |
43 | 28 | 14 | 1 |
The .md totals reproduce the PM measurement exactly (43 and 30). The concept
counts differ from the 29 and 16 it reported by exactly one each, and the
decomposition says why: md files minus index.md counts log.md as a
concept. The index walk does not, and neither does the pre-pass.
--keep-table-heading did NOT join the default: measured on two K2 bundles in
round 3 it buys 35 bytes and zero rank positions.
The red test is test_the_build_default_is_now_arm_d_plus_arm_e_plus_arm_f on
a grid-table fixture, where the arm's effect is visible in the TITLES: with it
off the sheet lands as 7 Romskjema plus Tabell linje 6, Tabell linje 8
and Tabell linje 10 — concepts named after a line number. Its opt-out half
asserts those four titles come back under --no-table-grid.
3. The move
Three modules moved from tools/ into src/llm_ingestion_okf/:
| was | is | reached as |
|---|---|---|
tools/okf_consume.py |
consume.py |
okf consume |
tools/okf_contract_check.py |
contract_check.py |
okf check |
tools/okf_skill.py |
skill.py |
okf skill |
and one is new: project.py, reached as okf project.
The tools/ files stay, because published reproduction blocks name them and a
measurement whose command no longer runs is a measurement nobody can repeat.
They are aliases, not re-exports: sys.modules[__name__] = _impl. A
re-export binds copies of the names into a second module object, so a caller
patching one patches a binding the implementation never reads — measured, two
tests that monkeypatch okf_consume went green again only under the alias.
Two authored files had to learn to travel. okf skill instantiates
skills/okf-consume-template/SKILL.md, and okf consume measures
docs/consumption-contract.md as its § 7.4 known-positive and refuses without
it. Neither is under src/. Both are force-included into the wheel from the
file they are authored in, and both resolvers prefer the packaged copy and fall
back to the source tree — one authored copy, no committed duplicate, because a
duplicate is the drift the generator exists to prevent.
The generated skill names commands, not paths. okf consume and
okf check, resolved by the shell. The two attribution lines that named
tools/okf_skill.py now name okf skill.
4. What okf project does, and what it refuses to decide
okf project <folder> runs okf build with this package's default into
<out>/.okf/<id>/, generates the skill into
<out>/.claude/skills/<id>-consume/, and prints a summary. <out> defaults to
the current directory, <id> to the folder's name NFC-normalised and reduced to
[a-z0-9-].
It owns no flag that changes a bundle's bytes. A test holds the project bundle
byte-equal to the okf build bundle of the same folder at the same stamp: two
build paths would mean every measurement report is pinned to a bundle nobody
produces.
The summary reports two things a concept count cannot:
- documents in the folder that no concept names — nothing can quote them;
- documents that landed whole, as one flat concept at the bundle root
(measured on the artefact: a concept id with no
/) — reachable, but a question returns the entire document as one excerpt, which the budget often refuses and which often does not carry the answer where it was asked.
Both are [sourced-not-sufficient] cases, and the summary names the documents.
5. The install measurement
From a fresh uv tool install into an isolated tool directory, run from an
empty folder with this repository nowhere on PYTHONPATH and PATH reduced to
the install plus /usr/bin:/bin:
Read 5 document(s) from <HOME>/okf-test/dokumenter.
Wrote 15 concept(s) to /private/tmp/o5-user/.okf/dokumenter.
Wrote the skill to /private/tmp/o5-user/.claude/skills/dokumenter-consume/SKILL.md.
0 of 5 document(s) were left out of the bundle. [...]
1 of 5 document(s) landed WHOLE [...] Expect `[sourced-not-sufficient]` there:
- Del II Bilag 6 - Teknisk oppsett.pdf
NEXT: start claude again in /private/tmp/o5-user and ask your question.
Exit 0. The generated skill carries 0 references to tools/ or to any
checkout. okf consume on that bundle exits 0, and okf check on the pair is
conformant: 15 rules over 8 excerpts and 7 withheld entries, 0 findings.
The first install measurement was wrong and is recorded here rather than
dropped. uv tool install from the unchanged local path returned a cached
build of the previous version, and okf project failed with invalid choice: 'project'. The version bump was already required; without it the measurement
would have reported the old wheel as the new one.
6. What was NOT done
docs/consumption-contract.mdwas not edited. Its § "What is mechanically checked" still namestools/okf_contract_check.py, which is true — the alias exists. Its bytes are the pre-pass's known-positive (KNOWN_POSITIVE_EXPECTED = 12563), so editing it makes the pre-pass refuse until the literal is updated. That coupling is deliberate and this was not the session to spend it.skills/okf-consume/was not regenerated, only its four command and attribution lines were updated in place. Regenerating needs the K2 bundle, which is never copied into this repository.- No push, and the tag is local. The order and the operator own that.
tests/test_okf_consume.pywas not left untouched, and the order asked that it be. Two assertions in it read a PATH, which is the one thing this work exists to change: one read the pre-pass's source text attools/okf_consume.py, the other required the README recipe to invokepython3 tools/<script>.py. Left alone, the first would have measured a wrapper with no vocabulary in it and passed, and the second would have failed. Both were moved to the new location and the second was made stronger — it now asserts every command the recipe names is a subcommand the CLI registers, which a file existing on disk never proved. Nothing else in the file changed; the pre-pass's behavioural contract is unaltered.- Round 4's per-file-type questions (
xlsxat 12 against a reference of 11,pdfat 3 of 8 and losing 940 characters of an opening) were not touched. They stand as next.
7. Numbers
| before | after | |
|---|---|---|
| tests | 1414 | 1427 |
okf subcommands |
1 | 5 |
| absolute paths in a generated skill | 4 | 0 |
mypy --strict source files |
17 | 21 |