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
25
CLAUDE.md
25
CLAUDE.md
|
|
@ -625,8 +625,18 @@ and fixtures, never code.
|
||||||
The route reads the PLAN, never the row: on R761 it names the ancestor the
|
The route reads the PLAN, never the row: on R761 it names the ancestor the
|
||||||
`<sec>` nesting names on **708 of 710** shells (the 2 misses sit at depth 7,
|
`<sec>` nesting names on **708 of 710** shells (the 2 misses sit at depth 7,
|
||||||
clipped to 6), where reading section numbers gets 686 (`12` begins with
|
clipped to 6), where reading section numbers gets 686 (`12` begins with
|
||||||
`1`). 35 of 710 have no ancestor holding text and get none. Off because
|
`1`). 35 of 710 have no ancestor holding text and get none. **Since K3-21
|
||||||
`okf consume` reads no `parent` key. **The index renders every such parent
|
`okf consume` reads the key** (`consume.link_parents`, resolved among the
|
||||||
|
concepts of the concept's OWN `source_file`, because `p1` exists in every
|
||||||
|
document): an excerpt carries `parent: { concept_id, title }`, conditional
|
||||||
|
like `req_number`, or `parent_unresolved: true` where the pointer lands
|
||||||
|
nowhere, and a heading-only body gains ONE line `Enclosing section:
|
||||||
|
[title](/path)` (SPEC SS 5.1 lineage through links, SS 6.1 the recommended
|
||||||
|
absolute form), appended AFTER structure derivation -- read as body text it
|
||||||
|
became a second, unresolved `references` edge -- and screened on its own.
|
||||||
|
`okf check`'s seventeenth rule, `parent_unfollowable`, holds the form.
|
||||||
|
Whether the build default moves is K3-21 B's measurement, not this line's.
|
||||||
|
**The index renders every such parent
|
||||||
UNRESOLVED** (`parent: p1?`): `structure` reads `parent` as a document
|
UNRESOLVED** (`parent: p1?`): `structure` reads `parent` as a document
|
||||||
NUMBER, and a segment id answers to none -- the one key with two meanings
|
NUMBER, and a segment id answers to none -- the one key with two meanings
|
||||||
`inbox.py` already names for a declared parent. Report:
|
`inbox.py` already names for a declared parent. Report:
|
||||||
|
|
@ -745,7 +755,10 @@ and fixtures, never code.
|
||||||
allowlist, because a list names the producers its author thought of and one
|
allowlist, because a list names the producers its author thought of and one
|
||||||
bundle locates by `source_element_id` on 269 of 274 concepts. A prefix, never
|
bundle locates by `source_element_id` on 269 of 274 concepts. A prefix, never
|
||||||
a substring (`resource_owner` is not a locator). An absent key stays absent
|
a substring (`resource_owner` is not a locator). An absent key stays absent
|
||||||
and an undecodable address is named (`sources_unreadable`). `sources` is READ
|
and an undecodable address is named (`sources_unreadable`). Since K3-21 an
|
||||||
|
excerpt also carries `parent`, the enclosing concept's `concept_id` and
|
||||||
|
`title` -- never the raw `segment_id` -- or `parent_unresolved`, and the
|
||||||
|
contract's SS 8 point 6 says the checker reads it. `sources` is READ
|
||||||
in both YAML forms because the two real bundles disagree (flow 629/629 on one,
|
in both YAML forms because the two real bundles disagree (flow 629/629 on one,
|
||||||
block 270/270 on the other) — reading block is not a licence to write it, the
|
block 270/270 on the other) — reading block is not a licence to write it, the
|
||||||
emission rule is unchanged. Contract § 8 makes `title` a MUST (checker code
|
emission rule is unchanged. Contract § 8 makes `title` a MUST (checker code
|
||||||
|
|
@ -769,7 +782,8 @@ and fixtures, never code.
|
||||||
numbers a generic skill can only leave as holes or state falsely, and that
|
numbers a generic skill can only leave as holes or state falsely, and that
|
||||||
argument NEVER rested on conformance, so closing the measurement leaves it
|
argument NEVER rested on conformance, so closing the measurement leaves it
|
||||||
standing. **That half is CLOSED 2026-09-10 by `bundle_mismatch`, the checker's
|
standing. **That half is CLOSED 2026-09-10 by `bundle_mismatch`, the checker's
|
||||||
SIXTEENTH rule** (`RULES` is a tuple and `Report.rules_evaluated` is
|
SIXTEENTH rule** (seventeen since K3-21 added `parent_unfollowable`;
|
||||||
|
`RULES` is a tuple and `Report.rules_evaluated` is
|
||||||
`len(RULES)`, so every published «15 rules» line is now «16 rules» — a
|
`len(RULES)`, so every published «15 rules» line is now «16 rules» — a
|
||||||
contract change for anyone quoting it). It compares the identity a skill
|
contract change for anyone quoting it). It compares the identity a skill
|
||||||
DECLARES against the identity its payload declares, **both `bundle_id` AND
|
DECLARES against the identity its payload declares, **both `bundle_id` AND
|
||||||
|
|
@ -809,7 +823,8 @@ and fixtures, never code.
|
||||||
veiledningen om krav?"` are both required (the payload test asserts bytes for
|
veiledningen om krav?"` are both required (the payload test asserts bytes for
|
||||||
that question), and the checkout prefix is then stripped, because `okf skill`
|
that question), and the checkout prefix is then stripped, because `okf skill`
|
||||||
writes the bundle root and the skill path ABSOLUTE when `--out` is not under
|
writes the bundle root and the skill path ABSOLUTE when `--out` is not under
|
||||||
`.claude/skills/`. The pair is rc 0, 16 rules, 0 findings, and a test holds
|
`.claude/skills/`. The pair is rc 0, 17 rules (16 before K3-21), 0 findings,
|
||||||
|
and a test holds
|
||||||
the shipped bytes to the generator's. Its frontmatter `name` is now
|
the shipped bytes to the generator's. Its frontmatter `name` is now
|
||||||
`b-golden-segmented-okf-v0-2-consume`: Claude Code takes a project skill's
|
`b-golden-segmented-okf-v0-2-consume`: Claude Code takes a project skill's
|
||||||
COMMAND from the directory name and uses `name` only as a display label, and
|
COMMAND from the directory name and uses `name` only as a display label, and
|
||||||
|
|
|
||||||
14
README.md
14
README.md
|
|
@ -170,9 +170,12 @@ lets every nested section inherit them: measured on one process code, **710 of
|
||||||
2 761** concepts are heading-only, and the plan's level and order name the same
|
2 761** concepts are heading-only, and the plan's level and order name the same
|
||||||
ancestor as the document's own nesting on **708 of 710** (the two others sit at
|
ancestor as the document's own nesting on **708 of 710** (the two others sit at
|
||||||
depth 7, which a markdown heading clips to 6, and point one level too high). It
|
depth 7, which a markdown heading clips to 6, and point one level too high). It
|
||||||
is off because `okf consume` does not read `parent`, so no payload ranks
|
was off because `okf consume` did not read `parent`. Since K3-21 it does: an
|
||||||
differently with it, while it moves the bytes of every bundle holding a
|
excerpt carries `parent` as the enclosing concept's `concept_id` and `title`
|
||||||
heading-only section. One cost is known and not repaired: the index projects
|
(resolved inside the concept's own document, never the raw segment id), and a
|
||||||
|
heading-only body gains one line, `Enclosing section: [title](/path)`, in the
|
||||||
|
bundle-relative form SPEC § 6.1 recommends. Whether that moves the default is
|
||||||
|
measured separately. One cost is known and not repaired: the index projects
|
||||||
`parent` as a document number, so a segment id always renders there as
|
`parent` as a document number, so a segment id always renders there as
|
||||||
unresolved (`parent: p1?`), even though the concept it names is in the bundle.
|
unresolved (`parent: p1?`), even though the concept it names is in the bundle.
|
||||||
|
|
||||||
|
|
@ -518,7 +521,10 @@ locators its author thought of, and one real bundle locates by
|
||||||
`source_element_id` on 269 of its 274 concepts. A key the producer did not write
|
`source_element_id` on 269 of its 274 concepts. A key the producer did not write
|
||||||
stays absent rather than arriving empty, and an address this reader cannot
|
stays absent rather than arriving empty, and an address this reader cannot
|
||||||
decode is named (`sources_unreadable`) rather than dropped into the same
|
decode is named (`sources_unreadable`) rather than dropped into the same
|
||||||
silence. The reason is a measurement: with `concept_id` and body text alone, a
|
silence. A concept naming the section that encloses it delivers `parent` — that
|
||||||
|
concept's `concept_id` and `title` — or `parent_unresolved` when the pointer
|
||||||
|
lands nowhere; `okf check` (seventeen rules) refuses a `parent` a reader could
|
||||||
|
not follow. The reason is a measurement: with `concept_id` and body text alone, a
|
||||||
delivered gold concept at rank 1 still left the answer unable to name the
|
delivered gold concept at rank 1 still left the answer unable to name the
|
||||||
document it was quoting.
|
document it was quoting.
|
||||||
`considered == withheld + delivered` closes by construction, and the payload is
|
`considered == withheld + delivered` closes by construction, and the payload is
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@ carries them.
|
||||||
## 8. The payload shape
|
## 8. The payload shape
|
||||||
|
|
||||||
The pre-pass MUST emit one JSON object with these members. Additional members
|
The pre-pass MUST emit one JSON object with these members. Additional members
|
||||||
are permitted and are not read by the checker.
|
are permitted; the checker reads only the members this section names.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|
@ -170,7 +170,8 @@ are permitted and are not read by the checker.
|
||||||
"title": "<the concept's own title>",
|
"title": "<the concept's own title>",
|
||||||
"req_number": "<the identifier the producer wrote, when there is one>",
|
"req_number": "<the identifier the producer wrote, when there is one>",
|
||||||
"sources": [ { "resource": "<what the concept came from>" } ],
|
"sources": [ { "resource": "<what the concept came from>" } ],
|
||||||
"source_pages": "<a locator into that resource, when the producer wrote one>"
|
"source_pages": "<a locator into that resource, when the producer wrote one>",
|
||||||
|
"parent": { "concept_id": "<the concept enclosing this one>", "title": "<its title>" }
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"withheld": [ { "concept_id": "<bundle-local id>", "rule": "<why it was cut>" } ]
|
"withheld": [ { "concept_id": "<bundle-local id>", "rule": "<why it was cut>" } ]
|
||||||
|
|
@ -206,6 +207,20 @@ are permitted and are not read by the checker.
|
||||||
by that producer under the same rule — and any allowlist drops it, leaving an
|
by that producer under the same rule — and any allowlist drops it, leaving an
|
||||||
excerpt that names a document without naming the place in it. The prefix is a
|
excerpt that names a document without naming the place in it. The prefix is a
|
||||||
prefix and never a substring: `resource_owner` is not a locator.
|
prefix and never a substring: `resource_owner` is not a locator.
|
||||||
|
6. An excerpt SHOULD carry `parent` when its concept names the section that
|
||||||
|
encloses it, as the `concept_id` and `title` of the concept that pointer
|
||||||
|
resolves to in the same bundle — never the producer's own pointer, which a
|
||||||
|
reader cannot open without enumerating the bundle (§ 9.2). It is conditional
|
||||||
|
on the producer exactly as point 4's fields are, and § 6.4 forbids reading
|
||||||
|
its absence as a claim that the source has no enclosing section. A pointer
|
||||||
|
the pre-pass cannot resolve is named rather than dropped, as
|
||||||
|
`parent_unresolved` set to `true`, for point 4's reason, and is not a defect:
|
||||||
|
SPEC § 6.1, "Consumers MUST tolerate broken links". A `parent` names that one concept as reachable
|
||||||
|
in § 2.2's sense and names nothing else. The checker refuses
|
||||||
|
(`parent_unfollowable`) a `parent` that is not both members, that names its
|
||||||
|
own excerpt, or whose `concept_id` is in neither `excerpts` nor `withheld` —
|
||||||
|
together those name every concept the pre-pass considered (§ 5.2), so a
|
||||||
|
pointer outside them names nothing in the bundle.
|
||||||
|
|
||||||
## 9. Prohibitions
|
## 9. Prohibitions
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,18 @@ because the producer wrote none — never because the source has none, and never
|
||||||
something to fill in. An excerpt carrying `sources_unreadable` has an address
|
something to fill in. An excerpt carrying `sources_unreadable` has an address
|
||||||
this reader could not decode: say so rather than reporting no address.
|
this reader could not decode: say so rather than reporting no address.
|
||||||
|
|
||||||
|
**An excerpt carrying `parent` names the section that encloses it** — the
|
||||||
|
`concept_id` and `title` of another concept in this bundle. The payload names
|
||||||
|
that one concept as reachable (§ 2.2), so it is the one file outside the
|
||||||
|
delivered excerpts you may read: when an excerpt's `text` is its heading alone,
|
||||||
|
what that section inherits stands in the enclosing concept, whose file is its
|
||||||
|
`concept_id` plus `.md` under the bundle root. The text links it too, on a line
|
||||||
|
`Enclosing section: [title](/path)`, where `/` is the bundle root. Cite what you
|
||||||
|
take from it by that concept's own `(bundle_id, concept_id)`, never by the
|
||||||
|
excerpt that pointed to it. An excerpt carrying `parent_unresolved` names a
|
||||||
|
parent this reader could not find in the bundle: say so rather than reporting
|
||||||
|
that it has none.
|
||||||
|
|
||||||
| Marking | Use when |
|
| Marking | Use when |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `extracted` | the bundle states it directly |
|
| `extracted` | the bundle states it directly |
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,18 @@ because the producer wrote none — never because the source has none, and never
|
||||||
something to fill in. An excerpt carrying `sources_unreadable` has an address
|
something to fill in. An excerpt carrying `sources_unreadable` has an address
|
||||||
this reader could not decode: say so rather than reporting no address.
|
this reader could not decode: say so rather than reporting no address.
|
||||||
|
|
||||||
|
**An excerpt carrying `parent` names the section that encloses it** — the
|
||||||
|
`concept_id` and `title` of another concept in this bundle. The payload names
|
||||||
|
that one concept as reachable (§ 2.2), so it is the one file outside the
|
||||||
|
delivered excerpts you may read: when an excerpt's `text` is its heading alone,
|
||||||
|
what that section inherits stands in the enclosing concept, whose file is its
|
||||||
|
`concept_id` plus `.md` under the bundle root. The text links it too, on a line
|
||||||
|
`Enclosing section: [title](/path)`, where `/` is the bundle root. Cite what you
|
||||||
|
take from it by that concept's own `(bundle_id, concept_id)`, never by the
|
||||||
|
excerpt that pointed to it. An excerpt carrying `parent_unresolved` names a
|
||||||
|
parent this reader could not find in the bundle: say so rather than reporting
|
||||||
|
that it has none.
|
||||||
|
|
||||||
| Marking | Use when |
|
| Marking | Use when |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `extracted` | the bundle states it directly |
|
| `extracted` | the bundle states it directly |
|
||||||
|
|
@ -179,6 +191,7 @@ measurement about the producer, never a fact about the source.
|
||||||
| `verified` | **0 of 3** | no concept in this bundle carries it | that the source document lacks what the field asserts |
|
| `verified` | **0 of 3** | no concept in this bundle carries it | that the source document lacks what the field asserts |
|
||||||
| `req_number` | **0 of 3** | no concept in this bundle carries it | that the source document lacks what the field asserts |
|
| `req_number` | **0 of 3** | no concept in this bundle carries it | that the source document lacks what the field asserts |
|
||||||
| `sources` | **3 of 3** | the producer wrote none for that concept | that the source document lacks what the field asserts |
|
| `sources` | **3 of 3** | the producer wrote none for that concept | that the source document lacks what the field asserts |
|
||||||
|
| `parent` | **2 of 3** | the producer wrote none for that concept | that the source document lacks what the field asserts |
|
||||||
| `source_file` | **3 of 3** | the producer wrote none for that concept | that the source document lacks what the field asserts |
|
| `source_file` | **3 of 3** | the producer wrote none for that concept | that the source document lacks what the field asserts |
|
||||||
| `source_lines` | **3 of 3** | the producer wrote none for that concept | that the source document lacks what the field asserts |
|
| `source_lines` | **3 of 3** | the producer wrote none for that concept | that the source document lacks what the field asserts |
|
||||||
| `source_offset` | **3 of 3** | the producer wrote none for that concept | that the source document lacks what the field asserts |
|
| `source_offset` | **3 of 3** | the producer wrote none for that concept | that the source document lacks what the field asserts |
|
||||||
|
|
@ -195,7 +208,7 @@ carries its denominator.
|
||||||
| Limit | `120000` |
|
| Limit | `120000` |
|
||||||
| Unit | `utf-8 bytes of emitted JSON` |
|
| Unit | `utf-8 bytes of emitted JSON` |
|
||||||
| Instrument | `okf_consume.measure (len of the ensure_ascii=False JSON encoding, utf-8)` |
|
| Instrument | `okf_consume.measure (len of the ensure_ascii=False JSON encoding, utf-8)` |
|
||||||
| Known-positive | `docs/consumption-contract.md, encoded as a JSON string` at `13238` |
|
| Known-positive | `docs/consumption-contract.md, encoded as a JSON string` at `14455` |
|
||||||
|
|
||||||
The instrument reproduces the known-positive figure before any of its own
|
The instrument reproduces the known-positive figure before any of its own
|
||||||
numbers are believed. Report what the run actually spent.
|
numbers are believed. Report what the run actually spent.
|
||||||
|
|
@ -206,10 +219,10 @@ finding requiring a decision — not something to retry with a narrower question
|
||||||
|
|
||||||
**Scaling. Cost tracks the question, not the corpus.** Measured on this
|
**Scaling. Cost tracks the question, not the corpus.** Measured on this
|
||||||
bundle at generation time, with the question `Hva sier veiledningen om krav?`: the delivered set
|
bundle at generation time, with the question `Hva sier veiledningen om krav?`: the delivered set
|
||||||
was **3 excerpts** costing **2125 utf-8 bytes of emitted JSON**,
|
was **3 excerpts** costing **2289 utf-8 bytes of emitted JSON**,
|
||||||
against a whole bundle that would cost **2092** by the same instrument if
|
against a whole bundle that would cost **2256** by the same instrument if
|
||||||
one answer delivered all 3 concepts — so that answer was about
|
one answer delivered all 3 concepts — so that answer was about
|
||||||
**101.6 %** of the corpus. One question is one measurement: a
|
**101.5 %** of the corpus. One question is one measurement: a
|
||||||
different question moves `spent` and this figure with it.
|
different question moves `spent` and this figure with it.
|
||||||
|
|
||||||
**The breaking point, stated so it can be observed to have been passed.**
|
**The breaking point, stated so it can be observed to have been passed.**
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ Why each part is there:
|
||||||
checkout on one machine. The line strips that checkout's prefix and nothing
|
checkout on one machine. The line strips that checkout's prefix and nothing
|
||||||
else, and a test holds the shipped `SKILL.md` to the generator's output with
|
else, and a test holds the shipped `SKILL.md` to the generator's output with
|
||||||
exactly that prefix removed.
|
exactly that prefix removed.
|
||||||
- **`okf check`** should report `conformant: 16 rules over 3 excerpts and 0
|
- **`okf check`** should report `conformant: 17 rules over 3 excerpts and 0
|
||||||
withheld entries, 0 findings` and exit 0.
|
withheld entries, 0 findings` and exit 0.
|
||||||
|
|
||||||
The generated `name` is `b-golden-segmented-okf-v0-2-consume` while this
|
The generated `name` is `b-golden-segmented-okf-v0-2-consume` while this
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,13 @@
|
||||||
"unit": "utf-8 bytes of emitted JSON",
|
"unit": "utf-8 bytes of emitted JSON",
|
||||||
"instrument": "okf_consume.measure (len of the ensure_ascii=False JSON encoding, utf-8)",
|
"instrument": "okf_consume.measure (len of the ensure_ascii=False JSON encoding, utf-8)",
|
||||||
"limit": 120000,
|
"limit": 120000,
|
||||||
"spent": 2125,
|
"spent": 2289,
|
||||||
"known_positive": {
|
"known_positive": {
|
||||||
"case": "docs/consumption-contract.md, encoded as a JSON string",
|
"case": "docs/consumption-contract.md, encoded as a JSON string",
|
||||||
"expected": 13238,
|
"expected": 14455,
|
||||||
"measured": 13238,
|
"measured": 14455,
|
||||||
"raw_bytes": 12893,
|
"raw_bytes": 14083,
|
||||||
"encoding_delta": 345
|
"encoding_delta": 372
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"denominators": {
|
"denominators": {
|
||||||
|
|
@ -42,6 +42,10 @@
|
||||||
"source_sha256": "6906ec0acbcfc246e825bda9863c716eb5611b465020e8204eeb448c32343f7d",
|
"source_sha256": "6906ec0acbcfc246e825bda9863c716eb5611b465020e8204eeb448c32343f7d",
|
||||||
"source_lines": "[5, 8]",
|
"source_lines": "[5, 8]",
|
||||||
"source_offset": "[94, 176]",
|
"source_offset": "[94, 176]",
|
||||||
|
"parent": {
|
||||||
|
"concept_id": "veiledning",
|
||||||
|
"title": "Veiledning for eksempelbundel"
|
||||||
|
},
|
||||||
"text_sha256": "08447c690087834883a78fb9f40d474c68b2526ed57d0dafe01c4868db3c04f0",
|
"text_sha256": "08447c690087834883a78fb9f40d474c68b2526ed57d0dafe01c4868db3c04f0",
|
||||||
"text": "\n## 1.1 Foerste krav\n\nEt krav som beskriver den foerste betingelsen i eksempelet.",
|
"text": "\n## 1.1 Foerste krav\n\nEt krav som beskriver den foerste betingelsen i eksempelet.",
|
||||||
"rank": 1
|
"rank": 1
|
||||||
|
|
@ -64,6 +68,10 @@
|
||||||
"source_sha256": "6906ec0acbcfc246e825bda9863c716eb5611b465020e8204eeb448c32343f7d",
|
"source_sha256": "6906ec0acbcfc246e825bda9863c716eb5611b465020e8204eeb448c32343f7d",
|
||||||
"source_lines": "[9, 11]",
|
"source_lines": "[9, 11]",
|
||||||
"source_offset": "[176, 253]",
|
"source_offset": "[176, 253]",
|
||||||
|
"parent": {
|
||||||
|
"concept_id": "veiledning",
|
||||||
|
"title": "Veiledning for eksempelbundel"
|
||||||
|
},
|
||||||
"text_sha256": "4c181e1debb141d3cb1a7bd47249fdac1c0b2c5db1b47cf255ef8f04f83ec840",
|
"text_sha256": "4c181e1debb141d3cb1a7bd47249fdac1c0b2c5db1b47cf255ef8f04f83ec840",
|
||||||
"text": "\n## 1.2 Andre krav\n\nEt krav som beskriver den andre betingelsen i eksempelet.",
|
"text": "\n## 1.2 Andre krav\n\nEt krav som beskriver den andre betingelsen i eksempelet.",
|
||||||
"rank": 2
|
"rank": 2
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ import re
|
||||||
import sys
|
import sys
|
||||||
import unicodedata
|
import unicodedata
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Mapping, Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, replace
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
|
|
@ -287,6 +287,58 @@ class Concept:
|
||||||
locators: Mapping[str, str]
|
locators: Mapping[str, str]
|
||||||
frontmatter: Mapping[str, str]
|
frontmatter: Mapping[str, str]
|
||||||
body: str
|
body: str
|
||||||
|
#: The concept this one's `parent` pointer resolves to, as its `concept_id`
|
||||||
|
#: and `title`, or `None`. Set by :func:`link_parents` over the whole set
|
||||||
|
#: and never by :func:`read_concept`: whether a pointer lands is a fact
|
||||||
|
#: about the bundle, and one file does not know its bundle.
|
||||||
|
parent: Mapping[str, str] | None = None
|
||||||
|
#: `True` when the concept carries a `parent` key that resolves to no
|
||||||
|
#: concept of its own document -- the third state, as for `sources`.
|
||||||
|
parent_unresolved: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
#: The key a concept names its enclosing section under, and the key that
|
||||||
|
#: section answers to. Both are the segmented profiles' (`inbox.py` writes a
|
||||||
|
#: plan entry's `parent_id` as `parent:`, naming the other entry's `segment_id`).
|
||||||
|
PARENT_KEY = "parent"
|
||||||
|
SEGMENT_ID_KEY = "segment_id"
|
||||||
|
|
||||||
|
|
||||||
|
def link_parents(concepts: Sequence[Concept]) -> list[Concept]:
|
||||||
|
"""Every concept, with its `parent` pointer resolved or named unresolved.
|
||||||
|
|
||||||
|
A `parent` value is a `segment_id`, and a segment id is unique only inside
|
||||||
|
the plan of ONE document -- `p1` exists in every document of a
|
||||||
|
multi-document bundle -- so it is resolved among the concepts sharing this
|
||||||
|
concept's `source_file`, never across the bundle. An id two concepts of one
|
||||||
|
document claim resolves to neither, the rule `structure._lookup` applies: an
|
||||||
|
ambiguous pointer that silently picks a winner is worse than one reported
|
||||||
|
unresolved.
|
||||||
|
|
||||||
|
One pass over concepts already read; no file is opened again. A concept
|
||||||
|
carrying no `parent` comes back as it went in, which is what keeps every
|
||||||
|
payload of a bundle without the key byte-identical.
|
||||||
|
"""
|
||||||
|
claims: dict[tuple[str, str], list[Concept]] = {}
|
||||||
|
for concept in concepts:
|
||||||
|
segment_id = concept.frontmatter.get(SEGMENT_ID_KEY, "").strip()
|
||||||
|
if segment_id:
|
||||||
|
claims.setdefault((concept.source_file, segment_id), []).append(concept)
|
||||||
|
linked: list[Concept] = []
|
||||||
|
for concept in concepts:
|
||||||
|
pointer = concept.frontmatter.get(PARENT_KEY, "").strip()
|
||||||
|
if not pointer:
|
||||||
|
linked.append(concept)
|
||||||
|
continue
|
||||||
|
owners = claims.get((concept.source_file, pointer), [])
|
||||||
|
if len(owners) == 1 and owners[0] is not concept:
|
||||||
|
target = owners[0]
|
||||||
|
linked.append(
|
||||||
|
replace(concept, parent={"concept_id": target.concept_id, "title": target.title})
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
linked.append(replace(concept, parent_unresolved=True))
|
||||||
|
return linked
|
||||||
|
|
||||||
|
|
||||||
def read_concept(path: Path, *, bundle_root: Path, root_bundle_id: str) -> Concept:
|
def read_concept(path: Path, *, bundle_root: Path, root_bundle_id: str) -> Concept:
|
||||||
|
|
@ -617,14 +669,14 @@ KNOWN_POSITIVE_CASE = "docs/consumption-contract.md, encoded as a JSON string"
|
||||||
|
|
||||||
#: `measure()`'s own answer for that file. Vacuous ALONE -- which is why the
|
#: `measure()`'s own answer for that file. Vacuous ALONE -- which is why the
|
||||||
#: delta below exists.
|
#: delta below exists.
|
||||||
KNOWN_POSITIVE_EXPECTED = 13_238
|
KNOWN_POSITIVE_EXPECTED = 14_455
|
||||||
|
|
||||||
#: The second, independent route. `wc -c` reports 12 893 raw bytes for the same
|
#: The second, independent route. `wc -c` reports 14 083 raw bytes for the same
|
||||||
#: file; the difference is this file's JSON quoting and escaping overhead. A
|
#: file; the difference is this file's JSON quoting and escaping overhead. A
|
||||||
#: reader can derive it without running `measure()` at all, and it moves the
|
#: reader can derive it without running `measure()` at all, and it moves the
|
||||||
#: moment `measure()` changes what it counts -- which is what stops
|
#: moment `measure()` changes what it counts -- which is what stops
|
||||||
#: `expected == measured` from proving nothing.
|
#: `expected == measured` from proving nothing.
|
||||||
KNOWN_POSITIVE_ENCODING_DELTA = 345
|
KNOWN_POSITIVE_ENCODING_DELTA = 372
|
||||||
|
|
||||||
#: The two places that file can be, resolved in this order.
|
#: The two places that file can be, resolved in this order.
|
||||||
#:
|
#:
|
||||||
|
|
@ -1585,6 +1637,13 @@ def excerpt_for(concept: Concept) -> dict[str, object] | None:
|
||||||
1 on three bundles while the model could not name it: the ranking found the
|
1 on three bundles while the model could not name it: the ranking found the
|
||||||
document and the delivery dropped the key.
|
document and the delivery dropped the key.
|
||||||
|
|
||||||
|
Since K3-21, `parent` too, on the same terms: the concept that encloses
|
||||||
|
this one, as its `concept_id` and `title`, written only when the concept
|
||||||
|
names one and the pointer resolves (`link_parents`). Never the raw
|
||||||
|
`segment_id` the producer wrote -- a reader holding `p1977` can open
|
||||||
|
nothing -- and `parent_unresolved` where the pointer lands nowhere, the
|
||||||
|
`sources_unreadable` rule for the same reason.
|
||||||
|
|
||||||
Trailing whitespace is stripped per line: a spreadsheet render is padded to
|
Trailing whitespace is stripped per line: a spreadsheet render is padded to
|
||||||
hundreds of trailing spaces per line, and unstripped, most of a budget goes
|
hundreds of trailing spaces per line, and unstripped, most of a budget goes
|
||||||
on padding.
|
on padding.
|
||||||
|
|
@ -1613,6 +1672,10 @@ def excerpt_for(concept: Concept) -> dict[str, object] | None:
|
||||||
# HAS an address and this reader could not decode it.
|
# HAS an address and this reader could not decode it.
|
||||||
excerpt["sources_unreadable"] = True
|
excerpt["sources_unreadable"] = True
|
||||||
excerpt.update(concept.locators)
|
excerpt.update(concept.locators)
|
||||||
|
if concept.parent is not None:
|
||||||
|
excerpt["parent"] = dict(concept.parent)
|
||||||
|
elif concept.parent_unresolved:
|
||||||
|
excerpt["parent_unresolved"] = True
|
||||||
excerpt["text_sha256"] = hashlib.sha256(text.encode("utf-8")).hexdigest()
|
excerpt["text_sha256"] = hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||||
excerpt["text"] = text
|
excerpt["text"] = text
|
||||||
return excerpt
|
return excerpt
|
||||||
|
|
@ -1864,14 +1927,16 @@ def build_payload(
|
||||||
)
|
)
|
||||||
root_bundle_id = root_bundle_id_of(bundle_root, profile=profile)
|
root_bundle_id = root_bundle_id_of(bundle_root, profile=profile)
|
||||||
concept_ids = enumerate_concepts(bundle_root, profile=profile)
|
concept_ids = enumerate_concepts(bundle_root, profile=profile)
|
||||||
concepts = [
|
concepts = link_parents(
|
||||||
read_concept(
|
[
|
||||||
bundle_root / f"{concept_id}{profile.paths.concept_suffix}",
|
read_concept(
|
||||||
bundle_root=bundle_root,
|
bundle_root / f"{concept_id}{profile.paths.concept_suffix}",
|
||||||
root_bundle_id=root_bundle_id,
|
bundle_root=bundle_root,
|
||||||
)
|
root_bundle_id=root_bundle_id,
|
||||||
for concept_id in concept_ids
|
)
|
||||||
]
|
for concept_id in concept_ids
|
||||||
|
]
|
||||||
|
)
|
||||||
# The bundle's OWN vocabulary, and the reason the rule is a set rather than
|
# The bundle's OWN vocabulary, and the reason the rule is a set rather than
|
||||||
# a threshold: `pris` is a word here and `bila` is not, which is what
|
# a threshold: `pris` is a word here and `bila` is not, which is what
|
||||||
# separates a Norwegian compound from four coincidental characters. One
|
# separates a Norwegian compound from four coincidental characters. One
|
||||||
|
|
|
||||||
|
|
@ -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]:
|
def rule_excerpt_states(ctx: Context) -> list[Finding]:
|
||||||
if not ctx.payload_is_mapping:
|
if not ctx.payload_is_mapping:
|
||||||
return []
|
return []
|
||||||
|
|
@ -523,6 +578,7 @@ RULES: tuple[Callable[[Context], list[Finding]], ...] = (
|
||||||
rule_bundle_identity,
|
rule_bundle_identity,
|
||||||
rule_excerpt_source_marking,
|
rule_excerpt_source_marking,
|
||||||
rule_excerpt_named,
|
rule_excerpt_named,
|
||||||
|
rule_excerpt_parent,
|
||||||
rule_excerpt_states,
|
rule_excerpt_states,
|
||||||
rule_denominator_identity,
|
rule_denominator_identity,
|
||||||
rule_denominator_lists,
|
rule_denominator_lists,
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ from .segmentation import (
|
||||||
SegmentationPlan,
|
SegmentationPlan,
|
||||||
SegmentEntry,
|
SegmentEntry,
|
||||||
assert_plan_applies,
|
assert_plan_applies,
|
||||||
|
heading_only,
|
||||||
observed_extractor_version,
|
observed_extractor_version,
|
||||||
slice_segments,
|
slice_segments,
|
||||||
)
|
)
|
||||||
|
|
@ -101,6 +102,33 @@ def inbox_filename(slug: str, *, profile: BundleProfile = DEFAULT) -> str:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#: The words around the one link a heading-only body gains. SPEC SS 6.1: the
|
||||||
|
#: kind of relationship "is conveyed by the surrounding prose, not by the link
|
||||||
|
#: itself", so the line names the relation, in two words, because generic code
|
||||||
|
#: writes them into a body in the source's own language.
|
||||||
|
ENCLOSING_SECTION = "Enclosing section"
|
||||||
|
|
||||||
|
|
||||||
|
def _link_enclosing(body: str, parent: SegmentEntry, gate: Gate) -> str:
|
||||||
|
"""`body` plus one line linking the section that encloses it.
|
||||||
|
|
||||||
|
SPEC SS 5.1: "Lineage is expressed through links, not a dedicated field";
|
||||||
|
the `parent:` key stays beside it as a SS 4.1 extension. Bundle-relative
|
||||||
|
and absolute, SS 6.1's "recommended form", because the parent sits in
|
||||||
|
another directory and a relative link would count `..` across a layout the
|
||||||
|
next round may change.
|
||||||
|
|
||||||
|
The line carries the parent's title, which is document text, and joins a
|
||||||
|
body the gate has already judged -- so it is screened on its own, the rule
|
||||||
|
`_screened` applies to a `description`, and dropped rather than refused
|
||||||
|
when the gate would not persist it.
|
||||||
|
"""
|
||||||
|
line = _screened(gate, f"{ENCLOSING_SECTION}: [{parent.title}](/{parent.path})")
|
||||||
|
if line is None:
|
||||||
|
return body
|
||||||
|
return f"{body.rstrip(chr(10))}\n\n{line}\n"
|
||||||
|
|
||||||
|
|
||||||
def _normalize_body(text: str) -> str:
|
def _normalize_body(text: str) -> str:
|
||||||
# LF-only with exactly one trailing newline is a byte-level guarantee, and
|
# LF-only with exactly one trailing newline is a byte-level guarantee, and
|
||||||
# dropped files legitimately arrive with CRLF — normalising is the
|
# dropped files legitimately arrive with CRLF — normalising is the
|
||||||
|
|
@ -820,6 +848,16 @@ def _render_segments(
|
||||||
extractor_version=observed_extractor_version(extractor_id),
|
extractor_version=observed_extractor_version(extractor_id),
|
||||||
)
|
)
|
||||||
sliced = slice_segments(text, plan)
|
sliced = slice_segments(text, plan)
|
||||||
|
# A body that is its heading alone gains ONE line linking the section its
|
||||||
|
# `parent` names. Only such a body: one holding text already has something
|
||||||
|
# to read, and the segmented goldens' declared parents are bodies holding
|
||||||
|
# text. Decided on the SLICE, the window the proposer's own predicate read.
|
||||||
|
enclosing = {entry.segment_id: entry for entry in plan.entries}
|
||||||
|
linked = {
|
||||||
|
entry.segment_id
|
||||||
|
for entry, body in sliced
|
||||||
|
if entry.parent_id is not None and heading_only(body)
|
||||||
|
}
|
||||||
|
|
||||||
decisions = [(entry, gate(body)) for entry, body in sliced]
|
decisions = [(entry, gate(body)) for entry, body in sliced]
|
||||||
refused = [
|
refused = [
|
||||||
|
|
@ -837,11 +875,21 @@ def _render_segments(
|
||||||
if profile.index.facets is not None:
|
if profile.index.facets is not None:
|
||||||
structure = derive_document_structure(decision.sanitized_text, source_file=source_file)
|
structure = derive_document_structure(decision.sanitized_text, source_file=source_file)
|
||||||
_validate_facets(structure, profile)
|
_validate_facets(structure, profile)
|
||||||
|
body = decision.sanitized_text
|
||||||
|
if entry.segment_id in linked:
|
||||||
|
# AFTER the structure is derived, and that order is the rule: read
|
||||||
|
# as body text, the link is a bundle-local target, so derivation
|
||||||
|
# would restate the `parent` relation as a `references` edge -- one
|
||||||
|
# relation under two kinds, the second rendered unresolved because
|
||||||
|
# nothing resolves the absolute form. Measured on the fixture
|
||||||
|
# before this order was chosen.
|
||||||
|
assert entry.parent_id is not None
|
||||||
|
body = _link_enclosing(body, enclosing[entry.parent_id], gate)
|
||||||
outputs.append(
|
outputs.append(
|
||||||
(
|
(
|
||||||
entry.path,
|
entry.path,
|
||||||
render_inbox_concept(
|
render_inbox_concept(
|
||||||
decision.sanitized_text,
|
body,
|
||||||
okf_type=entry.okf_type,
|
okf_type=entry.okf_type,
|
||||||
# DECLARED by the adjudicator, never derived from the
|
# DECLARED by the adjudicator, never derived from the
|
||||||
# segment's own first line: the plan is the record of the
|
# segment's own first line: the plan is the record of the
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ from .errors import IngestError
|
||||||
from .extract import OutlineMark, extract_text, strip_converter_attribute, xml_outline
|
from .extract import OutlineMark, extract_text, strip_converter_attribute, xml_outline
|
||||||
from .extract import pdf_outline as extract_pdf_outline
|
from .extract import pdf_outline as extract_pdf_outline
|
||||||
from .materialize import reduce_to_id_grammar
|
from .materialize import reduce_to_id_grammar
|
||||||
from .segmentation import observed_extractor_version
|
from .segmentation import heading_only, observed_extractor_version
|
||||||
|
|
||||||
#: Stamped into every entry's `derived` list. The marker is what keeps a
|
#: Stamped into every entry's `derived` list. The marker is what keeps a
|
||||||
#: proposal from being mistaken for the judgement the run path replays.
|
#: proposal from being mistaken for the judgement the run path replays.
|
||||||
|
|
@ -1488,8 +1488,7 @@ def _link_shells(entries: list[dict[str, Any]], levels: Sequence[int], text: str
|
||||||
grew the excerpts past the budget.
|
grew the excerpts past the budget.
|
||||||
"""
|
"""
|
||||||
bodied = [
|
bodied = [
|
||||||
any(line.strip() and not line.startswith("#") for line in text[start:end].split("\n"))
|
not heading_only(text[start:end]) for start, end in (entry["span"] for entry in entries)
|
||||||
for start, end in (entry["span"] for entry in entries)
|
|
||||||
]
|
]
|
||||||
for index, entry in enumerate(entries):
|
for index, entry in enumerate(entries):
|
||||||
if bodied[index]:
|
if bodied[index]:
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,18 @@ from typing import Any
|
||||||
from .errors import SegmentationError
|
from .errors import SegmentationError
|
||||||
from .materialize import reduce_to_id_grammar
|
from .materialize import reduce_to_id_grammar
|
||||||
|
|
||||||
|
|
||||||
|
def heading_only(text: str) -> bool:
|
||||||
|
"""`True` when no line of `text` holds anything but a markdown heading.
|
||||||
|
|
||||||
|
ONE predicate for two readers that must agree: the proposer gives such a
|
||||||
|
span a parent (`propose._link_shells`), and the door writes a link into
|
||||||
|
exactly those bodies. Both read the same window on the same extracted text,
|
||||||
|
so a second copy of the rule could only ever disagree with the first.
|
||||||
|
"""
|
||||||
|
return not any(line.strip() and not line.startswith("#") for line in text.split("\n"))
|
||||||
|
|
||||||
|
|
||||||
#: The top-level keys a plan payload must carry. Every one is required: a plan
|
#: The top-level keys a plan payload must carry. Every one is required: a plan
|
||||||
#: missing its extractor identity would still parse, and would then be replayed
|
#: missing its extractor identity would still parse, and would then be replayed
|
||||||
#: against an extraction nobody checked it against.
|
#: against an extraction nobody checked it against.
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ passes the UNFILLED template against a real payload (exit 0, 15 rules, 0
|
||||||
findings), and passes a skill built for a different bundle against this one's
|
findings), and passes a skill built for a different bundle against this one's
|
||||||
payload. `contract_check.rule_bundle_identity` now compares the identity a
|
payload. `contract_check.rule_bundle_identity` now compares the identity a
|
||||||
skill declares with the identity its payload declares, so all three measured
|
skill declares with the identity its payload declares, so all three measured
|
||||||
pairs are refused at exit 1 with one `bundle_mismatch` finding over 16 rules:
|
pairs are refused at exit 1 with one `bundle_mismatch` finding over 16 rules
|
||||||
|
(17 since K3-21's `parent_unfollowable`):
|
||||||
a skill against another bundle's payload, the unfilled template against a real
|
a skill against another bundle's payload, the unfilled template against a real
|
||||||
payload, and -- the arm an id comparison would miss -- a payload sharing the
|
payload, and -- the arm an id comparison would miss -- a payload sharing the
|
||||||
skill's `bundle_id` at a foreign `ref`. The right pair is untouched at exit 0
|
skill's `bundle_id` at a foreign `ref`. The right pair is untouched at exit 0
|
||||||
|
|
@ -99,6 +100,7 @@ CONDITIONAL_FIELDS = (
|
||||||
"verified",
|
"verified",
|
||||||
"req_number",
|
"req_number",
|
||||||
"sources",
|
"sources",
|
||||||
|
"parent",
|
||||||
)
|
)
|
||||||
|
|
||||||
#: Tokens too short to carry a question. The same floor the pre-pass's own
|
#: Tokens too short to carry a question. The same floor the pre-pass's own
|
||||||
|
|
@ -337,14 +339,16 @@ def render(
|
||||||
bundle_id = okf_consume.root_bundle_id_of(bundle_root, profile=profile)
|
bundle_id = okf_consume.root_bundle_id_of(bundle_root, profile=profile)
|
||||||
ref = okf_consume.bundle_ref(bundle_root, profile=profile)
|
ref = okf_consume.bundle_ref(bundle_root, profile=profile)
|
||||||
concept_ids = okf_consume.enumerate_concepts(bundle_root, profile=profile)
|
concept_ids = okf_consume.enumerate_concepts(bundle_root, profile=profile)
|
||||||
concepts = [
|
concepts = okf_consume.link_parents(
|
||||||
okf_consume.read_concept(
|
[
|
||||||
bundle_root / f"{concept_id}{profile.paths.concept_suffix}",
|
okf_consume.read_concept(
|
||||||
bundle_root=bundle_root,
|
bundle_root / f"{concept_id}{profile.paths.concept_suffix}",
|
||||||
root_bundle_id=bundle_id,
|
bundle_root=bundle_root,
|
||||||
)
|
root_bundle_id=bundle_id,
|
||||||
for concept_id in concept_ids
|
)
|
||||||
]
|
for concept_id in concept_ids
|
||||||
|
]
|
||||||
|
)
|
||||||
if not concepts:
|
if not concepts:
|
||||||
raise SkillError(
|
raise SkillError(
|
||||||
f"{bundle_root} has an index but no concept under it; a skill for an "
|
f"{bundle_root} has an index but no concept under it; a skill for an "
|
||||||
|
|
|
||||||
|
|
@ -156,12 +156,13 @@ def test_a_payload_declaring_no_identity_is_left_to_its_own_rule(tmp_path: Path)
|
||||||
|
|
||||||
|
|
||||||
def test_the_rule_count_is_the_denominator_the_report_quotes() -> None:
|
def test_the_rule_count_is_the_denominator_the_report_quotes() -> None:
|
||||||
"""The nevner moves 15 -> 16, and every consumer quoting "15 rules" is
|
"""The nevner moved 15 -> 16 with this rule and 16 -> 17 with
|
||||||
|
`parent_unfollowable` (K3-21), and every consumer quoting the old number is
|
||||||
quoting a number that has changed."""
|
quoting a number that has changed."""
|
||||||
text, payload = instantiate(BUNDLE_A)
|
text, payload = instantiate(BUNDLE_A)
|
||||||
report = contract_check.check(text, payload)
|
report = contract_check.check(text, payload)
|
||||||
assert report.rules_evaluated == len(contract_check.RULES) == 16
|
assert report.rules_evaluated == len(contract_check.RULES) == 17
|
||||||
assert "16 rules" in report.render()
|
assert "17 rules" in report.render()
|
||||||
|
|
||||||
|
|
||||||
def test_the_generator_writes_an_identity_the_checker_can_read() -> None:
|
def test_the_generator_writes_an_identity_the_checker_can_read() -> None:
|
||||||
|
|
|
||||||
|
|
@ -170,9 +170,10 @@ def test_a_pointer_resolves_inside_its_own_document(tmp_path: Path) -> None:
|
||||||
documents = {str(e["concept_id"]).split("/")[0] for e in carried}
|
documents = {str(e["concept_id"]).split("/")[0] for e in carried}
|
||||||
assert documents == {"a", "b"}
|
assert documents == {"a", "b"}
|
||||||
for excerpt in carried:
|
for excerpt in carried:
|
||||||
assert str(excerpt["parent"]["concept_id"]).split("/")[0] == str(
|
assert (
|
||||||
excerpt["concept_id"]
|
str(excerpt["parent"]["concept_id"]).split("/")[0]
|
||||||
).split("/")[0]
|
== str(excerpt["concept_id"]).split("/")[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# --- The checker ---------------------------------------------------------------
|
# --- The checker ---------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -168,16 +168,22 @@ def test_a_build_without_the_flag_is_the_build_with_the_opt_out(tmp_path: Path)
|
||||||
assert files == sorted(p.relative_to(opted_out) for p in opted_out.rglob("*") if p.is_file())
|
assert files == sorted(p.relative_to(opted_out) for p in opted_out.rglob("*") if p.is_file())
|
||||||
for relative in files:
|
for relative in files:
|
||||||
assert (default / relative).read_bytes() == (opted_out / relative).read_bytes()
|
assert (default / relative).read_bytes() == (opted_out / relative).read_bytes()
|
||||||
# The flag adds one `parent:` line to each shell with an ancestor, and the
|
# The flag adds one `parent:` line to each shell with an ancestor, and --
|
||||||
# index -- a projection of the frontmatter -- shows the same key as a facet
|
# since K3-21 -- one link line to that shell's body
|
||||||
# on that shell's entry. Nothing else moves.
|
# (`tests/test_parent_reaches_reader.py`). The index, a projection of the
|
||||||
added = 0
|
# frontmatter, shows the same key as a facet on that shell's entry. Nothing
|
||||||
|
# else moves.
|
||||||
|
added = linked = 0
|
||||||
for relative in files:
|
for relative in files:
|
||||||
before = (default / relative).read_text(encoding="utf-8").split("\n")
|
before = (default / relative).read_text(encoding="utf-8").split("\n")
|
||||||
after = (flagged / relative).read_text(encoding="utf-8").split("\n")
|
text = (flagged / relative).read_text(encoding="utf-8")
|
||||||
if relative.name == "index.md":
|
if relative.name == "index.md":
|
||||||
|
after = text.split("\n")
|
||||||
assert [re.sub(r"parent: p\d+\?; ", "", line) for line in after] == before
|
assert [re.sub(r"parent: p\d+\?; ", "", line) for line in after] == before
|
||||||
continue
|
continue
|
||||||
|
text, links = re.subn(r"\n\nEnclosing section: \[[^\]]+\]\(/[^)\s]+\)\n\Z", "\n", text)
|
||||||
|
linked += links
|
||||||
|
after = text.split("\n")
|
||||||
assert [line for line in after if not line.startswith("parent: ")] == before
|
assert [line for line in after if not line.startswith("parent: ")] == before
|
||||||
added += sum(1 for line in after if line.startswith("parent: "))
|
added += sum(1 for line in after if line.startswith("parent: "))
|
||||||
assert added == sum(1 for ancestor in EXPECTED.values() if ancestor is not None)
|
assert added == linked == sum(1 for ancestor in EXPECTED.values() if ancestor is not None)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue