Repository navigation
The attributes barrier stops at the merge: id-namespace membership, rewriting and reference resolution are inconsistent #125
Description
Splitting this out of #121, where it was found and then reverted.
The problem
attributes is a barrier for the merge (§4.3.1 item 4) and the run rule (§4.3.2 item 2): nothing beneath it is block content. It is not a barrier for §4.3.1 item 5. definedId still counts an id carried under attributes, so two anchor marks sharing an id inside attributes.semantic is a REJECT, while the merge treats that same subtree as inert data.
Schema-valid witness — blockAttributes.semantic is a jsonLdAnnotation with additionalProperties: true:
{"version":"0.1","blocks":[{"type":"paragraph","attributes":{"semantic":{"@type":"Q","children":[
{"type":"text","value":"a","marks":[{"type":"anchor","id":"x1"}]},
{"type":"text","value":"a","marks":[{"type":"anchor","id":"x1"}]}]}},
"children":[{"type":"text","value":"Q"}]}]}Before #121 this canonicalized, because the shape-keyed merge silently deduplicated the two nodes. Scoping the merge exposed the duplicate, so it now throws duplicate id "x1".
Why the obvious fix is wrong
Gating definedId on the barrier was implemented and reverted. It removes the id from the namespace but rewriteIds still relabels it, because rewriteIds has no barrier at all. Two distinct documents then canonicalize to identical bytes:
A: blocks:[{paragraph,id:"intro"}, {paragraph,attributes:{semantic:{cite:{type:"note",id:"intro"}}}}]
B: identical except that inner id is "b0"
computeDocumentId(A) == computeDocumentId(B) == sha256:42893c8af5cbe6d38df5a48e7ad11277b0ca9a86acf71c306b4248965fe55dee
Both canonicalize the inner object to {"id":"b0","type":"note"}. A signature over B verifies A. That is a silent document-ID collision in place of a loud false REJECT — strictly worse in a content-addressed format, which is why it was reverted rather than shipped.
What a correct fix has to cover
The barrier has to reach all of these together, or none:
definedId/_defined_id— namespace membership.rewriteIds(scripts/lib/canonicalize.ts) — has noblockedparameter; recurses withparentKeyonly. Reference-only, so no cross-implementation gate can see a divergence here.- The six
walkContentNodesconsumers inscripts/lib/reference-resolver.ts— none readsctx.blocked. Asemantic:refunderattributes.semantictargeting an id also underattributesreportsCDX-E-BLOCK-REFERENCE-DANGLING(WARNING) once the target stops being defined. rewriteContentAnchor's#b<digits>prohibition. Today{"attributes":{"semantic":{"n":{"type":"academic:proof","of":"#b0"}}}}throwsreference "#b0" uses the reserved canonical-name formwhile the pre-scan reports nothing — a schema-valid document canon rejects and the scan calls clean.- The oracle's
_walk_contentbarrier propagation, which has no witness: mutating it passes all 30 gates, because no id exists underattributesanywhere in the conformance corpus.
Scope question to settle first
Is an id under attributes in the shared namespace or not? §4.3.1 item 5 keys membership on where an id sits, and nothing can anchor into a closed attribute set — which argues it is not. But that is a normative reading, not an implementation detail, and item 5's enumeration does not mention attributes either way. Worth deciding before writing any of the five above.
Related
The barrier is one key wide, so the same false REJECT survives at any other open object space whose member name collides with BLOCK_CONTENT_KEYS. semantic.schema.json $defs.bibliographyEntry is CSL-JSON with additionalProperties: true, sits in a hashed semantic:bibliography block's entries, and has no barrier — a non-standard CSL member named content holding a split combining sequence is rejected on both main and the #121 branch.
Activity
gvonnessi commentedon Aug 30, 2026
Severity: this is a document-ID collision, not only a key rewrite
Measured on
mainate2740e5. Two documents differing ONLY in their CSL citation keys:Both canonicalize their keys to
b0/b1. A signature over B verifies A.§4.3.1 item 5's alpha-equivalence is deliberate for labels — two documents differing only in author-chosen block ids SHOULD share an id. It is being applied to third-party vocabulary data, where the keys are content: a
citationmark'srefsresolves against them, and item 5 explicitly exempts asemantic:bibliographyentry'sidfor exactly that reason. The same data one field over, underattributes.semantic, gets relabeled.Whether it fires turns on
typeversus@type— well-formed JSON-LD is accidentally immune, CSL-JSON and any JSON-LD 1.1 context aliasingtype→@typeare not. That is the shape test leaking, which is the opposite of what item 5 says membership is keyed on.This raises the issue's priority: the false REJECT it was filed for is loud and recoverable; this is silent and affects signature verification.
gvonnessi commentedon Sep 1, 2026
Partly closed by PR #133 (
8ce0b5d) — deliberately left openDONE — the id-namespace half. An
idunderattributesis preserved as authored rather than relabeled. The collision this issue's severity comment recorded is closed onmain: the two bibliographies now produce different document IDs.PRESERVED, not excluded, and the distinction is the fix. Excluding takes the id out of the namespace but leaves
rewriteIdsmapping it onto a generated name — the silent collision reverted from the #128 branch. §4.3.1 item 5 was amended to license it, since it had called the two type-keyed exceptions "the only namespace members whose ids are left as authored".Evidence: a 30,000-document differential fuzz against
main(coverage floor measured — 6,718 documents carrying a preserved id, 3,277 carrying both preserved and relabeled) found 0 new collision classes and 0 documentsmainrejects that HEAD accepts.STILL OPEN — the rest of the barrier. The research on this issue enumerated thirteen consumers. PR #133 changed the id-definition path and nothing else:
rewriteIds— unchanged; it never needed the barrier once ids are preserved, since a preserved id is not in the rename map. Verified: alinkhrefto such an id stays verbatim.walkContentNodesconsumers inreference-resolver.ts— unchanged. Asemantic:refunderattributeswith a dangling target still reportsCDX-E-BLOCK-REFERENCE-DANGLING; whether it should is the open question.checkAnchors/URI_RANGEover the deliberately barrier-transparentwalkBlocksinstructural-constraints.ts— unchanged.Note the reachability finding, which changes this issue's framing. The premise that nothing can reference material under
attributesis false as implemented: alinkmark'shrefresolves against an anchor id there, and offsets are bounds-checked against attributes-borne paragraphs. So the duplicate-id REJECT this issue opened with is not a false positive, and PR #133 deliberately preserved it. The code comment asserting "nothing anchors into it" stated a wish as a fact and has been corrected.Also opened by this work: #132 — the same collision class is still live through any extension data array not named
entries, and membership still turns ontypeversus@type. Both aredefinedIdkeying on element shape where item 5 says position, which is the root of this issue too.