   Compiling spt-net v0.4.2 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
   Compiling spt-daemon v0.4.2 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 13.06s
     Running unittests src\lib.rs (target\debug\deps\spt_daemon-07653a48dd6fa27e.exe)

running 9 tests
test digest::tests::common_prefix_len_finds_the_unchanged_head ... ok
test digesthub::tests::project_and_publish_stores_latest ... ok
test digesthub::tests::subscribe_returns_current_base ... ok
test digesthub::tests::dropped_subscriber_is_pruned ... ok
test digesthub::tests::publish_delta_is_tail_then_full_refresh ... ok
test digesthub::tests::render_is_glanceable ... ok
test release::tests::artifact_digest_mismatch_is_rejected ... ok
test digest::tests::projects_a_log_less_endpoint_from_digest_log ... ok
test digest::tests::projects_empty_for_a_sourceless_endpoint ... ok

test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 268 filtered out; finished in 0.01s


Lib digest tests green. Now the integration E2E:


Bash

cd C:/Users/decid/Documents/projects/spt-core && cargo test -p spt-daemon --test digest 2>&1 | tail -25

Run daemon CC-fixture E2E


running 2 tests
test reproject_after_log_growth_delivers_a_delta ... FAILED
test harness_hosted_digest_projects_from_logs ... FAILED

failures:

---- reproject_after_log_growth_delivers_a_delta stdout ----

thread 'reproject_after_log_growth_delivers_a_delta' (55904) panicked at crates\spt-daemon\tests\digest.rs:122:57:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "The system cannot find the file specified." }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

---- harness_hosted_digest_projects_from_logs stdout ----

thread 'harness_hosted_digest_projects_from_logs' (52700) panicked at crates\spt-daemon\tests\digest.rs:99:10:
pull ok: Os { code: 2, kind: NotFound, message: "The system cannot find the file specified." }

failures:
    harness_hosted_digest_projects_from_logs
    reproject_after_log_growth_delivers_a_delta

test result: FAILED. 0 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s

error: test failed, to rerun pass `-p spt-daemon --test digest`


Connect race — the client pulls before the serve thread binds the socket. Add a retry helper:


Edit

/// Serve a fresh digest-control channel on a unique socket; returns its name.
fn serve_hub() -> String {
    let name = unique("digest");
    let hub = Arc::new(DigestHub::new());
    let n = name.clone();
    thread::spawn(move || {
        let _ = serve_digest_control(&n, hub);
    });
    name
}

/// Serve a fresh digest-control channel on a unique socket; returns its name.
fn serve_hub() -> String {
    let name = unique("digest");
    let hub = Arc::new(DigestHub::new());
    let n = name.clone();
    thread::spawn(move || {
        let _ = serve_digest_control(&n, hub);
    });
    name
}

/// Retry `op` until it succeeds or `dur` elapses — bridges the bind/accept lag
/// between the serve thread and the first client connect.
fn retry<T>(dur: Duration, mut op: impl FnMut() -> std::io::Result<T>) -> std::io::Result<T> {
    let start = Instant::now();
    loop {
        match op() {
            Ok(v) => return Ok(v),
            Err(e) if start.elapsed() >= dur => return Err(e),
            Err(_) => thread::sleep(Duration::from_millis(20)),
        }
    }
}


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

/// Serve a fresh digest-control channel on a unique socket; returns its name.
fn serve_hub() -> String {
    let name = unique("digest");
    let hub = Arc::new(DigestHub::new());
    let n = name.clone();
    thread::spawn(move || {
        let _ = serve_digest_control(&n, hub);
    });
    name
}

/// Retry `op` until it succeeds or `dur` elapses — bridges the bind/accept lag
/// between the serve thread and the first client connect.
fn retry<T>(dur: Duration, mut op: impl FnMut() -> std::io::Result<T>) -> std::io::Result<T> {
    let start = Instant::now();
    loop {
        match op() {
            Ok(v) => return Ok(v),
            Err(e) if start.elapsed() >= dur => return Err(e),
            Err(_) => thread::sleep(Duration::from_millis(20)),
        }
    }
}

//! Session-digest **log-projection** integration — the topology-independence
//! proof (ADR-0008 amendment / REQ-TERM-4).
//!
//! There is no broker and no PTY here: the endpoint is **harness-hosted** (a
//! Claude-Code-style per-session log is Path A's worked example). The daemon
//! projects its digest **on demand** from the adapter's `[history]` strategy
//! (`locate_normalize` over a fixture JSONL of published digest-records) and
//! serves it over the real digest-control channel:
//!
//! - **snapshot pull** ([`pull_snapshot`]) projects the current buffer and
//!   returns it — proving a log-sourced digest works with no broker-owned PTY
//!   (impossible under the superseded byte-parse shape);
//! - **delta-stream** ([`follow`]) + a [`reproject`] nudge after the log grows
//!   delivers the structured delta to a subscriber (the push-driven freshness
//!   the `api digest-entry` path also drives — autonomous file-watch is deferred
//!   to the consuming frontend milestone).
// [int->REQ-TERM-4]

use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};

use spt_daemon::{follow, pull_snapshot, reproject, serve_digest_control, DigestHub};
use spt_store::info::{write_info, InfoJson};
use spt_store::perch::{self, resolve_perch_path, ParentHint};

static SEQ: AtomicU32 = AtomicU32::new(0);
fn unique(prefix: &str) -> String {
    let n = SEQ.fetch_add(1, Ordering::Relaxed);
    format!("spt-daemon-digest-{prefix}-{}-{}.sock", std::process::id(), n)
}

/// A stdin pass-through "normalizer": the located transcript bytes round-trip to
/// stdout unchanged, so the fixture JSONL *is* the normalized record stream.
#[cfg(unix)]
const PASSTHROUGH: &str = "cat";
#[cfg(windows)]
const PASSTHROUGH: &str = "cmd /C findstr /r \".\"";

/// Register a harness adapter whose `[history]` is `locate_normalize` over
/// `fixture` (a digest-record JSONL), and establish a harness-hosted perch for
/// `id` that records the adapter — the daemon resolves the projection source from
/// these two facts alone (no broker, no live feed).
fn establish_cc_endpoint(id: &str, fixture: &std::path::Path) {
    // TOML literal (single-quoted) strings carry the path + command verbatim —
    // no escaping of Windows backslashes or the findstr quotes.
    let manifest = format!(
        "[adapter]\nname = \"mock-cc\"\nkind = \"harness\"\nversion = \"1\"\n\
         min_spt_core_version = \"0\"\n\n\
         [history]\nstrategy = \"locate_normalize\"\n\
         locate_template = '{}'\nnormalize_command = '{}'\n",
        fixture.display(),
        PASSTHROUGH,
    );
    let src = perch::spt_home().join("srcs").join("mock-cc-src");
    std::fs::create_dir_all(&src).unwrap();
    std::fs::write(src.join("manifest.toml"), manifest).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    let perch_path = resolve_perch_path(id, ParentHint::Infer);
    std::fs::create_dir_all(&perch_path).unwrap();
    let mut rec = InfoJson::new(id, "0", std::process::id(), "cc-session", "live_agent");
    rec.adapter = Some("mock-cc".to_string());
    write_info(&perch_path, &rec).unwrap();
}

/// Serve a fresh digest-control channel on a unique socket; returns its name.
fn serve_hub() -> String {
    let name = unique("digest");
    let hub = Arc::new(DigestHub::new());
    let n = name.clone();
    thread::spawn(move || {
        let _ = serve_digest_control(&n, hub);
    });
    name
}

#[test]
fn harness_hosted_digest_projects_from_logs() {
    let home = tempfile::tempdir().expect("tempdir");
    std::env::set_var("SPT_HOME", home.path());

    // A fixture JSONL of published digest-records — the harness's "session log".
    let fixture = home.path().join("cc-session.jsonl");
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"add a file\"}\n\
         {\"role\":\"agent\",\"text\":\"on it\"}\n\
         {\"role\":\"tool\",\"tool\":{\"name\":\"Write\",\"arg\":\"src/a.rs\"}}\n",
    )
    .unwrap();
    establish_cc_endpoint("cc", &fixture);

    let digest_name = serve_hub();

    // ── Snapshot pull: projected on demand from the log, no broker/PTY ───────
    let (digest, version) = pull_snapshot(&digest_name, "cc")
        .expect("pull ok")
        .expect("a digest projects for the harness-hosted endpoint");
    assert!(version >= 1, "the projection has a version");
    assert_eq!(digest.turns.len(), 1, "one user turn: {digest:?}");
    assert_eq!(digest.turns[0].input.as_deref(), Some("add a file"));
    let rendered = spt_daemon::render_digest(&digest);
    assert!(
        rendered.contains("» add a file") && rendered.contains("used: Write(src/a.rs)"),
        "glanceable render carries the turn + collapsed sprint: {rendered}"
    );
}

#[test]
fn reproject_after_log_growth_delivers_a_delta() {
    let home = tempfile::tempdir().expect("tempdir");
    std::env::set_var("SPT_HOME", home.path());

    let fixture = home.path().join("cc-session.jsonl");
    std::fs::write(&fixture, "{\"role\":\"input\",\"text\":\"first\"}\n").unwrap();
    establish_cc_endpoint("cc2", &fixture);
    let digest_name = serve_hub();

    // Seed the hub via a pull, then subscribe from that version.
    let (_d, base) = pull_snapshot(&digest_name, "cc2").unwrap().unwrap();

    let (tx, rx) = std::sync::mpsc::channel::<usize>();
    {
        let dname = digest_name.clone();
        thread::spawn(move || {
            let _ = follow(&dname, "cc2", base, |up| {
                if up.version > base {
                    // The grown log adds a second turn → its delta.
                    let last = up.turns.last().and_then(|t| t.input.clone());
                    if last.as_deref() == Some("second") {
                        let _ = tx.send(up.turns.len());
                        return false;
                    }
                }
                true
            });
        });
    }
    // Let the subscriber register, then grow the log and nudge a re-projection.
    thread::sleep(Duration::from_millis(80));
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"first\"}\n{\"role\":\"input\",\"text\":\"second\"}\n",
    )
    .unwrap();
    reproject(&digest_name, "cc2").expect("nudge a re-projection");

    let got = wait_recv(&rx, Duration::from_secs(5));
    assert!(got.is_some(), "the subscriber received the push-driven delta");
}

/// Block up to `dur` for one value on `rx`.
fn wait_recv(rx: &std::sync::mpsc::Receiver<usize>, dur: Duration) -> Option<usize> {
    let start = Instant::now();
    loop {
        if let Ok(v) = rx.try_recv() {
            return Some(v);
        }
        if start.elapsed() >= dur {
            return None;
        }
        thread::sleep(Duration::from_millis(15));
    }
}

