Until now "run the door over a folder" was a shell loop over two scripts under `tools/`, with nine flags between them and a `--path-prefix` rule that lived in a code block in a measurement report. Neither script was packaged (`pyproject.toml` ships `src/llm_ingestion_okf` only), so the path the published K1/K2 numbers were measured on was reachable from a clone and nowhere else. `okf build <folder> --bundle <dir>` is that path, packaged, declared as a console script and installed with the wheel. It is orchestration only: the proposer and the corpus harness MOVED into the package (`llm_ingestion_okf.propose`, `llm_ingestion_okf.corpus`) and the two `tools/` scripts became thin entry points to them, so the published reproduction blocks still run and there is exactly one implementation of each rule. Neither move adds a dependency or a model call. Two decisions belong to this layer and are stated where they are made. A document's proposed paths are scoped by its RELATIVE PATH minus the extension, not its basename: the door walks recursively now, and two documents named alike in different folders would otherwise collide on a path Door B is supposed to make impossible rather than merely detect. And omitted timestamps do not come from the clock -- `--ingested-at` and `--proposed-at` default to one shared epoch constant, because a wall-clock default would put a changing byte in the artifact and take rebuild-equals-incremental away from every caller who did not pass them. Arm C and Arm D stay off and are not exposed here. Measured on the 43-file K2 corpus, one invocation against the two-script bundle of 2026-09-03: N = 43 computed, merged 39/43, coded rejections 4/43 (`extractor_unknown` 3, `extractor_empty_pdf` 1), K1b 39 + 4 = 43, exit 0, 779.43 s. 1107 of 1108 files byte-identical. The one that differs is the root `index.md`, by exactly the `log.md` link a commit fifteen hours younger than the stored artifact adds -- appending that line to the stored file reproduces the new one byte for byte. Against the two scripts at THIS commit the trees agree in full, which is what the byte-identity test holds. Suite 1127 passed after `git add` (1113 before), mypy --strict clean, ruff clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
401 lines
15 KiB
Python
401 lines
15 KiB
Python
"""`okf build <inbox> --bundle <dir>`: one installed command, the same bytes.
|
|
|
|
The two-script path this replaces is real and documented (`docs/2026-09-03-k2-
|
|
bundle-rebuild.md`, `docs/2026-09-04-k3-arm-c.md`): a shell loop over
|
|
`tools/okf_propose_segments.py`, then `tools/okf_corpus_run.py`. Neither script
|
|
is packaged, so "run the door over a folder" was reachable only from a clone,
|
|
and only by retyping a nine-flag invocation whose `--path-prefix` rule lived in
|
|
a code block in a report.
|
|
|
|
Four properties, each the answer to a way that could go wrong:
|
|
|
|
- **The command exists in an INSTALLED copy.** Measured against a real install
|
|
into a throwaway venv rather than against the clone, because the clone has
|
|
`tools/` on disk and would pass whatever the wheel contains.
|
|
- **The bytes do not move.** The bundle `okf build` produces is compared
|
|
byte-for-byte against the bundle the two scripts produce from the same
|
|
inbox -- the two scripts run as subprocesses, not re-implemented here.
|
|
"Similar" is not the requirement.
|
|
- **K1b is on stdout and in the exit status.** The conservation identity
|
|
`merged + coded rejections == N` is the one number a caller must not have to
|
|
take on trust, and a run where it breaks exits non-zero.
|
|
- **Omitting the timestamps is deterministic.** A wall-clock default would put
|
|
a changing byte in the artifact and break rebuild-equals-incremental (K6,
|
|
`tests/test_segmented_rebuild.py`) for anyone who did not pass the flags.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from dataclasses import replace
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from llm_ingestion_okf import cli
|
|
from llm_ingestion_okf.corpus import CorpusReport
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
TOOLS = PROJECT_ROOT / "tools"
|
|
|
|
INGESTED_AT = "2026-09-03T00:00:00Z"
|
|
PROPOSED_AT = "2026-09-03T00:00:00Z"
|
|
BUNDLE_ID = "cli-build-fixture"
|
|
OKF_VERSION = "0.2"
|
|
|
|
# Headings the mechanical rules match, in a tree with subdirectories -- the
|
|
# door walks recursively now, so a flat fixture would leave the interesting
|
|
# half of the prefix rule unmeasured.
|
|
DOCUMENTS = {
|
|
"alpha.md": (
|
|
"# 1 Innledning\n\nDette dokumentet beskriver krav til seksjonering.\n\n"
|
|
"# 1.1 Omfang\n\nOmfanget er hele anlegget og alle tilhoerende systemer.\n"
|
|
),
|
|
"sub/beta.md": (
|
|
"# 2 Brannkonsept\n\nBrannkonseptet stiller krav til roemningsveier.\n\n"
|
|
"# 2.1 Roemning\n\nRoemningsveier skal vaere merket og fri for hindringer.\n"
|
|
),
|
|
"sub/deep/gamma.md": (
|
|
"# 3 Vedlikehold\n\nVedlikeholdet foelger en fast plan gjennom aaret.\n\n"
|
|
"# 3.1 Intervaller\n\nIntervallene er angitt i tabellen under punkt tre.\n"
|
|
),
|
|
}
|
|
|
|
|
|
def inbox_with_subdirectories(root: Path) -> Path:
|
|
inbox = root / "inbox"
|
|
for name, body in DOCUMENTS.items():
|
|
path = inbox / name
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(body, encoding="utf-8", newline="")
|
|
return inbox
|
|
|
|
|
|
def tree(root: Path) -> dict[str, bytes]:
|
|
"""Every file under a bundle, keyed by relative path. The comparison unit."""
|
|
return {
|
|
path.relative_to(root).as_posix(): path.read_bytes()
|
|
for path in sorted(root.rglob("*"))
|
|
if path.is_file()
|
|
}
|
|
|
|
|
|
def two_script_bundle(inbox: Path, out: Path) -> Path:
|
|
"""The path this command replaces, run as it is documented, as subprocesses.
|
|
|
|
Re-implementing the loop here would compare the CLI against a copy of
|
|
itself. Driving the actual scripts is what makes the byte comparison mean
|
|
"the old path and the new path agree".
|
|
"""
|
|
plans = out / "plans"
|
|
plans.mkdir(parents=True, exist_ok=True)
|
|
bundle = out / "bundle"
|
|
sources = sorted(
|
|
(path for path in inbox.rglob("*") if path.is_file()),
|
|
key=lambda path: path.relative_to(inbox).as_posix(),
|
|
)
|
|
for position, source in enumerate(sources, start=1):
|
|
relative = source.relative_to(inbox)
|
|
subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(TOOLS / "okf_propose_segments.py"),
|
|
str(source),
|
|
"--out",
|
|
str(plans / f"{position:02d}.json"),
|
|
"--path-prefix",
|
|
relative.with_suffix("").as_posix(),
|
|
"--proposed-at",
|
|
PROPOSED_AT,
|
|
],
|
|
capture_output=True,
|
|
check=True,
|
|
)
|
|
subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(TOOLS / "okf_corpus_run.py"),
|
|
"--corpus",
|
|
str(inbox),
|
|
"--report",
|
|
str(out / "report.md"),
|
|
"--bundle",
|
|
str(bundle),
|
|
"--ingested-at",
|
|
INGESTED_AT,
|
|
"--plans-dir",
|
|
str(plans),
|
|
"--bundle-id",
|
|
BUNDLE_ID,
|
|
"--okf-version",
|
|
OKF_VERSION,
|
|
],
|
|
capture_output=True,
|
|
check=True,
|
|
)
|
|
return bundle
|
|
|
|
|
|
def build(inbox: Path, bundle: Path, *extra: str) -> int:
|
|
return cli.main(
|
|
[
|
|
"build",
|
|
str(inbox),
|
|
"--bundle",
|
|
str(bundle),
|
|
"--bundle-id",
|
|
BUNDLE_ID,
|
|
"--okf-version",
|
|
OKF_VERSION,
|
|
*extra,
|
|
]
|
|
)
|
|
|
|
|
|
# --- the command exists, in an installed copy ------------------------------
|
|
|
|
|
|
def test_the_console_script_is_declared_and_resolves() -> None:
|
|
"""`[project.scripts]` is what makes `okf` a command rather than a file.
|
|
|
|
Read off `pyproject.toml` rather than assumed from a working entry point in
|
|
this venv: an editable install keeps working after the table is deleted.
|
|
"""
|
|
tomllib = pytest.importorskip("tomllib")
|
|
pyproject = tomllib.loads((PROJECT_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
|
|
assert pyproject["project"]["scripts"] == {"okf": "llm_ingestion_okf.cli:main"}
|
|
assert callable(cli.main)
|
|
|
|
|
|
@pytest.mark.skipif(shutil.which("uv") is None, reason="uv is the install driver on this machine")
|
|
def test_build_help_exits_zero_from_an_installed_copy(tmp_path: Path) -> None:
|
|
"""The whole point of packaging it: it runs where `tools/` does not exist.
|
|
|
|
`--no-deps` on purpose. The guard is the one runtime dependency and it is
|
|
off-index, so pulling it here would make this test a network test; leaving
|
|
it out also measures something worth knowing -- that the build path does
|
|
not import the security boundary at module load.
|
|
"""
|
|
uv = shutil.which("uv")
|
|
assert uv is not None
|
|
venv = tmp_path / "venv"
|
|
subprocess.run([uv, "venv", str(venv), "-q"], check=True, capture_output=True)
|
|
python = venv / "bin" / "python"
|
|
subprocess.run(
|
|
[uv, "pip", "install", "--no-deps", "--python", str(python), str(PROJECT_ROOT), "-q"],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
okf = venv / "bin" / "okf"
|
|
assert okf.is_file(), "the console script was not installed"
|
|
|
|
proc = subprocess.run([str(okf), "build", "--help"], capture_output=True, text=True)
|
|
assert proc.returncode == 0, proc.stderr
|
|
assert "--bundle" in proc.stdout
|
|
|
|
# It must be the INSTALLED copy answering, not the clone reached through a
|
|
# stray path entry -- otherwise this test passes on a machine where the
|
|
# wheel ships nothing.
|
|
where = subprocess.run(
|
|
[str(python), "-c", "import llm_ingestion_okf.cli as m; print(m.__file__)"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
assert str(PROJECT_ROOT / "src") not in where.stdout
|
|
assert str(venv) in where.stdout
|
|
|
|
|
|
# --- the bytes do not move -------------------------------------------------
|
|
|
|
|
|
def test_one_command_gives_the_same_bundle_as_the_two_scripts(tmp_path: Path) -> None:
|
|
"""Byte-identical, over a tree with subdirectories. The requirement."""
|
|
inbox = inbox_with_subdirectories(tmp_path)
|
|
reference = two_script_bundle(inbox, tmp_path / "reference")
|
|
|
|
bundle = tmp_path / "cli-bundle"
|
|
assert build(inbox, bundle, "--ingested-at", INGESTED_AT, "--proposed-at", PROPOSED_AT) == 0
|
|
|
|
assert tree(bundle) == tree(reference)
|
|
assert len(tree(bundle)) > 1, "an empty bundle would compare equal to an empty bundle"
|
|
|
|
|
|
def test_the_bundle_carries_the_adjudication_layer_the_plans_produce(tmp_path: Path) -> None:
|
|
"""A positive control on the comparison above.
|
|
|
|
Two flat bundles would also compare equal, and would prove that the
|
|
segmentation lane never ran on either side.
|
|
"""
|
|
inbox = inbox_with_subdirectories(tmp_path)
|
|
bundle = tmp_path / "bundle"
|
|
assert build(inbox, bundle, "--ingested-at", INGESTED_AT, "--proposed-at", PROPOSED_AT) == 0
|
|
bodies = [body for name, body in tree(bundle).items() if name.endswith(".md")]
|
|
assert any(b"adjudication:" in body for body in bodies)
|
|
assert any(b"proposed" in body for body in bodies)
|
|
|
|
|
|
def test_arm_c_and_arm_d_are_off_unless_asked_for(tmp_path: Path) -> None:
|
|
"""Both arms stay off by default -- measured on the artifact, not the flag.
|
|
|
|
`rule:size-split` and `rule:outline` are the markers the two arms write
|
|
into a plan's `derived` list, so their absence is the arms being off.
|
|
"""
|
|
inbox = inbox_with_subdirectories(tmp_path)
|
|
plans = tmp_path / "plans"
|
|
bundle = tmp_path / "bundle"
|
|
assert build(inbox, bundle, "--plans-dir", str(plans), "--proposed-at", PROPOSED_AT) == 0
|
|
written = sorted(plans.glob("*.json"))
|
|
assert written, "the fixture must produce plans for this control to mean anything"
|
|
for path in written:
|
|
text = path.read_text(encoding="utf-8")
|
|
assert "rule:size-split" not in text
|
|
assert "rule:outline" not in text
|
|
|
|
|
|
def test_segments_off_builds_a_flat_bundle_without_a_bundle_id(tmp_path: Path) -> None:
|
|
"""`--segments off` is the unsegmented door, and asks for no root values."""
|
|
inbox = inbox_with_subdirectories(tmp_path)
|
|
bundle = tmp_path / "bundle"
|
|
assert (
|
|
cli.main(
|
|
["build", str(inbox), "--bundle", str(bundle), "--segments", "off"],
|
|
)
|
|
== 0
|
|
)
|
|
bodies = [body for name, body in tree(bundle).items() if name.endswith(".md")]
|
|
assert bodies
|
|
assert not any(b"adjudication:" in body for body in bodies)
|
|
|
|
|
|
def test_segmenting_without_a_bundle_id_is_refused_before_any_write(tmp_path: Path) -> None:
|
|
"""A profile names a key and the caller owns its value -- checked up front."""
|
|
inbox = inbox_with_subdirectories(tmp_path)
|
|
bundle = tmp_path / "bundle"
|
|
assert cli.main(["build", str(inbox), "--bundle", str(bundle)]) == 2
|
|
assert not bundle.exists()
|
|
|
|
|
|
# --- K1b on stdout, and in the exit status ---------------------------------
|
|
|
|
|
|
def test_the_conservation_identity_is_printed(
|
|
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
inbox = inbox_with_subdirectories(tmp_path)
|
|
bundle = tmp_path / "bundle"
|
|
assert build(inbox, bundle, "--ingested-at", INGESTED_AT, "--proposed-at", PROPOSED_AT) == 0
|
|
out = capsys.readouterr().out
|
|
assert f"merged + coded rejections = {len(DOCUMENTS)}" in out
|
|
assert f"N = {len(DOCUMENTS)}" in out
|
|
|
|
|
|
def test_a_broken_conservation_identity_exits_non_zero(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
"""The negative control. Without it every green run above proves nothing."""
|
|
inbox = inbox_with_subdirectories(tmp_path)
|
|
bundle = tmp_path / "bundle"
|
|
real = cli.measure
|
|
|
|
def losing_a_file(*args: object, **kwargs: object) -> CorpusReport:
|
|
report = real(*args, **kwargs) # type: ignore[arg-type]
|
|
return replace(report, n=report.n + 1, unaccounted=("ghost.md",))
|
|
|
|
monkeypatch.setattr(cli, "measure", losing_a_file)
|
|
assert build(inbox, bundle, "--ingested-at", INGESTED_AT, "--proposed-at", PROPOSED_AT) == 1
|
|
assert "K1b FAILED" in capsys.readouterr().err
|
|
|
|
|
|
# --- omitting the timestamps is deterministic ------------------------------
|
|
|
|
|
|
def test_omitted_timestamps_do_not_come_from_the_wall_clock(tmp_path: Path) -> None:
|
|
"""Two runs of the same inbox, no timestamp flags, identical bytes.
|
|
|
|
A `datetime.now()` default would pass a single run and fail here -- which
|
|
is the whole reason this test compares two builds instead of inspecting the
|
|
default's value.
|
|
"""
|
|
inbox = inbox_with_subdirectories(tmp_path)
|
|
first = tmp_path / "first"
|
|
second = tmp_path / "second"
|
|
assert build(inbox, first) == 0
|
|
assert build(inbox, second) == 0
|
|
assert tree(first) == tree(second)
|
|
|
|
|
|
def test_a_rebuild_from_scratch_equals_the_incremental_bundle(tmp_path: Path) -> None:
|
|
"""K6 through the command, with the timestamps left to the default.
|
|
|
|
`tests/test_segmented_rebuild.py` holds this property for the library call.
|
|
The default stamp is the seam the command adds, so the property is measured
|
|
again HERE rather than assumed to survive the new layer.
|
|
"""
|
|
inbox = inbox_with_subdirectories(tmp_path)
|
|
incremental = tmp_path / "incremental"
|
|
assert build(inbox, incremental) == 0
|
|
# A second document arrives, and the same bundle is updated in place.
|
|
(inbox / "sub" / "delta.md").write_text(
|
|
"# 4 Tilsyn\n\nTilsynet gjennomfoeres av en uavhengig part hvert aar.\n",
|
|
encoding="utf-8",
|
|
newline="",
|
|
)
|
|
assert build(inbox, incremental) == 0
|
|
|
|
rebuilt = tmp_path / "rebuilt"
|
|
assert build(inbox, rebuilt) == 0
|
|
assert tree(incremental) == tree(rebuilt)
|
|
|
|
|
|
def test_the_default_stamp_is_named_once(tmp_path: Path) -> None:
|
|
"""One constant, not two: the ingest stamp and the proposal stamp agree.
|
|
|
|
Two independently-defaulted literals would drift, and the drift would only
|
|
show up as two bundles that differ in a field nobody passed.
|
|
"""
|
|
assert cli.DEFAULT_STAMP == "1970-01-01T00:00:00Z"
|
|
inbox = inbox_with_subdirectories(tmp_path)
|
|
plans = tmp_path / "plans"
|
|
bundle = tmp_path / "bundle"
|
|
assert build(inbox, bundle, "--plans-dir", str(plans)) == 0
|
|
for path in sorted(plans.glob("*.json")):
|
|
assert cli.DEFAULT_STAMP in path.read_text(encoding="utf-8")
|
|
concepts = [body for name, body in tree(bundle).items() if not name.endswith("index.md")]
|
|
assert any(cli.DEFAULT_STAMP.encode() in body for body in concepts)
|
|
# The log dates its entry from the same stamp, to the day.
|
|
assert cli.DEFAULT_STAMP[:10] in (bundle / "log.md").read_text(encoding="utf-8")
|
|
|
|
|
|
# --- the one implementation ------------------------------------------------
|
|
|
|
|
|
def test_the_scripts_are_thin_entries_to_the_packaged_implementation() -> None:
|
|
"""No duplicated logic: `tools/` calls the package, and is small enough to see.
|
|
|
|
A line budget is a proxy, and a coarse one -- but the failure it catches is
|
|
exactly the one that matters here: logic copied back into `tools/` so the
|
|
two paths can drift apart while both stay green.
|
|
"""
|
|
for name in ("okf_propose_segments.py", "okf_corpus_run.py"):
|
|
source = (TOOLS / name).read_text(encoding="utf-8")
|
|
assert "from llm_ingestion_okf." in source
|
|
code = [
|
|
line
|
|
for line in source.splitlines()
|
|
if line.strip() and not line.lstrip().startswith("#")
|
|
]
|
|
assert len(code) < 40, f"tools/{name} carries logic again ({len(code)} lines)"
|
|
|
|
|
|
def test_the_packaged_modules_do_not_reach_back_into_the_clone() -> None:
|
|
"""`sys.path` surgery is what an unpackaged script needs and a package must not.
|
|
|
|
Left in place it would half-work from an install: importable, and reading a
|
|
`tools/` directory that is not there.
|
|
"""
|
|
for name in ("propose.py", "corpus.py", "cli.py"):
|
|
source = (PROJECT_ROOT / "src" / "llm_ingestion_okf" / name).read_text(encoding="utf-8")
|
|
assert "sys.path" not in source
|