# Pass 12 — Ratification Log ## Tranche 1 (2026-07-02): audit spec-alignment The 2026-07 six-agent spec-compliance audit sorted its findings into code-side MUST fixes (landed separately; see the crate `DECISIONS.md` files and `CONFORMANCE.md`) and places where the *spec text* trailed a disposition the project had already made — a documented crate decision, the ratified Operation Catalog, or the PHASE2_QUICKSTART model the implementation follows. This tranche ratifies that second set. **No byte-layout changes; architecture unchanged.** The main Pass 12 batch (`PASS12_BATCH.md`) stays open: the P12-H/K rows await G's ratification alongside Phase 2's open questions and are *not* resolved here. `adopt` = blessed the implementation/catalog behavior as normative spec text. `fixed` = the spec text was contradictory or stale and changed. | Item | Disposition | Spec locus | Authority followed | |---|---|---|---| | ModifyEvent metric placement | **adopt** — the catalog's §ModifyEvent reduction rule now states: a *metric* move (different `Musical` position/duration) is materialised, re-sorting the owning voice (id-tiebroken), behind a **placement precondition** read from the reducer's canonical voice-occupancy index — a non-positive span or an overlap with another live event refuses as a clean `EventDurationInvalid` no-op; a materialised move updates the occupancy index; non-metric moves and tuplet-member trimming stay deferred; same-placement field edits apply in place | operation_catalog §ModifyEvent | ops DECISIONS "ModifyEvent materializes metric placement changes (trim/move)"; `reduce.rs::modify_event` | | Slur/Spanner re-anchoring | **fixed (core-spec↔catalog contradiction)** — the rule-table rows now read: re-anchor to the nearest surviving endpoint/anchor while ≥ 1 survives, cascade-delete only when none does; a two-endpoint slur collapses onto its sole survivor (reference-clean degenerate form); proximity-aware re-targeting and per-spanner-kind bounds are deferred refinements (P11-C5 stand-in). Previously the Slur row said "nearest forward event in same voice; cascade if fewer than two members survive" | core_spec §The Re-Anchoring Rule Table | operation_catalog §DeleteEvent Re-anchoring; `reduce.rs` ledger + `materialize_graph_delete` (kept consistent per ops DECISIONS "the graph follows the ledger") | | RewriteTuplets payload | **adopt (id-only v1)** — core spec drops `TupletRewrite { tuplet, new_ratio, new_members }` for the catalog's `RewriteTuplets { tuplets: Vec }`, and states normatively that graph-aware reduction MUST refuse the variant as an ill-formed compensation rather than fabricate rewritten values; a future value-carrying payload revision reopens it (`ReplaceWithRest`/`CascadeDeleteTuplets` are the applicable compensations until then). Keeps v1 bytes stable; the functionality hole is documented instead of contradictory | core_spec §DeleteEvent (payload, preconditions, effect) | operation_catalog §DeleteEvent; `reduce.rs` refusal (`TupletCompensationInvalid`) | | Operation-block summaries | **adopt (bundle M4)** — an `OperationEnvelopeBlock` chunk payload is the envelope vector only (the spec'd `block_id` field was unrealizable: a content-addressed chunk cannot embed its own id); `OperationBlockSummary { dvv_summary, min_stamp, max_stamp }` lives in the **manifest**, keyed by the block's `ChunkId`, as opaque ops-supplied, non-canonical, optional metadata; the "writers SHOULD order blocks by min_stamp" line is dropped — the canonical manifest encoding sorts chunk references ascending by encoded form (Appendix D), so stamp-ordered scanning goes through the summaries | core_spec Ch8 §Operation Envelope Blocks + §Manifest | bundle DECISIONS "Operation-envelope block summary metadata is carried (M4 follow-up)"; `manifest.rs::OperationBlockSummary` | | Pre-pass output model | **fixed (stale Ch2/Ch3 text)** — the spelling and decomposition pre-passes produce **canonical derived annotations**: deterministic functions of (materialized graph, profile, versioned algorithm id), recomputed on materialization, never stored as graph state or serialized into canonical chunks — replacing "the pre-pass MUST produce / write `Inferred`-source attachments" and "stored as an attachment on the event". The pre-pass now MUST NOT mint attachments; promotion to a stored `UserChosen` attachment happens only via explicit editing operations. The incremental-re-run MUST is demoted to MAY (cache/incremental recompute permitted; neither observable in output; cache invalidates on derivation-key change). Ch1 design-principle retitled "notational rhythm is *derived*" | core_spec Ch2 §Spelling Source/Precedence/Pre-Pass + rationale; Ch3 §Notational Decomposition + §Decomposition Pre-Pass; Ch1 design principles | PHASE2_QUICKSTART "Canonical model: derived annotations, not stored objects"; `prepass.rs::derive_annotations` | | Spelling tie-break | **adopt** — conflicts break by precedence, then attachment `priority` (higher wins), then **canonical attachment order** (earliest in the score's canonical serialization wins); the "attachment creation timestamp" tie-break is deleted — attachments carry no timestamp, and one would hang resolution on non-canonical state | core_spec Ch2 §Configurable Precedence | `prepass.rs::resolve_spelling` | | Conflict-registry location | **fixed** — `Score.conflicts: ConflictRegistry` removed from the graph root; the registry is a component of canonical **materialized state** (which Ch6's requirement already said), i.e. a reduction product, not authored content; the hierarchy table, chapter overview, and §Conflict Records prose updated to match | core_spec Ch5 §Score root; Ch6 §Conflict Records / §The Conflict Registry | `reduce.rs::MaterializedState.conflicts` | | SolverTier::Stub | **adopt** — the spec enum gains the code's `Stub` variant: not a conformance tier, an interface-only/passthrough solver ordered below every conformant tier; declaring it makes no conformance claim and satisfies no minimum-tier requirement. Never canonically serialized, so no byte impact | core_spec Ch7 §Conformance Tiers | `layout-ir/solver.rs::SolverTier` | **Version movements.** Operation Catalog 0.2.0 → 0.3.0 (ModifyEvent reduction-rule behavior text). Core spec: revision-history row "Pass 12 tranche 1 (audit spec-alignment)" appended. ## Tranche 1 addendum (2026-07-02, Push-3 enablers) Two further ratifications landed the same day as spec-side *enablers* for the Push-3 wiring work — each defines normative text the implementation then built against (spec-first, per the batch discipline): | Item | Disposition | Spec locus | Consumer | |---|---|---|---| | `ResolveEquivocation` meta-operation | **fixed (dangling prose reference)** — the core spec named the operation only in prose (§Equivocation, resolution path 2) with no payload schema, no catalog entry, no K1 slot. Ratified: catalog §ResolveEquivocation pins `ResolveEquivocationPayload { target: OperationId, chosen: EnvelopeHash }`, order-independent earliest-resolve-governs promotion, `AlreadyApplied` idempotence, `StructuralFieldCollision` on `equivocation_resolution` for differing resolves, precondition no-ops, and the equivocated-resolve exclusion; core spec `OperationPayload` gains the variant. Catalog 0.3.0 → 0.4.0. The profile-policy path stays open (P12-K5) | operation_catalog §ResolveEquivocation; core_spec §Operation Envelope | `epiphany-ops` (payload discriminant 3 appended; set-level promotion pre-pass) | | Anchored break overrides | **fixed (representational gap)** — `OverrideKind::SystemBreak`/`PageBreak` were nullary, so a break's *position* was unrepresentable and the logical-stage projection required by §Engraving Overrides could not exist. Ratified: both kinds carry `anchor: TimeAnchor`; the `ScoreGraph` target names the owning region; projected break overrides carry `Internal` origin (authorship lives in the op log, P11-C8) | core_spec Ch7 §Engraving Overrides listing + note | `epiphany-layout-ir` (`to_logical` projection + paired `UserOverride` decisions) | **Deliberately not touched here** (still open in `PASS12_BATCH.md`): every P12-H/K row (algorithm-id ratification, decomposition precedence configurability, authored decompositions for ineligible events, system-pitch content modification, ResolveConflict beyond the concurrent case, RespellPitch v0 migration fallback, Transpose interval algebra), plus the audit's Push-3 wiring tracks (constraints, overrides, validation modes, edit barriers, operation index) — those are code work, not spec alignment. ## P12-I11 resolution (2026-07-03) — no spec change The batch's one measured-and-tracked conformance miss, **P12-I11** (RS-1 honestly failed the Minimal `casting_off_quality` threshold — greedy first-fit left a two-measure stub last system, width CV 0.6145 clamped to 1.0 > 0.90), is resolved **entirely on the implementation side**, so it appears here only to record that the Quality Metric Catalog and the core spec were **not** changed. | Item | Disposition | Spec locus | Consumer | |---|---|---|---| | P12-I11 casting-off stub last system | **no spec change (engrave fix)** — casting-off gained a second **widow-rebalance** phase (`epiphany-engrave` v3): it moves whole trailing measures from a region's penultimate system into its final one, choosing the shift that minimizes the larger of the two distribution penalties the catalog already defines for the break family (`casting_off_quality` width imbalance vs `system_break_penalty` non-final underfill; both share the 0.5 anchor). RS-1 casts six/four instead of eight/two — `casting_off` 1.0 → 0.4463, every axis ≤ 0.90 — so the suite's asserted Xfail row is promoted to a plain Pass. This is the deliberately-chosen *honest* resolution: the `casting_off` 0.5 anchor and the Minimal 0.90 column were **vindicated, not relaxed** (the engraver improved, no anchor rescale / threshold loosening / RS-1 per-entry override). Core spec Chapter 9's "Minimal makes no optimality claim" already permits the heuristic; nothing normative changed | — (no spec locus; core spec + Quality Metric Catalog unchanged) | `epiphany-engrave` (`casting::rebalance_widows`, `ENGRAVER_VERSION` 3); `epiphany-testkit` (RS-1 xfail row removed); render goldens regenerated | ## P12-I12 resolution (2026-07-03) — Quality Metric Catalog 0.1.0 → 0.2.0 Unlike P12-I11, the second measured finding's honest fix is a **catalog change**: the defect was in the metric's own definition, not the engraver. | Item | Disposition | Spec locus | Consumer | |---|---|---|---| | P12-I12 spacing floor on short scores | **fixed (metric measurement domain)** — `spacing_distortion`'s raw measurement is scoped to the system's **rhythmic columns** (spring slots bearing a notehead or rest); the clef / key-signature / time-signature lead and barlines contribute no column, so a note-to-note advance spans them. Rationale: the leading clef-to-first-note gap is furniture width, not note spacing, and folding it into the CV flagged perfectly-spaced short lines as distorted purely for carrying a clef. The three warning entries drop from 0.36–0.41 to 0.2188 / 0.0819 / 0.0856 (below the Standard 0.32 floor), and the axis stays live on real irregularity. The `1.0` anchor, orientation, range, tier thresholds, and the eight other axes are **unchanged**; this is measurement-only — no layout, canonical bytes, render golden, or `ENGRAVER_VERSION` change. Catalog **0.1.0 → 0.2.0** (a normative refinement of one axis's measurement domain). The duration-aware optical-spacing open question stays open | quality\_metric\_catalog §`spacing_distortion` (`req:qmc:spacing`); revision-history row 0.2.0 | `epiphany-engrave` (`quality::census` rhythmic-column filter, `is_rhythmic`); `epiphany-testkit` (RS-3/5/6 measured values drop, still Pass) | **Version movements.** Quality Metric Catalog 0.1.0 → 0.2.0 (`spacing_distortion` scoped to rhythmic columns). Core spec unchanged (the axis's formal definition is delegated to the catalog; Chapter 9 carries only the field, which is unchanged). ## Schema-major-1 tranche (2026-07-06) — data-model growth + engrave ratify-only The first binary-format schema-major bump (v0 → v1, `binary_format` 0.3.0), built machinery-first on a small payload, landed the two data-model rows the batch parked behind the frozen-layout rule (**P12-I7**, **P12-K7**) and ratifies the three engrave/layout-ir dispositions the implementation already made (**P12-I8/I9/I10**). Delivered as the schema-major-1 Phase A–F commit series (from `f4a2f1f`); this tranche records Phase F. | Item | Disposition | Spec locus | Authority followed | |---|---|---|---| | P12-I7 page-geometry graph home | **landed (schema-major-1)** — `Canvas` gained `layout_defaults: CanvasLayoutDefaults { page_size: CanvasSize, margins: CanvasMargins }` (staff spaces, A4/8 mm default), defined in core spec (Phase A) and added to the graph + canonical wire form at major 1 (Phase C, byte-for-byte v0→v1 migrate-on-read, zero golden churn). The engraver still defaults to the same geometry; wiring it to *read* the graph field (C′) is a byte-neutral follow-up deferred until a custom-geometry producer exists | core_spec Ch5 §The Canvas (`CanvasLayoutDefaults`); binary_format §Schema Major 1 | `epiphany-core` (`graph.rs`, `codec.rs::decode_v0_score`) | | P12-K7 advisory-precondition catalog | **landed (schema-major-1) + adopt** — the two graph-model gaps the advisory catalog named are filled: `Instrument.range: Option` and `Region.permits_spanning_slurs: bool` (major 1, Phases D1/D2), so `epiphany-ops` now enforces the InsertEvent pitch-in-range advisory (frame-guarded; "if any"/indeterminate → vacuous pass) and the CreateCrossCutting(Slur) region-spanning advisory. ModifyEvent's duration-boundary bucket is stated explicitly (it carries the full replacement value). Cross-region slur permission is read as **both** endpoint regions must permit (conservative AND) — see the open item below | core_spec Ch5 §Instruments / §Regions; Ch6 §6.10 advisory buckets; `PitchRange` | `epiphany-core` (`pitch.rs::PitchRange`, `graph.rs`); `epiphany-ops` (`validate.rs`) | | P12-I8 break-constraint satisfaction | **adopt** — Ch7 gains a normative predicate (Requirement `req:layoutir:break-satisfaction`): a `SystemBreakAt`/`PageBreakAt` at `slot` is satisfied iff the final `ResolvedLayoutIR` starts a system/page at that slot; a region-first slot is trivially satisfied; satisfaction is a predicate on the output, not the solver's spring state | core_spec Ch7 §ConstrainedLayoutIR (`req:layoutir:break-satisfaction`) | `epiphany-engrave` casting-off (evaluates hard break constraints for its tier claim) | | P12-I9 break-override attribution | **adopt (decline widening)** — Ch7 gains Requirement `req:layoutir:break-origin-attribution`: honouring a user break carries `DecisionSource::UserOverride(id)`, threaded through a `ConstrainedLayoutIR.break_origins` sidecar populated by the logical-stage projection. The normalized constraint record is deliberately **not** widened to carry override identity — attribution is a projection concern, not a solver input | core_spec Ch7 §Engraving Overrides (`req:layoutir:break-origin-attribution`) | `epiphany-layout-ir` (`constrained.rs::break_origins`, `to_constrained`) | | P12-I10 continuation synthesis | **adopt (bless registered id)** — Ch7 gains Requirement `req:layoutir:continuation-synthesis`: a system-spanning stroke's post-first segments are synthesized under `SynthesisKind::Registered(SYSTEM_CONTINUATION_SYNTHESIS)`, with `stable_semantic_instance_key = (original, ordinal)`; because `LayoutObjectId`s are non-canonical and re-derived per layout, the key need only be stable within a layout | core_spec Ch7 §Provenance (`req:layoutir:continuation-synthesis`) | `epiphany-layout-ir` (`provenance.rs`; `SYSTEM_CONTINUATION_SYNTHESIS`) | **Version movements.** Binary Format companion 0.2.0 → 0.3.0 (schema major 1 wire form + migration; Phase A). Core spec: `CanvasLayoutDefaults` / `PitchRange` / `Region.permits_spanning_slurs` data-model additions (Phase A) and the three engrave requirements above (Phase F); revision-history row appended. **Open item (flagged, not resolved).** Which region governs a *cross-region* slur's spanning permission is under-specified. The implementation chose the conservative **AND** (a boundary is permeable only when both endpoint regions set `permits_spanning_slurs`); a "the start region governs" or "either side" reading is equally defensible. Tracked for ratification; the advisory is authoring-only and never alters reduction, so the choice is not byte-affecting. ## G-pass tranche (2026-07-07) — the batch pass The full G-ratification of the accumulated batch: all 28 open rows retired in one deliberate pass (worklist: `PASS12_WORKLIST.md`). Four rows carried genuine forks and were decided by the project lead; three rows defer to *named* landing sites; the rest bless implemented, DECISIONS-recorded readings. Spec-first: the normative text landed with this tranche; the small code tranche (H7 surfacing, K3/K9 reasons, C4 variant) follows in its own commit. **Key decisions (project lead, 2026-07-07):** P12-K12 cross-region slur permission = **AND** (both endpoint regions); P12-H7 authored annotations for inference-ineligible events **surface** in derived annotations; P12-K4 ResolveConflict = **no supersede** (earliest applied resolve governs universally; re-resolution is a future dedicated op; no `TypedObjectId` Conflict kind); P12-K8 **genesis outside the operation set** (create-score/ canvas slots retired, not "unavailable"). | Item | Disposition | Spec locus | Authority followed | |---|---|---|---| | P12-H1 spelling algorithm id | **adopt** — `"default"` = Temperley-style line-of-fifths preference v1, ratified normative (`req:pitch:spelling-algorithm`); profile-declared disposition; no silent substitution. CONFORMANCE.md caveat dropped | core_spec Ch2 §Spelling Pre-Pass; Ch4 open question narrowed; Ch1 + App. D open-hooks lists updated | `epiphany-core/src/prepass.rs` | | P12-H2 key/clef model | **defer (premise stale, verified)** — the content model *exists* (I-0: `Clef`, `KeySignature`, content-bearing changes) and layout consumes it; what remains is algorithmic. Key-aware spelling → a spelling-algorithm **v2** (versioned rev, deterministically invalidates derived output); key-aware accidental display / cancelling naturals → the notation-refinement backlog (major-2 / Standard-tier neighborhood) | `req:pitch:spelling-algorithm` states v1 does not consult declared keys | verified: 0 `KeySignature` refs in `prepass.rs`; `PlacedKeySignature` in layout-ir | | P12-H3 chromatic-run convention | **adopt** — enharmonic choice absent tonal context is a property of the *versioned* algorithm; v1 = convention-as-tiebreak only; voice-leading refinement = future version, not a spec hole | `req:pitch:spelling-algorithm` | `prepass.rs` centre-of-gravity rule | | P12-H4 decomposition scope bounds (+P12-C5 folded) | **adopt** — the five v1 bounds (single governing meter; barline origin; dyadic compound grouping; no nested/cross-beat tuplets; `MAX_DOTS = 1`) become *declared normative bounds* of `DecompositionAlgorithmId "default"` v1 (`req:time:decomposition-algorithm`); a wider algorithm is a version bump. C5: `SetTimeSignature` reduction semantics already pinned (catalog §Meter and Tempo Overwrites); the derived-notation gap is subsumed by the single-meter bound | core_spec Ch3 §Decomposition Pre-Pass | `prepass.rs` integer-grid splitter; ops `reduce.rs` meter LWW | | P12-H5 aleatoric spelling | **adopt (open question closed)** — spelling is region-time-model-independent; no aleatoric-specific pass exists or is required; stated in the v1 algorithm definition | `req:pitch:spelling-algorithm` | `prepass.rs` (region-independent by construction) | | P12-H6 decomposition precedence | **decide: FIXED** — the fixed default order (`UserChosen > Imported > Propagated > Inferred`, canonical attachment order tie-break) is ratified; *not* configurable (a configurable order = new canonical `Score` field = schema-major with no consumer). "Same precedence machinery" reworded to "same source-rank discipline" | core_spec Ch3 §Notational Decomposition | `prepass.rs::resolve_decomposition` | | P12-H7 authored-uninferred surfacing | **decide: SURFACE (code follows)** — derived annotations MUST report the winning authored attachment for inference-ineligible targets, both pre-passes; taxonomy counts them distinctly (`req:pitch:authored-uninferred`). Derived-annotation-only: no canonical-byte impact | core_spec Ch2 (new requirement) + Ch3 cross-ref | decision reverses the implemented override-only mirror; code tranche implements | | P12-K1 RespellPitch migration | **adopt** — context-recovery + `Irreversible` → read-only is the *long-term* disposition; no richer v0 corpus required (none exists). Open-question box → ratified migration note | operation_catalog §RespellPitch | `epiphany-ops/src/migrate.rs` | | P12-K2 Transpose algebra | **defer (named site) + pin** — prototype semantics (CMN alteration shift, documented `i8` saturation) declared v1 behavior; faithful interval representation = *payload schema-major* landing with the Ch4 tuning catalog (Push 4) | operation_catalog §Transpose | `reduce.rs` transpose arm | | P12-K3 system-derived content rewrite | **decide: REFUSE (code follows)** — reduction MUST refuse intrinsic-content rewrites of `SYSTEM_DERIVED`-namespace pitches; appended `PreconditionFailureReason::SystemDerivedContentImmutable` (12). Core Ch5 states immutability; catalog pins the precondition for ModifyEvent + ModifyIdentifiedPitch | core_spec Ch5 §System-Derived Identifiers; operation_catalog §ModifyEvent; binary_format vocab (12) | protects Invariant 11; code tranche implements | | P12-K4 ResolveConflict beyond concurrent | **decide: NO SUPERSEDE** — earliest-applied-resolve governs universally; causally-later differing resolves + any resolve against `Dismissed` read `AlreadyApplied`; re-resolution = future `ReopenConflict`-class op; no `TypedObjectId` Conflict kind (meta-conflict names both resolvers) | operation_catalog §ResolveConflict | `reduce.rs` resolve arm (as implemented) | | P12-K5 equivocation selection policy | **defer (named site)** — v1 profiles declare *no* selection function (now stated); the hook's definition belongs to the Profile Conformance companion; no reducer policy hook until then | operation_catalog §ResolveEquivocation rationale | deliberate absence in `reduce.rs` | | P12-K6 equivocation edge semantics | **adopt** — single-pass promotion (no fixpoint); quarantined resolves never govern (verified in `reduce.rs` pre-pass comment + code); pending-by-causal-gaps resolves still govern (set-level); invalid-target/chosen no-op keeps `TargetMissing` (dedicated reason rejected — verdict does not change caller behavior) | operation_catalog §ResolveEquivocation (new Edge semantics block) | `reduce.rs` promotion pre-pass | | P12-K8 create score/canvas | **decide: GENESIS OUTSIDE OPS** — root + canvas are structural givens; genesis = empty-document constructor + bundle creation, normative; K1 slots *retired* (no kind will be assigned); revisit only under an addressable multi-canvas major | core_spec Ch5 §The Canvas; operation_catalog K1 chapter + Conformance Profiles | `Score::empty` + bundle creation path | | P12-K9 differing-value re-creates | **decide: DEDICATED REASON (code follows)** — appended `PreconditionFailureReason::RecreateContentMismatch` (13) replaces the misnaming `TargetMissing` reuse at every differing-value re-create site (CreateStaff, carried TimeSignature, container creates) | operation_catalog §CreateStaff + §Meter and Tempo Overwrites; binary_format vocab (13) | code tranche implements | | P12-K10 undo strand-blocks | **adopt (bless reuse)** — a StrictInverse strand-block *is* a transaction-scoped conflict; `TransactionConflict` reuse ratified; detail lives in the conflict record | operation_catalog §UndoTransaction | `undo.rs` | | P12-K11 undo idempotence asymmetry | **adopt** — restorations are ordinary chain writes (no distinguished undo provenance); second-undo conflict + idempotent absence-restores are normative; revisit only under undo-as-operation | operation_catalog §UndoTransaction | `undo.rs` write chains | | P12-K12 cross-region slur governance | **decide: AND** — boundary permeable only when both endpoint regions permit; advisory-only, never byte-affecting | core_spec Ch5 (after `Region` listing); operation_catalog §CreateCrossCutting | `validate.rs` conservative AND (as implemented) | | P12-C1 multi-source cue | **adopt** — cascade on *any* source deletion is normative; rationale prose fixed to match ("losing any source breaks quotation integrity"); truncate-while-any-survives recorded as rejected | core_spec §Re-Anchoring Rule Table (Cue row) | `reduce.rs` cue cascade | | P12-C2 Range truncate | **adopt** — truncate = dead event-anchored endpoint moves to its containing region's edge (start→Start, end→End, zero offset) | core_spec §Re-Anchoring Rule Table (graphic-gesture row) | `reduce.rs` re-anchor ledger | | P12-C3 annotation orphaning | **adopt** — orphaning is the sanctioned outcome for wall-clock/indeterminate spans inexpressible as stored `Range` anchors; an expressible form is future model work | core_spec §Re-Anchoring Rule Table (annotation row) | `reduce.rs` | | P12-C4 same-canvas reason | **decide: APPEND (code follows)** — `ReanchorReason::SameCanvasNearer` appended at discriminant **6** (5 was already owned by `DeclaredByExtension`); rank-4 survivors record it instead of `ExplicitFallback` | core_spec `ReanchorReason` listing + note; binary_format vocab (6) | code tranche implements | | P12-E4 barrier matching | **adopt** — target-free ops (`SetMetadata`, `DeclareTransaction`) match score-wide barriers only; opaque `Registered` ops match fully conservatively (`req:format:barrier-matching`) | core_spec Ch8 | `editor-core` barrier gate | | P12-E5 unsafe-edit tombstone | **adopt semantics + defer encoding (named site)** — `req:format:unsafe-tombstone`: immediate deactivation of the crossed extension's remaining barriers; durable record MUST land at next commit; `required = true` → read-only for dependents. The manifest-side *byte encoding* is a new binary_format open question (manifest frozen at major 0 → blob-layer or new chunk kind; next bundle-format tranche) | core_spec Ch8; binary_format §extension blobs (open question) | `editor-core::extensions_requiring_tombstone()` (producer exists) | | P12-I4 constraint strength | **adopt** — strength is *kind-determined*, no instance field (`req:solver:kind-strength`): breaks by `BreakKind` (Hard→Required, Soft→Preferred{1.0}), core families Required, `Registered` conservative Required; future families declare strength in their definitions. Deliberate Standard-tier design input | core_spec Ch9 §Strength Levels | `layout-ir` normalization | | P12-I5 sub-conformant report | **adopt** — `SolvedWithWarnings` + `satisfied_hard_constraints == false` + warning is the sanctioned constraints-present-but-unevaluated report; the one renderable status with unsatisfied-hard, because the field reports *evaluated* satisfaction (`req:solver:subconformant-report`) | core_spec Ch9 §SolveReport | `layout-ir` stub solver | | P12-I6 Minimal constraint floor | **adopt** — the implemented emission set (successive-notehead no-collision chains + per-glyph containment + user-break constraints) is the normative Minimal floor (`req:layoutir:constraint-floor`); higher-tier floors defined when those tiers land | core_spec Ch7 §ConstrainedLayoutIR | `constrained.rs` emission | **Ride-along staleness fix:** the Ch8 `OperationKindTag` listing gained the eleven appended tags the code has carried since M2/Phase-3 (append-only vocabulary; the listing had drifted). **Version movements.** Operation Catalog 0.5.0 → 0.6.0. Binary Format 0.3.0 → 0.4.0 (vocab appends 12/13/6 + the E5 encoding open question). Core spec: revision-history row "Pass 12 G-ratification (the batch pass)"; two open-question boxes replaced by ratified requirements (spelling, decomposition), one narrowed (Ch4 spelling-catalog), one added (binary_format E5 encoding). All three PDFs rebuilt clean, zero undefined references. **Code tranche (follows this commit):** H7 authored-only surfacing + taxonomy buckets (`prepass.rs`); K3 `SystemDerivedContentImmutable` (12); K9 `RecreateContentMismatch` (13); C4 `SameCanvasNearer` (6) — each with regression tests and wire goldens. ### G-pass addendum (2026-07-07): pre-pass error contract aligned A post-commit review caught a spec↔code disagreement introduced by the H1/H4 ratifications: the requirement text (MUST error on an unregistered algorithm id) had been adopted from CONFORMANCE.md's claim, but the implementation still returned a successful empty derivation under the requested profile. Resolution: the **spec text stands**; the code moved — `derive_annotations` now returns `Result` and rejects unregistered ids up front (`PrePassError`). No spec change; core DECISIONS records the reasoning. ## Schema-major-2 tranche (2026-07-08) — rendering (E1/E2), engrave ratify-only The schema-major-2 push's *rendering* consumers, ratified into core spec Chapter 7. The data model (Phases A–C) and the repeat-authoring op pair (Phase D) already landed with their own revision-history rows and companion-version bumps (Operation Catalog 0.7.0, Binary Format 0.6.0); this tranche records **Phase F**, which is layout-IR (Chapter 7) only. Delivered as the E1 (`7a9bf42` + `6651ae5`), E2 (`81b7f42` + `28f210e`) commit series. Layout geometry is non-canonical (no content hash, no wire form — see `req:layoutir:object-id-derivation`), so this is a ratify-as-implemented with **no byte-layout change and no companion bump**, mirroring the schema-major-1 Phase-F precedent. | Item | Disposition | Spec locus | Consumer | |---|---|---|---| | Non-glyph resolved primitives | **adopt (ratify pre-existing + E2 reality)** — Ch7 gains Requirement `req:layoutir:resolved-primitives`: `ResolvedLayoutIR` carries, beyond glyphs, non-glyph line `Stroke`s (staff lines, stems, barlines, volta brackets — present since staff lines, never previously ratified) and cubic-Bézier `Curve`s (slurs, E2). Each carries provenance (the hit-testing basis) and is re-spaced by the solver like a glyph; both are non-canonical. The struct listing and the `Stroke`/`Curve` shapes are added; the RenderIR provenance requirement widened from "originating `ResolvedGlyph`" to "…`ResolvedGlyph`, `Stroke`, or `Curve`" | core_spec Ch7 §ResolvedLayoutIR (`req:layoutir:resolved-primitives`) | `epiphany-layout-ir` (`constrained.rs::{Stroke,Curve}`, `resolved.rs`); `epiphany-render-svg` | | E1 repeat / volta rendering | **adopt** — Ch7 gains Requirement `req:layoutir:repeat-render`: a barline-drawing `RepeatStructure` renders a repeat barline at each resolved boundary (the precomposed sign, replacing a coinciding measure barline or standing alone; the dot pair beside a never-replaced final barline), each `Volta` a bracket with ending numbers; an unresolvable boundary draws no ink (traced anchor, honest placement); jump-kind (`DaCapo`/`DalSegno`) marks and cross-region repeats deferred. The glyph vocabulary/spacing stay engraving-algorithm concerns | core_spec Ch7 §ResolvedLayoutIR (`req:layoutir:repeat-render`); references Ch5 §Repeat Structures | `epiphany-layout-ir` (`logical.rs`, `constrained.rs`); `epiphany-engrave` (casting) | | E2 slur rendering | **adopt** — Ch7 gains Requirement `req:layoutir:slur-curve`: a `Slur` renders as a cubic Bézier between its endpoint columns, honoring `CurvatureOverride` (direction, height); an endpoint that does not resolve to a column on a single staff of one region draws no curve (traced anchor); a non-`Solid` `SpanStyle` line MAY be deferred but MUST be surfaced (a layout diagnostic), never silently rendered solid. The curvature algorithm and dash rendering stay forward-referenced out | core_spec Ch7 §ResolvedLayoutIR (`req:layoutir:slur-curve`); references Ch5 §Slurs and Phrase Marks | `epiphany-layout-ir` (`logical.rs`, `constrained.rs`, `hittest.rs`); `epiphany-editor-core` | **Version movements.** None on the wire: core spec Chapter 7 gains three `req:layoutir:*` requirements + the `Stroke`/`Curve` struct listings + a revision-history row (Phase F). Operation Catalog stays 0.7.0, Binary Format stays 0.6.0 — layout geometry is non-canonical. **Deferred (documented, not open candidates).** Dashed/dotted slur *rendering* (surfaced as a diagnostic today), kind-differentiated slur appearance (Phrase/Editorial), jump-kind repeat marks, cross-region and cross-system-break curve splitting (de Casteljau), and the `slur_shape` quality metric moving off `0.0`-by-construction — all Standard-tier / Push-3 work, named in the crate DECISIONS, none a Pass-13 ambiguity. ## Push-3 tranche (2026-07-08) — slur rendering completion The three slur refinements the schema-major-2 Phase-F ratification deferred, landed and ratified. Layout-IR (Chapter 7) only, non-canonical — a ratify-as-implemented with **no wire form and no companion version move** (mirrors the schema-major-2 rendering tranche). Delivered as commits `c5173d2` (dashed rendering), `5869691` (curve splitting), `7d61271` (slur_shape measured). | Item | Disposition | Spec locus | Consumer | |---|---|---|---| | Dashed/dotted slur rendering | **adopt** — `req:layoutir:slur-curve` extended: an authored non-`Solid` `SpanStyle` line renders faithfully (the `LineStyle` rides the `Curve`, whose Ch7 listing gains the field); an implementation that defers the pattern must surface it, never silently render solid. The E2 `SlurLineStyleNotRendered` diagnostic is retired (the style is rendered, not deferred) | core_spec Ch7 §ResolvedLayoutIR (`req:layoutir:slur-curve`, `Curve` listing) | `epiphany-layout-ir` (`Curve.line`); `epiphany-render-svg` (`stroke-dasharray`) | | Curve splitting across systems | **adopt** — `req:layoutir:slur-curve` extended: a slur spanning a system break splits into per-system sub-curves by de Casteljau (first segment keeps the slur's provenance, the rest synthesized continuations under `req:layoutir:continuation-synthesis`), replacing E2's draw-whole-in-start-system floating end | core_spec Ch7 §ResolvedLayoutIR (`req:layoutir:slur-curve`) | `epiphany-engrave` (casting `curve_fate`, `sub_cubic`) | | slur_shape measured | **adopt (implement pinned formula)** — `slur_shape_penalty` moves off its `0.0` placeholder to the catalog's pinned `req:qmc:slur` formula (arc ratio ρ = apex/chord against the `[0.08, 0.25]` band). The Quality Metric Catalog's §`slur_shape` rationale and the notated-but-unrendered open question are refreshed to record slurs now render and are measured; the formula is unchanged, so no catalog version move | quality_metric_catalog §`slur_shape_penalty` (`req:qmc:slur`, rationale) | `epiphany-engrave` (`quality.rs::slur_shape_raw`) | **Version movements.** None: core spec Ch7 `req:layoutir:slur-curve` extended + the `Curve` listing gains `line`; a revision-history row (Push 3); the Quality Metric Catalog rationale refreshed (no formula change). Operation Catalog stays 0.7.0, Binary Format 0.6.0, Quality Metric Catalog 0.2.0 — all non-canonical / formula-stable. **Deferred (documented, not open candidates).** Kind-differentiated slur appearance (Phrase/Editorial's own line, distinct from the SpanStyle dash), duration-aware slur height (would pull the clamped short/long slurs back into the shallow-arc band), collision-aware slur reshaping (the Standard-tier `slur_shape` driver) — all Standard-tier work. ## Push-3 tranche (2026-07-09) — primitive band ownership The Standard-tier inter-staff vertical solve needed to know which staff owns each primitive. `GlyphObject` had always declared its `vertical_band`; `Stroke` and `Curve` had not, so `epiphany-engrave` reconstructed ownership from geometry — and got it wrong twice, tearing stems (`4132a7a`) and then slurs (`b1bfe04`) off their own notes, both reaching a committed golden. A slur is the proof the inference cannot be made safe: its endpoints are lifted clear of its own staff by construction, into the zone where the nearest notehead belongs to the neighbouring staff. Ratified as implemented in `efaebb9` (code) and `fc411ea` (Ch7 listings). Layout-IR (Chapter 7) only, non-canonical — **no wire form, no companion version move**, and no golden churn (the declared owner agrees with the inferred one on the whole corpus, which is what licensed the swap). | Item | Disposition | Spec locus | Consumer | |---|---|---|---| | Primitive band ownership | **adopt (new requirement)** — Ch7 gains `req:layoutir:primitive-band-ownership`: every primitive presented to the solver in `ConstrainedLayoutIR` (`GlyphObject`, `Stroke`, `Curve`) MUST declare the `VerticalBandId` that owns it and that band MUST exist; a vertical solver MUST take ownership from the declaration and MUST NOT infer it from geometry. Staff-less content names a non-`Staff` band. A band MUST therefore exist for every staff of a region and for its margin, members or not; only glyphs are band *members* (a stroke's/curve's reference is one-way) | core_spec Ch7 §ConstrainedLayoutIR §Vertical Bands (`req:layoutir:primitive-band-ownership`) | `epiphany-layout-ir` (`constrained.rs::{Stroke,Curve}.vertical_band`, band emission); `epiphany-engrave` (`casting.rs` attribution) | | Ownership through the resolved stage | **adopt (new requirement)** — Ch7 gains `req:layoutir:resolved-band-ownership`: a resolved `Stroke`/`Curve` MUST retain its `vertical_band` (casting-off and the inter-staff solve both relocate them and must attribute them); a `ResolvedGlyph` carries none, its ownership already baked into its resolved position. Band ownership is non-canonical attribution metadata — it enters no content hash and no canonical encoding, so `ResolvedLayoutIR::canonical_bytes` omits it even from the primitives that carry it | core_spec Ch7 §ResolvedLayoutIR (`req:layoutir:resolved-band-ownership`) | `epiphany-layout-ir` (`resolved.rs::canonical_bytes`) | | Struct listings | **adopt (ratify reality)** — the `ConstrainedLayoutIR` listing gains `strokes: Vec` / `curves: Vec` (present in code since staff lines, never listed — without them Chapter 7 cannot describe supplying non-glyph ownership to the solver); the `Stroke`/`Curve` listings gain `vertical_band`; `Stroke`'s stale "the vertical-band model does not contain" gloss is dropped | core_spec Ch7 §ConstrainedLayoutIR, §ResolvedLayoutIR | `epiphany-layout-ir` | **Version movements.** None on the wire: core spec Chapter 7 gains two `req:layoutir:*` requirements, three struct-listing corrections, and a revision-history row. Operation Catalog stays 0.7.0, Binary Format stays 0.6.0 — layout geometry is non-canonical. **Known listing gap (pre-existing, not introduced here).** The `ConstrainedLayoutIR` listing still elides two fields the code carries: `break_origins: Vec` and `catalog: GlyphCatalogIdentity`. Both *types* are specified — the former's semantics by `req:layoutir:break-origin-attribution`, the latter by Ch7 §Glyph Catalog Identity — so only the struct listing is incomplete, and neither gap blocks an implementation the way a missing `strokes`/`curves` would have. Filed in `epiphany-layout-ir/DECISIONS.md` and **parked**: the Pass-13 batch is closed and the house rule opens a pass at ≥3 candidates, so this waits for company rather than reopening one. Deliberately not fixed here — the listing correction in this tranche is scoped to exactly what `req:layoutir:primitive-band-ownership` depends on. **Deferred (documented, not open candidates).** A height model for the inter-staff gap band — the missing piece behind both staff-less content placed *between* two staves (it holds still while the lower staff descends away) and `vertical_density_penalty` saturating at 1.0 when the solve targets content extents while the metric scores the realized gap against the band's *preferred* height. Both are named in `epiphany-engrave/DECISIONS.md`; neither is a Pass-13 ambiguity. ## Push-3 tranche (2026-07-09) — the inter-staff gap band as a height model Residue of the Standard-tier inter-staff solve, closed. **No spec requirement changes and no version moves**: the Quality Metric Catalog was right and the engraver was non-conforming. Delivered alongside `efaebb9`/`ced4b72` (primitive band ownership), which is what made the fix expressible. `req:qmc:vertical` has always defined the realized inter-staff gap as the separation "between the adjacent **content extents** the band separates". `engrave::quality::vertical_raw` measured the separation between the two bands' glyph `members` — because until `req:layoutir:primitive-band-ownership` landed, a band listed no strokes or curves to own. A staff's outermost ink is usually not a glyph. On `two_staff_close_content` the solve cleared the declared 2.0 gap exactly while the glyph-ink gap was 5.06, so the axis reported `|5.06 − 2|/2 = 1.53` → saturated **1.0**, and a Standard-tier `QualityFloorApproached` warning fired on a correct layout. Per §Conformance ("a solver that reports a `QualityMetricVector` computed by any function other than the ones defined here is non-conforming"), that was an implementation defect, not a definition defect. | Item | Disposition | Spec locus | Consumer | |---|---|---|---| | `vertical_density_penalty` measured over glyph members | **fix (conformance), no catalog change** — `vertical_raw` now measures each staff band's full content (glyphs, strokes, curves), attributed by declared `vertical_band`, read back from the BAKED output rather than the solve's own extents (so a shift the bake drops surfaces as a real deviation instead of hiding behind solver intent). Axis on the pressure fixture: 1.0 → 2.7e-7; the floor warning is gone. Formula, contributing units, anchor, normalization all unchanged | quality_metric_catalog §`vertical_density_penalty` (`req:qmc:vertical`) — **clarification only** | `epiphany-engrave` (`quality.rs::vertical_raw`, `casting.rs::CastLayout::{stroke_system,curve_system}`) | | "Content extent" was ambiguous | **clarify (editorial)** — `req:qmc:vertical` now spells out that content extent means every primitive the band owns, each attributed by its declared `vertical_band`, and not the band's glyph `members`. Before primitive band ownership this reading was arguably unimplementable, which is why the defect survived. The stale rationale (claiming the vertical spring solve is deferred) is refreshed, and the inter-system half of the axis is recorded as a genuine trade-off against `page_fill_efficiency`, not a defect | quality_metric_catalog §`vertical_density_penalty` rationale | — | | Solve read the gap from a constructor | **fix** — the inter-staff solve now targets the `preferred_height` of the `InterStaffGap` band `to_constrained` emitted for that staff pair, not `VerticalBand::inter_staff_gap`'s default. The band is now a height model: a region declaring a wider gap gets one, and the solve and the metric agree by construction rather than by both calling the same constructor | — (behavioural; no labelled requirement governs vertical-band *heights* — see P13-S4) | `epiphany-engrave` (`casting.rs`) | **Version movements.** Quality Metric Catalog **0.2.0 → 0.3.0** — see the review follow-up below. (The conformance fix above needed none on its own: formula, anchor, and normalization were unchanged, and a clarification is not a definition change. The follow-up's per-realization unit count *is* one, so the version moves with it.) Core spec unchanged. Operation Catalog 0.7.0, Binary Format 0.6.0 unchanged. **No layout change and no render-golden churn** — measurement-only, as `ENGRAVER_VERSION` staying at 11 records. **Deferred (documented, not open candidates).** Staff-less content placed *between* two staves would hold still while the lower staff descends away from it. No primitive can name an `InterStaffGap` band today (`band_of` yields a staff band or the region's margin band), so this is unreachable rather than latent; building the machinery now would be speculative. Named in `epiphany-engrave/DECISIONS.md`. ### Review follow-up (2026-07-09) — two more glyph-members assumptions An adversarial review of the fix above found the correction incomplete. Both findings were right, and the second falsified a comment I had written in the same commit. | Item | Disposition | Spec locus | Consumer | |---|---|---|---| | Region staff bands identified by glyph `members` | **fix (conformance)** — `vertical_raw` measured content over all primitives but still decided *which* staff bands belong to a region by glyph membership, reintroducing the assumption the change exists to shed. A staff band is allowed to own no glyphs: `to_constrained` emits one per staff of the region regardless, and a percussion-clef staff (no bundled glyph → traced anchor stroke) with no notes owns only its staff-line strokes. Region membership now comes from content presence in one of the region's systems — a staff band is per-(staff, region), so this identifies it exactly. Locked by the new `percussion_placeholder_staff` fixture and `a_staff_band_owning_no_glyphs_still_contributes_its_gap` | — (conformance to `req:qmc:vertical`) | `epiphany-engrave` (`quality.rs::vertical_raw`) | | Only the first realizing system measured | **fix + catalog definition move (0.3.0)** — the code measured one system per gap band, justified by a comment claiming rigid system translation makes every realization agree. The inter-staff solve had *just* falsified that: it sizes each system's gaps from that system's own content. `req:qmc:vertical` now counts **one unit per realization**, matching how realized inter-system gaps were already counted. Locked by the new `two_staff_wrapping_pressure` fixture (staff-line gap 15.93 in the pressured system, 7.87 in the slack one) and `inter_staff_gaps_are_measured_in_every_system_that_realizes_them` | quality_metric_catalog §`vertical_density_penalty` (`req:qmc:vertical`), **0.2.0 → 0.3.0** | `epiphany-engrave` | Both fixes are mutation-verified: reverting to glyph-member region identification scores `4.8e-7` on the percussion fixture, and reverting to first-system-only scores `1.3e-7` on the wrapping fixture — each ~0 where the corrected axis reports real deviation. **What the per-realization count exposes.** `two_staff_wrapping_pressure` now scores `vertical_density_penalty` **0.739**: its pressured system solves to the declared gap exactly (≈0 deviation), while its slack system sits at ≈5 staff spaces of content gap against a preferred 2.0. The axis is symmetric — a gap wider than preferred is sprawl exactly as a narrower one is crowding — and the inter-staff solve **only expands, never compresses**. That deferral (`epiphany-engrave/DECISIONS.md`, "compressing an OVER-wide fixed gap toward preferred… rarely wanted") is hereby promoted from *rarely wanted* to *measurably wrong*: any un-pressured multi-staff system reports honest sprawl until the solve can pull staves together. Named, not fixed — compression is a layout change (golden churn, `ENGRAVER_VERSION` move), not a measurement one. **Adjacent finding, not fixed here.** `Staff::default_clef` is never consulted by the projection: `to_constrained` takes the active clef from the staff instance's `clef_sequence` and falls back to `Clef::default()` (treble). A bass-clef staff that declares its clef only via `Staff::default_clef` engraves as treble. Found while building the percussion fixture (which therefore had to declare its clef as a `ClefChange`). Filed in `epiphany-layout-ir/DECISIONS.md`; parked with the `ConstrainedLayoutIR` listing gap pending a ≥3-candidate Pass-13 batch. ## Push-3 tranche (2026-07-09) — two-sided inter-staff renegotiation Acting on the sprawl the per-realization axis exposed. `ENGRAVER_VERSION` 11 → 12. **No normative change and no version move:** the Quality Metric Catalog's formula, contributing units, anchors and thresholds are untouched; only its non-normative rationale is refreshed (the reference solver no longer merely expands, and the axis's inter-staff half is now documented as a solver self-check). Core spec unchanged. Operation Catalog 0.7.0, Binary Format 0.6.0, Quality Metric Catalog 0.3.0 all unchanged. | Item | Disposition | Spec locus | Consumer | |---|---|---|---| | Expand-only inter-staff solve | **fix (layout)** — the solve now closes a slack pair as well as opening a crowded one, realizing the `InterStaffGap` band's declared height exactly. `SYSTEM_STAFF_PITCH` is demoted from a floor to an initial arrangement. This is what `vertical_density_penalty` was reporting: an un-pressured multi-staff system sat at 0.739, honest sprawl against the declared gap | — (behavioural; no labelled requirement governs vertical-band *heights* — see P13-S4) | `epiphany-engrave` (`casting.rs`) | | The gap band's height had no agreed meaning | **pin (code default, not spec)** — it is an **ink clearance**: the separation between the two staves' outermost content, which is exactly the unit `req:qmc:vertical` measures. `preferred` 2.0 → **5.0**, `min` 1.0 → **2.0**. The old 2.0 was a placeholder reconciled with nothing — neither the 8.0 staff-box gap the fixed pitch of 12 produces nor the ~6.4 ink clearance it leaves for plain content; realizing it would have crushed a relaxed system to a pitch of ~7.6. At 5.0 plain ledgered content settles near a pitch of 10.6 | `VerticalBand::inter_staff_gap` doc | `epiphany-layout-ir` | | **Cascade defect, latent since v11** | **fix (correctness)** — the recurrence subtracted the upper staff's shift from the measured gap and added it back through the accumulator, so every pair below the first was over-separated by exactly the shift above it. Both staves move: `shift_lower = shift_upper + target − (upper_lo − lower_hi)`, the *unshifted* gap. Invisible on two-staff fixtures (`shift_upper = 0`) and invisible to the cascade regression, which asserted only `s2 > s1` — true under both recurrences. `three_staff_close_content`'s lower pair realized **21.06** against a declared 4.0 | — | `epiphany-engrave` (`casting.rs`) | **How the defect was caught, and why that vindicates a design choice.** The metric measures the realized clearance back from the **baked output**, not from the solve's own extents. Reading back solver intent would have reported 0 and let the over-separation ship a second time. That choice was made one commit earlier for exactly this reason and paid immediately. The catalog rationale now recommends it to any conforming implementation. **Consequence for testing.** Once the solve realizes each declared clearance exactly, every inter-staff unit is 0 on a healthy solve — the axis is a solver self-check, and its *mean* can no longer distinguish "measured every realization" from "measured one". The regressions therefore assert the **unit set**, not the mean. Four mutations verified: the double-counting recurrence, expand-only, the glyph-`members` band filter, and first-system-only measurement each fail a named test. **Churn.** The two multi-staff engrave goldens only. `two_staff_close_content` grew by exactly 3.0 (the target change, no cascade); `three_staff_close_content` *shrank* by 9.06 — the same +3 per pair, less the 17.06 of over-separation the cascade defect was adding. Single-staff and every stub golden are byte-stable. **Process note.** The value 5.0 was the user's call. `4.0` ("one staff height") was chosen first and then withdrawn once its true consequence — a staff pitch of 9.57, not the 11.04 an arithmetic slip had projected — was **measured rather than inferred**. The slip: deriving plain-content ink clearance from an aggregate metric by assuming it had two contributing units when it had three.