+/// Retry `op` until it succeeds or `dur` elapses — bridges the bind/accept lag

+/// between the serve thread and the first client connect.

+fn retry<T>(dur: Duration, mut op: impl FnMut() -> std::io::Result<T>) -> std::io::Result<T> {

+            Err(e) if start.elapsed() >= dur => return Err(e),

+            Err(_) => thread::sleep(Duration::from_millis(20)),


    // ── Snapshot pull: projected on demand from the log, no broker/PTY ───────
    let (digest, version) = pull_snapshot(&digest_name, "cc")
        .expect("pull ok")
        .expect("a digest projects for the harness-hosted endpoint");

    // ── Snapshot pull: projected on demand from the log, no broker/PTY ───────
    let (digest, version) = retry(Duration::from_secs(3), || pull_snapshot(&digest_name, "cc"))
        .expect("pull ok")
        .expect("a digest projects for the harness-hosted endpoint");


    // ── Snapshot pull: projected on demand from the log, no broker/PTY ───────
    let (digest, version) = retry(Duration::from_secs(3), || pull_snapshot(&digest_name, "cc"))
        .expect("pull ok")
        .expect("a digest projects for the harness-hosted endpoint");

//! Session-digest **log-projection** integration — the topology-independence
//! proof (ADR-0008 amendment / REQ-TERM-4).
//!
//! There is no broker and no PTY here: the endpoint is **harness-hosted** (a
//! Claude-Code-style per-session log is Path A's worked example). The daemon
//! projects its digest **on demand** from the adapter's `[history]` strategy
//! (`locate_normalize` over a fixture JSONL of published digest-records) and
//! serves it over the real digest-control channel:
//!
//! - **snapshot pull** ([`pull_snapshot`]) projects the current buffer and
//!   returns it — proving a log-sourced digest works with no broker-owned PTY
//!   (impossible under the superseded byte-parse shape);
//! - **delta-stream** ([`follow`]) + a [`reproject`] nudge after the log grows
//!   delivers the structured delta to a subscriber (the push-driven freshness
//!   the `api digest-entry` path also drives — autonomous file-watch is deferred
//!   to the consuming frontend milestone).
// [int->REQ-TERM-4]

use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};

use spt_daemon::{follow, pull_snapshot, reproject, serve_digest_control, DigestHub};
use spt_store::info::{write_info, InfoJson};
use spt_store::perch::{self, resolve_perch_path, ParentHint};

static SEQ: AtomicU32 = AtomicU32::new(0);
fn unique(prefix: &str) -> String {
    let n = SEQ.fetch_add(1, Ordering::Relaxed);
    format!("spt-daemon-digest-{prefix}-{}-{}.sock", std::process::id(), n)
}

/// A stdin pass-through "normalizer": the located transcript bytes round-trip to
/// stdout unchanged, so the fixture JSONL *is* the normalized record stream.
#[cfg(unix)]
const PASSTHROUGH: &str = "cat";
#[cfg(windows)]
const PASSTHROUGH: &str = "cmd /C findstr /r \".\"";

/// Register a harness adapter whose `[history]` is `locate_normalize` over
/// `fixture` (a digest-record JSONL), and establish a harness-hosted perch for
/// `id` that records the adapter — the daemon resolves the projection source from
/// these two facts alone (no broker, no live feed).
fn establish_cc_endpoint(id: &str, fixture: &std::path::Path) {
    // TOML literal (single-quoted) strings carry the path + command verbatim —
    // no escaping of Windows backslashes or the findstr quotes.
    let manifest = format!(
        "[adapter]\nname = \"mock-cc\"\nkind = \"harness\"\nversion = \"1\"\n\
         min_spt_core_version = \"0\"\n\n\
         [history]\nstrategy = \"locate_normalize\"\n\
         locate_template = '{}'\nnormalize_command = '{}'\n",
        fixture.display(),
        PASSTHROUGH,
    );
    let src = perch::spt_home().join("srcs").join("mock-cc-src");
    std::fs::create_dir_all(&src).unwrap();
    std::fs::write(src.join("manifest.toml"), manifest).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    let perch_path = resolve_perch_path(id, ParentHint::Infer);
    std::fs::create_dir_all(&perch_path).unwrap();
    let mut rec = InfoJson::new(id, "0", std::process::id(), "cc-session", "live_agent");
    rec.adapter = Some("mock-cc".to_string());
    write_info(&perch_path, &rec).unwrap();
}

/// Serve a fresh digest-control channel on a unique socket; returns its name.
fn serve_hub() -> String {
    let name = unique("digest");
    let hub = Arc::new(DigestHub::new());
    let n = name.clone();
    thread::spawn(move || {
        let _ = serve_digest_control(&n, hub);
    });
    name
}

/// Retry `op` until it succeeds or `dur` elapses — bridges the bind/accept lag
/// between the serve thread and the first client connect.
fn retry<T>(dur: Duration, mut op: impl FnMut() -> std::io::Result<T>) -> std::io::Result<T> {
    let start = Instant::now();
    loop {
        match op() {
            Ok(v) => return Ok(v),
            Err(e) if start.elapsed() >= dur => return Err(e),
            Err(_) => thread::sleep(Duration::from_millis(20)),
        }
    }
}

#[test]
fn harness_hosted_digest_projects_from_logs() {
    let home = tempfile::tempdir().expect("tempdir");
    std::env::set_var("SPT_HOME", home.path());

    // A fixture JSONL of published digest-records — the harness's "session log".
    let fixture = home.path().join("cc-session.jsonl");
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"add a file\"}\n\
         {\"role\":\"agent\",\"text\":\"on it\"}\n\
         {\"role\":\"tool\",\"tool\":{\"name\":\"Write\",\"arg\":\"src/a.rs\"}}\n",
    )
    .unwrap();
    establish_cc_endpoint("cc", &fixture);

    let digest_name = serve_hub();

    // ── Snapshot pull: projected on demand from the log, no broker/PTY ───────
    let (digest, version) = pull_snapshot(&digest_name, "cc")
        .expect("pull ok")
        .expect("a digest projects for the harness-hosted endpoint");
    assert!(version >= 1, "the projection has a version");
    assert_eq!(digest.turns.len(), 1, "one user turn: {digest:?}");
    assert_eq!(digest.turns[0].input.as_deref(), Some("add a file"));
    let rendered = spt_daemon::render_digest(&digest);
    assert!(
        rendered.contains("» add a file") && rendered.contains("used: Write(src/a.rs)"),
        "glanceable render carries the turn + collapsed sprint: {rendered}"
    );
}

#[test]
fn reproject_after_log_growth_delivers_a_delta() {
    let home = tempfile::tempdir().expect("tempdir");
    std::env::set_var("SPT_HOME", home.path());

    let fixture = home.path().join("cc-session.jsonl");
    std::fs::write(&fixture, "{\"role\":\"input\",\"text\":\"first\"}\n").unwrap();
    establish_cc_endpoint("cc2", &fixture);
    let digest_name = serve_hub();

    // Seed the hub via a pull, then subscribe from that version.
    let (_d, base) = pull_snapshot(&digest_name, "cc2").unwrap().unwrap();

    let (tx, rx) = std::sync::mpsc::channel::<usize>();
    {
        let dname = digest_name.clone();
        thread::spawn(move || {
            let _ = follow(&dname, "cc2", base, |up| {
                if up.version > base {
                    // The grown log adds a second turn → its delta.
                    let last = up.turns.last().and_then(|t| t.input.clone());
                    if last.as_deref() == Some("second") {
                        let _ = tx.send(up.turns.len());
                        return false;
                    }
                }
                true
            });
        });
    }
    // Let the subscriber register, then grow the log and nudge a re-projection.
    thread::sleep(Duration::from_millis(80));
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"first\"}\n{\"role\":\"input\",\"text\":\"second\"}\n",
    )
    .unwrap();
    reproject(&digest_name, "cc2").expect("nudge a re-projection");

    let got = wait_recv(&rx, Duration::from_secs(5));
    assert!(got.is_some(), "the subscriber received the push-driven delta");
}

/// Block up to `dur` for one value on `rx`.
fn wait_recv(rx: &std::sync::mpsc::Receiver<usize>, dur: Duration) -> Option<usize> {
    let start = Instant::now();
    loop {
        if let Ok(v) = rx.try_recv() {
            return Some(v);
        }
        if start.elapsed() >= dur {
            return None;
        }
        thread::sleep(Duration::from_millis(15));
    }
}

-    let (digest, version) = pull_snapshot(&digest_name, "cc")

+    let (digest, version) = retry(Duration::from_secs(3), || pull_snapshot(&digest_name, "cc"))


    // Seed the hub via a pull, then subscribe from that version.
    let (_d, base) = pull_snapshot(&digest_name, "cc2").unwrap().unwrap();

    // Seed the hub via a pull, then subscribe from that version.
    let (_d, base) = retry(Duration::from_secs(3), || pull_snapshot(&digest_name, "cc2"))
        .unwrap()
        .unwrap();


    // Seed the hub via a pull, then subscribe from that version.
    let (_d, base) = retry(Duration::from_secs(3), || pull_snapshot(&digest_name, "cc2"))
        .unwrap()
        .unwrap();

//! Session-digest **log-projection** integration — the topology-independence
//! proof (ADR-0008 amendment / REQ-TERM-4).
//!
//! There is no broker and no PTY here: the endpoint is **harness-hosted** (a
//! Claude-Code-style per-session log is Path A's worked example). The daemon
//! projects its digest **on demand** from the adapter's `[history]` strategy
//! (`locate_normalize` over a fixture JSONL of published digest-records) and
//! serves it over the real digest-control channel:
//!
//! - **snapshot pull** ([`pull_snapshot`]) projects the current buffer and
//!   returns it — proving a log-sourced digest works with no broker-owned PTY
//!   (impossible under the superseded byte-parse shape);
//! - **delta-stream** ([`follow`]) + a [`reproject`] nudge after the log grows
//!   delivers the structured delta to a subscriber (the push-driven freshness
//!   the `api digest-entry` path also drives — autonomous file-watch is deferred
//!   to the consuming frontend milestone).
// [int->REQ-TERM-4]

use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};

use spt_daemon::{follow, pull_snapshot, reproject, serve_digest_control, DigestHub};
use spt_store::info::{write_info, InfoJson};
use spt_store::perch::{self, resolve_perch_path, ParentHint};

static SEQ: AtomicU32 = AtomicU32::new(0);
fn unique(prefix: &str) -> String {
    let n = SEQ.fetch_add(1, Ordering::Relaxed);
    format!("spt-daemon-digest-{prefix}-{}-{}.sock", std::process::id(), n)
}

/// A stdin pass-through "normalizer": the located transcript bytes round-trip to
/// stdout unchanged, so the fixture JSONL *is* the normalized record stream.
#[cfg(unix)]
const PASSTHROUGH: &str = "cat";
#[cfg(windows)]
const PASSTHROUGH: &str = "cmd /C findstr /r \".\"";

/// Register a harness adapter whose `[history]` is `locate_normalize` over
/// `fixture` (a digest-record JSONL), and establish a harness-hosted perch for
/// `id` that records the adapter — the daemon resolves the projection source from
/// these two facts alone (no broker, no live feed).
fn establish_cc_endpoint(id: &str, fixture: &std::path::Path) {
    // TOML literal (single-quoted) strings carry the path + command verbatim —
    // no escaping of Windows backslashes or the findstr quotes.
    let manifest = format!(
        "[adapter]\nname = \"mock-cc\"\nkind = \"harness\"\nversion = \"1\"\n\
         min_spt_core_version = \"0\"\n\n\
         [history]\nstrategy = \"locate_normalize\"\n\
         locate_template = '{}'\nnormalize_command = '{}'\n",
        fixture.display(),
        PASSTHROUGH,
    );
    let src = perch::spt_home().join("srcs").join("mock-cc-src");
    std::fs::create_dir_all(&src).unwrap();
    std::fs::write(src.join("manifest.toml"), manifest).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    let perch_path = resolve_perch_path(id, ParentHint::Infer);
    std::fs::create_dir_all(&perch_path).unwrap();
    let mut rec = InfoJson::new(id, "0", std::process::id(), "cc-session", "live_agent");
    rec.adapter = Some("mock-cc".to_string());
    write_info(&perch_path, &rec).unwrap();
}

/// Serve a fresh digest-control channel on a unique socket; returns its name.
fn serve_hub() -> String {
    let name = unique("digest");
    let hub = Arc::new(DigestHub::new());
    let n = name.clone();
    thread::spawn(move || {
        let _ = serve_digest_control(&n, hub);
    });
    name
}

/// Retry `op` until it succeeds or `dur` elapses — bridges the bind/accept lag
/// between the serve thread and the first client connect.
fn retry<T>(dur: Duration, mut op: impl FnMut() -> std::io::Result<T>) -> std::io::Result<T> {
    let start = Instant::now();
    loop {
        match op() {
            Ok(v) => return Ok(v),
            Err(e) if start.elapsed() >= dur => return Err(e),
            Err(_) => thread::sleep(Duration::from_millis(20)),
        }
    }
}

#[test]
fn harness_hosted_digest_projects_from_logs() {
    let home = tempfile::tempdir().expect("tempdir");
    std::env::set_var("SPT_HOME", home.path());

    // A fixture JSONL of published digest-records — the harness's "session log".
    let fixture = home.path().join("cc-session.jsonl");
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"add a file\"}\n\
         {\"role\":\"agent\",\"text\":\"on it\"}\n\
         {\"role\":\"tool\",\"tool\":{\"name\":\"Write\",\"arg\":\"src/a.rs\"}}\n",
    )
    .unwrap();
    establish_cc_endpoint("cc", &fixture);

    let digest_name = serve_hub();

    // ── Snapshot pull: projected on demand from the log, no broker/PTY ───────
    let (digest, version) = retry(Duration::from_secs(3), || pull_snapshot(&digest_name, "cc"))
        .expect("pull ok")
        .expect("a digest projects for the harness-hosted endpoint");
    assert!(version >= 1, "the projection has a version");
    assert_eq!(digest.turns.len(), 1, "one user turn: {digest:?}");
    assert_eq!(digest.turns[0].input.as_deref(), Some("add a file"));
    let rendered = spt_daemon::render_digest(&digest);
    assert!(
        rendered.contains("» add a file") && rendered.contains("used: Write(src/a.rs)"),
        "glanceable render carries the turn + collapsed sprint: {rendered}"
    );
}

#[test]
fn reproject_after_log_growth_delivers_a_delta() {
    let home = tempfile::tempdir().expect("tempdir");
    std::env::set_var("SPT_HOME", home.path());

    let fixture = home.path().join("cc-session.jsonl");
    std::fs::write(&fixture, "{\"role\":\"input\",\"text\":\"first\"}\n").unwrap();
    establish_cc_endpoint("cc2", &fixture);
    let digest_name = serve_hub();

    // Seed the hub via a pull, then subscribe from that version.
    let (_d, base) = pull_snapshot(&digest_name, "cc2").unwrap().unwrap();

    let (tx, rx) = std::sync::mpsc::channel::<usize>();
    {
        let dname = digest_name.clone();
        thread::spawn(move || {
            let _ = follow(&dname, "cc2", base, |up| {
                if up.version > base {
                    // The grown log adds a second turn → its delta.
                    let last = up.turns.last().and_then(|t| t.input.clone());
                    if last.as_deref() == Some("second") {
                        let _ = tx.send(up.turns.len());
                        return false;
                    }
                }
                true
            });
        });
    }
    // Let the subscriber register, then grow the log and nudge a re-projection.
    thread::sleep(Duration::from_millis(80));
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"first\"}\n{\"role\":\"input\",\"text\":\"second\"}\n",
    )
    .unwrap();
    reproject(&digest_name, "cc2").expect("nudge a re-projection");

    let got = wait_recv(&rx, Duration::from_secs(5));
    assert!(got.is_some(), "the subscriber received the push-driven delta");
}

/// Block up to `dur` for one value on `rx`.
fn wait_recv(rx: &std::sync::mpsc::Receiver<usize>, dur: Duration) -> Option<usize> {
    let start = Instant::now();
    loop {
        if let Ok(v) = rx.try_recv() {
            return Some(v);
        }
        if start.elapsed() >= dur {
            return None;
        }
        thread::sleep(Duration::from_millis(15));
    }
}

-    let (_d, base) = pull_snapshot(&digest_name, "cc2").unwrap().unwrap();

+    let (_d, base) = retry(Duration::from_secs(3), || pull_snapshot(&digest_name, "cc2"))


Bash

cd C:/Users/decid/Documents/projects/spt-core && cargo test -p spt-daemon --test digest 2>&1 | tail -15

Re-run daemon CC-fixture E2E


failures:

---- reproject_after_log_growth_delivers_a_delta stdout ----

thread 'reproject_after_log_growth_delivers_a_delta' (12204) panicked at crates\spt-daemon\tests\digest.rs:166:5:
the subscriber received the push-driven delta
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

failures:
    reproject_after_log_growth_delivers_a_delta

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.16s

error: test failed, to rerun pass `-p spt-daemon --test digest`


Test 1 (the core topology-independence proof) passes. Test 2 has a race: `reproject` can publish before the subscriber registers. Make it deterministic — wait for the base delivery (which proves registration) before growing the log:


Edit

    // Seed the hub via a pull, then subscribe from that version.
    let (_d, base) = retry(Duration::from_secs(3), || pull_snapshot(&digest_name, "cc2"))
        .unwrap()
        .unwrap();

    let (tx, rx) = std::sync::mpsc::channel::<usize>();
    {
        let dname = digest_name.clone();
        thread::spawn(move || {
            let _ = follow(&dname, "cc2", base, |up| {
                if up.version > base {
                    // The grown log adds a second turn → its delta.
                    let last = up.turns.last().and_then(|t| t.input.clone());
                    if last.as_deref() == Some("second") {
                        let _ = tx.send(up.turns.len());
                        return false;
                    }
                }
                true
            });
        });
    }
    // Let the subscriber register, then grow the log and nudge a re-projection.
    thread::sleep(Duration::from_millis(80));
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"first\"}\n{\"role\":\"input\",\"text\":\"second\"}\n",
    )
    .unwrap();
    reproject(&digest_name, "cc2").expect("nudge a re-projection");

    let got = wait_recv(&rx, Duration::from_secs(5));
    assert!(got.is_some(), "the subscriber received the push-driven delta");

    // Seed the hub via a pull, then subscribe from that version.
    let (_d, base) = retry(Duration::from_secs(3), || pull_snapshot(&digest_name, "cc2"))
        .unwrap()
        .unwrap();

    let (ready_tx, ready_rx) = std::sync::mpsc::channel::<()>();
    let (tx, rx) = std::sync::mpsc::channel::<usize>();
    {
        let dname = digest_name.clone();
        thread::spawn(move || {
            let _ = follow(&dname, "cc2", base, |up| {
                if up.version <= base {
                    // The base delivery proves we are registered as a subscriber.
                    let _ = ready_tx.send(());
                    return true;
                }
                // The grown log adds a second turn → its delta.
                let last = up.turns.last().and_then(|t| t.input.clone());
                if last.as_deref() == Some("second") {
                    let _ = tx.send(up.turns.len());
                    return false;
                }
                true
            });
        });
    }
    // Wait until the subscriber is registered (base received), THEN grow the log
    // and nudge a re-projection — no publish can race ahead of the subscription.
    ready_rx
        .recv_timeout(Duration::from_secs(3))
        .expect("subscriber registered (base delivered)");
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"first\"}\n{\"role\":\"input\",\"text\":\"second\"}\n",
    )
    .unwrap();
    reproject(&digest_name, "cc2").expect("nudge a re-projection");

    let got = wait_recv(&rx, Duration::from_secs(5));
    assert!(got.is_some(), "the subscriber received the push-driven delta");


    // Seed the hub via a pull, then subscribe from that version.
    let (_d, base) = retry(Duration::from_secs(3), || pull_snapshot(&digest_name, "cc2"))
        .unwrap()
        .unwrap();

    let (ready_tx, ready_rx) = std::sync::mpsc::channel::<()>();
    let (tx, rx) = std::sync::mpsc::channel::<usize>();
    {
        let dname = digest_name.clone();
        thread::spawn(move || {
            let _ = follow(&dname, "cc2", base, |up| {
                if up.version <= base {
                    // The base delivery proves we are registered as a subscriber.
                    let _ = ready_tx.send(());
                    return true;
                }
                // The grown log adds a second turn → its delta.
                let last = up.turns.last().and_then(|t| t.input.clone());
                if last.as_deref() == Some("second") {
                    let _ = tx.send(up.turns.len());
                    return false;
                }
                true
            });
        });
    }
    // Wait until the subscriber is registered (base received), THEN grow the log
    // and nudge a re-projection — no publish can race ahead of the subscription.
    ready_rx
        .recv_timeout(Duration::from_secs(3))
        .expect("subscriber registered (base delivered)");
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"first\"}\n{\"role\":\"input\",\"text\":\"second\"}\n",
    )
    .unwrap();
    reproject(&digest_name, "cc2").expect("nudge a re-projection");

    let got = wait_recv(&rx, Duration::from_secs(5));
    assert!(got.is_some(), "the subscriber received the push-driven delta");

//! Session-digest **log-projection** integration — the topology-independence
//! proof (ADR-0008 amendment / REQ-TERM-4).
//!
//! There is no broker and no PTY here: the endpoint is **harness-hosted** (a
//! Claude-Code-style per-session log is Path A's worked example). The daemon
//! projects its digest **on demand** from the adapter's `[history]` strategy
//! (`locate_normalize` over a fixture JSONL of published digest-records) and
//! serves it over the real digest-control channel:
//!
//! - **snapshot pull** ([`pull_snapshot`]) projects the current buffer and
//!   returns it — proving a log-sourced digest works with no broker-owned PTY
//!   (impossible under the superseded byte-parse shape);
//! - **delta-stream** ([`follow`]) + a [`reproject`] nudge after the log grows
//!   delivers the structured delta to a subscriber (the push-driven freshness
//!   the `api digest-entry` path also drives — autonomous file-watch is deferred
//!   to the consuming frontend milestone).
// [int->REQ-TERM-4]

use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};

use spt_daemon::{follow, pull_snapshot, reproject, serve_digest_control, DigestHub};
use spt_store::info::{write_info, InfoJson};
use spt_store::perch::{self, resolve_perch_path, ParentHint};

static SEQ: AtomicU32 = AtomicU32::new(0);
fn unique(prefix: &str) -> String {
    let n = SEQ.fetch_add(1, Ordering::Relaxed);
    format!("spt-daemon-digest-{prefix}-{}-{}.sock", std::process::id(), n)
}

/// A stdin pass-through "normalizer": the located transcript bytes round-trip to
/// stdout unchanged, so the fixture JSONL *is* the normalized record stream.
#[cfg(unix)]
const PASSTHROUGH: &str = "cat";
#[cfg(windows)]
const PASSTHROUGH: &str = "cmd /C findstr /r \".\"";

/// Register a harness adapter whose `[history]` is `locate_normalize` over
/// `fixture` (a digest-record JSONL), and establish a harness-hosted perch for
/// `id` that records the adapter — the daemon resolves the projection source from
/// these two facts alone (no broker, no live feed).
fn establish_cc_endpoint(id: &str, fixture: &std::path::Path) {
    // TOML literal (single-quoted) strings carry the path + command verbatim —
    // no escaping of Windows backslashes or the findstr quotes.
    let manifest = format!(
        "[adapter]\nname = \"mock-cc\"\nkind = \"harness\"\nversion = \"1\"\n\
         min_spt_core_version = \"0\"\n\n\
         [history]\nstrategy = \"locate_normalize\"\n\
         locate_template = '{}'\nnormalize_command = '{}'\n",
        fixture.display(),
        PASSTHROUGH,
    );
    let src = perch::spt_home().join("srcs").join("mock-cc-src");
    std::fs::create_dir_all(&src).unwrap();
    std::fs::write(src.join("manifest.toml"), manifest).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    let perch_path = resolve_perch_path(id, ParentHint::Infer);
    std::fs::create_dir_all(&perch_path).unwrap();
    let mut rec = InfoJson::new(id, "0", std::process::id(), "cc-session", "live_agent");
    rec.adapter = Some("mock-cc".to_string());
    write_info(&perch_path, &rec).unwrap();
}

/// Serve a fresh digest-control channel on a unique socket; returns its name.
fn serve_hub() -> String {
    let name = unique("digest");
    let hub = Arc::new(DigestHub::new());
    let n = name.clone();
    thread::spawn(move || {
        let _ = serve_digest_control(&n, hub);
    });
    name
}

/// Retry `op` until it succeeds or `dur` elapses — bridges the bind/accept lag
/// between the serve thread and the first client connect.
fn retry<T>(dur: Duration, mut op: impl FnMut() -> std::io::Result<T>) -> std::io::Result<T> {
    let start = Instant::now();
    loop {
        match op() {
            Ok(v) => return Ok(v),
            Err(e) if start.elapsed() >= dur => return Err(e),
            Err(_) => thread::sleep(Duration::from_millis(20)),
        }
    }
}

#[test]
fn harness_hosted_digest_projects_from_logs() {
    let home = tempfile::tempdir().expect("tempdir");
    std::env::set_var("SPT_HOME", home.path());

    // A fixture JSONL of published digest-records — the harness's "session log".
    let fixture = home.path().join("cc-session.jsonl");
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"add a file\"}\n\
         {\"role\":\"agent\",\"text\":\"on it\"}\n\
         {\"role\":\"tool\",\"tool\":{\"name\":\"Write\",\"arg\":\"src/a.rs\"}}\n",
    )
    .unwrap();
    establish_cc_endpoint("cc", &fixture);

    let digest_name = serve_hub();

    // ── Snapshot pull: projected on demand from the log, no broker/PTY ───────
    let (digest, version) = retry(Duration::from_secs(3), || pull_snapshot(&digest_name, "cc"))
        .expect("pull ok")
        .expect("a digest projects for the harness-hosted endpoint");
    assert!(version >= 1, "the projection has a version");
    assert_eq!(digest.turns.len(), 1, "one user turn: {digest:?}");
    assert_eq!(digest.turns[0].input.as_deref(), Some("add a file"));
    let rendered = spt_daemon::render_digest(&digest);
    assert!(
        rendered.contains("» add a file") && rendered.contains("used: Write(src/a.rs)"),
        "glanceable render carries the turn + collapsed sprint: {rendered}"
    );
}

#[test]
fn reproject_after_log_growth_delivers_a_delta() {
    let home = tempfile::tempdir().expect("tempdir");
    std::env::set_var("SPT_HOME", home.path());

    let fixture = home.path().join("cc-session.jsonl");
    std::fs::write(&fixture, "{\"role\":\"input\",\"text\":\"first\"}\n").unwrap();
    establish_cc_endpoint("cc2", &fixture);
    let digest_name = serve_hub();

    // Seed the hub via a pull, then subscribe from that version.
    let (_d, base) = retry(Duration::from_secs(3), || pull_snapshot(&digest_name, "cc2"))
        .unwrap()
        .unwrap();

    let (tx, rx) = std::sync::mpsc::channel::<usize>();
    {
        let dname = digest_name.clone();
        thread::spawn(move || {
            let _ = follow(&dname, "cc2", base, |up| {
                if up.version > base {
                    // The grown log adds a second turn → its delta.
                    let last = up.turns.last().and_then(|t| t.input.clone());
                    if last.as_deref() == Some("second") {
                        let _ = tx.send(up.turns.len());
                        return false;
                    }
                }
                true
            });
        });
    }
    // Let the subscriber register, then grow the log and nudge a re-projection.
    thread::sleep(Duration::from_millis(80));
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"first\"}\n{\"role\":\"input\",\"text\":\"second\"}\n",
    )
    .unwrap();
    reproject(&digest_name, "cc2").expect("nudge a re-projection");

    let got = wait_recv(&rx, Duration::from_secs(5));
    assert!(got.is_some(), "the subscriber received the push-driven delta");
}

/// Block up to `dur` for one value on `rx`.
fn wait_recv(rx: &std::sync::mpsc::Receiver<usize>, dur: Duration) -> Option<usize> {
    let start = Instant::now();
    loop {
        if let Ok(v) = rx.try_recv() {
            return Some(v);
        }
        if start.elapsed() >= dur {
            return None;
        }
        thread::sleep(Duration::from_millis(15));
    }
}

+    let (ready_tx, ready_rx) = std::sync::mpsc::channel::<()>();

-                    // The grown log adds a second turn → its delta.

-                    let last = up.turns.last().and_then(|t| t.input.clone());

-                    if last.as_deref() == Some("second") {

-                        let _ = tx.send(up.turns.len());

+                if up.version <= base {

+                    // The base delivery proves we are registered as a subscriber.

+                    let _ = ready_tx.send(());

+                // The grown log adds a second turn → its delta.

+                let last = up.turns.last().and_then(|t| t.input.clone());

+                if last.as_deref() == Some("second") {

+                    let _ = tx.send(up.turns.len());

-    // Let the subscriber register, then grow the log and nudge a re-projection.

-    thread::sleep(Duration::from_millis(80));

+    // Wait until the subscriber is registered (base received), THEN grow the log

+    // and nudge a re-projection — no publish can race ahead of the subscription.

+        .recv_timeout(Duration::from_secs(3))

+        .expect("subscriber registered (base delivered)");


Bash

cd C:/Users/decid/Documents/projects/spt-core && cargo test -p spt-daemon --test digest 2>&1 | tail -12

Re-run daemon CC-fixture E2E


thread 'reproject_after_log_growth_delivers_a_delta' (24764) panicked at crates\spt-daemon\tests\digest.rs:164:10:
subscriber registered (base delivered): Timeout
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

failures:
    reproject_after_log_growth_delivers_a_delta

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.08s

error: test failed, to rerun pass `-p spt-daemon --test digest`


Both tests race on the process-global `SPT_HOME` env var (cargo runs them in parallel threads). The original B8 was a single test for this reason. Merge into one sequential test:


Edit

#[test]
fn harness_hosted_digest_projects_from_logs() {
    let home = tempfile::tempdir().expect("tempdir");
    std::env::set_var("SPT_HOME", home.path());

    // A fixture JSONL of published digest-records — the harness's "session log".
    let fixture = home.path().join("cc-session.jsonl");
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"add a file\"}\n\
         {\"role\":\"agent\",\"text\":\"on it\"}\n\
         {\"role\":\"tool\",\"tool\":{\"name\":\"Write\",\"arg\":\"src/a.rs\"}}\n",
    )
    .unwrap();
    establish_cc_endpoint("cc", &fixture);

    let digest_name = serve_hub();

    // ── Snapshot pull: projected on demand from the log, no broker/PTY ───────
    let (digest, version) = retry(Duration::from_secs(3), || pull_snapshot(&digest_name, "cc"))
        .expect("pull ok")
        .expect("a digest projects for the harness-hosted endpoint");
    assert!(version >= 1, "the projection has a version");
    assert_eq!(digest.turns.len(), 1, "one user turn: {digest:?}");
    assert_eq!(digest.turns[0].input.as_deref(), Some("add a file"));
    let rendered = spt_daemon::render_digest(&digest);
    assert!(
        rendered.contains("» add a file") && rendered.contains("used: Write(src/a.rs)"),
        "glanceable render carries the turn + collapsed sprint: {rendered}"
    );
}

#[test]
fn reproject_after_log_growth_delivers_a_delta() {
    let home = tempfile::tempdir().expect("tempdir");
    std::env::set_var("SPT_HOME", home.path());

    let fixture = home.path().join("cc-session.jsonl");
    std::fs::write(&fixture, "{\"role\":\"input\",\"text\":\"first\"}\n").unwrap();
    establish_cc_endpoint("cc2", &fixture);
    let digest_name = serve_hub();

    // Seed the hub via a pull, then subscribe from that version.

// One test fn: the two assertions share a home. `SPT_HOME` is a process-global
// env var, so splitting into parallel `#[test]`s would race on it — the B8
// original was single-test for the same reason.
#[test]
fn harness_hosted_digest_projects_and_pushes_deltas() {
    let home = tempfile::tempdir().expect("tempdir");
    std::env::set_var("SPT_HOME", home.path());

    // ── Snapshot pull: projected on demand from a harness log, no broker/PTY ──
    // A fixture JSONL of published digest-records — the harness's "session log".
    let fixture = home.path().join("cc-session.jsonl");
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"add a file\"}\n\
         {\"role\":\"agent\",\"text\":\"on it\"}\n\
         {\"role\":\"tool\",\"tool\":{\"name\":\"Write\",\"arg\":\"src/a.rs\"}}\n",
    )
    .unwrap();
    establish_cc_endpoint("cc", &fixture);
    let digest_name = serve_hub();

    let (digest, version) = retry(Duration::from_secs(3), || pull_snapshot(&digest_name, "cc"))
        .expect("pull ok")
        .expect("a digest projects for the harness-hosted endpoint");
    assert!(version >= 1, "the projection has a version");
    assert_eq!(digest.turns.len(), 1, "one user turn: {digest:?}");
    assert_eq!(digest.turns[0].input.as_deref(), Some("add a file"));
    let rendered = spt_daemon::render_digest(&digest);
    assert!(
        rendered.contains("» add a file") && rendered.contains("used: Write(src/a.rs)"),
        "glanceable render carries the turn + collapsed sprint: {rendered}"
    );

    // ── Delta-stream: a reproject after the log grows delivers a delta ───────
    let fixture2 = home.path().join("cc2-session.jsonl");
    std::fs::write(&fixture2, "{\"role\":\"input\",\"text\":\"first\"}\n").unwrap();
    establish_cc_endpoint("cc2", &fixture2);

    // Seed the hub via a pull, then subscribe from that version.


// One test fn: the two assertions share a home. `SPT_HOME` is a process-global
// env var, so splitting into parallel `#[test]`s would race on it — the B8
// original was single-test for the same reason.
#[test]
fn harness_hosted_digest_projects_and_pushes_deltas() {
    let home = tempfile::tempdir().expect("tempdir");
    std::env::set_var("SPT_HOME", home.path());

    // ── Snapshot pull: projected on demand from a harness log, no broker/PTY ──
    // A fixture JSONL of published digest-records — the harness's "session log".
    let fixture = home.path().join("cc-session.jsonl");
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"add a file\"}\n\
         {\"role\":\"agent\",\"text\":\"on it\"}\n\
         {\"role\":\"tool\",\"tool\":{\"name\":\"Write\",\"arg\":\"src/a.rs\"}}\n",
    )
    .unwrap();
    establish_cc_endpoint("cc", &fixture);
    let digest_name = serve_hub();

    let (digest, version) = retry(Duration::from_secs(3), || pull_snapshot(&digest_name, "cc"))
        .expect("pull ok")
        .expect("a digest projects for the harness-hosted endpoint");
    assert!(version >= 1, "the projection has a version");
    assert_eq!(digest.turns.len(), 1, "one user turn: {digest:?}");
    assert_eq!(digest.turns[0].input.as_deref(), Some("add a file"));
    let rendered = spt_daemon::render_digest(&digest);
    assert!(
        rendered.contains("» add a file") && rendered.contains("used: Write(src/a.rs)"),
        "glanceable render carries the turn + collapsed sprint: {rendered}"
    );

    // ── Delta-stream: a reproject after the log grows delivers a delta ───────
    let fixture2 = home.path().join("cc2-session.jsonl");
    std::fs::write(&fixture2, "{\"role\":\"input\",\"text\":\"first\"}\n").unwrap();
    establish_cc_endpoint("cc2", &fixture2);

    // Seed the hub via a pull, then subscribe from that version.

//! Session-digest **log-projection** integration — the topology-independence
//! proof (ADR-0008 amendment / REQ-TERM-4).
//!
//! There is no broker and no PTY here: the endpoint is **harness-hosted** (a
//! Claude-Code-style per-session log is Path A's worked example). The daemon
//! projects its digest **on demand** from the adapter's `[history]` strategy
//! (`locate_normalize` over a fixture JSONL of published digest-records) and
//! serves it over the real digest-control channel:
//!
//! - **snapshot pull** ([`pull_snapshot`]) projects the current buffer and
//!   returns it — proving a log-sourced digest works with no broker-owned PTY
//!   (impossible under the superseded byte-parse shape);
//! - **delta-stream** ([`follow`]) + a [`reproject`] nudge after the log grows
//!   delivers the structured delta to a subscriber (the push-driven freshness
//!   the `api digest-entry` path also drives — autonomous file-watch is deferred
//!   to the consuming frontend milestone).
// [int->REQ-TERM-4]

use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};

use spt_daemon::{follow, pull_snapshot, reproject, serve_digest_control, DigestHub};
use spt_store::info::{write_info, InfoJson};
use spt_store::perch::{self, resolve_perch_path, ParentHint};

static SEQ: AtomicU32 = AtomicU32::new(0);
fn unique(prefix: &str) -> String {
    let n = SEQ.fetch_add(1, Ordering::Relaxed);
    format!("spt-daemon-digest-{prefix}-{}-{}.sock", std::process::id(), n)
}

/// A stdin pass-through "normalizer": the located transcript bytes round-trip to
/// stdout unchanged, so the fixture JSONL *is* the normalized record stream.
#[cfg(unix)]
const PASSTHROUGH: &str = "cat";
#[cfg(windows)]
const PASSTHROUGH: &str = "cmd /C findstr /r \".\"";

/// Register a harness adapter whose `[history]` is `locate_normalize` over
/// `fixture` (a digest-record JSONL), and establish a harness-hosted perch for
/// `id` that records the adapter — the daemon resolves the projection source from
/// these two facts alone (no broker, no live feed).
fn establish_cc_endpoint(id: &str, fixture: &std::path::Path) {
    // TOML literal (single-quoted) strings carry the path + command verbatim —
    // no escaping of Windows backslashes or the findstr quotes.
    let manifest = format!(
        "[adapter]\nname = \"mock-cc\"\nkind = \"harness\"\nversion = \"1\"\n\
         min_spt_core_version = \"0\"\n\n\
         [history]\nstrategy = \"locate_normalize\"\n\
         locate_template = '{}'\nnormalize_command = '{}'\n",
        fixture.display(),
        PASSTHROUGH,
    );
    let src = perch::spt_home().join("srcs").join("mock-cc-src");
    std::fs::create_dir_all(&src).unwrap();
    std::fs::write(src.join("manifest.toml"), manifest).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    let perch_path = resolve_perch_path(id, ParentHint::Infer);
    std::fs::create_dir_all(&perch_path).unwrap();
    let mut rec = InfoJson::new(id, "0", std::process::id(), "cc-session", "live_agent");
    rec.adapter = Some("mock-cc".to_string());
    write_info(&perch_path, &rec).unwrap();
}

/// Serve a fresh digest-control channel on a unique socket; returns its name.
fn serve_hub() -> String {
    let name = unique("digest");
    let hub = Arc::new(DigestHub::new());
    let n = name.clone();
    thread::spawn(move || {
        let _ = serve_digest_control(&n, hub);
    });
    name
}

/// Retry `op` until it succeeds or `dur` elapses — bridges the bind/accept lag
/// between the serve thread and the first client connect.
fn retry<T>(dur: Duration, mut op: impl FnMut() -> std::io::Result<T>) -> std::io::Result<T> {
    let start = Instant::now();
    loop {
        match op() {
            Ok(v) => return Ok(v),
            Err(e) if start.elapsed() >= dur => return Err(e),
            Err(_) => thread::sleep(Duration::from_millis(20)),
        }
    }
}

#[test]
fn harness_hosted_digest_projects_from_logs() {
    let home = tempfile::tempdir().expect("tempdir");
    std::env::set_var("SPT_HOME", home.path());

    // A fixture JSONL of published digest-records — the harness's "session log".
    let fixture = home.path().join("cc-session.jsonl");
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"add a file\"}\n\
         {\"role\":\"agent\",\"text\":\"on it\"}\n\
         {\"role\":\"tool\",\"tool\":{\"name\":\"Write\",\"arg\":\"src/a.rs\"}}\n",
    )
    .unwrap();
    establish_cc_endpoint("cc", &fixture);

    let digest_name = serve_hub();

    // ── Snapshot pull: projected on demand from the log, no broker/PTY ───────
    let (digest, version) = retry(Duration::from_secs(3), || pull_snapshot(&digest_name, "cc"))
        .expect("pull ok")
        .expect("a digest projects for the harness-hosted endpoint");
    assert!(version >= 1, "the projection has a version");
    assert_eq!(digest.turns.len(), 1, "one user turn: {digest:?}");
    assert_eq!(digest.turns[0].input.as_deref(), Some("add a file"));
    let rendered = spt_daemon::render_digest(&digest);
    assert!(
        rendered.contains("» add a file") && rendered.contains("used: Write(src/a.rs)"),
        "glanceable render carries the turn + collapsed sprint: {rendered}"
    );
}

#[test]
fn reproject_after_log_growth_delivers_a_delta() {
    let home = tempfile::tempdir().expect("tempdir");
    std::env::set_var("SPT_HOME", home.path());

    let fixture = home.path().join("cc-session.jsonl");
    std::fs::write(&fixture, "{\"role\":\"input\",\"text\":\"first\"}\n").unwrap();
    establish_cc_endpoint("cc2", &fixture);
    let digest_name = serve_hub();

    // Seed the hub via a pull, then subscribe from that version.
    let (_d, base) = retry(Duration::from_secs(3), || pull_snapshot(&digest_name, "cc2"))
        .unwrap()
        .unwrap();

    let (ready_tx, ready_rx) = std::sync::mpsc::channel::<()>();
    let (tx, rx) = std::sync::mpsc::channel::<usize>();
    {
        let dname = digest_name.clone();
        thread::spawn(move || {
            let _ = follow(&dname, "cc2", base, |up| {
                if up.version <= base {
                    // The base delivery proves we are registered as a subscriber.
                    let _ = ready_tx.send(());
                    return true;
                }
                // The grown log adds a second turn → its delta.
                let last = up.turns.last().and_then(|t| t.input.clone());
                if last.as_deref() == Some("second") {
                    let _ = tx.send(up.turns.len());
                    return false;
                }
                true
            });
        });
    }
    // Wait until the subscriber is registered (base received), THEN grow the log
    // and nudge a re-projection — no publish can race ahead of the subscription.
    ready_rx
        .recv_timeout(Duration::from_secs(3))
        .expect("subscriber registered (base delivered)");
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"first\"}\n{\"role\":\"input\",\"text\":\"second\"}\n",
    )
    .unwrap();
    reproject(&digest_name, "cc2").expect("nudge a re-projection");

    let got = wait_recv(&rx, Duration::from_secs(5));
    assert!(got.is_some(), "the subscriber received the push-driven delta");
}

/// Block up to `dur` for one value on `rx`.
fn wait_recv(rx: &std::sync::mpsc::Receiver<usize>, dur: Duration) -> Option<usize> {
    let start = Instant::now();
    loop {
        if let Ok(v) = rx.try_recv() {
            return Some(v);
        }
        if start.elapsed() >= dur {
            return None;
        }
        thread::sleep(Duration::from_millis(15));
    }
}

+// One test fn: the two assertions share a home. `SPT_HOME` is a process-global

+// env var, so splitting into parallel `#[test]`s would race on it — the B8

+// original was single-test for the same reason.

+    // ── Snapshot pull: projected on demand from a harness log, no broker/PTY ──

-    // ── Snapshot pull: projected on demand from the log, no broker/PTY ───────

-    let home = tempfile::tempdir().expect("tempdir");

-    std::env::set_var("SPT_HOME", home.path());

+    // ── Delta-stream: a reproject after the log grows delivers a delta ───────

+    let fixture2 = home.path().join("cc2-session.jsonl");

+    std::fs::write(&fixture2, "{\"role\":\"input\",\"text\":\"first\"}\n").unwrap();

+    establish_cc_endpoint("cc2", &fixture2);

-    let fixture = home.path().join("cc-session.jsonl");

-    std::fs::write(&fixture, "{\"role\":\"input\",\"text\":\"first\"}\n").unwrap();

-    establish_cc_endpoint("cc2", &fixture);


Fix endpoint isolation (the two endpoints need distinct adapters/fixtures, not the shared `mock-cc`) and the grow-write target:


fn establish_cc_endpoint(id: &str, fixture: &std::path::Path) {
    // TOML literal (single-quoted) strings carry the path + command verbatim —
    // no escaping of Windows backslashes or the findstr quotes.
    let manifest = format!(
        "[adapter]\nname = \"mock-cc\"\nkind = \"harness\"\nversion = \"1\"\n\
         min_spt_core_version = \"0\"\n\n\
         [history]\nstrategy = \"locate_normalize\"\n\
         locate_template = '{}'\nnormalize_command = '{}'\n",
        fixture.display(),
        PASSTHROUGH,
    );
    let src = perch::spt_home().join("srcs").join("mock-cc-src");
    std::fs::create_dir_all(&src).unwrap();
    std::fs::write(src.join("manifest.toml"), manifest).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    let perch_path = resolve_perch_path(id, ParentHint::Infer);
    std::fs::create_dir_all(&perch_path).unwrap();
    let mut rec = InfoJson::new(id, "0", std::process::id(), "cc-session", "live_agent");
    rec.adapter = Some("mock-cc".to_string());
    write_info(&perch_path, &rec).unwrap();
}

fn establish_cc_endpoint(id: &str, fixture: &std::path::Path) {
    // A per-endpoint adapter (so distinct endpoints get distinct `[history]`
    // sources). TOML literal (single-quoted) strings carry the path + command
    // verbatim — no escaping of Windows backslashes or the findstr quotes.
    let adapter = format!("mock-cc-{id}");
    let manifest = format!(
        "[adapter]\nname = \"{adapter}\"\nkind = \"harness\"\nversion = \"1\"\n\
         min_spt_core_version = \"0\"\n\n\
         [history]\nstrategy = \"locate_normalize\"\n\
         locate_template = '{}'\nnormalize_command = '{}'\n",
        fixture.display(),
        PASSTHROUGH,
    );
    let src = perch::spt_home().join("srcs").join(format!("{adapter}-src"));
    std::fs::create_dir_all(&src).unwrap();
    std::fs::write(src.join("manifest.toml"), manifest).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    let perch_path = resolve_perch_path(id, ParentHint::Infer);
    std::fs::create_dir_all(&perch_path).unwrap();
    let mut rec = InfoJson::new(id, "0", std::process::id(), "cc-session", "live_agent");
    rec.adapter = Some(adapter);
    write_info(&perch_path, &rec).unwrap();
}


fn establish_cc_endpoint(id: &str, fixture: &std::path::Path) {
    // A per-endpoint adapter (so distinct endpoints get distinct `[history]`
    // sources). TOML literal (single-quoted) strings carry the path + command
    // verbatim — no escaping of Windows backslashes or the findstr quotes.
    let adapter = format!("mock-cc-{id}");
    let manifest = format!(
        "[adapter]\nname = \"{adapter}\"\nkind = \"harness\"\nversion = \"1\"\n\
         min_spt_core_version = \"0\"\n\n\
         [history]\nstrategy = \"locate_normalize\"\n\
         locate_template = '{}'\nnormalize_command = '{}'\n",
        fixture.display(),
        PASSTHROUGH,
    );
    let src = perch::spt_home().join("srcs").join(format!("{adapter}-src"));
    std::fs::create_dir_all(&src).unwrap();
    std::fs::write(src.join("manifest.toml"), manifest).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    let perch_path = resolve_perch_path(id, ParentHint::Infer);
    std::fs::create_dir_all(&perch_path).unwrap();
    let mut rec = InfoJson::new(id, "0", std::process::id(), "cc-session", "live_agent");
    rec.adapter = Some(adapter);
    write_info(&perch_path, &rec).unwrap();
}

//! Session-digest **log-projection** integration — the topology-independence
//! proof (ADR-0008 amendment / REQ-TERM-4).
//!
//! There is no broker and no PTY here: the endpoint is **harness-hosted** (a
//! Claude-Code-style per-session log is Path A's worked example). The daemon
//! projects its digest **on demand** from the adapter's `[history]` strategy
//! (`locate_normalize` over a fixture JSONL of published digest-records) and
//! serves it over the real digest-control channel:
//!
//! - **snapshot pull** ([`pull_snapshot`]) projects the current buffer and
//!   returns it — proving a log-sourced digest works with no broker-owned PTY
//!   (impossible under the superseded byte-parse shape);
//! - **delta-stream** ([`follow`]) + a [`reproject`] nudge after the log grows
//!   delivers the structured delta to a subscriber (the push-driven freshness
//!   the `api digest-entry` path also drives — autonomous file-watch is deferred
//!   to the consuming frontend milestone).
// [int->REQ-TERM-4]

use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};

use spt_daemon::{follow, pull_snapshot, reproject, serve_digest_control, DigestHub};
use spt_store::info::{write_info, InfoJson};
use spt_store::perch::{self, resolve_perch_path, ParentHint};

static SEQ: AtomicU32 = AtomicU32::new(0);
fn unique(prefix: &str) -> String {
    let n = SEQ.fetch_add(1, Ordering::Relaxed);
    format!("spt-daemon-digest-{prefix}-{}-{}.sock", std::process::id(), n)
}

/// A stdin pass-through "normalizer": the located transcript bytes round-trip to
/// stdout unchanged, so the fixture JSONL *is* the normalized record stream.
#[cfg(unix)]
const PASSTHROUGH: &str = "cat";
#[cfg(windows)]
const PASSTHROUGH: &str = "cmd /C findstr /r \".\"";

/// Register a harness adapter whose `[history]` is `locate_normalize` over
/// `fixture` (a digest-record JSONL), and establish a harness-hosted perch for
/// `id` that records the adapter — the daemon resolves the projection source from
/// these two facts alone (no broker, no live feed).
fn establish_cc_endpoint(id: &str, fixture: &std::path::Path) {
    // TOML literal (single-quoted) strings carry the path + command verbatim —
    // no escaping of Windows backslashes or the findstr quotes.
    let manifest = format!(
        "[adapter]\nname = \"mock-cc\"\nkind = \"harness\"\nversion = \"1\"\n\
         min_spt_core_version = \"0\"\n\n\
         [history]\nstrategy = \"locate_normalize\"\n\
         locate_template = '{}'\nnormalize_command = '{}'\n",
        fixture.display(),
        PASSTHROUGH,
    );
    let src = perch::spt_home().join("srcs").join("mock-cc-src");
    std::fs::create_dir_all(&src).unwrap();
    std::fs::write(src.join("manifest.toml"), manifest).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    let perch_path = resolve_perch_path(id, ParentHint::Infer);
    std::fs::create_dir_all(&perch_path).unwrap();
    let mut rec = InfoJson::new(id, "0", std::process::id(), "cc-session", "live_agent");
    rec.adapter = Some("mock-cc".to_string());
    write_info(&perch_path, &rec).unwrap();
}

/// Serve a fresh digest-control channel on a unique socket; returns its name.
fn serve_hub() -> String {
    let name = unique("digest");
    let hub = Arc::new(DigestHub::new());
    let n = name.clone();
    thread::spawn(move || {
        let _ = serve_digest_control(&n, hub);
    });
    name
}

/// Retry `op` until it succeeds or `dur` elapses — bridges the bind/accept lag
/// between the serve thread and the first client connect.
fn retry<T>(dur: Duration, mut op: impl FnMut() -> std::io::Result<T>) -> std::io::Result<T> {
    let start = Instant::now();
    loop {
        match op() {
            Ok(v) => return Ok(v),
            Err(e) if start.elapsed() >= dur => return Err(e),
            Err(_) => thread::sleep(Duration::from_millis(20)),
        }
    }
}

// One test fn: the two assertions share a home. `SPT_HOME` is a process-global
// env var, so splitting into parallel `#[test]`s would race on it — the B8
// original was single-test for the same reason.
#[test]
fn harness_hosted_digest_projects_and_pushes_deltas() {
    let home = tempfile::tempdir().expect("tempdir");
    std::env::set_var("SPT_HOME", home.path());

    // ── Snapshot pull: projected on demand from a harness log, no broker/PTY ──
    // A fixture JSONL of published digest-records — the harness's "session log".
    let fixture = home.path().join("cc-session.jsonl");
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"add a file\"}\n\
         {\"role\":\"agent\",\"text\":\"on it\"}\n\
         {\"role\":\"tool\",\"tool\":{\"name\":\"Write\",\"arg\":\"src/a.rs\"}}\n",
    )
    .unwrap();
    establish_cc_endpoint("cc", &fixture);
    let digest_name = serve_hub();

    let (digest, version) = retry(Duration::from_secs(3), || pull_snapshot(&digest_name, "cc"))
        .expect("pull ok")
        .expect("a digest projects for the harness-hosted endpoint");
    assert!(version >= 1, "the projection has a version");
    assert_eq!(digest.turns.len(), 1, "one user turn: {digest:?}");
    assert_eq!(digest.turns[0].input.as_deref(), Some("add a file"));
    let rendered = spt_daemon::render_digest(&digest);
    assert!(
        rendered.contains("» add a file") && rendered.contains("used: Write(src/a.rs)"),
        "glanceable render carries the turn + collapsed sprint: {rendered}"
    );

    // ── Delta-stream: a reproject after the log grows delivers a delta ───────
    let fixture2 = home.path().join("cc2-session.jsonl");
    std::fs::write(&fixture2, "{\"role\":\"input\",\"text\":\"first\"}\n").unwrap();
    establish_cc_endpoint("cc2", &fixture2);

    // Seed the hub via a pull, then subscribe from that version.
    let (_d, base) = retry(Duration::from_secs(3), || pull_snapshot(&digest_name, "cc2"))
        .unwrap()
        .unwrap();

    let (ready_tx, ready_rx) = std::sync::mpsc::channel::<()>();
    let (tx, rx) = std::sync::mpsc::channel::<usize>();
    {
        let dname = digest_name.clone();
        thread::spawn(move || {
            let _ = follow(&dname, "cc2", base, |up| {
                if up.version <= base {
                    // The base delivery proves we are registered as a subscriber.
                    let _ = ready_tx.send(());
                    return true;
                }
                // The grown log adds a second turn → its delta.
                let last = up.turns.last().and_then(|t| t.input.clone());
                if last.as_deref() == Some("second") {
                    let _ = tx.send(up.turns.len());
                    return false;
                }
                true
            });
        });
    }
    // Wait until the subscriber is registered (base received), THEN grow the log
    // and nudge a re-projection — no publish can race ahead of the subscription.
    ready_rx
        .recv_timeout(Duration::from_secs(3))
        .expect("subscriber registered (base delivered)");
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"first\"}\n{\"role\":\"input\",\"text\":\"second\"}\n",
    )
    .unwrap();
    reproject(&digest_name, "cc2").expect("nudge a re-projection");

    let got = wait_recv(&rx, Duration::from_secs(5));
    assert!(got.is_some(), "the subscriber received the push-driven delta");
}

/// Block up to `dur` for one value on `rx`.
fn wait_recv(rx: &std::sync::mpsc::Receiver<usize>, dur: Duration) -> Option<usize> {
    let start = Instant::now();
    loop {
        if let Ok(v) = rx.try_recv() {
            return Some(v);
        }
        if start.elapsed() >= dur {
            return None;
        }
        thread::sleep(Duration::from_millis(15));
    }
}

-    // TOML literal (single-quoted) strings carry the path + command verbatim —

-    // no escaping of Windows backslashes or the findstr quotes.

+    // A per-endpoint adapter (so distinct endpoints get distinct `[history]`

+    // sources). TOML literal (single-quoted) strings carry the path + command

+    // verbatim — no escaping of Windows backslashes or the findstr quotes.

+    let adapter = format!("mock-cc-{id}");

-        "[adapter]\nname = \"mock-cc\"\nkind = \"harness\"\nversion = \"1\"\n\

+        "[adapter]\nname = \"{adapter}\"\nkind = \"harness\"\nversion = \"1\"\n\

-    let src = perch::spt_home().join("srcs").join("mock-cc-src");

+    let src = perch::spt_home().join("srcs").join(format!("{adapter}-src"));

-    rec.adapter = Some("mock-cc".to_string());


    ready_rx
        .recv_timeout(Duration::from_secs(3))
        .expect("subscriber registered (base delivered)");
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"first\"}\n{\"role\":\"input\",\"text\":\"second\"}\n",
    )
    .unwrap();

    ready_rx
        .recv_timeout(Duration::from_secs(3))
        .expect("subscriber registered (base delivered)");
    std::fs::write(
        &fixture2,
        "{\"role\":\"input\",\"text\":\"first\"}\n{\"role\":\"input\",\"text\":\"second\"}\n",
    )
    .unwrap();


    ready_rx
        .recv_timeout(Duration::from_secs(3))
        .expect("subscriber registered (base delivered)");
    std::fs::write(
        &fixture2,
        "{\"role\":\"input\",\"text\":\"first\"}\n{\"role\":\"input\",\"text\":\"second\"}\n",
    )
    .unwrap();

//! Session-digest **log-projection** integration — the topology-independence
//! proof (ADR-0008 amendment / REQ-TERM-4).
//!
//! There is no broker and no PTY here: the endpoint is **harness-hosted** (a
//! Claude-Code-style per-session log is Path A's worked example). The daemon
//! projects its digest **on demand** from the adapter's `[history]` strategy
//! (`locate_normalize` over a fixture JSONL of published digest-records) and
//! serves it over the real digest-control channel:
//!
//! - **snapshot pull** ([`pull_snapshot`]) projects the current buffer and
//!   returns it — proving a log-sourced digest works with no broker-owned PTY
//!   (impossible under the superseded byte-parse shape);
//! - **delta-stream** ([`follow`]) + a [`reproject`] nudge after the log grows
//!   delivers the structured delta to a subscriber (the push-driven freshness
//!   the `api digest-entry` path also drives — autonomous file-watch is deferred
//!   to the consuming frontend milestone).
// [int->REQ-TERM-4]

use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};

use spt_daemon::{follow, pull_snapshot, reproject, serve_digest_control, DigestHub};
use spt_store::info::{write_info, InfoJson};
use spt_store::perch::{self, resolve_perch_path, ParentHint};

static SEQ: AtomicU32 = AtomicU32::new(0);
fn unique(prefix: &str) -> String {
    let n = SEQ.fetch_add(1, Ordering::Relaxed);
    format!("spt-daemon-digest-{prefix}-{}-{}.sock", std::process::id(), n)
}

/// A stdin pass-through "normalizer": the located transcript bytes round-trip to
/// stdout unchanged, so the fixture JSONL *is* the normalized record stream.
#[cfg(unix)]
const PASSTHROUGH: &str = "cat";
#[cfg(windows)]
const PASSTHROUGH: &str = "cmd /C findstr /r \".\"";

/// Register a harness adapter whose `[history]` is `locate_normalize` over
/// `fixture` (a digest-record JSONL), and establish a harness-hosted perch for
/// `id` that records the adapter — the daemon resolves the projection source from
/// these two facts alone (no broker, no live feed).
fn establish_cc_endpoint(id: &str, fixture: &std::path::Path) {
    // A per-endpoint adapter (so distinct endpoints get distinct `[history]`
    // sources). TOML literal (single-quoted) strings carry the path + command
    // verbatim — no escaping of Windows backslashes or the findstr quotes.
    let adapter = format!("mock-cc-{id}");
    let manifest = format!(
        "[adapter]\nname = \"{adapter}\"\nkind = \"harness\"\nversion = \"1\"\n\
         min_spt_core_version = \"0\"\n\n\
         [history]\nstrategy = \"locate_normalize\"\n\
         locate_template = '{}'\nnormalize_command = '{}'\n",
        fixture.display(),
        PASSTHROUGH,
    );
    let src = perch::spt_home().join("srcs").join(format!("{adapter}-src"));
    std::fs::create_dir_all(&src).unwrap();
    std::fs::write(src.join("manifest.toml"), manifest).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    let perch_path = resolve_perch_path(id, ParentHint::Infer);
    std::fs::create_dir_all(&perch_path).unwrap();
    let mut rec = InfoJson::new(id, "0", std::process::id(), "cc-session", "live_agent");
    rec.adapter = Some(adapter);
    write_info(&perch_path, &rec).unwrap();
}

/// Serve a fresh digest-control channel on a unique socket; returns its name.
fn serve_hub() -> String {
    let name = unique("digest");
    let hub = Arc::new(DigestHub::new());
    let n = name.clone();
    thread::spawn(move || {
        let _ = serve_digest_control(&n, hub);
    });
    name
}

/// Retry `op` until it succeeds or `dur` elapses — bridges the bind/accept lag
/// between the serve thread and the first client connect.
fn retry<T>(dur: Duration, mut op: impl FnMut() -> std::io::Result<T>) -> std::io::Result<T> {
    let start = Instant::now();
    loop {
        match op() {
            Ok(v) => return Ok(v),
            Err(e) if start.elapsed() >= dur => return Err(e),
            Err(_) => thread::sleep(Duration::from_millis(20)),
        }
    }
}

// One test fn: the two assertions share a home. `SPT_HOME` is a process-global
// env var, so splitting into parallel `#[test]`s would race on it — the B8
// original was single-test for the same reason.
#[test]
fn harness_hosted_digest_projects_and_pushes_deltas() {
    let home = tempfile::tempdir().expect("tempdir");
    std::env::set_var("SPT_HOME", home.path());

    // ── Snapshot pull: projected on demand from a harness log, no broker/PTY ──
    // A fixture JSONL of published digest-records — the harness's "session log".
    let fixture = home.path().join("cc-session.jsonl");
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"add a file\"}\n\
         {\"role\":\"agent\",\"text\":\"on it\"}\n\
         {\"role\":\"tool\",\"tool\":{\"name\":\"Write\",\"arg\":\"src/a.rs\"}}\n",
    )
    .unwrap();
    establish_cc_endpoint("cc", &fixture);
    let digest_name = serve_hub();

    let (digest, version) = retry(Duration::from_secs(3), || pull_snapshot(&digest_name, "cc"))
        .expect("pull ok")
        .expect("a digest projects for the harness-hosted endpoint");
    assert!(version >= 1, "the projection has a version");
    assert_eq!(digest.turns.len(), 1, "one user turn: {digest:?}");
    assert_eq!(digest.turns[0].input.as_deref(), Some("add a file"));
    let rendered = spt_daemon::render_digest(&digest);
    assert!(
        rendered.contains("» add a file") && rendered.contains("used: Write(src/a.rs)"),
        "glanceable render carries the turn + collapsed sprint: {rendered}"
    );

    // ── Delta-stream: a reproject after the log grows delivers a delta ───────
    let fixture2 = home.path().join("cc2-session.jsonl");
    std::fs::write(&fixture2, "{\"role\":\"input\",\"text\":\"first\"}\n").unwrap();
    establish_cc_endpoint("cc2", &fixture2);

    // Seed the hub via a pull, then subscribe from that version.
    let (_d, base) = retry(Duration::from_secs(3), || pull_snapshot(&digest_name, "cc2"))
        .unwrap()
        .unwrap();

    let (ready_tx, ready_rx) = std::sync::mpsc::channel::<()>();
    let (tx, rx) = std::sync::mpsc::channel::<usize>();
    {
        let dname = digest_name.clone();
        thread::spawn(move || {
            let _ = follow(&dname, "cc2", base, |up| {
                if up.version <= base {
                    // The base delivery proves we are registered as a subscriber.
                    let _ = ready_tx.send(());
                    return true;
                }
                // The grown log adds a second turn → its delta.
                let last = up.turns.last().and_then(|t| t.input.clone());
                if last.as_deref() == Some("second") {
                    let _ = tx.send(up.turns.len());
                    return false;
                }
                true
            });
        });
    }
    // Wait until the subscriber is registered (base received), THEN grow the log
    // and nudge a re-projection — no publish can race ahead of the subscription.
    ready_rx
        .recv_timeout(Duration::from_secs(3))
        .expect("subscriber registered (base delivered)");
    std::fs::write(
        &fixture,
        "{\"role\":\"input\",\"text\":\"first\"}\n{\"role\":\"input\",\"text\":\"second\"}\n",
    )
    .unwrap();
    reproject(&digest_name, "cc2").expect("nudge a re-projection");

    let got = wait_recv(&rx, Duration::from_secs(5));
    assert!(got.is_some(), "the subscriber received the push-driven delta");
}

