fix(frontmatter): a nested key must not substitute for a top-level one
The line-oriented frontmatter grammar exists in three copies, each with the duplication documented at its site: `materialize` reads a path, `structure` needs a character offset, `profiles` returns body lines. All three keyed on `key.strip()`, which discards the indentation that is the only thing telling a nested key from a top-level one. An indented `title:` under a `sources:` block therefore landed in the same flat namespace as the document's own `title:` and, arriving later, won. The failure is substitution, not omission. A dropped value is visible to whoever reads the concept; a substituted one is not -- the document carries a title that looks entirely right and belongs to something else. Because `number` derives from `title` and `parent` derives from `number`, one substitution walks the hierarchy. Measured, not inferred: a document titled `N100.2` with a nested source titled `N200.7` came back as N200.7 with parent N200 instead of N100.2 with parent N100. Measured incidence across the two corpora, denominators stated: `_okf-canonical` @ ad30107, 54 documents with parsable frontmatter, 49 carry a nested key colliding with a top-level name (90.7%); `_okf-upstream` @ 9a15b13, 66 documents, 58 collide (87.9%). The colliding key is `title`, and often `resource` with it -- in `acme_retail/tables/orders.md` the concept's own BigQuery resource pointer was replaced by a nested one. This is a fix that clears observed damage, not a hardening without a witness. The fix refuses indented lines; it does not read them. Block form stays unreadable -- `sources` and `verified` still come back empty -- so D4's flow-form emission rule is untouched and the structured reader is still D1b. Two characterization tests that pinned the old behaviour now pin the new: the block-list family still DROPS its value, and only the key-space pollution is gone. That family is not otherwise addressed here. Test first, red before the code was touched, with known-positive controls for all three parsers so that a parser returning nothing could not pass. Order: 20260830T000740Z-4733930312-from-.claude Co-Authored-By: Claude <claude-opus-5>
This commit is contained in:
parent
087be0bc87
commit
2337a328d9
6 changed files with 239 additions and 20 deletions
|
|
@ -160,20 +160,26 @@ def test_the_contract_fields_round_trip_in_their_flow_form(tmp_path: Path) -> No
|
|||
assert parse_frontmatter(path) == values
|
||||
|
||||
|
||||
def test_two_nested_block_mappings_sharing_a_key_collide_in_the_scalar_parser(
|
||||
def test_two_nested_block_mappings_sharing_a_key_are_refused_not_flattened(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Measured 2026-07-31, and the reason D4 stops at the flow form.
|
||||
"""Measured 2026-07-31, re-measured 2026-08-31, and still the reason D4
|
||||
stops at the flow form.
|
||||
|
||||
§10.2 presents `executor` and `attester` as nested BLOCK mappings, and both
|
||||
carry a `resource`. The line-oriented parser has no indentation model, so it
|
||||
flattens them into one namespace where the second `resource` overwrites the
|
||||
first: `executor.resource` is lost and `attester.resource` surfaces as a
|
||||
top-level key. No error is raised.
|
||||
carry a `resource`. Neither is READABLE here -- both come back empty, which
|
||||
is exactly why D4 pins emission to the flow form and why reading this shape
|
||||
is D1b's work, not this parser's.
|
||||
|
||||
Pinned rather than fixed. Reading this form needs the structured reader
|
||||
(D1b), and a half-reader that silently drops half a contract is worse than
|
||||
one that never claimed to read it.
|
||||
What the parser no longer does is FLATTEN them. Before order
|
||||
`...4733930312` it had no indentation model at all: both `resource` lines
|
||||
landed in the document's own namespace, the second overwrote the first, and
|
||||
`attester.resource` surfaced as a top-level `resource` that no document
|
||||
declared -- silently, with no error. A dropped value is visible to whoever
|
||||
reads the concept; a substituted one is not.
|
||||
|
||||
So the contract is still half-read, and that is deliberate. It is now
|
||||
half-read by REFUSAL rather than by substitution.
|
||||
"""
|
||||
path = tmp_path / "block-form.md"
|
||||
path.write_text(
|
||||
|
|
@ -192,8 +198,13 @@ def test_two_nested_block_mappings_sharing_a_key_collide_in_the_scalar_parser(
|
|||
|
||||
assert parsed["executor"] == ""
|
||||
assert parsed["attester"] == ""
|
||||
assert parsed["resource"] == "attesters/revenue.py"
|
||||
# The substitution is gone: neither nested `resource` reaches the namespace.
|
||||
assert "resource" not in parsed
|
||||
assert "receipt" not in parsed
|
||||
assert set(parsed) == {"type", "runtime", "executor", "attester"}
|
||||
# Still unreadable, as D4 requires -- neither value survives anywhere.
|
||||
assert "skills/run-on-bq.md" not in parsed.values()
|
||||
assert "attesters/revenue.py" not in parsed.values()
|
||||
|
||||
|
||||
# --- door C surfaces the §10 pointers it imports ---------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue