feat(cli): okf project/consume/check/skill, and a generated skill with no path into a checkout

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>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-08 21:47:38 +02:00
commit f6fea13299
21 changed files with 4017 additions and 2833 deletions

View file

@ -0,0 +1,178 @@
# 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:
```sh
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.md` was not edited.** Its § "What is
mechanically checked" still names `tools/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.py` was 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 at
`tools/okf_consume.py`, the other required the README recipe to invoke
`python3 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** (`xlsx` at 12 against a reference of
11, `pdf` at 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 |