Skip to main content

aidens_testkit/
lib.rs

1//! Independent reference interpreters for AiDENs semantic conformance tests.
2//!
3//! This crate intentionally depends only on contracts and general JSON support.
4//! Production crates may use it from tests, but the interpreters here do not
5//! call production policy, provider, boundary, tool, permit, or receipt code.
6
7use aidens_contracts::{
8    CanonicalToolSideEffectClass, DifferentialConformanceFindingV1, GoldenFixtureManifestV1,
9    MemoryModeV1, ReferenceCaseV1, ReferenceDomainV1, ReferenceInterpreterReportV1, ReportLevelV1,
10    ToolLifecycleStateV1,
11};
12use chrono::{DateTime, Utc};
13use serde_json::{json, Value};
14use std::collections::BTreeSet;
15use thiserror::Error;
16
17pub const REFERENCE_PROVIDER_KINDS: &[&str] = &[
18    "anthropic",
19    "compatible",
20    "disabled",
21    "mock",
22    "ollama",
23    "openai",
24    "openai-compatible",
25    "openrouter",
26];
27
28#[derive(Debug, Error)]
29pub enum ReferenceError {
30    #[error("reference case {case_id} is missing required field {field}")]
31    MissingField { case_id: String, field: String },
32    #[error("reference case {case_id} has invalid field {field}: {reason}")]
33    InvalidField {
34        case_id: String,
35        field: String,
36        reason: String,
37    },
38    #[error("reference case {case_id} uses unsupported domain {domain}")]
39    UnsupportedDomain { case_id: String, domain: String },
40}
41
42pub fn all_provider_kinds() -> Vec<String> {
43    REFERENCE_PROVIDER_KINDS
44        .iter()
45        .map(|kind| (*kind).to_string())
46        .collect()
47}
48
49pub fn all_risk_classes() -> Vec<CanonicalToolSideEffectClass> {
50    vec![
51        CanonicalToolSideEffectClass::ReadOnly,
52        CanonicalToolSideEffectClass::Analysis,
53        CanonicalToolSideEffectClass::PreviewWrite,
54        CanonicalToolSideEffectClass::Write,
55        CanonicalToolSideEffectClass::Admin,
56    ]
57}
58
59pub fn all_memory_modes() -> Vec<MemoryModeV1> {
60    vec![
61        MemoryModeV1::Disabled,
62        MemoryModeV1::Optional,
63        MemoryModeV1::Required,
64    ]
65}
66
67pub fn all_receipt_levels() -> Vec<ReportLevelV1> {
68    vec![
69        ReportLevelV1::Minimal,
70        ReportLevelV1::Standard,
71        ReportLevelV1::Full,
72    ]
73}
74
75pub fn all_tool_lifecycle_states() -> Vec<ToolLifecycleStateV1> {
76    vec![
77        ToolLifecycleStateV1::Declared,
78        ToolLifecycleStateV1::Registered,
79        ToolLifecycleStateV1::Executable,
80        ToolLifecycleStateV1::Exposed,
81        ToolLifecycleStateV1::ExposedThisTurn,
82        ToolLifecycleStateV1::Invoked,
83        ToolLifecycleStateV1::Succeeded,
84        ToolLifecycleStateV1::Failed,
85        ToolLifecycleStateV1::Hidden,
86        ToolLifecycleStateV1::Blocked,
87    ]
88}
89
90pub fn interpret_case(case: &ReferenceCaseV1) -> Result<Value, ReferenceError> {
91    match case.domain {
92        ReferenceDomainV1::PlanConfig => reference_plan_config_value(case),
93        ReferenceDomainV1::ProviderRoute => reference_provider_route_value(case),
94        ReferenceDomainV1::ToolExposure => reference_tool_exposure_value(case),
95        ReferenceDomainV1::Permit => reference_permit_value(case),
96        ReferenceDomainV1::BoundaryRepair => reference_boundary_repair_value(case),
97        ReferenceDomainV1::ReceiptLineage => reference_receipt_lineage_value(case),
98        ReferenceDomainV1::TemporalQuery => reference_temporal_query_value(case),
99        ReferenceDomainV1::ProofDebt => reference_proof_debt_value(case),
100        ReferenceDomainV1::SemanticState => reference_semantic_state_value(case),
101        ReferenceDomainV1::ViewDisclosure => reference_view_disclosure_value(case),
102    }
103}
104
105pub fn evaluate_reference_cases(cases: &[ReferenceCaseV1]) -> ReferenceInterpreterReportV1 {
106    let mut findings = Vec::new();
107    for case in cases {
108        match interpret_case(case) {
109            Ok(actual) if actual == case.expected => {}
110            Ok(actual) => findings.push(DifferentialConformanceFindingV1::mismatch(
111                case,
112                "aidens-testkit:reference-self-check",
113                "$",
114                case.expected.clone(),
115                actual,
116            )),
117            Err(error) => findings.push(DifferentialConformanceFindingV1::mismatch(
118                case,
119                "aidens-testkit:reference-self-check",
120                "$",
121                case.expected.clone(),
122                json!({"error": error.to_string()}),
123            )),
124        }
125    }
126    ReferenceInterpreterReportV1::new("aidens-testkit:p09", cases.len(), findings, cases)
127}
128
129pub fn compare_case_to_actual(
130    case: &ReferenceCaseV1,
131    production_subject: &str,
132    actual: Value,
133) -> ReferenceInterpreterReportV1 {
134    let expected = interpret_case(case).unwrap_or_else(|error| json!({"error": error.to_string()}));
135    let findings = if expected == actual {
136        Vec::new()
137    } else {
138        vec![DifferentialConformanceFindingV1::mismatch(
139            case,
140            production_subject,
141            "$",
142            expected,
143            actual,
144        )]
145    };
146    ReferenceInterpreterReportV1::new(production_subject, 1, findings, std::slice::from_ref(case))
147}
148
149pub fn reference_cases() -> Vec<ReferenceCaseV1> {
150    let mut cases = Vec::new();
151    for provider_kind in REFERENCE_PROVIDER_KINDS {
152        cases.push(reference_provider_case(provider_kind));
153    }
154    for risk in all_risk_classes() {
155        cases.push(reference_permit_case(risk));
156    }
157    for memory_mode in all_memory_modes() {
158        for receipt_level in all_receipt_levels() {
159            cases.push(reference_plan_config_case(
160                memory_mode.clone(),
161                receipt_level.clone(),
162            ));
163        }
164    }
165    cases.push(reference_safe_coding_exposure_case());
166    cases.push(reference_tool_lifecycle_catalog_case());
167    cases.push(reference_boundary_strict_case());
168    cases.push(reference_boundary_markdown_case());
169    cases.push(reference_boundary_substring_case());
170    cases.push(reference_receipt_lineage_case());
171    cases.push(reference_temporal_query_case());
172    cases.extend(reference_bitemporal_fixture_cases());
173    cases.push(reference_proof_waiver_debt_case());
174    cases.push(reference_semantic_state_contamination_case());
175    cases.push(reference_view_widening_disclosure_case());
176    cases
177}
178
179pub fn golden_fixture_manifest() -> GoldenFixtureManifestV1 {
180    let cases = reference_cases();
181    GoldenFixtureManifestV1::new(
182        vec![
183            "tests/fixtures/reference/reference_case_v1.json".into(),
184            "tests/fixtures/reference/reference_interpreter_report_v1.json".into(),
185            "tests/fixtures/reference/differential_conformance_finding_v1.json".into(),
186            "tests/fixtures/reference/golden_fixture_manifest_v1.json".into(),
187        ],
188        &cases,
189    )
190}
191
192pub fn reference_provider_case(provider_kind: &str) -> ReferenceCaseV1 {
193    let input = match provider_kind {
194        "mock" => json!({"kind": provider_kind, "mock_response_configured": true}),
195        "ollama" => json!({"kind": provider_kind, "model_configured": true}),
196        "openai" | "openrouter" | "anthropic" | "openai-compatible" | "compatible" => {
197            json!({"kind": provider_kind, "api_key_configured": true, "model_configured": true})
198        }
199        _ => json!({"kind": provider_kind}),
200    };
201    let mut case = ReferenceCaseV1::new(
202        ReferenceDomainV1::ProviderRoute,
203        format!("provider route semantics for {provider_kind}"),
204        input,
205        json!({}),
206    )
207    .with_provider_kind(provider_kind)
208    .with_source_fixture("tests/fixtures/reference/reference_case_v1.json");
209    case.expected = reference_provider_route_value(&case).expect("valid provider reference case");
210    case
211}
212
213pub fn reference_permit_case(risk_class: CanonicalToolSideEffectClass) -> ReferenceCaseV1 {
214    let mut case = ReferenceCaseV1::new(
215        ReferenceDomainV1::Permit,
216        format!("default permit semantics for {risk_class}"),
217        json!({
218            "risk_class": json_string(&risk_class),
219            "matching_permit": false
220        }),
221        json!({}),
222    )
223    .with_risk_class(risk_class)
224    .with_source_fixture("tests/fixtures/reference/golden_fixture_manifest_v1.json");
225    case.expected = reference_permit_value(&case).expect("valid permit reference case");
226    case
227}
228
229pub fn reference_plan_config_case(
230    memory_mode: MemoryModeV1,
231    receipt_level: ReportLevelV1,
232) -> ReferenceCaseV1 {
233    let memory_store_configured = false;
234    let mut case = ReferenceCaseV1::new(
235        ReferenceDomainV1::PlanConfig,
236        format!("plan config semantics for {memory_mode}/{receipt_level}"),
237        json!({
238            "memory_mode": json_string(&memory_mode),
239            "receipt_level": json_string(&receipt_level),
240            "memory_store_configured": memory_store_configured
241        }),
242        json!({}),
243    )
244    .with_memory_mode(memory_mode)
245    .with_receipt_level(receipt_level)
246    .with_source_fixture("tests/fixtures/reference/golden_fixture_manifest_v1.json");
247    case.expected = reference_plan_config_value(&case).expect("valid plan reference case");
248    case
249}
250
251pub fn reference_safe_coding_exposure_case() -> ReferenceCaseV1 {
252    let mut case = ReferenceCaseV1::new(
253        ReferenceDomainV1::ToolExposure,
254        "safe coding exposure semantics",
255        json!({
256            "max_tools": 16,
257            "native_tool_loop_available": false,
258            "tools": [
259                reference_tool(ReferenceToolSpec::new("aidens:file-write:1", "write")),
260                reference_tool(ReferenceToolSpec::new("aidens:file-stat:1", "read_only")
261                    .registered()
262                    .executable()
263                    .allowed_risk()),
264                reference_tool(ReferenceToolSpec::new("aidens:memory-write:1", "write")),
265                reference_tool(ReferenceToolSpec::new("aidens:network:1", "analysis")),
266                reference_tool(ReferenceToolSpec::new("aidens:patch-apply:1", "write")
267                    .registered()
268                    .executable()),
269                reference_tool(ReferenceToolSpec::new("aidens:patch-propose:1", "read_only")
270                    .registered()
271                    .executable()
272                    .allowed_risk()),
273                reference_tool(ReferenceToolSpec::new("aidens:repo-list:1", "read_only")
274                    .registered()
275                    .executable()
276                    .allowed_risk()),
277                reference_tool(ReferenceToolSpec::new("aidens:repo-read:1", "read_only")
278                    .registered()
279                    .executable()
280                    .allowed_risk()),
281                reference_tool(ReferenceToolSpec::new("aidens:repo-search:1", "read_only")
282                    .registered()
283                    .executable()
284                    .allowed_risk()),
285                reference_tool(ReferenceToolSpec::new("aidens:run-checks:1", "admin")
286                    .registered()
287                    .executable()),
288                reference_tool(ReferenceToolSpec::new("aidens:schedule:1", "admin")),
289                reference_tool(ReferenceToolSpec::new("aidens:shell:1", "admin"))
290            ]
291        }),
292        json!({}),
293    )
294    .with_risk_class(CanonicalToolSideEffectClass::ReadOnly)
295    .with_risk_class(CanonicalToolSideEffectClass::Write)
296    .with_tool_lifecycle_state(ToolLifecycleStateV1::Declared)
297    .with_tool_lifecycle_state(ToolLifecycleStateV1::Registered)
298    .with_tool_lifecycle_state(ToolLifecycleStateV1::Executable)
299    .with_tool_lifecycle_state(ToolLifecycleStateV1::ExposedThisTurn)
300    .with_tool_lifecycle_state(ToolLifecycleStateV1::Hidden)
301    .with_tool_lifecycle_state(ToolLifecycleStateV1::Blocked)
302    .with_source_fixture("tests/fixtures/reference/golden_fixture_manifest_v1.json");
303    case.expected = reference_tool_exposure_value(&case).expect("valid exposure reference case");
304    case
305}
306
307pub fn reference_boundary_markdown_case() -> ReferenceCaseV1 {
308    let mut case = ReferenceCaseV1::new(
309        ReferenceDomainV1::BoundaryRepair,
310        "markdown fence repair semantics",
311        json!({
312            "raw": "```json\n{\"ok\":true}\n```",
313            "allow_markdown_fence_repair": true,
314            "allow_json_substring_extract": true
315        }),
316        json!({}),
317    )
318    .with_source_fixture("tests/fixtures/reference/golden_fixture_manifest_v1.json");
319    case.expected = reference_boundary_repair_value(&case).expect("valid boundary reference case");
320    case
321}
322
323pub fn reference_temporal_query_case() -> ReferenceCaseV1 {
324    let mut case = ReferenceCaseV1::new(
325        ReferenceDomainV1::TemporalQuery,
326        "temporal as-of reference query semantics",
327        json!({
328            "valid_at": "2026-04-01T00:00:00Z",
329            "recorded_at_or_before": "9999-01-01T00:00:00Z",
330            "records": [
331                {
332                    "claim_id": "claim-phase09-temporal",
333                    "claim_version_id": "claim-version-phase09-temporal-old",
334                    "valid_from": "2026-01-01T00:00:00Z",
335                    "valid_to": "2026-03-01T00:00:00Z",
336                    "recorded_at": "2026-04-28T12:00:00Z",
337                    "content": "phase nine temporal old value"
338                },
339                {
340                    "claim_id": "claim-phase09-temporal",
341                    "claim_version_id": "claim-version-phase09-temporal-new",
342                    "valid_from": "2026-03-01T00:00:00Z",
343                    "valid_to": null,
344                    "recorded_at": "2026-04-28T12:00:00Z",
345                    "content": "phase nine temporal new value"
346                }
347            ]
348        }),
349        json!({}),
350    )
351    .with_source_fixture("tests/fixtures/reference/golden_fixture_manifest_v1.json");
352    case.expected = reference_temporal_query_value(&case).expect("valid temporal reference case");
353    case
354}
355
356pub fn reference_bitemporal_fixture_cases() -> Vec<ReferenceCaseV1> {
357    vec![
358        reference_temporal_valid_time_only_case(),
359        reference_temporal_recorded_time_only_case(),
360        reference_temporal_retroactive_correction_case(),
361        reference_temporal_stale_projection_case(),
362        reference_temporal_degraded_disclosure_case(),
363    ]
364}
365
366pub fn reference_temporal_valid_time_only_case() -> ReferenceCaseV1 {
367    reference_temporal_fixture_case(
368        "valid-time-only temporal reference query semantics",
369        json!({
370            "valid_at": "2026-02-01T00:00:00Z",
371            "records": [
372                {
373                    "claim_id": "claim-phase09-valid-only",
374                    "claim_version_id": "claim-version-phase09-valid-old",
375                    "valid_from": "2026-01-01T00:00:00Z",
376                    "valid_to": "2026-03-01T00:00:00Z",
377                    "recorded_at": "2026-01-15T00:00:00Z",
378                    "content": "valid-only old value"
379                },
380                {
381                    "claim_id": "claim-phase09-valid-only",
382                    "claim_version_id": "claim-version-phase09-valid-new",
383                    "valid_from": "2026-03-01T00:00:00Z",
384                    "valid_to": null,
385                    "recorded_at": "2026-01-15T00:00:00Z",
386                    "content": "valid-only new value"
387                }
388            ]
389        }),
390    )
391}
392
393pub fn reference_temporal_recorded_time_only_case() -> ReferenceCaseV1 {
394    reference_temporal_fixture_case(
395        "recorded-time-only temporal reference query semantics",
396        json!({
397            "recorded_at_or_before": "2026-02-15T00:00:00Z",
398            "records": [
399                {
400                    "claim_id": "claim-phase09-recorded-only",
401                    "claim_version_id": "claim-version-phase09-recorded-early",
402                    "valid_from": "2026-01-01T00:00:00Z",
403                    "valid_to": null,
404                    "recorded_at": "2026-02-01T00:00:00Z",
405                    "content": "recorded-only early value"
406                },
407                {
408                    "claim_id": "claim-phase09-recorded-only",
409                    "claim_version_id": "claim-version-phase09-recorded-late",
410                    "valid_from": "2026-01-01T00:00:00Z",
411                    "valid_to": null,
412                    "recorded_at": "2026-03-01T00:00:00Z",
413                    "content": "recorded-only late value"
414                }
415            ]
416        }),
417    )
418}
419
420pub fn reference_temporal_retroactive_correction_case() -> ReferenceCaseV1 {
421    reference_temporal_fixture_case(
422        "retroactive correction and supersession temporal reference semantics",
423        json!({
424            "valid_at": "2026-04-01T00:00:00Z",
425            "recorded_at_or_before": "2026-05-01T00:00:00Z",
426            "records": [
427                {
428                    "claim_id": "claim-phase09-retro",
429                    "claim_version_id": "claim-version-phase09-retro-original",
430                    "valid_from": "2026-01-01T00:00:00Z",
431                    "valid_to": null,
432                    "recorded_at": "2026-02-01T00:00:00Z",
433                    "content": "retroactive original value"
434                },
435                {
436                    "claim_id": "claim-phase09-retro",
437                    "claim_version_id": "claim-version-phase09-retro-correction",
438                    "valid_from": "2026-01-01T00:00:00Z",
439                    "valid_to": null,
440                    "recorded_at": "2026-04-15T00:00:00Z",
441                    "supersedes_claim_version_ids": ["claim-version-phase09-retro-original"],
442                    "content": "retroactive corrected value"
443                }
444            ]
445        }),
446    )
447}
448
449pub fn reference_temporal_stale_projection_case() -> ReferenceCaseV1 {
450    reference_temporal_fixture_case(
451        "stale projection temporal disclosure semantics",
452        json!({
453            "valid_at": "2026-04-01T00:00:00Z",
454            "recorded_at_or_before": "2026-05-01T00:00:00Z",
455            "projection_recorded_at": "2026-03-01T00:00:00Z",
456            "records": [
457                {
458                    "claim_id": "claim-phase09-stale",
459                    "claim_version_id": "claim-version-phase09-stale-current",
460                    "valid_from": "2026-01-01T00:00:00Z",
461                    "valid_to": null,
462                    "recorded_at": "2026-04-15T00:00:00Z",
463                    "content": "stale projection current value"
464                }
465            ]
466        }),
467    )
468}
469
470pub fn reference_temporal_degraded_disclosure_case() -> ReferenceCaseV1 {
471    reference_temporal_fixture_case(
472        "degraded temporal disclosure semantics",
473        json!({
474            "valid_at": "2026-04-01T00:00:00Z",
475            "recorded_at_or_before": "2026-05-01T00:00:00Z",
476            "temporal_index_exact": false,
477            "records": [
478                {
479                    "claim_id": "claim-phase09-degraded",
480                    "claim_version_id": "claim-version-phase09-degraded-candidate",
481                    "valid_from": "2026-01-01T00:00:00Z",
482                    "valid_to": null,
483                    "recorded_at": "2026-04-15T00:00:00Z",
484                    "content": "degraded temporal candidate"
485                }
486            ]
487        }),
488    )
489}
490
491fn reference_temporal_fixture_case(title: &str, input: Value) -> ReferenceCaseV1 {
492    let mut case = ReferenceCaseV1::new(ReferenceDomainV1::TemporalQuery, title, input, json!({}))
493        .with_source_fixture("tests/fixtures/reference/golden_fixture_manifest_v1.json");
494    case.expected = reference_temporal_query_value(&case).expect("valid temporal reference case");
495    case
496}
497
498fn reference_boundary_strict_case() -> ReferenceCaseV1 {
499    let mut case = ReferenceCaseV1::new(
500        ReferenceDomainV1::BoundaryRepair,
501        "strict json boundary semantics",
502        json!({"raw": "{\"ok\":true}", "allow_markdown_fence_repair": true, "allow_json_substring_extract": true}),
503        json!({}),
504    )
505    .with_source_fixture("tests/fixtures/reference/golden_fixture_manifest_v1.json");
506    case.expected = reference_boundary_repair_value(&case).expect("valid boundary reference case");
507    case
508}
509
510fn reference_boundary_substring_case() -> ReferenceCaseV1 {
511    let mut case = ReferenceCaseV1::new(
512        ReferenceDomainV1::BoundaryRepair,
513        "substring repair semantics",
514        json!({"raw": "prefix {\"ok\":true} suffix", "allow_markdown_fence_repair": true, "allow_json_substring_extract": true}),
515        json!({}),
516    )
517    .with_source_fixture("tests/fixtures/reference/golden_fixture_manifest_v1.json");
518    case.expected = reference_boundary_repair_value(&case).expect("valid boundary reference case");
519    case
520}
521
522fn reference_receipt_lineage_case() -> ReferenceCaseV1 {
523    let mut case = ReferenceCaseV1::new(
524        ReferenceDomainV1::ReceiptLineage,
525        "receipt parent-child lineage semantics",
526        json!({
527            "receipts": [
528                {"receipt_id":"run:fixture","kind":"run","parent_receipt_ids":[]},
529                {"receipt_id":"tool:fixture","kind":"tool-invocation","parent_receipt_ids":["run:fixture"]},
530                {"receipt_id":"schema:fixture","kind":"schema-validation","parent_receipt_ids":["tool:fixture"]}
531            ]
532        }),
533        json!({}),
534    )
535    .with_source_fixture("tests/fixtures/reference/golden_fixture_manifest_v1.json");
536    case.expected = reference_receipt_lineage_value(&case).expect("valid lineage reference case");
537    case
538}
539
540pub fn reference_proof_waiver_debt_case() -> ReferenceCaseV1 {
541    let mut case = ReferenceCaseV1::new(
542        ReferenceDomainV1::ProofDebt,
543        "proof waiver remains debt with expiry escalation semantics",
544        json!({
545            "now": "2026-05-07T00:00:00Z",
546            "requested_use": "local-advisory-display",
547            "obligations": [
548                {
549                    "obligation_id": "proof-obligation:phase09-waiver",
550                    "satisfied_by": [],
551                    "waived_by": ["proof-waiver:phase09"],
552                    "expires_at": "2026-05-01T00:00:00Z"
553                },
554                {
555                    "obligation_id": "proof-obligation:phase09-missing",
556                    "satisfied_by": [],
557                    "waived_by": []
558                }
559            ]
560        }),
561        json!({}),
562    )
563    .with_source_fixture("tests/fixtures/reference/golden_fixture_manifest_v1.json");
564    case.expected = reference_proof_debt_value(&case).expect("valid proof debt reference case");
565    case
566}
567
568pub fn reference_semantic_state_contamination_case() -> ReferenceCaseV1 {
569    let mut case = ReferenceCaseV1::new(
570        ReferenceDomainV1::SemanticState,
571        "semantic state makes degradation contradiction and execution contamination visible",
572        json!({
573            "exactness": "exact",
574            "degradation_record_ids": ["degradation:phase09"],
575            "view_disclosure_ids": ["view-disclosure:phase09"],
576            "contradiction_record_ids": ["semantic-contradiction:phase09"],
577            "execution_contamination_ids": ["execution-contamination:phase09"]
578        }),
579        json!({}),
580    )
581    .with_source_fixture("tests/fixtures/reference/golden_fixture_manifest_v1.json");
582    case.expected =
583        reference_semantic_state_value(&case).expect("valid semantic state reference case");
584    case
585}
586
587pub fn reference_view_widening_disclosure_case() -> ReferenceCaseV1 {
588    let mut case = ReferenceCaseV1::new(
589        ReferenceDomainV1::ViewDisclosure,
590        "view widening and degradation are user visible before results",
591        json!({
592            "query_widening_receipt_ids": ["query-widening:phase09"],
593            "degradation_event_ids": ["degradation-event:phase09"],
594            "separates_execution_from_domain_truth": true
595        }),
596        json!({}),
597    )
598    .with_source_fixture("tests/fixtures/reference/golden_fixture_manifest_v1.json");
599    case.expected =
600        reference_view_disclosure_value(&case).expect("valid view disclosure reference case");
601    case
602}
603
604fn reference_tool_lifecycle_catalog_case() -> ReferenceCaseV1 {
605    let states = all_tool_lifecycle_states();
606    let mut case = ReferenceCaseV1::new(
607        ReferenceDomainV1::ToolExposure,
608        "tool lifecycle state catalog semantics",
609        json!({"catalog_lifecycle_states": states.iter().map(json_string).collect::<Vec<_>>()}),
610        json!({"known_tool_lifecycle_states": states.iter().map(json_string).collect::<Vec<_>>()}),
611    )
612    .with_source_fixture("tests/fixtures/reference/golden_fixture_manifest_v1.json");
613    for state in states {
614        case = case.with_tool_lifecycle_state(state);
615    }
616    case
617}
618
619fn reference_provider_route_value(case: &ReferenceCaseV1) -> Result<Value, ReferenceError> {
620    let kind = string_field(case, "kind")?.trim().to_ascii_lowercase();
621    let api_key_configured = bool_field(case, "api_key_configured", false);
622    let model_configured = bool_field(case, "model_configured", false);
623    let mock_response_configured = bool_field(case, "mock_response_configured", false);
624    let (configured, executable, route_label, reason_codes) = match kind.as_str() {
625        "" | "disabled" | "none" => (false, false, "disabled", vec!["provider-disabled"]),
626        "mock" => {
627            if mock_response_configured {
628                (true, true, "mock", vec!["mock-response-configured"])
629            } else {
630                (true, false, "unavailable", vec!["mock-response-missing"])
631            }
632        }
633        "ollama" => {
634            if model_configured {
635                (
636                    true,
637                    true,
638                    "ollama-chat",
639                    vec![
640                        "ollama-chat-boundary-configured",
641                        "ollama-local-service-required",
642                        "ollama-native-tool-loop-via-function-calling",
643                    ],
644                )
645            } else {
646                (false, false, "unavailable", vec!["ollama-model-missing"])
647            }
648        }
649        "openai" | "openrouter" | "anthropic" | "openai-compatible" | "compatible" => {
650            if api_key_configured {
651                (
652                    true,
653                    false,
654                    "unavailable",
655                    vec!["provider-boundary-unavailable"],
656                )
657            } else {
658                (true, false, "unavailable", vec!["api-key-missing"])
659            }
660        }
661        _ => (
662            true,
663            false,
664            "unavailable",
665            vec!["unsupported-provider-kind"],
666        ),
667    };
668    let native_tool_loop = kind == "ollama" && model_configured;
669    Ok(json!({
670        "configured": configured,
671        "executable": executable,
672        "route_label": route_label,
673        "native_tool_loop": native_tool_loop,
674        "reason_codes": reason_codes
675    }))
676}
677
678fn reference_plan_config_value(case: &ReferenceCaseV1) -> Result<Value, ReferenceError> {
679    let memory_mode = string_field(case, "memory_mode")?;
680    let receipt_level = string_field(case, "receipt_level")?;
681    let memory_store_configured = bool_field(case, "memory_store_configured", false);
682    let memory_blocked = memory_mode == "required" && !memory_store_configured;
683    let durable_receipts_required = receipt_level != "minimal";
684    let mut reason_codes = vec![
685        format!("memory-mode-{memory_mode}"),
686        format!("receipt-level-{receipt_level}"),
687    ];
688    if memory_blocked {
689        reason_codes.push("memory-required-without-durable-store".into());
690    }
691    Ok(json!({
692        "memory_mode": memory_mode,
693        "receipt_level": receipt_level,
694        "memory_store_configured": memory_store_configured,
695        "durable_receipts_required": durable_receipts_required,
696        "memory_blocked": memory_blocked,
697        "reason_codes": sorted_strings(reason_codes)
698    }))
699}
700
701fn reference_permit_value(case: &ReferenceCaseV1) -> Result<Value, ReferenceError> {
702    let risk_class = string_field(case, "risk_class")?;
703    let matching_permit = bool_field(case, "matching_permit", false);
704    let permit_required = risk_class != "read_only";
705    let (decision, reason_codes) = if !permit_required {
706        ("allow", vec!["read-only-risk"])
707    } else if matching_permit {
708        ("allow", vec!["permit-scope-matched"])
709    } else {
710        ("requires-approval", vec!["approval-required"])
711    };
712    Ok(json!({
713        "risk_class": risk_class,
714        "permit_required": permit_required,
715        "decision": decision,
716        "reason_codes": reason_codes
717    }))
718}
719
720fn reference_tool_exposure_value(case: &ReferenceCaseV1) -> Result<Value, ReferenceError> {
721    if let Some(catalog) = case.input.get("catalog_lifecycle_states") {
722        return Ok(json!({"known_tool_lifecycle_states": catalog.clone()}));
723    }
724
725    let max_tools = case
726        .input
727        .get("max_tools")
728        .and_then(Value::as_u64)
729        .unwrap_or(u64::MAX) as usize;
730    let native_tool_loop_available = bool_field(case, "native_tool_loop_available", false);
731    let tools = case
732        .input
733        .get("tools")
734        .and_then(Value::as_array)
735        .ok_or_else(|| ReferenceError::MissingField {
736            case_id: case.case_id.0.clone(),
737            field: "tools".into(),
738        })?;
739
740    let mut declared = Vec::new();
741    let mut registered = Vec::new();
742    let mut executable = Vec::new();
743    let mut exposed = Vec::new();
744    let mut hidden = Vec::new();
745    let mut blocked = Vec::new();
746    let mut lifecycles = serde_json::Map::new();
747    let mut reason_codes = Vec::new();
748
749    for tool in tools {
750        let tool_id = tool_field(tool, "tool_id")?;
751        let risk_class = tool_field(tool, "risk_class")?;
752        let is_declared = tool_bool(tool, "declared", true);
753        let is_registered = tool_bool(tool, "registered", false);
754        let is_executable = tool_bool(tool, "executable", false);
755        let is_hidden = tool_bool(tool, "hidden", false);
756        let allowed_risk = tool_bool(tool, "allowed_risk", false);
757        let matching_permit = tool_bool(tool, "matching_permit", false);
758        let requires_native_tool_loop = tool_bool(tool, "requires_native_tool_loop", false);
759        let mut lifecycle = Vec::new();
760        if is_declared {
761            declared.push(tool_id.clone());
762            lifecycle.push("declared");
763        }
764        if is_registered {
765            registered.push(tool_id.clone());
766            lifecycle.push("registered");
767        }
768        if is_executable {
769            executable.push(tool_id.clone());
770            lifecycle.push("executable");
771        }
772
773        let outcome = if !is_registered {
774            reason_codes.push(format!("tool-declared-not-registered:{tool_id}"));
775            "hidden"
776        } else if is_hidden {
777            reason_codes.push(format!("tool-hidden:{tool_id}"));
778            "hidden"
779        } else if !allowed_risk {
780            reason_codes.push(format!("risk-blocked:{risk_class}"));
781            "blocked"
782        } else if risk_class != "read_only" && !matching_permit {
783            reason_codes.push(format!("permit-required:{risk_class}"));
784            "blocked"
785        } else if requires_native_tool_loop && !native_tool_loop_available {
786            reason_codes.push(format!("route-requires-native-tool-loop:{tool_id}"));
787            "blocked"
788        } else if !is_executable {
789            reason_codes.push(format!("tool-executor-missing:{tool_id}"));
790            "hidden"
791        } else if exposed.len() >= max_tools {
792            reason_codes.push("max-tools-reached".into());
793            "hidden"
794        } else {
795            "exposed"
796        };
797
798        match outcome {
799            "exposed" => {
800                exposed.push(tool_id.clone());
801                lifecycle.push("exposed-this-turn");
802            }
803            "blocked" => {
804                blocked.push(tool_id.clone());
805                lifecycle.push("blocked");
806            }
807            _ => {
808                hidden.push(tool_id.clone());
809                lifecycle.push("hidden");
810            }
811        }
812        lifecycles.insert(
813            tool_id,
814            Value::Array(
815                lifecycle
816                    .into_iter()
817                    .map(|state| Value::String(state.into()))
818                    .collect(),
819            ),
820        );
821    }
822
823    Ok(json!({
824        "declared_tool_ids": sorted_strings(declared),
825        "registered_tool_ids": sorted_strings(registered),
826        "executable_tool_ids": sorted_strings(executable),
827        "exposed_tool_ids": sorted_strings(exposed),
828        "hidden_tool_ids": sorted_strings(hidden),
829        "blocked_tool_ids": sorted_strings(blocked),
830        "lifecycles": Value::Object(lifecycles),
831        "reason_codes": sorted_unique_strings(reason_codes)
832    }))
833}
834
835fn reference_boundary_repair_value(case: &ReferenceCaseV1) -> Result<Value, ReferenceError> {
836    let raw = string_field(case, "raw")?;
837    let allow_markdown = bool_field(case, "allow_markdown_fence_repair", false);
838    let allow_substring = bool_field(case, "allow_json_substring_extract", false);
839    if serde_json::from_str::<Value>(&raw).is_ok() {
840        return Ok(json!({
841            "accepted": true,
842            "degraded": false,
843            "repair_kind": null,
844            "reason_codes": ["boundary-compile-accepted"]
845        }));
846    }
847    if allow_markdown {
848        if let Some(candidate) = strip_markdown_json_fence(&raw) {
849            if serde_json::from_str::<Value>(&candidate).is_ok() {
850                return Ok(repaired_boundary_value("markdown-json-fence-stripped"));
851            }
852        }
853    }
854    if allow_substring {
855        if let Some(candidate) = extract_first_json_candidate(&raw) {
856            if serde_json::from_str::<Value>(&candidate).is_ok() {
857                return Ok(repaired_boundary_value("json-substring-extracted"));
858            }
859        }
860    }
861    Ok(json!({
862        "accepted": false,
863        "degraded": true,
864        "repair_kind": null,
865        "reason_codes": ["invalid-json"]
866    }))
867}
868
869fn reference_receipt_lineage_value(case: &ReferenceCaseV1) -> Result<Value, ReferenceError> {
870    let receipts = case
871        .input
872        .get("receipts")
873        .and_then(Value::as_array)
874        .ok_or_else(|| ReferenceError::MissingField {
875            case_id: case.case_id.0.clone(),
876            field: "receipts".into(),
877        })?;
878    let mut nodes = Vec::new();
879    let mut edges = Vec::new();
880    let mut orphans = Vec::new();
881    for receipt in receipts {
882        let receipt_id = tool_field(receipt, "receipt_id")?;
883        nodes.push(receipt_id.clone());
884        let parents = receipt
885            .get("parent_receipt_ids")
886            .and_then(Value::as_array)
887            .cloned()
888            .unwrap_or_default();
889        if parents.is_empty() {
890            orphans.push(receipt_id.clone());
891        }
892        for parent in parents {
893            if let Some(parent_id) = parent.as_str() {
894                edges.push(format!("{parent_id}->{receipt_id}"));
895            }
896        }
897    }
898    Ok(json!({
899        "node_count": nodes.len(),
900        "edge_count": edges.len(),
901        "receipt_ids": sorted_strings(nodes),
902        "edges": sorted_strings(edges),
903        "orphan_receipt_ids": sorted_strings(orphans)
904    }))
905}
906
907fn reference_temporal_query_value(case: &ReferenceCaseV1) -> Result<Value, ReferenceError> {
908    let valid_at = optional_datetime_field(case, "valid_at")?;
909    let recorded_at_or_before = optional_datetime_field(case, "recorded_at_or_before")?;
910    let records = case
911        .input
912        .get("records")
913        .and_then(Value::as_array)
914        .ok_or_else(|| ReferenceError::MissingField {
915            case_id: case.case_id.0.clone(),
916            field: "records".into(),
917        })?;
918    let temporal_index_exact = case
919        .input
920        .get("temporal_index_exact")
921        .and_then(Value::as_bool)
922        .unwrap_or(true);
923    let projection_recorded_at = optional_datetime_field(case, "projection_recorded_at")?;
924    let mut candidate_claim_version_ids = BTreeSet::new();
925    let mut superseded_claim_version_ids = BTreeSet::new();
926    let mut max_candidate_recorded_at: Option<DateTime<Utc>> = None;
927    for record in records {
928        let claim_version_id = value_string_field(record, "claim_version_id", case)?;
929        let valid_from = value_datetime_field(record, "valid_from", case)?;
930        let valid_to = optional_value_datetime_field(record, "valid_to", case)?;
931        let recorded_at = value_datetime_field(record, "recorded_at", case)?;
932        let valid_at_visible = valid_at
933            .map(|valid_at| {
934                valid_from <= valid_at
935                    && valid_to.map(|valid_to| valid_at < valid_to).unwrap_or(true)
936            })
937            .unwrap_or(true);
938        let recorded_visible = recorded_at_or_before
939            .map(|recorded_at_or_before| recorded_at <= recorded_at_or_before)
940            .unwrap_or(true);
941        if valid_at_visible && recorded_visible {
942            if max_candidate_recorded_at
943                .map(|current| recorded_at > current)
944                .unwrap_or(true)
945            {
946                max_candidate_recorded_at = Some(recorded_at);
947            }
948            for superseded_id in record
949                .get("supersedes_claim_version_ids")
950                .and_then(Value::as_array)
951                .into_iter()
952                .flatten()
953                .filter_map(Value::as_str)
954            {
955                superseded_claim_version_ids.insert(superseded_id.to_string());
956            }
957            candidate_claim_version_ids.insert(claim_version_id);
958        }
959    }
960    let mut visible_claim_version_ids = Vec::new();
961    let mut hidden_claim_version_ids = Vec::new();
962    for record in records {
963        let claim_version_id = value_string_field(record, "claim_version_id", case)?;
964        if candidate_claim_version_ids.contains(&claim_version_id)
965            && !superseded_claim_version_ids.contains(&claim_version_id)
966        {
967            visible_claim_version_ids.push(claim_version_id);
968        } else {
969            hidden_claim_version_ids.push(claim_version_id);
970        }
971    }
972
973    let stale_projection = projection_recorded_at
974        .zip(max_candidate_recorded_at)
975        .map(|(projection_recorded_at, max_recorded_at)| projection_recorded_at < max_recorded_at)
976        .unwrap_or(false);
977    let degraded = !temporal_index_exact || stale_projection;
978    let mut reason_codes = vec!["temporal-query-reference".to_string()];
979    if valid_at.is_some() {
980        reason_codes.push("valid-time-filter-applied".into());
981    }
982    if recorded_at_or_before.is_some() {
983        reason_codes.push("recorded-time-filter-applied".into());
984    }
985    if !superseded_claim_version_ids.is_empty() {
986        reason_codes.push("supersession-applied".into());
987    }
988    if stale_projection {
989        reason_codes.push("stale-projection-disclosed".into());
990    }
991    if !temporal_index_exact {
992        reason_codes.push("temporal-index-degraded-disclosed".into());
993    }
994    if !degraded {
995        reason_codes.push("temporal-query-exact-reference".into());
996    }
997    reason_codes.sort();
998    reason_codes.dedup();
999
1000    Ok(json!({
1001        "accepted": true,
1002        "deferred": false,
1003        "temporal_mode": if degraded { "degraded" } else { "exact" },
1004        "valid_as_of": valid_at.map(|valid_at| valid_at.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)),
1005        "recorded_as_of": recorded_at_or_before.map(|recorded_at_or_before| recorded_at_or_before.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)),
1006        "visible_claim_version_ids": sorted_strings(visible_claim_version_ids),
1007        "hidden_claim_version_ids": sorted_strings(hidden_claim_version_ids),
1008        "stale_projection": stale_projection,
1009        "view_disclosure_required": degraded,
1010        "degradation_record_required": degraded,
1011        "reason_codes": reason_codes
1012    }))
1013}
1014
1015fn reference_proof_debt_value(case: &ReferenceCaseV1) -> Result<Value, ReferenceError> {
1016    let now = value_datetime_field(&case.input, "now", case)?;
1017    let requested_use = case
1018        .input
1019        .get("requested_use")
1020        .and_then(Value::as_str)
1021        .unwrap_or("local-advisory-display");
1022    let obligations = case
1023        .input
1024        .get("obligations")
1025        .and_then(Value::as_array)
1026        .ok_or_else(|| ReferenceError::MissingField {
1027            case_id: case.case_id.0.clone(),
1028            field: "obligations".into(),
1029        })?;
1030    let mut debt_ids = Vec::new();
1031    let mut restrictions = Vec::new();
1032    let mut expired_debt_ids = Vec::new();
1033    let mut escalated_debt_ids = Vec::new();
1034    let mut allowed_use_count = 0usize;
1035    let mut waiver_without_proof = false;
1036    for obligation in obligations {
1037        let obligation_id = value_string_field(obligation, "obligation_id", case)?;
1038        let satisfied = obligation
1039            .get("satisfied_by")
1040            .and_then(Value::as_array)
1041            .map(|items| !items.is_empty())
1042            .unwrap_or(false);
1043        if satisfied {
1044            continue;
1045        }
1046        let waived = obligation
1047            .get("waived_by")
1048            .and_then(Value::as_array)
1049            .map(|items| !items.is_empty())
1050            .unwrap_or(false);
1051        let debt_id = format!("proof-debt:{obligation_id}");
1052        let expires_at = optional_value_datetime_field(obligation, "expires_at", case)?;
1053        let expired = expires_at
1054            .map(|expires_at| now >= expires_at)
1055            .unwrap_or(false);
1056        if waived {
1057            waiver_without_proof = true;
1058        }
1059        if expired {
1060            expired_debt_ids.push(debt_id.clone());
1061            escalated_debt_ids.push(debt_id.clone());
1062            restrictions.push("block-release".to_string());
1063        } else if waived {
1064            restrictions.push("advisory-only".to_string());
1065            if requested_use == "local-advisory-display" || requested_use == "operator-triage" {
1066                allowed_use_count += 1;
1067            }
1068        } else {
1069            restrictions.push("block-release".to_string());
1070        }
1071        debt_ids.push(debt_id);
1072    }
1073    let blocks_promotion = !debt_ids.is_empty();
1074    Ok(json!({
1075        "debt_ids": sorted_strings(debt_ids),
1076        "restriction_kinds": sorted_strings(restrictions),
1077        "waiver_is_not_proof": true,
1078        "waiver_without_proof": waiver_without_proof,
1079        "blocks_promotion": blocks_promotion,
1080        "requested_use_allowed": allowed_use_count > 0 && escalated_debt_ids.is_empty(),
1081        "expired_debt_ids": sorted_strings(expired_debt_ids),
1082        "escalated_debt_ids": sorted_strings(escalated_debt_ids),
1083        "reason_codes": [
1084            "proof-debt-queryable",
1085            "proof-waiver-does-not-satisfy-obligation",
1086            "expired-proof-debt-escalates"
1087        ]
1088    }))
1089}
1090
1091fn reference_semantic_state_value(case: &ReferenceCaseV1) -> Result<Value, ReferenceError> {
1092    let exactness = case
1093        .input
1094        .get("exactness")
1095        .and_then(Value::as_str)
1096        .ok_or_else(|| ReferenceError::MissingField {
1097            case_id: case.case_id.0.clone(),
1098            field: "exactness".into(),
1099        })?;
1100    let degradation_ids = optional_string_array(&case.input, "degradation_record_ids");
1101    let view_ids = optional_string_array(&case.input, "view_disclosure_ids");
1102    let contradiction_ids = optional_string_array(&case.input, "contradiction_record_ids");
1103    let contamination_ids = optional_string_array(&case.input, "execution_contamination_ids");
1104    let visible_degradation = !degradation_ids.is_empty();
1105    let visible_contradiction = !contradiction_ids.is_empty();
1106    let visible_contamination = !contamination_ids.is_empty();
1107    let can_answer_as_exact = exactness == "exact"
1108        && !visible_degradation
1109        && !visible_contradiction
1110        && !visible_contamination;
1111    Ok(json!({
1112        "can_answer_as_exact": can_answer_as_exact,
1113        "blocks_promotion": !can_answer_as_exact,
1114        "degradation_visible": visible_degradation,
1115        "view_disclosure_visible": !view_ids.is_empty(),
1116        "contradiction_visible": visible_contradiction,
1117        "execution_contamination_visible": visible_contamination,
1118        "effective_exactness": if visible_contradiction { "refuted" } else if can_answer_as_exact { "exact" } else { "degraded" },
1119        "reason_codes": [
1120            "semantic-state-queryable",
1121            "contradiction-blocks-promotion",
1122            "execution-contamination-disclosed"
1123        ]
1124    }))
1125}
1126
1127fn reference_view_disclosure_value(case: &ReferenceCaseV1) -> Result<Value, ReferenceError> {
1128    let widening_ids = optional_string_array(&case.input, "query_widening_receipt_ids");
1129    let degradation_ids = optional_string_array(&case.input, "degradation_event_ids");
1130    let separates_execution = case
1131        .input
1132        .get("separates_execution_from_domain_truth")
1133        .and_then(Value::as_bool)
1134        .unwrap_or(false);
1135    let has_visible_event = !widening_ids.is_empty() || !degradation_ids.is_empty();
1136    Ok(json!({
1137        "has_visible_widening_or_degradation": has_visible_event,
1138        "discloses_policy_events": separates_execution
1139            && widening_ids.iter().all(|id| !id.is_empty())
1140            && degradation_ids.iter().all(|id| !id.is_empty()),
1141        "result_exactness": if has_visible_event { "degraded" } else { "exact" },
1142        "query_widening_receipt_ids": sorted_strings(widening_ids),
1143        "degradation_event_ids": sorted_strings(degradation_ids),
1144        "reason_codes": [
1145            "view-disclosure-queryable",
1146            "widening-visible-before-results"
1147        ]
1148    }))
1149}
1150
1151struct ReferenceToolSpec<'a> {
1152    tool_id: &'a str,
1153    risk_class: &'a str,
1154    declared: bool,
1155    registered: bool,
1156    executable: bool,
1157    hidden: bool,
1158    allowed_risk: bool,
1159    matching_permit: bool,
1160    requires_native_tool_loop: bool,
1161}
1162
1163impl<'a> ReferenceToolSpec<'a> {
1164    fn new(tool_id: &'a str, risk_class: &'a str) -> Self {
1165        Self {
1166            tool_id,
1167            risk_class,
1168            declared: true,
1169            registered: false,
1170            executable: false,
1171            hidden: false,
1172            allowed_risk: false,
1173            matching_permit: false,
1174            requires_native_tool_loop: false,
1175        }
1176    }
1177
1178    fn registered(mut self) -> Self {
1179        self.registered = true;
1180        self
1181    }
1182
1183    fn executable(mut self) -> Self {
1184        self.executable = true;
1185        self
1186    }
1187
1188    fn allowed_risk(mut self) -> Self {
1189        self.allowed_risk = true;
1190        self
1191    }
1192}
1193
1194fn reference_tool(spec: ReferenceToolSpec<'_>) -> Value {
1195    json!({
1196        "tool_id": spec.tool_id,
1197        "risk_class": spec.risk_class,
1198        "declared": spec.declared,
1199        "registered": spec.registered,
1200        "executable": spec.executable,
1201        "hidden": spec.hidden,
1202        "allowed_risk": spec.allowed_risk,
1203        "matching_permit": spec.matching_permit,
1204        "requires_native_tool_loop": spec.requires_native_tool_loop
1205    })
1206}
1207
1208fn repaired_boundary_value(repair_kind: &str) -> Value {
1209    let reason_codes = sorted_strings(vec![
1210        "boundary-compile-accepted".into(),
1211        format!("boundary-repair:{repair_kind}"),
1212        format!("json-repair:{repair_kind}"),
1213    ]);
1214    json!({
1215        "accepted": true,
1216        "degraded": true,
1217        "repair_kind": repair_kind,
1218        "reason_codes": reason_codes
1219    })
1220}
1221
1222fn strip_markdown_json_fence(raw: &str) -> Option<String> {
1223    let trimmed = raw.trim();
1224    if !trimmed.starts_with("```") {
1225        return None;
1226    }
1227    let mut lines = trimmed.lines();
1228    let first = lines.next()?;
1229    if !first.trim_start_matches("```").trim().is_empty()
1230        && first.trim_start_matches("```").trim() != "json"
1231    {
1232        return None;
1233    }
1234    let mut body = lines.collect::<Vec<_>>();
1235    if body.last().is_some_and(|line| line.trim() == "```") {
1236        body.pop();
1237    }
1238    Some(body.join("\n"))
1239}
1240
1241fn extract_first_json_candidate(raw: &str) -> Option<String> {
1242    let object_start = raw.find('{');
1243    let array_start = raw.find('[');
1244    let start = match (object_start, array_start) {
1245        (Some(left), Some(right)) => left.min(right),
1246        (Some(left), None) => left,
1247        (None, Some(right)) => right,
1248        (None, None) => return None,
1249    };
1250    let end = raw.rfind('}').or_else(|| raw.rfind(']'))?;
1251    (end >= start).then(|| raw[start..=end].to_string())
1252}
1253
1254fn string_field(case: &ReferenceCaseV1, field: &str) -> Result<String, ReferenceError> {
1255    case.input
1256        .get(field)
1257        .and_then(Value::as_str)
1258        .map(str::to_string)
1259        .ok_or_else(|| ReferenceError::MissingField {
1260            case_id: case.case_id.0.clone(),
1261            field: field.into(),
1262        })
1263}
1264
1265fn bool_field(case: &ReferenceCaseV1, field: &str, default: bool) -> bool {
1266    case.input
1267        .get(field)
1268        .and_then(Value::as_bool)
1269        .unwrap_or(default)
1270}
1271
1272fn optional_datetime_field(
1273    case: &ReferenceCaseV1,
1274    field: &str,
1275) -> Result<Option<DateTime<Utc>>, ReferenceError> {
1276    match case.input.get(field) {
1277        None | Some(Value::Null) => Ok(None),
1278        Some(Value::String(raw)) => parse_datetime_field(raw, field, case).map(Some),
1279        Some(_) => Err(ReferenceError::InvalidField {
1280            case_id: case.case_id.0.clone(),
1281            field: field.into(),
1282            reason: "expected string or null".into(),
1283        }),
1284    }
1285}
1286
1287fn value_string_field(
1288    value: &Value,
1289    field: &str,
1290    case: &ReferenceCaseV1,
1291) -> Result<String, ReferenceError> {
1292    value
1293        .get(field)
1294        .and_then(Value::as_str)
1295        .map(str::to_string)
1296        .ok_or_else(|| ReferenceError::InvalidField {
1297            case_id: case.case_id.0.clone(),
1298            field: field.into(),
1299            reason: "expected string".into(),
1300        })
1301}
1302
1303fn value_datetime_field(
1304    value: &Value,
1305    field: &str,
1306    case: &ReferenceCaseV1,
1307) -> Result<DateTime<Utc>, ReferenceError> {
1308    parse_datetime_field(&value_string_field(value, field, case)?, field, case)
1309}
1310
1311fn optional_value_datetime_field(
1312    value: &Value,
1313    field: &str,
1314    case: &ReferenceCaseV1,
1315) -> Result<Option<DateTime<Utc>>, ReferenceError> {
1316    match value.get(field) {
1317        None | Some(Value::Null) => Ok(None),
1318        Some(Value::String(raw)) => parse_datetime_field(raw, field, case).map(Some),
1319        Some(_) => Err(ReferenceError::InvalidField {
1320            case_id: case.case_id.0.clone(),
1321            field: field.into(),
1322            reason: "expected string or null".into(),
1323        }),
1324    }
1325}
1326
1327fn optional_string_array(value: &Value, field: &str) -> Vec<String> {
1328    value
1329        .get(field)
1330        .and_then(Value::as_array)
1331        .into_iter()
1332        .flatten()
1333        .filter_map(Value::as_str)
1334        .map(str::to_string)
1335        .collect()
1336}
1337
1338fn parse_datetime_field(
1339    raw: &str,
1340    field: &str,
1341    case: &ReferenceCaseV1,
1342) -> Result<DateTime<Utc>, ReferenceError> {
1343    DateTime::parse_from_rfc3339(raw)
1344        .map(|value| value.with_timezone(&Utc))
1345        .map_err(|error| ReferenceError::InvalidField {
1346            case_id: case.case_id.0.clone(),
1347            field: field.into(),
1348            reason: error.to_string(),
1349        })
1350}
1351
1352fn tool_field(tool: &Value, field: &str) -> Result<String, ReferenceError> {
1353    tool.get(field)
1354        .and_then(Value::as_str)
1355        .map(str::to_string)
1356        .ok_or_else(|| ReferenceError::InvalidField {
1357            case_id: "tool-exposure".into(),
1358            field: field.into(),
1359            reason: "expected string".into(),
1360        })
1361}
1362
1363fn tool_bool(tool: &Value, field: &str, default: bool) -> bool {
1364    tool.get(field).and_then(Value::as_bool).unwrap_or(default)
1365}
1366
1367pub fn json_string<T: serde::Serialize>(value: &T) -> String {
1368    serde_json::to_value(value)
1369        .ok()
1370        .and_then(|value| value.as_str().map(str::to_string))
1371        .unwrap_or_default()
1372}
1373
1374fn sorted_strings(mut values: Vec<String>) -> Vec<String> {
1375    values.sort();
1376    values
1377}
1378
1379fn sorted_unique_strings(values: Vec<String>) -> Vec<String> {
1380    let mut set = BTreeSet::new();
1381    for value in values {
1382        set.insert(value);
1383    }
1384    set.into_iter().collect()
1385}
1386
1387#[cfg(test)]
1388mod tests {
1389    use super::*;
1390
1391    #[test]
1392    fn reference_cases_cover_required_catalogs() {
1393        let cases = reference_cases();
1394        let report = evaluate_reference_cases(&cases);
1395        let manifest = golden_fixture_manifest();
1396
1397        assert!(report.passed, "{:?}", report.findings);
1398        assert_eq!(manifest.provider_kinds, all_provider_kinds());
1399        assert_eq!(manifest.risk_classes, all_risk_classes());
1400        assert_eq!(manifest.memory_modes, all_memory_modes());
1401        assert_eq!(manifest.receipt_levels, all_receipt_levels());
1402        assert_eq!(manifest.tool_lifecycle_states, all_tool_lifecycle_states());
1403    }
1404
1405    #[test]
1406    fn mismatch_report_has_human_readable_diff() {
1407        let case = reference_permit_case(CanonicalToolSideEffectClass::Write);
1408        let report =
1409            compare_case_to_actual(&case, "unit-test:bad-permit", json!({"decision":"allow"}));
1410
1411        assert!(!report.passed);
1412        assert!(report.findings[0].human_diff.contains("expected"));
1413        assert!(report.findings[0].human_diff.contains("actual"));
1414        assert!(report.findings[0]
1415            .reason_codes
1416            .contains(&"reference-production-mismatch".into()));
1417    }
1418
1419    #[test]
1420    fn safe_coding_exposure_case_interprets_expected() {
1421        let case = reference_safe_coding_exposure_case();
1422        let actual = interpret_case(&case).unwrap();
1423
1424        assert_eq!(actual, case.expected);
1425        assert_eq!(
1426            actual["exposed_tool_ids"],
1427            json!([
1428                "aidens:file-stat:1",
1429                "aidens:patch-propose:1",
1430                "aidens:repo-list:1",
1431                "aidens:repo-read:1",
1432                "aidens:repo-search:1"
1433            ])
1434        );
1435        assert!(actual["blocked_tool_ids"]
1436            .as_array()
1437            .unwrap()
1438            .contains(&json!("aidens:patch-apply:1")));
1439    }
1440
1441    #[test]
1442    fn temporal_query_reference_case_interprets_as_of_semantics() {
1443        let case = reference_temporal_query_case();
1444        let actual = interpret_case(&case).unwrap();
1445
1446        assert_eq!(actual, case.expected);
1447        assert_eq!(actual["deferred"], json!(false));
1448        assert_eq!(actual["temporal_mode"], json!("exact"));
1449        assert_eq!(
1450            actual["visible_claim_version_ids"],
1451            json!(["claim-version-phase09-temporal-new"])
1452        );
1453        assert_eq!(
1454            actual["hidden_claim_version_ids"],
1455            json!(["claim-version-phase09-temporal-old"])
1456        );
1457    }
1458
1459    #[test]
1460    fn p28_bitemporal_reference_fixture_matrix_interprets_required_cases() {
1461        let cases = reference_bitemporal_fixture_cases();
1462
1463        assert_eq!(cases.len(), 5);
1464        for case in &cases {
1465            assert_eq!(interpret_case(case).unwrap(), case.expected);
1466        }
1467
1468        let valid_only = interpret_case(&cases[0]).unwrap();
1469        assert_eq!(
1470            valid_only["visible_claim_version_ids"],
1471            json!(["claim-version-phase09-valid-old"])
1472        );
1473        assert!(valid_only["recorded_as_of"].is_null());
1474
1475        let recorded_only = interpret_case(&cases[1]).unwrap();
1476        assert_eq!(
1477            recorded_only["visible_claim_version_ids"],
1478            json!(["claim-version-phase09-recorded-early"])
1479        );
1480        assert!(recorded_only["valid_as_of"].is_null());
1481
1482        let retro = interpret_case(&cases[2]).unwrap();
1483        assert_eq!(
1484            retro["visible_claim_version_ids"],
1485            json!(["claim-version-phase09-retro-correction"])
1486        );
1487        assert_eq!(
1488            retro["hidden_claim_version_ids"],
1489            json!(["claim-version-phase09-retro-original"])
1490        );
1491        assert!(retro["reason_codes"]
1492            .as_array()
1493            .unwrap()
1494            .contains(&json!("supersession-applied")));
1495
1496        let stale = interpret_case(&cases[3]).unwrap();
1497        assert_eq!(stale["temporal_mode"], json!("degraded"));
1498        assert_eq!(stale["stale_projection"], json!(true));
1499        assert_eq!(stale["view_disclosure_required"], json!(true));
1500
1501        let degraded = interpret_case(&cases[4]).unwrap();
1502        assert_eq!(degraded["temporal_mode"], json!("degraded"));
1503        assert_eq!(degraded["degradation_record_required"], json!(true));
1504        assert!(degraded["reason_codes"]
1505            .as_array()
1506            .unwrap()
1507            .contains(&json!("temporal-index-degraded-disclosed")));
1508    }
1509
1510    #[test]
1511    fn p09_proof_semantic_and_view_reference_cases_are_semantic_not_marker_checks() {
1512        let proof = interpret_case(&reference_proof_waiver_debt_case()).unwrap();
1513        assert_eq!(proof["blocks_promotion"], json!(true));
1514        assert_eq!(proof["waiver_is_not_proof"], json!(true));
1515        assert_eq!(
1516            proof["escalated_debt_ids"],
1517            json!(["proof-debt:proof-obligation:phase09-waiver"])
1518        );
1519        assert_eq!(proof["requested_use_allowed"], json!(false));
1520
1521        let semantic = interpret_case(&reference_semantic_state_contamination_case()).unwrap();
1522        assert_eq!(semantic["can_answer_as_exact"], json!(false));
1523        assert_eq!(semantic["effective_exactness"], json!("refuted"));
1524        assert_eq!(semantic["contradiction_visible"], json!(true));
1525        assert_eq!(semantic["execution_contamination_visible"], json!(true));
1526
1527        let view = interpret_case(&reference_view_widening_disclosure_case()).unwrap();
1528        assert_eq!(view["has_visible_widening_or_degradation"], json!(true));
1529        assert_eq!(view["discloses_policy_events"], json!(true));
1530        assert_eq!(view["result_exactness"], json!("degraded"));
1531    }
1532}