Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Precise layout example (`examples/presentation-document/presentation/layouts/letter.json`)

#### Tooling
- Conformance suite `conformance/suite.json` `suiteVersion` 0.2.3: the four coverage gaps the B1b-3c-1a review panel proved were real but left open. No new defect codes — `conformance/errors.json` stays at **v0.14** — and one added case, which is where the patch position sits under the rule 0.2.2 recorded. `document_oracle.py` gains a `selftest()`, as `archive_oracle.py` already had: an arm of `confirm_clean` runs only on a CLEAN fixture, and a fixture carrying the defect that arm looks for is by definition not clean, so no corpus case ever reaches it with its defect present. Measured: deleting any one of the four arms B1b-3c-1a added — the root envelope, the stored-byte text scan, the anchor-position pass and the out-of-hash annotation position — left every gate green, and each deletion made `confirm_clean` return "clean" rather than being caught by a later arm. The same was true of every OTHER arm, so the selftest covers all of them: 52 drivers reaching all **37** refusal arms, each asserting the SPECIFIC refusal message, because `confirm_clean` returns on its first failing arm and a bare "refused it" assertion is satisfied by any later one. Where an arm returns a tagged set — the classifier's six block/mark tags, the four reference tags, the three position rows — each tag gets its own row. Thirty-six arms are driven from the malformed corpus; the thirty-seventh, the archive-unreadable guard, is driven from synthetic bytes, no document fixture being an unreadable zip. Crucially the table does not merely restore coverage but RATCHETS it: the arms are enumerated from the function's own AST and the drivers traced, so an arm added later with no row fails the selftest instead of being silent — without that, inserting a new arm was measured to leave every gate green, which is precisely how the four arms above came to be unmutatable. Selftest failures now accumulate rather than returning on the first, so one rotted fragment reports the others in the same run. Building the table surfaced a latent crash it also fixes: `confirm_clean` assumed a parsed manifest was an object and raised `AttributeError` on a JSON array, and since `check` does not wrap the call, a clean fixture with an array manifest would have aborted the whole oracle run with a traceback instead of reporting a refusal. `check:fixtures` gains a hash-coherence pass over every `{path, hash}` pair a document fixture's manifest DECLARES — content part, asset index, presentation layer and extension config slot alike, 143 hashes across 123 fixtures: each must agree with the bytes the recipe stores, and must DISAGREE exactly where the case expects the mismatch code for that reference's class. This cannot live in `check:conformance` — the fixture comparator is a subset check, so an unexpected hash-mismatch finding is tolerated outright, and on a case already expecting a WARNING or a REJECT it cannot move the document disposition either. Measured on `reject-content-key-non-nfc`: flipping one hex digit of its content hash leaves all 366 conformance checks green while the new gate names it. The claim is scoped deliberately — on the five cases expecting exactly [IGNORE, IGNORE] the extra WARNING DOES move the disposition and `check:conformance` catches it, so the hole closed is the WARNING and REJECT population, not literally every non-clean fixture. Scope beyond `content.hash` was not optional: corrupting one non-clean fixture's asset-index hash was measured invisible to `check:conformance`, `check:fixtures`, `check:document-oracle` and `check:document-id` together. The pass declines a digest this runtime cannot compute rather than throwing — blake3 is permitted by Container §3.2 and declared in `capabilities.json`, and an uncaught throw there aborted the gate and took both independent oracles with it — and it pins BOTH its skip sets by NAME — the fixtures carrying no comparable reference, and the individual references it declined — with a floor on what was compared, because a gate that reports a tick having compared nothing, or that folds an unverified hash into the count of those that agree, is the same silent pass it exists to remove. `manifest.security.accessControl` is carried deliberately with NO code: it is the one security-block reference declared as a `{path, hash}` rather than a bare path and nothing in the reader verifies its hash, so the pass still requires it to match its bytes but does not attribute a disagreement to a code no implementation emits — which would make such a fixture unauthorable. The boundary is stated with what it leaves out rather than only what it covers: an asset whose bytes diverge from the hash listed INSIDE an index file is pinned by the oracle's asset arms on clean fixtures only, so it remains unpinned on a non-clean one, which is this same hole one level down. One document fixture (157 fixtures, 366 checks), `reject-metadata-term-non-nfc-array-element` is the coverage one: §4.3.1 keeps exactly five Dublin Core terms and projects `creator`, `subject` and `language` as ARRAYS, a scalar being coerced to a one-element array, while the AUTHORED value may be either shape — and every other Dublin Core part in the corpus authors the scalar, so no case had the array shape reaching §4.3.2's scan. Verified against both implementations separately: dropping the array terms from the reference's loop fails it on the document DISPOSITION, and dropping them from the oracle's makes it unable to confirm the defect. Four `test-document-verdict` cases for what the corpus cannot express: the array shape at the verdict layer for each of the three array-valued terms — pinning only `creator` was measured to leave `subject` and `language` unscanned in BOTH implementations with the entire corpus green — and the negative control for the concatenation scan's run flush. `reject-content-split-combining-sequence-mid-array` pins that a run IS reported at a non-text element but not that the flush SEPARATES; the control is a PAIR carrying the same four text-node values inside a `blockquote`, differing only by a paragraph between the second and third, each run NFC-stable alone and only their concatenation not. Measured: deleting the reset leaves all 366 checks and both oracles green and fails only the new test

