Write

//! Precedence-guarded context store (M2b T4, KNOWN-HAZARDS 6.5; distributed
//! marker since M4-D6b).
//!
//! Every context write carries a **precedence marker** as the file's first
//! line — parse/render and the cross-node merge math live in
//! [`spt_store::contextmark`] (the sync path shares them); this module owns the
//! **local guard + the guarded file write**:
//!
//! ```text
//! <!-- spt:source=llm spt:routed_at_ms=… spt:node=… spt:vector=… -->
//! ```
//!
//! The local guard: a **direct** write always proceeds; an **llm** write
//! landing within the *protection window* after a recent **direct** write is
//! suppressed (and logged), so a stale LLM snapshot never clobbers a fresher
//! direct write.
//!
//! The distributed stamp (D6b): every accepted write carries the authoring
//! **node** and a **version vector** — the file's prior vector with this
//! node's entry bumped to a fresh [`EpochSource`](spt_store::epoch::EpochSource)
//! epoch (the same monotonic counter behind the D3b registry lease; wall-clock
//! never orders a merge). Pre-identity homes (no `node.key` yet) stamp the v1
//! marker shape — sync requires identity anyway, and a context write must
//! never mint one as a side effect.
//
// [impl->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE]

use spt_store::atomic::atomic_write_string;
use spt_store::epoch::EpochSource;
use std::path::Path;

pub use spt_store::contextmark::{Marker, WriteSource};

/// Default protection window: an llm write within this long after a direct write
/// is suppressed. Interim default; per-endpoint override is a later knob.
pub const DEFAULT_PROTECTION_WINDOW_MS: u64 = 60_000;

/// Result of a guarded write.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WriteOutcome {
    /// The write landed; the new marker timestamp.
    Written { routed_at_ms: u64 },
    /// An llm write was suppressed within the protection window of a direct
    /// write; the file is unchanged.
    Suppressed { direct_at_ms: u64 },
}

/// Decide whether a write proceeds, given the existing marker (if any). Pure —
/// the testable core of the 6.5 guard. A direct write always wins; an llm write
/// is suppressed only when the *existing* marker is a direct write still inside
/// the protection window.
pub fn decide(
    existing: Option<&Marker>,
    source: WriteSource,
    now_ms: u64,
    window_ms: u64,
) -> WriteOutcome {
    if source == WriteSource::Direct {
        return WriteOutcome::Written { routed_at_ms: now_ms };
    }
    if let Some(m) = existing {
        if m.source == WriteSource::Direct && now_ms.saturating_sub(m.routed_at_ms) < window_ms {
            return WriteOutcome::Suppressed {
                direct_at_ms: m.routed_at_ms,
            };
        }
    }
    WriteOutcome::Written { routed_at_ms: now_ms }
}

/// Read the leading precedence marker of a context file, if present.
pub fn read_marker(path: &Path) -> Option<Marker> {
    let s = std::fs::read_to_string(path).ok()?;
    Marker::parse_content(&s)
}

/// Apply a precedence-guarded write to a context file. On `Written`, the file
/// is atomically replaced with `marker-line + body` — the marker carrying this
/// node's identity and the file's vector advanced by a fresh epoch (the D6b
/// distributed stamp). On `Suppressed`, the file is left untouched and the
/// suppression is logged (the body is discarded as the stale snapshot it is).
pub fn write_context(
    path: &Path,
    body: &str,
    source: WriteSource,
    now_ms: u64,
    window_ms: u64,
) -> std::io::Result<WriteOutcome> {
    let existing = read_marker(path);
    let outcome = decide(existing.as_ref(), source, now_ms, window_ms);
    match outcome {
        WriteOutcome::Written { routed_at_ms } => {
            // Carry the file's causal history; bump our entry on an identified
            // node. Pre-identity (no node.key) stamps the v1 shape — never mint
            // an identity from a context write.
            let mut vector = existing.map(|m| m.vector).unwrap_or_default();
            let node = spt_store::nodeid::load_existing().map(|i| i.public_key().to_hex());
            if let Some(n) = &node {
                let mut epochs = EpochSource::load();
                vector.bump(n, epochs.next_epoch()?);
            }
            let marker = Marker { source, routed_at_ms, node, vector };
            let content = format!("{}\n{}", marker.render(), body);
            atomic_write_string(path, &content)?;
        }
        WriteOutcome::Suppressed { direct_at_ms } => {
            // [impl->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] stale llm overwrite
            // within the window: suppress + log, never clobber the fresher write.
            eprintln!(
                "PRECEDENCE_SUPPRESS:{} (llm write within {}ms of direct write @ {})",
                path.display(),
                window_ms,
                direct_at_ms
            );
        }
    }
    Ok(outcome)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn m(source: WriteSource, routed_at_ms: u64) -> Marker {
        Marker {
            source,
            routed_at_ms,
            node: None,
            vector: Default::default(),
        }
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] a direct write always proceeds.
    fn direct_always_writes() {
        let prior = m(WriteSource::Direct, 1000);
        assert_eq!(
            decide(Some(&prior), WriteSource::Direct, 1001, 60_000),
            WriteOutcome::Written { routed_at_ms: 1001 }
        );
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] llm within the window of a direct write is suppressed.
    fn llm_within_window_suppressed() {
        let prior = m(WriteSource::Direct, 1000);
        assert_eq!(
            decide(Some(&prior), WriteSource::Llm, 1500, 60_000),
            WriteOutcome::Suppressed { direct_at_ms: 1000 }
        );
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] llm past the window proceeds.
    fn llm_past_window_writes() {
        let prior = m(WriteSource::Direct, 1000);
        assert_eq!(
            decide(Some(&prior), WriteSource::Llm, 61_001, 60_000),
            WriteOutcome::Written { routed_at_ms: 61_001 }
        );
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] llm over a prior llm write proceeds (no direct to protect).
    fn llm_over_llm_writes() {
        let prior = m(WriteSource::Llm, 1000);
        assert_eq!(
            decide(Some(&prior), WriteSource::Llm, 1001, 60_000),
            WriteOutcome::Written { routed_at_ms: 1001 }
        );
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] guarded file write honors the decision.
    fn write_context_suppresses_stale_llm() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("context.md");
        write_context(&path, "DIRECT", WriteSource::Direct, 1000, 60_000).unwrap();
        let out = write_context(&path, "STALE", WriteSource::Llm, 1500, 60_000).unwrap();
        assert_eq!(out, WriteOutcome::Suppressed { direct_at_ms: 1000 });
        let content = std::fs::read_to_string(&path).unwrap();
        assert!(content.contains("DIRECT") && !content.contains("STALE"));
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] an identified node stamps node + a strictly-advancing vector.
    fn identified_write_stamps_advancing_vector() {
        crate::test_support::with_home(|_| {
            // Mint the node identity through the canonical creation path.
            let id = spt_store::nodeid::load_or_create().expect("mint node identity");
            let node_hex = id.public_key().to_hex();

            let dir = tempfile::tempdir().unwrap();
            let path = dir.path().join("context.md");
            write_context(&path, "v1", WriteSource::Llm, 1000, 60_000).unwrap();
            let m1 = read_marker(&path).expect("marker");
            assert_eq!(m1.node.as_deref(), Some(node_hex.as_str()));
            let e1 = *m1.vector.0.get(&node_hex).expect("own entry");

            write_context(&path, "v2", WriteSource::Llm, 2000, 60_000).unwrap();
            let m2 = read_marker(&path).expect("marker");
            let e2 = *m2.vector.0.get(&node_hex).expect("own entry");
            assert!(e2 > e1, "vector entry must strictly advance ({e1} -> {e2})");
            assert_eq!(
                m2.vector.compare(&m1.vector),
                spt_store::contextmark::VectorOrder::Dominates,
                "a successor local write dominates its predecessor"
            );
        });
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] pre-identity homes stamp the v1 marker shape (no minting).
    fn pre_identity_write_stamps_v1_shape() {
        crate::test_support::with_home(|home| {
            let dir = tempfile::tempdir().unwrap();
            let path = dir.path().join("context.md");
            write_context(&path, "body", WriteSource::Llm, 1000, 60_000).unwrap();
            let marker = read_marker(&path).expect("marker");
            assert_eq!(marker.node, None);
            assert!(marker.vector.0.is_empty());
            // No node.key was minted as a side effect.
            assert!(
                !home.join("identity").join("node.key").exists(),
                "context write must not mint an identity"
            );
        });
    }
}


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-live\src\context.rs has been updated successfully. (file state is current in your context — no need to Read it back)

//! Precedence-guarded context store (M2b T4, KNOWN-HAZARDS 6.5; distributed
//! marker since M4-D6b).
//!
//! Every context write carries a **precedence marker** as the file's first
//! line — parse/render and the cross-node merge math live in
//! [`spt_store::contextmark`] (the sync path shares them); this module owns the
//! **local guard + the guarded file write**:
//!
//! ```text
//! <!-- spt:source=llm spt:routed_at_ms=… spt:node=… spt:vector=… -->
//! ```
//!
//! The local guard: a **direct** write always proceeds; an **llm** write
//! landing within the *protection window* after a recent **direct** write is
//! suppressed (and logged), so a stale LLM snapshot never clobbers a fresher
//! direct write.
//!
//! The distributed stamp (D6b): every accepted write carries the authoring
//! **node** and a **version vector** — the file's prior vector with this
//! node's entry bumped to a fresh [`EpochSource`](spt_store::epoch::EpochSource)
//! epoch (the same monotonic counter behind the D3b registry lease; wall-clock
//! never orders a merge). Pre-identity homes (no `node.key` yet) stamp the v1
//! marker shape — sync requires identity anyway, and a context write must
//! never mint one as a side effect.
//
// [impl->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE]

use spt_store::atomic::atomic_write_string;
use spt_store::epoch::EpochSource;
use std::path::Path;

pub use spt_store::contextmark::{Marker, WriteSource};

/// Default protection window: an llm write within this long after a direct write
/// is suppressed. Interim default; per-endpoint override is a later knob.
pub const DEFAULT_PROTECTION_WINDOW_MS: u64 = 60_000;

/// Result of a guarded write.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WriteOutcome {
    /// The write landed; the new marker timestamp.
    Written { routed_at_ms: u64 },
    /// An llm write was suppressed within the protection window of a direct
    /// write; the file is unchanged.
    Suppressed { direct_at_ms: u64 },
}

/// Decide whether a write proceeds, given the existing marker (if any). Pure —
/// the testable core of the 6.5 guard. A direct write always wins; an llm write
/// is suppressed only when the *existing* marker is a direct write still inside
/// the protection window.
pub fn decide(
    existing: Option<&Marker>,
    source: WriteSource,
    now_ms: u64,
    window_ms: u64,
) -> WriteOutcome {
    if source == WriteSource::Direct {
        return WriteOutcome::Written { routed_at_ms: now_ms };
    }
    if let Some(m) = existing {
        if m.source == WriteSource::Direct && now_ms.saturating_sub(m.routed_at_ms) < window_ms {
            return WriteOutcome::Suppressed {
                direct_at_ms: m.routed_at_ms,
            };
        }
    }
    WriteOutcome::Written { routed_at_ms: now_ms }
}

/// Read the leading precedence marker of a context file, if present.
pub fn read_marker(path: &Path) -> Option<Marker> {
    let s = std::fs::read_to_string(path).ok()?;
    Marker::parse_content(&s)
}

/// Apply a precedence-guarded write to a context file. On `Written`, the file
/// is atomically replaced with `marker-line + body` — the marker carrying this
/// node's identity and the file's vector advanced by a fresh epoch (the D6b
/// distributed stamp). On `Suppressed`, the file is left untouched and the
/// suppression is logged (the body is discarded as the stale snapshot it is).
pub fn write_context(
    path: &Path,
    body: &str,
    source: WriteSource,
    now_ms: u64,
    window_ms: u64,
) -> std::io::Result<WriteOutcome> {
    let existing = read_marker(path);
    let outcome = decide(existing.as_ref(), source, now_ms, window_ms);
    match outcome {
        WriteOutcome::Written { routed_at_ms } => {
            // Carry the file's causal history; bump our entry on an identified
            // node. Pre-identity (no node.key) stamps the v1 shape — never mint
            // an identity from a context write.
            let mut vector = existing.map(|m| m.vector).unwrap_or_default();
            let node = spt_store::nodeid::load_existing().map(|i| i.public_key().to_hex());
            if let Some(n) = &node {
                let mut epochs = EpochSource::load();
                vector.bump(n, epochs.next_epoch()?);
            }
            let marker = Marker { source, routed_at_ms, node, vector };
            let content = format!("{}\n{}", marker.render(), body);
            atomic_write_string(path, &content)?;
        }
        WriteOutcome::Suppressed { direct_at_ms } => {
            // [impl->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] stale llm overwrite
            // within the window: suppress + log, never clobber the fresher write.
            eprintln!(
                "PRECEDENCE_SUPPRESS:{} (llm write within {}ms of direct write @ {})",
                path.display(),
                window_ms,
                direct_at_ms
            );
        }
    }
    Ok(outcome)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn m(source: WriteSource, routed_at_ms: u64) -> Marker {
        Marker {
            source,
            routed_at_ms,
            node: None,
            vector: Default::default(),
        }
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] a direct write always proceeds.
    fn direct_always_writes() {
        let prior = m(WriteSource::Direct, 1000);
        assert_eq!(
            decide(Some(&prior), WriteSource::Direct, 1001, 60_000),
            WriteOutcome::Written { routed_at_ms: 1001 }
        );
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] llm within the window of a direct write is suppressed.
    fn llm_within_window_suppressed() {
        let prior = m(WriteSource::Direct, 1000);
        assert_eq!(
            decide(Some(&prior), WriteSource::Llm, 1500, 60_000),
            WriteOutcome::Suppressed { direct_at_ms: 1000 }
        );
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] llm past the window proceeds.
    fn llm_past_window_writes() {
        let prior = m(WriteSource::Direct, 1000);
        assert_eq!(
            decide(Some(&prior), WriteSource::Llm, 61_001, 60_000),
            WriteOutcome::Written { routed_at_ms: 61_001 }
        );
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] llm over a prior llm write proceeds (no direct to protect).
    fn llm_over_llm_writes() {
        let prior = m(WriteSource::Llm, 1000);
        assert_eq!(
            decide(Some(&prior), WriteSource::Llm, 1001, 60_000),
            WriteOutcome::Written { routed_at_ms: 1001 }
        );
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] guarded file write honors the decision.
    fn write_context_suppresses_stale_llm() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("context.md");
        write_context(&path, "DIRECT", WriteSource::Direct, 1000, 60_000).unwrap();
        let out = write_context(&path, "STALE", WriteSource::Llm, 1500, 60_000).unwrap();
        assert_eq!(out, WriteOutcome::Suppressed { direct_at_ms: 1000 });
        let content = std::fs::read_to_string(&path).unwrap();
        assert!(content.contains("DIRECT") && !content.contains("STALE"));
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] an identified node stamps node + a strictly-advancing vector.
    fn identified_write_stamps_advancing_vector() {
        crate::test_support::with_home(|_| {
            // Mint the node identity through the canonical creation path.
            let id = spt_store::nodeid::load_or_create().expect("mint node identity");
            let node_hex = id.public_key().to_hex();

            let dir = tempfile::tempdir().unwrap();
            let path = dir.path().join("context.md");
            write_context(&path, "v1", WriteSource::Llm, 1000, 60_000).unwrap();
            let m1 = read_marker(&path).expect("marker");
            assert_eq!(m1.node.as_deref(), Some(node_hex.as_str()));
            let e1 = *m1.vector.0.get(&node_hex).expect("own entry");

            write_context(&path, "v2", WriteSource::Llm, 2000, 60_000).unwrap();
            let m2 = read_marker(&path).expect("marker");
            let e2 = *m2.vector.0.get(&node_hex).expect("own entry");
            assert!(e2 > e1, "vector entry must strictly advance ({e1} -> {e2})");
            assert_eq!(
                m2.vector.compare(&m1.vector),
                spt_store::contextmark::VectorOrder::Dominates,
                "a successor local write dominates its predecessor"
            );
        });
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] pre-identity homes stamp the v1 marker shape (no minting).
    fn pre_identity_write_stamps_v1_shape() {
        crate::test_support::with_home(|home| {
            let dir = tempfile::tempdir().unwrap();
            let path = dir.path().join("context.md");
            write_context(&path, "body", WriteSource::Llm, 1000, 60_000).unwrap();
            let marker = read_marker(&path).expect("marker");
            assert_eq!(marker.node, None);
            assert!(marker.vector.0.is_empty());
            // No node.key was minted as a side effect.
            assert!(
                !home.join("identity").join("node.key").exists(),
                "context write must not mint an identity"
            );
        });
    }
}

-//! Precedence-guarded context store (M2b T4, KNOWN-HAZARDS 6.5).

+//! Precedence-guarded context store (M2b T4, KNOWN-HAZARDS 6.5; distributed

-//! Every context write carries a **precedence marker** — `source` (`direct` vs

-//! `llm`) + a `routed_at` timestamp — recorded as the file's first line:

+//! Every context write carries a **precedence marker** as the file's first

+//! line — parse/render and the cross-node merge math live in

+//! [`spt_store::contextmark`] (the sync path shares them); this module owns the

+//! **local guard + the guarded file write**:

+//! <!-- spt:source=llm spt:routed_at_ms=… spt:node=… spt:vector=… -->

-//! The guard: a **direct** write always proceeds; an **llm** write that lands

-//! within the *protection window* after a recent **direct** write is suppressed

-//! (and logged), so a stale LLM snapshot never clobbers a fresher direct write.

+//! The local guard: a **direct** write always proceeds; an **llm** write

+//! landing within the *protection window* after a recent **direct** write is

+//! suppressed (and logged), so a stale LLM snapshot never clobbers a fresher

-//! 6.5's full form adds **node identity** to the marker and generalizes to the

-//! distributed *newest-and-newer-than-mine* rule (ADR-0003, M4). M2b lands the

-//! **local** guard only: one node, `source` + `routed_at` (epoch-ms for exact

-//! window math; the ISO/`spt:node` form is the M4 sync concern). Keep the marker

-//! parse/render here so the M4 generalization is an extension, not a rewrite.

+//! The distributed stamp (D6b): every accepted write carries the authoring

+//! **node** and a **version vector** — the file's prior vector with this

+//! node's entry bumped to a fresh [`EpochSource`](spt_store::epoch::EpochSource)

+//! epoch (the same monotonic counter behind the D3b registry lease; wall-clock

+//! never orders a merge). Pre-identity homes (no `node.key` yet) stamp the v1

+//! marker shape — sync requires identity anyway, and a context write must

+pub use spt_store::contextmark::{Marker, WriteSource};

pub const DEFAULT_PROTECTION_WINDOW_MS: u64 = 60_000;

-/// Who authored a context write. `Direct` = spt-core / operator / sync

-/// propagation (authoritative, fresh); `Llm` = a model snapshot (possibly

-/// stale) — the case the guard protects against.

-#[derive(Debug, Clone, Copy, PartialEq, Eq)]

-            WriteSource::Direct => "direct",

