3.7 KiB
ADR-0002 — Surgical re-emission with splice-point invariants
- Status: Accepted
- Date: 2025-07-25
- Plan reference: §4.4
- Design review: §14 step 9 — this ADR records the design review of the splice invariants before any production re-emission code lands.
Context
Naïvely regenerating a page's content stream on every edit loses everything we
didn't model: obscure operators, BDC marked-content nesting, transparency
groups, printer-specific DP properties, comments. A full-rebuild emitter
either has to model the entire PDF operator set losslessly (infeasible before
1.0) or silently drops what it doesn't understand (a fidelity regression per
§1.1). The decision shapes the L3 content model: every DisplayItem carries
its SourceSpan, and the emitter splices rather than rebuilds.
This is the M0 design review that §14 step 9 calls for, recorded as an ADR.
Decision
Re-emission is surgical. For a page whose edits touch regions B and C of
[A][B][C][D], the emitter copies A and D verbatim and writes a new B'C'
in place of [B][C]. Rules:
- Each
DisplayItemcarries itsSourceSpan— a(streamIndex, offset, length)triple over the logical concatenation of the page's content streams (a page's content may be an array of streams and operators can straddle the boundary; spans are over the concatenation but the emitted output preserves the array structure). - Dirty items' spans are coalesced into replacement regions.
- Graphics state at a region boundary is reconstructed and explicitly
re-established inside the replacement (
q ... Qwrapping), so the following verbatim bytes still see the state they expect. This is verified by an assertion pass that re-interprets the emitted stream and diffs the graphics state at every splice point against the original. Zero violations is a CI gate (§8.2). - Newly created objects append after the last region.
- If a page's structure is too tangled to splice safely — deeply interleaved
marked content across a dirty region, an unrecognised operator inside it,
or a
q/Qimbalance — fall back to a full re-emit of that page and record it as a fidelity event. We never guess. - Resources added by an edit are merged into the page's resource dictionary
with fresh, non-colliding names. A resource dictionary inherited from an
ancestor
Pagesnode is copied down to the page before mutation — never edited in place, it is shared.
Consequences
Positive. Untouched content is byte-identical to the input — the round-trip gate (§8.2) is realistically achievable. Unknown operators are preserved by construction rather than dropped. The splice-point assertion gives a mechanical, property-based check on the trickiest invariant.
Negative. The L3 interpreter must record SourceSpan on every item,
which is bookkeeping cost throughout. The fall-back full re-emit path must
also exist and be correct, which is roughly the work of a full emitter anyway
— but it only runs on tangled pages, so its fidelity budget is per-page
rather than corpus-wide.
Neutral. A page that needed a full re-emit is reported in the fidelity panel; the user is told, not lied to.
Alternatives considered
- Full rebuild always — rejected; cannot preserve unmodelled operators without modelling the whole operator set.
- Full rebuild with a "pass-through unknown" hack — rejected; the graphics state at the rebuild boundary is the failure mode, and a hack doesn't address it. Splicing with explicit state re-establishment does.
- Splice without the assertion pass — rejected; the splice-point invariant is the one thing that, if violated, silently corrupts complex pages. A property-based check is cheap insurance.