/// Block up to `dur` for one value on `rx`.
fn wait_recv(rx: &std::sync::mpsc::Receiver<usize>, dur: Duration) -> Option<usize> {
    let start = Instant::now();
    loop {
        if let Ok(v) = rx.try_recv() {
            return Some(v);
        }
        if start.elapsed() >= dur {
            return None;
        }
        thread::sleep(Duration::from_millis(15));
    }
}


Bash

cd C:/Users/decid/Documents/projects/spt-core && cargo test -p spt-daemon --test digest 2>&1 | tail -12

Re-run merged daemon CC-fixture E2E


   Compiling spt-daemon v0.4.2 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 3.14s
     Running tests\digest.rs (target\debug\deps\digest-da4e52bc4a545470.exe)

running 1 test
test harness_hosted_digest_projects_and_pushes_deltas ... ok

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


CC-fixture E2E green (topology-independent projection + push-driven delta both proven). Now validate the whole workspace for commit 3 — clippy across all targets catches any dangling references:


cd C:/Users/decid/Documents/projects/spt-core && cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -25

Clippy whole workspace, all targets


    Checking spt-term v0.4.2 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
    Checking spt-live v0.4.2 (C:\Users\decid\Documents\projects\spt-core\crates\spt-live)
    Checking mock-adapter v0.4.2 (C:\Users\decid\Documents\projects\spt-core\adapters\mock)
    Checking spt-runtime v0.4.2 (C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime)
    Checking spt-net v0.4.2 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
    Checking spt-msg v0.4.2 (C:\Users\decid\Documents\projects\spt-core\crates\spt-msg)
    Checking spt-daemon v0.4.2 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Checking spt v0.4.2 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Checking xtask v0.4.2 (C:\Users\decid\Documents\projects\spt-core\crates\xtask)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.99s


Clippy clean workspace-wide. Run the affected test suites + traceability:


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

Test daemon, term, store suites


test result: ok. 277 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 7.60s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.26s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.20s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.09s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.24s
test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.47s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.20s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.12s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.79s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.53s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.25s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.29s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.21s
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.44s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.21s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.60s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.15s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.22s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.41s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.09s
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.19s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.32s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.86s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.37s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.53s
test result: ok. 153 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 13.73s
test result: ok. 16 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.08s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.01s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
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
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


All green. Run the `spt` bin suite (the reproject wiring) + traceability, then commit 3:


cd C:/Users/decid/Documents/projects/spt-core && cargo test -p spt --bin spt -- api:: 2>&1 | grep -E "test result|error|FAILED" | head && traceable-reqs check >/dev/null 2>&1; echo "traceable EXIT=$?"

spt api tests + traceable check


test result: ok. 46 passed; 0 failed; 0 ignored; 0 measured; 74 filtered out; finished in 1.08s
traceable EXIT=0


Commit 3 (T7b core — green, REQ-TERM-4 over-evidenced, parser/PtyDigest/title untouched):


cd C:/Users/decid/Documents/projects/spt-core && git add -A && git status --short && git commit -q -m "$(cat <<'EOF'
feat(daemon): on-demand session-digest log projection + push-driven deltas (T7b core)

Re-found the live activity buffer on normalized session logs (ADR-0008
amendment). The digest is now projected ON DEMAND from an endpoint's records —
there is no live feed (the prior PTY byte-feed driver had no production caller):

- `spt_daemon::digest::project_endpoint_digest(id)` resolves the source from the
  endpoint alone — the adapter's `[history]` strategy (Path A locate+normalize /
  fetcher / Path B native) when declared, else the perch's `digest.log` (the
  `api digest-entry` push sink) — and folds the records through the published
  contract projection. Topology-independent: a harness-hosted (Claude-Code)
  endpoint with no broker PTY gets a digest.
- `DigestHub::project_and_publish` folds the delta diff (common_prefix_len →
  DigestUpdate) into the hub; the snapshot/subscribe handlers project on demand,
  and a new `digest_reproject` control verb + `reproject` client let
  `api digest-entry` drive a push delta to live subscribers. The delta-stream
  CONTRACT ships; autonomous file-watch freshness is deferred to the consuming
  frontend milestone (doyle ruling 2026-06-13).
- Removed `Brain::run_digest_feed` (the dead byte-feed driver). The byte-parse
  engine retire + REQ-TERM-4 re-point land next (T7b commit 2), atomically.

New REQ-TERM-4 evidence: the daemon projector + hub + the CC-fixture E2E
(`harness_hosted_digest_projects_and_pushes_deltas` — Path A JSONL, no broker).
The superseded parser + `[pty_digest]` seam stay green until the retire commit.

// [impl->REQ-TERM-4] // [unit->REQ-TERM-4] // [int->REQ-TERM-4]

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
EOF
)" && echo "--- commit 3 done ---" && git log --oneline -4

Commit T7b core


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-daemon/src/digest.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/digest.rs', LF will be replaced by CRLF the next time Git touches it
M  Cargo.lock
M  crates/spt-daemon/src/brain.rs
M  crates/spt-daemon/src/digest.rs
M  crates/spt-daemon/src/digesthub.rs
M  crates/spt-daemon/src/lib.rs
M  crates/spt-daemon/tests/digest.rs
M  crates/spt/src/api/reporting.rs
--- commit 3 done ---
5a09577 feat(daemon): on-demand session-digest log projection + push-driven deltas (T7b core)
76ff4a3 docs(m9): Wave3 status — T7a landed; T7b scope discovery (byte-feed never live-wired)
6033326 feat(api): digest-entry push door for log-less adapters (T7a)
82ead73 feat(term): session-digest log projection primitive + published record contract (T7a)


Commit 3 landed (T7b core, all green). Now **commit 4** — the atomic retire + REQ-TERM-4 re-point + docs. Start by slimming `spt-term/src/digest.rs` to just the output types (delete the parser):


Write

//! Session-digest output types + presentation config (ADR-0008 amendment,
//! 2026-06-12; REQ-TERM-4).
//!
//! The structured, source-tagged shape a projected digest takes — the last ~N
//! user turns plus the agent output between them, with tool-usage sprints
//! collapsed to a single entry. Distinct from raw scrollback (bytes) and
//! transcript history (the full conversation); its job is *"what is this agent
//! doing right now, at a glance."*
//!
//! These are the **output** types only. The projection that produces a [`Digest`]
//! from an endpoint's normalized session-log records lives in
//! [`crate::projection`] (the digest is a log projection, never a PTY-byte parse —
//! the superseded source mechanism). The types cross the daemon IPC + the `spt
//! endpoint digest` control channel, so they are serde-serializable.

use serde::{Deserialize, Serialize};

/// Presentation knobs owned by spt-core (not the adapter): window depth `N`
/// (~3 user turns) and tool-arg truncation width (~25 chars). Extraction is the
/// adapter's (the published digest-record contract); presentation is spt-core's.
#[derive(Debug, Clone, Copy)]
pub struct DigestConfig {
    /// Window depth — keep the last `window_turns` user turns (~3).
    pub window_turns: usize,
    /// Tool-arg truncation width in characters (~25).
    pub arg_truncation: usize,
}

impl Default for DigestConfig {
    fn default() -> Self {
        Self {
            window_turns: 3,
            arg_truncation: 25,
        }
    }
}

/// A single collapsed tool invocation.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolUse {
    /// The tool name (e.g. `Write`).
    pub name: String,
    /// The (truncated) argument fragment.
    pub arg: String,
}

/// One source-tagged entry within a turn.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DigestEntry {
    /// Agent output text (between boundaries).
    Agent(String),
    /// A collapsed run of consecutive tool invocations (one "sprint").
    ToolSprint(Vec<ToolUse>),
}

/// One user turn: the user input that opened it (None for any pre-input
/// preamble) plus the agent output / tool sprints that followed, in order.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct Turn {
    /// The user input text that opened this turn.
    pub input: Option<String>,
    /// Agent + tool-sprint entries that followed, in stream order.
    pub entries: Vec<DigestEntry>,
}

/// The structured rolling digest — the last `N` turns.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct Digest {
    /// Turns oldest-to-newest, at most `window_turns` of them.
    pub turns: Vec<Turn>,
}

/// Truncate `arg` to `width` characters, appending `…` when shortened.
pub(crate) fn truncate_arg(arg: &str, width: usize) -> String {
    if arg.chars().count() <= width {
        return arg.to_string();
    }
    let mut s: String = arg.chars().take(width).collect();
    s.push('…');
    s
}

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

    // [unit->REQ-TERM-4] arg truncation is spt-core presentation: short args pass
    // through; long args are cut to width with an ellipsis.
    #[test]
    fn truncate_arg_cuts_to_width_with_ellipsis() {
        assert_eq!(truncate_arg("short", 25), "short");
        assert_eq!(truncate_arg("this-is-a-very-long-arg", 10), "this-is-a-…");
    }

    // [unit->REQ-TERM-4] the presentation defaults are the spt-core formula
    // (~3-turn window, ~25-char arg truncation).
    #[test]
    fn config_defaults_are_the_presentation_formula() {
        let c = DigestConfig::default();
        assert_eq!((c.window_turns, c.arg_truncation), (3, 25));
    }
}