fix(structure): the index resolves a parent naming a segment of its own document

K3-21 C. `resolve_structure` asks `_segment_lookup` first for a `parent`
edge: `(source_file, segment_id)` -> concept name, keyed off each concept's
own frontmatter (`DocumentStructure.declared`, no file read again), so a
pointer lands only inside the pointing concept's document -- `p1` exists in
every document of a multi-document bundle. A value no segment answers to is a
document number and is looked up exactly as before; a pointer naming nothing
keeps `UNRESOLVED_MARKER`. The rendering rule is untouched: a resolved
relation renders as its subject, so `parent: p1977?` becomes `parent: p1977`.

Moved on purpose, each named: both segmented goldens' index files
(`examples/ingest-golden-segmented{,-okf-v0-2}/expected-bundle/krav/1-{1,2}/
index.md`), whose declared parents s1 and s2 -> s0 rendered `parent: s0?`
while s0 stood in the bundle -- four lines, `?` removed. The four goldens
`test_the_four_existing_goldens_are_untouched` guards are not among them.
`skills/okf-consume/` regenerated, because the golden's index bytes -- and so
its ref -- moved. `test_shell_parent`'s byte test now expects the resolved
facet. README and CLAUDE.md no longer say the index renders it unresolved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-11 13:55:03 +02:00
commit 59703699a5
10 changed files with 50 additions and 17 deletions

View file

@ -641,10 +641,13 @@ and fixtures, never code.
set is the same with it as without it; a text that does not fit is cut to set is the same with it as without it; a text that does not fit is cut to
the longest prefix that does and marked `truncated`. Whether either default the longest prefix that does and marked `truncated`. Whether either default
moves is K3-21 B's measurement, not this line's. moves is K3-21 B's measurement, not this line's.
**The index renders every such parent **Since K3-21 C the index RESOLVES such a parent**: it rendered every one
UNRESOLVED** (`parent: p1?`): `structure` reads `parent` as a document unresolved (`parent: p1?`, 675 of 675) because `structure` read `parent` as
NUMBER, and a segment id answers to none -- the one key with two meanings a document NUMBER and a segment id answers to none. `structure._segment_lookup`
`inbox.py` already names for a declared parent. Report: keys `(source_file, segment_id)` off the concept's own frontmatter and is
asked first, inside the pointing concept's document; a value no segment
answers to is a number, looked up as before, and a pointer naming nothing
keeps `?`. The one key keeps its two meanings (`inbox.py`). Report:
`docs/2026-09-11-k3-runde20-delt-katalog-og-arvet-kontekst.md`. `docs/2026-09-11-k3-runde20-delt-katalog-og-arvet-kontekst.md`.
- **A TWELFTH flag, `--pdf-outline`, is OFF** (round 12, 2026-09-10) and it is - **A TWELFTH flag, `--pdf-outline`, is OFF** (round 12, 2026-09-10) and it is
the only one here that does not read the extracted text at all: it cuts a PDF the only one here that does not read the extracted text at all: it cuts a PDF

View file

@ -177,9 +177,9 @@ heading-only body gains one line, `Enclosing section: [title](/path)`, in the
bundle-relative form SPEC § 6.1 recommends. `okf consume --follow-parent` bundle-relative form SPEC § 6.1 recommends. `okf consume --follow-parent`
carries the enclosing section's text inside `parent` as well, with that carries the enclosing section's text inside `parent` as well, with that
concept's own `sha256`, and only from the room the cut left — so it never concept's own `sha256`, and only from the room the cut left — so it never
displaces an excerpt. Whether either default moves is measured separately. One cost is known and not repaired: the index projects displaces an excerpt. Whether either default moves is measured separately. The index resolves the pointer too: a segment id is looked up among the
`parent` as a document number, so a segment id always renders there as concepts of its own document, so it renders `parent: p1` when the concept it
unresolved (`parent: p1?`), even though the concept it names is in the bundle. names is in the bundle and keeps the `?` only when nothing answers to it.
### The segmentation flags ### The segmentation flags

View file

