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

@ -763,6 +763,7 @@ def test_the_build_default_is_now_arm_d_plus_arm_f(tmp_path: Path) -> None:
PROPOSED_AT,
"--outline-run",
"3",
"--table-grid",
"--unit-fold",
)
== 0
@ -787,6 +788,7 @@ def test_each_arm_in_the_default_has_an_explicit_opt_out(tmp_path: Path) -> None
PROPOSED_AT,
"--outline-run",
"0",
"--no-table-grid",
"--no-unit-fold",
)
== 0
@ -802,6 +804,7 @@ def test_each_arm_in_the_default_has_an_explicit_opt_out(tmp_path: Path) -> None
PROPOSED_AT,
"--outline-run",
"0",
"--no-table-grid",
"--no-unit-fold",
)
== 0
@ -831,7 +834,9 @@ def test_a_bundle_built_with_no_flags_is_byte_identical_to_the_shipped_one(
"""
inbox = inbox_with_subdirectories(tmp_path)
armed = two_script_bundle(
inbox, tmp_path / "reference-armed", proposer_flags=("--outline-run", "3", "--unit-fold")
inbox,
tmp_path / "reference-armed",
proposer_flags=("--outline-run", "3", "--table-grid", "--unit-fold"),
)
bundle = tmp_path / "cli-bundle-2"
assert build(inbox, bundle, "--ingested-at", INGESTED_AT, "--proposed-at", PROPOSED_AT) == 0
@ -849,8 +854,92 @@ def test_a_bundle_built_with_no_flags_is_byte_identical_to_the_shipped_one(
PROPOSED_AT,
"--outline-run",
"0",
"--no-table-grid",
"--no-unit-fold",
)
== 0
)
assert tree(opted_out) == tree(plain)
# --- the default moved again: Arm E joined it (2026-09-08, round 4) --------
#
# Round 3 moved the default to Arm D plus Arm F and left `--table-grid` a flag.
# Measured afterwards on the operator's five-document folder, that combination
# is Arm F WITHOUT a joined table to fold: the fold's table clause folds a
# table back into the heading that introduces it, and with Arm E off a grid
# table is not one block but one block per rule line, so there is nothing whole
# to fold. The published "Arm F matches 5 of 12" was measured with
# `--table-grid` ON; the shipped default scored 2 of 12, and `docx` 0 of 3.
#
# Arm E therefore joins the default, with the same explicit opt-out every arm
# in it has. `--keep-table-heading` does NOT join: measured on two K2 bundles
# it buys 35 bytes and zero rank positions.
GRID_SHEET_DOCUMENT = {
"theta.md": (
"## 7 Romskjema\n\nInnledende avsnitt.\n\n"
"+-------+-------+\n| Navn | Verdi |\n+=======+=======+\n"
"| Areal | 120 |\n+-------+-------+\n| Hoyde | 3 |\n+-------+-------+\n"
)
}
def inbox_for_grid_sheet(root: Path) -> Path:
inbox = root / "grid-inbox"
inbox.mkdir(parents=True, exist_ok=True)
for name, body in GRID_SHEET_DOCUMENT.items():
(inbox / name).write_text(body, encoding="utf-8", newline="")
return inbox
def test_the_build_default_is_now_arm_d_plus_arm_e_plus_arm_f(tmp_path: Path) -> None:
"""The red test for round 4's move: no flags must EQUAL the three flags.
Measured on a grid table, which is the one shape Arm E decides: with the
arm off, each rule line closes the block, so the sheet lands as one concept
per row group with a title naming a LINE NUMBER -- `Tabell linje 6` -- and
the fold has no whole table to fold back into the heading above it. That is
the defect the round-3 default shipped with, and it is asserted on titles
rather than a count because a wrong cut can still produce a right number.
"""
inbox = inbox_for_grid_sheet(tmp_path)
plans = tmp_path / "plans-grid-default"
assert build(inbox, tmp_path / "grid-default", "--plans-dir", str(plans)) == 0
assert plan_titles(plans) == ["7 Romskjema"]
explicit = tmp_path / "plans-grid-explicit"
assert (
build(
inbox,
tmp_path / "grid-explicit",
"--plans-dir",
str(explicit),
"--outline-run",
"3",
"--table-grid",
"--unit-fold",
)
== 0
)
assert plan_titles(explicit) == plan_titles(plans)
def test_arm_e_in_the_default_has_an_explicit_opt_out(tmp_path: Path) -> None:
"""The other half: `--no-table-grid` gets the pre-move cut back, and says so.
Same rule every other arm in the default follows -- a default a caller
cannot turn off is not a default. The titles asserted here are the ones the
round-3 default produced on this fixture.
"""
inbox = inbox_for_grid_sheet(tmp_path)
plans = tmp_path / "plans-grid-opt-out"
assert (
build(inbox, tmp_path / "grid-opt-out", "--plans-dir", str(plans), "--no-table-grid") == 0
)
assert plan_titles(plans) == [
"7 Romskjema",
"Tabell linje 6",
"Tabell linje 8",
"Tabell linje 10",
]