- Conformance defect vocabulary `conformance/errors.json` **v0.14**: eight new codes for content-part validity. `CDX-E-CONTENT-ROOT-MALFORMED` (REJECT), `CDX-E-PART-STRING-NOT-NFC` and `CDX-E-PART-STRING-ILL-FORMED` (REJECT, both citing §5.4.3 as their siblings do), `CDX-E-ANNOTATION-ANCHOR-POSITION-INVALID` (WARNING, state-invariant), `CDX-E-ANCHOR-POSITION-INVALID` with **`disposition: null`** — §7.2's severity table covers only three anchor conditions and a structurally invalid position is none of them, so the gap is recorded rather than filled by analogy, and joins OQ-007's register — and **three** out-of-range codes rather than one, because an out-of-range position is disposed of by the §5.4.2 row of the FIELD carrying it and those rows do not agree. `CDX-E-ANCHOR-POSITION-OUT-OF-RANGE` is scoped to a `link` href, taking the dangling-core-anchor row, whose cited Anchors and References §7.2 tabulates exactly this condition and escalates it when frozen; `CDX-E-BLOCK-REFERENCE-POSITION-OUT-OF-RANGE` takes the extension-block-field row and inherits `CDX-E-BLOCK-REFERENCE-DANGLING`'s recorded contest; `CDX-E-CROSS-REFERENCE-POSITION-OUT-OF-RANGE` takes the extension-cross-reference row, WARNING in every state. One code across all three would publish a single disposition for rows that diverge, and post-B3 an `academic:theorem-ref` whose target was MISSING would stay a WARNING while one naming a merely stale offset became an INTEGRITY-ERROR. The vocabulary is append-only, so the arms had to be separated before a disposition was published, not after; the §7.2-versus-§5.4.2 conflict the cross-reference code records is new **OQ-011**, settled by neither. The structural arm is deliberately NOT split — a null disposition contributes nothing to a verdict and so cannot diverge between rows. The two Unicode codes are scanned over the part's **hashed material** — the stored bytes minus the fields canonicalization deletes — matching the "a **hashed** object key or string value" both rows and both summaries bind. A violation inside a deleted field (`crdt`, `codeBlock.tokens`, `measurement.display`) is therefore NOT reported: it is not hashed, and the split-view harm cannot reach it, since two readers cannot derive different canonical bytes from a field both delete. `positive-content-derived-field-non-nfc` is the published control for that boundary — a reader scanning the whole stored part rejects a conformant document in every state, and `crdt` carries opaque third-party state holding arbitrary user text. A field the transform *rewrites* rather than deletes IS reported, an asset path being replaced before validation runs. The third arm of the same clause, `CDX-E-PART-NUMBER-NON-REPRESENTABLE`, stays raw-scoped and so does not match: it was published that way, and narrowing it would flip an expectation an implementation may already meet. Twenty-nine document fixtures (156 total), two new `anchor-range` structural vectors (209 published vectors), independent Python confirmers for each new code a fixture can assert (seven of the eight; `CDX-E-ANCHOR-POSITION-INVALID` has none, having nothing to confirm) plus the matching `confirm_clean` arms, and forty `test-document-verdict` cases for the properties a fixture cannot express — the comparator requires every expected finding to carry a disposition interval, so no fixture can assert a null-disposition code, and being a subset check it cannot assert that a defect reported under one row is NOT also reported under another. Two of those fixtures pin properties of the position check that a single-text-node target cannot express: a range whose bounds are DISTINCT integers above 2^53, ordered by comparing the digit strings rather than the doubles they round to (`contentAnchorUri` admits an unbounded `[0-9]+`, and §4.3.2's 2^53 limit governs hashed *numbers*, not digits inside a string — so a double comparison finds the bounds equal and silently downgrades a tabulated WARNING to the null-disposition structural code); and a target whose text spans two text nodes, which is what makes §3's concatenation model observable at all — `join('')` to `join(' ')` previously survived in the reference AND the oracle, a shared misreading no gate could see. The derived text-bearing vocabulary is now pinned by assertion rather than only by the throw-on-empty guard, which measurement showed was not enough on its own: dropping `figcaption` from the derived set leaves the entire corpus green — every conformance check and every oracle confirmation — because no fixture anchors into one and a wrong set only produces the null-disposition structural code, which is invisible in both of the comparator's dimensions. The pin is written against the SPECIFICATION rather than against the derivation's output, which is what caught the next defect: `codeBlock` was MISSING from the derived set, because its schema reaches `textNode` through an `allOf` that narrows it (§4: exactly one marks-free text node) rather than through a direct reference. §3 names `codeBlock` text-bearing outright, so an anchor into a code block — the ordinary reason to anchor at all — was reported as targeting a non-text-bearing block on conformant content, and an out-of-bounds one was downgraded from the tabulated WARNING to the null-disposition code. The pin asserts two ONE-SIDED relations rather than an equality, which is the form §3 actually supports: it names its five types with "such as", so the list is illustrative and the definition is the children test. An equality assertion would fail on a CORRECT widening, and the cheapest route back to green from that failure is to narrow the derivation again — re-creating the defect. The lower bound is that every type §3 names is derived; the upper bound is that no container block is, §3 being explicit that a container "has no text content of its own". `conformance/suite.json` `suiteVersion` 0.2.1 -> 0.2.2, and its `versioning.rules` gains the rule both patch releases to date — 0.2.1 and this one — have followed: while pre-1.0 the minor position carries the breaking bump, so added cases and newly reported defect classes land in patch, which means a pre-1.0 patch bump CAN add an expectation an implementation is held to. v0.13's entry below stated it in prose; what it had never been given was a rule in the normative artefact. `conformance/capabilities.json` 0.2.0 -> 0.2.1, its `document` summary having claimed block/mark structure, NFC and the hash recomputes all still rode with a later phase, two of which had been false since B1b-2 and B1b-3a
- Conformance defect vocabulary `conformance/errors.json` **v0.13**: two new codes for the footnote arm — `CDX-E-FOOTNOTE-REFERENCE-DANGLING` and `CDX-E-FOOTNOTE-REFERENCE-AMBIGUOUS`, both WARNING. The dangling code's disposition is transcribed from §5.4.2's extension-cross-reference row, which already named the footnote mark, and §4.5.2 restates it. The ambiguous one is weaker and the vocabulary says so: §5.4.2 assigns the ambiguity case no row at all, and §4.5.2 states the WARNING without a state qualifier and without citing §5.4 — so unlike its three siblings that entry does not claim state-invariance, and neither does this line. `CDX-E-CROSS-REFERENCE-DANGLING`'s "COVERAGE IS PARTIAL" note is rewritten: four of the row's five arms now have codes — citation, glossary, footnote, and the three `academic:*-ref` marks that code itself covers. The LEGAL arm remains uncovered, and the note now says so rather than passing over it: Legal Extension §5 states the disposition for a `legal:cite` whose `category` matches no Table of Authorities category, but it matches against a configuration rather than a content namespace, so it needs its own collector and its own answer on whether an absent configuration is indeterminate. Three document fixtures (127 total) and an independent Python confirmer that counts blocks per marker, since a flat id set cannot tell an ambiguous marker from a dangling one; `conformance/suite.json` `suiteVersion` 0.2.0 -> 0.2.1, the patch position being where added cases land now that the minor position carries the pre-1.0 breaking bump
- Conformance defect vocabulary `conformance/errors.json` **v0.12**: `CDX-E-ID-COLLISION`'s note re-synced — it asserted the shared namespace "is exactly the one Document Hashing §4.3.1 item 5 relabels", which the preserved-id erratum makes false; the namespace is now a superset of what item 5 relabels, and the note distinguishes a `semantic:footnote` BLOCK id (a member) from the footnote MARK id that resolves to it (a separate namespace). `CDX-E-GLOSSARY-REFERENCE-DANGLING`'s recorded "specification tension" is retracted as resolved. Six `canonicalize` vectors added and one flipped (207 published vectors), covering the preserved id, the matching glossary `ref`, index compaction, and the three new rejects. `conformance/suite.json` `suiteVersion` 0.1.0 -> 0.2.0: the flipped vector replaces a published `expectedCanonicalJcs` and `expectedId`, which the suite's own rule calls a changed expectation (the breaking bump sits in the minor position while the suite is pre-1.0)
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"description": "A non-NFC string (`cafe` followed by U+0301 COMBINING ACUTE) carried as an ELEMENT of the array-valued `creator` term, while every string-valued term is conformant. Document Hashing section 4.3.2 binds the whole hashed basis, and section 4.3.1 keeps exactly five Dublin Core terms in it — projecting `creator`, `subject` and `language` as ARRAYS (a scalar value is coerced to a one-element array). It is the AUTHORED value that may be either a string or an array of strings, per dublin-core.schema.json, and this part authors the array form. So a reader that scanned only the terms authored as bare strings admits a non-NFC byte into the document identity through three of the five. The part is otherwise conformant — `title` is present and `creator` holds a non-empty member — so it projects cleanly and this is the section 4.3.2 scan arm rather than the malformed-metadata or missing-term rows. It carries the corpus's only array-shaped projected term, so it is the only fixture in which that shape reaches the scan. REJECT in every state.",
"layer": "document",
"requires": [
"container",
"document"
],
"clause": "Document Hashing sections 4.3.1, 4.3.2; State Machine section 5.4.3",
"recipe": {
"entries": [
{
"name": "manifest.json",
"text": "{\"cdx\":\"0.1\",\"id\":\"pending\",\"state\":\"draft\",\"created\":\"2025-01-10T08:00:00Z\",\"modified\":\"2025-01-10T08:00:00Z\",\"content\":{\"path\":\"content/document.json\",\"hash\":\"sha256:5d69e5acc01e8b76df35531f9199eda7f594a972eec7d5718071842062fb39cc\"},\"metadata\":{\"dublinCore\":\"metadata/dublin-core.json\"}}"
},
{
"name": "content/document.json",
"text": "{\"version\":\"0.1\",\"blocks\":[]}"
},
{
"name": "metadata/dublin-core.json",
"text": "{\"version\":\"1.1\",\"terms\":{\"title\":\"T\",\"creator\":[\"A. Author\",\"cafe\\u0301\"]}}"
}
]
},
"expect": {
"documentDisposition": {
"atLeast": "REJECT",
"atMost": "REJECT"
},
"findings": [
{
"code": "CDX-E-PART-STRING-NOT-NFC",
"atLeast": "REJECT",
"atMost": "REJECT"
}
]
}
}
Loading
Loading