Push 4: Binary Format companion, F1 benches, subquadratic reduction order

The audit's fourth push: the biggest outstanding Phase-2 item plus the
performance gate. 793 workspace tests pass; clippy -D warnings, fmt,
and rustdoc (deny-warnings) clean; all three spec documents build with
zero undefined references.

Binary Format companion (spec/binary_format.tex, v0.1.0 — Agent J's
deliverable, 43 pages):
- Twelve chapters transcribed from the golden-locked implementation:
  encoding conventions (the three prefix/endianness regimes, a
  normative no-varint rule, reject-never-normalize decode discipline),
  identifiers imported from the core spec's Canonical Byte-Layout
  Reference, primitive value encodings, the whole-Score positional
  codec ratified as the schema-major-0 wire form, operation wire
  forms (envelope field order with the normative id-leads property,
  the OperationPayload 0..=3 and OperationKind 0..=23 tables,
  effects/conflict/anomaly/MaterializedState vocabulary), the bundle
  physical layout (64-byte header, 256-byte superblock, chunk
  preimages and framing, ChunkRef, manifest body order), the
  operation-index payload, and the extension-blob/edit-barrier byte
  forms.
- Ratifies P12-D1 (req:binfmt:opindex), P12-E1 (req:binfmt:ext-blobs),
  P12-E2 (req:binfmt:condition-depth, MAX_CONDITION_DEPTH = 64
  normative), and P12-E3 (req:binfmt:object-kind-open) — batch rows
  struck through; discharges the provisional-codec notes in core
  (P11-4), ops, and bundle (P11-D2/D4/D5) DECISIONS with ratification
  cross-references.
- Pins the frozen-layout schema-evolution keystone: within schema
  major 0 every positional struct layout is frozen; a field-set change
  is a schema-major change with migration — formally grounding the
  data-model-expansion staging decision. Open questions kept honest
  in-document: SnapshotId derivation, index-refresh threshold, u64/u32
  prefix unification at the next major.
- Not yet delivered from J's charter: the cross-implementation decoder
  test and the wire-format fuzzer (follow-up harnesses).

F1 benches (crates/epiphany-testkit/benches/, per the F0 decision):
- criterion 0.5.1 (workspace dev-dependency; MSRV 1.77 respected with
  documented transitive pins: clap 4.5.53, half 2.4.1).
- reduction bench at 1K/10K/50K envelopes with the Chapter-10 budget
  (>10,000 envelopes/second cold) written in the bench as a Pass/Xfail
  gate; bundle benches for the typical-edit commit (<=50 ms; measured
  ~14.7 ms on real disk after catching that tmpfs neuters fsync) and
  the open/bootstrap read (<=200 ms; measured ~60 us).
- CI: quick budget gates in the conformance job, full gates nightly.

Subquadratic canonical_reduction_order (the F-surfaces/K-fixes
handshake, closing K's 10K-envelope acceptance gate):
- The bench documented the failure (50K at ~1.7K env/s, a 29 s cold
  reduction; two O(n^2) loops); the fix replaces pair enumeration with
  threshold/frontier readiness per replica plus explicit-dot dependent
  lists and a stamp-tuple binary heap — O((n + sum(context)) log n),
  never materializing covered pairs.
- Byte-identical order: same edge relation, same ready predicate, same
  total order; the old implementation is retained as a test-only
  oracle with element-for-element order-equality property tests over
  fuzz sets, adversarial sets, and directed shapes (2,000-envelope
  full-coverage chains, dot cycles, duplicate-id stamp ties),
  mutation-tested for sensitivity.
- Measured: 1K 155K->674K env/s, 10K 12.5K->257K, 50K 1.7K->87K; all
  three scale points now pass and the 50K row is promoted from Xfail.

Also: fixed nine rustdoc private/unresolved intra-doc links that had
accumulated across the pushes (the CI deny-doc-warnings job would have
failed on them).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd
This commit is contained in:
Levi Neuwirth 2026-07-02 19:02:07 -04:00
parent 92aaccf7e2
commit 3e91a8302a
27 changed files with 4448 additions and 24 deletions

View File

@ -105,6 +105,17 @@ jobs:
- name: Run conformance suite
run: cargo run --release -p epiphany-testkit --example conformance_suite 1
# The Chapter 10 performance-budget gates (Phase 2 worklist F1), in
# release with reduced sampling (EPIPHANY_BENCH_QUICK): the 1K and 10K
# reduction points and both bundle budgets must pass; known-pending
# points are documented xfails inside the benches ("F surfaces, K
# fixes"). The heaviest (50K) reduction point runs only in the nightly
# soak below.
- name: Performance budget gates (quick)
env:
EPIPHANY_BENCH_QUICK: "1"
run: cargo bench -p epiphany-testkit
# Track A, Agent H's merge gate (Phase 2). A discrete job so a spelling /
# decomposition pre-pass regression is attributable to H, not buried in the
# workspace test run. Asserts H's PHASE2_QUICKSTART acceptance criterion: the
@ -138,3 +149,7 @@ jobs:
- uses: Swatinem/rust-cache@v2
- name: Run conformance soak
run: cargo run --release -p epiphany-testkit --example conformance_suite 10
# The full budget-gate run, including the minute-scale 50K reduction
# point (a documented xfail until the O(n²) reducer fix lands).
- name: Performance budget gates (full)
run: cargo bench -p epiphany-testkit

226
Cargo.lock generated
View File

@ -127,6 +127,15 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "android-activity"
version = "0.6.1"
@ -161,6 +170,18 @@ dependencies = [
"libc",
]
[[package]]
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "arboard"
version = "3.6.1"
@ -583,6 +604,12 @@ dependencies = [
"wayland-client",
]
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
version = "1.2.65"
@ -622,6 +649,58 @@ dependencies = [
"libc",
]
[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]]
name = "clap"
version = "4.5.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
dependencies = [
"anstyle",
"clap_lex",
]
[[package]]
name = "clap_lex"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
[[package]]
name = "clipboard-win"
version = "5.4.1"
@ -789,12 +868,52 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "criterion"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"is-terminal",
"itertools",
"num-traits",
"once_cell",
"oorandom",
"regex",
"serde",
"serde_derive",
"serde_json",
"tinytemplate",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
dependencies = [
"cast",
"itertools",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-common"
version = "0.1.7"
@ -997,6 +1116,12 @@ dependencies = [
"winit",
]
[[package]]
name = "either"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
[[package]]
name = "emath"
version = "0.29.1"
@ -1151,6 +1276,7 @@ dependencies = [
name = "epiphany-testkit"
version = "0.0.0"
dependencies = [
"criterion",
"epiphany-bundle",
"epiphany-core",
"epiphany-determinism",
@ -1593,6 +1719,16 @@ dependencies = [
"bitflags 2.13.0",
]
[[package]]
name = "half"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
dependencies = [
"cfg-if",
"crunchy",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
@ -1792,6 +1928,32 @@ dependencies = [
"hashbrown 0.17.1",
]
[[package]]
name = "is-terminal"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jni"
version = "0.22.4"
@ -2466,6 +2628,12 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "oorandom"
version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]]
name = "orbclient"
version = "0.3.55"
@ -2794,6 +2962,35 @@ dependencies = [
"bitflags 2.13.0",
]
[[package]]
name = "regex"
version = "1.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "renderdoc-sys"
version = "1.1.0"
@ -2973,6 +3170,19 @@ dependencies = [
"syn 2.0.118",
]
[[package]]
name = "serde_json"
version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "serde_repr"
version = "0.1.20"
@ -3312,6 +3522,16 @@ dependencies = [
"zerovec",
]
[[package]]
name = "tinytemplate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "tinyvec"
version = "1.11.0"
@ -4541,6 +4761,12 @@ dependencies = [
"syn 2.0.118",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]]
name = "zstd"
version = "0.13.3"

View File

@ -88,6 +88,13 @@ unicode-normalization = "0.1"
# produce compressed fixtures. Default features (legacy formats, dictionary
# building) are off — none are needed to decode standard frames.
zstd = { version = "0.13", default-features = false }
# Criterion drives the Chapter 10 performance benches (Phase 2 worklist F1) in
# epiphany-testkit/benches (see that crate's DECISIONS.md, F0/F1). Pinned to
# the 0.5 line: its MSRV (1.70) fits the workspace's pinned 1.77, while
# criterion 0.6+ requires 1.80. Default features are off — the plotting stack
# (plotters) and rayon are dead weight for budget gates — keeping only
# `cargo_bench_support` so plain `cargo bench` remains the entry point.
criterion = { version = "0.5", default-features = false, features = ["cargo_bench_support"] }
# Determinism-sensitive: never enable fast-math-style codegen on canonical
# numerical paths (Appendix D, "Rounding and CPU Behavior"). The default

View File

@ -351,6 +351,14 @@ When that companion lands, reconcile this crate's `header`, `superblock`,
round-trip would be the trigger, per the QUICKSTART process notes). This is the
file-format analogue of `epiphany-core`'s P11-4.
> **Ratified (2026-07-02):** `spec/binary_format.tex` v0.1.0 Chapter 7 pins
> this crate's header (64-byte table), superblock (256-byte table), chunk
> framing and hash preimages, `ChunkRef`, block framing, the manifest body
> order with its sort/dedup rules, and the operation-index payload (P12-D1,
> `req:binfmt:opindex`) exactly as implemented and golden-locked here. The
> reconciliation trigger never fired: the companion was transcribed from this
> crate.
### P11-D3 — Blob hashing shape is ambiguous
Chapter 8 §"Blobs" says blobs are *"content-addressed identically to chunks

View File

@ -139,6 +139,14 @@ companion lands, reconcile this crate's `CanonicalEncode`/`CanonicalDecode` and
the whole-score `codec` with it (a failing cross-crate round-trip test would be
the trigger, per the QUICKSTART process notes).
> **Ratified (2026-07-02):** the Binary Format companion now exists
> (`spec/binary_format.tex`, v0.1.0). Its Chapter 5 ratifies this crate's
> whole-`Score` positional codec, the convention macros, every discriminant
> table, and the `CanonicalValue` seam as the schema-major-0 wire form, with
> the frozen-layout rule (a field-set change is a schema-major change). No
> reconciliation was needed: the companion was transcribed from this codec and
> its golden anchors, so the trigger never fired.
**Phase 2 — Agent K (Operation Catalog): the `CanonicalValue` seam.** Track B's
Operation Catalog shifts `epiphany-ops` from identifier-only operation payloads
to *value-typed* ones (an `InsertEvent` carrying the real `Event`, a

View File

@ -1186,7 +1186,7 @@ fn decomposition_source_rank(source: &DecompositionSource) -> usize {
/// decompositions only "for events that lack a higher-precedence attachment",
/// with the "same precedence machinery" as spelling, minus the axes the
/// attachment does not carry (no analysis layers, no `priority` field). Among
/// competing authored attachments the lowest [`decomposition_source_rank`]
/// competing authored attachments the lowest `decomposition_source_rank`
/// wins; a remaining tie keeps the first candidate in the score's
/// `decomposition_attachments` order, which is canonical (codec-fixed), so the
/// resolution is deterministic across replicas.

View File

@ -163,7 +163,7 @@ const INVERSION_TOLERANCE_WHOLE_NOTES: f64 = 1e-6;
/// The typed absolute residual tolerance of the inverse conversion
/// ([`TempoMap::wallclock_to_musical`]): a [`Tolerance`] of class
/// [`ToleranceClass::TempoIntegration`] (Appendix D §"Tolerance Classes" — no
/// ad-hoc epsilons), absolute bound [`INVERSION_TOLERANCE_WHOLE_NOTES`] whole
/// ad-hoc epsilons), absolute bound `INVERSION_TOLERANCE_WHOLE_NOTES` whole
/// notes, no relative bound, governing validation (it decides whether a
/// continued-fraction candidate is accepted; conversion is advisory, never
/// canonical state).

View File

@ -1000,7 +1000,7 @@ impl EditorSession {
/// replaying the log reconstructs the same atomic unit. This is the substrate for
/// intents that must land several primitives together (e.g. a value change with a
/// matching respelling); editor-level atomicity (commit only on full success) is
/// inherited from [`Self::commit`].
/// inherited from the private `commit` path.
pub fn apply_transaction(
&mut self,
label: &str,

View File

@ -4,7 +4,7 @@
//! Agent I's **engraving constraint solver** (spec **Chapter 9**, "Constraint-
//! Solver Interface"): it turns a [`ConstrainedLayoutIR`] into a
//! [`ResolvedLayoutIR`] with real geometry. It is the production-side replacement
//! for `epiphany-layout-ir`'s interface-only [`StubSolver`] — the QUICKSTART puts
//! for `epiphany-layout-ir`'s interface-only `StubSolver` — the QUICKSTART puts
//! the *interface* (`layout-ir`) and the *algorithm* (`engrave`) in separate
//! crates so the core/product boundary stays sharp (`spec/PHASE2_QUICKSTART.md`,
//! crate topology).
@ -12,7 +12,7 @@
//! ## Phase status — `Minimal` tier
//!
//! [`Engraver`] runs a genuine deterministic **horizontal spacing pass** (see
//! [`spacing`]) — placing each glyph-bearing slot left-to-right by a
//! the private `spacing` module) — placing each glyph-bearing slot left-to-right by a
//! collision-aware advance (its preferred width floored by the real glyph
//! bearings) — and **evaluates the IR's declared constraints** against the
//! resolved geometry, routed by [`LayoutConstraint::strength`] (Chapter 9

View File

@ -343,6 +343,12 @@ object is covered); the provenance-preservation contract itself is unchanged.
declarations and gates `apply`/`apply_transaction` through
`EditBarrier::prohibits_edit`.
> **Ratified (2026-07-02):** `spec/binary_format.tex` v0.1.0 Chapter 8
> ratifies the blob byte form (P12-E1, `req:binfmt:ext-blobs`), pins
> `MAX_CONDITION_DEPTH = 64` as the normative recursion bound (P12-E2,
> `req:binfmt:condition-depth`), and adopts the open-value `ObjectKind`
> decode stance (P12-E3, `req:binfmt:object-kind-open`).
## Pass 12 candidates (ambiguities for the spec, not resolved in code)
1. **Strength attachment to constraint instances.** Chapter 9 §"Strength Levels"

View File

@ -11,7 +11,7 @@
//! ## Coordinate frame
//!
//! Shapes are in **staff-space, y-up world** coordinates — the same frame as
//! [`RenderPrimitive::position`] and [`Stroke`] endpoints, *before* any
//! [`RenderPrimitive::position`] and stroke endpoints, *before* any
//! renderer's world→screen transform. A GUI maps a screen point to this frame
//! with the inverse of the same transform its renderer uses for display (for the
//! SVG renderer, the inverse of its single `translate(-min_x, max_y) scale(1,-1)`

View File

@ -431,6 +431,15 @@ is deterministic and unambiguous but **provisional**: when the Binary Format
companion lands, reconcile `encode.rs` and the per-type `CanonicalEncode` impls
with it. A failing cross-crate round-trip test is the trigger.
> **Ratified (2026-07-02):** `spec/binary_format.tex` v0.1.0 Chapter 6 pins
> this crate's wire forms exactly as implemented — the envelope field order and
> its normative id-leads property, the `OperationPayload` (0..=3) and
> `OperationKind` (0..=23, append-only) discriminant tables, per-payload
> framing, `OperationKindTag`'s separate space, the 28-byte stamp, the DVV
> layout, and the full effects/conflict/anomaly/`MaterializedState` vocabulary.
> The reconciliation trigger never fired: the companion was transcribed from
> this crate and its golden anchors.
## Spec-compliance audit follow-up (2026-07, Push 1)
Four reduction-semantics fixes closing MUST-level gaps the six-agent spec audit
@ -708,3 +717,69 @@ equivocation fuzz plus the unchanged `run_equivocation_fuzz` gate this).
no change to the determinism crate. No encoding changed and no discriminant
was appended; `operation_kind_tag_decode_mirrors_encode_exactly` /
`operation_kind_tag_decode_rejects_malformed_bytes` pin the contract.
## Subquadratic `canonical_reduction_order` (2026-07, the F1 → K fix)
- **The defect (F surfaces, K fixes).** The testkit's F1 bench
(`crates/epiphany-testkit/benches/reduction.rs`, Chapter 10: > 10,000
envelopes/s cold) documented `canonical_reduction_order` as O(n²) twice
over: a literal double loop over all (predecessor, successor) pairs to
build indegrees, and a full ready-scan per emission. Measured pre-fix:
~155K / ~12.5K / ~1.7K env/s at 1K / 10K / 50K envelopes (50K ≈ 29 s per
cold reduce — the documented xfail row).
- **The algorithm: term decomposition + monotone thresholds, never pairs.**
Materializing edges is inherently quadratic for the common chain shape
(every DVV floor covers the whole replica prefix, so covered *pairs* are
Θ(n²)); the rewrite therefore never enumerates pairs. Each causal-context
entry becomes one *requirement term* over the present set:
- *Vector floor `(r, n)`* — covers exactly the present envelopes of replica
`r` with counter `<= n` (zero-based floor, P11-C7): a **prefix** of the
replica's lane sorted by `(counter, slice index)`. The term is satisfied
when the lane's emission **frontier** (first unemitted slot, monotone)
passes the prefix; if the floor covers the envelope's own id, only the
self-pair is exempt, so that term instead reads "frontier at own slot and
**second frontier** past the prefix" (both monotone). Terms park in
per-lane `BTreeMap`s keyed by the threshold slot and are drained exactly
once as the frontiers advance.
- *Explicit dot* — covers exactly the present envelopes bearing that id;
satisfied when the id's unemitted multiplicity reaches 0 (1 for a
self-dot, which covers only duplicate-id twins).
An envelope is ready when its unsatisfied-term count reaches zero; ready
envelopes sit in a `BinaryHeap` keyed by `(reduction tuple, slice index)`.
A pre-sorted `(tuple, index)` list with a cursor supplies the malformed-
cycle fallback (heap empty, envelopes remain). Total work is
`O((n + Σ|context|) log n)`.
- **Why the order is byte-identical (the consensus argument).** The order is
*defined* by: edge `p → s` iff `p != s` and `s.context.covers(p.id)`;
ready iff every present covered predecessor emitted; emit the minimum
`(reduction tuple, slice index)` among ready (slice index reproduces
`min_by_key`'s first-minimum rule — reachable only under duplicate-id
tuple ties); if none is ready, the same minimum over all unemitted (cycle
break). The rewrite computes the *same readiness predicate*: each term is
satisfied iff all envelopes it covers are emitted, every covered
predecessor is covered by at least one term, and terms cover only covered
predecessors — so "all terms satisfied" ⇔ "all covered predecessors
emitted", including the exempt self-pair (a floor/dot naming the
envelope's own id) and envelopes covered by both a dot and a floor (the
conjunction needs no per-pair dedup; the redundant term is harmless).
Absent context entries (unknown replicas, floors below every present
counter, absent dots) yield no term, exactly as they yield no edge. Same
edge relation, same ready set, same total order on ready ⇒ the same
emission sequence, element for element.
- **The retained oracle + regression gate.** The pre-fix implementation is
kept verbatim as `canonical_reduction_order_reference` (`#[cfg(test)]`),
and three property tests assert element-for-element *pointer* equality of
both orders (the strictest check — it distinguishes byte-identical twins):
`canonical_order_matches_reference_on_fuzz_sets` (the crate's well-formed
fuzz generator, 250 seeds × 2 slice orders),
`..._on_adversarial_sets` (400 randomized hostile sets: self-covering
floors, dots to present/absent/own ids, duplicate ids with tied stamps,
`SYSTEM_DERIVED` replicas, stamps contradicting the causal edges, empty
contexts), and `..._on_directed_shapes` (a 2,000-envelope full-coverage
chain with descending stamps, self-covering and dot-only chains, dot
2-/3-cycles, mutual-floor cycles, twin permutations). Mutation-checked:
breaking the self-exemption or the self-dot threshold fails the suite.
- **Measured post-fix (same bench, dev profile 2026-07):** ~674K / ~257K /
~87K env/s at 1K / 10K / 50K — the 50K row cleared its budget by ~8.7x,
the gate printed its XPASS promotion notice, and the row was flipped to
`Pass` in the same change (see `epiphany-testkit/DECISIONS.md` F1).

View File

@ -709,7 +709,7 @@ pub(crate) fn resolved_anchor_position(anchor: &TimeAnchor) -> MusicalPosition {
impl SetUserSystemBreakOp {
/// The anchor's resolved musical position — the canonical LWW bucketing key
/// (see [`resolved_anchor_position`]).
/// (see the private `resolved_anchor_position` helper).
pub fn resolved_position(&self) -> MusicalPosition {
resolved_anchor_position(&self.anchor)
}
@ -1118,7 +1118,7 @@ pub struct SetUserPageBreakOp {
impl SetUserPageBreakOp {
/// The anchor's resolved musical position — the canonical LWW bucketing key
/// (see [`resolved_anchor_position`]).
/// (see the private `resolved_anchor_position` helper).
pub fn resolved_position(&self) -> MusicalPosition {
resolved_anchor_position(&self.anchor)
}

View File

@ -28,7 +28,8 @@
//! deferred to the Operation Catalog (§6.11); see `DECISIONS.md` for the exact
//! boundary.
use std::collections::{BTreeMap, BTreeSet};
use std::cmp::Reverse;
use std::collections::{BTreeMap, BTreeSet, BinaryHeap};
use epiphany_core::{
canonical_pitch_bytes, derive_promoted_voice_id, AnchorOffset, AnnotationAnchor,
@ -59,6 +60,7 @@ use crate::payload::{
RespellPitchOp, SetMetadataOp, SetMetricGridOp, SetUserPageBreakOp, TransposeOp,
TupletCompensation,
};
use crate::stamp::StampTuple;
use crate::support::{ObjectKind, SerializedCanonicalInputs};
use crate::undo::{UndoPolicy, UndoTransactionPayload};
@ -71,8 +73,297 @@ use crate::undo::{UndoPolicy, UndoTransactionPayload};
/// coverage, choosing the smallest HLC reduction tuple among ready operations.
/// A malformed causal cycle has no valid topological order; the smallest HLC
/// tuple deterministically breaks the cycle so every replica still converges.
///
/// ## Subquadratic construction (worklist F1 → K fix)
///
/// The order is *defined* pairwise: an edge `p → s` exists iff `p != s` and
/// `s.causal_context.covers(p.id)`; an envelope is *ready* when every present
/// covered predecessor has been emitted; the smallest reduction tuple among
/// ready envelopes emits next (slice position breaks the — duplicate-id-only —
/// tuple ties, matching `min_by_key`'s first-minimum rule, which the retained
/// test-only `canonical_reduction_order_reference` oracle implements literally
/// in O(n²)). Materializing the edges is inherently quadratic for the common
/// chain-context shape (every DVV floor covers the full replica prefix), so
/// this implementation never enumerates pairs. It decomposes each context into
/// *requirement terms* whose conjunction is exactly pairwise readiness:
///
/// * A **vector floor** `(r, n)` covers precisely the present envelopes of
/// replica `r` with counter `<= n` (the zero-based DVV floor, P11-C7) —
/// a *prefix* of the replica's lane sorted by `(counter, slice index)`.
/// The term is satisfied when the lane's emission frontier (the first
/// unemitted lane slot) passes the prefix; a floor that covers the
/// envelope's *own* id exempts only the self-pair, so that term is instead
/// "frontier at own slot and *second* frontier past the prefix". Both
/// frontiers are monotone, so each term is woken exactly once from a
/// `BTreeMap` keyed by the threshold slot.
/// * An **explicit dot** covers precisely the present envelopes bearing that
/// id; the term is satisfied when the id's unemitted multiplicity reaches
/// zero (or one, for a dot naming the envelope's own id). A dot also lying
/// under one of the context's own floors yields a (redundant) second term —
/// harmless, because readiness is the conjunction of terms, not a
/// predecessor count, so no per-pair dedup is needed.
///
/// Context entries covering no present envelope (absent replicas, floors below
/// every present counter, absent dots) yield no term, exactly as they yield no
/// edge pairwise. Total work is `O((n + Σ context entries) · log n)`.
pub fn canonical_reduction_order<'a>(
envelopes: &[&'a OperationEnvelope],
) -> Vec<&'a OperationEnvelope> {
let len = envelopes.len();
let keys: Vec<StampTuple> = envelopes
.iter()
.map(|env| env.stamp.reduction_tuple())
.collect();
// Static indexes over the present set: per-replica lanes sorted by
// (counter, slice index), each envelope's (lane, slot) position, and the
// per-id unemitted multiplicity (> 1 only for duplicate ids, e.g.
// equivocation twins fed to this function directly).
let mut lane_of: BTreeMap<ReplicaId, usize> = BTreeMap::new();
let mut lanes: Vec<OrderLane> = Vec::new();
let mut position = vec![(0usize, 0usize); len];
{
let mut sorted: Vec<(ReplicaId, u64, usize)> = envelopes
.iter()
.enumerate()
.map(|(index, env)| (env.id.replica, env.id.counter, index))
.collect();
sorted.sort_unstable();
for (replica, counter, index) in sorted {
let lane_index = *lane_of.entry(replica).or_insert_with(|| {
lanes.push(OrderLane::default());
lanes.len() - 1
});
let lane = &mut lanes[lane_index];
position[index] = (lane_index, lane.slots.len());
lane.slots.push(index);
lane.counters.push(counter);
}
for lane in &mut lanes {
// All slots start unemitted: frontier 0, second frontier 1
// (clamped to the lane length, the "exhausted" sentinel).
lane.second = 1.min(lane.slots.len());
}
}
let mut id_slots: BTreeMap<OperationId, IdSlot> = BTreeMap::new();
for env in envelopes {
id_slots.entry(env.id).or_default().unemitted += 1;
}
// One requirement term per covering context entry; `remaining` counts the
// currently-unsatisfied terms. Terms already satisfied here (they cover
// nothing, or nothing beyond the envelope itself) register no watcher.
let mut remaining = vec![0usize; len];
for (index, env) in envelopes.iter().enumerate() {
for (&replica, &floor) in &env.causal_context.vector {
let Some(&lane_index) = lane_of.get(&replica) else {
continue;
};
let lane = &mut lanes[lane_index];
let prefix = lane.counters.partition_point(|&counter| counter <= floor);
if prefix == 0 {
continue;
}
if env.id.replica == replica && env.id.counter <= floor {
// The floor covers this envelope's own id; only the self-pair
// is exempt. Required: every other prefix slot emitted, i.e.
// frontier at the own slot *and* second frontier past the
// prefix (the own slot stays unemitted until emission).
let own_slot = position[index].1;
if lane.frontier < own_slot {
remaining[index] += 1;
lane.frontier_watchers.entry(own_slot).or_default().push(
FloorWatcher::ExceptSelf {
node: index,
prefix,
},
);
} else if lane.second < prefix {
remaining[index] += 1;
lane.second_watchers.entry(prefix).or_default().push(index);
}
} else if lane.frontier < prefix {
remaining[index] += 1;
lane.frontier_watchers
.entry(prefix)
.or_default()
.push(FloorWatcher::Whole { node: index });
}
}
for dot in env.causal_context.dots() {
let Some(id_slot) = id_slots.get_mut(&dot) else {
continue; // absent id: covers nothing present, no edge
};
if dot == env.id {
// A dot naming the envelope's own id covers only duplicates.
if id_slot.unemitted > 1 {
remaining[index] += 1;
id_slot.watch_one.push(index);
}
} else {
remaining[index] += 1;
id_slot.watch_zero.push(index);
}
}
}
// Deterministic Kahn walk. The heap holds every envelope whose terms are
// all satisfied (pushed exactly at the transition; entries for envelopes
// already emitted through cycle-breaking are skipped lazily), keyed by
// (reduction tuple, slice index) — the reference's `min_by_key` order.
let mut heap: BinaryHeap<Reverse<(StampTuple, usize)>> = (0..len)
.filter(|&index| remaining[index] == 0)
.map(|index| Reverse((keys[index], index)))
.collect();
let mut by_key: Vec<usize> = (0..len).collect();
by_key.sort_unstable_by_key(|&index| (keys[index], index));
let mut cycle_cursor = 0usize;
let mut emitted = vec![false; len];
let mut ordered = Vec::with_capacity(len);
let mut woken: Vec<usize> = Vec::new();
while ordered.len() < len {
let mut ready = None;
while let Some(Reverse((_, index))) = heap.pop() {
if !emitted[index] {
ready = Some(index);
break;
}
}
let next = match ready {
Some(index) => index,
None => {
// A cycle is malformed, but selecting by the canonical
// tie-breaker keeps reduction deterministic and unlocks the
// forced envelope's dependents.
while emitted[by_key[cycle_cursor]] {
cycle_cursor += 1;
}
by_key[cycle_cursor]
}
};
emitted[next] = true;
ordered.push(envelopes[next]);
// Dot wake-ups: the id's unemitted multiplicity dropped by one.
let id_slot = id_slots
.get_mut(&envelopes[next].id)
.expect("every present id has a slot");
id_slot.unemitted -= 1;
if id_slot.unemitted <= 1 {
woken.append(&mut id_slot.watch_one);
}
if id_slot.unemitted == 0 {
woken.append(&mut id_slot.watch_zero);
}
// Floor wake-ups: advance the lane frontiers (both point at unemitted
// slots — or the lane length — by invariant, so an emission below the
// second frontier is at one of them) and drain the passed watchers.
let (lane_index, slot) = position[next];
let lane = &mut lanes[lane_index];
if slot == lane.frontier {
lane.frontier = lane.second;
lane.second = next_unemitted(&lane.slots, &emitted, lane.frontier + 1);
} else if slot == lane.second {
lane.second = next_unemitted(&lane.slots, &emitted, slot + 1);
}
while let Some(entry) = lane.frontier_watchers.first_entry() {
if *entry.key() > lane.frontier {
break;
}
for watcher in entry.remove() {
match watcher {
FloorWatcher::Whole { node } => woken.push(node),
FloorWatcher::ExceptSelf { node, prefix } => {
if lane.second >= prefix {
woken.push(node);
} else {
lane.second_watchers.entry(prefix).or_default().push(node);
}
}
}
}
}
while let Some(entry) = lane.second_watchers.first_entry() {
if *entry.key() > lane.second {
break;
}
for node in entry.remove() {
woken.push(node);
}
}
for node in woken.drain(..) {
remaining[node] -= 1;
if remaining[node] == 0 && !emitted[node] {
heap.push(Reverse((keys[node], node)));
}
}
}
ordered
}
/// One replica's present envelopes in [`canonical_reduction_order`], sorted by
/// `(counter, slice index)`, with the two monotone emission frontiers and the
/// floor-term watchers keyed by the frontier slot they wait for.
#[derive(Default)]
struct OrderLane {
/// Envelope slice indexes, sorted by `(counter, slice index)`.
slots: Vec<usize>,
/// The slots' counters (parallel to `slots`, ascending).
counters: Vec<u64>,
/// First unemitted slot (== `slots.len()` once exhausted).
frontier: usize,
/// Second unemitted slot (>= `slots.len()` once fewer than two remain).
second: usize,
/// Floor terms waiting for `frontier >= key`.
frontier_watchers: BTreeMap<usize, Vec<FloorWatcher>>,
/// Self-exempt floor terms waiting for `second >= key`.
second_watchers: BTreeMap<usize, Vec<usize>>,
}
/// One present `OperationId`'s bookkeeping in [`canonical_reduction_order`]:
/// its unemitted multiplicity and the dot terms watching it.
#[derive(Default)]
struct IdSlot {
/// Present envelopes bearing this id that are not yet emitted.
unemitted: usize,
/// Dot terms satisfied when `unemitted` reaches zero.
watch_zero: Vec<usize>,
/// Self-dot terms (duplicate ids) satisfied when `unemitted` reaches one.
watch_one: Vec<usize>,
}
/// A floor term parked in [`OrderLane::frontier_watchers`].
enum FloorWatcher {
/// Satisfied outright when the frontier reaches its key (the prefix end).
Whole { node: usize },
/// A floor covering the node's own id: when the frontier reaches the
/// node's own slot (its key), the term is satisfied if the second
/// frontier already passed `prefix`, else it re-parks on the second
/// frontier.
ExceptSelf { node: usize, prefix: usize },
}
/// The first unemitted slot position at or after `from` (== `slots.len()` when
/// exhausted). Frontier scans only ever move forward, so the total scan work
/// per lane is linear.
fn next_unemitted(slots: &[usize], emitted: &[bool], from: usize) -> usize {
let mut at = from.min(slots.len());
while at < slots.len() && emitted[slots[at]] {
at += 1;
}
at
}
/// The pre-F1 O(n²) implementation, retained verbatim as the property-test
/// oracle for [`canonical_reduction_order`]: it materializes every covered
/// `(predecessor, successor)` pair and re-scans the whole set per emission,
/// which *is* the order's pairwise definition, executed literally.
#[cfg(test)]
pub(crate) fn canonical_reduction_order_reference<'a>(
envelopes: &[&'a OperationEnvelope],
) -> Vec<&'a OperationEnvelope> {
let len = envelopes.len();
let mut indegree = vec![0usize; len];
@ -4839,6 +5130,346 @@ mod tests {
}
}
// --- Subquadratic canonical order vs. the retained O(n²) oracle. --------
/// A minimal envelope for pure ordering tests: payload content never
/// affects the canonical reduction order.
fn order_env(
replica: ReplicaId,
counter: u64,
physical: i64,
logical: u32,
ctx: CausalContext,
) -> OperationEnvelope {
let id = OperationId::new(replica, counter);
OperationEnvelope {
id,
author: AuthorId(1),
stamp: OperationStamp::new(
HybridLogicalClock::new(WallClockTime(physical), logical),
id,
),
causal_context: ctx,
transaction: None,
payload: OperationPayload::Primitive(OperationKind::DeleteEvent(DeleteEventOp {
event: EventId::new(ReplicaId(7), counter % 5),
tuplet_compensation: TupletCompensation::NotInTuplet,
})),
}
}
/// Asserts the subquadratic order equals the reference oracle's
/// element-for-element — by slice element identity (pointer equality), the
/// strictest possible check: it distinguishes even byte-identical
/// duplicate envelopes, whose tuple ties both implementations must break
/// by slice position.
fn assert_order_matches_reference(envelopes: &[OperationEnvelope]) {
let refs: Vec<&OperationEnvelope> = envelopes.iter().collect();
let fast = canonical_reduction_order(&refs);
let oracle = canonical_reduction_order_reference(&refs);
assert_eq!(fast.len(), oracle.len());
for (at, (a, b)) in fast.iter().zip(&oracle).enumerate() {
assert!(
std::ptr::eq(*a, *b),
"canonical order diverges from the reference at position {at}: \
{:?} vs {:?} (n = {})",
a.id,
b.id,
envelopes.len()
);
}
}
/// In-place FisherYates driven by the seeded generator (slice order is an
/// input to the tuple tie-break, so permutations must be exercised too).
fn shuffle_envelopes(
envelopes: &mut [OperationEnvelope],
rng: &mut epiphany_determinism::fuzz::SplitMix64,
) {
for i in (1..envelopes.len()).rev() {
let j = (rng.next_u64() % (i as u64 + 1)) as usize;
envelopes.swap(i, j);
}
}
/// A hostile ordering input the crate's well-formed generators avoid:
/// floors that cover the envelope's own id or absent counters, dots to
/// present / absent / own ids, duplicate ids (byte-identical twins),
/// `SYSTEM_DERIVED` replicas, colliding stamps that contradict the causal
/// edges (so the topological pass and the cycle-breaker both engage), and
/// empty contexts.
fn adversarial_set(
rng: &mut epiphany_determinism::fuzz::SplitMix64,
n: usize,
) -> Vec<OperationEnvelope> {
let replicas = 1 + rng.next_u64() % 4;
let mut next_counter: BTreeMap<ReplicaId, u64> = BTreeMap::new();
let mut envs: Vec<OperationEnvelope> = Vec::with_capacity(n);
for _ in 0..n {
if !envs.is_empty() && rng.next_u64() % 8 == 0 {
// A duplicate id — and a byte-identical stamp, so the
// reduction tuple genuinely ties.
let victim = envs[(rng.next_u64() % envs.len() as u64) as usize].clone();
envs.push(victim);
continue;
}
let replica = if rng.next_u64() % 16 == 0 {
ReplicaId::SYSTEM_DERIVED
} else {
ReplicaId(1 + rng.next_u64() % replicas)
};
let slot = next_counter.entry(replica).or_insert(0);
// Occasionally skip counters so floors assert absent predecessors.
let counter = *slot + rng.next_u64() % 2;
*slot = counter + 1;
let id = OperationId::new(replica, counter);
let mut ctx = CausalContext::new();
for _ in 0..rng.next_u64() % 3 {
let target = match rng.next_u64() % 5 {
0 => replica, // may cover the envelope's own id
1 => ReplicaId(9), // absent replica
2 => ReplicaId::SYSTEM_DERIVED,
_ => ReplicaId(1 + rng.next_u64() % replicas),
};
// May exceed every present counter (covering future authoring
// of that replica — a causal cycle) or fall below all of them.
ctx = ctx.with_seen(target, rng.next_u64() % 8);
}
for _ in 0..rng.next_u64() % 3 {
let dot = if !envs.is_empty() && rng.next_u64() % 2 == 0 {
envs[(rng.next_u64() % envs.len() as u64) as usize].id
} else if rng.next_u64() % 4 == 0 {
id // the envelope's own id
} else {
OperationId::new(ReplicaId(1 + rng.next_u64() % 5), rng.next_u64() % 10)
};
ctx = ctx.with_dot(dot);
}
// Tiny stamp ranges force heavy tuple collisions and stamps that
// contradict the causal edges.
envs.push(order_env(
replica,
counter,
(rng.next_u64() % 6) as i64,
(rng.next_u64() % 3) as u32,
ctx,
));
}
envs
}
#[test]
fn canonical_order_matches_reference_on_fuzz_sets() {
// The crate's own well-formed generator: multi-replica meshes of
// vector floors, occasional equivocation twins (duplicate ids) and
// HLC-monotonicity anomalies, empty contexts on counter-0 roots.
let mut rng = epiphany_determinism::fuzz::SplitMix64::new(0xF1_0DE2_0001);
for _ in 0..250 {
let n = 1 + (rng.next_u64() % 40) as usize;
let mut envs = crate::fuzz::gen_envelope_set(&mut rng, n);
assert_order_matches_reference(&envs);
shuffle_envelopes(&mut envs, &mut rng);
assert_order_matches_reference(&envs);
}
}
#[test]
fn canonical_order_matches_reference_on_adversarial_sets() {
let mut rng = epiphany_determinism::fuzz::SplitMix64::new(0xADE5_A71A_0002);
assert_order_matches_reference(&[]);
for iteration in 0..400 {
let n = 1 + (rng.next_u64() % 60) as usize;
let mut envs = adversarial_set(&mut rng, n);
assert_order_matches_reference(&envs);
if iteration % 4 == 0 {
shuffle_envelopes(&mut envs, &mut rng);
assert_order_matches_reference(&envs);
}
}
}
#[test]
fn canonical_order_matches_reference_on_directed_shapes() {
let r = ReplicaId(1);
// A 2,000-envelope single-replica chain whose every DVV floor covers
// the full replica prefix — the inherently-quadratic-pairs shape the
// subquadratic construction exists for — with *descending* stamps, so
// the causal edges (not the HLC) decide every single emission.
let full_chain: Vec<OperationEnvelope> = (0..2_000)
.map(|c| {
let ctx = if c == 0 {
CausalContext::new()
} else {
CausalContext::new().with_seen(r, c - 1)
};
order_env(r, c, 2_000 - c as i64, 0, ctx)
})
.collect();
assert_order_matches_reference(&full_chain);
// A self-covering chain: every floor also covers the envelope's own
// id (the exempted self-pair).
let self_chain: Vec<OperationEnvelope> = (0..600)
.map(|c| {
order_env(
r,
c,
600 - c as i64,
0,
CausalContext::new().with_seen(r, c),
)
})
.collect();
assert_order_matches_reference(&self_chain);
// A dot-only chain (no vector floors at all).
let dot_chain: Vec<OperationEnvelope> = (0..600)
.map(|c| {
let ctx = if c == 0 {
CausalContext::new()
} else {
CausalContext::new().with_dot(OperationId::new(r, c - 1))
};
order_env(r, c, 600 - c as i64, 0, ctx)
})
.collect();
assert_order_matches_reference(&dot_chain);
// Malformed dot cycles (2-cycle and 3-cycle) among bystanders with
// empty contexts and identical stamps.
let id = |rep: u64, c: u64| OperationId::new(ReplicaId(rep), c);
let cycles = vec![
order_env(
ReplicaId(2),
0,
5,
0,
CausalContext::new().with_dot(id(2, 1)),
),
order_env(
ReplicaId(2),
1,
5,
0,
CausalContext::new().with_dot(id(2, 0)),
),
order_env(
ReplicaId(3),
0,
5,
0,
CausalContext::new().with_dot(id(3, 2)),
),
order_env(
ReplicaId(3),
1,
5,
0,
CausalContext::new().with_dot(id(3, 0)),
),
order_env(
ReplicaId(3),
2,
5,
0,
CausalContext::new().with_dot(id(3, 1)),
),
order_env(ReplicaId(4), 0, 5, 0, CausalContext::new()),
order_env(ReplicaId(5), 0, 5, 0, CausalContext::new()),
];
assert_order_matches_reference(&cycles);
// Mutual full-coverage floors (a floor cycle where each envelope also
// covers itself), plus coverage of absent ids on an absent replica.
let floor_cycle = vec![
order_env(
ReplicaId(6),
0,
9,
0,
CausalContext::new().with_seen(ReplicaId(6), 1),
),
order_env(
ReplicaId(6),
1,
8,
0,
CausalContext::new().with_seen(ReplicaId(6), 1),
),
order_env(
ReplicaId(6),
2,
7,
0,
CausalContext::new()
.with_seen(ReplicaId(40), 12)
.with_dot(id(41, 3)),
),
];
assert_order_matches_reference(&floor_cycle);
// Byte-identical duplicate ids (tuple ties broken by slice position)
// in several slice orders, including a dot and a floor onto the
// duplicated id.
let twin = order_env(ReplicaId(2), 3, 1, 0, CausalContext::new());
let mut twins = vec![
twin.clone(),
twin.clone(),
order_env(
ReplicaId(2),
4,
0,
0,
CausalContext::new().with_dot(id(2, 3)),
),
order_env(
ReplicaId(3),
0,
0,
0,
CausalContext::new().with_seen(ReplicaId(2), 3),
),
// A twin that dots its own id: covers only its duplicate.
order_env(
ReplicaId(2),
3,
1,
0,
CausalContext::new().with_dot(id(2, 3)),
),
];
assert_order_matches_reference(&twins);
twins.reverse();
assert_order_matches_reference(&twins);
twins.swap(0, 2);
assert_order_matches_reference(&twins);
// SYSTEM_DERIVED authoring under floors and dots from user replicas.
let sys = ReplicaId::SYSTEM_DERIVED;
let system = vec![
order_env(sys, 0, 3, 0, CausalContext::new()),
order_env(sys, 1, 2, 0, CausalContext::new().with_seen(sys, 0)),
order_env(
ReplicaId(2),
0,
1,
0,
CausalContext::new().with_seen(sys, 1),
),
order_env(
ReplicaId(2),
1,
0,
0,
CausalContext::new().with_dot(id(u64::MAX, 0)),
),
];
assert_order_matches_reference(&system);
}
#[test]
fn reduction_is_permutation_invariant() {
let envs = vec![

View File

@ -269,7 +269,7 @@ pub fn region(id: RegionId) -> Region {
}
/// Score metadata with a `nth`-distinct title (M2d) — distinct `nth` give
/// distinct [`ScoreMetadata`] values so a harness can drive concurrent
/// distinct `ScoreMetadata` values so a harness can drive concurrent
/// `SetMetadata`s, an advisory LWW field that resolves by canonical order with
/// no conflict.
pub fn score_metadata(nth: u8) -> epiphany_core::ScoreMetadata {

View File

@ -3,7 +3,7 @@
//!
//! Agent I's **SVG renderer** behind the Epiphany `RenderIR` interface (spec
//! **Chapter 7** §"RenderIR"): it turns a
//! [`ResolvedLayoutIR`](epiphany_layout_ir::ResolvedLayoutIR) into well-formed
//! [`ResolvedLayoutIR`] into well-formed
//! **SVG 1.1**, drawing each glyph from **genuine Bravura SMuFL** data: inline
//! outline `<path>`s by default, or `<text>` set in an `@font-face`-embedded
//! subset. It is the visible end of the v0 `Score → layout IR` pipeline: from a
@ -15,7 +15,7 @@
//! the renderer was golden-locked against the **stub solver's** output first, then
//! against the real [`epiphany_engrave`](../epiphany_engrave/index.html) solver
//! once real notation and re-spacing landed. The renderer consumes any solver's
//! [`ResolvedLayoutIR`](epiphany_layout_ir::ResolvedLayoutIR): it preserves the
//! [`ResolvedLayoutIR`]: it preserves the
//! resolved geometry, provenance traces, XML validity, deterministic output, and
//! glyph-mode choice without making engraving-semantic decisions.
//!
@ -24,8 +24,9 @@
//! The bundled outlines are extracted from the official OFL `Bravura.otf` (see
//! `tools/extract_bravura_outlines.py` and `tools/OFL.txt`) in staff-space,
//! y-up coordinates. The renderer makes SVG-encoding choices only and never
//! engraving-semantic ones; see [`svg`] for the coordinate system, the
//! provenance-tracing contract, and the diagnostic-not-paper-over rule.
//! engraving-semantic ones; see the private `svg` module for the coordinate
//! system, the provenance-tracing contract, and the diagnostic-not-paper-over
//! rule.
//!
//! ## Font availability
//!

View File

@ -16,7 +16,25 @@ epiphany-ops.workspace = true
# now drives the real crate instead of an in-tree stub.
epiphany-layout-ir.workspace = true
# The Chapter 10 performance benches (worklist F1) are the only dev-dependency
# user: the budget-gate logic itself lives in `src/budget.rs` on plain `std`,
# so the library builds without criterion.
[dev-dependencies]
criterion.workspace = true
# Drives the whole suite at scale outside the unit-test timeout — the analogue
# of epiphany-determinism's `fuzz_roundtrip` and epiphany-bundle's `fuzz_crash`.
[[example]]
name = "conformance_suite"
# The Chapter 10 performance-budget benches (worklist F1; DECISIONS.md F0/F1).
# `harness = false`: each bench owns `main()`, running criterion's measurements
# first and then the budget gate — the assertion layer criterion cannot express
# — exiting nonzero on an unexpected budget miss.
[[bench]]
name = "reduction"
harness = false
[[bench]]
name = "bundle"
harness = false

View File

@ -48,7 +48,7 @@ Concretely:
`[prepass-harness]` stage in the conformance suite.
- **`benches/` lives in this crate** (not per-crate). The Chapter-10 budgets are
workspace-level, and the marquee bench (the reducer's `O(n²)`
workspace-level, and the marquee bench (the reducer's — at the time `O(n²)`
`canonical_reduction_order` at 10K+ envelopes, worklist F1) drives
`epiphany-ops` *through* the testkit's envelope generators — exactly what this
crate already does. A bench that lived in `epiphany-ops` could not reuse the
@ -70,6 +70,96 @@ mirrors how v0's `convergence`/`roundtrip` modules are shared between
**Unblocks:** F1 (benches), F3 (corpus + taxonomy harness — done), F4 (per-agent
harness skeletons — H done), F5 (integration skeleton).
## F1 — The Chapter 10 budget benches (`benches/reduction.rs`, `benches/bundle.rs`)
*Worklist item F1: a criterion bench asserting Chapter-10 budgets at documented
scale points, known-pending points marked xfail with the numeric budget written
in the bench. Home per F0: this crate's `benches/`.*
**Criterion version: `0.5` (locks to 0.5.1), default features off plus
`cargo_bench_support`.** The 0.5 line's MSRV (1.70) fits the workspace's pinned
`rust-version = "1.77"`; criterion 0.6+ requires 1.80. Disabling default
features drops the plotting stack (`plotters`) and `rayon` — dead weight for
budget gates — while `cargo_bench_support` keeps plain `cargo bench` as the
entry point. Two transitive pins in `Cargo.lock` keep the tree MSRV-clean,
because the resolver (v2) is not MSRV-aware: `clap 4.5.53` (MSRV 1.74; 4.6
requires 1.85) and `half 2.4.1` (MSRV 1.70; 2.5+ requires 1.81). Re-check those
two if `cargo update` touches the criterion tree.
**The gate mechanism.** Criterion measures but never asserts, so each bench is
`harness = false` and its `main()` runs criterion's measurements first, then
the budget gate in [`crate::budget`] — a library module (the F0 pattern: shared
logic lives in `src/`, and it deliberately uses no criterion types, so the
library itself takes no new dependency). Every budget row carries an
`Expectation` written in the bench source next to its numeric threshold:
- `Pass` — must hold today; a miss exits nonzero (the CI tripwire).
- `Xfail(reason)` — documented known-pending miss; the reason names the defect
and owner. A miss prints an expected-failure line; a **pass** prints an XPASS
promotion notice, so a stale marking is loud in both directions.
Skipped rows (the 50K point in quick mode) print an explicit `skip` line, never
silence. Under `cargo test --benches` (criterion test mode, detected by the
absence of the `--bench` flag `cargo bench` passes) the measurements run once
and the gate is skipped — the gate belongs to `cargo bench`.
**Sampling calibration (a documented deviation from Chapter 10).** The
conformance methodology — p99 over ≥1000 iterations on the reference hardware
profile — is the reference suite's job, not this gate's: 1000 iterations of the
(pre-fix ~29 s) 50K cold reduction was not a CI shape. The gate takes the
**median of a small per-row iteration count** (reduction: 9/3/1 iterations at
1K/10K/50K; bundle: 40 commit / 25 read; cut to 5/2/skip and 12/8 under
`EPIPHANY_BENCH_QUICK=1`),
cold (no in-gate warm-up — the marquee budget is an explicitly *cold* rate).
Criterion uses flat sampling with `sample_size(10)` for the reduction group;
the 50K point is gate-only (calibrated when a single iteration took ~29 s and
criterion's ≥10-sample loop would have blown the wall-clock budget; ~0.58 s
post-fix). Full `cargo bench -p epiphany-testkit` stays under ~2 minutes of
measurement; quick mode under ~15 seconds.
**Measured on the dev profile (2026-07, post-K-fix), the table's ground
truth:**
| row | budget | measured | verdict |
|-----|--------|----------|---------|
| `reduction/1000` | > 10,000 env/s cold | ~674,000 env/s (~1.5 ms) | Pass |
| `reduction/10000` | > 10,000 env/s cold | ~257,000 env/s (~39 ms) | Pass |
| `reduction/50000` | > 10,000 env/s cold | ~87,000 env/s (~0.58 s) | Pass (promoted from Xfail) |
| `bundle/typical_edit_commit` | ≤ 50 ms | ~14.7 ms (real fsync) | Pass |
| `bundle/open_bootstrap_read` | ≤ 200 ms | ~63 µs (moderate corpus) | Pass |
As F1 first measured it (2026-07, pre-fix), the O(n²)
`canonical_reduction_order` indegree construction was unambiguous in the curve
(~155K / ~12.5K / ~1.7K env/s at 1K/10K/50K — 10x envelopes cost ~120x time),
sinking the 50K row (~29 s per cold reduce, the documented xfail) and leaving
10K only ~25% over budget. Agent K's subquadratic rewrite
(threshold/frontier readiness, byte-identical order — see
`epiphany-ops/DECISIONS.md`) triggered the gate's XPASS promotion notice, and
the 50K row was flipped to Pass in the same change (**F surfaces, K fixes**,
round-tripped). Any future red row is a fresh regression: fix the reducer, do
not re-mark rows xfail without a written decision here.
**Two honesty notes on the bundle rows.** (1) They run on the build target's
filesystem, *not* `std::env::temp_dir()`: `/tmp` is commonly tmpfs, where fsync
is a near-no-op — measured, the commit row is ~54 µs on tmpfs vs ~14.7 ms on a
real NVMe filesystem, a 270x difference that would have made the 50 ms budget
vacuous. (2) The read budget is spec'd against "a 100-page orchestral score";
no such corpus generator exists yet, so the row is a moderate-corpus (1K
envelopes + canonical-base snapshot) stand-in with margin to spare, and says so
in its docs.
**Scope, and the budget rows deliberately not built yet.** F1 covers the three
budgets that are measurable against shipped subsystems: the reduction rate and
the two file-format budgets. The remaining Chapter 10 rows are blocked on
unimplemented or in-flight subsystems and become future benches in this same
`benches/` + gate shape: the interactive keystroke→frame budgets (blocked on
Agent I's engrave/render pipeline maturing past the visible slice), cold
solving of a 100-page score within 2 s p99 and the incremental-propagation
bound (blocked on the real Chapter 9 solver and a 100-page corpus generator —
the latter also unblocks the honest read-row corpus), and the memory ceilings
(blocked on the same corpus). CI runs the gates in the `conformance` job with
`EPIPHANY_BENCH_QUICK=1` (50K skipped) and in the nightly `soak` job in full.
## F3 — The representative score corpus + eligibility-taxonomy harness (Agent H)
*Worklist item F3 — "the most underbuilt dependency; unblocks H entirely."*

View File

@ -147,6 +147,40 @@ Per the QUICKSTART, implementation-discovered gaps are batched, not improvised:
remaining layout-specific Pass 11 candidates (the `OperationKindTag` variant set
and the layout-object id derivation).
## Performance benches (Chapter 10 budgets, worklist F1)
`benches/` holds the criterion benches for the spec's measurable Chapter 10
budgets (see `DECISIONS.md` F0 for why they live in this crate, F1 for every
call made). Criterion measures; the **budget gate** (`src/budget.rs`) asserts:
each bench's `main()` ends by re-timing every budget row and exiting nonzero if
a `Pass`-marked row misses its threshold. Known-pending rows are marked
`Xfail(reason)` *in the bench source* next to the numeric budget — a miss is
reported and tolerated, and a pass prints a loud promotion notice so stale
markings cannot linger. This is the "F surfaces, K fixes" handshake, and its
inaugural round has completed: the bench documented the reducer's O(n²)
`canonical_reduction_order` failure at scale, and Agent K's subquadratic
rewrite (see `epiphany-ops/DECISIONS.md`) flipped the xfail row to `Pass`.
| row | budget (spec Chapter 10) | expectation |
|-----|--------------------------|-------------|
| `reduction/1000` | > 10,000 envelopes/s, cold | Pass (~674K env/s measured) |
| `reduction/10000` | > 10,000 envelopes/s, cold | Pass (~257K env/s measured) |
| `reduction/50000` | > 10,000 envelopes/s, cold | Pass (~87K env/s measured; promoted from Xfail by Agent K's reducer fix — was ~1.7K env/s) |
| `bundle/typical_edit_commit` | ≤ 50 ms (append + manifest + superblock flip, fsync'd) | Pass (~15 ms) |
| `bundle/open_bootstrap_read` | ≤ 200 ms (manifest + bootstrap chunks) | Pass (moderate-corpus stand-in) |
```sh
# Full run (includes the 50K cold-reduction point, ~0.6 s per iteration):
cargo bench -p epiphany-testkit
# The reduced CI shape: smaller sampling, 50K point skipped (PR CI runs this):
EPIPHANY_BENCH_QUICK=1 cargo bench -p epiphany-testkit
```
The gate is a calibrated median over a few iterations, deliberately not the
spec's p99-over-1000-iterations conformance methodology (that is the reference
suite's job; the deviation is documented in `src/budget.rs`).
## Running
```sh

View File

@ -0,0 +1,284 @@
//! The Chapter 10 file-format budgets: typical-edit bundle write and
//! manifest+bootstrap read (Phase 2 worklist F1).
//!
//! The normative budgets (`spec/core_spec.tex`, Chapter 10 §"File Format
//! Performance"):
//!
//! > Bundle write of a typical edit (one or more operation envelopes appended
//! > to the operation-envelope block stream, manifest rewrite, superblock flip)
//! > completes within 50 ms at p99 on the reference hardware profile.
//! >
//! > Bundle read of the manifest and bootstrap chunks (sufficient for first
//! > interactive frame) completes within 200 ms at p99 for a 100-page
//! > orchestral score.
//!
//! Both rows run against a real on-disk bundle (`FileStore`, whose flush is a
//! genuine `fsync`) in a scratch directory under the build's **target dir** —
//! deliberately not `std::env::temp_dir()`, which is commonly tmpfs on Linux,
//! where fsync is a near-no-op and the commit budget would be measured against
//! RAM. The corpus is moderate: 1,000 generated operation envelopes packed
//! into operation blocks plus a canonical `MaterializedState` snapshot wired
//! as the manifest's `canonical_base`. Both
//! are expected to **Pass** today, so a regression fails `cargo bench` loudly.
//! The read row's honesty note: the spec sizes its 200 ms against a 100-page
//! orchestral score; no such corpus generator exists yet, so this row is the
//! moderate-corpus stand-in (recorded in `DECISIONS.md` F1) and the budget is
//! asserted with margin to spare. The OS page cache is warm across iterations
//! (dropping it needs privileges); each iteration's *process-level* state is
//! fresh.
//!
//! The timed sections:
//!
//! * **typical_edit_commit** — on an opened bundle restored to the same base
//! image (restore + open are un-timed setup): one `commit` staging a small
//! envelope block, i.e. block append + manifest rewrite + superblock flip,
//! every write fsync'd. This mirrors `bundle_harness`'s commit driver.
//! * **open_bootstrap_read** — `FileStore::open` + `Bundle::open` (superblock
//! selection + header + manifest decode) + reading the `canonical_base`
//! snapshot and every operation block — the bytes a first interactive frame
//! needs.
//!
//! Criterion measures; the budget gate in `main` asserts (see
//! `epiphany_testkit::budget` for the Pass/Xfail semantics and the documented
//! deviation from Chapter 10's p99-over-1000-iterations conformance
//! methodology). Run: `cargo bench -p epiphany-testkit --bench bundle`;
//! `EPIPHANY_BENCH_QUICK=1` shrinks sampling for PR CI.
use std::fs;
use std::path::{Path, PathBuf};
use std::time::Duration;
use criterion::{BatchSize, Criterion};
use epiphany_bundle::{
pack_operation_blocks, Bundle, ChunkKind, CommitContext, DocumentId, FileStore, FileUuid,
FrontierBytes, Manifest, MemStore, ProfileId, ReductionAlgorithmVersion, SchemaVersion,
SnapshotId, SnapshotRef, StagedChunk,
};
use epiphany_determinism::CanonicalEncode;
use epiphany_ops::{OperationEnvelope, OperationSet};
use epiphany_testkit::budget::{self, Expectation};
use epiphany_testkit::{generators, Rng};
/// Chapter 10: a typical-edit bundle write completes within 50 ms (p99).
const COMMIT_BUDGET: Duration = Duration::from_millis(50);
/// Chapter 10: manifest + bootstrap read completes within 200 ms (p99).
const READ_BUDGET: Duration = Duration::from_millis(200);
/// Base-corpus scale: the criterion-5 envelope count, packed into real blocks.
const BASE_ENVELOPES: usize = 1_000;
/// The typical edit: a handful of envelopes appended as one block.
const EDIT_ENVELOPES: usize = 4;
/// Everything the two rows measure against, built once from a fixed seed.
struct Fixture {
/// The committed base image (blocks + canonical-base snapshot).
base_image: Vec<u8>,
/// The typical edit, staged (one small operation block).
edit: Vec<StagedChunk>,
/// Temp-dir file paths: one per row so commit growth never skews reads.
commit_path: PathBuf,
read_path: PathBuf,
}
/// The commit-context closure the harness uses: append the new chunks to the
/// previous manifest's `operation_roots`.
fn append_roots(ctx: &CommitContext) -> Manifest {
let mut manifest = ctx.previous_manifest.clone();
manifest
.operation_roots
.extend(ctx.new_chunks.iter().copied());
manifest
}
fn staged_blocks(envelopes: &[OperationEnvelope]) -> Vec<StagedChunk> {
let payloads: Vec<Vec<u8>> = envelopes.iter().map(|e| e.to_canonical_bytes()).collect();
pack_operation_blocks(&payloads)
.into_iter()
.map(StagedChunk::operation_block)
.collect()
}
/// Builds the moderate base corpus: 1,000 envelopes committed as operation
/// blocks, then their cold reduction committed as a `Snapshot` chunk wired to
/// the manifest's `canonical_base` (the roundtrip harness's snapshot shape).
fn build_fixture(dir: &Path) -> Fixture {
let mut rng = Rng::new(0x00F1_B0DE_0001);
let envelopes = generators::operation_envelopes(&mut rng, BASE_ENVELOPES, 3, 40, 40);
let edit_envelopes = generators::operation_envelopes(&mut rng, EDIT_ENVELOPES, 3, 8, 8);
let uuid = FileUuid(rng.array16());
let doc = DocumentId(rng.array16());
let mut bundle =
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create base bundle");
bundle
.commit(&staged_blocks(&envelopes), append_roots)
.expect("commit base operation blocks");
// The canonical base: the corpus's cold reduction, stored as a snapshot.
let mut set = OperationSet::new();
set.accept_all(envelopes.iter().cloned());
let canonical = set.reduce().canonical_bytes();
let snapshot = StagedChunk {
kind: ChunkKind::Snapshot,
schema_version: SchemaVersion::V0,
payload: canonical,
};
let frontier = generators::frontier_bytes(&envelopes);
bundle
.commit(&[snapshot], |ctx| {
let mut manifest = ctx.previous_manifest.clone();
let root = ctx.new_chunks[0];
let mut sid = [0u8; 16];
sid.copy_from_slice(&root.hash.as_bytes()[..16]);
manifest.canonical_base = Some(SnapshotRef {
snapshot_id: SnapshotId(sid),
covers_causal_frontier: FrontierBytes::from_bytes(frontier.clone()),
reduction_algorithm_version: ReductionAlgorithmVersion(0),
profile_id: ProfileId::Full,
hash: root.hash,
root,
});
manifest
})
.expect("commit canonical-base snapshot");
Fixture {
base_image: bundle.into_store().into_bytes(),
edit: staged_blocks(&edit_envelopes),
commit_path: dir.join("commit.epb"),
read_path: dir.join("read.epb"),
}
}
/// Un-timed setup for the commit row: restore the base image and open it.
fn restore_and_open(path: &Path, image: &[u8]) -> Bundle<FileStore> {
fs::write(path, image).expect("restore base image");
Bundle::open(FileStore::open(path).expect("open store")).expect("open bundle")
}
/// The timed commit: block append + manifest rewrite + superblock flip, fsync'd.
fn typical_edit_commit(mut bundle: Bundle<FileStore>, edit: &[StagedChunk]) -> u64 {
bundle
.commit(edit, append_roots)
.expect("typical-edit commit");
bundle.generation()
}
/// The timed read: open (superblock selection + manifest decode) + the
/// bootstrap chunks — canonical-base snapshot and every operation block.
fn open_bootstrap_read(path: &Path) -> usize {
let bundle = Bundle::open(FileStore::open(path).expect("open store")).expect("open bundle");
let manifest = bundle.manifest();
let mut bytes = 0usize;
let base = manifest
.canonical_base
.as_ref()
.expect("the fixture wires a canonical base");
bytes += bundle
.read_chunk(&base.root)
.expect("snapshot chunk reads")
.len();
for root in &manifest.operation_roots {
for envelope in bundle
.read_operation_block(root)
.expect("operation block reads")
{
bytes += envelope.len();
}
}
bytes
}
/// The criterion measurement side.
fn criterion_measurements(criterion: &mut Criterion, fixture: &Fixture, quick: bool) {
let mut group = criterion.benchmark_group("bundle");
group.sample_size(if quick { 10 } else { 30 });
group.measurement_time(Duration::from_secs(if quick { 1 } else { 4 }));
group.warm_up_time(Duration::from_millis(if quick { 300 } else { 1000 }));
group.bench_function("typical_edit_commit", |b| {
b.iter_batched(
|| restore_and_open(&fixture.commit_path, &fixture.base_image),
|bundle| typical_edit_commit(bundle, &fixture.edit),
BatchSize::PerIteration,
)
});
fs::write(&fixture.read_path, &fixture.base_image).expect("write read-row image");
group.bench_function("open_bootstrap_read", |b| {
b.iter(|| open_bootstrap_read(&fixture.read_path))
});
group.finish();
}
/// The budget-gate side: both rows are expected to **Pass** today.
fn budget_gate(fixture: &Fixture, quick: bool) -> Vec<budget::GateReport> {
let commit_median = budget::median_time(
if quick { 12 } else { 40 },
|| restore_and_open(&fixture.commit_path, &fixture.base_image),
|bundle| typical_edit_commit(bundle, &fixture.edit),
);
fs::write(&fixture.read_path, &fixture.base_image).expect("write read-row image");
let read_median = budget::median_time(
if quick { 8 } else { 25 },
|| (),
|()| open_bootstrap_read(&fixture.read_path),
);
vec![
budget::latency_gate(
"bundle/typical_edit_commit",
commit_median,
COMMIT_BUDGET,
Expectation::Pass,
),
budget::latency_gate(
"bundle/open_bootstrap_read",
read_median,
READ_BUDGET,
Expectation::Pass,
),
]
}
/// A scratch directory on the **build target's filesystem** (real disk), not
/// `temp_dir()`: `/tmp` is commonly tmpfs, where the commit row's fsyncs would
/// be free and the 50 ms budget vacuous. Honors `CARGO_TARGET_DIR`.
fn scratch_dir() -> PathBuf {
let target = std::env::var_os("CARGO_TARGET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("target")
});
target.join(format!("f1-bundle-bench-{}", std::process::id()))
}
fn main() {
// `cargo bench` passes `--bench`; its absence means criterion's test mode
// (`cargo test --benches` / `--all-targets`): run each measurement once,
// skip the gate.
let bench_mode = std::env::args().any(|arg| arg == "--bench");
let quick = budget::quick_mode();
let dir = scratch_dir();
fs::create_dir_all(&dir).expect("create bench scratch dir");
let fixture = build_fixture(&dir);
let mut criterion = Criterion::default().configure_from_args();
criterion_measurements(&mut criterion, &fixture, quick);
criterion.final_summary();
let holds = if bench_mode {
budget::verdict(&budget_gate(&fixture, quick))
} else {
true
};
let _ = fs::remove_dir_all(&dir);
if !holds {
std::process::exit(1);
}
}

View File

@ -0,0 +1,195 @@
//! The Chapter 10 operation-envelope reduction-rate bench + budget gate
//! (Phase 2 worklist F1).
//!
//! The normative budget (`spec/core_spec.tex`, Chapter 10 §"File Format
//! Performance"):
//!
//! > Operation-envelope reduction rate MUST exceed 10,000 envelopes per second
//! > on the reference hardware profile, measured during cold reduction from a
//! > fresh canonical base.
//!
//! Three documented scale points: **1K** (the acceptance suite's criterion-5
//! scale), **10K**, and **50K** envelopes. The timed section is the cold
//! reduction an opener performs — `OperationSet::new()` + `accept_all` +
//! `reduce()` on a fresh set — with the envelope vector generated once per
//! scale point from a fixed seed and *cloned outside* the timed section.
//!
//! Criterion measures (throughput in envelopes/s); the budget gate in `main`
//! asserts, with the budget table below (`epiphany_testkit::budget` explains
//! the Pass/Xfail semantics and the deliberate deviation from Chapter 10's
//! p99-over-1000-iterations conformance methodology). The defect this bench
//! was written to surface — `canonical_reduction_order`'s literal O(n²)
//! double-loop indegree construction plus its per-emission full ready-scan
//! (`crates/epiphany-ops/src/reduce.rs`) — sank the 50K point decisively
//! (~1.7K env/s ≈ 29 s per cold reduce on the dev profile) and left the 10K
//! point clearing the budget with only ~25% margin. Agent K's subquadratic
//! rewrite (threshold/frontier readiness; see the epiphany-ops DECISIONS
//! entry) closed the loop — **F surfaces, K fixes**
//! (`spec/PHASE2_F_WEEK0_WORKLIST.md` F1) — and the gate's XPASS notice
//! promoted the 50K row to `Pass` (~87K env/s measured, ~8.7x budget).
//!
//! Run: `cargo bench -p epiphany-testkit --bench reduction`. Set
//! `EPIPHANY_BENCH_QUICK=1` for the reduced PR-CI shape (smaller sampling, 50K
//! point skipped). Under `cargo test --benches` criterion runs each measurement
//! once in test mode and the gate is skipped — the gate belongs to `cargo
//! bench`.
use std::time::Duration;
use criterion::{BatchSize, BenchmarkId, Criterion, SamplingMode, Throughput};
use epiphany_ops::{MaterializedState, OperationEnvelope, OperationSet};
use epiphany_testkit::budget::{self, Expectation};
use epiphany_testkit::{generators, Rng};
/// Chapter 10: the reduction rate MUST exceed 10,000 envelopes per second.
const RATE_BUDGET_ENV_PER_SEC: f64 = 10_000.0;
/// One documented scale point of THE BUDGET TABLE below.
struct ScalePoint {
/// Envelope count (also the criterion throughput element count).
n_ops: usize,
/// Fixed generator seed — bench inputs are reproducible byte-for-byte.
seed: u64,
/// The documented expectation against `RATE_BUDGET_ENV_PER_SEC`.
expectation: Expectation,
/// Budget-gate timed iterations (full mode, quick mode); `0` skips the
/// row in that mode (printed as an explicit skip, never silent).
gate_iters: (usize, usize),
/// Criterion measurement time (full mode), or `None` to leave the point
/// gate-only (the 50K point: it predates the reducer fix, when a single
/// cold reduction was minute-scale; the budget gate's median covers it).
criterion_time: Option<Duration>,
}
/// THE BUDGET TABLE (worklist F1). Budget: > 10,000 envelopes/second, cold.
///
/// | envelopes | expectation | measured (dev profile, 2026-07, post-K-fix) | why |
/// |-----------|-------------|----------------------------------------------|-----|
/// | 1,000 | Pass | ~674,000 env/s (~1.5 ms) | criterion-5 scale; ~67x margin |
/// | 10,000 | Pass | ~257,000 env/s (~39 ms) | ~26x margin |
/// | 50,000 | Pass | ~87,000 env/s (~0.58 s) | promoted from Xfail by Agent K's subquadratic reducer; full/nightly runs only |
///
/// Pre-fix (the numbers the F1 xfail table documented): ~155K / ~12.5K /
/// ~1.7K env/s — the O(n²) `canonical_reduction_order` indegree construction,
/// which sank 50K (~29 s per cold reduce) and left 10K only ~25% over budget.
/// Agent K's threshold/frontier rewrite (see the epiphany-ops DECISIONS
/// entry) is byte-identical in order and subquadratic; the gate's XPASS
/// notice triggered the 50K promotion recorded here.
///
/// If any row starts missing the budget again, that is a fresh regression:
/// fix the reducer, do not re-mark rows Xfail without a written decision.
const SCALE_POINTS: &[ScalePoint] = &[
ScalePoint {
n_ops: 1_000,
seed: 0x00F1_5EED_0001,
expectation: Expectation::Pass,
gate_iters: (9, 5),
criterion_time: Some(Duration::from_secs(6)),
},
ScalePoint {
n_ops: 10_000,
seed: 0x00F1_5EED_0002,
expectation: Expectation::Pass,
gate_iters: (3, 2),
criterion_time: Some(Duration::from_secs(20)),
},
ScalePoint {
n_ops: 50_000,
seed: 0x00F1_5EED_0003,
expectation: Expectation::Pass,
gate_iters: (1, 0),
criterion_time: None,
},
];
/// The scale point's reproducible envelope set — the criterion-5 session shape
/// (3 replicas, 40 events, 40 pitches) at `n_ops` envelopes.
fn envelopes_at(point: &ScalePoint) -> Vec<OperationEnvelope> {
let mut rng = Rng::new(point.seed);
generators::operation_envelopes(&mut rng, point.n_ops, 3, 40, 40)
}
/// The timed section: cold reduction from a fresh canonical base — build a
/// fresh set (acceptance) and reduce it. The envelope clone happens in the
/// caller's un-timed setup.
fn cold_reduce(envelopes: Vec<OperationEnvelope>) -> MaterializedState {
let mut set = OperationSet::new();
set.accept_all(envelopes);
set.reduce()
}
/// The criterion measurement side (envelopes/s via `Throughput::Elements`).
fn criterion_measurements(criterion: &mut Criterion, quick: bool) {
let mut group = criterion.benchmark_group("reduction_cold");
// Cold multi-second iterations at 10K: flat sampling, the minimum sample
// count, and per-point measurement times keep `cargo bench` wall-clock sane.
group.sampling_mode(SamplingMode::Flat);
group.sample_size(10);
for point in SCALE_POINTS {
let Some(time) = point.criterion_time else {
continue; // 50K is gate-only; see THE BUDGET TABLE.
};
if quick && point.n_ops > 1_000 {
continue; // quick mode: the gate still measures 10K, cheaply.
}
let envelopes = envelopes_at(point);
group.throughput(Throughput::Elements(point.n_ops as u64));
group.measurement_time(if quick { Duration::from_secs(2) } else { time });
group.warm_up_time(Duration::from_millis(if quick { 500 } else { 1500 }));
group.bench_with_input(
BenchmarkId::from_parameter(point.n_ops),
&envelopes,
|b, envs| b.iter_batched(|| envs.clone(), cold_reduce, BatchSize::PerIteration),
);
}
group.finish();
}
/// The budget-gate side: evaluates THE BUDGET TABLE and returns the rows.
fn budget_gate(quick: bool) -> Vec<budget::GateReport> {
let mut reports = Vec::new();
for point in SCALE_POINTS {
let iters = if quick {
point.gate_iters.1
} else {
point.gate_iters.0
};
if iters == 0 {
println!(
"skip reduction/{}: heaviest scale point; full/nightly runs only \
(unset EPIPHANY_BENCH_QUICK)",
point.n_ops
);
continue;
}
let envelopes = envelopes_at(point);
let median = budget::median_time(iters, || envelopes.clone(), cold_reduce);
reports.push(budget::rate_gate(
format!("reduction/{}", point.n_ops),
point.n_ops as u64,
median,
RATE_BUDGET_ENV_PER_SEC,
point.expectation,
));
}
reports
}
fn main() {
// `cargo bench` passes `--bench`; its absence means criterion's test mode
// (`cargo test --benches` / `--all-targets`): run each measurement once,
// skip the gate.
let bench_mode = std::env::args().any(|arg| arg == "--bench");
let quick = budget::quick_mode();
let mut criterion = Criterion::default().configure_from_args();
criterion_measurements(&mut criterion, quick);
criterion.final_summary();
if !bench_mode {
return;
}
if !budget::verdict(&budget_gate(quick)) {
std::process::exit(1);
}
}

View File

@ -0,0 +1,293 @@
//! The Chapter 10 performance-budget gate (Phase 2 worklist F1).
//!
//! The benches in `benches/` (home per `DECISIONS.md` F0) *measure* with
//! criterion; criterion never *asserts*, so each bench binary's `main()` ends
//! by running these gates: a calibrated timing check per budget row, with the
//! numeric threshold written at the call site in the bench source. Every row
//! carries an [`Expectation`]:
//!
//! * [`Expectation::Pass`] — the budget must hold **today**; a miss fails the
//! bench run with a nonzero exit (the CI tripwire).
//! * [`Expectation::Xfail`] — a documented, known-pending miss whose reason
//! names the defect and its owner. A miss prints an expected-failure line
//! and does **not** fail the run; a *pass* prints a promotion notice,
//! because the marking is then stale and must be flipped to `Pass`.
//!
//! This is the "F surfaces, K fixes" handshake (`spec/PHASE2_F_WEEK0_WORKLIST.md`
//! F1): a known-pending scale point stays `Xfail` — budget written in the
//! bench — until the named defect is fixed, at which point the gate itself
//! reports that the row should be promoted. The inaugural round completed: the
//! reducer's `O(n²)` `canonical_reduction_order` sank `reduction/50000` until
//! Agent K's subquadratic rewrite, whose XPASS notice promoted the row.
//!
//! ## Methodology note (a deliberate deviation from Chapter 10)
//!
//! Chapter 10's conformance methodology is **p99 over ≥ 1000 iterations per
//! scenario** on the reference hardware profile. That is the reference suite's
//! job, not this gate's: 1000 iterations of a minute-long 50K-envelope cold
//! reduction would be unusable in CI. The gate instead takes the **median of a
//! small, per-row calibrated iteration count** in a release build — enough to
//! reject flukes while keeping `cargo bench` wall-clock sane. Runs are cold
//! (no in-gate warm-up): the marquee budget is an explicitly *cold* reduction
//! rate, and the criterion measurements that precede the gate have already
//! warmed the allocator and caches for the warm-appropriate rows. Conformance
//! *claims* still require the full Chapter 10 methodology.
use std::time::{Duration, Instant};
/// How a budget row is expected to behave on the current implementation.
#[derive(Copy, Clone, Debug)]
pub enum Expectation {
/// The budget must hold; a miss fails the bench run (nonzero exit).
Pass,
/// A documented known-pending miss; the string names the defect and who
/// fixes it. A miss is reported but tolerated; a pass demands promotion.
Xfail(&'static str),
}
/// One evaluated budget row.
#[derive(Debug)]
pub struct GateReport {
/// The row label, e.g. `reduction/10000`.
pub label: String,
/// Human-readable `measured vs budget` detail.
pub detail: String,
/// Whether the measurement met the budget.
pub met_budget: bool,
/// The row's documented expectation.
pub expectation: Expectation,
}
impl GateReport {
/// A `Pass`-marked row that missed its budget — the only outcome that
/// fails the bench run.
pub fn unexpected_failure(&self) -> bool {
!self.met_budget && matches!(self.expectation, Expectation::Pass)
}
/// An `Xfail`-marked row that met its budget: the marking is stale and the
/// row should be promoted to `Pass`.
pub fn unexpected_pass(&self) -> bool {
self.met_budget && matches!(self.expectation, Expectation::Xfail(_))
}
/// The verdict line printed for this row.
pub fn line(&self) -> String {
match (self.met_budget, self.expectation) {
(true, Expectation::Pass) => format!("PASS {}: {}", self.label, self.detail),
(false, Expectation::Pass) => format!(
"FAIL {}: {} — budget missed on a Pass-marked row",
self.label, self.detail
),
(false, Expectation::Xfail(reason)) => format!(
"XFAIL {}: {} — expected failure: {}",
self.label, self.detail, reason
),
(true, Expectation::Xfail(reason)) => format!(
"XPASS {}: {} — met the budget despite the xfail marking ({}); \
PROMOTE this row to Pass",
self.label, self.detail, reason
),
}
}
}
/// The median over `iters` timed runs of `op`, with per-run input built by
/// `setup` **outside** the timed section (this is how a cold-reduction row
/// clones its envelope vector without the clone being charged to the budget).
/// The output is dropped outside the timed section too. `iters ≥ 1`.
pub fn median_time<S, T>(
iters: usize,
mut setup: impl FnMut() -> S,
mut op: impl FnMut(S) -> T,
) -> Duration {
assert!(iters >= 1, "a gate row needs at least one timed iteration");
let mut samples = Vec::with_capacity(iters);
for _ in 0..iters {
let input = setup();
let start = Instant::now();
let out = op(input);
let elapsed = start.elapsed();
std::hint::black_box(&out);
samples.push(elapsed);
drop(out);
}
samples.sort();
samples[samples.len() / 2]
}
/// Evaluates a throughput budget: `elements` per timed run must exceed
/// `budget_per_sec` (the Chapter 10 reduction-rate form, "MUST exceed").
pub fn rate_gate(
label: impl Into<String>,
elements: u64,
median: Duration,
budget_per_sec: f64,
expectation: Expectation,
) -> GateReport {
let rate = elements as f64 / median.as_secs_f64();
GateReport {
label: label.into(),
detail: format!(
"{rate:.0} elements/s ({elements} elements, median {median:.2?}); \
budget > {budget_per_sec:.0}/s"
),
met_budget: rate > budget_per_sec,
expectation,
}
}
/// Evaluates a latency budget: the median must come in at or under `budget`
/// (the Chapter 10 file-format form, "completes within").
pub fn latency_gate(
label: impl Into<String>,
median: Duration,
budget: Duration,
expectation: Expectation,
) -> GateReport {
GateReport {
label: label.into(),
detail: format!("median {median:.2?}; budget <= {budget:.0?}"),
met_budget: median <= budget,
expectation,
}
}
/// Prints every row's verdict and returns whether the gate holds — i.e. no
/// `Pass`-marked row missed its budget. The bench binary exits nonzero when
/// this returns `false`; `Xfail` misses and `XPASS` promotions never fail the
/// run (the latter print a loud promotion notice instead).
pub fn verdict(reports: &[GateReport]) -> bool {
println!("\n== Chapter 10 budget gate (worklist F1) ==");
for report in reports {
println!("{}", report.line());
}
let unexpected: Vec<&GateReport> = reports.iter().filter(|r| r.unexpected_failure()).collect();
let promotions = reports.iter().filter(|r| r.unexpected_pass()).count();
if promotions > 0 {
println!(
"note: {promotions} xfail row(s) met their budget — promote them to Pass \
(the marking is stale)."
);
}
if unexpected.is_empty() {
println!("budget gate: OK ({} row(s))", reports.len());
true
} else {
println!(
"budget gate: FAILED — {} Pass-marked row(s) missed their budget",
unexpected.len()
);
false
}
}
/// Whether the CI-friendly quick mode is on (`EPIPHANY_BENCH_QUICK=1`):
/// reduced criterion sampling, reduced gate iteration counts, and the heaviest
/// scale points (the 50K-envelope reduction row) skipped entirely. PR CI sets
/// it; the nightly soak and local full runs leave it unset.
pub fn quick_mode() -> bool {
std::env::var("EPIPHANY_BENCH_QUICK").is_ok_and(|v| !v.is_empty() && v != "0")
}
#[cfg(test)]
mod tests {
use super::*;
fn row(met_budget: bool, expectation: Expectation) -> GateReport {
GateReport {
label: "test/row".to_owned(),
detail: "detail".to_owned(),
met_budget,
expectation,
}
}
#[test]
fn pass_row_meeting_budget_holds() {
let r = row(true, Expectation::Pass);
assert!(!r.unexpected_failure());
assert!(!r.unexpected_pass());
assert!(verdict(&[r]));
}
#[test]
fn pass_row_missing_budget_fails_the_gate() {
let r = row(false, Expectation::Pass);
assert!(r.unexpected_failure());
assert!(r.line().starts_with("FAIL"));
assert!(!verdict(&[r]));
}
#[test]
fn xfail_row_missing_budget_is_tolerated() {
let r = row(false, Expectation::Xfail("documented defect"));
assert!(!r.unexpected_failure());
assert!(r.line().starts_with("XFAIL"));
assert!(r.line().contains("documented defect"));
assert!(verdict(&[r]));
}
#[test]
fn xfail_row_meeting_budget_demands_promotion_but_holds() {
let r = row(true, Expectation::Xfail("documented defect"));
assert!(r.unexpected_pass());
assert!(r.line().starts_with("XPASS"));
assert!(r.line().contains("PROMOTE"));
assert!(verdict(&[r]));
}
#[test]
fn gates_evaluate_their_thresholds() {
// 100 elements in 1 ms = 100,000/s.
let fast = rate_gate(
"rate/fast",
100,
Duration::from_millis(1),
10_000.0,
Expectation::Pass,
);
assert!(fast.met_budget);
let slow = rate_gate(
"rate/slow",
100,
Duration::from_millis(100),
10_000.0,
Expectation::Pass,
);
assert!(!slow.met_budget);
let ok = latency_gate(
"lat/ok",
Duration::from_millis(10),
Duration::from_millis(50),
Expectation::Pass,
);
assert!(ok.met_budget);
let over = latency_gate(
"lat/over",
Duration::from_millis(60),
Duration::from_millis(50),
Expectation::Pass,
);
assert!(!over.met_budget);
}
#[test]
fn median_time_takes_the_middle_sample() {
// Deterministic ordering check via a controlled op: the median of an
// odd sample count must be a real observed sample, not an average.
let mut calls = 0u32;
let d = median_time(
5,
|| (),
|()| {
calls += 1;
},
);
assert_eq!(calls, 5);
// No timing assertion (flaky); the structural property is that a
// duration was produced at all and the closure ran `iters` times.
let _ = d;
}
}

View File

@ -177,7 +177,7 @@ pub struct Fixture {
pub tier: Tier,
pub build: fn() -> Score,
/// Buckets this fixture must drive non-zero (validated per-fixture, and
/// aggregated for corpus coverage). For the [`UNUSUAL_BUCKETS`] this list is
/// aggregated for corpus coverage). For the crate-private `UNUSUAL_BUCKETS` this list is
/// also an *exact whitelist*: a fixture that lands an event in a
/// loss/deferral bucket it did not declare fails the harness.
pub expect: &'static [Bucket],

View File

@ -92,6 +92,11 @@
pub mod rng;
// Phase 2, Agent F (worklist F1): the Chapter 10 performance-budget gate the
// `benches/` targets assert through (Pass / Xfail rows with thresholds written
// in the bench source; see `DECISIONS.md` F0/F1).
pub mod budget;
pub mod fixtures;
pub mod generators;
pub mod roundtrip;

View File

@ -52,10 +52,10 @@ code instead is the failure mode this batch exists to prevent.
| P12-I4 | `epiphany-layout-ir` I | Constraint-strength attachment: Ch9 defines `ConstraintStrength` and says the solver consumes constraints "in normalized form", but neither the normalized form nor Ch7's `LayoutConstraint` provides a channel for an instance to carry strength. Implemented rule: break strength = `BreakKind` (Hard→Required, Soft→Preferred{1.0}); other core families Required; `Registered` conservative Required. Bless the rule or add a strength field. | G / Pass 12 (solver) |
| P12-I5 | `epiphany-layout-ir` I | No renderable status exists for "constraints present but not evaluated": every renderable `SolveStatus` is documented as "all hard constraints satisfied", leaving a below-conformance passthrough solver no honest report. Implemented encoding: `SolvedWithWarnings` + `satisfied_hard_constraints == false` + a warning. Sanction it or define a non-evaluating-tier report shape. | G / Pass 12 (solver) |
| P12-I6 | `epiphany-layout-ir` I | The spacing pass MUST "build collision constraints" but no per-tier minimum emission set is named. Implemented floor: successive-notehead-column no-collision chains + per-glyph region containment + user-break constraints. A normative Minimal-tier floor would make the acceptance surface testable. | G / Pass 12 (solver) |
| P12-D1 | `epiphany-bundle` D | Operation-index provisional encoding (block-refs + id-sorted entries with u32 block ordinal and u32 in-block offset; golden-locked) awaiting Binary Format companion ratification, together with: the offset's meaning (first content byte within the uncompressed block payload), a normative definition of "stale" (implemented: index block-set ≠ manifest `operation_roots` under full-`ChunkRef` equality), the one-slot-per-id invariant, the load-bearing property that the envelope encoding *leads* with the 16-byte OperationId, and whether the commit-time "grown significantly" SHOULD gets a threshold or stays implementation-defined. | J (Binary Format companion) |
| P12-E1 | `epiphany-layout-ir` E | Provisional canonical byte form for the `EditBarrier`/`BarrierScope`/`BarrierCondition` tree and the two `ExtensionDeclaration` blobs (`push_set` framing, u64 LE lengths; golden-locked). Ratify into the Binary Format companion. | J (Binary Format companion) |
| P12-E2 | `epiphany-layout-ir` E | The spec places no bound on `BarrierCondition` recursion; the decoder needs one against adversarial bytes. `MAX_CONDITION_DEPTH = 64` implemented — ratify a normative bound or bless the constant. | J (Binary Format companion) |
| P12-E3 | `epiphany-layout-ir` E | Barrier `ObjectKind` byte form = the `TypedObjectId` 16-bit discriminant (2 LE bytes) with open-value decode (unknown kinds never match, preserving append-only forward compat). Ratify representation + stance. | J (Binary Format companion) |
| ~~P12-D1~~ **RESOLVED (Binary Format 0.1.0 §7.6, `req:binfmt:opindex`)** | `epiphany-bundle` D | Operation-index provisional encoding (block-refs + id-sorted entries with u32 block ordinal and u32 in-block offset; golden-locked) awaiting Binary Format companion ratification, together with: the offset's meaning (first content byte within the uncompressed block payload), a normative definition of "stale" (implemented: index block-set ≠ manifest `operation_roots` under full-`ChunkRef` equality), the one-slot-per-id invariant, the load-bearing property that the envelope encoding *leads* with the 16-byte OperationId, and whether the commit-time "grown significantly" SHOULD gets a threshold or stays implementation-defined. Ratified as-implemented; refresh threshold pinned implementation-defined (open question retained in the companion). | ✅ done |
| ~~P12-E1~~ **RESOLVED (Binary Format 0.1.0 §8.1, `req:binfmt:ext-blobs`)** | `epiphany-layout-ir` E | Provisional canonical byte form for the `EditBarrier`/`BarrierScope`/`BarrierCondition` tree and the two `ExtensionDeclaration` blobs (`push_set` framing, u64 LE lengths; golden-locked). Ratified as-implemented. | ✅ done |
| ~~P12-E2~~ **RESOLVED (Binary Format 0.1.0 §8.2, `req:binfmt:condition-depth`)** | `epiphany-layout-ir` E | The spec places no bound on `BarrierCondition` recursion; the decoder needs one against adversarial bytes. `MAX_CONDITION_DEPTH = 64` implemented — the companion pins 64 as the normative bound (decoders MUST reject deeper; writers MUST NOT emit deeper). | ✅ done |
| ~~P12-E3~~ **RESOLVED (Binary Format 0.1.0 §8.1, `req:binfmt:object-kind-open`)** | `epiphany-layout-ir` E | Barrier `ObjectKind` byte form = the `TypedObjectId` 16-bit discriminant (2 LE bytes) with open-value decode (unknown kinds never match, preserving append-only forward compat). Representation and open-value stance ratified. | ✅ done |
| P12-E4 | `epiphany-editor-core` E | Barrier matching for operations with no graph target (`SetMetadata`, `DeclareTransaction` — implemented: score-wide barriers only) and for opaque `Registered` operations (implemented: fully conservative match) is unspecified. | G (Ch. 8) |
| P12-E5 | `epiphany-editor-core` E | The unsafe-edit tombstone MUST has no defined mechanism: the manifest-side form (drop declaration + preserved roots? an explicit tombstone record?), interaction with `required = true`, and whether crossing immediately deactivates the extension's remaining barriers (implemented: yes, recorded via `extensions_requiring_tombstone()` for the next bundle write). | G (Ch. 8) |
@ -65,7 +65,10 @@ Agent I (Track A) has contributed P12-I1..I6. Track B's Agent K has
contributed P12-K1..K7; H has contributed P12-H1..H7 (H6/H7 from the 2026-07
spec-compliance audit follow-up, alongside K3/K4). The 2026-07 Push-3 wiring
work added C1..C4 (re-anchoring), D1 (bundle operation index), and E1..E5
(edit barriers). Agent J (Binary Format companion) has not yet contributed;
when it does, append rows — the batch is already open, so it joins directly
(no new threshold). Note the P12-D1/E1/E2/E3 rows are *inputs* to J's
companion rather than G-dispositions.
(edit barriers). Agent J's Binary Format companion now exists
(`spec/binary_format.tex`, v0.1.0): it ratified the P12-D1/E1/E2/E3 inputs
(struck through above) and discharged the crates' provisional-codec notes
(core P11-4, ops "provisional canonical encoding", bundle P11-D2/D4/D5). Its
three open questions (SnapshotId derivation, index-refresh threshold, u64/u32
prefix unification at the next schema major) live in the companion itself, not
as batch rows.

BIN
spec/binary_format.pdf Normal file

Binary file not shown.

2525
spec/binary_format.tex Normal file

File diff suppressed because it is too large Load Diff