Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/); versioning is [SemVer](

- sl-viewer search/memory property surface (WBS-6.2 #435): `crates/sl-viewer/tests/properties_viewer_search_memory.rs` adds 12 proptest properties — `search_view::build_query` trims each field, emits a field iff its post-trim value is non-empty, encodes the documented break-character set (` `, `,`, `#`, `&`, `=`, `+`), and always emits `limit=` whose value is the parsed input or the documented `"50"` fallback. `advanced_filter_active_count` counts `min_tokens`/`tags` non-empty fields, treats `"50"` as the default for `limit`, and is trim-invariant. `memory_tab::to_wiki_page` carries `session_id` and `title` through unchanged and is deterministic across calls; `all_wiki_pages_from_sessions` produces exactly one page per input session, in input order.

- sl-viewer history_tab property surface (WBS-6.2 #444): `crates/sl-viewer/tests/properties_viewer_history.rs` adds 15 proptest properties — `history_tab::to_timeline_entry` carries `summary.id` / `summary.title` / `summary.message_count` / `summary.intent_state` (= `Extracted`), `corpus`, and `cwd` through unchanged; `message_previews` is capped at 3 (empty when input has no messages); `total_messages` matches input. `unfinished` is `false` for empty sessions, `false` when the last message content (case-insensitive) contains one of the six documented done-phrases ("looks good", "approved", "ship it", "all good", "thanks", "done"), and `true` otherwise. `to_timeline_entry` is deterministic. `all_timeline_entries` produces one entry per input session, sorts by `total_messages` descending (newest-first), every session id appears exactly once, and is deterministic.

- Wave-44 plan landed: `WAVE44_SCOPE.md` + `docs/ops/WAVE44_PERT.md` enumerate 6 close-out lanes (3 machine, 3 human-gated) for the 6 unpaid residuals from Wave-43 (396/402 → 402/402 target). Theme: stack-stability closure + i18n migration + eval coverage + supply-chain signing.
- Wave-44 reaudit (Wave-44-D): `audit/SCORECARD.md` refresh at commit `13c974f7` (machine-w44-reaudit); `docs/ops/TRACEABILITY.json` overall_audit wave=Wave-44 commit=13c974f7 (conservative hold at 396/402); `docs/ops/GAP_QA_MATRIX.md` C00 + C08 + PLAN-W8-B rows reflect Wave-44 closure (#368 W44-B6 corpus / #372 W44-B1 loom / #373 PERT correction). 2 of 3 machine lanes shipped 2026-07-24; remaining 6 raw pts across C04 L36 / C08 L76 / C11 L110.

Expand Down
280 changes: 280 additions & 0 deletions crates/sl-viewer/tests/properties_viewer_history.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
//! Property evidence for sl-viewer's `history_tab::to_timeline_entry`

Check failure on line 1 in crates/sl-viewer/tests/properties_viewer_history.rs

View workflow job for this annotation

GitHub Actions / Trunk Check

rustfmt

Incorrect formatting, autoformat by running 'trunk fmt'
//! and `history_tab::all_timeline_entries` reductions.
//!
//! Integration tests. The unit tests in `history_tab.rs` pin specific
//! values; these properties pin invariants over the full shape of
//! inputs the helpers can receive.
//!
//! `history_tab::to_timeline_entry` invariants:
//! * `summary.id` is the session id; `summary.title` is the session
//! title (mirrors `Option<String>` identity); `summary.message_count`
//! equals the session's `messages.len()`; `summary.intent_state` is
//! always `IntentState::Extracted`.
//! * `corpus` and `cwd` are carried through unchanged.
//! * `message_previews` has at most 3 entries (the documented cap)
//! and is empty when the session has no messages. The first 3
//! messages are previewed, in input order.
//! * `total_messages` equals `session.messages.len()`.
//! * `unfinished` is `false` when the session has no messages, and
//! `false` when the last message content (case-insensitive) contains
//! one of the documented "done" phrases ("looks good", "approved",
//! "ship it", "all good", "thanks", "done"); otherwise `true`.
//! * Deterministic across calls.
//!
//! `history_tab::all_timeline_entries` invariants:
//! * Output length matches input length.
//! * The output is sorted by `total_messages` descending (newest-first
//! by message count, per the documented comment).
//! * Every session's `id` appears in the output exactly once.
//!
//! proptest is added to `sl-viewer/[dev-dependencies]` (mirroring the
//! workspace root); see PR #425 for the initial wiring.

use proptest::prelude::*;
use session_ledger::domain::intent::IntentState;
use session_ledger::domain::session::{Corpus, Message, Role, Session};
use sl_viewer::history_tab::{all_timeline_entries, to_timeline_entry};

// ── strategies ──────────────────────────────────────────────────────────────

/// `Message` strategy — role + content + optional ts_ms.
fn message_strategy() -> impl Strategy<Value = Message> {
(
prop::sample::select(vec![
Role::User,
Role::Assistant,
Role::Subagent,
Role::Tool,
Role::System,
]),
prop::string::string_regex("[ -~]{0,120}").expect("valid regex"),

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: The message strategy generates only printable ASCII content with a maximum length of 120 bytes, so it never exercises multibyte UTF-8 or longer messages. Consequently, the property suite cannot detect the production slicing panic for a multibyte message whose byte length exceeds 100 but whose byte 97 is not a character boundary. Generate arbitrary Unicode content and lengths beyond the truncation threshold. [possible bug]

Severity Level: Major ⚠️
- ❌ History timeline rendering can panic on long Unicode messages.
- ⚠️ Viewer history becomes unavailable for affected transcripts.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

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

**Path:** crates/sl-viewer/tests/properties_viewer_history.rs
**Line:** 50:50
**Comment:**
	*Possible Bug: The message strategy generates only printable ASCII content with a maximum length of 120 bytes, so it never exercises multibyte UTF-8 or longer messages. Consequently, the property suite cannot detect the production slicing panic for a multibyte message whose byte length exceeds 100 but whose byte 97 is not a character boundary. Generate arbitrary Unicode content and lengths beyond the truncation threshold.

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
👍 | 👎

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repo files of interest =="
git ls-files | grep -E '(^|/)(properties_viewer_history\.rs|history_tab\.rs|*.rs)$' | grep -E 'crates/sl-viewer/(tests/properties_viewer_history\.rs|src/history_tab\.rs)' || true

echo
echo "== outline history_tab.rs =="
ast-grep outline crates/sl-viewer/src/history_tab.rs --view expanded || true

echo
echo "== relevant tests =="
wc -l crates/sl-viewer/tests/properties_viewer_history.rs
sed -n '1,120p' crates/sl-viewer/tests/properties_viewer_history.rs

echo
echo "== relevant history_tab.rs =="
wc -l crates/sl-viewer/src/history_tab.rs
sed -n '1,240p' crates/sl-viewer/src/history_tab.rs

echo
echo "== search message_strategy and content slicing =="
rg -n "message_strategy|\\.content\\[\\.\\.\\.97\\]|\\.content\\[\\.\\.\\.|string_regex|to_json|SessionMessage|properties" crates/sl-viewer -S

Repository: KooshaPari/SessionLedger

Length of output: 19237


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
s = "\u00e9" * 60
byte_len = len(s.encode("utf-8"))
print(f"bytes={byte_len}")
for cut in [96, 97, 98]:
    try:
        print(f"slice ...{cut}: OK len={len(s[:cut])}")
    except Exception as e:
        print(f"slice ...{cut}: ERROR {type(e).__name__}: {e}")
PY

Repository: KooshaPari/SessionLedger

Length of output: 241


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
s = "é" * 60
byte_len = len(s.encode("utf-8"))
print(f"bytes={byte_len}")
for cut in range(90, 106):
    try:
        print(f"slice ...{cut}: OK len={len(s[:cut])}")
    except Exception as e:
        print(f"slice ...{cut}: ERROR {type(e).__name__}: {e}")
PY

Repository: KooshaPari/SessionLedger

Length of output: 546


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import unicodedata

s = "".join(chr(ord("é") + i % 8) for i in range(80))
byte_len = len(s.encode("utf-8"))
print(f"bytes={byte_len}")
print(f"{s[45:55]!r}")
print([((ord(c), unicodedata.name(c, "?"))) for c in s[45:55]])
for cut in range(80, 106):
    try:
        print(f"slice ...{cut}: OK len={len(s[:cut])}")
    except Exception as e:
        print(f"slice ...{cut}: ERROR {type(e).__name__}: {e}")

# Find slices that do not end on codepoint boundaries while content is longer than 100 bytes.
bad = []
for cut in range(80, 106):
    prefix = s[:cut]
    if prefix.isascii() is False and (len(s.encode("utf-8")) > 100 and not prefix.isascii() or cut > len(s)):
        pass
print("bad slices near 100-byte check:", [cut for cut in range(80, 106) if 'slice ...' in None])
PY

Repository: KooshaPari/SessionLedger

Length of output: 1398


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import random
random.seed(1)
samples = []
for _ in range(10000):
    n = random.randint(1, 100)
    s = "".join(chr(0x80 + random.randint(0, 0x7F)) for _ in range(n))
    if len(s.encode("utf-8")) > 100:
        samples.append(s)
        if len(samples) == 10:
            break
print(len(samples), "samples with over-100-byte UTF-8 messages")
fails = []
for s in samples:
    if len(s.encode("utf-8")) >= 100:
        for cut in range(80, 106):
            try:
                s[:cut]
            except UnicodeDecodeError as e:
                fails.append((s.encode("utf-8").hex(), cut, str(e)))
print("finds:", len(fails))
for f in fails[:5]:
    print(f)
PY

Repository: KooshaPari/SessionLedger

Length of output: 216


🌐 Web query:

Rust String indexing panic byte boundary &str[..n] documentation

💡 Result:

In Rust, string slicing using the &s[..n] or &s[start..end] syntax is designed to operate on byte offsets rather than character positions [1][2][3]. Because Rust strings are UTF-8 encoded, characters can occupy between 1 and 4 bytes [3][4]. To ensure memory safety and maintain valid UTF-8 sequences, the Rust runtime will panic if the specified byte indices do not fall on a valid character boundary [1][5][2]. Key details regarding this behavior include: Why it panics: Rust requires that any string slice represents a valid UTF-8 string [1][3]. If an index falls inside a multi-byte character sequence, returning a slice would result in an invalid, non-UTF-8 string. To prevent this, the language enforces that slices must begin and end at valid character boundaries [5][4]. How to check for boundaries: You can use the is_char_boundary(index) method on a &str or String to verify if a specific byte index is a valid starting point for a character [1][6]. Non-panicking alternatives: - The .get() method: If you want to avoid a panic, use the get method (e.g., s.get(start..end)), which returns an Option<&str>. It returns Some(&str) if the range is valid and None if the indices are out of bounds or not on character boundaries [1][6][3]. - Finding safe boundaries: You can use floor_char_boundary(index) or ceil_char_boundary(index) to find the closest valid byte offsets to your desired position [1][6][7]. Under the hood, slicing is implemented via the SliceIndex trait, which dictates that indexing must be constant-time [1][5]. This is why byte indexing is used instead of character indexing—counting characters would require iterating through the string, which is an O(n) operation [1][8]. The indexing syntax is essentially syntactic sugar for calling the index method, which is why it panics when the safety constraints of SliceIndex are violated [5][9][10].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rustc --version || true

tmp="$(mktemp -d)"
cat > "$tmp/slice_boundary.rs" <<'RS'
fn main() {
    let s = "é".repeat(50);
    println!("bytes={}", s.len());
    for n in 94..99 {
        std::panic::set_hook(Box::new(|_| {}));
        let r = std::panic::catch_unwind(|| {
            let trimmed = &s[..n];
            println!("safe_n={n} trimmed_byte_len={}", trimmed.len());
        });
        if r.is_err() {
            println!("panic_at_byte={n}");
        }
    }
}
RS
rustc "$tmp/slice_boundary.rs" -o "$tmp/bin"
"$tmp/bin"

Repository: KooshaPari/SessionLedger

Length of output: 505


Exercise Unicode truncation inputs.

message_strategy only generates ASCII content. to_timeline_entry truncates oversized message bodies with &m.content[..97], which panics on out-of-range UTF-8 multibyte boundaries. Add a multibyte over-limit proptest case and update the converter to truncate using a character boundary instead of a raw byte offset.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/sl-viewer/tests/properties_viewer_history.rs` at line 50, Extend
message_strategy with a Unicode multibyte input exceeding the truncation limit,
and update to_timeline_entry to truncate at a valid character boundary rather
than slicing content at byte offset 97. Preserve the existing truncation length
and behavior for ASCII and shorter messages.

prop::option::of(0i64..1_000_000_000_000i64),
)
.prop_map(|(role, content, ts_ms)| {
let mut m = Message::new(role, content);
m.ts_ms = ts_ms;
m
})
}

/// `Session` strategy — id + 0..8 messages + optional title + corpus.
fn session_strategy() -> impl Strategy<Value = Session> {
(
// session_id — non-empty, identifier-shaped.
prop::string::string_regex("[a-zA-Z0-9_-]{1,16}").expect("valid regex"),
// 0..8 messages.
prop::collection::vec(message_strategy(), 0..8),
// title — `Option<String>`.
prop::option::of(
prop::string::string_regex("[A-Za-z0-9 ._-]{0,40}").expect("valid regex"),
),
// corpus — pick one of the documented variants.
prop::sample::select(vec![
Corpus::Forge,
Corpus::Codex,
Corpus::ClaudeCode,
Corpus::Cursor,
Corpus::FactoryDroid,
Corpus::ChatGptWeb,
Corpus::ClaudeWeb,
Corpus::GeminiWeb,
]),
// cwd — `Option<String>`.
prop::option::of(
prop::string::string_regex("[/a-zA-Z0-9._-]{0,40}").expect("valid regex"),
),
)
.prop_map(|(id, messages, title, corpus, cwd)| {
let mut s = Session::new(id, corpus);
s.messages = messages;
s.title = title;
s.cwd = cwd;
s
})
}

// ── history_tab::to_timeline_entry ──────────────────────────────────────────

proptest! {
/// Property: `summary.id` is the session id.
#[test]
fn to_timeline_entry_carries_session_id(session in session_strategy()) {
let entry = to_timeline_entry(&session);
prop_assert_eq!(&entry.summary.id, &session.id);
}

/// Property: `summary.title` is the session title (mirrors
/// `Option<String>` identity — `None` stays `None`).
#[test]
fn to_timeline_entry_carries_title(session in session_strategy()) {
let entry = to_timeline_entry(&session);
prop_assert_eq!(entry.summary.title, session.title);
}

/// Property: `summary.message_count` equals the session's
/// `messages.len()`.
#[test]
fn to_timeline_entry_message_count_matches(session in session_strategy()) {
let entry = to_timeline_entry(&session);
prop_assert_eq!(entry.summary.message_count, session.messages.len());
}

/// Property: `summary.intent_state` is always `IntentState::Extracted`
/// — the only state the reduction can produce given the heuristic
/// extractors.
#[test]
fn to_timeline_entry_intent_state_always_extracted(session in session_strategy()) {
let entry = to_timeline_entry(&session);
prop_assert_eq!(entry.summary.intent_state, IntentState::Extracted);
}

/// Property: `corpus` and `cwd` are carried through unchanged.
#[test]
fn to_timeline_entry_carries_corpus_and_cwd(session in session_strategy()) {
let entry = to_timeline_entry(&session);
prop_assert_eq!(entry.corpus, session.corpus);
prop_assert_eq!(entry.cwd, session.cwd);
}

/// Property: `message_previews` has at most 3 entries (the
/// documented cap) and is empty when the session has no messages.
/// When non-empty, the previews cover the first N ≤ 3 messages,
/// in input order.
#[test]
fn to_timeline_entry_message_previews_capped(session in session_strategy()) {
let entry = to_timeline_entry(&session);
prop_assert!(entry.message_previews.len() <= 3);
let expected = session.messages.len().min(3);
prop_assert_eq!(entry.message_previews.len(), expected);
}

/// Property: `total_messages` equals `session.messages.len()`.
#[test]
fn to_timeline_entry_total_messages_matches(session in session_strategy()) {
let entry = to_timeline_entry(&session);
prop_assert_eq!(entry.total_messages, session.messages.len());
}

/// Property: `unfinished` is `false` when the session has no
/// messages (empty sessions aren't "in-progress").
#[test]
fn to_timeline_entry_unfinished_false_for_empty(
(id, corpus) in (
prop::string::string_regex("[a-zA-Z0-9_-]{1,8}").expect("valid regex"),
prop::sample::select(vec![Corpus::Forge, Corpus::ClaudeCode]),
)
) {
let session = Session::new(id, corpus);
let entry = to_timeline_entry(&session);
prop_assert!(!entry.summary.unfinished);
}

/// Property: `unfinished` is `false` when the last message's
/// content (case-insensitive) contains one of the documented
/// "done" phrases — "looks good", "approved", "ship it",
/// "all good", "thanks", "done".
#[test]
fn to_timeline_entry_unfinished_false_for_done_phrase(phrase in prop::sample::select(vec![
"looks good", "approved", "ship it", "all good", "thanks", "done",
"Looks Good", "APPROVED", "Ship It", "All Good", "THANKS", "DONE",
])) {
// Build a session whose last message content == phrase.
let mut session = Session::new("sess-1", Corpus::Forge);
let mut msg = Message::new(Role::Assistant, phrase.to_owned());
msg.ts_ms = Some(0);
session.messages.push(msg);
let entry = to_timeline_entry(&session);
prop_assert!(!entry.summary.unfinished, "phrase {phrase:?} should mark session as finished");
}

/// Property: `unfinished` is `true` when the session has
/// messages and the last message content does NOT contain any
/// done-phrase substring (case-insensitive).
#[test]
fn to_timeline_entry_unfinished_true_for_non_done_last(
content in prop::string::string_regex("[A-Za-z0-9 ]{3,40}").expect("valid regex"),
) {
// Filter out any content that happens to match a done phrase.
let lower = content.to_lowercase();
let matches_done = ["looks good", "approved", "ship it", "all good", "thanks", "done"]
.iter().any(|p| lower.contains(p));
prop_assume!(!matches_done);
prop_assume!(!content.is_empty());

let mut session = Session::new("sess-1", Corpus::Forge);
let mut msg = Message::new(Role::User, content.clone());
msg.ts_ms = Some(0);
session.messages.push(msg);
let entry = to_timeline_entry(&session);
prop_assert!(
entry.summary.unfinished,
"session with non-done last message {content:?} should be unfinished",
);
}

/// Property: `to_timeline_entry` is deterministic — applying it
/// twice to the same session yields the same entry.
#[test]
fn to_timeline_entry_is_deterministic(session in session_strategy()) {
let a = to_timeline_entry(&session);
let b = to_timeline_entry(&session);
prop_assert_eq!(a, b);
}
}

// ── history_tab::all_timeline_entries ───────────────────────────────────────

proptest! {
/// Property: output length equals input length.
#[test]
fn all_timeline_entries_length_matches(
sessions in prop::collection::vec(session_strategy(), 0..6),
) {
let entries = all_timeline_entries(&sessions);
prop_assert_eq!(entries.len(), sessions.len());
}

/// Property: the output is sorted by `total_messages` descending
/// (newest-first by message count, per the documented comment).
/// Tied entries remain in stable-sort input order.
#[test]
fn all_timeline_entries_sorted_by_message_count_desc(
sessions in prop::collection::vec(session_strategy(), 1..8),
) {
let entries = all_timeline_entries(&sessions);
for win in entries.windows(2) {
prop_assert!(
win[0].total_messages >= win[1].total_messages,
"entry {} ({} msgs) should sort before entry {} ({} msgs)",
0,
win[0].total_messages,
1,
win[1].total_messages,
);
}
}

/// Property: every session's id appears in the output exactly once.
#[test]
fn all_timeline_entries_unique_ids(
sessions in prop::collection::vec(session_strategy(), 1..6),
) {
let entries = all_timeline_entries(&sessions);
let mut ids: Vec<_> = entries.iter().map(|e| e.summary.id.clone()).collect();
ids.sort();
let mut unique = ids.clone();
unique.dedup();
prop_assert_eq!(ids.len(), unique.len(), "duplicate ids in output: {:?}", ids);
Comment on lines +259 to +267

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Define the duplicate-ID contract consistently.

The generator permits duplicate input IDs. all_timeline_entries produces one entry for each input session and does not deduplicate IDs.

  • crates/sl-viewer/tests/properties_viewer_history.rs#L259-L267: Sort and compare input and output ID vectors to verify ID-multiset preservation.
  • CHANGELOG.md#L25-L25: Replace the unique-ID claim with one output entry per input session.
📍 Affects 2 files
  • crates/sl-viewer/tests/properties_viewer_history.rs#L259-L267 (this comment)
  • CHANGELOG.md#L25-L25
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/sl-viewer/tests/properties_viewer_history.rs` around lines 259 - 267,
Update all_timeline_entries_unique_ids in
crates/sl-viewer/tests/properties_viewer_history.rs:259-267 to collect, sort,
and compare the input session ID multiset with the output entry ID multiset,
preserving duplicate IDs rather than deduplicating them. Update CHANGELOG.md:25
to replace the unique-ID claim with the contract that all_timeline_entries emits
one output entry per input session.

}

/// Property: `all_timeline_entries` is deterministic — applying
/// it twice to the same slice yields the same `Vec<TimelineEntry>`.
#[test]
fn all_timeline_entries_is_deterministic(
sessions in prop::collection::vec(session_strategy(), 0..6),
) {
let a = all_timeline_entries(&sessions);
let b = all_timeline_entries(&sessions);
prop_assert_eq!(a, b);
}
}
Comment on lines +33 to +280

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Fix the rustfmt gate.

The supplied Trunk check reports a rustfmt failure for this new Rust file. Run trunk fmt and commit the resulting changes. Then run the required pinned-toolchain validation, including the locked build, all-features tests, Clippy, rustfmt, and cargo check -p sl-viewer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/sl-viewer/tests/properties_viewer_history.rs` around lines 33 - 280,
Run trunk fmt on the new property-test file and commit the resulting formatting
changes. Then validate with the pinned toolchain using the locked build,
all-features tests, Clippy, rustfmt, and cargo check -p sl-viewer; resolve any
failures before completing the change.

Sources: Coding guidelines, Linters/SAST tools

1 change: 1 addition & 0 deletions docs/ops/TRACEABILITY.json
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@
"crates/sl-viewer/tests/properties_viewer_unfinished_tab.rs",
"crates/sl-viewer/tests/properties_viewer_timeline.rs",
"crates/sl-viewer/tests/properties_viewer_search_memory.rs",
"crates/sl-viewer/tests/properties_viewer_history.rs",
"fuzz/fuzz_targets/okf_roundtrip.rs",
"fuzz/fuzz_targets/jsonl_ingest.rs",
".github/workflows/ci.yml",
Expand Down
2 changes: 1 addition & 1 deletion docs/ops/WBS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ without a new audit.
| WBS-4.2 | P4 FTS recall via context-mode and explicit TUI decision | partial | human | `docs/DESIGN.md` §3, §7; `crates/sl-viewer/` | DESIGN P4 residual; C00, C11 |
| WBS-5.1 | P5 deterministic dedup merge and crash/lost-work recovery E2E | done | machine | `src/domain/merge.rs`; `src/domain/worklog.rs`; `tests/merge_recovery.rs` | FR-011; T-024, T-035; C03 |
| WBS-6.1 | P6 85% coverage gate and deterministic golden corpus | done | machine | `.github/workflows/ci.yml`; `tests/okf_golden.rs`; `tests/fixtures/okf/` | T-037, T-038; C01, C08 |
| WBS-6.2 | P6 property tests, fuzzing, race checks, and enforced performance budgets | partial | machine | `tests/properties.rs`; `crates/sl-viewer/tests/properties_viewer.rs`; `crates/sl-viewer/tests/properties_viewer_theme_url.rs`; `crates/sl-viewer/tests/properties_viewer_unfinished_tab.rs`; `crates/sl-viewer/tests/properties_viewer_timeline.rs`; `crates/sl-viewer/tests/properties_viewer_search_memory.rs`; `fuzz/fuzz_targets/okf_roundtrip.rs`; `fuzz/fuzz_targets/jsonl_ingest.rs`; `.github/workflows/ci.yml`; `.github/workflows/bench-gate.yml`; `docs/ops/perf-baseline.json`; `scripts/bench-gate.ps1`; `benches/pipeline.rs`; `tests/loom_model.rs` | DESIGN P6 residual; C00 L6-L8; C07 L66-L68; C08 L74; perf-budget enforced Wave-26 #223; p95 latency enforced Wave-30 #256; FSM properties Wave-31 #261; soft loom Wave-31 #264; viewer corpus_paths/parquet/settings properties #425; viewer theme + daemon_url properties #427; viewer unfinished_tab properties + fuzz/rootless CI drift fixes #428; viewer bundle_diff + timeline properties + web_exports/hmetic-pin cleanups #432; viewer bundle_diff properties #434; viewer search/memory properties #435; full loom/shuttle unpaid |
| WBS-6.2 | P6 property tests, fuzzing, race checks, and enforced performance budgets | partial | machine | `tests/properties.rs`; `crates/sl-viewer/tests/properties_viewer.rs`; `crates/sl-viewer/tests/properties_viewer_theme_url.rs`; `crates/sl-viewer/tests/properties_viewer_unfinished_tab.rs`; `crates/sl-viewer/tests/properties_viewer_timeline.rs`; `crates/sl-viewer/tests/properties_viewer_search_memory.rs`; `crates/sl-viewer/tests/properties_viewer_history.rs`; `fuzz/fuzz_targets/okf_roundtrip.rs`; `fuzz/fuzz_targets/jsonl_ingest.rs`; `.github/workflows/ci.yml`; `.github/workflows/bench-gate.yml`; `docs/ops/perf-baseline.json`; `scripts/bench-gate.ps1`; `benches/pipeline.rs`; `tests/loom_model.rs` | DESIGN P6 residual; C00 L6-L8; C07 L66-L68; C08 L74; perf-budget enforced Wave-26 #223; p95 latency enforced Wave-30 #256; FSM properties Wave-31 #261; soft loom Wave-31 #264; viewer corpus_paths/parquet/settings properties #425; viewer theme + daemon_url properties #427; viewer unfinished_tab properties + fuzz/rootless CI drift fixes #428; viewer bundle_diff + timeline properties + web_exports/hmetic-pin cleanups #432; viewer bundle_diff properties #434; viewer search/memory properties #435; viewer history_tab properties #444; full loom/shuttle unpaid |

## audit-v38 waves

Expand Down
Loading