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>
187 lines
7.1 KiB
Python
187 lines
7.1 KiB
Python
"""`okf project`: one folder in, one bundle plus one skill out.
|
|
|
|
The command adds no rule and owns no flag that changes a bundle's bytes, so
|
|
these tests are mostly about that: the project bundle must be the SAME bytes
|
|
`okf build` writes for the same folder at the same stamp, or there are two
|
|
build paths and the reports are pinned to one of them.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(PROJECT_ROOT / "src"))
|
|
|
|
from llm_ingestion_okf import project # noqa: E402
|
|
from llm_ingestion_okf.cli import build # noqa: E402
|
|
from llm_ingestion_okf.cli import main as okf_main # noqa: E402
|
|
from llm_ingestion_okf.errors import IngestError # noqa: E402
|
|
|
|
DOCUMENTS = {
|
|
"krav.md": (
|
|
"## 4 Grunnforhold\n\nGrunnen er morene over berg.\n\n"
|
|
"### 4.1 Loesmasser\n\nLoesmassene er telefarlige.\n"
|
|
),
|
|
"notat.md": "Et notat uten overskrift, uten tabell og uten nummerering.\n",
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def folder(tmp_path: Path) -> Path:
|
|
target = tmp_path / "Mine Dokumenter"
|
|
target.mkdir()
|
|
for name, body in DOCUMENTS.items():
|
|
(target / name).write_text(body, encoding="utf-8", newline="")
|
|
return target
|
|
|
|
|
|
def tree(root: Path) -> dict[str, str]:
|
|
return {
|
|
path.relative_to(root).as_posix(): hashlib.sha256(path.read_bytes()).hexdigest()
|
|
for path in sorted(root.rglob("*"))
|
|
if path.is_file()
|
|
}
|
|
|
|
|
|
def test_the_project_bundle_is_the_bytes_okf_build_writes(folder: Path, tmp_path: Path) -> None:
|
|
"""The invariant the whole command rests on: ONE build path, not two.
|
|
|
|
`okf project` runs `okf build` with this package's default and no flag list
|
|
of its own. If it ever grew one, a project bundle and a build bundle of the
|
|
same folder would differ, and every measurement report pinned to the build
|
|
path would be describing a bundle nobody produces.
|
|
"""
|
|
out = tmp_path / "project"
|
|
bundle, _, _ = project.create(folder, out=out)
|
|
|
|
reference = tmp_path / "reference"
|
|
build(folder, reference, bundle_id="mine-dokumenter", okf_version="0.2")
|
|
assert tree(bundle) == tree(reference)
|
|
|
|
|
|
def test_the_id_defaults_to_the_folder_name_in_the_id_grammar(folder: Path, tmp_path: Path) -> None:
|
|
out = tmp_path / "project"
|
|
bundle, skill_path, _ = project.create(folder, out=out)
|
|
assert bundle == out / ".okf" / "mine-dokumenter"
|
|
assert skill_path == out / ".claude" / "skills" / "mine-dokumenter-consume" / "SKILL.md"
|
|
assert bundle.is_dir() and skill_path.is_file()
|
|
|
|
|
|
def test_a_named_id_is_used_verbatim(folder: Path, tmp_path: Path) -> None:
|
|
out = tmp_path / "project"
|
|
bundle, skill_path, _ = project.create(folder, out=out, bundle_id="anbud-2026")
|
|
assert bundle.name == "anbud-2026"
|
|
assert skill_path.parent.name == "anbud-2026-consume"
|
|
|
|
|
|
def test_a_folder_name_that_reduces_to_nothing_refuses_by_code(tmp_path: Path) -> None:
|
|
"""A refusal with a code, not a bundle called `""`.
|
|
|
|
A folder named only in punctuation reduces to the empty string, and an
|
|
empty bundle id would produce a bundle whose concepts join on nothing.
|
|
"""
|
|
weird = tmp_path / "..."
|
|
weird.mkdir()
|
|
(weird / "a.md").write_text("# A\n\nKropp.\n", encoding="utf-8", newline="")
|
|
with pytest.raises(IngestError) as caught:
|
|
project.create(weird, out=tmp_path / "project")
|
|
assert caught.value.code == "manifest_invalid"
|
|
|
|
|
|
def test_the_folder_name_is_normalised_before_it_is_reduced(tmp_path: Path) -> None:
|
|
"""NFC first, for the reason the rest of this package normalises first.
|
|
|
|
macOS hands a filename over decomposed, so `é` arrives as `e` plus a
|
|
combining acute. Reduced without normalising, the same visible folder name
|
|
produces two different bundle ids depending on which form it arrived in.
|
|
"""
|
|
assert project.slug("Prosjekt É") == project.slug("Prosjekt É")
|
|
|
|
|
|
def test_the_summary_names_the_documents_that_landed_whole(folder: Path, tmp_path: Path) -> None:
|
|
"""SS 6.4 discipline applied to a summary: the number carries its denominator.
|
|
|
|
`notat.md` has no heading, no table and no numbered outline, so the rules
|
|
find no boundary and it lands as one concept. A reader who is told only
|
|
"3 concepts" cannot tell that asking about that document returns the whole
|
|
of it as one excerpt.
|
|
"""
|
|
out = tmp_path / "project"
|
|
_, _, summary = project.create(folder, out=out)
|
|
assert "Read 2 document(s)" in summary
|
|
assert "1 of 2 document(s) landed WHOLE" in summary
|
|
assert "notat.md" in summary
|
|
assert "krav.md" not in summary
|
|
assert "[sourced-not-sufficient]" in summary
|
|
assert f"NEXT: start claude again in {out}" in summary
|
|
|
|
|
|
def test_a_document_that_is_in_the_bundle_is_not_reported_as_missing(
|
|
folder: Path, tmp_path: Path
|
|
) -> None:
|
|
"""The known-positive for `inventory`'s first list.
|
|
|
|
Its own control: with every document ingested the list must be empty, and
|
|
with the bundle read against a DIFFERENT folder every document must appear.
|
|
A search that cannot find would report an empty list either way.
|
|
"""
|
|
out = tmp_path / "project"
|
|
bundle, _, _ = project.create(folder, out=out)
|
|
missing, whole = project.inventory(folder, bundle)
|
|
assert missing == ()
|
|
assert whole == ("notat.md",)
|
|
|
|
other = tmp_path / "other"
|
|
other.mkdir()
|
|
(other / "fremmed.md").write_text("# Fremmed\n\nKropp.\n", encoding="utf-8", newline="")
|
|
stranger, _ = project.inventory(other, bundle)
|
|
assert stranger == ("fremmed.md",)
|
|
|
|
|
|
def test_the_generated_skill_names_no_path_into_this_repository(
|
|
folder: Path, tmp_path: Path
|
|
) -> None:
|
|
"""O5's whole point, asserted where a user actually meets it.
|
|
|
|
The known-positive runs first: the string this searches for occurs in the
|
|
environment running the test, so a zero means the generator kept it out.
|
|
"""
|
|
out = tmp_path / "project"
|
|
_, skill_path, _ = project.create(folder, out=out)
|
|
text = skill_path.read_text(encoding="utf-8")
|
|
assert str(PROJECT_ROOT) in str(Path(__file__).resolve())
|
|
assert str(PROJECT_ROOT) not in text
|
|
assert "okf consume" in text
|
|
assert "okf check" in text
|
|
|
|
|
|
def test_the_subcommand_exists_and_reports_zero(folder: Path, tmp_path: Path) -> None:
|
|
assert okf_main(["project", str(folder), "--out", str(tmp_path / "project")]) == 0
|
|
|
|
|
|
def test_a_missing_folder_is_two_and_not_one(tmp_path: Path) -> None:
|
|
"""Three exit codes, not two: an unread folder is not a refused build."""
|
|
assert okf_main(["project", str(tmp_path / "nope"), "--out", str(tmp_path / "p")]) == 2
|
|
|
|
|
|
def test_the_installed_command_reaches_every_subcommand() -> None:
|
|
"""`okf --help` must LIST them, or a reader has to be told they exist.
|
|
|
|
The dispatch happens before argparse, so without the registration in
|
|
`parse_args` these four would work and be invisible.
|
|
"""
|
|
listed = subprocess.run(
|
|
[sys.executable, "-m", "llm_ingestion_okf.cli", "--help"],
|
|
capture_output=True,
|
|
text=True,
|
|
cwd=PROJECT_ROOT,
|
|
)
|
|
assert listed.returncode == 0
|
|
for command in ("build", "consume", "check", "skill", "project"):
|
|
assert command in listed.stdout, command
|