feat(consume): parent reaches the reader -- excerpt field, body link, checker rule
K3-21 A. `okf consume` resolves a concept's `parent:` pointer -- a
`segment_id`, unique only inside one document's plan -- among the concepts
sharing its `source_file` (`consume.link_parents`, one pass, no file opened
again) and an excerpt carries `parent: { concept_id, title }`. Conditional
like `req_number`: a concept with no `parent` key moves no byte. A pointer
that lands nowhere is named `parent_unresolved: true`, never dropped.
The door writes ONE line into a heading-only body whose entry has a parent:
`Enclosing section: [<title>](/<bundle-relative path>)` (SPEC SS 5.1 lineage
through links, SS 6.1 the recommended absolute form and the kind in the
prose). Only such a body, so the segmented goldens' declared parents -- bodies
holding text -- are untouched. Appended AFTER structure derivation and
screened on its own (`_screened`, the `description` rule): read as body text
the link was derived into a second, unresolved `references` edge, measured on
the fixture. `segmentation.heading_only` is the one predicate the proposer and
the door share.
`okf check` gains its seventeenth rule, `parent_unfollowable`: a `parent`
that is not a concept_id and title, names its own excerpt, or names a concept
in neither `excerpts` nor `withheld` (together every considered concept).
Contract SS 8 point 6 added, the figure carries `parent`, and "additional
members are not read by the checker" now says the checker reads only the
members SS 8 names. The template tells the reader what `parent` is and that
SS 2.2 lets it read that one concept; `skill.CONDITIONAL_FIELDS` gains
`parent`. README and CLAUDE.md say what consume now reads.
Moved on purpose, each named: the SS 7.4 known-positive IS the contract
document, so `budget.known_positive` moves in every payload (13 238 / 12 893
/ 345 -> 14 455 / 14 083 / 372); `skills/okf-consume/` regenerated from the
segmented golden, whose plan declares s1 and s2 under s0 -- its example
payload now carries both parents; `test_bundle_identity` 16 -> 17 rules;
`test_shell_parent`'s byte test also accounts for the link line.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
a5cd7c5688
commit
4f7bd61500
16 changed files with 319 additions and 58 deletions
|
|
@ -301,6 +301,61 @@ def rule_excerpt_named(ctx: Context) -> list[Finding]:
|
|||
]
|
||||
|
||||
|
||||
def rule_excerpt_parent(ctx: Context) -> list[Finding]:
|
||||
"""SS 8.6: an excerpt's `parent`, when it carries one, is a pointer a
|
||||
reader can follow.
|
||||
|
||||
Added 2026-09-11 (K3-21). Round 20 wrote `parent:` onto 675 of 710
|
||||
heading-only sections of one standard as a `segment_id`, a value a reader
|
||||
can open nothing with. The pre-pass now resolves it, and this rule holds
|
||||
the resolved form: a `concept_id` and a `title`, naming a concept other
|
||||
than the excerpt itself.
|
||||
|
||||
**The payload is its own denominator.** `excerpts` and `withheld` together
|
||||
name every concept the pre-pass considered, which is every concept of the
|
||||
bundle (SS 5.2), so a `parent.concept_id` in neither names nothing in the
|
||||
bundle -- and the rule sees that without opening the bundle, the boundary
|
||||
`rule_bundle_identity` keeps too. A payload lying about both lists at once
|
||||
passes here and fails `denominator_identity` instead.
|
||||
|
||||
**Conditional, like SS 8.4's fields.** An excerpt with no `parent` meets
|
||||
this rule as it always did. `parent_unresolved` is not a finding: SPEC SS
|
||||
6.1, "Consumers MUST tolerate broken links", and a pointer named as
|
||||
unresolved is the honest form of one.
|
||||
"""
|
||||
if not ctx.payload_is_mapping:
|
||||
return []
|
||||
excerpts = [_mapping(raw) for raw in _sequence(ctx.payload.get("excerpts"))]
|
||||
considered = {_text(excerpt.get("concept_id")) for excerpt in excerpts} | {
|
||||
_text(_mapping(raw).get("concept_id")) for raw in _sequence(ctx.payload.get("withheld"))
|
||||
}
|
||||
considered.discard("")
|
||||
findings = []
|
||||
for position, excerpt in enumerate(excerpts):
|
||||
if "parent" not in excerpt:
|
||||
continue
|
||||
parent = excerpt.get("parent")
|
||||
target = _text(_mapping(parent).get("concept_id"))
|
||||
if not isinstance(parent, Mapping) or not target or not _text(parent.get("title")):
|
||||
reason = "is not a `concept_id` and a `title`, so a reader can neither open nor cite it"
|
||||
elif target == _text(excerpt.get("concept_id")):
|
||||
reason = f"names the excerpt itself ({target!r})"
|
||||
elif target not in considered:
|
||||
reason = (
|
||||
f"names {target!r}, which is in neither `excerpts` nor `withheld` and so is "
|
||||
"no concept of this bundle"
|
||||
)
|
||||
else:
|
||||
continue
|
||||
findings.append(
|
||||
Finding(
|
||||
"parent_unfollowable",
|
||||
f"excerpt {position}'s `parent` {reason} (SS 8.6)",
|
||||
)
|
||||
)
|
||||
return findings
|
||||
|
||||
|
||||
def rule_excerpt_states(ctx: Context) -> list[Finding]:
|
||||
if not ctx.payload_is_mapping:
|
||||
return []
|
||||
|
|
@ -523,6 +578,7 @@ RULES: tuple[Callable[[Context], list[Finding]], ...] = (
|
|||
rule_bundle_identity,
|
||||
rule_excerpt_source_marking,
|
||||
rule_excerpt_named,
|
||||
rule_excerpt_parent,
|
||||
rule_excerpt_states,
|
||||
rule_denominator_identity,
|
||||
rule_denominator_lists,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue