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:
parent
92aaccf7e2
commit
3e91a8302a
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)`
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 Fisher–Yates 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![
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
//!
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue