Skip to content

The attributes barrier stops at the merge: id-namespace membership, rewriting and reference resolution are inconsistent #125

Open

Description

@gvonnessi

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:

  1. definedId / _defined_id — namespace membership.
  2. rewriteIds (scripts/lib/canonicalize.ts) — has no blocked parameter; recurses with parentKey only. Reference-only, so no cross-implementation gate can see a divergence here.
  3. The six walkContentNodes consumers in scripts/lib/reference-resolver.ts — none reads ctx.blocked. A semantic:ref under attributes.semantic targeting an id also under attributes reports CDX-E-BLOCK-REFERENCE-DANGLING (WARNING) once the target stops being defined.
  4. rewriteContentAnchor's #b<digits> prohibition. Today {"attributes":{"semantic":{"n":{"type":"academic:proof","of":"#b0"}}}} throws reference "#b0" uses the reserved canonical-name form while the pre-scan reports nothing — a schema-valid document canon rejects and the scan calls clean.
  5. The oracle's _walk_content barrier propagation, which has no witness: mutating it passes all 30 gates, because no id exists under attributes anywhere 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

  1. gvonnessi commented on Aug 30, 2026

    @gvonnessi
    CollaboratorAuthor

    Severity: this is a document-ID collision, not only a key rewrite

    Measured on main at e2740e5. Two documents differing ONLY in their CSL citation keys:

    A: attributes.semantic.citation = [{id:"smith2020",type:"article-journal"},{id:"jones2019",type:"book"}]
    B: attributes.semantic.citation = [{id:"OTHER-KEY",type:"article-journal"},{id:"DIFFERENT",type:"book"}]
    
    computeDocumentId(A) == computeDocumentId(B)
      == sha256:869299f2c3d550ca698e09ad7db9ac0152e5a23dae32370742ae48aa8546ce5c
    

    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 citation mark's refs resolves against them, and item 5 explicitly exempts a semantic:bibliography entry's id for exactly that reason. The same data one field over, under attributes.semantic, gets relabeled.

    Whether it fires turns on type versus @type — well-formed JSON-LD is accidentally immune, CSL-JSON and any JSON-LD 1.1 context aliasing type→@type are 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.

  2. gvonnessi commented on Sep 1, 2026

    @gvonnessi
    CollaboratorAuthor

    Partly closed by PR #133 (8ce0b5d) — deliberately left open

    DONE — the id-namespace half. An id under attributes is preserved as authored rather than relabeled. The collision this issue's severity comment recorded is closed on main: 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 rewriteIds mapping 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 documents main rejects 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: a link href to such an id stays verbatim.
    • The six walkContentNodes consumers in reference-resolver.ts — unchanged. A semantic:ref under attributes with a dangling target still reports CDX-E-BLOCK-REFERENCE-DANGLING; whether it should is the open question.
    • checkAnchors / URI_RANGE over the deliberately barrier-transparent walkBlocks in structural-constraints.ts — unchanged.
    • The three unbarriered oracle walks — unchanged.

    Note the reachability finding, which changes this issue's framing. The premise that nothing can reference material under attributes is false as implemented: a link mark's href resolves 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 on type versus @type. Both are definedId keying on element shape where item 5 says position, which is the root of this issue too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions