diff --git a/crates/warp-core/src/engine_impl.rs b/crates/warp-core/src/engine_impl.rs index 12207c96..d13474ee 100644 --- a/crates/warp-core/src/engine_impl.rs +++ b/crates/warp-core/src/engine_impl.rs @@ -194,7 +194,8 @@ impl Engine { /// Executes all pending rewrites for the transaction, producing both a snapshot and a tick receipt. /// /// The receipt records (in canonical plan order) which candidates were accepted vs rejected. - /// This is the first step toward Paper II “tick receipts” (and later, the blocking poset). + /// For rejected candidates, it also records which earlier applied candidates blocked them + /// (a minimal blocking-causality witness / poset edge list, per Paper II). /// /// # Errors /// - Returns [`EngineError::UnknownTx`] if `tx` does not refer to a live transaction. @@ -209,21 +210,40 @@ impl Engine { } // Drain pending to form the ready set and compute a plan digest over its canonical order. let drained = self.scheduler.drain_for_tx(tx); - let plan_digest = { - let mut hasher = blake3::Hasher::new(); - hasher.update(&(drained.len() as u64).to_le_bytes()); - for pr in &drained { - hasher.update(&pr.scope_hash); - hasher.update(&pr.rule_id); - } - hasher.finalize().into() - }; + let plan_digest = compute_plan_digest(&drained); // Reserve phase: enforce independence against active frontier. let mut receipt_entries: Vec = Vec::with_capacity(drained.len()); + let mut blocked_by: Vec> = Vec::with_capacity(drained.len()); let mut reserved: Vec = Vec::new(); - for mut rewrite in drained { + let mut reserved_entry_indices: Vec = Vec::new(); + for (entry_idx, mut rewrite) in drained.into_iter().enumerate() { + let entry_idx_u32 = u32::try_from(entry_idx).map_err(|_| { + EngineError::InternalCorruption("too many receipt entries to index") + })?; let accepted = self.scheduler.reserve(tx, &mut rewrite); + let blockers = if accepted { + Vec::new() + } else { + // O(n) scan over reserved rewrites. Acceptable for typical tick sizes; + // consider spatial indexing if tick candidate counts grow large. + let mut blockers: Vec = Vec::new(); + for (k, prior) in reserved.iter().enumerate() { + if footprints_conflict(&rewrite.footprint, &prior.footprint) { + blockers.push(reserved_entry_indices[k]); + } + } + if blockers.is_empty() { + // `reserve()` currently returns `false` exclusively on footprint + // conflicts (see scheduler reserve rustdoc). If additional rejection + // reasons are added, update the scheduler contract and this attribution + // logic accordingly. + return Err(EngineError::InternalCorruption( + "scheduler rejected rewrite but no blockers were found", + )); + } + blockers + }; receipt_entries.push(TickReceiptEntry { rule_id: rewrite.rule_id, scope_hash: rewrite.scope_hash, @@ -231,28 +251,22 @@ impl Engine { disposition: if accepted { TickReceiptDisposition::Applied } else { - // NOTE: reserve() currently only rejects for footprint conflicts. + // NOTE: reserve() currently returns `false` exclusively on + // footprint conflicts (see scheduler reserve rustdoc). // If additional rejection reasons are added, update this mapping. TickReceiptDisposition::Rejected(TickReceiptRejection::FootprintConflict) }, }); if accepted { reserved.push(rewrite); + reserved_entry_indices.push(entry_idx_u32); } + blocked_by.push(blockers); } - let receipt = TickReceipt::new(tx, receipt_entries); + let receipt = TickReceipt::new(tx, receipt_entries, blocked_by); // Deterministic digest of the ordered rewrites we will apply. - let rewrites_digest = { - let mut hasher = blake3::Hasher::new(); - hasher.update(&(reserved.len() as u64).to_le_bytes()); - for r in &reserved { - hasher.update(&r.rule_id); - hasher.update(&r.scope_hash); - hasher.update(&(r.scope).0); - } - hasher.finalize().into() - }; + let rewrites_digest = compute_rewrites_digest(&reserved); for rewrite in reserved { let id = rewrite.compact_rule; @@ -353,6 +367,67 @@ impl Engine { } } +fn footprints_conflict(a: &crate::footprint::Footprint, b: &crate::footprint::Footprint) -> bool { + // IMPORTANT: do not use `Footprint::independent` here yet. + // + // This logic MUST remain consistent with the scheduler’s footprint conflict + // predicate (`RadixScheduler::has_conflict` in `scheduler.rs`). If one + // changes, the other must change too, or receipts will attribute blockers + // differently than the scheduler rejects candidates. + // + // `Footprint::independent` includes a `factor_mask` fast-path that assumes + // masks are correctly populated as a conservative superset. Many current + // footprints in the engine spike use `factor_mask = 0` as a placeholder, + // which would incorrectly classify conflicting rewrites as independent. + // + // The scheduler’s conflict logic is defined by explicit overlap checks on + // nodes/edges/ports; this mirrors that behavior exactly and stays correct + // while factor masks are still being wired through. + if a.b_in.intersects(&b.b_in) + || a.b_in.intersects(&b.b_out) + || a.b_out.intersects(&b.b_in) + || a.b_out.intersects(&b.b_out) + { + return true; + } + if a.e_write.intersects(&b.e_write) + || a.e_write.intersects(&b.e_read) + || b.e_write.intersects(&a.e_read) + { + return true; + } + a.n_write.intersects(&b.n_write) + || a.n_write.intersects(&b.n_read) + || b.n_write.intersects(&a.n_read) +} + +fn compute_plan_digest(plan: &[PendingRewrite]) -> Hash { + if plan.is_empty() { + return *crate::constants::DIGEST_LEN0_U64; + } + let mut hasher = Hasher::new(); + hasher.update(&(plan.len() as u64).to_le_bytes()); + for pr in plan { + hasher.update(&pr.scope_hash); + hasher.update(&pr.rule_id); + } + hasher.finalize().into() +} + +fn compute_rewrites_digest(rewrites: &[PendingRewrite]) -> Hash { + if rewrites.is_empty() { + return *crate::constants::DIGEST_LEN0_U64; + } + let mut hasher = Hasher::new(); + hasher.update(&(rewrites.len() as u64).to_le_bytes()); + for r in rewrites { + hasher.update(&r.rule_id); + hasher.update(&r.scope_hash); + hasher.update(&(r.scope).0); + } + hasher.finalize().into() +} + impl Engine { fn rule_by_compact(&self, id: CompactRuleId) -> Option<&RewriteRule> { let name = self.rules_by_compact.get(&id)?; diff --git a/crates/warp-core/src/receipt.rs b/crates/warp-core/src/receipt.rs index f17cf311..26f1f957 100644 --- a/crates/warp-core/src/receipt.rs +++ b/crates/warp-core/src/receipt.rs @@ -24,15 +24,22 @@ use crate::tx::TxId; pub struct TickReceipt { tx: TxId, entries: Vec, + blocked_by: Vec>, digest: Hash, } impl TickReceipt { - pub(crate) fn new(tx: TxId, entries: Vec) -> Self { + pub(crate) fn new(tx: TxId, entries: Vec, blocked_by: Vec>) -> Self { + assert_eq!( + entries.len(), + blocked_by.len(), + "blocked_by must be parallel to entries" + ); let digest = compute_tick_receipt_digest(&entries); Self { tx, entries, + blocked_by, digest, } } @@ -49,6 +56,35 @@ impl TickReceipt { &self.entries } + /// Returns the indices of the candidates that blocked entry `idx`. + /// + /// This is the (currently minimal) *blocking causality poset* witness described + /// by Paper II: when a candidate is rejected due to a footprint conflict, the + /// receipt records which already-applied candidates blocked it. + /// + /// Semantics and invariants: + /// - Returned indices are indices into [`TickReceipt::entries`]. + /// - The list is sorted in ascending order and contains no duplicates. + /// - Every returned index is strictly less than `idx`. + /// - For an [`TickReceiptDisposition::Applied`] entry, the list is empty. + /// - For a [`TickReceiptDisposition::Rejected`] entry, the list is expected to + /// be non-empty for `Rejected(FootprintConflict)`. + /// + /// Note: these blocker indices are *not* included in [`TickReceipt::digest`] + /// today; the digest commits only to accepted vs rejected outcomes. + /// + /// # Panics + /// Panics if `idx` is out of bounds for [`TickReceipt::entries`]. + #[must_use] + pub fn blocked_by(&self, idx: usize) -> &[u32] { + assert!( + idx < self.blocked_by.len(), + "blocked_by index {idx} out of bounds for {} entries", + self.blocked_by.len() + ); + &self.blocked_by[idx] + } + /// Canonical digest of the tick receipt entries. /// /// This digest is stable across architectures and depends only on: @@ -56,6 +92,10 @@ impl TickReceipt { /// - the number of entries, and /// - the ordered per-entry content. /// + /// It intentionally does **not** include blocking attribution metadata + /// (see [`TickReceipt::blocked_by`]) so that commit hashes remain stable + /// across improvements to rejection explanations. + /// /// It intentionally does **not** include `tx` so that receipts can be /// compared across runs that use different transaction numbering. #[must_use] diff --git a/crates/warp-core/src/scheduler.rs b/crates/warp-core/src/scheduler.rs index f2062494..d3c5de0a 100644 --- a/crates/warp-core/src/scheduler.rs +++ b/crates/warp-core/src/scheduler.rs @@ -117,6 +117,16 @@ impl RadixScheduler { /// /// On success, marks all resources in the active `GenSets` and transitions /// the phase to `Reserved`. + /// + /// Return value contract (engine spike): + /// - Returns `true` when the rewrite is reserved and will be applied. + /// - Returns `false` exclusively when the rewrite footprint conflicts with + /// the already-reserved frontier for this tick. In this case the rewrite + /// phase is transitioned to `Aborted`. + /// + /// If additional rejection reasons are introduced in the future (beyond + /// footprint conflicts), upgrade the return type to an explicit reason enum + /// so callers can distinguish between them. pub(crate) fn reserve(&mut self, tx: TxId, pr: &mut PendingRewrite) -> bool { let active = self.active.entry(tx).or_insert_with(ActiveFootprints::new); @@ -543,6 +553,22 @@ impl LegacyScheduler { .unwrap_or_default() } + /// Attempts to reserve a rewrite by checking full footprint independence + /// against the currently reserved frontier. + /// + /// This legacy implementation performs an O(k) scan over the reserved + /// footprints for the tick, using [`Footprint::independent`] to detect + /// conflicts. + /// + /// Return value contract (engine spike): + /// - Returns `true` when the rewrite is reserved and will be applied. + /// - Returns `false` exclusively when the rewrite footprint conflicts with + /// the already-reserved frontier for this tick. In this case the rewrite + /// phase is transitioned to `Aborted`. + /// + /// If additional rejection reasons are introduced in the future (beyond + /// footprint conflicts), upgrade the return type to an explicit reason enum + /// so callers can distinguish between them. pub(crate) fn reserve(&mut self, tx: TxId, pr: &mut PendingRewrite) -> bool { let frontier = self.active.entry(tx).or_default(); for fp in frontier.iter() { @@ -633,6 +659,19 @@ impl DeterministicScheduler { } } + /// Attempts to reserve `pr` in the scheduler for `tx`. + /// + /// This forwards to the selected scheduler implementation (radix vs legacy). + /// + /// Return value contract (engine spike): + /// - Returns `true` when the rewrite is reserved and will be applied. + /// - Returns `false` exclusively when the rewrite footprint conflicts with + /// the already-reserved frontier for this tick. In this case the rewrite + /// phase is transitioned to `Aborted`. + /// + /// If additional rejection reasons are introduced in the future (beyond + /// footprint conflicts), upgrade the return type to an explicit reason enum + /// so callers can distinguish between them. pub(crate) fn reserve(&mut self, tx: TxId, pr: &mut PendingRewrite) -> bool { match &mut self.inner { SchedulerImpl::Radix(s) => s.reserve(tx, pr), diff --git a/crates/warp-core/tests/tick_receipt_tests.rs b/crates/warp-core/tests/tick_receipt_tests.rs index 8be87963..758fa75c 100644 --- a/crates/warp-core/tests/tick_receipt_tests.rs +++ b/crates/warp-core/tests/tick_receipt_tests.rs @@ -4,9 +4,9 @@ #![allow(missing_docs)] use warp_core::{ - encode_motion_payload, make_node_id, make_type_id, Engine, GraphStore, Hash, NodeRecord, - RewriteRule, TickReceiptDisposition, TickReceiptEntry, TickReceiptRejection, TxId, - MOTION_RULE_NAME, + encode_motion_payload, make_node_id, make_type_id, ConflictPolicy, Engine, Footprint, + GraphStore, Hash, NodeId, NodeRecord, PatternGraph, RewriteRule, TickReceiptDisposition, + TickReceiptEntry, TickReceiptRejection, TxId, MOTION_RULE_NAME, }; fn rule_id(name: &str) -> Hash { @@ -16,6 +16,15 @@ fn rule_id(name: &str) -> Hash { hasher.finalize().into() } +// Mirrors the engine implementation in `crates/warp-core/src/engine_impl.rs`. +// If the engine's scope hash semantics change, this helper must be updated to match. +fn scope_hash(rule_id: &Hash, scope: &NodeId) -> Hash { + let mut hasher = blake3::Hasher::new(); + hasher.update(rule_id); + hasher.update(&scope.0); + hasher.finalize().into() +} + fn compute_plan_digest(entries: &[TickReceiptEntry]) -> Hash { let mut hasher = blake3::Hasher::new(); hasher.update(&(entries.len() as u64).to_le_bytes()); @@ -43,6 +52,75 @@ fn compute_rewrites_digest(entries: &[TickReceiptEntry]) -> Hash { hasher.finalize().into() } +fn always_match(_: &GraphStore, _: &NodeId) -> bool { + true +} + +fn exec_noop(_: &mut GraphStore, _: &NodeId) {} + +fn other_of(scope: &NodeId) -> NodeId { + NodeId(blake3::hash(&scope.0).into()) +} + +fn fp_write_scope(_: &GraphStore, scope: &NodeId) -> Footprint { + let mut fp = Footprint::default(); + fp.n_write.insert_node(scope); + fp.factor_mask = 1; + fp +} + +fn fp_write_scope_and_other(_: &GraphStore, scope: &NodeId) -> Footprint { + let mut fp = Footprint::default(); + fp.n_write.insert_node(scope); + fp.n_write.insert_node(&other_of(scope)); + fp.factor_mask = 1; + fp +} + +/// Finds three synthetic rule ids (A, B, C) such that when applied to +/// `(scope_a, scope_b, scope_a)` the deterministic scheduler order is `[A, B, C]` +/// with `C` last. +/// +/// This ensures the first two candidates are accepted and the last candidate is +/// rejected with two blockers for stable multi-blocker assertions. +fn pick_rule_ids_for_blocker_test(scope_a: &NodeId, scope_b: &NodeId) -> (Hash, Hash, Hash) { + // Pick three distinct synthetic ids such that: + // - the combined-write rule (C) sorts last by scope_hash + // - the two single-write rules (A, B) sort before it + // + // This ensures the first two candidates are accepted and the last candidate + // is rejected with *two* blockers, making the test stable even if rule IDs + // or scope hashing semantics evolve. + let mut candidates: Vec<(Hash, u8)> = (0u8..=255) + .map(|b| { + let id = [b; 32]; + (scope_hash(&id, scope_a), b) + }) + .collect(); + candidates.sort_by(|(ha, ba), (hb, bb)| ha.cmp(hb).then(ba.cmp(bb))); + + for (_, c_byte) in candidates.iter().rev() { + let c_id: Hash = [*c_byte; 32]; + let h_c = scope_hash(&c_id, scope_a); + + let Some((_, a_byte)) = candidates.iter().find(|(h, b)| *b != *c_byte && *h < h_c) else { + continue; + }; + let a_id: Hash = [*a_byte; 32]; + + let b_byte = (0u8..=255) + .find(|b| *b != *c_byte && *b != *a_byte && scope_hash(&[*b; 32], scope_b) < h_c); + let Some(b_byte) = b_byte else { + continue; + }; + let b_id: Hash = [b_byte; 32]; + + return (a_id, b_id, c_id); + } + + panic!("failed to find stable test ids for blocker ordering"); +} + #[test] fn commit_with_receipt_records_accept_reject_and_matches_snapshot_digests() { let entity = make_node_id("tick-receipt-entity"); @@ -102,6 +180,15 @@ fn commit_with_receipt_records_accept_reject_and_matches_snapshot_digests() { entries[1].disposition, TickReceiptDisposition::Rejected(TickReceiptRejection::FootprintConflict) ); + assert!( + receipt.blocked_by(0).is_empty(), + "applied entries should not have blockers" + ); + assert_eq!( + receipt.blocked_by(1), + &[0], + "the rejected candidate should be blocked by the applied candidate" + ); assert_eq!(snapshot.plan_digest, compute_plan_digest(entries)); assert_eq!(snapshot.rewrites_digest, compute_rewrites_digest(entries)); @@ -112,3 +199,88 @@ fn commit_with_receipt_records_accept_reject_and_matches_snapshot_digests() { "non-empty tick receipt should not use the canonical empty digest" ); } + +#[test] +fn commit_with_receipt_records_multi_blocker_causality() { + let scope_a = make_node_id("tick-receipt-poset-a"); + let scope_b = other_of(&scope_a); + assert_ne!(scope_a, scope_b, "expected distinct nodes for the test"); + + let ty = make_type_id("entity"); + let mut store = GraphStore::default(); + store.insert_node(scope_a, NodeRecord { ty, payload: None }); + store.insert_node(scope_b, NodeRecord { ty, payload: None }); + + let mut engine = Engine::new(store, scope_a); + + let (id_a, id_b, id_c) = pick_rule_ids_for_blocker_test(&scope_a, &scope_b); + + const RULE_A: &str = "test/write-scope-a"; + const RULE_B: &str = "test/write-scope-b"; + const RULE_C: &str = "test/write-scope-a-and-b"; + + engine + .register_rule(RewriteRule { + id: id_a, + name: RULE_A, + left: PatternGraph { nodes: vec![] }, + matcher: always_match, + executor: exec_noop, + compute_footprint: fp_write_scope, + factor_mask: 1, + conflict_policy: ConflictPolicy::Abort, + join_fn: None, + }) + .expect("rule A registers"); + engine + .register_rule(RewriteRule { + id: id_b, + name: RULE_B, + left: PatternGraph { nodes: vec![] }, + matcher: always_match, + executor: exec_noop, + compute_footprint: fp_write_scope, + factor_mask: 1, + conflict_policy: ConflictPolicy::Abort, + join_fn: None, + }) + .expect("rule B registers"); + engine + .register_rule(RewriteRule { + id: id_c, + name: RULE_C, + left: PatternGraph { nodes: vec![] }, + matcher: always_match, + executor: exec_noop, + compute_footprint: fp_write_scope_and_other, + factor_mask: 1, + conflict_policy: ConflictPolicy::Abort, + join_fn: None, + }) + .expect("rule C registers"); + + let tx = engine.begin(); + engine.apply(tx, RULE_A, &scope_a).expect("apply A"); + engine.apply(tx, RULE_B, &scope_b).expect("apply B"); + engine.apply(tx, RULE_C, &scope_a).expect("apply C"); + + let (_snapshot, receipt) = engine.commit_with_receipt(tx).expect("commit_with_receipt"); + let entries = receipt.entries(); + assert_eq!(entries.len(), 3); + + assert_eq!(entries[2].rule_id, id_c, "combined write should sort last"); + assert_eq!(entries[0].disposition, TickReceiptDisposition::Applied); + assert_eq!(entries[1].disposition, TickReceiptDisposition::Applied); + assert_eq!( + entries[2].disposition, + TickReceiptDisposition::Rejected(TickReceiptRejection::FootprintConflict) + ); + + assert!(receipt.blocked_by(0).is_empty()); + assert!(receipt.blocked_by(1).is_empty()); + assert_eq!( + receipt.blocked_by(2), + &[0, 1], + "combined write should be blocked by both prior applied candidates" + ); +} diff --git a/docs/aion-papers-bridge.md b/docs/aion-papers-bridge.md index 9c77ffec..4e513d28 100644 --- a/docs/aion-papers-bridge.md +++ b/docs/aion-papers-bridge.md @@ -2,7 +2,7 @@ # AIΩN Foundations → Echo: Bridge -Last reviewed: 2025-12-28. +Last reviewed: 2025-12-29. This doc maps the **AIΩN Foundations series** (“WARP Graphs”, Papers I–VI) onto the **Echo** repository as it exists today. @@ -60,7 +60,7 @@ These tables are intentionally “backlog-driving”: they identify what exists | Tick = atomic commit (all-or-nothing) | Implemented for the spike (`commit` finalizes tx) | `crates/warp-core/src/engine_impl.rs` | Make abort/stutter semantics explicit if/when partial failure exists (currently “reserve rejects conflicts”) | Echo currently models conflicts as “not reserved”; explicit abort receipts are a good future addition. | | Independence via footprints (delete/use; read/write sets) | Implemented (expanded to nodes/edges/ports + factor mask) | `crates/warp-core/src/footprint.rs`, `crates/warp-core/src/scheduler.rs` | Ensure footprint semantics remain “Paper II compatible” as optimizations land (bitmaps/SIMD, etc.) | Echo adds boundary ports + factor masks for engine practicality; document as an extension of the footprint idea. | | Deterministic scheduling via total key order (“left-most wins”) | Implemented (deterministic ordering + deterministic reserve filter) | `crates/warp-core/src/scheduler.rs` | Specify the canonical key format (what exactly is “scope”?); keep stable across releases | Echo’s key is currently (`scope_hash`, `rule_id`, `nonce`); may evolve, but must remain deterministic. | -| Tick receipts (accepted vs rejected + blocking poset) | Implemented (minimal receipt; poset pending) | `crates/warp-core/src/receipt.rs`, `crates/warp-core/src/engine_impl.rs`, `docs/spec-merkle-commit.md` | Extend receipts with blocking attribution (poset) + richer rejection reasons once conflict policy/join semantics land | Current receipt captures accepted vs rejected in canonical plan order; only rejection reason today is footprint conflict. | +| Tick receipts (accepted vs rejected + blocking poset) | Implemented (receipt + blocking attribution; richer reasons pending) | `crates/warp-core/src/receipt.rs`, `crates/warp-core/src/engine_impl.rs`, `docs/spec-merkle-commit.md` | Decide when/if to commit blocking edges into the hash and extend receipts with richer rejection reasons once conflict policy/join semantics land | Receipt captures accepted vs rejected in canonical plan order and records blockers (poset edges) for footprint conflicts; only rejection reason today is footprint conflict. | ### Paper III — Holography (payloads, BTRs, wormholes) @@ -135,7 +135,7 @@ These tables are intentionally “backlog-driving”: they identify what exists **Notable gap (intentional/expected):** -- Echo’s current engine exposes “plan_digest” / “rewrites_digest”, but does not yet expose a first-class “tick receipt poset” structure for debugging/provenance the way Paper II describes. +- Echo’s current engine exposes “plan_digest” / “rewrites_digest”, and now exposes a minimal tick receipt blocking witness: for rejected candidates, the receipt lists which earlier applied candidates blocked it (indices in canonical plan order). ## Paper III — Computational holography: provenance payloads, BTRs, wormholes diff --git a/docs/decision-log.md b/docs/decision-log.md index 5ad6bd9a..94a5a569 100644 --- a/docs/decision-log.md +++ b/docs/decision-log.md @@ -6,6 +6,7 @@ | Date | Context | Decision | Rationale | Consequence | | ---- | ------- | -------- | --------- | ----------- | +| 2025-12-29 | Paper II tick receipts: blocking causality | Extend `TickReceipt` to record a blocking-causality witness for `Rejected(FootprintConflict)` entries (indices of the applied candidates that blocked it, in canonical plan order); keep `decision_digest` encoding stable (digest commits only to accept/reject outcomes, not blocker metadata). | Paper II’s receipts are meant to explain *why* candidates were rejected and support deterministic debugging/provenance. Blocker indices provide a minimal poset edge list without destabilizing commit hashes as explanations evolve. | Tools/tests can surface “blocked by …” explanations; receipts now carry poset edges for footprint conflicts; commit ids remain stable for identical accept/reject outcomes. | | 2025-12-28 | AIΩN bridge doc + Paper II tick receipts | Promote the AIΩN Foundations bridge from `docs/notes/` into a canonical doc (`docs/aion-papers-bridge.md`, keeping a stub for historical links); implement `TickReceipt` + `Engine::commit_with_receipt` in `warp-core` and commit the receipt digest via `Snapshot.decision_digest` (encoding defined in `docs/spec-merkle-commit.md`). | The bridge is a long-lived architectural dependency and should not be buried in dated notes; Paper II tick receipts are required for deterministic debugging and provenance. Reusing `decision_digest` commits to accepted/rejected outcomes without changing the commit header shape. | A stable bridge doc is indexed in `docs/docs-index.md`; tools/tests can obtain receipts from `commit_with_receipt`. Commit ids now incorporate receipt outcomes whenever the candidate set is non-empty (empty receipts still use the canonical empty digest). | | 2025-12-28 | Brand asset variants | Add `assets/echo-white-radial.svg`: an `echo-white.svg`-derived logo variant with a shared radial gradient fill (`#F5E6AD` center → `#F13C77` edges) and an interior stroke emulated via clipped stroke paths for broad SVG renderer support. | Provide a ready-to-use, style-forward logo asset while keeping the original `echo-white.svg` unchanged; prefer a clip-path “inner stroke” technique over `stroke-alignment` to avoid uneven renderer support. | Consumers can pick the original flat-white logo or the gradient+inner-stroke variant without changing code; the new SVG renders consistently in common SVG engines (rsvg/InkScape). | | 2025-12-28 | Repo tour + onboarding refresh | Replace the root README with a reality-aligned overview, link Echo to the AIΩN Framework + the Foundations series papers, and add durable tour/bridge notes; fix `spec-merkle-commit` empty-digest semantics to match `warp-core`. | Keep onboarding and determinism specs consistent with the implemented engine contracts and the motivating research lineage; reduce future churn caused by doc drift. | New `README.md`, `docs/notes/project-tour-2025-12-28.md`, and `docs/notes/aion-papers-bridge.md` provide a single entry path; the commit digest spec now matches code behavior. | diff --git a/docs/execution-plan.md b/docs/execution-plan.md index 95403f83..24ef1734 100644 --- a/docs/execution-plan.md +++ b/docs/execution-plan.md @@ -35,6 +35,13 @@ This is Codex’s working map for building Echo. Update it relentlessly—each s ## Today’s Intent +> 2025-12-29 — Tick receipts: blocking causality (COMPLETED) + +- Goal: finish the Paper II tick receipt slice by recording *blocking causality* for rejected candidates (a poset edge list) in `warp-core`. +- Scope: extend `TickReceipt` to expose the applied candidates that blocked a `Rejected(FootprintConflict)` entry; keep `decision_digest` stable (digest commits only to accept/reject outcomes, not blocker metadata). +- Exit criteria: new tests cover multi-blocker cases; `cargo test --workspace` and `cargo clippy --workspace --all-targets -- -D warnings -D missing_docs` are green; bridge docs updated to match implementation; decision log entry recorded. +- Evidence: implemented `TickReceipt::blocked_by` and blocker attribution in `Engine::commit_with_receipt`; added multi-blocker tests; updated `docs/aion-papers-bridge.md` and `docs/spec-merkle-commit.md`; validated via `cargo test --workspace` + `cargo clippy --workspace --all-targets -- -D warnings -D missing_docs`. + > 2025-12-28 — Promote AIΩN bridge doc + add tick receipts (COMPLETED) - Goal: promote the AIΩN Foundations ↔ Echo bridge from a dated note into a canonical doc, then implement Paper II “tick receipts” in `warp-core`. diff --git a/docs/spec-merkle-commit.md b/docs/spec-merkle-commit.md index 2307cb2a..b89fb7f0 100644 --- a/docs/spec-merkle-commit.md +++ b/docs/spec-merkle-commit.md @@ -64,6 +64,11 @@ Canonical encoding (v1) for the tick receipt digest: - `1` = Applied - `2` = Rejected(FootprintConflict) +Note: `TickReceipt` may expose additional debugging/provenance metadata (e.g. a +blocking-causality witness for rejections). `decision_digest` v1 intentionally +commits only to accepted vs rejected outcomes (and the coarse rejection code), +not to the blocker metadata. + ## 3. Invariants and Notes - Any change to ordering, lengths, or endianness breaks all prior hashes.