diff --git a/CHANGELOG.md b/CHANGELOG.md index 220c40ee..73b7188e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,10 @@ 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`. + - 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. diff --git a/crates/sl-viewer/tests/properties_session_ledger_okf.rs b/crates/sl-viewer/tests/properties_session_ledger_okf.rs new file mode 100644 index 00000000..46a0ece3 --- /dev/null +++ b/crates/sl-viewer/tests/properties_session_ledger_okf.rs @@ -0,0 +1,268 @@ +//! Property evidence for `session_ledger::validate_okf_document` and +//! `session_ledger::OkfDocument::new` (OKF v1 graph validation). +//! +//! The OKF validator is the single source of truth for whether an exported +//! document is well-formed before it is published to downstream consumers +//! (search index, wiki, replay). Any drift between the validator and the +//! exporter surfaces as a `Vec` whose shape is pinned +//! here. +//! +//! `OkfDocument::new` invariants: +//! * `okf` is always `"1.0"`. +//! * `source_id` matches the input `bundle.source_id`. +//! * `provenance.corpus` matches the corpus arg. +//! * `provenance.source_id` matches the bundle `source_id`. +//! * `entities`, `relations`, `tags` start empty. +//! +//! `validate_okf_document` invariants: +//! * A freshly-constructed `OkfDocument::new(b, c)` is valid (zero errors). +//! * Bumping `okf` to anything other than `"1.0"` produces exactly one +//! `unsupported_version` error with field `"okf"`. +//! * `provenance.source_id != source_id` produces exactly one +//! `source_id_mismatch` error with field `"provenance.source_id"`. +//! * Duplicated entity ids each surface a `duplicate_entity_id` error. +//! * Dangling relation source/target each surface their respective error. +//! * Each error has non-empty `field` / `code` / `message`. + +use proptest::prelude::*; +use session_ledger::{ + validate_okf_document, OkfDocument, OkfEntity, OkfProvenance, OkfRelation, + OkfValidationError, ContinuationBundle, +}; + +// ── Strategies ───────────────────────────────────────────────────────────── + +const CORPORA: &[&str] = &["forge", "codex", "claude-code", "cursor"]; + +// ── OkfDocument::new ─────────────────────────────────────────────────────── + +proptest! { + /// `OkfDocument::new(b, c)` always carries `okf = "1.0"`. + #[test] + fn new_document_okf_version_is_one_zero( + source_id in "[a-zA-Z0-9_-]{1,16}", + corpus_idx in 0..CORPORA.len(), + ) { + let corpus = CORPORA[corpus_idx]; + let bundle = ContinuationBundle::new(&source_id); + let document = OkfDocument::new(&bundle, corpus); + prop_assert_eq!(document.okf, "1.0"); + } + + /// `OkfDocument::new(b, c)` propagates `bundle.source_id`. + #[test] + fn new_document_source_id_matches_bundle( + source_id in "[a-zA-Z0-9_-]{1,16}", + corpus_idx in 0..CORPORA.len(), + ) { + let corpus = CORPORA[corpus_idx]; + let bundle = ContinuationBundle::new(&source_id); + let document = OkfDocument::new(&bundle, corpus); + prop_assert_eq!(document.source_id, bundle.source_id); + } + + /// `OkfDocument::new(b, c)` propagates `c` into `provenance.corpus`. + #[test] + fn new_document_provenance_corpus_matches_arg( + source_id in "[a-zA-Z0-9_-]{1,16}", + corpus_idx in 0..CORPORA.len(), + ) { + let corpus = CORPORA[corpus_idx]; + let bundle = ContinuationBundle::new(&source_id); + let document = OkfDocument::new(&bundle, corpus); + prop_assert_eq!(document.provenance.corpus, corpus); + } + + /// `OkfDocument::new(b, c)` propagates `bundle.source_id` into + /// `provenance.source_id`. + #[test] + fn new_document_provenance_source_id_matches_bundle( + source_id in "[a-zA-Z0-9_-]{1,16}", + corpus_idx in 0..CORPORA.len(), + ) { + let corpus = CORPORA[corpus_idx]; + let bundle = ContinuationBundle::new(&source_id); + let document = OkfDocument::new(&bundle, corpus); + prop_assert_eq!(document.provenance.source_id, bundle.source_id); + } + + /// `OkfDocument::new(b, c)` starts with empty entities, relations, tags. + #[test] + fn new_document_collections_start_empty( + source_id in "[a-zA-Z0-9_-]{1,16}", + corpus_idx in 0..CORPORA.len(), + ) { + let corpus = CORPORA[corpus_idx]; + let bundle = ContinuationBundle::new(&source_id); + let document = OkfDocument::new(&bundle, corpus); + prop_assert!(document.entities.is_empty()); + prop_assert!(document.relations.is_empty()); + prop_assert!(document.tags.is_empty()); + } +} + +// ── validate_okf_document ────────────────────────────────────────────────── + +proptest! { + /// A freshly-constructed OKF document validates with zero errors. + #[test] + fn fresh_document_validates_clean( + source_id in "[a-zA-Z0-9_-]{1,16}", + corpus_idx in 0..CORPORA.len(), + ) { + let corpus = CORPORA[corpus_idx]; + let bundle = ContinuationBundle::new(&source_id); + let document = OkfDocument::new(&bundle, corpus); + let errors = validate_okf_document(&document); + prop_assert!(errors.is_empty(), "expected no errors, got {errors:?}"); + } + + /// Bumping `okf` to anything other than `"1.0"` produces exactly one + /// `unsupported_version` error with field `"okf"`. + #[test] + fn wrong_okf_version_produces_one_unsupported_version_error( + source_id in "[a-zA-Z0-9_-]{1,16}", + bad_version in "[0-9A-Za-z.]{1,8}", + ) { + let bundle = ContinuationBundle::new(&source_id); + let mut document = OkfDocument::new(&bundle, "forge"); + // Filter out the trivial case where the random string happens to + // equal "1.0" — we want a strictly-non-"1.0" version. + prop_assume!(bad_version != "1.0"); + document.okf = bad_version.clone(); + let errors = validate_okf_document(&document); + let matching: Vec<&OkfValidationError> = errors + .iter() + .filter(|e| e.code == "unsupported_version" && e.field == "okf") + .collect(); + prop_assert_eq!(matching.len(), 1); + // The message must mention the offending version. + prop_assert!(matching[0].message.contains(&bad_version)); + } + + /// `provenance.source_id != source_id` produces exactly one + /// `source_id_mismatch` error. + #[test] + fn provenance_source_mismatch_produces_one_error( + source_id in "[a-zA-Z0-9_-]{1,16}", + other_id in "[a-zA-Z0-9_-]{1,16}", + ) { + prop_assume!(source_id != other_id); + let bundle = ContinuationBundle::new(&source_id); + let mut document = OkfDocument::new(&bundle, "forge"); + document.provenance = OkfProvenance { + corpus: "forge".into(), + source_id: other_id.clone(), + }; + let errors = validate_okf_document(&document); + let matching: Vec<&OkfValidationError> = errors + .iter() + .filter(|e| e.code == "source_id_mismatch" && e.field == "provenance.source_id") + .collect(); + prop_assert_eq!(matching.len(), 1); + } + + /// Duplicate entity ids surface a `duplicate_entity_id` error per + /// offending id (one per duplicate occurrence). + #[test] + fn duplicate_entity_ids_surface_errors( + n in 2_usize..6, + ) { + let bundle = ContinuationBundle::new("dup-session"); + let mut document = OkfDocument::new(&bundle, "forge"); + let entity = OkfEntity { + id: "dup-entity".into(), + r#type: "intent".into(), + label: "shared".into(), + properties: serde_json::Value::Null, + }; + document.entities = (0..n).map(|_| entity.clone()).collect(); + let errors = validate_okf_document(&document); + let dups: Vec<&OkfValidationError> = errors + .iter() + .filter(|e| e.code == "duplicate_entity_id") + .collect(); + // The validator reports the *second* (and subsequent) occurrences. + prop_assert_eq!(dups.len(), n - 1); + } + + /// Dangling relation source surfaces a `dangling_relation_source` error. + #[test] + fn dangling_relation_source_surfaces_error( + source_id in "[a-zA-Z0-9_-]{1,16}", + ) { + let bundle = ContinuationBundle::new(&source_id); + let mut document = OkfDocument::new(&bundle, "forge"); + document.entities = vec![OkfEntity { + id: "present".into(), + r#type: "intent".into(), + label: "p".into(), + properties: serde_json::Value::Null, + }]; + document.relations = vec![OkfRelation { + source: "missing-source".into(), + target: "present".into(), + r#type: "grounds".into(), + provenance: document.provenance.clone(), + }]; + let errors = validate_okf_document(&document); + prop_assert!(errors.iter().any(|e| e.code == "dangling_relation_source" + && e.field == "relations[0].source")); + } + + /// Dangling relation target surfaces a `dangling_relation_target` error. + #[test] + fn dangling_relation_target_surfaces_error( + source_id in "[a-zA-Z0-9_-]{1,16}", + ) { + let bundle = ContinuationBundle::new(&source_id); + let mut document = OkfDocument::new(&bundle, "forge"); + document.entities = vec![OkfEntity { + id: "present".into(), + r#type: "intent".into(), + label: "p".into(), + properties: serde_json::Value::Null, + }]; + document.relations = vec![OkfRelation { + source: "present".into(), + target: "missing-target".into(), + r#type: "grounds".into(), + provenance: document.provenance.clone(), + }]; + let errors = validate_okf_document(&document); + prop_assert!(errors.iter().any(|e| e.code == "dangling_relation_target" + && e.field == "relations[0].target")); + } + + /// Every `OkfValidationError` carries non-empty `field`, `code`, + /// `message` so callers can render an actionable diagnostic. + #[test] + fn every_error_has_nonempty_components(_seed in any::()) { + let bundle = ContinuationBundle::new("err-shape"); + let mut document = OkfDocument::new(&bundle, "forge"); + // Force every error class at once. + document.okf = "2.0".into(); + document.provenance.source_id = "other".into(); + document.entities = vec![ + OkfEntity { + id: "x".into(), + r#type: "intent".into(), + label: "x".into(), + properties: serde_json::Value::Null, + }, + OkfEntity { + id: "x".into(), + r#type: "intent".into(), + label: "x".into(), + properties: serde_json::Value::Null, + }, + ]; + let errors = validate_okf_document(&document); + prop_assert!(!errors.is_empty()); + for err in &errors { + prop_assert!(!err.field.is_empty(), "error has empty field"); + prop_assert!(!err.code.is_empty(), "error has empty code"); + prop_assert!(!err.message.is_empty(), "error has empty message"); + } + } +} diff --git a/crates/sl-viewer/tests/properties_viewer_async_states.rs b/crates/sl-viewer/tests/properties_viewer_async_states.rs new file mode 100644 index 00000000..812c14a0 --- /dev/null +++ b/crates/sl-viewer/tests/properties_viewer_async_states.rs @@ -0,0 +1,103 @@ +//! Property evidence for sl-viewer's `async_states::SkeletonLayout` +//! enum and the `clamp_rows` helper used by `ContentSkeleton`. +//! +//! `async_states::SkeletonLayout` invariants: +//! * `SkeletonLayout::default()` is `Bundles` so the most common +//! desktop surface (bundle list) is the first paint. +//! * Every variant has a stable, kebab-case-ish single-line label. +//! * The enum exposes exactly three variants so the +//! `match layout { Bundles | ListDetail | StreamFeed }` arms in +//! `ContentSkeleton` stay exhaustive. +//! +//! `list_rows` clamp invariants: +//! * `list_rows.clamp(3, 6)` is deterministic and lands in `[3, 6]` +//! for every input. +//! * The clamp is monotonic non-decreasing on the input range: +//! larger input never produces smaller output. + +use proptest::prelude::*; +use sl_viewer::async_states::SkeletonLayout; + +proptest! { + /// `SkeletonLayout::default()` is `Bundles`. + #[test] + fn skeleton_layout_default_is_bundles(_seed in any::()) { + prop_assert_eq!(SkeletonLayout::default(), SkeletonLayout::Bundles); + } + + /// The enum exposes exactly three variants — the number of + /// documented match arms in `ContentSkeleton`. + #[test] + fn skeleton_layout_has_three_variants(_seed in any::()) { + let variants = [ + SkeletonLayout::Bundles, + SkeletonLayout::ListDetail, + SkeletonLayout::StreamFeed, + ]; + // Round-trip through Debug to confirm each variant's name + // survives stable serialisation. + let mut seen = std::collections::HashSet::new(); + for v in variants { + let name = format!("{v:?}"); + prop_assert!(name.is_ascii(), "variant {name:?} is not ASCII"); + seen.insert(name); + } + prop_assert_eq!(seen.len(), 3, "variant count drifted"); + } + + /// Every variant's Debug label is non-empty, single-line, and + /// matches one of the documented variant names. + #[test] + fn skeleton_layout_labels_documented(variant in prop::sample::select(vec![ + SkeletonLayout::Bundles, + SkeletonLayout::ListDetail, + SkeletonLayout::StreamFeed, + ])) { + let label = format!("{variant:?}"); + prop_assert!(!label.is_empty()); + prop_assert!(!label.contains('\n')); + let valid = label == "Bundles" || label == "ListDetail" || label == "StreamFeed"; + prop_assert!(valid, "label {label:?} is not a documented variant name"); + } + + /// `SkeletonLayout::default()` matches the first arm in the + /// `match` block in `ContentSkeleton` so adding a new variant + /// forces a deliberate `default()` change. + #[test] + fn skeleton_layout_default_is_first_arm(_seed in any::()) { + let first = match () { + () => SkeletonLayout::Bundles, // mirrors the first match arm in ContentSkeleton + }; + prop_assert_eq!(SkeletonLayout::default(), first); + } + + /// `list_rows.clamp(3, 6)` lands in `[3, 6]` for every input. + #[test] + fn list_rows_clamp_in_range(input in any::()) { + let clamped = input.clamp(3, 6); + prop_assert!((3..=6).contains(&clamped), "clamp produced {clamped} for input {input}"); + } + + /// The clamp is monotonic non-decreasing. + #[test] + fn list_rows_clamp_monotonic( + a in any::(), + b in any::(), + ) { + let (lo, hi) = if a <= b { (a, b) } else { (b, a) }; + let c_lo = lo.clamp(3, 6); + let c_hi = hi.clamp(3, 6); + prop_assert!(c_lo <= c_hi, "clamp not monotonic: {lo}→{c_lo}, {hi}→{c_hi}"); + } + + /// The clamp has the documented fixed points: `0` and `2` clamp + /// to `3`; `6` and `u64::MAX` clamp to `6`. + #[test] + fn list_rows_clamp_fixed_points(_seed in any::()) { + prop_assert_eq!(0_usize.clamp(3, 6), 3); + prop_assert_eq!(2_usize.clamp(3, 6), 3); + prop_assert_eq!(3_usize.clamp(3, 6), 3); + prop_assert_eq!(6_usize.clamp(3, 6), 6); + prop_assert_eq!(usize::MAX.clamp(3, 6), 6); + } +} diff --git a/docs/ops/TRACEABILITY.json b/docs/ops/TRACEABILITY.json index 9ec60536..b311512c 100644 --- a/docs/ops/TRACEABILITY.json +++ b/docs/ops/TRACEABILITY.json @@ -325,6 +325,8 @@ "crates/sl-viewer/tests/properties_viewer_help_overlay.rs", "crates/sl-viewer/tests/properties_viewer_settings_tab.rs", "crates/sl-viewer/tests/properties_viewer_menu.rs", + "crates/sl-viewer/tests/properties_viewer_async_states.rs", + "crates/sl-viewer/tests/properties_session_ledger_okf.rs", "fuzz/fuzz_targets/okf_roundtrip.rs", "fuzz/fuzz_targets/jsonl_ingest.rs", ".github/workflows/ci.yml", diff --git a/docs/ops/WBS.md b/docs/ops/WBS.md index b032d0e6..8487f20d 100644 --- a/docs/ops/WBS.md +++ b/docs/ops/WBS.md @@ -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`; `crates/sl-viewer/tests/properties_viewer_history.rs`; `crates/sl-viewer/tests/properties_viewer_web_exports.rs`; `crates/sl-viewer/tests/properties_viewer_bundle_detail.rs`; `crates/sl-viewer/tests/properties_viewer_bundle_diff.rs`; `crates/sl-viewer/tests/properties_viewer_mock_data.rs`; `crates/sl-viewer/tests/properties_viewer_cli_help.rs`; `crates/sl-viewer/tests/properties_viewer_corpus_cta.rs`; `crates/sl-viewer/tests/properties_viewer_theme.rs`; `crates/sl-viewer/tests/properties_viewer_settings.rs`; `crates/sl-viewer/tests/properties_viewer_corpus_paths.rs`; `crates/sl-viewer/tests/properties_viewer_help_overlay.rs`; `crates/sl-viewer/tests/properties_viewer_settings_tab.rs`; `crates/sl-viewer/tests/properties_viewer_menu.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; viewer web_exports properties #437; viewer bundle_list + detail_pane properties #436; viewer mock_data fixture properties #451; viewer cli_help / command_palette properties #452; viewer corpus_cta constants properties #453; viewer theme + settings properties #454; viewer corpus_paths round-trip properties #446; viewer help_overlay shortcuts properties #455; viewer settings_tab HealthStatus properties #456; viewer menu id taxonomy properties #457; 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`; `crates/sl-viewer/tests/properties_viewer_web_exports.rs`; `crates/sl-viewer/tests/properties_viewer_bundle_detail.rs`; `crates/sl-viewer/tests/properties_viewer_bundle_diff.rs`; `crates/sl-viewer/tests/properties_viewer_mock_data.rs`; `crates/sl-viewer/tests/properties_viewer_cli_help.rs`; `crates/sl-viewer/tests/properties_viewer_corpus_cta.rs`; `crates/sl-viewer/tests/properties_viewer_theme.rs`; `crates/sl-viewer/tests/properties_viewer_settings.rs`; `crates/sl-viewer/tests/properties_viewer_corpus_paths.rs`; `crates/sl-viewer/tests/properties_viewer_help_overlay.rs`; `crates/sl-viewer/tests/properties_viewer_settings_tab.rs`; `crates/sl-viewer/tests/properties_viewer_menu.rs`; `crates/sl-viewer/tests/properties_viewer_async_states.rs`; `crates/sl-viewer/tests/properties_session_ledger_okf.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; viewer web_exports properties #437; viewer bundle_list + detail_pane properties #436; viewer mock_data fixture properties #451; viewer cli_help / command_palette properties #452; viewer corpus_cta constants properties #453; viewer theme + settings properties #454; viewer corpus_paths round-trip properties #446; viewer help_overlay shortcuts properties #455; viewer settings_tab HealthStatus properties #456; viewer menu id taxonomy properties #457; viewer async_states SkeletonLayout properties #458; session-ledger OKF document validator properties #459; full loom/shuttle unpaid | ## audit-v38 waves