docs(plan): record the Door C facet decisions, the mutation result, and the harness that lied

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.
This commit is contained in:
Kjell Tore Guttormsen 2026-08-27 11:03:37 +02:00
commit d35bcb21ec
2 changed files with 202 additions and 4 deletions

View file

@ -256,7 +256,167 @@ change — rather than as "not possible".
(`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.** Import writes concepts verbatim and
derives nothing, unchanged. Deriving structure for content another sender
wrote would put our inference into their bytes; if that is wanted it needs a
decision about whose claim the frontmatter makes.
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.