Skip to content

cnd: decorative content yields no footnote or citation - #8

Merged
Maxanox merged 5 commits into
mainfrom
cos-420-decorative-footnotes
Oct 10, 2026
Merged

Maxanox merged 5 commits into
mainfrom
cos-420-decorative-footnotes

Conversation

@Maxanox

@Maxanox Maxanox commented Oct 10, 2026 •

Copy link
Copy Markdown

Content inside pdf.artifact is decorative and never emitted. See the cnd-sdk spec §2, "Decorative content" (cosmyc-lab/cnd-sdk#3, proposal 0011).

Changes

  • Footnotes and citations: resolve_footnotes skips footnote declarations and markers located in an artifact, and resolve_cite_edges skips citations located in one.
  • References and links: a @ref or #link written inside an artifact records no marker. The bare-content collectors and the index-correlation fallback skip artifact subtrees through extract::traverse_kept, which behaves exactly like Content::traverse outside artifacts.
  • Labels on decorative content name nothing: resolve_label no longer falls back to the first table figure for them.
  • Floats: layout already marks the frame group of a float's body with the place as its FrameParent, and the ancestry walk now uses that.
    • The body is decorative exactly when its place was written inside an artifact, or inside the body of a decorative float.
    • Artifacts left open in the flow (a long block crossing a page) no longer reach into a kept float's body.
    • No source-position heuristics are involved. An earlier version of this PR used them, and review kept finding losses there.
  • Glyph rule: an element whose every glyph is drawn inside artifacts is decorative. This now applies to headings, lists, term lists and quotes, not only paragraphs, which covers a show rule that wraps a whole heading in pdf.artifact. Figures and tables are excluded, since an image carries no glyph.

Tests

  • decorative_content.typ covers:
    • every element kind;
    • decorative footnotes, a re-reference and a citation;
    • references, internal links and external links;
    • a decorative float;
    • a #let shown both kept and decorative.
  • decorative_float_helpers.typ covers:
    • decorative floats built by helpers, with kept text around them;
    • a #let list and table shown in a kept float and in a decorative block;
    • a nested float;
    • a heading wrapped by a show rule.
  • decorative_multipage.typ covers:
    • kept floats, including one built in context, drawn while a multi-page artifact is open;
    • metadata and state content written in a decorative float and shown in the flow.
  • cargo test --workspace, clippy -Dwarnings and fmt are clean.
  • The 40 corpus and 10 audit documents give the same output as main, ids aside.
  • A 5000-paragraph, 500-float document exports in 1.75 s, against 2.28 s on main.
  • Four adversarial review rounds; the last one found no loss of kept text.

Remaining leak (known)

A show rule that puts only part of an element in an artifact while drawing the rest outside still yields the element with its text. The typical case is a heading whose body is wrapped but whose number is not.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS

Maxanox and others added 5 commits October 10, 2026 14:00
Content inside pdf.artifact is decorative and never emitted (cnd-sdk
spec §2, "Decorative content"). Every element kind under an artifact
was already dropped, but footnotes and citations still leaked:

- a footnote declared in an artifact became a pool entry, and a kept
  re-reference to it got an edge;
- a footnote re-reference or a citation written in an artifact put an
  edge on the kept node around it.

resolve_footnotes skips declarations and markers located in an
artifact; resolve_cite_edges skips citations located in one.

decorative_content.typ wraps every element kind, labelled targets
referenced from kept text, a decorative footnote numbered before a
kept one, a decorative re-reference to the kept note and a decorative
citation. The suite test asserts no decorative word reaches the CND,
the kept note keeps ordinal 2 and is the only one pooled and linked,
and the citing paragraph cites only its kept key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closes the remaining gaps of the "Decorative content" rule (cnd-sdk
spec §2):

- A reference or link written inside an artifact records no marker
  (extract tracks open artifacts), and the bare-content collectors and
  the index-correlation fallback skip artifact subtrees
  (`extract::traverse_kept`, equivalent to `Content::traverse` outside
  artifacts). Skipping them also keeps a decorative paragraph from
  shifting the fallback's paragraph indices.
- A label on decorative content names nothing: `resolve_label` returns
  no node for it instead of falling back to the first table figure.
- A float written inside an artifact is drawn away from the artifact's
  tags. The float pre-pass now records whether its `place` sits in an
  artifact, and what such a float surely holds is decorative: a
  paragraph by source, any other element by body span and source range.
  Content reused from a `#let` stays kept where it shows.

decorative_content.typ gains decorative references, internal and
external links (in a paragraph and a list item), a decorative block
referring to a kept figure, a decorative float with a footnote and a
list, and a `#let` body shown both kept and in a decorative float.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
Review found kept text lost with the previous rule. A float built by a
helper (`#let f(x) = pdf.artifact(place(..)[#x])`) has its `place` in
the helper and its body at the call site, so its source range reaches
across the paragraphs in between, and content reused from a `#let`
shares its spans with the kept copy. Both made kept paragraphs, lists
and tables count as decorative.

A float written inside an artifact now counts as decorative only when
every span of its body lies inside its own `place` call, and an element
is decorative only when its own source range lies inside such a call
(a float nested in one included). Anything else is emitted: a helper-
built decorative float leaks, kept text is never at stake.

decorative_float_helpers.typ pins the review's cases: helper-built
decorative floats with kept text around them, a helper used both kept
and decorative, a `#let` list and table shown kept and in a decorative
float, and a float nested in a decorative one. On the 40 corpus and 10
audit documents the output is unchanged from main (ids aside).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
Two losses of kept text, found in review:

- Content written inside a decorative float's `place` call but shown in
  the flow (a `metadata` value queried, a `state` read in `context`)
  has its range inside the call. An element now counts as decorative
  through such a float only if every span in it — tag elements
  included — and every open ancestor's range lie inside the call; the
  `context` that shows it carries a span from outside.
- A kept float drawn at the top of a page while a multi-page artifact
  is still open in the frames was marked decorative. An artifact open
  on the stack now counts only when it encloses the element in the
  sense of `Open::encloses` (the float-aware test containers use), for
  the element itself and for the glyph count of open paragraphs. This
  loss predates the branch.

decorative_multipage.typ pins both. On the 40 corpus and 10 audit
documents the output is unchanged from main (ids aside); a 5000-
paragraph, 500-float document exports in 1.3 s (main: 1.8 s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
Review kept finding kept text lost or decorative text leaked by the
span- and range-based float rules: helper-built floats, `#let` content
shared between a float and the flow, `context` bodies with detached
spans, kept floats drawn while a multi-page artifact is still open.

Layout already marks where a float's body is drawn: the frame group
it pushes carries the `place` as its logical parent (`FrameParent`).
The ancestry walk now uses that:

- `FloatPlaces` finds every floating `place` and which are decorative:
  written inside an artifact opened in their own drawing scope, or
  inside the body of a decorative float.
- `DrawScope`: inside a float's body group, the body is decorative
  exactly when its `place` is; artifacts open on the stack from the
  page's flow no longer reach into it, only artifacts opened inside
  the body do.

This replaces the `decorative_range`/`spans_within`/`encloses`
heuristics of the previous commits with ancestry.rs back on main's
version plus these two pieces.

An element whose every glyph is drawn inside artifacts is decorative
for headings, lists, term lists and quotes too, not only paragraphs: a
show rule wrapping a heading in `pdf.artifact` leaves the heading's
own tags outside the artifact (this leak predates the branch). Figures
and tables are left out, since an image carries no glyphs.

Fixtures now assert the stronger behaviour: helper-built decorative
floats are dropped, a `#let` shown kept and decorative appears once,
a `context`-built kept float survives a multi-page artifact, and a
show-rule-wrapped heading is dropped. 40 corpus and 10 audit documents
are unchanged from main (ids aside); the 5000-paragraph, 500-float
document exports in 1.75 s (main 2.28 s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
@Maxanox
Maxanox merged commit b3678c5 into main Oct 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant