Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ Follows [Keep a Changelog](https://keepachangelog.com/); versioning is [SemVer](

- sl-viewer menu id taxonomy property surface (WBS-6.2 #457): `crates/sl-viewer/tests/properties_viewer_menu.rs` adds 5 proptest properties — every documented menu id (9 of them: `ID_APP_ABOUT`, `ID_APP_SETTINGS`, `ID_FILE_RELOAD_DISCOVERY`, `ID_FILE_SETTINGS`, `ID_EDIT_FIND`, `ID_VIEW_RELOAD`, `ID_VIEW_TOGGLE_THEME`, `ID_VIEW_COMMAND_PALETTE`, `ID_HELP_TOGGLE`) is non-empty, kebab-case ASCII, carries the `sl-viewer.` prefix, and is unique across the set so a muda event resolves to one DOM action. The menu taxonomy has exactly 9 documented ids so the operator documentation can be re-aligned if it drifts.

- sl-viewer async_states SkeletonLayout property surface (WBS-6.2 #458): `crates/sl-viewer/tests/properties_viewer_async_states.rs` adds 7 proptest properties — `SkeletonLayout::default()` is `Bundles`, exposes exactly three variants (`Bundles`, `ListDetail`, `StreamFeed`), and every variant's `Debug` label is non-empty, single-line, and matches one of the documented names. `list_rows.clamp(3, 6)` lands in `[3, 6]` for every input, is monotonic non-decreasing, and has the documented fixed points (`0` / `2` → `3`, `6` / `usize::MAX` → `6`).

- session-ledger OKF document validator property surface (WBS-6.2 #459): `crates/sl-viewer/tests/properties_session_ledger_okf.rs` adds 12 proptest properties pinning `session_ledger::OkfDocument::new` and `validate_okf_document` (the OKF v1 graph validator that backs the export / wiki / search pipeline). `new(b, c)` always produces `okf = "1.0"`, propagates `bundle.source_id` into `source_id` and `provenance.source_id`, propagates `c` into `provenance.corpus`, and starts with empty entities/relations/tags. `validate_okf_document` reports exactly one `unsupported_version` error per non-`"1.0"` `okf` (with the offending version in the message), exactly one `source_id_mismatch` error per provenance/source mismatch, exactly one `duplicate_entity_id` error per duplicate entity occurrence, `dangling_relation_source`/`dangling_relation_target` errors with field paths, and every `OkfValidationError` carries non-empty `field`/`code`/`message`.

- session-ledger worklog projector property surface (WBS-6.2 #460): `crates/sl-viewer/tests/properties_session_ledger_worklog.rs` adds 11 proptest properties pinning the crash-recovery / lost-work projector. Empty sessions project `None`; final user turns project as `AwaitingAssistantResponse`; final tool/subagent turns project as `InterruptedExecution`; final assistant turns with one of the 9 documented completion markers (`complete`, `completed`, `done`, `[completed]`, `<completed>`, `status: complete`, `status: completed`, `task complete`, `task completed`) project as `None`; final assistant turns without any marker project as `MissingCompletionMarker`; the summary never exceeds 241 characters, is single-line, and carries the originating session id, corpus, and message count. `project_unfinished_work` returns one item per unfinished session in input order and is deterministic. `WorklogProjection::from_session` carries `message_count` and matches `detect_unfinished` exactly.

- session-ledger OKF export adapter property surface (WBS-6.2 #461): `crates/sl-viewer/tests/properties_session_ledger_export.rs` adds 8 proptest properties pinning `session_ledger::export_to_okf` (the main entry point of the OKF v1 export pipeline). `export_to_okf` always produces `okf = "1.0"` and propagates `bundle.source_id` into `source_id` and `provenance.source_id`, propagates the corpus arg into `provenance.corpus`, and produces zero entities/relations/tags on an empty bundle. Every exported document passes `validate_okf_document` (validator contract for downstream consumers). `export_to_okf` is deterministic across calls. Intent bundles always emit exactly one goal entity (label = goal) plus one `acceptance` entity per acceptance signal and one `constraint` entity per constraint. Context bundles emit exactly one `resource` entity when `cwd` is present. Acceptance bundles emit exactly one `gate` entity with `label = "resume-gate"` and `properties.ready = true` / `properties.scope_sized = true`. The exporter never produces duplicate entity ids across multiple intents/contexts/acceptances/contracts.

- session-ledger distill compiler property surface (WBS-6.2 #462): `crates/sl-viewer/tests/properties_session_ledger_distill.rs` adds 9 proptest properties pinning `session_ledger::distill::compile` and `compile_and_store` (the deterministic compilation pipeline that turns a `Session` into a `ContinuationBundle` + episodic memories). `compile` always produces a bundle whose `source_id` equals `session.id`, is injectable (carries an `Acceptance` slice), emits one slice for every documented kind (`Acceptance` / `Intent` / `Context` / `Contract` / `Provenance` / `Worklog`) — even for empty sessions — and whose `total_token_estimate()` equals the sum of per-slice `token_estimate` values. `compile` is deterministic across calls. The `Worklog` slice's body deserializes to a `WorklogProjection` whose `message_count` equals `session.messages.len()`. `compile_and_store` returns an injectable bundle with the input `source_id`, writes exactly 3 episodic memories (intent / contract / context) to the memory store, and is deterministic across fresh stores.

- Commit signing header scan (C04 L34): `commit-signing-check.ps1` reads bounded commit headers via line-scanner (no unbounded `git cat-file` buffers or `(?ms)` regex); `-SelfCheck` + `tests/commit_signing_check.rs`.

- Loom permutation CI timeout (P0 stability): split blocking `loom-permutation.yml` into core + per-daemon `loom_model` jobs with `LOOM_MAX_PREEMPTIONS` on broadcast/pipeline/shutdown; mirror in soft `loom-smoke.yml` so Wave-40 tokio-shaped daemon graph tests no longer exceed single-job ceilings.
Expand Down
198 changes: 198 additions & 0 deletions crates/sl-viewer/tests/properties_session_ledger_distill.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
//! Property evidence for `session_ledger::distill::compile` and
//! `compile_and_store` (the deterministic compilation pipeline
//! that turns a `Session` into a `ContinuationBundle`).
//!
//! The compile pipeline is the SSOT for "what does an injectable
//! bundle look like?". If it stops emitting the documented slice
//! kinds, or `is_injectable()` ever returns false, downstream
//! resume / inject / search all break.

use proptest::prelude::*;
use session_ledger::distill::{compile, compile_and_store, DistillOutput};
use session_ledger::domain::bundle::BundleKind;
use session_ledger::domain::session::{Corpus, Message, Role, Session};
use session_ledger::ports::adapters::InMemoryMemoryStore;
use session_ledger::ports::MemoryStore;

const ALL_KINDS: &[BundleKind] = &[
BundleKind::Acceptance,
BundleKind::Intent,
BundleKind::Context,
BundleKind::Contract,
BundleKind::Provenance,
BundleKind::Worklog,
];

proptest! {
/// `compile(session)` always returns a bundle whose `source_id`
/// equals `session.id`.
#[test]
fn compile_carries_session_id(
session_id in "[a-zA-Z0-9_-]{1,16}",
) {
let session = Session::new(&session_id, Corpus::Forge);
let bundle = compile(&session);
prop_assert_eq!(bundle.source_id, session.id);
}

/// `compile(session)` always returns an injectable bundle
/// (i.e., it carries an `Acceptance` slice). This is the
/// load-bearing contract for resume.
#[test]
fn compile_is_always_injectable(
session_id in "[a-zA-Z0-9_-]{1,16}",
corpus_idx in 0..4_usize,
messages in proptest::collection::vec(
(0..5_usize, role_choice()),
0..5,
),
) {
let corpus = [Corpus::Forge, Corpus::Codex, Corpus::Cursor, Corpus::ClaudeCode][corpus_idx];
let mut session = Session::new(&session_id, corpus);
for (i, role) in messages {
session.messages.push(Message::new(role, format!("message-{i}")));
}
let bundle = compile(&session);
prop_assert!(bundle.is_injectable(),
"compile({session_id}) did not produce an injectable bundle");
prop_assert!(bundle.has(BundleKind::Acceptance));
}

/// `compile(session)` always emits one slice for every documented
/// kind — even when the session is empty.
#[test]
fn compile_emits_all_documented_slice_kinds(
session_id in "[a-zA-Z0-9_-]{1,16}",
corpus_idx in 0..4_usize,
) {
let corpus = [Corpus::Forge, Corpus::Codex, Corpus::Cursor, Corpus::ClaudeCode][corpus_idx];
let session = Session::new(&session_id, corpus);
let bundle = compile(&session);
for kind in ALL_KINDS {
prop_assert!(bundle.has(*kind),
"compile({session_id}) missing {kind:?} slice");
}
}

/// `compile(session)` always returns a bundle whose
/// `total_token_estimate()` equals the sum of per-slice
/// `token_estimate` values.
#[test]
fn compile_token_total_equals_sum(
n_messages in 0_usize..5,
) {
let mut session = Session::new("token-sum", Corpus::Forge);
for i in 0..n_messages {
session.messages.push(Message::new(Role::User, format!("user message {i}")));
}
let bundle = compile(&session);
let sum: u32 = bundle.bundles.iter().map(|slice| slice.token_estimate).sum();
prop_assert_eq!(bundle.total_token_estimate(), sum);
}

/// `compile(session)` is deterministic across calls.
#[test]
fn compile_is_deterministic(
n_messages in 0_usize..4,
corpus_idx in 0..4_usize,
) {
let corpus = [Corpus::Forge, Corpus::Codex, Corpus::Cursor, Corpus::ClaudeCode][corpus_idx];
let mut session = Session::new("det", corpus);
for i in 0..n_messages {
session.messages.push(Message::new(Role::User, format!("msg-{i}")));
}
let a = compile(&session);
let b = compile(&session);
prop_assert_eq!(a, b);
}

/// The `Worklog` slice's body deserializes to a `WorklogProjection`
/// whose `message_count` equals `session.messages.len()`.
#[test]
fn compile_worklog_carries_message_count(
n_messages in 0_usize..5,
) {
let mut session = Session::new("wl", Corpus::Forge);
for i in 0..n_messages {
session.messages.push(Message::new(Role::User, format!("m-{i}")));
}
let bundle = compile(&session);
let worklog = bundle
.bundles
.iter()
.find(|slice| slice.kind == BundleKind::Worklog)
.expect("compile must emit Worklog slice");
let projection: session_ledger::domain::worklog::WorklogProjection =
serde_json::from_value(worklog.body.clone()).expect("worklog body should deserialize");
prop_assert_eq!(projection.message_count, n_messages);
}

/// `compile_and_store` returns an injectable bundle with the same
/// `source_id` as the input session.
#[test]
fn compile_and_store_returns_injectable_bundle(
session_id in "[a-zA-Z0-9_-]{1,16}",
n_messages in 0_usize..3,
) {
let mut session = Session::new(&session_id, Corpus::Forge);
for i in 0..n_messages {
session.messages.push(Message::new(Role::User, format!("do it {i}")));
}
let store = InMemoryMemoryStore::default();
let output: DistillOutput = compile_and_store(&session, &store)
.expect("compile_and_store must succeed for well-formed session");
let source_id = output.bundle.source_id.clone();
prop_assert_eq!(source_id, session.id);
prop_assert!(output.bundle.is_injectable());
}

/// `compile_and_store` writes exactly 3 episodic memories
/// (intent, contract, context) to the memory store.
#[test]
fn compile_and_store_writes_three_memories(
n_messages in 0_usize..3,
) {
let mut session = Session::new("store-3", Corpus::Forge);
for i in 0..n_messages {
session.messages.push(Message::new(Role::User, format!("Goal: m-{i}\nConstraint: c-{i}")));
}
let store = InMemoryMemoryStore::default();
let output = compile_and_store(&session, &store).expect("compile_and_store");
prop_assert_eq!(output.memories.len(), 3,
"expected 3 memories (intent / contract / context), got {}", output.memories.len());
let recalled = store
.recall("session/store-3/episodic", 10)
.expect("recall stored facts");
prop_assert_eq!(recalled.len(), 3);
}

/// `compile_and_store` is deterministic — same session + store
/// produce the same DistillOutput.
#[test]
fn compile_and_store_is_deterministic(
n_messages in 0_usize..3,
) {
let mut session = Session::new("det-store", Corpus::Forge);
for i in 0..n_messages {
session.messages.push(Message::new(Role::User, format!("m {i}")));
}
let store_a = InMemoryMemoryStore::default();
let store_b = InMemoryMemoryStore::default();
let out_a = compile_and_store(&session, &store_a).expect("compile_and_store a");
let out_b = compile_and_store(&session, &store_b).expect("compile_and_store b");
prop_assert_eq!(out_a.bundle, out_b.bundle);
prop_assert_eq!(out_a.memories.len(), out_b.memories.len());
Comment on lines +183 to +184

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: This test claims that the complete DistillOutput is deterministic but compares only the number of memories, not their IDs, keys, or kinds. A regression that changes the persisted memory metadata or writes different facts while preserving the count would pass. Compare the complete memories values, and verify the stored contents if those are part of the contract. [incomplete implementation]

Severity Level: Major ⚠️
- ⚠️ Distill determinism property misses changed memory IDs or keys.
- ⚠️ ETL memory metadata regressions can pass CI undetected.
- ⚠️ Episodic persistence contents are not compared by this property.

Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** crates/sl-viewer/tests/properties_session_ledger_distill.rs
**Line:** 183:184
**Comment:**
	*Incomplete Implementation: This test claims that the complete `DistillOutput` is deterministic but compares only the number of memories, not their IDs, keys, or kinds. A regression that changes the persisted memory metadata or writes different facts while preserving the count would pass. Compare the complete `memories` values, and verify the stored contents if those are part of the contract.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

}
}

// ── Strategy helpers ───────────────────────────────────────────────────────

fn role_choice() -> impl Strategy<Value = Role> {
prop::sample::select(vec![
Role::User,
Role::Assistant,
Role::Subagent,
Role::Tool,
Role::System,
])
}
Loading
Loading