74 lines
3.7 KiB
Markdown
74 lines
3.7 KiB
Markdown
# 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:
|
|
|
|
1. Each `DisplayItem` carries its `SourceSpan` — 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).
|
|
2. Dirty items' spans are coalesced into replacement regions.
|
|
3. Graphics state at a region boundary is reconstructed and **explicitly
|
|
re-established** inside the replacement (`q ... Q` wrapping), 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).
|
|
4. Newly created objects append after the last region.
|
|
5. 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`/`Q` imbalance — fall back to a full re-emit of that page and
|
|
record it as a fidelity event. **We never guess.**
|
|
6. 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 `Pages` node 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. |