Skip to main content

remem/eval/coding_bench/
audit_contract.rs

1use anyhow::{bail, Result};
2use rusqlite::Connection;
3use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5use std::collections::BTreeMap;
6
7use super::types::{BenchCondition, RunReport};
8use crate::context_bundle::{ChannelKind, ContextAudit, DegradedMode};
9
10const SUPPORTED_CONTEXT_BUNDLE_SCHEMA_V1: u32 = 1;
11
12#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
13#[serde(rename_all = "snake_case")]
14pub enum RememContextAuditStatus {
15    Verified,
16    ContractFailure,
17    NotApplicable,
18}
19
20#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
21#[serde(deny_unknown_fields)]
22pub struct RememContextAuditSnapshot {
23    pub injection_run_id: String,
24    pub bundle_schema_version: u32,
25    pub plan_schema_version: u32,
26    pub policy_version: String,
27    pub relevance_policy_version: String,
28    pub plan_hash: String,
29    pub audit_hash: String,
30    pub injection_binding_hash: String,
31    pub degraded_mode: DegradedMode,
32    pub candidates_considered: u32,
33    pub selected_count: u32,
34    pub dropped_count: u32,
35    pub token_budget: u32,
36    pub token_estimate: u32,
37    pub truncation_reason: Option<String>,
38    pub canonical_audit_json: String,
39}
40
41pub(crate) fn load_context_audit_snapshot(
42    conn: &Connection,
43    injection_run_id: &str,
44) -> Result<Option<RememContextAuditSnapshot>> {
45    let Some(persisted) = crate::context_bundle::persistence::load_verified_context_bundle_audit(
46        conn,
47        injection_run_id,
48    )?
49    else {
50        return Ok(None);
51    };
52    let snapshot = snapshot_from_persisted(persisted);
53    verify_context_audit_snapshot(&snapshot)?;
54    Ok(Some(snapshot))
55}
56
57pub fn verify_context_audit_snapshot(snapshot: &RememContextAuditSnapshot) -> Result<()> {
58    if snapshot.injection_run_id.trim().is_empty() {
59        bail!("coding-bench ContextAudit injection_run_id must not be blank");
60    }
61    let (audit, actual_hash) =
62        crate::context_bundle::persistence::decode_verified_context_audit_json(
63            &snapshot.canonical_audit_json,
64            snapshot.plan_schema_version,
65        )?;
66    if actual_hash != snapshot.audit_hash {
67        bail!(
68            "coding-bench ContextAudit hash mismatch for injection_run_id={}: stored={} actual={actual_hash}",
69            snapshot.injection_run_id,
70            snapshot.audit_hash
71        );
72    }
73    let actual_binding =
74        context_audit_binding_hash(&snapshot.injection_run_id, &snapshot.audit_hash);
75    if actual_binding != snapshot.injection_binding_hash {
76        bail!(
77            "coding-bench ContextAudit injection binding mismatch for injection_run_id={}",
78            snapshot.injection_run_id
79        );
80    }
81    verify_summary(snapshot, &audit)
82}
83
84pub(crate) fn context_audit_binding_hash(injection_run_id: &str, audit_hash: &str) -> String {
85    let mut hasher = Sha256::new();
86    hasher.update(b"remem-coding-bench-context-audit-binding-v1\0");
87    hasher.update((injection_run_id.len() as u64).to_be_bytes());
88    hasher.update(injection_run_id.as_bytes());
89    hasher.update((audit_hash.len() as u64).to_be_bytes());
90    hasher.update(audit_hash.as_bytes());
91    format!("{:x}", hasher.finalize())
92}
93
94pub(crate) fn verify_snapshot_against_persisted_injection(
95    conn: &Connection,
96    snapshot: &RememContextAuditSnapshot,
97    injected_context: &str,
98) -> Result<()> {
99    let actual =
100        load_context_audit_snapshot(conn, &snapshot.injection_run_id)?.ok_or_else(|| {
101            anyhow::anyhow!(
102                "coding-bench ContextAudit missing for injection_run_id={}",
103                snapshot.injection_run_id
104            )
105        })?;
106    if actual != *snapshot {
107        bail!(
108            "coding-bench ContextAudit snapshot differs from persisted injection_run_id={}",
109            snapshot.injection_run_id
110        );
111    }
112    let context_hashes = {
113        let mut statement = conn.prepare(
114            "SELECT DISTINCT context_hash
115             FROM context_injection_items
116             WHERE injection_run_id = ?1",
117        )?;
118        let hashes = statement
119            .query_map([&snapshot.injection_run_id], |row| {
120                row.get::<_, Option<String>>(0)
121            })?
122            .collect::<rusqlite::Result<Vec<_>>>()?;
123        hashes
124    };
125    if context_hashes.len() != 1 {
126        bail!(
127            "coding-bench ContextAudit injection_run_id={} must link exactly one emitted context hash",
128            snapshot.injection_run_id
129        );
130    }
131    let persisted_context_hash = context_hashes[0].as_deref().ok_or_else(|| {
132        anyhow::anyhow!(
133            "coding-bench ContextAudit injection_run_id={} has no emitted context hash",
134            snapshot.injection_run_id
135        )
136    })?;
137    let actual_context_hash = crate::context::context_output_fingerprint(injected_context);
138    if persisted_context_hash != actual_context_hash {
139        bail!(
140            "coding-bench injected context differs from persisted injection_run_id={}",
141            snapshot.injection_run_id
142        );
143    }
144    verify_emitted_token_estimate(snapshot, injected_context)?;
145    verify_persisted_item_mapping(conn, snapshot)?;
146    Ok(())
147}
148
149fn verify_emitted_token_estimate(
150    snapshot: &RememContextAuditSnapshot,
151    injected_context: &str,
152) -> Result<()> {
153    let chars = u32::try_from(injected_context.chars().count()).map_err(|_| {
154        anyhow::anyhow!("coding-bench injected context character count exceeds u32")
155    })?;
156    let emitted = chars.div_ceil(4);
157    if snapshot.token_estimate != emitted {
158        bail!(
159            "coding-bench ContextAudit token estimate mismatch for injection_run_id={}: snapshot={} emitted={emitted}",
160            snapshot.injection_run_id,
161            snapshot.token_estimate
162        );
163    }
164    Ok(())
165}
166
167#[derive(Debug, Clone)]
168struct PersistedInjectionAuditItem {
169    item_kind: String,
170    item_id: Option<i64>,
171    memory_id: Option<i64>,
172    channel: String,
173    score: Option<f64>,
174    status: String,
175    drop_reason: Option<String>,
176}
177
178fn verify_persisted_item_mapping(
179    conn: &Connection,
180    snapshot: &RememContextAuditSnapshot,
181) -> Result<()> {
182    let (audit, _) = crate::context_bundle::persistence::decode_verified_context_audit_json(
183        &snapshot.canonical_audit_json,
184        snapshot.plan_schema_version,
185    )?;
186    let persisted_items = {
187        let mut statement = conn.prepare(
188            "SELECT item_kind, item_id, memory_id, channel, score, status, drop_reason
189             FROM context_injection_items
190             WHERE injection_run_id = ?1
191             ORDER BY id",
192        )?;
193        let items = statement
194            .query_map([&snapshot.injection_run_id], |row| {
195                Ok(PersistedInjectionAuditItem {
196                    item_kind: row.get(0)?,
197                    item_id: row.get(1)?,
198                    memory_id: row.get(2)?,
199                    channel: row.get(3)?,
200                    score: row.get(4)?,
201                    status: row.get(5)?,
202                    drop_reason: row.get(6)?,
203                })
204            })?
205            .collect::<rusqlite::Result<Vec<_>>>()?;
206        items
207    };
208    verify_persisted_items(snapshot, &audit, persisted_items)
209}
210
211fn verify_persisted_items(
212    snapshot: &RememContextAuditSnapshot,
213    audit: &ContextAudit,
214    persisted_items: Vec<PersistedInjectionAuditItem>,
215) -> Result<()> {
216    let mut keyed_items = BTreeMap::new();
217    let mut relevance_policy_count = 0_u32;
218    let mut abstention_count = 0_u32;
219    for item in persisted_items {
220        if item.item_kind == "sessionstart_relevance_policy" {
221            verify_relevance_policy_item(&item)?;
222            relevance_policy_count += 1;
223            continue;
224        }
225        if item.item_kind == "memory" && item.item_id.is_none() && item.memory_id.is_none() {
226            verify_abstention_item(&item)?;
227            abstention_count += 1;
228            continue;
229        }
230        let Some(stable_key) = persisted_item_stable_key(&item)? else {
231            bail!("linked ContextAudit item is missing its stable identity");
232        };
233        if keyed_items.insert(stable_key.clone(), item).is_some() {
234            bail!(
235                "coding-bench ContextAudit injection_run_id={} has duplicate linked item {stable_key}",
236                snapshot.injection_run_id
237            );
238        }
239    }
240    if relevance_policy_count != 1 {
241        bail!(
242            "coding-bench ContextAudit injection_run_id={} must have exactly one linked relevance policy row",
243            snapshot.injection_run_id
244        );
245    }
246    if abstention_count > 1 {
247        bail!(
248            "coding-bench ContextAudit injection_run_id={} has duplicate linked abstention rows",
249            snapshot.injection_run_id
250        );
251    }
252    if keyed_items.len() != audit.entries.len() {
253        bail!(
254            "coding-bench ContextAudit injection_run_id={} linked item set does not match canonical audit",
255            snapshot.injection_run_id
256        );
257    }
258    for entry in &audit.entries {
259        let Some(item) = keyed_items.remove(&entry.stable_key) else {
260            bail!(
261                "coding-bench ContextAudit injection_run_id={} is missing linked item {}",
262                snapshot.injection_run_id,
263                entry.stable_key
264            );
265        };
266        let expected_status = if entry.selected {
267            "injected"
268        } else {
269            "dropped"
270        };
271        let expected_drop_reason = if entry.selected {
272            let expected_selected_reason = if entry.relevance_score.is_some() {
273                "relevance_selected"
274            } else {
275                "channel_default_selected"
276            };
277            if entry.reason != expected_selected_reason {
278                bail!(
279                    "coding-bench ContextAudit injection_run_id={} selected item {} has non-canonical reason {}",
280                    snapshot.injection_run_id,
281                    entry.stable_key,
282                    entry.reason
283                );
284            }
285            None
286        } else {
287            Some(entry.reason.as_str())
288        };
289        if item.channel != channel_name(entry.channel)
290            || item.score != entry.relevance_score
291            || item.status != expected_status
292            || item.drop_reason.as_deref() != expected_drop_reason
293        {
294            bail!(
295                "coding-bench ContextAudit injection_run_id={} linked item {} differs from canonical audit: persisted={item:?} canonical={entry:?}",
296                snapshot.injection_run_id,
297                entry.stable_key
298            );
299        }
300    }
301    if !keyed_items.is_empty() {
302        bail!(
303            "coding-bench ContextAudit injection_run_id={} has extra linked items",
304            snapshot.injection_run_id
305        );
306    }
307    Ok(())
308}
309
310fn persisted_item_stable_key(item: &PersistedInjectionAuditItem) -> Result<Option<String>> {
311    match item.item_kind.as_str() {
312        "memory" => match (item.item_id, item.memory_id) {
313            (Some(item_id), Some(memory_id)) if item_id == memory_id => {
314                Ok(Some(format!("memory:{memory_id}")))
315            }
316            _ => bail!("linked memory item has non-canonical identity columns"),
317        },
318        "session_summary" => match (item.item_id, item.memory_id) {
319            (Some(id), None) => Ok(Some(format!("session_summary:{id}"))),
320            _ => bail!("linked session summary has non-canonical identity columns"),
321        },
322        "workstream" => match (item.item_id, item.memory_id) {
323            (Some(id), None) => Ok(Some(format!("workstream:{id}"))),
324            _ => bail!("linked workstream has non-canonical identity columns"),
325        },
326        other => bail!("linked ContextAudit item has unknown item_kind={other}"),
327    }
328}
329
330fn verify_relevance_policy_item(item: &PersistedInjectionAuditItem) -> Result<()> {
331    if item.item_id.is_some()
332        || item.memory_id.is_some()
333        || item.channel != "policy"
334        || item.score.is_some_and(|score| !score.is_finite())
335        || item.status != "injected"
336        || item.drop_reason.is_some()
337    {
338        bail!("linked ContextAudit relevance policy row has non-canonical fields");
339    }
340    Ok(())
341}
342
343fn verify_abstention_item(item: &PersistedInjectionAuditItem) -> Result<()> {
344    if item.channel != "memory"
345        || item.score.is_some()
346        || item.status != "abstained"
347        || item.drop_reason.as_deref() != Some("no_relevant_context")
348    {
349        bail!("linked ContextAudit abstention row has non-canonical fields");
350    }
351    Ok(())
352}
353
354const fn channel_name(channel: ChannelKind) -> &'static str {
355    match channel {
356        ChannelKind::Preferences => "preferences",
357        ChannelKind::Lessons => "lessons",
358        ChannelKind::Core => "core",
359        ChannelKind::Workstreams => "workstreams",
360        ChannelKind::MemoryIndex => "index",
361        ChannelKind::Sessions => "sessions",
362    }
363}
364
365pub(crate) fn validate_run_context_audit(run: &RunReport) -> Result<()> {
366    match run.condition {
367        BenchCondition::RememSeededSessionStart => match run.context_audit_status {
368            RememContextAuditStatus::Verified => {
369                if run.context_audit_failure_reason.is_some()
370                    || run.runtime_contract_failure
371                    || run.runtime_contract_failure_reason.is_some()
372                {
373                    bail!(
374                        "remem coding-bench run {}#{} has verified ContextAudit with contract-failure state",
375                        run.task_id,
376                        run.run_index
377                    );
378                }
379                let snapshot = run.remem_context_audit.as_ref().ok_or_else(|| {
380                    anyhow::anyhow!(
381                        "remem coding-bench run {}#{} is missing ContextAudit snapshot",
382                        run.task_id,
383                        run.run_index
384                    )
385                })?;
386                verify_context_audit_snapshot(snapshot)?;
387            }
388            RememContextAuditStatus::ContractFailure => {
389                if run
390                    .context_audit_failure_reason
391                    .as_deref()
392                    .is_none_or(|reason| reason.trim().is_empty())
393                    || !run.runtime_contract_failure
394                    || run
395                        .runtime_contract_failure_reason
396                        .as_deref()
397                        .is_none_or(|reason| reason.trim().is_empty())
398                    || run.remem_context_audit.is_some()
399                {
400                    bail!(
401                        "remem coding-bench run {}#{} has inconsistent ContextAudit contract-failure state",
402                        run.task_id,
403                        run.run_index
404                    );
405                }
406            }
407            RememContextAuditStatus::NotApplicable => bail!(
408                "remem coding-bench run {}#{} must not mark ContextAudit not_applicable",
409                run.task_id,
410                run.run_index
411            ),
412        },
413        BenchCondition::NoMemory | BenchCondition::CuratedFileExpert => {
414            if run.context_audit_status != RememContextAuditStatus::NotApplicable
415                || run.context_audit_failure_reason.is_some()
416                || run.remem_context_audit.is_some()
417                || run.runtime_contract_failure
418                || run.runtime_contract_failure_reason.is_some()
419            {
420                bail!(
421                    "{} coding-bench run {}#{} must mark ContextAudit not_applicable",
422                    run.condition.as_str(),
423                    run.task_id,
424                    run.run_index
425                );
426            }
427        }
428        other => bail!(
429            "{} coding-bench run {}#{} is invalid: live execution is not implemented for this condition",
430            other.as_str(),
431            run.task_id,
432            run.run_index
433        ),
434    }
435    Ok(())
436}
437
438fn snapshot_from_persisted(
439    persisted: crate::context_bundle::persistence::PersistedContextBundleAudit,
440) -> RememContextAuditSnapshot {
441    let audit = persisted.audit;
442    let injection_binding_hash =
443        context_audit_binding_hash(&persisted.injection_run_id, &persisted.audit_hash);
444    RememContextAuditSnapshot {
445        injection_run_id: persisted.injection_run_id,
446        bundle_schema_version: persisted.bundle_schema_version,
447        plan_schema_version: persisted.plan_schema_version,
448        policy_version: audit.policy_version,
449        relevance_policy_version: audit.relevance_policy_version,
450        plan_hash: audit.plan_hash,
451        audit_hash: persisted.audit_hash,
452        injection_binding_hash,
453        degraded_mode: audit.degraded_mode,
454        candidates_considered: audit.candidates_considered,
455        selected_count: audit.selected_count,
456        dropped_count: audit.dropped_count,
457        token_budget: audit.token_budget,
458        token_estimate: audit.token_estimate,
459        truncation_reason: audit.truncation_reason,
460        canonical_audit_json: persisted.canonical_audit_json,
461    }
462}
463
464fn verify_summary(snapshot: &RememContextAuditSnapshot, audit: &ContextAudit) -> Result<()> {
465    validate_supported_bundle_schema(snapshot.bundle_schema_version)?;
466    validate_supported_bundle_schema(audit.schema_version)?;
467    let candidates_considered = u32::try_from(audit.entries.len())
468        .map_err(|_| anyhow::anyhow!("coding-bench ContextAudit entry count exceeds u32"))?;
469    let selected_count = u32::try_from(audit.entries.iter().filter(|entry| entry.selected).count())
470        .map_err(|_| anyhow::anyhow!("coding-bench ContextAudit selected count exceeds u32"))?;
471    let dropped_count = candidates_considered - selected_count;
472    if audit.candidates_considered != candidates_considered
473        || audit.selected_count != selected_count
474        || audit.dropped_count != dropped_count
475    {
476        bail!(
477            "coding-bench ContextAudit entry counts do not match summary for injection_run_id={}",
478            snapshot.injection_run_id
479        );
480    }
481    let matches = snapshot.bundle_schema_version == audit.schema_version
482        && snapshot.policy_version == audit.policy_version
483        && snapshot.relevance_policy_version == audit.relevance_policy_version
484        && snapshot.plan_hash == audit.plan_hash
485        && snapshot.degraded_mode == audit.degraded_mode
486        && snapshot.candidates_considered == audit.candidates_considered
487        && snapshot.selected_count == audit.selected_count
488        && snapshot.dropped_count == audit.dropped_count
489        && snapshot.token_budget == audit.token_budget
490        && snapshot.token_estimate == audit.token_estimate
491        && snapshot.truncation_reason == audit.truncation_reason;
492    if !matches {
493        bail!(
494            "coding-bench ContextAudit summary mismatch for injection_run_id={}",
495            snapshot.injection_run_id
496        );
497    }
498    Ok(())
499}
500
501fn validate_supported_bundle_schema(version: u32) -> Result<()> {
502    match version {
503        SUPPORTED_CONTEXT_BUNDLE_SCHEMA_V1 => Ok(()),
504        unsupported => {
505            bail!("unsupported coding-bench ContextAudit bundle schema version {unsupported}")
506        }
507    }
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513    use crate::context_bundle::{
514        AuditEntry, ChannelKind, ContextAudit, ItemValidity, SourceKind,
515        CONTEXT_BUNDLE_SCHEMA_VERSION,
516    };
517
518    fn audit_entry(stable_key: &str, selected: bool) -> AuditEntry {
519        AuditEntry {
520            stable_key: stable_key.to_string(),
521            channel: ChannelKind::Core,
522            source_kind: SourceKind::Canonical,
523            validity: ItemValidity::Current,
524            selected,
525            reason: if selected {
526                "relevance_selected".to_string()
527            } else {
528                "section_budget".to_string()
529            },
530            relevance_score: Some(0.75),
531            token_estimate: if selected { 3 } else { 5 },
532        }
533    }
534
535    #[rustfmt::skip]
536    fn snapshot() -> Result<RememContextAuditSnapshot> {
537        let audit = ContextAudit {
538            schema_version: CONTEXT_BUNDLE_SCHEMA_VERSION,
539            policy_version: "retrieval_router_v2".to_string(),
540            relevance_policy_version: "sessionstart_significant_token_v1".to_string(),
541            plan_hash: "a".repeat(64),
542            degraded_mode: DegradedMode::Full,
543            candidates_considered: 2,
544            selected_count: 1,
545            dropped_count: 1,
546            token_estimate: 7,
547            token_budget: 100,
548            truncation_reason: Some("section_budget".to_string()),
549            entries: vec![audit_entry("memory:1", true), audit_entry("memory:2", false)],
550            shadow_comparison: Vec::new(),
551        };
552        let (canonical_audit_json, audit_hash) =
553            crate::context_bundle::persistence::canonical_context_audit(
554                &audit,
555                crate::context_bundle::persistence::PERSISTED_PLAN_SCHEMA_V1,
556            )?;
557        let injection_run_id = "run-1".to_string();
558        let injection_binding_hash = context_audit_binding_hash(&injection_run_id, &audit_hash);
559        Ok(RememContextAuditSnapshot {
560            injection_run_id,
561            bundle_schema_version: CONTEXT_BUNDLE_SCHEMA_VERSION,
562            plan_schema_version: crate::context_bundle::persistence::PERSISTED_PLAN_SCHEMA_V1,
563            policy_version: audit.policy_version,
564            relevance_policy_version: audit.relevance_policy_version,
565            plan_hash: audit.plan_hash,
566            audit_hash,
567            injection_binding_hash,
568            degraded_mode: audit.degraded_mode,
569            candidates_considered: audit.candidates_considered,
570            selected_count: audit.selected_count,
571            dropped_count: audit.dropped_count,
572            token_budget: audit.token_budget,
573            token_estimate: audit.token_estimate,
574            truncation_reason: audit.truncation_reason,
575            canonical_audit_json,
576        })
577    }
578
579    fn persisted_item(
580        id: i64,
581        status: &str,
582        drop_reason: Option<&str>,
583    ) -> PersistedInjectionAuditItem {
584        PersistedInjectionAuditItem {
585            item_kind: "memory".to_string(),
586            item_id: Some(id),
587            memory_id: Some(id),
588            channel: "core".to_string(),
589            score: Some(0.75),
590            status: status.to_string(),
591            drop_reason: drop_reason.map(str::to_string),
592        }
593    }
594
595    fn persisted_items() -> Vec<PersistedInjectionAuditItem> {
596        vec![
597            PersistedInjectionAuditItem {
598                item_kind: "sessionstart_relevance_policy".to_string(),
599                item_id: None,
600                memory_id: None,
601                channel: "policy".to_string(),
602                score: Some(0.5),
603                status: "injected".to_string(),
604                drop_reason: None,
605            },
606            persisted_item(1, "injected", None),
607            persisted_item(2, "dropped", Some("section_budget")),
608        ]
609    }
610
611    fn abstention_item() -> PersistedInjectionAuditItem {
612        PersistedInjectionAuditItem {
613            item_kind: "memory".to_string(),
614            item_id: None,
615            memory_id: None,
616            channel: "memory".to_string(),
617            score: None,
618            status: "abstained".to_string(),
619            drop_reason: Some("no_relevant_context".to_string()),
620        }
621    }
622
623    #[test]
624    fn persisted_item_mapping_rejects_every_provenance_mutation() -> Result<()> {
625        let snapshot = snapshot()?;
626        let (audit, _) = crate::context_bundle::persistence::decode_verified_context_audit_json(
627            &snapshot.canonical_audit_json,
628            snapshot.plan_schema_version,
629        )?;
630        verify_persisted_items(&snapshot, &audit, persisted_items())?;
631        let mut with_abstention = persisted_items();
632        with_abstention.push(abstention_item());
633        verify_persisted_items(&snapshot, &audit, with_abstention)?;
634
635        let mut mutations = Vec::new();
636        let mut missing = persisted_items();
637        missing.pop();
638        mutations.push(missing);
639        let mut extra = persisted_items();
640        extra.push(persisted_item(3, "dropped", Some("section_budget")));
641        mutations.push(extra);
642        let mut duplicate = persisted_items();
643        duplicate.push(duplicate[1].clone());
644        mutations.push(duplicate);
645        let mut identity = persisted_items();
646        identity[1].item_id = Some(9);
647        mutations.push(identity);
648        let mut score = persisted_items();
649        score[1].score = Some(0.5);
650        mutations.push(score);
651        let mut status = persisted_items();
652        status[1].status = "dropped".to_string();
653        mutations.push(status);
654        let mut reason = persisted_items();
655        reason[2].drop_reason = Some("tampered".to_string());
656        mutations.push(reason);
657        let mut duplicate_policy = persisted_items();
658        duplicate_policy.push(duplicate_policy[0].clone());
659        mutations.push(duplicate_policy);
660        let mut malformed_policy = persisted_items();
661        malformed_policy[0].status = "dropped".to_string();
662        mutations.push(malformed_policy);
663        let mut malformed_abstention = persisted_items();
664        let mut abstention = abstention_item();
665        abstention.drop_reason = Some("tampered".to_string());
666        malformed_abstention.push(abstention);
667        mutations.push(malformed_abstention);
668        let mut duplicate_abstention = persisted_items();
669        duplicate_abstention.extend([abstention_item(), abstention_item()]);
670        mutations.push(duplicate_abstention);
671        for item_kind in ["session_summary", "workstream"] {
672            let mut unexpected_identity = persisted_items();
673            let mut item = persisted_item(3, "injected", None);
674            item.item_kind = item_kind.to_string();
675            unexpected_identity.push(item);
676            mutations.push(unexpected_identity);
677        }
678        let mut unknown = persisted_items();
679        unknown.push(PersistedInjectionAuditItem {
680            item_kind: "unknown".to_string(),
681            item_id: Some(3),
682            memory_id: None,
683            channel: "core".to_string(),
684            score: None,
685            status: "injected".to_string(),
686            drop_reason: None,
687        });
688        mutations.push(unknown);
689
690        for mutated in mutations {
691            assert!(verify_persisted_items(&snapshot, &audit, mutated).is_err());
692        }
693
694        let mut selected_reason = audit.clone();
695        selected_reason.entries[0].reason = "tampered".to_string();
696        assert!(verify_persisted_items(&snapshot, &selected_reason, persisted_items()).is_err());
697        Ok(())
698    }
699
700    #[test]
701    fn verifier_recomputes_hash_and_summary() -> Result<()> {
702        let original = snapshot()?;
703        verify_context_audit_snapshot(&original)?;
704
705        let mut wrong_hash = original.clone();
706        wrong_hash.audit_hash = "b".repeat(64);
707        assert!(verify_context_audit_snapshot(&wrong_hash)
708            .unwrap_err()
709            .to_string()
710            .contains("hash mismatch"));
711
712        let mut wrong_run_id = snapshot()?;
713        wrong_run_id.injection_run_id = "run-2".to_string();
714        assert!(verify_context_audit_snapshot(&wrong_run_id)
715            .unwrap_err()
716            .to_string()
717            .contains("injection binding mismatch"));
718
719        let mut blank_run_id = snapshot()?;
720        blank_run_id.injection_run_id = "  ".to_string();
721        assert!(verify_context_audit_snapshot(&blank_run_id)
722            .unwrap_err()
723            .to_string()
724            .contains("must not be blank"));
725
726        let mut wrong_summary = original;
727        wrong_summary.selected_count = 2;
728        assert!(verify_context_audit_snapshot(&wrong_summary)
729            .unwrap_err()
730            .to_string()
731            .contains("summary mismatch"));
732
733        let mut unsupported_version = snapshot()?;
734        unsupported_version.plan_schema_version = 2;
735        assert!(verify_context_audit_snapshot(&unsupported_version)
736            .unwrap_err()
737            .to_string()
738            .contains("unsupported persisted retrieval plan schema version 2"));
739
740        let mut unsupported_bundle = snapshot()?;
741        let mut future_audit: ContextAudit =
742            serde_json::from_str(&unsupported_bundle.canonical_audit_json)?;
743        future_audit.schema_version = 2;
744        let (canonical_audit_json, audit_hash) =
745            crate::context_bundle::persistence::canonical_context_audit(
746                &future_audit,
747                unsupported_bundle.plan_schema_version,
748            )?;
749        unsupported_bundle.bundle_schema_version = 2;
750        unsupported_bundle.canonical_audit_json = canonical_audit_json;
751        unsupported_bundle.audit_hash = audit_hash;
752        unsupported_bundle.injection_binding_hash = context_audit_binding_hash(
753            &unsupported_bundle.injection_run_id,
754            &unsupported_bundle.audit_hash,
755        );
756        assert!(verify_context_audit_snapshot(&unsupported_bundle)
757            .unwrap_err()
758            .to_string()
759            .contains("unsupported coding-bench ContextAudit bundle schema version 2"));
760
761        let mut inconsistent_entries = snapshot()?;
762        let mut inconsistent_audit: ContextAudit =
763            serde_json::from_str(&inconsistent_entries.canonical_audit_json)?;
764        inconsistent_audit.entries.clear();
765        let (canonical_audit_json, audit_hash) =
766            crate::context_bundle::persistence::canonical_context_audit(
767                &inconsistent_audit,
768                inconsistent_entries.plan_schema_version,
769            )?;
770        inconsistent_entries.canonical_audit_json = canonical_audit_json;
771        inconsistent_entries.audit_hash = audit_hash;
772        inconsistent_entries.injection_binding_hash = context_audit_binding_hash(
773            &inconsistent_entries.injection_run_id,
774            &inconsistent_entries.audit_hash,
775        );
776        assert!(verify_context_audit_snapshot(&inconsistent_entries)
777            .unwrap_err()
778            .to_string()
779            .contains("entry counts do not match summary"));
780        Ok(())
781    }
782
783    #[test]
784    fn emitted_context_token_estimate_is_hash_bound_aggregate() -> Result<()> {
785        let mut snapshot = snapshot()?;
786        let injected_context = "记忆系统有效";
787        snapshot.token_estimate = 2;
788        assert_ne!(
789            audit_entry("memory:1", true).token_estimate,
790            snapshot.token_estimate
791        );
792        verify_emitted_token_estimate(&snapshot, injected_context)?;
793
794        snapshot.token_estimate = 3;
795        let error = verify_emitted_token_estimate(&snapshot, injected_context).unwrap_err();
796        assert!(error.to_string().contains("token estimate mismatch"));
797        Ok(())
798    }
799}