-    fn from_str(s: &str) -> Option<WriteSource> {

-            "direct" => Some(WriteSource::Direct),

-            "llm" => Some(WriteSource::Llm),

-/// The precedence marker stamped as a context file's first line.

-    /// Render the marker as its HTML-comment first line (no trailing newline).

-            "<!-- spt:source={} spt:routed_at_ms={} -->",

-    /// Parse a marker from a context file's first line. Returns `None` if the

-    /// line isn't a well-formed marker (an unmarked / hand-edited file ⇒ no

-    /// prior precedence, so any write proceeds).

-    pub fn parse(line: &str) -> Option<Marker> {

-        let inner = line.trim().strip_prefix("<!--")?.strip_suffix("-->")?.trim();

-        for tok in inner.split_whitespace() {

-            if let Some(v) = tok.strip_prefix("spt:source=") {

-                source = WriteSource::from_str(v);

-            } else if let Some(v) = tok.strip_prefix("spt:routed_at_ms=") {

-                routed_at_ms = v.parse::<u64>().ok();

-            routed_at_ms: routed_at_ms?,

/// Read the leading precedence marker of a context file, if present.

pub fn read_marker(path: &Path) -> Option<Marker> {

let s = std::fs::read_to_string(path).ok()?;

-    Marker::parse(s.lines().next().unwrap_or(""))

-/// Apply a precedence-guarded write to a context file. On `Written`, the file is

-/// atomically replaced with `marker-line + body`; on `Suppressed`, the file is

-/// left untouched and the suppression is logged (the body is discarded as the

+/// Apply a precedence-guarded write to a context file. On `Written`, the file

+/// is atomically replaced with `marker-line + body` — the marker carrying this

+/// node's identity and the file's vector advanced by a fresh epoch (the D6b

+/// distributed stamp). On `Suppressed`, the file is left untouched and the

+/// suppression is logged (the body is discarded as the stale snapshot it is).

-    let outcome = decide(existing, source, now_ms, window_ms);

+    let outcome = decide(existing.as_ref(), source, now_ms, window_ms);

WriteOutcome::Written { routed_at_ms } => {

-            let marker = Marker { source, routed_at_ms };

+            // Carry the file's causal history; bump our entry on an identified

+            // node. Pre-identity (no node.key) stamps the v1 shape — never mint

+            // an identity from a context write.

+            let mut vector = existing.map(|m| m.vector).unwrap_or_default();

+            let node = spt_store::nodeid::load_existing().map(|i| i.public_key().to_hex());

+                let mut epochs = EpochSource::load();

+                vector.bump(n, epochs.next_epoch()?);

+            let marker = Marker { source, routed_at_ms, node, vector };

let content = format!("{}\n{}", marker.render(), body);

-    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] marker round-trips.

-        let m = Marker { source: WriteSource::Llm, routed_at_ms: 1717200000000 };

-        assert_eq!(Marker::parse(&m.render()), Some(m));

-        assert_eq!(Marker::parse("just some text"), None);

+    fn m(source: WriteSource, routed_at_ms: u64) -> Marker {

+            vector: Default::default(),

-        let prior = Some(Marker { source: WriteSource::Direct, routed_at_ms: 1000 });

+        let prior = m(WriteSource::Direct, 1000);

-            decide(prior, WriteSource::Direct, 1001, 60_000),

+            decide(Some(&prior), WriteSource::Direct, 1001, 60_000),

WriteOutcome::Written { routed_at_ms: 1001 }

-            decide(prior, WriteSource::Llm, 1500, 60_000),

+            decide(Some(&prior), WriteSource::Llm, 1500, 60_000),

WriteOutcome::Suppressed { direct_at_ms: 1000 }

-            decide(prior, WriteSource::Llm, 61_001, 60_000),

+            decide(Some(&prior), WriteSource::Llm, 61_001, 60_000),

WriteOutcome::Written { routed_at_ms: 61_001 }

-        let prior = Some(Marker { source: WriteSource::Llm, routed_at_ms: 1000 });

+        let prior = m(WriteSource::Llm, 1000);

-            decide(prior, WriteSource::Llm, 1001, 60_000),

+            decide(Some(&prior), WriteSource::Llm, 1001, 60_000),

let path = dir.path().join("context.md");

+        write_context(&path, "DIRECT", WriteSource::Direct, 1000, 60_000).unwrap();

+        let out = write_context(&path, "STALE", WriteSource::Llm, 1500, 60_000).unwrap();

+        assert_eq!(out, WriteOutcome::Suppressed { direct_at_ms: 1000 });

+        let content = std::fs::read_to_string(&path).unwrap();

+        assert!(content.contains("DIRECT") && !content.contains("STALE"));

-        let o = write_context(&path, "DIRECT BODY", WriteSource::Direct, 1000, 60_000).unwrap();

-        assert!(matches!(o, WriteOutcome::Written { .. }));

-        assert!(std::fs::read_to_string(&path).unwrap().contains("DIRECT BODY"));

+    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] an identified node stamps node + a strictly-advancing vector.

+    fn identified_write_stamps_advancing_vector() {

+        crate::test_support::with_home(|_| {

+            // Mint the node identity through the canonical creation path.

+            let id = spt_store::nodeid::load_or_create().expect("mint node identity");

+            let node_hex = id.public_key().to_hex();

-        // A stale llm write within the window is suppressed — file unchanged.

-        let o = write_context(&path, "STALE LLM", WriteSource::Llm, 1500, 60_000).unwrap();

-        assert!(matches!(o, WriteOutcome::Suppressed { .. }));

-        let after = std::fs::read_to_string(&path).unwrap();

-        assert!(after.contains("DIRECT BODY") && !after.contains("STALE LLM"));

+            let dir = tempfile::tempdir().unwrap();

+            let path = dir.path().join("context.md");

+            write_context(&path, "v1", WriteSource::Llm, 1000, 60_000).unwrap();

+            let m1 = read_marker(&path).expect("marker");

+            assert_eq!(m1.node.as_deref(), Some(node_hex.as_str()));

+            let e1 = *m1.vector.0.get(&node_hex).expect("own entry");

-        // Past the window, the llm write proceeds.

-        let o = write_context(&path, "FRESH LLM", WriteSource::Llm, 70_000, 60_000).unwrap();

-        assert!(matches!(o, WriteOutcome::Written { .. }));

-        assert!(std::fs::read_to_string(&path).unwrap().contains("FRESH LLM"));

+            write_context(&path, "v2", WriteSource::Llm, 2000, 60_000).unwrap();

+            let m2 = read_marker(&path).expect("marker");

+            let e2 = *m2.vector.0.get(&node_hex).expect("own entry");

+            assert!(e2 > e1, "vector entry must strictly advance ({e1} -> {e2})");

+                m2.vector.compare(&m1.vector),

+                spt_store::contextmark::VectorOrder::Dominates,

+                "a successor local write dominates its predecessor"

+    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] pre-identity homes stamp the v1 marker shape (no minting).

+    fn pre_identity_write_stamps_v1_shape() {

+        crate::test_support::with_home(|home| {

+            let dir = tempfile::tempdir().unwrap();

+            let path = dir.path().join("context.md");

+            write_context(&path, "body", WriteSource::Llm, 1000, 60_000).unwrap();

+            let marker = read_marker(&path).expect("marker");

+            assert_eq!(marker.node, None);

+            assert!(marker.vector.0.is_empty());

+            // No node.key was minted as a side effect.

+                !home.join("identity").join("node.key").exists(),

+                "context write must not mint an identity"

//! Precedence-guarded context store (M2b T4, KNOWN-HAZARDS 6.5).
//!
//! Every context write carries a **precedence marker** — `source` (`direct` vs
//! `llm`) + a `routed_at` timestamp — recorded as the file's first line:
//!
//! ```text
//! <!-- spt:source=llm spt:routed_at_ms=1717200000000 -->
//! ```
//!
//! The guard: a **direct** write always proceeds; an **llm** write that lands
//! within the *protection window* after a recent **direct** write is suppressed
//! (and logged), so a stale LLM snapshot never clobbers a fresher direct write.
//!
//! ## Interim vs M4
//! 6.5's full form adds **node identity** to the marker and generalizes to the
//! distributed *newest-and-newer-than-mine* rule (ADR-0003, M4). M2b lands the
//! **local** guard only: one node, `source` + `routed_at` (epoch-ms for exact
//! window math; the ISO/`spt:node` form is the M4 sync concern). Keep the marker
//! parse/render here so the M4 generalization is an extension, not a rewrite.
//
// [impl->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE]

use spt_store::atomic::atomic_write_string;
use std::path::Path;

/// Default protection window: an llm write within this long after a direct write
/// is suppressed. Interim default; per-endpoint override is a later knob.
pub const DEFAULT_PROTECTION_WINDOW_MS: u64 = 60_000;

/// Who authored a context write. `Direct` = spt-core / operator / sync
/// propagation (authoritative, fresh); `Llm` = a model snapshot (possibly
/// stale) — the case the guard protects against.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriteSource {
    Direct,
    Llm,
}

impl WriteSource {
    fn as_str(self) -> &'static str {
        match self {
            WriteSource::Direct => "direct",
            WriteSource::Llm => "llm",
        }
    }
    fn from_str(s: &str) -> Option<WriteSource> {
        match s {
            "direct" => Some(WriteSource::Direct),
            "llm" => Some(WriteSource::Llm),
            _ => None,
        }
    }
}

/// The precedence marker stamped as a context file's first line.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Marker {
    pub source: WriteSource,
    pub routed_at_ms: u64,
}

impl Marker {
    /// Render the marker as its HTML-comment first line (no trailing newline).
    pub fn render(&self) -> String {
        format!(
            "<!-- spt:source={} spt:routed_at_ms={} -->",
            self.source.as_str(),
            self.routed_at_ms
        )
    }

    /// Parse a marker from a context file's first line. Returns `None` if the
    /// line isn't a well-formed marker (an unmarked / hand-edited file ⇒ no
    /// prior precedence, so any write proceeds).
    pub fn parse(line: &str) -> Option<Marker> {
        let inner = line.trim().strip_prefix("<!--")?.strip_suffix("-->")?.trim();
        let mut source = None;
        let mut routed_at_ms = None;
        for tok in inner.split_whitespace() {
            if let Some(v) = tok.strip_prefix("spt:source=") {
                source = WriteSource::from_str(v);
            } else if let Some(v) = tok.strip_prefix("spt:routed_at_ms=") {
                routed_at_ms = v.parse::<u64>().ok();
            }
        }
        Some(Marker {
            source: source?,
            routed_at_ms: routed_at_ms?,
        })
    }
}

/// Result of a guarded write.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WriteOutcome {
    /// The write landed; the new marker timestamp.
    Written { routed_at_ms: u64 },
    /// An llm write was suppressed within the protection window of a direct
    /// write; the file is unchanged.
    Suppressed { direct_at_ms: u64 },
}

/// Decide whether a write proceeds, given the existing marker (if any). Pure —
/// the testable core of the 6.5 guard. A direct write always wins; an llm write
/// is suppressed only when the *existing* marker is a direct write still inside
/// the protection window.
pub fn decide(
    existing: Option<Marker>,
    source: WriteSource,
    now_ms: u64,
    window_ms: u64,
) -> WriteOutcome {
    if source == WriteSource::Direct {
        return WriteOutcome::Written { routed_at_ms: now_ms };
    }
    if let Some(m) = existing {
        if m.source == WriteSource::Direct && now_ms.saturating_sub(m.routed_at_ms) < window_ms {
            return WriteOutcome::Suppressed {
                direct_at_ms: m.routed_at_ms,
            };
        }
    }
    WriteOutcome::Written { routed_at_ms: now_ms }
}

/// Read the leading precedence marker of a context file, if present.
pub fn read_marker(path: &Path) -> Option<Marker> {
    let s = std::fs::read_to_string(path).ok()?;
    Marker::parse(s.lines().next().unwrap_or(""))
}

/// Apply a precedence-guarded write to a context file. On `Written`, the file is
/// atomically replaced with `marker-line + body`; on `Suppressed`, the file is
/// left untouched and the suppression is logged (the body is discarded as the
/// stale snapshot it is).
pub fn write_context(
    path: &Path,
    body: &str,
    source: WriteSource,
    now_ms: u64,
    window_ms: u64,
) -> std::io::Result<WriteOutcome> {
    let existing = read_marker(path);
    let outcome = decide(existing, source, now_ms, window_ms);
    match outcome {
        WriteOutcome::Written { routed_at_ms } => {
            let marker = Marker { source, routed_at_ms };
            let content = format!("{}\n{}", marker.render(), body);
            atomic_write_string(path, &content)?;
        }
        WriteOutcome::Suppressed { direct_at_ms } => {
            // [impl->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] stale llm overwrite
            // within the window: suppress + log, never clobber the fresher write.
            eprintln!(
                "PRECEDENCE_SUPPRESS:{} (llm write within {}ms of direct write @ {})",
                path.display(),
                window_ms,
                direct_at_ms
            );
        }
    }
    Ok(outcome)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] marker round-trips.
    fn marker_round_trips() {
        let m = Marker { source: WriteSource::Llm, routed_at_ms: 1717200000000 };
        assert_eq!(Marker::parse(&m.render()), Some(m));
        assert_eq!(Marker::parse("just some text"), None);
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] a direct write always proceeds.
    fn direct_always_writes() {
        let prior = Some(Marker { source: WriteSource::Direct, routed_at_ms: 1000 });
        assert_eq!(
            decide(prior, WriteSource::Direct, 1001, 60_000),
            WriteOutcome::Written { routed_at_ms: 1001 }
        );
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] llm within the window of a direct write is suppressed.
    fn llm_within_window_suppressed() {
        let prior = Some(Marker { source: WriteSource::Direct, routed_at_ms: 1000 });
        assert_eq!(
            decide(prior, WriteSource::Llm, 1500, 60_000),
            WriteOutcome::Suppressed { direct_at_ms: 1000 }
        );
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] llm past the window proceeds.
    fn llm_past_window_writes() {
        let prior = Some(Marker { source: WriteSource::Direct, routed_at_ms: 1000 });
        assert_eq!(
            decide(prior, WriteSource::Llm, 61_001, 60_000),
            WriteOutcome::Written { routed_at_ms: 61_001 }
        );
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] llm over a prior llm write proceeds (no direct to protect).
    fn llm_over_llm_writes() {
        let prior = Some(Marker { source: WriteSource::Llm, routed_at_ms: 1000 });
        assert_eq!(
            decide(prior, WriteSource::Llm, 1001, 60_000),
            WriteOutcome::Written { routed_at_ms: 1001 }
        );
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] guarded file write honors the decision.
    fn write_context_suppresses_stale_llm() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("context.md");

        // A fresh direct write lands.
        let o = write_context(&path, "DIRECT BODY", WriteSource::Direct, 1000, 60_000).unwrap();
        assert!(matches!(o, WriteOutcome::Written { .. }));
        assert!(std::fs::read_to_string(&path).unwrap().contains("DIRECT BODY"));

        // A stale llm write within the window is suppressed — file unchanged.
        let o = write_context(&path, "STALE LLM", WriteSource::Llm, 1500, 60_000).unwrap();
        assert!(matches!(o, WriteOutcome::Suppressed { .. }));
        let after = std::fs::read_to_string(&path).unwrap();
        assert!(after.contains("DIRECT BODY") && !after.contains("STALE LLM"));

        // Past the window, the llm write proceeds.
        let o = write_context(&path, "FRESH LLM", WriteSource::Llm, 70_000, 60_000).unwrap();
        assert!(matches!(o, WriteOutcome::Written { .. }));
        assert!(std::fs::read_to_string(&path).unwrap().contains("FRESH LLM"));
    }
}


1	//! Shared test scaffolding for spt-live unit tests.
2	//!
3	//! **One** process-wide `SPT_HOME` lock for the whole crate. Per the cross-
4	//! module env-race gotcha, a per-module lock is *not* enough: every module's
5	//! `SPT_HOME`-touching test races on the single global env var, so they must all
6	//! serialize on the same `Mutex`.
7
8	use std::path::Path;
9	use std::sync::Mutex;
10
11	static HOME_LOCK: Mutex<()> = Mutex::new(());
12
13	/// Run `body` with `SPT_HOME` pointed at a fresh temp dir, serialized against
14	/// every other `SPT_HOME` test in this binary and restored afterward.
15	pub(crate) fn with_home<F: FnOnce(&Path)>(body: F) {
16	    let _g = HOME_LOCK.lock().unwrap_or_else(|p| p.into_inner());
17	    let prev = std::env::var("SPT_HOME").ok();
18	    let tmp = tempfile::tempdir().unwrap();
19	    std::env::set_var("SPT_HOME", tmp.path());
20	    body(tmp.path());
21	    match prev {
22	        Some(v) => std::env::set_var("SPT_HOME", v),
23	        None => std::env::remove_var("SPT_HOME"),
24	    }
25	}
26

//! Shared test scaffolding for spt-live unit tests.
//!
//! **One** process-wide `SPT_HOME` lock for the whole crate. Per the cross-
//! module env-race gotcha, a per-module lock is *not* enough: every module's
//! `SPT_HOME`-touching test races on the single global env var, so they must all
//! serialize on the same `Mutex`.

use std::path::Path;
use std::sync::Mutex;

static HOME_LOCK: Mutex<()> = Mutex::new(());

/// Run `body` with `SPT_HOME` pointed at a fresh temp dir, serialized against
/// every other `SPT_HOME` test in this binary and restored afterward.
pub(crate) fn with_home<F: FnOnce(&Path)>(body: F) {
    let _g = HOME_LOCK.lock().unwrap_or_else(|p| p.into_inner());
    let prev = std::env::var("SPT_HOME").ok();
    let tmp = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", tmp.path());
    body(tmp.path());
    match prev {
        Some(v) => std::env::set_var("SPT_HOME", v),
        None => std::env::remove_var("SPT_HOME"),
    }
}


D6a-1: BranchStore + ContextStore + project_id in spt-store

gitrun.rs (bounded git, 5.3), branchstore.rs (git-CLI BranchStore: bare seed, ensure branch/worktree, commit=checkpoint/tip=resume, REQ-STORE-1 activate), contextstore.rs (tracked/ two-tier layout, a-/p- branches), project.rs (project_id from git remote / folder fallback), perch.rs tracked paths, STORAGE.md seed-repo note. Push CI-green.

D6a-2: two-tier ingest routing + rename arm in spt-live

ingest.rs routes commune slices (parse_two_slice) into ContextStore tier files (precedence-guarded + committed); lifecycle wiring (project dir → project_id); spt rename context-branch arm (a-<old>→a-<new>); REQ-INST-2 activate [impl,unit]. Push CI-green.

D6b: vector merge + Psyche reconcile + D7.5a driver

VersionVector + marker v2 (node+vector), per-file merge fn, conflict artifacts (REQ-HAZARD-CONFLICT-BOTH-PRESERVED register+activate), reconciler election, bounded stdout-captured Psyche turn driver (D7.5a pulled forward), reconcile flow + validation + retry + event seam.

D6c: bundle sync over broker streams + gates

SyncRequest/bundle-chunk records, pull-based ref-scoped (synced(E,S)+visibility server-side fail-closed), fetch+driver merge+two-parent commits, triggers (write-notify, activation freshness feed, anti-entropy), loopback two-daemon tests, FAULT-MATRIX rows. REQ-NET-3 + REQ-INST-5 activate.


Bash

cd C:/Users/decid/Documents/projects/spt-core && cargo test -p spt-store -p spt-live --quiet 2>&1 | grep -E "test result|error\[|FAILED|panicked" | head -20

Test store + live after marker v2


<task-notification>
<task-id>b333pvg32</task-id>
<tool-use-id>toolu_01U9s4DfkDLZDgtqGJqi98AT</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\b333pvg32.output</output-file>
<status>completed</status>
<summary>Background command "Poll CI for 1ef9802" completed (exit code 0)</summary>
</task-notification>


test result: ok. 37 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.79s
test result: ok. 107 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.51s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


Prompt:
<task-notification>
<task-id>b333pvg32</task-id>
<tool-use-id>toolu_01U9s4DfkDLZDgtqGJqi98AT</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\b333pvg32.output</output-file>
<status>completed</status>
<summary>Background command "Poll CI for 1ef9802" completed (exit code 0)</summary>
</task-notification>


1	[{"conclusion":"","headSha":"1ef980218554bf2f7ba8c8cd7f7e210e2b34129f","status":"in_progress"}]
2	[{"conclusion":"","headSha":"1ef980218554bf2f7ba8c8cd7f7e210e2b34129f","status":"in_progress"}]
3	[{"conclusion":"","headSha":"1ef980218554bf2f7ba8c8cd7f7e210e2b34129f","status":"in_progress"}]
4	[{"conclusion":"success","headSha":"1ef980218554bf2f7ba8c8cd7f7e210e2b34129f","status":"completed"}]
5


Updated task #2 status


Updated task #3 status


Edit

/// The context-store arm of `spt rename <old> <new>` (REQ-INST-11, the

    /// Record a surfaced concurrent-write conflict (ADR-0013, hazard 6.6):
    /// persist the **incoming** version as a tracked artifact beside the local
    /// file — `.conflicts/<file>.<hash8>.md` inside the same worktree — and
    /// checkpoint it. The local working file stays untouched (the active mind
    /// is never clobbered by an unresolved conflict), and the artifact
    /// replicates like any context file, so conflict state itself syncs.
    /// Deterministic content-hash naming makes recording idempotent across
    /// nodes — every holder of the same pair mints the same artifact.
    // [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]
    pub fn record_conflict(
        &self,
        worktree: &Path,
        file_rel: &str,
        incoming: &[u8],
    ) -> std::io::Result<PathBuf> {
        let dir = worktree.join(CONFLICT_DIR);
        std::fs::create_dir_all(&dir)?;
        let name = format!(
            "{}.{}.md",
            file_rel.replace(['/', '\\'], "_"),
            content_hash8(incoming)
        );
        let path = dir.join(&name);
        if !path.exists() {
            crate::atomic::atomic_write_string(&path, &String::from_utf8_lossy(incoming))?;
        }
        self.store.commit_in_worktree(
            worktree,
            &[],
            &format!("conflict: {file_rel} ({name})"),
        )?;
        Ok(path)
    }

    /// The surfaced-and-unresolved conflicts in a worktree (artifact paths for
    /// `file_rel`, or all when `None`). Empty = no conflicts pending.
    // [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]
    pub fn list_conflicts(
        &self,
        worktree: &Path,
        file_rel: Option<&str>,
    ) -> std::io::Result<Vec<PathBuf>> {
        let dir = worktree.join(CONFLICT_DIR);
        let mut out = Vec::new();
        let Ok(entries) = std::fs::read_dir(&dir) else {
            return Ok(out); // no dir = no conflicts
        };
        let prefix = file_rel.map(|f| format!("{}.", f.replace(['/', '\\'], "_")));
        for e in entries.flatten() {
            let name = e.file_name().to_string_lossy().to_string();
            if prefix.as_ref().is_none_or(|p| name.starts_with(p.as_str())) {
                out.push(e.path());
            }
        }
        out.sort();
        Ok(out)
    }

    /// Clear `file_rel`'s conflict artifacts after a dominating (reconciled)
    /// write landed — `git rm` + one checkpoint commit. A no-op when none
    /// exist. Never called by the merge path on anything but a dominating
    /// write: an unresolved pair must outlive every failure (hazard 6.6).
    // [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]
    pub fn clear_conflicts(&self, worktree: &Path, file_rel: &str) -> std::io::Result<usize> {
        let paths = self.list_conflicts(worktree, Some(file_rel))?;
        if paths.is_empty() {
            return Ok(0);
        }
        for p in &paths {
            std::fs::remove_file(p)?;
        }
        self.store
            .commit_in_worktree(worktree, &[], &format!("conflict resolved: {file_rel}"))?;
        Ok(paths.len())
    }

    /// The context-store arm of `spt rename <old> <new>` (REQ-INST-11, the


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\contextstore.rs has been updated successfully. (file state is current in your context — no need to Read it back)

    /// Record a surfaced concurrent-write conflict (ADR-0013, hazard 6.6):
    /// persist the **incoming** version as a tracked artifact beside the local
    /// file — `.conflicts/<file>.<hash8>.md` inside the same worktree — and
    /// checkpoint it. The local working file stays untouched (the active mind
    /// is never clobbered by an unresolved conflict), and the artifact
    /// replicates like any context file, so conflict state itself syncs.
    /// Deterministic content-hash naming makes recording idempotent across
    /// nodes — every holder of the same pair mints the same artifact.
    // [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]
    pub fn record_conflict(
        &self,
        worktree: &Path,
        file_rel: &str,
        incoming: &[u8],
    ) -> std::io::Result<PathBuf> {
        let dir = worktree.join(CONFLICT_DIR);
        std::fs::create_dir_all(&dir)?;
        let name = format!(
            "{}.{}.md",
            file_rel.replace(['/', '\\'], "_"),
            content_hash8(incoming)
        );
        let path = dir.join(&name);
        if !path.exists() {
            crate::atomic::atomic_write_string(&path, &String::from_utf8_lossy(incoming))?;
        }
        self.store.commit_in_worktree(
            worktree,
            &[],
            &format!("conflict: {file_rel} ({name})"),
        )?;
        Ok(path)
    }

    /// The surfaced-and-unresolved conflicts in a worktree (artifact paths for
    /// `file_rel`, or all when `None`). Empty = no conflicts pending.
    // [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]
    pub fn list_conflicts(
        &self,
        worktree: &Path,
        file_rel: Option<&str>,
    ) -> std::io::Result<Vec<PathBuf>> {
        let dir = worktree.join(CONFLICT_DIR);
        let mut out = Vec::new();
        let Ok(entries) = std::fs::read_dir(&dir) else {
            return Ok(out); // no dir = no conflicts
        };
        let prefix = file_rel.map(|f| format!("{}.", f.replace(['/', '\\'], "_")));
        for e in entries.flatten() {
            let name = e.file_name().to_string_lossy().to_string();
            if prefix.as_ref().is_none_or(|p| name.starts_with(p.as_str())) {
                out.push(e.path());
            }
        }
        out.sort();
        Ok(out)
    }

    /// Clear `file_rel`'s conflict artifacts after a dominating (reconciled)
    /// write landed — `git rm` + one checkpoint commit. A no-op when none
    /// exist. Never called by the merge path on anything but a dominating
    /// write: an unresolved pair must outlive every failure (hazard 6.6).
    // [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]
    pub fn clear_conflicts(&self, worktree: &Path, file_rel: &str) -> std::io::Result<usize> {
        let paths = self.list_conflicts(worktree, Some(file_rel))?;
        if paths.is_empty() {
            return Ok(0);
        }
        for p in &paths {
            std::fs::remove_file(p)?;
        }
        self.store
            .commit_in_worktree(worktree, &[], &format!("conflict resolved: {file_rel}"))?;
        Ok(paths.len())
    }

    /// The context-store arm of `spt rename <old> <new>` (REQ-INST-11, the

+    /// Record a surfaced concurrent-write conflict (ADR-0013, hazard 6.6):

+    /// persist the **incoming** version as a tracked artifact beside the local

+    /// file — `.conflicts/<file>.<hash8>.md` inside the same worktree — and

+    /// checkpoint it. The local working file stays untouched (the active mind

+    /// is never clobbered by an unresolved conflict), and the artifact

+    /// replicates like any context file, so conflict state itself syncs.

+    /// Deterministic content-hash naming makes recording idempotent across

+    /// nodes — every holder of the same pair mints the same artifact.

+    // [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]

+        let dir = worktree.join(CONFLICT_DIR);

+        std::fs::create_dir_all(&dir)?;

+            crate::atomic::atomic_write_string(&path, &String::from_utf8_lossy(incoming))?;

+            &format!("conflict: {file_rel} ({name})"),

+    /// The surfaced-and-unresolved conflicts in a worktree (artifact paths for

+    /// `file_rel`, or all when `None`). Empty = no conflicts pending.

+    ) -> std::io::Result<Vec<PathBuf>> {

+        let Ok(entries) = std::fs::read_dir(&dir) else {

+            return Ok(out); // no dir = no conflicts

+            let name = e.file_name().to_string_lossy().to_string();

+            if prefix.as_ref().is_none_or(|p| name.starts_with(p.as_str())) {

+    /// Clear `file_rel`'s conflict artifacts after a dominating (reconciled)

+    /// write landed — `git rm` + one checkpoint commit. A no-op when none

+    /// exist. Never called by the merge path on anything but a dominating

+    /// write: an unresolved pair must outlive every failure (hazard 6.6).

+    pub fn clear_conflicts(&self, worktree: &Path, file_rel: &str) -> std::io::Result<usize> {

+        let paths = self.list_conflicts(worktree, Some(file_rel))?;

+            .commit_in_worktree(worktree, &[], &format!("conflict resolved: {file_rel}"))?;

/// D3f-documented seam): move the `a-<old>` branch + its worktree to

/// `a-<new>`, and rename `<old>/` → `<new>/` inside every project worktree


/// The per-agent live-context file inside an `a-<id>` worktree.
pub const LIVE_CONTEXT_FILE: &str = "live-context.md";

/// The per-agent live-context file inside an `a-<id>` worktree.
pub const LIVE_CONTEXT_FILE: &str = "live-context.md";
/// Per-worktree directory holding surfaced concurrent-write conflict artifacts
/// (ADR-0013; tracked, so conflict state replicates).
pub const CONFLICT_DIR: &str = ".conflicts";


/// The per-agent live-context file inside an `a-<id>` worktree.
pub const LIVE_CONTEXT_FILE: &str = "live-context.md";
/// Per-worktree directory holding surfaced concurrent-write conflict artifacts
/// (ADR-0013; tracked, so conflict state replicates).
pub const CONFLICT_DIR: &str = ".conflicts";

+/// Per-worktree directory holding surfaced concurrent-write conflict artifacts

+/// (ADR-0013; tracked, so conflict state replicates).

+pub const CONFLICT_DIR: &str = ".conflicts";

/// The per-(agent,project) context file inside a `p-<project>` worktree,

pub const PROJECT_CONTEXT_FILE: &str = "project-context.md";


/// The two-tier context store rooted at a `tracked/` directory.
pub struct ContextStore {

/// First 8 hex chars of the SHA-256 of `bytes` — the deterministic conflict
/// artifact name component (every node minting an artifact for the same
/// incoming version produces the same name → recording is idempotent).
fn content_hash8(bytes: &[u8]) -> String {
    use sha2::{Digest, Sha256};
    let digest = Sha256::digest(bytes);
    digest[..4].iter().map(|b| format!("{b:02x}")).collect()
}

/// The two-tier context store rooted at a `tracked/` directory.
pub struct ContextStore {


/// First 8 hex chars of the SHA-256 of `bytes` — the deterministic conflict
/// artifact name component (every node minting an artifact for the same
/// incoming version produces the same name → recording is idempotent).
fn content_hash8(bytes: &[u8]) -> String {
    use sha2::{Digest, Sha256};
    let digest = Sha256::digest(bytes);
    digest[..4].iter().map(|b| format!("{b:02x}")).collect()
}

/// The two-tier context store rooted at a `tracked/` directory.
pub struct ContextStore {

+/// First 8 hex chars of the SHA-256 of `bytes` — the deterministic conflict

+/// artifact name component (every node minting an artifact for the same

+/// incoming version produces the same name → recording is idempotent).

+fn content_hash8(bytes: &[u8]) -> String {

+    let digest = Sha256::digest(bytes);

+    digest[..4].iter().map(|b| format!("{b:02x}")).collect()


Bash

cd C:/Users/decid/Documents/projects/spt-core && grep -n "sha2" Cargo.lock | head -3 && grep -n "ed25519" crates/spt-store/Cargo.toml crates/spt-proto/Cargo.toml

Check sha2 in tree


878: "sha2 0.10.9",
893: "sha2 0.11.0-rc.5",
1811: "sha2 0.11.0-rc.5",
crates/spt-proto/Cargo.toml:9:ed25519-dalek = { version = "2", features = ["rand_core"] }


Edit

[dependencies]
rusqlite = { version = "0.32", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
spt-proto = { path = "../spt-proto" }

[dependencies]
rusqlite = { version = "0.32", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
sha2 = "0.10"
spt-proto = { path = "../spt-proto" }


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-store\Cargo.toml has been updated successfully. (file state is current in your context — no need to Read it back)

[dependencies]
rusqlite = { version = "0.32", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
sha2 = "0.10"
spt-proto = { path = "../spt-proto" }

[package]
name = "spt-store"
version = "0.0.0"
edition = "2021"
publish = false
description = "spt-core persistence layer: atomic writes, spool, perch layout, registry, info.json. Built on spt-proto."

[dependencies]
rusqlite = { version = "0.32", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
spt-proto = { path = "../spt-proto" }

[target.'cfg(unix)'.dependencies]
libc = "0.2"

[dev-dependencies]
tempfile = "3"


Add conflict artifact tests + hazard docs. Test first:


    #[test] // [unit->REQ-HAZARD-ID-CHARSET] a path-smuggling id is refused before any branch/dir derives from it.
    fn invalid_id_refused() {

    #[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] a surfaced conflict preserves BOTH versions; only a resolve clears it.
    fn conflict_artifacts_preserve_both_versions() {
        let tmp = tempdir().unwrap();
        let cs = ContextStore::open_or_init_in(tmp.path()).unwrap();
        let live = cs.live_context_path("doyle").unwrap();
        std::fs::write(&live, "LOCAL version").unwrap();
        cs.commit_live("doyle", "local").unwrap();
        let wt = live.parent().unwrap().to_path_buf();

        // Surface a concurrent incoming: artifact minted, local untouched.
        let a1 = cs.record_conflict(&wt, LIVE_CONTEXT_FILE, b"REMOTE version").unwrap();
        assert_eq!(std::fs::read_to_string(&a1).unwrap(), "REMOTE version");
        assert_eq!(std::fs::read_to_string(&live).unwrap(), "LOCAL version");
        // Tracked: the artifact is in the branch tip (replicates like context).
        let rel = format!("{CONFLICT_DIR}/{}", a1.file_name().unwrap().to_string_lossy());
        assert!(cs.branch_store().read_at_tip(&agent_branch("doyle"), &rel).unwrap().is_some());

        // Idempotent: same incoming bytes → same artifact, no duplicate.
        let a2 = cs.record_conflict(&wt, LIVE_CONTEXT_FILE, b"REMOTE version").unwrap();
        assert_eq!(a1, a2);
        assert_eq!(cs.list_conflicts(&wt, Some(LIVE_CONTEXT_FILE)).unwrap().len(), 1);

        // A different incoming version is a second artifact, not an overwrite.
        cs.record_conflict(&wt, LIVE_CONTEXT_FILE, b"THIRD version").unwrap();
        assert_eq!(cs.list_conflicts(&wt, Some(LIVE_CONTEXT_FILE)).unwrap().len(), 2);

        // Resolve clears exactly this file's artifacts.
        assert_eq!(cs.clear_conflicts(&wt, LIVE_CONTEXT_FILE).unwrap(), 2);
        assert!(cs.list_conflicts(&wt, None).unwrap().is_empty());
        assert_eq!(cs.clear_conflicts(&wt, LIVE_CONTEXT_FILE).unwrap(), 0, "no-op when clean");
    }

    #[test] // [unit->REQ-HAZARD-ID-CHARSET] a path-smuggling id is refused before any branch/dir derives from it.
    fn invalid_id_refused() {


    #[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] a surfaced conflict preserves BOTH versions; only a resolve clears it.
    fn conflict_artifacts_preserve_both_versions() {
        let tmp = tempdir().unwrap();
        let cs = ContextStore::open_or_init_in(tmp.path()).unwrap();
        let live = cs.live_context_path("doyle").unwrap();
        std::fs::write(&live, "LOCAL version").unwrap();
        cs.commit_live("doyle", "local").unwrap();
        let wt = live.parent().unwrap().to_path_buf();

        // Surface a concurrent incoming: artifact minted, local untouched.
        let a1 = cs.record_conflict(&wt, LIVE_CONTEXT_FILE, b"REMOTE version").unwrap();
        assert_eq!(std::fs::read_to_string(&a1).unwrap(), "REMOTE version");
        assert_eq!(std::fs::read_to_string(&live).unwrap(), "LOCAL version");
        // Tracked: the artifact is in the branch tip (replicates like context).
        let rel = format!("{CONFLICT_DIR}/{}", a1.file_name().unwrap().to_string_lossy());
        assert!(cs.branch_store().read_at_tip(&agent_branch("doyle"), &rel).unwrap().is_some());

        // Idempotent: same incoming bytes → same artifact, no duplicate.
        let a2 = cs.record_conflict(&wt, LIVE_CONTEXT_FILE, b"REMOTE version").unwrap();
        assert_eq!(a1, a2);
        assert_eq!(cs.list_conflicts(&wt, Some(LIVE_CONTEXT_FILE)).unwrap().len(), 1);

        // A different incoming version is a second artifact, not an overwrite.
        cs.record_conflict(&wt, LIVE_CONTEXT_FILE, b"THIRD version").unwrap();
        assert_eq!(cs.list_conflicts(&wt, Some(LIVE_CONTEXT_FILE)).unwrap().len(), 2);

        // Resolve clears exactly this file's artifacts.
        assert_eq!(cs.clear_conflicts(&wt, LIVE_CONTEXT_FILE).unwrap(), 2);
        assert!(cs.list_conflicts(&wt, None).unwrap().is_empty());
        assert_eq!(cs.clear_conflicts(&wt, LIVE_CONTEXT_FILE).unwrap(), 0, "no-op when clean");
    }

    #[test] // [unit->REQ-HAZARD-ID-CHARSET] a path-smuggling id is refused before any branch/dir derives from it.
    fn invalid_id_refused() {

+    #[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] a surfaced conflict preserves BOTH versions; only a resolve clears it.

+    fn conflict_artifacts_preserve_both_versions() {

+        let cs = ContextStore::open_or_init_in(tmp.path()).unwrap();

+        let live = cs.live_context_path("doyle").unwrap();

+        std::fs::write(&live, "LOCAL version").unwrap();

+        cs.commit_live("doyle", "local").unwrap();

+        let wt = live.parent().unwrap().to_path_buf();

+        // Surface a concurrent incoming: artifact minted, local untouched.

+        let a1 = cs.record_conflict(&wt, LIVE_CONTEXT_FILE, b"REMOTE version").unwrap();

+        assert_eq!(std::fs::read_to_string(&a1).unwrap(), "REMOTE version");

+        assert_eq!(std::fs::read_to_string(&live).unwrap(), "LOCAL version");

+        // Tracked: the artifact is in the branch tip (replicates like context).

+        let rel = format!("{CONFLICT_DIR}/{}", a1.file_name().unwrap().to_string_lossy());

+        assert!(cs.branch_store().read_at_tip(&agent_branch("doyle"), &rel).unwrap().is_some());

+        // Idempotent: same incoming bytes → same artifact, no duplicate.

+        let a2 = cs.record_conflict(&wt, LIVE_CONTEXT_FILE, b"REMOTE version").unwrap();

+        assert_eq!(cs.list_conflicts(&wt, Some(LIVE_CONTEXT_FILE)).unwrap().len(), 1);

+        // A different incoming version is a second artifact, not an overwrite.

+        cs.record_conflict(&wt, LIVE_CONTEXT_FILE, b"THIRD version").unwrap();

+        assert_eq!(cs.list_conflicts(&wt, Some(LIVE_CONTEXT_FILE)).unwrap().len(), 2);

+        // Resolve clears exactly this file's artifacts.

+        assert_eq!(cs.clear_conflicts(&wt, LIVE_CONTEXT_FILE).unwrap(), 2);

+        assert!(cs.list_conflicts(&wt, None).unwrap().is_empty());

+        assert_eq!(cs.clear_conflicts(&wt, LIVE_CONTEXT_FILE).unwrap(), 0, "no-op when clean");


Bash

cd C:/Users/decid/Documents/projects/spt-core && cargo test -p spt-store --lib --quiet 2>&1 | grep -E "test result|error\[|FAILED|panicked" | head -10

Test conflict artifacts


test result: ok. 108 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.60s


Register hazard 6.6 + req:


Edit

- **Sister cite:** CHANGELOG v1.11.6; `src/owl/echo_commune.rs`.

---

- **Sister cite:** CHANGELOG v1.11.6; `src/owl/echo_commune.rs`.

### 6.6 Surfaced context conflicts preserve both versions until dominated
- **Failure:** a cross-node concurrent context write (version vectors, neither dominates) gets auto-picked or partially dropped — half a mind silently lost.
- **Invariant:** a surfaced concurrent pair is durably preserved (both versions) until a strictly dominating write clears it; no merge/reconcile failure path may discard an unmerged version. Resolution is the Psyche reconcile turn (ADR-0013), whose merged write `join(vA,vB)+bump` dominates both parents — only that dominance clears the artifacts.
- **spt-core mapping:** `ContextStore::record_conflict` (tracked `.conflicts/` artifacts, content-hash named, idempotent, replicate like context) / `list_conflicts` / `clear_conflicts` (dominating-write-only). Local working file stays untouched while a conflict is pending.
- **Origin:** ADR-0013 design invariant (red-team #7's "wall-clock loses concurrent writes" closed M4-D6), not a sister bug — registered ahead of the wire path so D6c is born conformant.

---


The file C:\Users\decid\Documents\projects\spt-core\docs\KNOWN-HAZARDS.md has been updated successfully. (file state is current in your context — no need to Read it back)

- **Sister cite:** CHANGELOG v1.11.6; `src/owl/echo_commune.rs`.

---

- **Sister cite:** CHANGELOG v1.11.6; `src/owl/echo_commune.rs`.

### 6.6 Surfaced context conflicts preserve both versions until dominated
- **Failure:** a cross-node concurrent context write (version vectors, neither dominates) gets auto-picked or partially dropped — half a mind silently lost.
- **Invariant:** a surfaced concurrent pair is durably preserved (both versions) until a strictly dominating write clears it; no merge/reconcile failure path may discard an unmerged version. Resolution is the Psyche reconcile turn (ADR-0013), whose merged write `join(vA,vB)+bump` dominates both parents — only that dominance clears the artifacts.
- **spt-core mapping:** `ContextStore::record_conflict` (tracked `.conflicts/` artifacts, content-hash named, idempotent, replicate like context) / `list_conflicts` / `clear_conflicts` (dominating-write-only). Local working file stays untouched while a conflict is pending.
- **Origin:** ADR-0013 design invariant (red-team #7's "wall-clock loses concurrent writes" closed M4-D6), not a sister bug — registered ahead of the wire path so D6c is born conformant.

---

- **spt-core mapping:** cross-node Psyche sync (ADR-0003) makes this multi-writer across machines — the precedence marker must include node identity **plus a per-node version vector** (entries from each node's monotonic `EpochSource`; wall-clock never orders). Distributed rule (ADR-0013, M4-D6): dominate→accept, dominated→drop, **concurrent→surface as durable replicated conflict artifacts + Psyche-reconcile on the active instance's node — never silent newest-wins, never lose either version**. The freshness rule (newest-and-newer-than-mine, per the cross-instance context-freshness feature) is the same guard read as vector dominance. Highest-value carryover for the sync design.

+### 6.6 Surfaced context conflicts preserve both versions until dominated

+- **Failure:** a cross-node concurrent context write (version vectors, neither dominates) gets auto-picked or partially dropped — half a mind silently lost.

+- **Invariant:** a surfaced concurrent pair is durably preserved (both versions) until a strictly dominating write clears it; no merge/reconcile failure path may discard an unmerged version. Resolution is the Psyche reconcile turn (ADR-0013), whose merged write `join(vA,vB)+bump` dominates both parents — only that dominance clears the artifacts.

+- **spt-core mapping:** `ContextStore::record_conflict` (tracked `.conflicts/` artifacts, content-hash named, idempotent, replicate like context) / `list_conflicts` / `clear_conflicts` (dominating-write-only). Local working file stays untouched while a conflict is pending.

+- **Origin:** ADR-0013 design invariant (red-team #7's "wall-clock loses concurrent writes" closed M4-D6), not a sister bug — registered ahead of the wire path so D6c is born conformant.

## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)


| 6.5 | Direct-write precedence marker (+ node id) | cross-node Psyche sync |

| 6.5 | Direct-write precedence marker (+ node id) | cross-node Psyche sync |
| 6.6 | Surfaced conflicts preserve both versions until dominated | context conflict artifacts (ADR-0013) |


| 6.5 | Direct-write precedence marker (+ node id) | cross-node Psyche sync |
| 6.6 | Surfaced conflicts preserve both versions until dominated | context conflict artifacts (ADR-0013) |

| 6.1 | Single path/registry source of truth | storage layout |

| 6.4 | Drop files supervisor-owned single-writer | runtime contract |

+| 6.6 | Surfaced conflicts preserve both versions until dominated | context conflict artifacts (ADR-0013) |

| 7.1 | Local `api` mutation authenticated to endpoint | api surface / broker IPC |

| 7.2 | Idempotent delivery across brain restart | broker↔brain IPC |

| 7.3 | Psyche outbound captured + `from=`/target stripped + reply-to-sender / notify-to-own-user | live-Psyche driver / daemon relay (ADR-0012) |


id = "REQ-HAZARD-DIRECT-WRITE-PRECEDENCE"
title = "Direct-write precedence marker (with node id) guards stale overwrite (6.5)"
required_stages = ["impl", "unit"]

id = "REQ-HAZARD-DIRECT-WRITE-PRECEDENCE"
title = "Direct-write precedence marker (with node id) guards stale overwrite (6.5)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-CONFLICT-BOTH-PRESERVED"
title = "A surfaced concurrent context pair is durably preserved (both versions, tracked artifacts) until a strictly dominating write clears it; no reconcile failure path discards an unmerged version (6.6, ADR-0013)"
required_stages = ["impl", "unit"]  # activated M4-D6b: ContextStore::record_conflict (tracked .conflicts/ artifacts, SHA-256-prefix named — idempotent across nodes, replicate like context; local working file untouched) + list_conflicts + clear_conflicts (dominating-write-only). unit = both-versions-preserved + idempotent-recording + multi-version-artifacts + resolve-clears-exactly-this-file + clean-no-op


The file C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml has been updated successfully. (file state is current in your context — no need to Read it back)

id = "REQ-HAZARD-DIRECT-WRITE-PRECEDENCE"
title = "Direct-write precedence marker (with node id) guards stale overwrite (6.5)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-CONFLICT-BOTH-PRESERVED"
title = "A surfaced concurrent context pair is durably preserved (both versions, tracked artifacts) until a strictly dominating write clears it; no reconcile failure path discards an unmerged version (6.6, ADR-0013)"
required_stages = ["impl", "unit"]  # activated M4-D6b: ContextStore::record_conflict (tracked .conflicts/ artifacts, SHA-256-prefix named — idempotent across nodes, replicate like context; local working file untouched) + list_conflicts + clear_conflicts (dominating-write-only). unit = both-versions-preserved + idempotent-recording + multi-version-artifacts + resolve-clears-exactly-this-file + clean-no-op

+title = "A surfaced concurrent context pair is durably preserved (both versions, tracked artifacts) until a strictly dominating write clears it; no reconcile failure path discards an unmerged version (6.6, ADR-0013)"

+required_stages = ["impl", "unit"]  # activated M4-D6b: ContextStore::record_conflict (tracked .conflicts/ artifacts, SHA-256-prefix named — idempotent across nodes, replicate like context; local working file untouched) + list_conflicts + clear_conflicts (dominating-write-only). unit = both-versions-preserved + idempotent-recording + multi-version-artifacts + resolve-clears-exactly-this-file + clean-no-op

# --- Added 2026-05-31 from Stage A red-team + Spike #1 (inactive until M-activation) ---


Bash

cd C:/Users/decid/Documents/projects/spt-core && traceable-reqs check > /dev/null 2>&1; echo "exit=$?" && cargo clippy --workspace --all-targets 2>&1 | grep -cE "^warning|^error"; cargo test --workspace --quiet 2>&1 | grep -E "FAILED|panicked|error\[" | head -5; echo ALL_DONE

Check traceability + clippy + full tests


exit=0
3
ALL_DONE


cd C:/Users/decid/Documents/projects/spt-core && cargo clippy --workspace --all-targets 2>&1 | grep -E "^warning|^error" -A8 | head -40

Show clippy warnings


warning: method `from_str` can be confused for the standard trait method `std::str::FromStr::from_str`
  --> crates\spt-store\src\contextmark.rs:46:5
   |
46 | /     pub fn from_str(s: &str) -> Option<WriteSource> {
47 | |         match s {
48 | |             "direct" => Some(WriteSource::Direct),
49 | |             "llm" => Some(WriteSource::Llm),
...  |
52 | |     }
--
warning: `spt-store` (lib) generated 1 warning
warning: `spt-store` (lib test) generated 1 warning (1 duplicate)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.51s


cd C:/Users/decid/Documents/projects/spt-core && sed -i 's/pub fn from_str(s: \&str) -> Option<WriteSource>/pub fn from_token(s: \&str) -> Option<WriteSource>/; s/source = WriteSource::from_str(v);/source = WriteSource::from_token(v);/' crates/spt-store/src/contextmark.rs && cargo clippy --workspace --all-targets 2>&1 | grep -cE "^warning: |^error"; echo CLIPPY

Rename from_str to from_token + re-clippy


0
CLIPPY


1	//! The context precedence marker (KNOWN-HAZARDS 6.5, distributed form) — every
2	//! context file's first line, and the **merge authority** for cross-node sync
3	//! (ADR-0013).
4	//!
5	//! ```text
6	//! <!-- spt:source=llm spt:routed_at_ms=1717200000000 spt:node=ab12… spt:vector=ab12…:3,ee34…:5 -->
7	//! ```
8	//!
9	//! - `source` + `routed_at_ms` — the local 6.5 guard's inputs (direct beats
10	//!   stale llm inside the protection window). `routed_at_ms` is **wall-clock
11	//!   and never orders a merge** — it is at most the human tiebreaker hint
12	//!   inside a surfaced conflict.
13	//! - `node` — the authoring node (Ed25519 pubkey hex), added for cross-node.
14	//! - `vector` — a per-node **version vector**: one monotonic epoch per node
15	//!   ([`crate::epoch::EpochSource`], the same counter behind the D3b registry
16	//!   lease — one ordering source serves both, by design). The vector is the
17	//!   ordering authority: dominate→accept, dominated→drop, concurrent→surface.
18	//!
19	//! v1 markers (no `node`/`vector`) parse as an **empty vector** (⊥): any real
20	//! vector dominates them, and two ⊥ writes with differing content are
21	//! **concurrent** (surfaced, never silently newest-wins).
22	//!
23	//! Lives in spt-store (not spt-live) so the D6c sync path — which sits in the
24	//! net/daemon layers — and the live-agent write path share one parser; the
25	//! layering forbids net→live edges (R-ARCH-1).
26	// [impl->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE]
27
28	use std::collections::BTreeMap;
29
30	/// Who authored a context write. `Direct` = spt-core / operator / sync
31	/// propagation (authoritative, fresh); `Llm` = a model snapshot (possibly
32	/// stale) — the case the local 6.5 guard protects against.
33	#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34	pub enum WriteSource {
35	    Direct,
36	    Llm,
37	}
38
39	impl WriteSource {
40	    pub fn as_str(self) -> &'static str {
41	        match self {
42	            WriteSource::Direct => "direct",
43	            WriteSource::Llm => "llm",
44	        }
45	    }
46	    pub fn from_token(s: &str) -> Option<WriteSource> {
47	        match s {
48	            "direct" => Some(WriteSource::Direct),
49	            "llm" => Some(WriteSource::Llm),
50	            _ => None,
51	        }
52	    }
53	}
54
55	/// A per-node version vector: `node id → that node's epoch at write time`.
56	/// Empty = ⊥ (a legacy/v1 write with no causal history).
57	#[derive(Debug, Clone, Default, PartialEq, Eq)]
58	pub struct VersionVector(pub BTreeMap<String, u64>);
59
60	/// How two vectors relate — the merge-ordering verdict.
61	#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62	pub enum VectorOrder {
63	    /// Identical histories.
64	    Equal,
65	    /// `self` strictly dominates `other` (knows everything it knows, and more).
66	    Dominates,
67	    /// `self` is strictly dominated by `other`.
68	    Dominated,
69	    /// Neither dominates — concurrent writes.
70	    Concurrent,
71	}
72
73	impl VersionVector {
74	    /// Render as the marker token value: `node:epoch,…` sorted by node
75	    /// (BTreeMap order — deterministic). Empty vector renders empty.
76	    pub fn render(&self) -> String {
77	        self.0
78	            .iter()
79	            .map(|(n, e)| format!("{n}:{e}"))
80	            .collect::<Vec<_>>()
81	            .join(",")
82	    }
83
84	    /// Parse the marker token value. Undecodable entries are skipped (tolerant,
85	    /// 4.2 posture) — a corrupt vector degrades toward ⊥, which can only make
86	    /// merges *more* conservative (conflict-surfacing), never silently lossy.
87	    pub fn parse(s: &str) -> VersionVector {
88	        let mut map = BTreeMap::new();
89	        for entry in s.split(',') {
90	            if let Some((node, epoch)) = entry.rsplit_once(':') {
91	                if let Ok(e) = epoch.trim().parse::<u64>() {
92	                    if !node.trim().is_empty() {
93	                        map.insert(node.trim().to_string(), e);
94	                    }
95	                }
96	            }
97	        }
98	        VersionVector(map)
99	    }
100
101	    /// Compare two vectors for merge ordering.
102	    pub fn compare(&self, other: &VersionVector) -> VectorOrder {
103	        let mut self_ahead = false;
104	        let mut other_ahead = false;
105	        for (n, &e) in &self.0 {
106	            match other.0.get(n) {
107	                Some(&oe) if oe > e => other_ahead = true,
108	                Some(&oe) if oe < e => self_ahead = true,
109	                Some(_) => {}
110	                None => self_ahead = true,
111	            }
112	        }
113	        for n in other.0.keys() {
114	            if !self.0.contains_key(n) {
115	                other_ahead = true;
116	            }
117	        }
118	        match (self_ahead, other_ahead) {
119	            (false, false) => VectorOrder::Equal,
120	            (true, false) => VectorOrder::Dominates,
121	            (false, true) => VectorOrder::Dominated,
122	            (true, true) => VectorOrder::Concurrent,
123	        }
124	    }
125
126	    /// The pointwise maximum of two vectors — the merge parent a reconciled
127	    /// write starts from (ADR-0013: `join(vA, vB)` + a bump dominates both).
128	    pub fn join(&self, other: &VersionVector) -> VersionVector {
129	        let mut out = self.0.clone();
130	        for (n, &e) in &other.0 {
131	            let slot = out.entry(n.clone()).or_insert(0);
132	            if e > *slot {
133	                *slot = e;
134	            }
135	        }
136	        VersionVector(out)
137	    }
138
139	    /// Advance `node`'s entry to `epoch` (the author's fresh
140	    /// [`EpochSource`](crate::epoch::EpochSource) stamp).
141	    pub fn bump(&mut self, node: &str, epoch: u64) {
142	        let slot = self.0.entry(node.to_string()).or_insert(0);
143	        if epoch > *slot {
144	            *slot = epoch;
145	        }
146	    }
147	}
148
149	/// The full precedence marker — a context file's first line.
150	#[derive(Debug, Clone, PartialEq, Eq)]
151	pub struct Marker {
152	    pub source: WriteSource,
153	    /// Wall-clock stamp — the local-window guard input + the human tiebreaker
154	    /// hint. NEVER a merge-ordering input.
155	    pub routed_at_ms: u64,
156	    /// Authoring node id (pubkey hex); `None` on a pre-identity / v1 write.
157	    pub node: Option<String>,
158	    /// The write's causal history; empty = ⊥ (v1/legacy).
159	    pub vector: VersionVector,
160	}
161
162	impl Marker {
163	    /// Render as the HTML-comment first line (no trailing newline). v2 tokens
164	    /// are emitted only when present, so pre-identity writes render exactly the
165	    /// v1 shape.
166	    pub fn render(&self) -> String {
167	        let mut s = format!(
168	            "<!-- spt:source={} spt:routed_at_ms={}",
169	            self.source.as_str(),
170	            self.routed_at_ms
171	        );
172	        if let Some(node) = &self.node {
173	            s.push_str(&format!(" spt:node={node}"));
174	        }
175	        if !self.vector.0.is_empty() {
176	            s.push_str(&format!(" spt:vector={}", self.vector.render()));
177	        }
178	        s.push_str(" -->");
179	        s
180	    }
181
182	    /// Parse a marker line. `None` if the line isn't a well-formed marker (an
183	    /// unmarked / hand-edited file ⇒ no prior precedence, any write proceeds).
184	    /// v1 lines (no node/vector tokens) parse with `node=None`, vector ⊥.
185	    pub fn parse(line: &str) -> Option<Marker> {
186	        let inner = line.trim().strip_prefix("<!--")?.strip_suffix("-->")?.trim();
187	        let mut source = None;
188	        let mut routed_at_ms = None;
189	        let mut node = None;
190	        let mut vector = VersionVector::default();
191	        for tok in inner.split_whitespace() {
192	            if let Some(v) = tok.strip_prefix("spt:source=") {
193	                source = WriteSource::from_token(v);
194	            } else if let Some(v) = tok.strip_prefix("spt:routed_at_ms=") {
195	                routed_at_ms = v.parse::<u64>().ok();
196	            } else if let Some(v) = tok.strip_prefix("spt:node=") {

... [145 lines truncated] ...


Commit D6b-1:


cd C:/Users/decid/Documents/projects/spt-core && git add -A && git commit -m "feat(store): M4-D6b distributed precedence — marker v2, version vectors, conflict artifacts

- contextmark.rs (spt-store, shared by the live write path and the D6c sync
  path — net layer can't reach spt-live): Marker v2 grows spt:node +
  spt:vector; v1 lines parse as ⊥ vector; pre-identity markers render the
  exact v1 shape. VersionVector: compare (dominate/dominated/equal/concurrent),
  join, bump; tolerant parse degrades toward ⊥ (more conservative, never
  lossy). decide_merge: dominate→accept, dominated→drop, concurrent + ⊥⊥ →
  Conflict — never silent newest-wins; wall-clock plays no part (tested).
- spt-live context.rs: guarded write now stamps node + vector — the file's
  prior vector with this node's entry bumped by a fresh EpochSource epoch
  (the D3b lease counter, unified by design). Pre-identity homes stamp v1
  and never mint node.key as a side effect (tested).
- nodeid.rs: load_existing — the non-minting read for write-path stamping.
- contextstore.rs: conflict artifacts (hazard 6.6, ADR-0013) —
  record_conflict (tracked .conflicts/, SHA-256-prefix names: idempotent
  across nodes, replicate like context, local file untouched), list_conflicts,
  clear_conflicts (dominating-write-only).
- KNOWN-HAZARDS 6.6 registered + REQ-HAZARD-CONFLICT-BOTH-PRESERVED activated
  [impl, unit] (rule 3).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>" && git push

Commit + push D6b-1


warning: in the working copy of 'Cargo.lock', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-live/src/context.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-store/Cargo.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-store/src/contextstore.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-store/src/lib.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-store/src/nodeid.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/KNOWN-HAZARDS.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'traceable-reqs.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-store/src/contextmark.rs', LF will be replaced by CRLF the next time Git touches it
[dev-freeform 7e9c2b5] feat(store): M4-D6b distributed precedence — marker v2, version vectors, conflict artifacts
 9 files changed, 594 insertions(+), 112 deletions(-)
 create mode 100644 crates/spt-store/src/contextmark.rs
To https://github.com/SaberMage/spt-core.git
   1ef9802..7e9c2b5  dev-freeform -> dev-freeform


cd C:/Users/decid/Documents/projects/spt-core && for i in 1 2 3 4 5 6 7; do sleep 60; out=$(gh run list --branch dev-freeform --limit 1 --json conclusion,status,headSha); echo "$out"; if echo "$out" | grep -q '"status":"completed"'; then break; fi; done

Poll CI for 7e9c2b5


Command running in background with ID: bpwsjtm3v. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\bpwsjtm3v.output. You will be notified when it completes. To check interim output, use Read on that file path.


While CI runs — D6b-2 prep. Check manifest role plumbing:


107:    // the `psyche_*`/`echo_commune`/`signoff` roles are parsed here but only
110:    pub self_: Option<SessionRole>,
112:    pub psyche_init: Option<SessionRole>,
114:    pub psyche_resume: Option<SessionRole>,
116:    pub echo_commune: Option<SessionRole>,
118:    pub signoff: Option<SessionRole>,
127:            && self.psyche_init.is_none()
128:            && self.psyche_resume.is_none()
129:            && self.echo_commune.is_none()
135:    pub fn roles(&self) -> Vec<(&'static str, &SessionRole)> {
139:            ("psyche_init", &self.psyche_init),
140:            ("psyche_resume", &self.psyche_resume),
141:            ("echo_commune", &self.echo_commune),
157:pub struct SessionRole {
633:[session.psyche_init]
707:        let role = m.session.psyche_init.as_ref().unwrap();


crates\spt-daemon\src\attach.rs:54:/// Stamp (or clear) the endpoint's `driven_by` remote-drive marker — the
crates\spt-daemon\src\attach.rs:59:fn mark_driven_by(endpoint: &str, origin_node: Option<&str>) {
crates\spt-daemon\src\attach.rs:61:    let _ = spt_store::info::set_driven_by(&perch, origin_node);
crates\spt-daemon\src\attach.rs:123:                            mark_driven_by(endpoint, Some(origin_node));
crates\spt-daemon\src\attach.rs:142:                    mark_driven_by(endpoint, None);
crates\spt-daemon\src\attach.rs:160:                mark_driven_by(endpoint, None);
crates\spt-daemon\src\attach.rs:166:                    mark_driven_by(endpoint, None);


Write

//! The bounded live-Psyche turn driver (ADR-0012 / M4-PLAN D7.5a, pulled
//! forward into D6b per ADR-0013).
//!
//! Drives one Psyche turn as a **bounded, stdin-fed, stdout-CAPTURED** harness
//! invocation of the `psyche_resume` role (per-turn `--resume`, mirroring the
//! sister) — **never** the detached `Stdio::null()` `spawn_session`, whose
//! discarded stdout is exactly the KNOWN-HAZARDS 7.3 failure (every Psyche
//! reply silently dropped). The captured stdout **is** the Psyche's outbound
//! channel: D6b consumes it as the reconcile-merge output; D7.5b/c parse it
//! for `<EVENT type="reply|notify">` envelopes and relay through the daemon's
//! sanitize boundary.
//!
//! Bounded (REQ-HAZARD-SUBPROCESS-TIMEOUT): a hung harness is killed at the
//! deadline, never an indefinite block. Empty stdout is an **error** — a
//! driver that yields nothing is indistinguishable from the null-stdout bug
//! 7.3 guards against, so it fails loudly instead of returning success.
//
// [impl->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]
// [impl->REQ-HAZARD-SUBPROCESS-TIMEOUT]

use spt_runtime::{AgentRuntime, RuntimeError};
use std::collections::BTreeMap;
use std::time::Duration;

/// The role a live-Psyche turn resumes — the Psyche's persistent harness
/// session (`[session.psyche_resume]`, continue-existing).
pub const PSYCHE_TURN_ROLE: &str = "psyche_resume";

/// Default per-turn budget. Generous — a turn is one LLM call — but finite;
/// the caller tunes per use (the reconcile turn passes its own).
pub const DEFAULT_TURN_TIMEOUT: Duration = Duration::from_secs(300);

/// A failed Psyche turn.
#[derive(Debug)]
pub enum TurnError {
    /// Spawn/timeout/template failure from the runtime (timeout = killed, 5.3).
    Runtime(RuntimeError),
    /// The harness exited non-zero; stderr preserved.
    Failed { status_code: Option<i32>, stderr: String },
    /// The turn completed but produced no stdout — indistinguishable from the
    /// 7.3 null-stdout bug, so it is an error, never a silent success.
    EmptyOutput,
}

impl std::fmt::Display for TurnError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            TurnError::Runtime(e) => write!(f, "psyche turn runtime: {e}"),
            TurnError::Failed { status_code, stderr } => {
                write!(f, "psyche turn exited {status_code:?}: {}", stderr.trim())
            }
            TurnError::EmptyOutput => write!(
                f,
                "psyche turn produced no stdout (null-stdout shape — hazard 7.3)"
            ),
        }
    }
}

impl std::error::Error for TurnError {}

/// Run one bounded live-Psyche turn: feed `stdin` to the `psyche_resume` role,
/// capture and return its stdout. The substitution `keys` must carry what the
/// adapter's resume template needs (typically `session_id`).
// [impl->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]
pub fn run_psyche_turn(
    runtime: &dyn AgentRuntime,
    keys: &BTreeMap<String, String>,
    stdin: &[u8],
    timeout: Duration,
) -> Result<String, TurnError> {
    let out = runtime
        .run_bounded_stdin(PSYCHE_TURN_ROLE, keys, timeout, Some(stdin))
        .map_err(TurnError::Runtime)?;
    if !out.success() {
        return Err(TurnError::Failed { status_code: out.status_code, stderr: out.stderr });
    }
    if out.stdout.trim().is_empty() {
        return Err(TurnError::EmptyOutput);
    }
    Ok(out.stdout)
}

#[cfg(test)]
mod tests {
    use super::*;
    use spt_runtime::{Manifest, ManifestRuntime};

    #[cfg(windows)]
    const ECHO_STDIN: &str = "cmd /C findstr x*"; // copies stdin to stdout
    #[cfg(unix)]
    const ECHO_STDIN: &str = "sh -c cat";
    #[cfg(windows)]
    const NO_OUTPUT: &str = "cmd /C exit 0";
    #[cfg(unix)]
    const NO_OUTPUT: &str = "sh -c true";

    fn rt(cmd: &str) -> ManifestRuntime {
        let toml = format!(
            "[adapter]\nname=\"mock\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\
             [session.psyche_resume]\ncommand='{cmd}'\n"
        );
        ManifestRuntime::new(Manifest::from_toml_str(&toml).unwrap())
    }

    #[test] // [unit->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY] the turn is stdin-fed and its stdout is CAPTURED (not discarded).
    fn turn_captures_stdout() {
        let out = run_psyche_turn(
            &rt(ECHO_STDIN),
            &BTreeMap::new(),
            b"merged mind body",
            Duration::from_secs(30),
        )
        .expect("turn");
        assert!(out.contains("merged mind body"), "stdout must round-trip: {out:?}");
    }

    #[test] // [unit->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY] a no-stdout turn FAILS the guard (7.3's null-stdout shape).
    fn empty_stdout_is_an_error() {
        let err = run_psyche_turn(
            &rt(NO_OUTPUT),
            &BTreeMap::new(),
            b"input",
            Duration::from_secs(30),
        )
        .unwrap_err();
        assert!(matches!(err, TurnError::EmptyOutput), "got {err}");
    }
}


File created successfully at: C:\Users\decid\Documents\projects\spt-core\crates\spt-live\src\turn.rs (file state is current in your context — no need to Read it back)

//! The bounded live-Psyche turn driver (ADR-0012 / M4-PLAN D7.5a, pulled
//! forward into D6b per ADR-0013).
//!
//! Drives one Psyche turn as a **bounded, stdin-fed, stdout-CAPTURED** harness
//! invocation of the `psyche_resume` role (per-turn `--resume`, mirroring the
//! sister) — **never** the detached `Stdio::null()` `spawn_session`, whose
//! discarded stdout is exactly the KNOWN-HAZARDS 7.3 failure (every Psyche
//! reply silently dropped). The captured stdout **is** the Psyche's outbound
//! channel: D6b consumes it as the reconcile-merge output; D7.5b/c parse it
//! for `<EVENT type="reply|notify">` envelopes and relay through the daemon's
//! sanitize boundary.
//!
//! Bounded (REQ-HAZARD-SUBPROCESS-TIMEOUT): a hung harness is killed at the
//! deadline, never an indefinite block. Empty stdout is an **error** — a
//! driver that yields nothing is indistinguishable from the null-stdout bug
//! 7.3 guards against, so it fails loudly instead of returning success.
//
// [impl->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]
// [impl->REQ-HAZARD-SUBPROCESS-TIMEOUT]

use spt_runtime::{AgentRuntime, RuntimeError};
use std::collections::BTreeMap;
use std::time::Duration;

/// The role a live-Psyche turn resumes — the Psyche's persistent harness
/// session (`[session.psyche_resume]`, continue-existing).
pub const PSYCHE_TURN_ROLE: &str = "psyche_resume";

/// Default per-turn budget. Generous — a turn is one LLM call — but finite;
/// the caller tunes per use (the reconcile turn passes its own).
pub const DEFAULT_TURN_TIMEOUT: Duration = Duration::from_secs(300);

/// A failed Psyche turn.
#[derive(Debug)]
pub enum TurnError {
    /// Spawn/timeout/template failure from the runtime (timeout = killed, 5.3).
    Runtime(RuntimeError),
    /// The harness exited non-zero; stderr preserved.
    Failed { status_code: Option<i32>, stderr: String },
    /// The turn completed but produced no stdout — indistinguishable from the
    /// 7.3 null-stdout bug, so it is an error, never a silent success.
    EmptyOutput,
}

impl std::fmt::Display for TurnError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            TurnError::Runtime(e) => write!(f, "psyche turn runtime: {e}"),
            TurnError::Failed { status_code, stderr } => {
                write!(f, "psyche turn exited {status_code:?}: {}", stderr.trim())
            }
            TurnError::EmptyOutput => write!(
                f,
                "psyche turn produced no stdout (null-stdout shape — hazard 7.3)"
            ),
        }
    }
}

impl std::error::Error for TurnError {}

/// Run one bounded live-Psyche turn: feed `stdin` to the `psyche_resume` role,
/// capture and return its stdout. The substitution `keys` must carry what the
/// adapter's resume template needs (typically `session_id`).
// [impl->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]
pub fn run_psyche_turn(
    runtime: &dyn AgentRuntime,
    keys: &BTreeMap<String, String>,
    stdin: &[u8],
    timeout: Duration,
) -> Result<String, TurnError> {
    let out = runtime
        .run_bounded_stdin(PSYCHE_TURN_ROLE, keys, timeout, Some(stdin))
        .map_err(TurnError::Runtime)?;
    if !out.success() {
        return Err(TurnError::Failed { status_code: out.status_code, stderr: out.stderr });
    }
    if out.stdout.trim().is_empty() {
        return Err(TurnError::EmptyOutput);
    }
    Ok(out.stdout)
}

#[cfg(test)]
mod tests {
    use super::*;
    use spt_runtime::{Manifest, ManifestRuntime};

    #[cfg(windows)]
    const ECHO_STDIN: &str = "cmd /C findstr x*"; // copies stdin to stdout
    #[cfg(unix)]
    const ECHO_STDIN: &str = "sh -c cat";
    #[cfg(windows)]
    const NO_OUTPUT: &str = "cmd /C exit 0";
    #[cfg(unix)]
    const NO_OUTPUT: &str = "sh -c true";

    fn rt(cmd: &str) -> ManifestRuntime {
        let toml = format!(
            "[adapter]\nname=\"mock\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\
             [session.psyche_resume]\ncommand='{cmd}'\n"
        );
        ManifestRuntime::new(Manifest::from_toml_str(&toml).unwrap())
    }

    #[test] // [unit->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY] the turn is stdin-fed and its stdout is CAPTURED (not discarded).
    fn turn_captures_stdout() {
        let out = run_psyche_turn(
            &rt(ECHO_STDIN),
            &BTreeMap::new(),
            b"merged mind body",
            Duration::from_secs(30),
        )
        .expect("turn");
        assert!(out.contains("merged mind body"), "stdout must round-trip: {out:?}");
    }

    #[test] // [unit->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY] a no-stdout turn FAILS the guard (7.3's null-stdout shape).
    fn empty_stdout_is_an_error() {
        let err = run_psyche_turn(
            &rt(NO_OUTPUT),
            &BTreeMap::new(),
            b"input",
            Duration::from_secs(30),
        )
        .unwrap_err();
        assert!(matches!(err, TurnError::EmptyOutput), "got {err}");
    }
}


Edit

pub fn write_context(
    path: &Path,
    body: &str,
    source: WriteSource,
    now_ms: u64,
    window_ms: u64,
) -> std::io::Result<WriteOutcome> {
    let existing = read_marker(path);
    let outcome = decide(existing.as_ref(), source, now_ms, window_ms);
    match outcome {
        WriteOutcome::Written { routed_at_ms } => {
            // Carry the file's causal history; bump our entry on an identified
            // node. Pre-identity (no node.key) stamps the v1 shape — never mint
            // an identity from a context write.
            let mut vector = existing.map(|m| m.vector).unwrap_or_default();
            let node = spt_store::nodeid::load_existing().map(|i| i.public_key().to_hex());
            if let Some(n) = &node {
                let mut epochs = EpochSource::load();
                vector.bump(n, epochs.next_epoch()?);
            }
            let marker = Marker { source, routed_at_ms, node, vector };
            let content = format!("{}\n{}", marker.render(), body);
            atomic_write_string(path, &content)?;
        }
        WriteOutcome::Suppressed { direct_at_ms } => {
            // [impl->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] stale llm overwrite
            // within the window: suppress + log, never clobber the fresher write.
            eprintln!(
                "PRECEDENCE_SUPPRESS:{} (llm write within {}ms of direct write @ {})",
                path.display(),
                window_ms,
                direct_at_ms
            );
        }
    }
    Ok(outcome)
}

pub fn write_context(
    path: &Path,
    body: &str,
    source: WriteSource,
    now_ms: u64,
    window_ms: u64,
) -> std::io::Result<WriteOutcome> {
    write_context_joined(path, body, source, now_ms, window_ms, None)
}

/// [`write_context`] with an extra **vector base** joined into the stamp — the
/// reconciled-merge write shape (ADR-0013): pass `join` = the union of the
/// conflicting versions' vectors so the accepted write carries
/// `file ⊔ join + bump(self)` and **strictly dominates every parent**,
/// clearing the conflict subnet-wide as it propagates.
pub fn write_context_joined(
    path: &Path,
    body: &str,
    source: WriteSource,
    now_ms: u64,
    window_ms: u64,
    join: Option<&spt_store::contextmark::VersionVector>,
) -> std::io::Result<WriteOutcome> {
    let existing = read_marker(path);
    let outcome = decide(existing.as_ref(), source, now_ms, window_ms);
    match outcome {
        WriteOutcome::Written { routed_at_ms } => {
            // Carry the file's causal history (joined with any merge parents);
            // bump our entry on an identified node. Pre-identity (no node.key)
            // stamps the v1 shape — never mint an identity from a context write.
            let mut vector = existing.map(|m| m.vector).unwrap_or_default();
            if let Some(base) = join {
                vector = vector.join(base);
            }
            let node = spt_store::nodeid::load_existing().map(|i| i.public_key().to_hex());
            if let Some(n) = &node {
                let mut epochs = EpochSource::load();
                vector.bump(n, epochs.next_epoch()?);
            }
            let marker = Marker { source, routed_at_ms, node, vector };
            let content = format!("{}\n{}", marker.render(), body);
            atomic_write_string(path, &content)?;
        }
        WriteOutcome::Suppressed { direct_at_ms } => {
            // [impl->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] stale llm overwrite
            // within the window: suppress + log, never clobber the fresher write.
            eprintln!(
                "PRECEDENCE_SUPPRESS:{} (llm write within {}ms of direct write @ {})",
                path.display(),
                window_ms,
                direct_at_ms
            );
        }
    }
    Ok(outcome)
}