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.
422 lines
21 KiB
Markdown
422 lines
21 KiB
Markdown
# 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".
|
||
|
||
1. **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.
|
||
|
||
2. **A reference to a parent-level number does not resolve to a child.** In the
|
||
synthetic corpus, `N130` in the body of one document does not resolve to a
|
||
document numbered `N130.2`; it is reported unresolved (`N130?`). This is
|
||
honest but incomplete — the fix is a prefix-resolution rule
|
||
(`subject` resolves 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.
|
||
|
||
3. **`OKF_V0_2`'s `verified` / `status` / `stale_after` are 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`'s
|
||
`status` is a **scalar** facet and is a different key in a different profile;
|
||
it is not that field arriving early.
|
||
|
||
4. **Door B's ownership stamp is still the literal `true`** (D-S6). Open, and
|
||
deliberately not decided inside this order.
|
||
|
||
5. **A faceted index cannot be judged against a directory**
|
||
(`entries_match_directory`) in the same call, because `STRUCTURED_V1` does
|
||
not set it. Not attempted; no consumer has asked.
|
||
|
||
6. ~~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.render` already
|
||
filters falsy values (`profiles.py:423`), so the guard in `_project_facets` is
|
||
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 `changed` guard 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
|
||
|
||
1. **`title` is not in `STRUCTURED_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.
|
||
2. **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.
|
||
3. **Parent-prefix resolution is still deferred**, unchanged and for the
|
||
unchanged reason: "unique prefix match" must be defined against a real
|
||
corpus. `vegnormal-okf` has that corpus; the count is theirs to take.
|
||
4. **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.
|