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:
parent
1f7d3502b8
commit
d35bcb21ec
2 changed files with 202 additions and 4 deletions
|
|
@ -256,7 +256,167 @@ change — rather than as "not possible".
|
||||||
(`entries_match_directory`) in the same call, because `STRUCTURED_V1` does
|
(`entries_match_directory`) in the same call, because `STRUCTURED_V1` does
|
||||||
not set it. Not attempted; no consumer has asked.
|
not set it. Not attempted; no consumer has asked.
|
||||||
|
|
||||||
6. **No profile is applied to Door C.** Import writes concepts verbatim and
|
6. ~~No profile is applied to Door C.~~ **Closed** — see the Door C section
|
||||||
derives nothing, unchanged. Deriving structure for content another sender
|
below. The objection stated here is the one the answer is built on: Door C
|
||||||
wrote would put our inference into their bytes; if that is wanted it needs a
|
PROJECTS the sender's declared facts and derives nothing.
|
||||||
decision about whose claim the frontmatter makes.
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
|
||||||
|
|
@ -319,3 +319,41 @@ def test_an_unfaceted_profile_keeps_the_early_return_it_always_had(tmp_path: Pat
|
||||||
link_in_index(bundle, "import-a.md", "a", profile=DEFAULT)
|
link_in_index(bundle, "import-a.md", "a", profile=DEFAULT)
|
||||||
|
|
||||||
assert (bundle / "index.md").read_text(encoding="utf-8") == "- [Hand Edited](import-a.md)\n"
|
assert (bundle / "index.md").read_text(encoding="utf-8") == "- [Hand Edited](import-a.md)\n"
|
||||||
|
|
||||||
|
|
||||||
|
# --- properties that were pinned only as a side effect ---------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_door_writes_into_the_profiles_namespace_not_defaults(tmp_path: Path) -> None:
|
||||||
|
# Every profile shipped today reuses `DEFAULT.paths`, so no existing test
|
||||||
|
# could tell a threaded profile from a hard-coded DEFAULT here — the
|
||||||
|
# mutation that reverts the threading survived the whole suite. That makes
|
||||||
|
# this a property held by coincidence rather than by test, which is the one
|
||||||
|
# kind of green that means nothing. A profile owning its own namespace says
|
||||||
|
# it directly.
|
||||||
|
profile = replace(
|
||||||
|
STRUCTURED_V1,
|
||||||
|
paths=replace(STRUCTURED_V1.paths, import_prefix="ekstern-"),
|
||||||
|
)
|
||||||
|
place(tmp_path / "source", "a.md", "---\ntype: dataset\nnumber: N500\n---\n\nBody.\n")
|
||||||
|
|
||||||
|
_, bundle = run_with(tmp_path, StubImportGate(), profile=profile)
|
||||||
|
|
||||||
|
assert (bundle / "ekstern-a.md").is_file()
|
||||||
|
assert not (bundle / "import-a.md").exists()
|
||||||
|
assert index_of(bundle, profile) == "- [a](ekstern-a.md) — number: N500\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_key_the_sender_declared_empty_produces_no_facet(tmp_path: Path) -> None:
|
||||||
|
# Senders leave keys in with nothing after the colon. An empty value must
|
||||||
|
# not become `status: ` in the index — a facet stating nothing is worse than
|
||||||
|
# an absent one, because it reads as a value the sender chose.
|
||||||
|
place(
|
||||||
|
tmp_path / "source",
|
||||||
|
"a.md",
|
||||||
|
"---\ntype: dataset\nnumber: N500\nstatus:\ndate:\n---\n\nBody.\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
_, bundle = run_with(tmp_path, StubImportGate(), profile=STRUCTURED_V1)
|
||||||
|
|
||||||
|
assert index_of(bundle, STRUCTURED_V1) == "- [a](import-a.md) — number: N500\n"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue