Four decisions: Door C projects and never derives (D-C1), the projection is key-agnostic so it is not about numbering (D-C2), an unrenderable value drops the facet and never the concept (D-C3), and a faceted entry is refreshed because it is the first entry that can go stale (D-C4). Also records a measurement about the measuring. The mutation harness first reported 11 of 11 killed; the number was entirely false, because it invoked pytest with an unrecognised `--timeout` flag and every run exited non-zero. The negative control caught it by being reported killed when it provably could not be. Honest result is 9 of 11, with both survivors analysed as equivalent mutants and the one real gap they exposed (profile threading pinned only by coincidence) closed with a direct test. Closes open item 6 of the Door B record, which asked exactly the question this answers: whose claim does the frontmatter make.
21 KiB
Structure derivation: numbering, hierarchy, cross-references, supersession
Answers order 20260826T124341Z-181848459 (from .claude, 2026-08-26). The
premise it waited on — the Door B / Door A capability gap — was closed in
dc9ea59: the answer was title derivation, not a door change and not a profile
on Door B alone.
The operator's requirement, quoted rather than paraphrased:
"Og akkurat dette MÅ llm-ingestion-okf kunne lage automatisk og så ideelt som mulig når dokumenter legges inn i en okf bundle innboks (som kan skje en eller flere ganger, altså additivt)"
where "akkurat dette" is the form .claude described: documents that are
numbered, hierarchical, cross-referencing, and versioned such that one edition
supersedes another.
What was measured, and what the measurement does and does not say
ms-ai-architect ran a pre-registered bake-off on 2026-08-26 over 55 documents
and 40 gold questions. Every arm retrieved 40/40, so the measurement could not
separate the arms on retrieval quality. The only axis that separated them was
trap exposure: 18/20 for the OKF-index arm against 8/20 for a frontmatter
head-scan over the same content. The reason was measured independently on both
sides — the flat DEFAULT index carries title/date/status/supersedes 0 times
while the documents in the same bundle carry them 55/55/55/5.
That is evidence against DEFAULT's index, not against OKF. The metadata is
in the bundle; the index throws it away.
Decisions
D-S1. Every derived fact is marked, in the format
derived is a flat flow list naming exactly the fields this library inferred.
A field present and absent from that list was stated by the producer. The
format carries it, so this was not a case where "the format cannot express
confidence" — a flat list of strings round-trips through this library's
line-oriented parser and through the guard's T2 grammar alike.
Rationale: an unmarked heuristic is worse than no heuristic, because the consumer cannot know when to doubt it. With the marker, a consumer that trusts nothing derived can still use everything declared.
D-S2. Certainty is stated per field, not per document
| Field | Source | Certainty |
|---|---|---|
title |
producer's title key |
declared |
leading # heading |
derived | |
| filename stem | derived | |
number |
producer's number key |
declared |
| leading number token of filename, then of title | derived | |
parent |
arithmetic on number (drop last dotted part) |
inherits number's |
references |
producer's references key |
declared |
| bundle-local link targets + number mentions in the body | derived | |
supersedes |
producer's supersedes key |
declared |
same number + ordered version |
derived | |
version, status, date |
producer only | declared |
The number grammar is alpha-prefixed (N500, V720, R610.4) or dotted
numeric (4.2.1). A bare integer is deliberately not a number: admitting
12-things.md or 2026-notes.md would stamp a document number on most of a
second brain that never had one.
A parent our own grammar could never admit is not emitted at all (4.2 would
drop to 4). An unresolved list that never clears is one a consumer learns to
ignore.
D-S3. Supersession is never inferred from one document
Whether A supersedes B is a fact about a pair. derive_document_structure
records only what the producer declared; the bundle-level resolver may propose
a chain from a shared number plus ordered versions, and every edge it proposes
is marked derived. A group whose members do not all carry a version proposes
nothing — that is exactly the case where a guess would be indistinguishable
from a fact, and supersession is the relation a consumer is most likely to act
on.
D-S4. The index is a projection, recomputed from the whole bundle
This is the answer to the additive requirement, and it is a design decision
rather than an algorithm: resolve_structure is a pure function of the whole
document set, and Door B rewrites the managed region of the index from it every
round. Nothing is diffed, so there is no diffing algorithm to prove correct.
Three of the order's four additive requirements fall out by construction:
- rebuild == incremental — both are the same function of the same files;
- idempotence — the concept name is the identity, so re-dropping a document replaces its entry rather than doubling it;
- a round-3 document can supersede a round-1 document — the round-1 entry is rewritten, which an append-only index could never do.
The fourth is explicit: an unresolved pointer is rendered with a ? suffix
rather than omitted. During build-up, pointing at something not dropped yet is
the normal state; the dangerous version of it is the one that leaves no trace.
D-S5. Facets arrive as a new profile (STRUCTURED_V1), never on DEFAULT
DEFAULT states commons' ingest-spec §6 index layer. Changing its rendered
bytes from here would be this repo editing a contract it does not own (O2), and
it would churn every golden fixture Door B has written. entry_pattern is
link_pattern when a policy carries no facets, so DEFAULT and STRICT_V1
match the same lines and emit the same bytes; a test asserts Door B's output is
byte-identical with and without the new parameter.
D-S6. Door B keeps writing the literal generated: true
Not routed through profile.ownership.stamp(...), which would move DEFAULT's
bytes to the O2 mapping and orphan every bundle this door has already written.
Which stamp Door B should write is a separate question from this order's, and
answering it here would have answered it silently. Open.
D-S7. Structural edge inference only — never semantic
Added the same day, from external evidence that arrived mid-session
(.claude broadcast 20260826T223230Z, deep-research report on the OKF track).
A multimodal regulatory-document system on a near-identical problem shape
(numbered requirements, cross-references, tables and figures spanning pages,
arXiv 2606.29399) reports two numbers that bear directly on this work:
- index-selection strategy contributed +38.0 percentage points of accuracy — which is what a faceted index is;
- edge inference gave no accuracy gain at 2.8x the cost, and structural edges (References, Specifies) dominated single-hop questions while semantic edges only surfaced on composite multi-hop ones.
The second is a negative finding worth inheriting rather than re-measuring. It
does not condemn what is built here: references and parent are EXTRACTED
from explicit tokens rather than inferred, and the one proposed relation —
supersession from a shared number plus ordered versions — is structural and
costs a single pass. It does draw a line: do not extend this module to
semantic edge inference, and if anyone proposes it, the cost side of that
2.8x must be measured against our own corpus before any of it is built.
Treated as a premise, not a result. An external number is somebody else's measurement; it changes what is worth trying next, not what this repo has proved.
D-S8. The cost dial is an original contribution, not only a cost
The same report states that no published source gives per-query token counts
for structured versus flat versus full-verbatim context, and that no source
reports an indexed superseded-by facet at all. The 3.3x–6.4x dial measured
below is therefore a number the literature does not have, and the supersedes
facet is not a re-implementation of a known technique. That reframes the cost
finding: it is the tradeoff nobody has published, which is a reason to measure
it properly on a real corpus rather than a reason to hide it.
Verification
All commands are runnable and were run. Suite: 615 → 695.
.venv/bin/python -m pytest -q # 695 passed
.venv/bin/ruff check . && .venv/bin/ruff format --check .
.venv/bin/mypy --strict src/
Mutation testing, with proof the mutation was applied
Each run asserts the anchor was present and that exactly one # MUTANT marker
landed in the file before the suite was read; a run whose anchor was absent
reports "NO RESULT READ" rather than a green suite. The first attempt reverted
mutations with git checkout --, which discarded uncommitted work; the harness
now snapshots the file text instead.
| # | Mutation | Result |
|---|---|---|
| M1 | index entry drops its facet tail | 6 failed |
| M2 | unresolved always returns empty |
2 failed |
| M3 | title derivation not marked derived | 3 failed |
| M4 | resolver iterates unsorted | survived — equivalent |
| M5 | self-reference guard removed | 1 failed |
| M6 | unnamed facet key accepted | 1 failed |
| M7 | index append-only (never removes a managed line) | 3 failed |
| M8 | reprojection claims any managed line | 1 failed |
| M9 | derived block emitted unsorted | survived — equivalent |
| M9b | both sorts removed | 1 failed (after adding the ordering test) |
| M10 | unresolved marker dropped | 2 failed |
| M11 | facet validation skipped | 1 failed |
| M12 | derived title not threaded to the entry | 1 failed |
M4 and M9 are equivalent mutants, not test gaps: edges is sorted before it is
returned, and documents is populated from sorted(glob(...)), so the loop
order cannot reach the output. M9 nonetheless showed the ordering property was
pinned only as a side effect of a byte comparison, so a test that states it
directly was added; M9b confirms it kills the mutation that removes both sorts.
Cost, with the denominator stated
Measured on a synthetic 55-document corpus shaped like the one the bake-off described — numbered, hierarchical, cross-referencing, versioned. This is a measurement of this library's emission, not a re-run of anyone's bake-off, and it is synthetic precisely because a private consumer's documents do not come into this repo.
Facet coverage in the index, 55 documents, same counting query both sides:
| Facet | DEFAULT |
STRUCTURED_V1 |
|---|---|---|
| number / parent / status / date / version / references / derived | 0/55 | 55/55 |
| supersedes | 0/55 | 0/55 (corpus has no shared numbers) |
The supersedes zero is a property of that corpus, not of the code. Known
positive control, same query and same counter, on a corpus where documents do
share a number: 50/55 — 5 chains of 11 versions give 10 edges each, which
is the arithmetic the code should produce.
Index size for the same 55 documents, by facet key set — the dial a consumer sets:
| Facet keys | Index chars | vs flat |
|---|---|---|
none (DEFAULT) |
1 540 | 1.00x |
| status, supersedes, derived | 5 122 | 3.33x |
| number, status, supersedes, derived | 6 002 | 3.90x |
| number, parent, status, date, supersedes, derived | 7 817 | 5.08x |
all eight (STRUCTURED_V1) |
9 797 | 6.36x |
What was not achieved, and what would be needed
Reported as the order asked — what was tried, what happened, what would have to change — rather than as "not possible".
-
The cost of closing the gap is 3.3x–6.4x the flat index. The bake-off's OKF arm won on cost (6 031 characters against 21 879 for the head-scan it lost to on traps). Whether a faceted index keeps that advantage cannot be answered from here: it needs their bake-off re-run against
STRUCTURED_V1, on their corpus, with their questions. We own the emission measurement; the arm comparison is theirs. The facet key set is the dial, and a three-key set is the cheapest configuration that still carries status and supersession. -
A reference to a parent-level number does not resolve to a child. In the synthetic corpus,
N130in the body of one document does not resolve to a document numberedN130.2; it is reported unresolved (N130?). This is honest but incomplete — the fix is a prefix-resolution rule (subjectresolves to the unique document whose number starts with it), and it was left out because "unique" has to be defined against a real corpus before it can be defended. Deferred, not forgotten. -
OKF_V0_2'sverified/status/stale_afterare still declared and never written. The order asked whether the alignment doc's "expressible" reading is still right. It is, and for the reason already recorded: their v0.2 values are block lists of mappings, which this library's value model cannot round-trip until D1's structured reader lands.STRUCTURED_V1'sstatusis a scalar facet and is a different key in a different profile; it is not that field arriving early. -
Door B's ownership stamp is still the literal
true(D-S6). Open, and deliberately not decided inside this order. -
A faceted index cannot be judged against a directory (
entries_match_directory) in the same call, becauseSTRUCTURED_V1does not set it. Not attempted; no consumer has asked. -
No profile is applied to Door C.Closed — see the Door C section below. The objection stated here is the one the answer is built on: Door C PROJECTS the sender's declared facts and derives nothing.
Door C: the index carries the sender's facts, and only theirs
Ordered by vegnormal-okf (20260826T224500Z-873805419) on a measurement, not a
preference. A pre-registered reasoning bake-off on 2026-08-27 ran the same
bundle through two index shapes:
| arm | index shape | hits |
|---|---|---|
| B | DEFAULT profile's index.md |
0 of 8 |
| — | faceted index over the same frontmatter | 25 of 29 |
Same bundle, same concept files, same model. The DEFAULT arm did not answer
wrongly; it abstained, because the fact it needed was never in the file it
was given. Measured on their index.md: 30 974 characters describing 269
requirements, with 0 occurrences of any of the eight facts (title, date,
status, supersedes, erstatter, req_number, seksjon, kravtype) that the concepts
in the same bundle carry.
FacetPolicy and STRUCTURED_V1 already did this. They did not reach Door C.
Decisions
D-C1. Door C PROJECTS, and never DERIVES
Door B derives structure from documents this repo's own operator dropped. Door C merges documents a third party wrote, verbatim, and the entire door is built on refusing to put anything of ours inside their bytes.
Running the deriver here would not have broken the verbatim guarantee where anyone would look for it. The concept file would still be byte-identical. It would have broken it in the index entry about the concept — where our inference is printed next to their name and reads as their claim. Forging an attribution is not a smaller failure than editing a file; it is a quieter one.
So every facet in a Door C entry is a value the sender wrote in their own
frontmatter, copied. Nothing is inferred from their body, their filename, or
their neighbours in the bundle. Where the sender carries derived, their
list travels unchanged — so a reader can still see which of the sender's facts
the sender inferred, a distinction we would erase by adding inferences of our
own beside them. That is the whole ownership stamp at this door: the concept is
verbatim, and so is the index's account of what the concept claims.
resolve_structure is deliberately not called. It computes supersession across
a whole document set, and at this door that set is somebody else's.
D-C2. The projection is key-agnostic, which is why it is not about numbering
The loop asks the policy which keys to carry and never what a key means. It reads a value only to check the policy can render it.
Operator directive 2026-08-27, verbatim: "alt vi lager rundt OKF publiseres på
Forgejo og skal virke for alt av innhold" — so a design that only works for
numbered norms is wrong even when it scores well on N100/N200/N500. Nothing in
_project_facets can key off a numbering scheme, and the suite says so with an
unnumbered Norwegian meeting note (status + date, no number, no parent, no
version) and with a document declaring none of the policy's keys, which renders
the bare link rather than a separator with nothing after it.
The same property pays a second time: a consumer whose concepts are named by
UUID gets title into the index by naming the key in their own facet
policy, with no change here. Door C's index label is the concept path, so for
those senders the title travels as a facet or not at all.
D-C3. An unrenderable value drops the FACET, never the CONCEPT — and is reported
The policy refuses a value carrying its own separator or joiner rather than escaping it. At Door B that refuses the document, and rightly: the value is one we derived and the operator can fix the source.
At Door C it must not. This door judges no shape and refuses no sender on form —
that is what the module docstring already commits to — so refusing a merge over
a semicolon in someone else's frontmatter is precisely the failure it names.
Dropping it silently is the other failure: the sender made a claim our index
does not show. So the facet is dropped, the concept merges verbatim, and the
drop is reported per concept and key in ImportResult.unrendered_facets — an
advisory over the merged set, like unverified_references, never a fifth
bucket.
D-C4. A faceted entry is refreshed, because it is the first one that can go stale
Found by measurement during the work, not predicted. A flat entry carries a
label and a target, both stable, so it can never disagree with the file it
points at, and link_in_index's early return on a present target is exactly
right. An entry carrying the concept's facts can disagree.
The path is reachable: the collision gate refuses an updated concept outright,
so the operator's only route to accept an update is to remove the merged file —
which the refusal message itself instructs — and import again. Measured after
doing so: the concept file said gjeldende while the index still said utkast.
An index that contradicts the bundle it indexes is worse than one that says
nothing, because the consumer reads the index and stops. A faceted entry for a
present target is now re-rendered in place, keyed on the policy's entry pattern
and the parsed target — never on a substring, so a curated line that merely
mentions the target survives verbatim, line ending included. With facets=None
none of this runs, so every unfaceted caller emits the bytes it always did.
This is the one addition beyond the order's three edits. It is here because it is a divergence this change introduced, and shipping a measured file/index contradiction would have been worse than the scope it costs.
Verification
Suite 695 → 709; ruff check, ruff format --check, mypy --strict clean.
import_bundle's profile is keyword-only and defaults to DEFAULT, so the
171 branch bases built through this door emit the bytes they always did — pinned
by a test that feeds the DEFAULT path the same frontmatter that would produce
a facet tail under a faceted profile.
Mutation testing, and the harness that lied first
11 mutations of the new code, plus a permanent negative control (a behaviour-preserving local rename that must SURVIVE).
The control earned its place on its first run. The harness invoked
pytest --timeout=120; pytest-timeout is not installed, so every run exited
non-zero and every mutant was reported killed — 11 of 11. The number was
entirely false: zero mutations had actually been evaluated. The control is what
exposed it, by being reported killed when it provably could not be. A clean
sweep is worthless until the harness has been shown able to report a survivor.
The harness now asserts the suite passes on unmutated source before a single "killed" is believed, and asserts per mutant that the anchor matched exactly once and that the mutation landed on disk.
Honest result: 9 of 11 killed. Both survivors are equivalent mutants, not gaps:
- M1 (
if not value→if value is None) —FacetPolicy.renderalready filters falsy values (profiles.py:423), so the guard in_project_facetsis redundant with the policy's own. The observable property is now pinned directly anyway: a key the sender declared empty produces no facet. - M8 (drop the
changedguard before writing) — writes byte-identical content.
M10 — reverting the profile threading in import_filename — survived the
first honest run, and that one was a gap: every profile shipped today reuses
DEFAULT.paths, so no existing test could tell a threaded profile from a
hard-coded DEFAULT. The property was held by coincidence. A profile owning its
own import_prefix now states it directly, and M10 is killed.
What was not achieved
titleis not inSTRUCTURED_V1's facet set. Widening it would move Door B's index bytes, so it stays a caller's key to name. For a sender whose concept paths are UUIDs this is the difference between a title in the index and none — flagged to vegnormal-okf rather than decided here.- Door C's index is an append plus a per-target refresh, not a projection of the whole bundle (contrast D-S4). A concept removed from the bundle by hand leaves its entry behind. Reprojection would mean enumerating the directory, which no profile has asked this door to do.
- Parent-prefix resolution is still deferred, unchanged and for the
unchanged reason: "unique prefix match" must be defined against a real
corpus.
vegnormal-okfhas that corpus; the count is theirs to take. - The before/after number is not ours to produce. Whether the faceted index is worth its cost on a real corpus is answered by re-running the same arm with the same scoring, on their data.