llm-ingestion-okf/docs/2026-09-03-k2-bundle-rebuild.md
Kjell Tore Guttormsen 2a2eb9c8bd test(corpus): a rebuild with log.md present reproduces the bundle exactly
`log.md` is written INTO a directory Door B enumerates on the next round: it
matches the concept glob and is excluded only by `index.md`'s name, so a
rebuild could have seen it as pre-existing curated content or pruned it.
Rebuild-equals-incremental is the property the segmented bundle rests on.

Measured on the real artifact, not only the synthetic: the K2 corpus was run
a second time into the same bundle and compared against a snapshot with
`diff -r`, exit 0 over all 1108 files. The test pins the same property in
seconds instead of 13 minutes.

Also corrects the report's reproduction command -- it documented plan
filenames the run did not use, and re-running it into the existing plans
directory would leave two files claiming one `source_sha256`, which
`_resolve_plans` refuses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 05:16:25 +02:00

8.6 KiB
Raw Blame History

Rebuilding the K2 bundle so it meets the consumption contract, 2026-09-03

A consumer measured the bundle this repository's corpus harness built on 2026-09-02 and found four things missing (docs/2026-09-03-syretest-s7a-k2.md in portfolio-optimiser): no adjudication key in any of the 39 concepts, no log.md, so N was not recoverable from the artifact, and two concepts above 100 000 tokens — one file, one concept, for the largest PDFs.

This is the rebuild, the cause, and the numbers. Counts only: the corpus is public procurement material, but nothing here needs a document body or a full filename list to be checkable.

The cause: one, not four

The harness passed profile=STRUCTURED_V1 and no segmentation plans at all.

grep -n "STRUCTURED_V1\|SEGMENTED\|segmentations" tools/okf_corpus_run.py
git log --oneline -- tools/okf_corpus_run.py

Before this session that printed two hits, both STRUCTURED_V1, and a single commit. STRUCTURED_V1 does not declare the segmentation capability, so a plan passed to it would have been refused outright rather than ignored — and none was passed, because nothing in the run path produced one.

All three findings follow from that. adjudication is written only inside the plan-covered branch (inbox.py), so a run with no plans cannot emit it. A document with no plan lands as one concept, which is what the >100 000-token concepts were. log.md was never written by anything.

The plan this work came from says so directly: step 17's Reuses names process_inbox "with the per-document plan mapping from Step 15". The harness shipped without that wiring. It is an omission in one file, not a design decision that was later regretted.

What changed

Three commits, each test-first.

  1. The harness replays plans and writes the bundle's log. --plans-dir selects the proposals to replay and the profile follows from it; --bundle-id and --okf-version are arguments, because a profile names a key and the caller owns its value (decision E1). log.md is written in SPEC §9 form and dated from ingested_at, never the wall clock. Without --plans-dir the run is the flat STRUCTURED_V1 run that produced the published K1/K2 numbers.
  2. The proposer scopes a document's segments under a caller's prefix. Measured first: 39 documents proposed 618 entries under 601 distinct paths — 17 paths claimed by two documents each. Section numbering is document-local, so this is structural. Every collision reaches Door B's gate, which refuses per document, so those documents would have become coded rejections instead of concepts. With --path-prefix set to each document's stem: 618 entries, 618 distinct paths, 0 collisions.
  3. Nothing to propose writes no artifact. 11 of the 39 documents proposed zero segments. The proposer wrote an empty artifact for each and exited 0; process_inbox refuses an empty plan by design, and the first rebuild attempt stopped on segmentation_plan_invalid before writing anything. Exit 1 and no file, distinct from exit 2, so a driver can tell "this document lands as one flat concept" from "stop".

The rebuild

Bundle: ~/corpora/okf-telling-20260829/K2-bundle-20260903/ — 5.5 MB, 1108 files: 629 concepts, 478 index.md (one per directory, the profile sets per_directory), and one log.md. Plans: ~/corpora/okf-telling-20260829/K2-plans-20260903/ — 28 of them, one per document that had something to propose. Both outside the repository, both durable.

Reproduce into a FRESH plans directory. A plan is selected by source_sha256, so re-running the proposer into a directory that already holds these plans leaves two files claiming the same hash, and _resolve_plans refuses that rather than picking one:

