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}