@ -1 +1 @@
- [Foerste krav](foerste-krav.md) — parent: s0?; adjudication: proposed - [Foerste krav](foerste-krav.md) — parent: s0; adjudication: proposed

View file

@ -1 +1 @@
- [Andre krav](andre-krav.md) — parent: s0?; adjudication: proposed - [Andre krav](andre-krav.md) — parent: s0; adjudication: proposed

View file

@ -1 +1 @@
- [Foerste krav](foerste-krav.md) — parent: s0? - [Foerste krav](foerste-krav.md) — parent: s0

View file

@ -1 +1 @@
- [Andre krav](andre-krav.md) — parent: s0? - [Andre krav](andre-krav.md) — parent: s0

View file

@ -1,6 +1,6 @@
--- ---
name: b-golden-segmented-okf-v0-2-consume name: b-golden-segmented-okf-v0-2-consume
description: Answer one question about the OKF bundle `b-golden-segmented-okf-v0-2` (3 concepts, ref sha256-tree:f85646c1f77982a69b97afca08209f369b79cd39c217f6f2c074c2dc5bac23fe) from a bounded payload assembled by a deterministic pre-pass, marking every claim with its source, its title and its provenance locator. Use whenever a question is about what that bundle's documents require, say or contain. Generated by `okf skill`; every value below is measured against this bundle at this ref. description: Answer one question about the OKF bundle `b-golden-segmented-okf-v0-2` (3 concepts, ref sha256-tree:cce7a02c769793cdb6e3afda45c955461b57373deab13a986d8bf7843d6e436f) from a bounded payload assembled by a deterministic pre-pass, marking every claim with its source, its title and its provenance locator. Use whenever a question is about what that bundle's documents require, say or contain. Generated by `okf skill`; every value below is measured against this bundle at this ref.
--- ---
# b-golden-segmented-okf-v0-2 consumption # b-golden-segmented-okf-v0-2 consumption
@ -9,7 +9,7 @@ Answer one question about the `b-golden-segmented-okf-v0-2` bundle, from the pay
assembled, at one ref. assembled, at one ref.
**This file is an instantiated copy of `skills/okf-consume-template/SKILL.md`,** generated by `okf skill` for one bundle: `b-golden-segmented-okf-v0-2` at ref **This file is an instantiated copy of `skills/okf-consume-template/SKILL.md`,** generated by `okf skill` for one bundle: `b-golden-segmented-okf-v0-2` at ref
`sha256-tree:f85646c1f77982a69b97afca08209f369b79cd39c217f6f2c074c2dc5bac23fe`. Every value below was measured against those bytes. If the `sha256-tree:cce7a02c769793cdb6e3afda45c955461b57373deab13a986d8bf7843d6e436f`. Every value below was measured against those bytes. If the
bundle moves, the ref moves with it and this file is stale — regenerate bundle moves, the ref moves with it and this file is stale — regenerate
it rather than editing a number here. The section headings are fixed: it rather than editing a number here. The section headings are fixed:
the contract checker reads them by name. the contract checker reads them by name.
@ -25,7 +25,7 @@ Step 1 is always the pre-pass. Run it, read its JSON payload, and judge that.
okf consume \ okf consume \
examples/ingest-golden-segmented-okf-v0-2/expected-bundle \ examples/ingest-golden-segmented-okf-v0-2/expected-bundle \
--question "your question" \ --question "your question" \
--ref sha256-tree:f85646c1f77982a69b97afca08209f369b79cd39c217f6f2c074c2dc5bac23fe \ --ref sha256-tree:cce7a02c769793cdb6e3afda45c955461b57373deab13a986d8bf7843d6e436f \
--out /tmp/payload.json --out /tmp/payload.json
``` ```

View file