PLANS=~/corpora/okf-telling-20260829/K2-plans-$(date +%Y%m%d-%H%M%S); mkdir -p "$PLANS"
i=0
for f in ~/corpora/okf-telling-20260829/K2/trinn1/*; do
  i=$((i+1)); b=$(basename "$f")
  .venv/bin/python tools/okf_propose_segments.py "$f" \
    --out "$PLANS/$(printf '%02d' $i).json" \
    --path-prefix "${b%.*}" --proposed-at 2026-09-03T00:00:00Z
done   # exit 1 for the 11 with nothing to propose, exit 2 for the 4 unreadable
.venv/bin/python tools/okf_corpus_run.py \
  --corpus ~/corpora/okf-telling-20260829/K2/trinn1 \
  --report ~/corpora/okf-telling-20260829/K2-bundle-20260903-report.md \
  --bundle ~/corpora/okf-telling-20260829/K2-bundle-20260903 \
  --ingested-at 2026-09-03T00:00:00Z \
  --plans-dir "$PLANS" \
  --bundle-id k2-trinn1-20260903 --okf-version 0.2

Converter as the harness resolved it: the vendored pypandoc binary, version 3.9 — not the host's 3.10.2.

The numbers, each with its denominator

figure value
N (corpus directory file count, computed) 43
merged 39/43
coded rejections 4/43 (extractor_unknown 3, extractor_empty_pdf 1)
concepts 629
concepts carrying adjudication 618/629, every one of them proposed
body characters, max 217 472
body characters, median 441
concepts over 100 000 characters 4/629
wall time 784.29 s total, 18.239 s per file
K1b 39 + 4 = 43 = N, run exited 0
rebuild == incremental diff -r exit 0 over all 1108 files

The previous bundle, re-measured here rather than quoted: 39 concepts, 0/39 carrying adjudication, max body 267 548 characters, no log.md.

log.md is a file the run path writes into a directory Door B enumerates on the next round, so the rebuild property was re-measured rather than assumed: the whole corpus was run a second time into the same bundle and compared against a snapshot with diff -r, which exited 0. A test in tests/test_corpus_run.py pins the same property on a synthetic corpus, so it fails in seconds rather than in 13 minutes.

K1b is now recoverable from the bundle alone, which was the point of §9:

python - <<'PY'
import re; log=open('.../K2-bundle-20260903/log.md').read()
n=int(re.search(r'N = (\d+)',log).group(1))
merged=int(re.search(r'merged = (\d+)',log).group(1))
codes=[int(m.group(2)) for m in re.finditer(r'`([a-z_]+)`: (\d+)',log)]
assert merged+sum(codes)==n; print(merged,'+',sum(codes),'=',n)
PY

Two numbers that need their units stated

The consumer measured tokens (max 121 462); the figures above are characters, which is what this repository can count without adopting somebody's tokenizer. Their own two numbers imply ≈2.20 characters per token on this corpus. Applied to the maximum here that is ≈98 700 tokens — a derived figure, and the only one in this document that is not a direct count.

The 11 concepts with no adjudication

They are exactly the 11 documents that proposed zero segments: no plan, so the ordinary one-concept path, so no key. Marking them proposed would claim a proposal that was never made. A consumer distinguishes three states — proposed, adjudicated, and absent — and absent is the honest one here. Reported rather than changed: the wire form is a ratified contract (docs/plan/office-intake.md §45) and is not this session's to move.

Point 4: why the large concepts were not split — and which one still is not

Not "the proposer refused". It was never run. Given plans, the two documents the consumer named do split:

document entries proposed largest segment (chars)
Bilag 3.1 (was 270 572 chars, one concept) 34 180 604
Bilag 1 (was 257 072 chars, one concept) 18 142 667

Both are now below the 100 000-token line. In each the trailing segment still absorbs most of the document, because the headings the mechanical rules find in a PDF are largely table-of-contents lines near the front.

The largest concept in the new bundle is a different document: Bilag 9.1, at 217 472 characters, one of the 11 with no proposable structure at all. This is §10's "no declared structure" as Topic 1b measured it — 23 of 33 PDFs carry no outline, and 95 % of the outline entries that do exist are AutoCAD export metadata. The mechanical rules cannot reach it, and nothing here pretends otherwise. Reported, not fixed: Arm C is its own decision.

What this does not measure

  • The bundle is not re-measured against the consumption contract's checkable half by tools/okf_contract_check.py; that check reads a consumption skill, not a bundle.
  • pptx, odt and rtf still have denominator 0 in this corpus. Unmeasured, not passing.
  • Segmentation quality is unchanged. K3 measured 7 of 12 splits as too coarse and that number is a floor; the entries here are proposed precisely because no one has judged them.