@ -2,7 +2,7 @@
"contract": "okf-consumption/1", "contract": "okf-consumption/1",
"bundle": { "bundle": {
"bundle_id": "b-golden-segmented-okf-v0-2", "bundle_id": "b-golden-segmented-okf-v0-2",
"ref": "sha256-tree:f85646c1f77982a69b97afca08209f369b79cd39c217f6f2c074c2dc5bac23fe" "ref": "sha256-tree:cce7a02c769793cdb6e3afda45c955461b57373deab13a986d8bf7843d6e436f"
}, },
"budget": { "budget": {
"unit": "utf-8 bytes of emitted JSON", "unit": "utf-8 bytes of emitted JSON",

View file

@ -411,6 +411,28 @@ def _lookup(documents: Mapping[str, DocumentStructure]) -> dict[str, str]:
return {key: next(iter(owners)) for key, owners in claims.items() if len(owners) == 1} return {key: next(iter(owners)) for key, owners in claims.items() if len(owners) == 1}
#: The key a segmentation plan's entry id is written under (`inbox.py`), and
#: the value a declared `parent` names.
SEGMENT_ID_KEY = "segment_id"
def _segment_lookup(documents: Mapping[str, DocumentStructure]) -> dict[tuple[str, str], str]:
"""`(source_file, segment_id)` -> concept name, for every concept a plan wrote.
A plan entry's `parent_id` names another entry of the SAME plan, and a plan
is one document's: `p1` exists in every document of a multi-document bundle,
so the key carries the source file. `declared` is the concept's own
frontmatter, verbatim, which is where the door wrote `segment_id` -- no
file is read again. A key claimed twice is dropped, `_lookup`'s rule.
"""
claims: dict[tuple[str, str], set[str]] = {}
for name, document in documents.items():
segment_id = document.declared.get(SEGMENT_ID_KEY, "").strip()
if segment_id:
claims.setdefault((document.source_file, segment_id), set()).add(name)
return {key: next(iter(owners)) for key, owners in claims.items() if len(owners) == 1}
def resolve_structure(documents: Mapping[str, DocumentStructure]) -> BundleStructure: def resolve_structure(documents: Mapping[str, DocumentStructure]) -> BundleStructure:
"""Resolve every pointer in `documents` against the bundle as a whole. """Resolve every pointer in `documents` against the bundle as a whole.
@ -426,17 +448,24 @@ def resolve_structure(documents: Mapping[str, DocumentStructure]) -> BundleStruc
function proposes on its own same number, ordered versions always is. function proposes on its own same number, ordered versions always is.
""" """
by_key = _lookup(documents) by_key = _lookup(documents)
by_segment = _segment_lookup(documents)
edges: list[StructureEdge] = [] edges: list[StructureEdge] = []
for name in sorted(documents): for name in sorted(documents):
document = documents[name] document = documents[name]
if document.parent_number is not None: if document.parent_number is not None:
# One key, two meanings (`inbox.py`): a plan's DECLARED `parent_id`
# names a segment of the same document, and `structure`'s DERIVED
# parent is a document number. The segment is asked first and only
# inside the pointing concept's own document; a value no segment
# answers to is a number, looked up exactly as before.
edges.append( edges.append(
StructureEdge( StructureEdge(
source=name, source=name,
kind="parent", kind="parent",
subject=document.parent_number, subject=document.parent_number,
target=by_key.get(document.parent_number.upper()), target=by_segment.get((document.source_file, document.parent_number))
or by_key.get(document.parent_number.upper()),
derived="parent" in document.derived, derived="parent" in document.derived,
) )
) )

View file

@ -179,7 +179,8 @@ def test_a_build_without_the_flag_is_the_build_with_the_opt_out(tmp_path: Path)
text = (flagged / relative).read_text(encoding="utf-8") text = (flagged / relative).read_text(encoding="utf-8")
if relative.name == "index.md": if relative.name == "index.md":
after = text.split("\n") after = text.split("\n")
assert [re.sub(r"parent: p\d+\?; ", "", line) for line in after] == before # Resolved since K3-21 C: the concept the pointer names is here.
assert [re.sub(r"parent: p\d+; ", "", line) for line in after] == before
continue continue
text, links = re.subn(r"\n\nEnclosing section: \[[^\]]+\]\(/[^)\s]+\)\n\Z", "\n", text) text, links = re.subn(r"\n\nEnclosing section: \[[^\]]+\]\(/[^)\s]+\)\n\Z", "\n", text)
linked += links linked += links