Skip to main content

falsegreen_agent/
genui_native.rs

1//! Read-only FalseGreen-native state surfaces.
2//!
3//! This module is deliberately conservative. A surface is allowed to show a
4//! FalseGreen value only after a typed authority view has validated the exact
5//! object that supplied it and all bindings to the other views. Missing or
6//! contradictory authority is represented as `Unknown`/`Unavailable`; it is
7//! never filled in from a neighbouring event or from Agent session state.
8
9use std::collections::{BTreeMap, BTreeSet};
10
11use serde::{Deserialize, Serialize, Serializer, ser::SerializeStruct};
12use serde_json::Value;
13use sha2::{Digest, Sha256};
14use thiserror::Error;
15
16use crate::event::{
17    Event, EventError, EventKind, EventStore, SessionReplacementRecord, history_sha256,
18};
19use crate::genui::{
20    ActionCatalog, Component, ComponentKind, EvidenceItem, GenUiError, StatusLevel, Surface,
21    TimelineEntry, TimelineState, default_negotiated_capabilities,
22    render_surface_with_capabilities_and_catalog_with_store, sanitize_text,
23};
24use crate::session::{Session, SessionError, SessionState};
25
26const MAX_DISPLAY_EVIDENCE: usize = 100;
27const MAX_EVIDENCE_ITEMS: usize = 100_000;
28const MAX_EVIDENCE_VALUE_BYTES: usize = 4_096;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum AuthorityClass {
33    AuthoritativeFalseGreenState,
34    AuthoritativeAgentSessionState,
35    DiagnosticNonAuthoritativeMetadata,
36}
37
38impl AuthorityClass {
39    #[must_use]
40    pub const fn label(self) -> &'static str {
41        match self {
42            Self::AuthoritativeFalseGreenState => "authoritative FalseGreen state",
43            Self::AuthoritativeAgentSessionState => "authoritative Agent/session state",
44            Self::DiagnosticNonAuthoritativeMetadata => "diagnostic/non-authoritative metadata",
45        }
46    }
47}
48
49/// A value carries its source class all the way to the renderer. Values
50/// emitted by the projector are created by validated view builders.
51#[derive(Debug, Clone, PartialEq, Eq)]
52#[non_exhaustive]
53pub struct NativeField {
54    value: Option<String>,
55    authority: AuthorityClass,
56}
57
58impl Serialize for NativeField {
59    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
60    where
61        S: Serializer,
62    {
63        let mut state = serializer.serialize_struct("NativeField", 2)?;
64        state.serialize_field("value", &self.value)?;
65        state.serialize_field("authority", &self.authority)?;
66        state.end()
67    }
68}
69
70impl NativeField {
71    #[must_use]
72    pub(crate) fn falsegreen(value: impl Into<String>) -> Self {
73        let value = value.into();
74        Self {
75            value: Some(safe_text(&value)),
76            authority: AuthorityClass::AuthoritativeFalseGreenState,
77        }
78    }
79
80    #[must_use]
81    pub(crate) fn agent(value: impl Into<String>) -> Self {
82        let value = value.into();
83        Self {
84            value: Some(safe_text(&value)),
85            authority: AuthorityClass::AuthoritativeAgentSessionState,
86        }
87    }
88
89    #[must_use]
90    pub(crate) fn diagnostic(value: impl Into<String>) -> Self {
91        let value = value.into();
92        Self {
93            value: Some(safe_text(&value)),
94            authority: AuthorityClass::DiagnosticNonAuthoritativeMetadata,
95        }
96    }
97
98    #[must_use]
99    pub(crate) fn unavailable(authority: AuthorityClass) -> Self {
100        Self {
101            value: None,
102            authority,
103        }
104    }
105
106    #[must_use]
107    pub fn display(&self, unknown: bool) -> String {
108        sanitize_text(self.value.as_deref().unwrap_or(if unknown {
109            "Unknown"
110        } else {
111            "Unavailable"
112        }))
113    }
114
115    #[must_use]
116    pub fn value(&self) -> Option<&str> {
117        self.value.as_deref()
118    }
119
120    #[must_use]
121    pub const fn authority(&self) -> AuthorityClass {
122        self.authority
123    }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
128pub enum NativeStatus {
129    Accepted,
130    Failed,
131    InsufficientEvidence,
132    Queued,
133    Claimed,
134    Running,
135    Executing,
136    Terminal,
137    Recoverable,
138    Unknown,
139}
140
141impl NativeStatus {
142    #[must_use]
143    pub const fn label(self) -> &'static str {
144        match self {
145            Self::Accepted => "ACCEPTED",
146            Self::Failed => "FAILED",
147            Self::InsufficientEvidence => "INSUFFICIENT EVIDENCE",
148            Self::Queued => "QUEUED",
149            Self::Claimed => "CLAIMED",
150            Self::Running => "RUNNING",
151            Self::Executing => "EXECUTING",
152            Self::Terminal => "TERMINAL",
153            Self::Recoverable => "RECOVERABLE",
154            Self::Unknown => "UNKNOWN",
155        }
156    }
157
158    #[must_use]
159    const fn level(self) -> StatusLevel {
160        match self {
161            Self::Accepted => StatusLevel::Success,
162            Self::Failed => StatusLevel::Error,
163            Self::InsufficientEvidence | Self::Recoverable | Self::Unknown => StatusLevel::Warning,
164            _ => StatusLevel::Info,
165        }
166    }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
170#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
171pub enum NativeVerdict {
172    Accepted,
173    Failed,
174    InsufficientEvidence,
175    Incomplete,
176    Invalid,
177    Unknown,
178}
179
180impl NativeVerdict {
181    #[must_use]
182    pub const fn label(self) -> &'static str {
183        match self {
184            Self::Accepted => "ACCEPTED",
185            Self::Failed => "FAILED",
186            Self::InsufficientEvidence => "INSUFFICIENT EVIDENCE",
187            Self::Incomplete => "INCOMPLETE",
188            Self::Invalid => "INVALID",
189            Self::Unknown => "UNKNOWN",
190        }
191    }
192
193    #[must_use]
194    const fn status(self) -> NativeStatus {
195        match self {
196            Self::Accepted => NativeStatus::Accepted,
197            Self::Failed => NativeStatus::Failed,
198            Self::InsufficientEvidence => NativeStatus::InsufficientEvidence,
199            Self::Incomplete | Self::Invalid | Self::Unknown => NativeStatus::Unknown,
200        }
201    }
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
205#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
206pub enum EvidenceState {
207    Passed,
208    Failed,
209    Insufficient,
210    Unknown,
211}
212
213impl EvidenceState {
214    #[must_use]
215    const fn label(self) -> &'static str {
216        match self {
217            Self::Passed => "PASS",
218            Self::Failed => "FAIL",
219            Self::Insufficient => "INSUFFICIENT",
220            Self::Unknown => "UNKNOWN",
221        }
222    }
223}
224
225#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
226pub struct NativeSnapshotIdentity {
227    pub source_session_id: String,
228    pub state_generation: u64,
229    pub digest: String,
230}
231
232#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
233pub struct NativeTask {
234    pub task_id: NativeField,
235    pub lifecycle: NativeField,
236    pub frozen: NativeField,
237    pub scope_digest: NativeField,
238    pub scope_summary: NativeField,
239    pub source_sha256: NativeField,
240    pub candidate_sha256: NativeField,
241    pub current_job_id: NativeField,
242}
243
244#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
245pub struct NativeScopeObligation {
246    pub id: NativeField,
247    pub requirement: NativeField,
248}
249
250#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
251pub struct NativeFrozenScope {
252    pub scope_digest: NativeField,
253    pub frozen: NativeField,
254    pub obligations: Vec<NativeScopeObligation>,
255    pub distinction: String,
256}
257
258#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
259pub struct NativeJob {
260    pub id: NativeField,
261    pub status: NativeStatus,
262    pub status_raw: NativeField,
263    pub task_id: NativeField,
264    pub source_sha256: NativeField,
265    pub candidate_sha256: NativeField,
266    pub entitlement_consumption: NativeField,
267    pub run_ids: Vec<NativeField>,
268    pub authority_decision: NativeField,
269}
270
271#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
272pub struct NativeRun {
273    pub id: NativeField,
274    pub worker_id: NativeField,
275    pub job_id: NativeField,
276    pub assignment_generation: NativeField,
277    pub status: NativeStatus,
278    pub status_raw: NativeField,
279    pub execution_started_at: NativeField,
280    pub package_runtime_identity: NativeField,
281    pub recovery_lease: NativeField,
282}
283
284#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
285pub struct NativeObligationOutcome {
286    pub obligation_id: NativeField,
287    pub outcome: EvidenceState,
288    pub detail: NativeField,
289    pub evidence_count: usize,
290}
291
292#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
293pub struct NativeVerification {
294    pub verdict: NativeVerdict,
295    pub raw_verdict: NativeField,
296    pub source_bound: bool,
297    pub candidate_sha256: NativeField,
298    pub authoritative_source_sha256: NativeField,
299    pub result_digest: NativeField,
300    pub report_reference: NativeField,
301    pub completion_authority: NativeField,
302    pub obligation_outcomes: Vec<NativeObligationOutcome>,
303    pub certification_note: String,
304    pub binding_error: Option<String>,
305}
306
307#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
308pub struct NativeEvidenceItem {
309    pub evidence_id: NativeField,
310    pub obligation: NativeField,
311    pub source: NativeField,
312    pub evidence_type: NativeField,
313    pub reference: NativeField,
314    pub state: EvidenceState,
315    pub detail: NativeField,
316}
317
318#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
319pub struct NativeEvidence {
320    pub items: Vec<NativeEvidenceItem>,
321    pub complete: bool,
322    pub duplicate_ids: Vec<String>,
323    pub unavailable_reason: Option<String>,
324    pub total: usize,
325    pub passed: usize,
326    pub failed: usize,
327    pub insufficient: usize,
328    pub unknown: usize,
329    pub observed_rows: usize,
330    pub invalid_rows: usize,
331    pub declared_rows: Option<usize>,
332}
333
334#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
335pub struct NativeReport {
336    pub id: NativeField,
337    pub report_digest: NativeField,
338    pub candidate_sha256: NativeField,
339    pub source_sha256: NativeField,
340    pub verdict: NativeVerdict,
341    pub key_evidence_summary: NativeField,
342    pub created_at: NativeField,
343    pub provenance: NativeField,
344}
345
346#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
347#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
348pub enum RecoveryState {
349    NotApplicable,
350    Recoverable,
351    Blocked,
352    Terminal,
353    Unknown,
354}
355
356#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
357pub struct NativeRecovery {
358    pub state: RecoveryState,
359    pub reason: NativeField,
360    pub pre_begin_claim: NativeField,
361    pub action_available: bool,
362    pub distinction: String,
363}
364
365#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
366pub struct NativeReplacement {
367    pub predecessor_session: NativeField,
368    pub replacement_session: NativeField,
369    pub predecessor_state: NativeField,
370    pub continuity: NativeField,
371    pub candidate_sha256: NativeField,
372    pub workspace_state: NativeField,
373    pub terminal_outcome: NativeField,
374}
375
376/// Typed authority inputs. They intentionally contain raw values; rendered
377/// `NativeField`s are sanitized only after these values have been bound.
378#[derive(Debug, Clone, PartialEq, Eq)]
379#[non_exhaustive]
380pub struct AuthoritativeTaskView {
381    task_id: String,
382    lifecycle: Option<String>,
383    scope_id: String,
384    scope_digest: String,
385    source_sha256: Option<String>,
386    candidate_sha256: Option<String>,
387    current_job_id: Option<String>,
388}
389
390#[derive(Debug, Clone, PartialEq, Eq)]
391#[non_exhaustive]
392pub struct AuthoritativeFrozenScopeView {
393    scope_id: String,
394    scope_digest: String,
395    obligations: Vec<(String, String)>,
396}
397
398#[derive(Debug, Clone, PartialEq, Eq)]
399#[non_exhaustive]
400pub struct AuthoritativeJobView {
401    id: String,
402    status: NativeStatus,
403    task_id: String,
404    source_sha256: Option<String>,
405    candidate_sha256: Option<String>,
406    run_ids: Vec<String>,
407    entitlement_consumption: Option<String>,
408}
409
410#[derive(Debug, Clone, PartialEq, Eq)]
411#[non_exhaustive]
412pub struct AuthoritativeRunView {
413    id: String,
414    status: NativeStatus,
415    job_id: String,
416    worker_id: Option<String>,
417    assignment_generation: Option<String>,
418    execution_started_at: Option<String>,
419    package_runtime_identity: Option<String>,
420    recovery_lease: Option<String>,
421}
422
423#[derive(Debug, Clone, PartialEq, Eq)]
424#[non_exhaustive]
425pub struct AuthoritativeRecoveryView {
426    state: RecoveryState,
427    reason: Option<String>,
428    action_available: bool,
429    task_id: String,
430    job_id: String,
431    run_id: String,
432    worker_id: String,
433    assignment_generation: String,
434    source_sha256: String,
435    candidate_sha256: Option<String>,
436    provenance: String,
437}
438
439#[derive(Debug, Clone, PartialEq, Eq)]
440#[non_exhaustive]
441pub struct AuthoritativeResultView {
442    verdict: NativeVerdict,
443    candidate_sha256: String,
444    source_sha256: String,
445    run_id: String,
446    job_id: String,
447    worker_id: Option<String>,
448    assignment_generation: Option<String>,
449    result_digest: String,
450}
451
452#[derive(Debug, Clone, PartialEq, Eq)]
453#[non_exhaustive]
454pub struct AuthoritativeEvidenceView {
455    items: Vec<NativeEvidenceItem>,
456    total: usize,
457    passed: usize,
458    failed: usize,
459    insufficient: usize,
460    unknown: usize,
461}
462
463#[derive(Debug, Clone, PartialEq, Eq)]
464#[non_exhaustive]
465pub struct AuthoritativeReportView {
466    id: String,
467    report_digest: String,
468    candidate_sha256: String,
469    source_sha256: String,
470    verdict: NativeVerdict,
471    job_id: String,
472    run_id: String,
473    result_digest: String,
474}
475
476impl AuthoritativeTaskView {
477    #[must_use]
478    pub fn task_id(&self) -> &str {
479        &self.task_id
480    }
481    #[must_use]
482    pub fn lifecycle(&self) -> Option<&str> {
483        self.lifecycle.as_deref()
484    }
485    #[must_use]
486    pub fn scope_id(&self) -> &str {
487        &self.scope_id
488    }
489    #[must_use]
490    pub fn scope_digest(&self) -> &str {
491        &self.scope_digest
492    }
493    #[must_use]
494    pub fn source_sha256(&self) -> Option<&str> {
495        self.source_sha256.as_deref()
496    }
497    #[must_use]
498    pub fn candidate_sha256(&self) -> Option<&str> {
499        self.candidate_sha256.as_deref()
500    }
501    #[must_use]
502    pub fn current_job_id(&self) -> Option<&str> {
503        self.current_job_id.as_deref()
504    }
505}
506
507impl AuthoritativeFrozenScopeView {
508    #[must_use]
509    pub fn scope_id(&self) -> &str {
510        &self.scope_id
511    }
512    #[must_use]
513    pub fn scope_digest(&self) -> &str {
514        &self.scope_digest
515    }
516    #[must_use]
517    pub fn obligations(&self) -> &[(String, String)] {
518        &self.obligations
519    }
520}
521
522impl AuthoritativeJobView {
523    #[must_use]
524    pub fn id(&self) -> &str {
525        &self.id
526    }
527    #[must_use]
528    pub const fn status(&self) -> NativeStatus {
529        self.status
530    }
531    #[must_use]
532    pub fn task_id(&self) -> &str {
533        &self.task_id
534    }
535    #[must_use]
536    pub fn source_sha256(&self) -> Option<&str> {
537        self.source_sha256.as_deref()
538    }
539    #[must_use]
540    pub fn candidate_sha256(&self) -> Option<&str> {
541        self.candidate_sha256.as_deref()
542    }
543    #[must_use]
544    pub fn run_ids(&self) -> &[String] {
545        &self.run_ids
546    }
547    #[must_use]
548    pub fn entitlement_consumption(&self) -> Option<&str> {
549        self.entitlement_consumption.as_deref()
550    }
551}
552
553impl AuthoritativeRunView {
554    #[must_use]
555    pub fn id(&self) -> &str {
556        &self.id
557    }
558    #[must_use]
559    pub const fn status(&self) -> NativeStatus {
560        self.status
561    }
562    #[must_use]
563    pub fn job_id(&self) -> &str {
564        &self.job_id
565    }
566    #[must_use]
567    pub fn worker_id(&self) -> Option<&str> {
568        self.worker_id.as_deref()
569    }
570    #[must_use]
571    pub fn assignment_generation(&self) -> Option<&str> {
572        self.assignment_generation.as_deref()
573    }
574    #[must_use]
575    pub fn execution_started_at(&self) -> Option<&str> {
576        self.execution_started_at.as_deref()
577    }
578    #[must_use]
579    pub fn package_runtime_identity(&self) -> Option<&str> {
580        self.package_runtime_identity.as_deref()
581    }
582    #[must_use]
583    pub fn recovery_lease(&self) -> Option<&str> {
584        self.recovery_lease.as_deref()
585    }
586}
587
588impl AuthoritativeRecoveryView {
589    #[must_use]
590    pub const fn state(&self) -> RecoveryState {
591        self.state
592    }
593    #[must_use]
594    pub fn reason(&self) -> Option<&str> {
595        self.reason.as_deref()
596    }
597    #[must_use]
598    pub const fn action_available(&self) -> bool {
599        self.action_available
600    }
601    #[must_use]
602    pub fn task_id(&self) -> &str {
603        &self.task_id
604    }
605    #[must_use]
606    pub fn job_id(&self) -> &str {
607        &self.job_id
608    }
609    #[must_use]
610    pub fn run_id(&self) -> &str {
611        &self.run_id
612    }
613    #[must_use]
614    pub fn worker_id(&self) -> &str {
615        &self.worker_id
616    }
617    #[must_use]
618    pub fn assignment_generation(&self) -> &str {
619        &self.assignment_generation
620    }
621    #[must_use]
622    pub fn source_sha256(&self) -> &str {
623        &self.source_sha256
624    }
625    #[must_use]
626    pub fn candidate_sha256(&self) -> Option<&str> {
627        self.candidate_sha256.as_deref()
628    }
629    #[must_use]
630    pub fn provenance(&self) -> &str {
631        &self.provenance
632    }
633}
634
635impl AuthoritativeResultView {
636    #[must_use]
637    pub const fn verdict(&self) -> NativeVerdict {
638        self.verdict
639    }
640    #[must_use]
641    pub fn candidate_sha256(&self) -> &str {
642        &self.candidate_sha256
643    }
644    #[must_use]
645    pub fn source_sha256(&self) -> &str {
646        &self.source_sha256
647    }
648    #[must_use]
649    pub fn run_id(&self) -> &str {
650        &self.run_id
651    }
652    #[must_use]
653    pub fn job_id(&self) -> &str {
654        &self.job_id
655    }
656    #[must_use]
657    pub fn worker_id(&self) -> Option<&str> {
658        self.worker_id.as_deref()
659    }
660    #[must_use]
661    pub fn assignment_generation(&self) -> Option<&str> {
662        self.assignment_generation.as_deref()
663    }
664    #[must_use]
665    pub fn result_digest(&self) -> &str {
666        &self.result_digest
667    }
668}
669
670impl AuthoritativeEvidenceView {
671    #[must_use]
672    pub fn items(&self) -> &[NativeEvidenceItem] {
673        &self.items
674    }
675    #[must_use]
676    pub const fn total(&self) -> usize {
677        self.total
678    }
679    #[must_use]
680    pub const fn passed(&self) -> usize {
681        self.passed
682    }
683    #[must_use]
684    pub const fn failed(&self) -> usize {
685        self.failed
686    }
687    #[must_use]
688    pub const fn insufficient(&self) -> usize {
689        self.insufficient
690    }
691    #[must_use]
692    pub const fn unknown(&self) -> usize {
693        self.unknown
694    }
695}
696
697impl AuthoritativeReportView {
698    #[must_use]
699    pub fn id(&self) -> &str {
700        &self.id
701    }
702    #[must_use]
703    pub fn report_digest(&self) -> &str {
704        &self.report_digest
705    }
706    #[must_use]
707    pub fn candidate_sha256(&self) -> &str {
708        &self.candidate_sha256
709    }
710    #[must_use]
711    pub fn source_sha256(&self) -> &str {
712        &self.source_sha256
713    }
714    #[must_use]
715    pub const fn verdict(&self) -> NativeVerdict {
716        self.verdict
717    }
718    #[must_use]
719    pub fn job_id(&self) -> &str {
720        &self.job_id
721    }
722    #[must_use]
723    pub fn run_id(&self) -> &str {
724        &self.run_id
725    }
726    #[must_use]
727    pub fn result_digest(&self) -> &str {
728        &self.result_digest
729    }
730}
731
732impl AuthoritativeTaskView {
733    pub fn try_from_value(value: &Value) -> Result<Self, String> {
734        parse_task_view(value)
735    }
736    pub fn from_value(value: &Value) -> Result<Self, String> {
737        Self::try_from_value(value)
738    }
739}
740impl AuthoritativeFrozenScopeView {
741    pub fn try_from_value(value: &Value) -> Result<Self, String> {
742        parse_scope_view(value)
743    }
744    pub fn from_value(value: &Value) -> Result<Self, String> {
745        Self::try_from_value(value)
746    }
747}
748impl AuthoritativeJobView {
749    pub fn try_from_value(value: &Value) -> Result<Self, String> {
750        parse_job_view(value)
751    }
752    pub fn from_value(value: &Value) -> Result<Self, String> {
753        Self::try_from_value(value)
754    }
755}
756impl AuthoritativeRunView {
757    pub fn try_from_value(value: &Value) -> Result<Self, String> {
758        parse_run_view(value)
759    }
760    pub fn from_value(value: &Value) -> Result<Self, String> {
761        Self::try_from_value(value)
762    }
763}
764impl AuthoritativeRecoveryView {
765    #[allow(clippy::too_many_arguments)]
766    pub fn try_from_value(
767        value: &Value,
768        task: &AuthoritativeTaskView,
769        job: &AuthoritativeJobView,
770        run: &AuthoritativeRunView,
771        source: &str,
772    ) -> Result<Self, String> {
773        parse_recovery_view(value, Some(task), Some(job), Some(run), Some(source))
774    }
775    #[allow(clippy::too_many_arguments)]
776    pub fn from_value(
777        value: &Value,
778        task: &AuthoritativeTaskView,
779        job: &AuthoritativeJobView,
780        run: &AuthoritativeRunView,
781        source: &str,
782    ) -> Result<Self, String> {
783        Self::try_from_value(value, task, job, run, source)
784    }
785}
786
787impl AuthoritativeEvidenceView {
788    pub fn try_from_value(
789        _value: &Value,
790        _scope: &AuthoritativeFrozenScopeView,
791    ) -> Result<Self, String> {
792        Err("evidence authority requires a validated parent Result and source context".to_owned())
793    }
794
795    pub fn try_from_value_with_result(
796        value: &Value,
797        scope: &AuthoritativeFrozenScopeView,
798        result: &AuthoritativeResultView,
799    ) -> Result<Self, String> {
800        let values = strict_evidence_array(value)?.ok_or("evidence array is missing")?;
801        if values.len() > MAX_EVIDENCE_ITEMS {
802            return Err("evidence set exceeds validation bound".to_owned());
803        }
804        let mut ids = BTreeSet::new();
805        let mut items = Vec::with_capacity(values.len());
806        let mut counts = [0usize; 4];
807        for value in values {
808            let (id, item, state) = parse_evidence_item(
809                value,
810                scope,
811                Some(result.job_id()),
812                Some(result.run_id()),
813                Some(result.source_sha256()),
814                Some(result.result_digest()),
815            )?;
816            if !ids.insert(id) {
817                return Err("duplicate evidence ID".to_owned());
818            }
819            match state {
820                EvidenceState::Passed => counts[0] += 1,
821                EvidenceState::Failed => counts[1] += 1,
822                EvidenceState::Insufficient => counts[2] += 1,
823                EvidenceState::Unknown => counts[3] += 1,
824            }
825            items.push(item);
826        }
827        Ok(Self {
828            items,
829            total: values.len(),
830            passed: counts[0],
831            failed: counts[1],
832            insufficient: counts[2],
833            unknown: counts[3],
834        })
835    }
836
837    pub fn from_value(value: &Value, scope: &AuthoritativeFrozenScopeView) -> Result<Self, String> {
838        Self::try_from_value(value, scope)
839    }
840
841    pub fn from_value_with_result(
842        value: &Value,
843        scope: &AuthoritativeFrozenScopeView,
844        result: &AuthoritativeResultView,
845    ) -> Result<Self, String> {
846        Self::try_from_value_with_result(value, scope, result)
847    }
848}
849
850#[derive(Debug, Clone, PartialEq, Eq)]
851#[non_exhaustive]
852pub struct ValidatedReplacementSessionView {
853    predecessor_session_id: String,
854    replacement_session_id: String,
855    predecessor_state: String,
856    candidate_sha256: String,
857    workspace_state: String,
858    source_git_state_sequence: u64,
859}
860
861impl ValidatedReplacementSessionView {
862    #[must_use]
863    pub fn predecessor_session_id(&self) -> &str {
864        &self.predecessor_session_id
865    }
866    #[must_use]
867    pub fn replacement_session_id(&self) -> &str {
868        &self.replacement_session_id
869    }
870    #[must_use]
871    pub fn predecessor_state(&self) -> &str {
872        &self.predecessor_state
873    }
874    #[must_use]
875    pub fn candidate_sha256(&self) -> &str {
876        &self.candidate_sha256
877    }
878    #[must_use]
879    pub fn workspace_state(&self) -> &str {
880        &self.workspace_state
881    }
882    #[must_use]
883    pub const fn source_git_state_sequence(&self) -> u64 {
884        self.source_git_state_sequence
885    }
886}
887
888impl ValidatedReplacementSessionView {
889    fn try_from_record_predecessor(
890        record: &SessionReplacementRecord,
891        predecessor_events: &[Event],
892    ) -> Result<Self, String> {
893        validate_replacement_predecessor(record, predecessor_events)?;
894        let git = predecessor_events
895            .iter()
896            .find(|event| {
897                event.sequence == record.source_git_state_sequence
898                    && event.kind == EventKind::GitState
899            })
900            .ok_or("replacement GitState sequence is not bound")?;
901        let workspace_state = git
902            .payload
903            .get("workspace_state_sha256")
904            .and_then(Value::as_str)
905            .filter(|value| valid_digest(value))
906            .ok_or("replacement workspace identity is unavailable")?;
907        Ok(Self {
908            predecessor_session_id: record.predecessor_session_id.clone(),
909            replacement_session_id: record.replacement_session_id.clone(),
910            predecessor_state: record.predecessor_state.clone(),
911            candidate_sha256: record.candidate_sha256.clone(),
912            workspace_state: workspace_state.to_owned(),
913            source_git_state_sequence: record.source_git_state_sequence,
914        })
915    }
916
917    /// Construct trusted replacement authority only from both complete,
918    /// session-bound histories. The predecessor-only path is intentionally
919    /// private so a forged successor ID cannot mint a trusted view.
920    pub fn try_from_record_with_related(
921        record: &SessionReplacementRecord,
922        predecessor_events: &[Event],
923        replacement_events: &[Event],
924    ) -> Result<Self, String> {
925        let view = Self::try_from_record_predecessor(record, predecessor_events)?;
926        if replacement_events.is_empty()
927            || replacement_events
928                .iter()
929                .any(|event| event.session_id != record.replacement_session_id)
930        {
931            return Err("replacement events are not bound to the recorded successor".to_owned());
932        }
933        let replaced = replacement_events
934            .iter()
935            .filter(|event| event.kind == EventKind::SessionReplaced)
936            .collect::<Vec<_>>();
937        if replaced.len() != 1 {
938            return Err("replacement relation event is missing or duplicated".to_owned());
939        }
940        let payload = &replaced[0].payload;
941        for (key, expected) in [
942            (
943                "predecessor_session_id",
944                record.predecessor_session_id.as_str(),
945            ),
946            ("predecessor_state", record.predecessor_state.as_str()),
947            (
948                "predecessor_history_sha256",
949                record.predecessor_history_sha256.as_str(),
950            ),
951            ("candidate_sha256", record.candidate_sha256.as_str()),
952            ("falsegreen_task_id", record.falsegreen_task_id.as_str()),
953        ] {
954            if payload.get(key).and_then(Value::as_str) != Some(expected) {
955                return Err(format!("replacement relation field {key} is not exact"));
956            }
957        }
958        if payload
959            .get("candidate_event_sequence")
960            .and_then(Value::as_u64)
961            != Some(record.candidate_event_sequence)
962            || payload
963                .get("source_git_state_sequence")
964                .and_then(Value::as_u64)
965                != Some(record.source_git_state_sequence)
966        {
967            return Err("replacement relation sequence binding is not exact".to_owned());
968        }
969        let authority = predecessor_events
970            .iter()
971            .find(|event| {
972                event.kind == EventKind::Checkpoint
973                    && event.payload["checkpoint_kind"] == "acceptance_authority"
974            })
975            .ok_or("predecessor task authority is missing")?;
976        let mut expected_authority = authority.payload.clone();
977        expected_authority["replacement_predecessor_session_id"] =
978            Value::String(record.predecessor_session_id.clone());
979        let replacement_authority = replacement_events
980            .iter()
981            .filter(|event| {
982                event.kind == EventKind::Checkpoint
983                    && event.payload["checkpoint_kind"] == "acceptance_authority"
984            })
985            .collect::<Vec<_>>();
986        if replacement_authority.len() != 1
987            || replacement_authority[0].payload != expected_authority
988        {
989            return Err("replacement task authority binding is not exact".to_owned());
990        }
991        let carried = replacement_events
992            .iter()
993            .filter(|event| {
994                event.kind == EventKind::CandidateReady
995                    && event.payload["carried_forward"].as_bool() == Some(true)
996            })
997            .collect::<Vec<_>>();
998        if carried.len() != 1
999            || carried[0].payload["candidate_sha256"].as_str()
1000                != Some(record.candidate_sha256.as_str())
1001            || carried[0].payload["predecessor_session_id"].as_str()
1002                != Some(record.predecessor_session_id.as_str())
1003            || carried[0].payload["predecessor_candidate_event_sequence"].as_u64()
1004                != Some(record.candidate_event_sequence)
1005        {
1006            return Err("replacement candidate continuity is incomplete".to_owned());
1007        }
1008        let git_states = replacement_events
1009            .iter()
1010            .filter(|event| event.kind == EventKind::GitState)
1011            .collect::<Vec<_>>();
1012        let source_git = predecessor_events
1013            .iter()
1014            .find(|event| event.sequence == record.source_git_state_sequence)
1015            .ok_or("predecessor GitState is missing")?;
1016        if git_states.len() != 1
1017            || git_states[0].sequence <= carried[0].sequence
1018            || git_states[0].payload != source_git.payload
1019        {
1020            return Err("replacement GitState continuity is incomplete".to_owned());
1021        }
1022        let validations = replacement_events
1023            .iter()
1024            .filter(|event| {
1025                event.kind == EventKind::Checkpoint
1026                    && event.payload["checkpoint_kind"] == "replacement_candidate_validation"
1027            })
1028            .collect::<Vec<_>>();
1029        if validations.len() != 1
1030            || validations[0].payload["state"] != "candidate_ready"
1031            || validations[0].payload["predecessor_session_id"].as_str()
1032                != Some(record.predecessor_session_id.as_str())
1033            || validations[0].payload["expected_candidate_sha256"].as_str()
1034                != Some(record.candidate_sha256.as_str())
1035            || validations[0].payload["actual_candidate_sha256"].as_str()
1036                != Some(record.candidate_sha256.as_str())
1037            || validations[0].payload["matched"] != true
1038        {
1039            return Err("replacement candidate validation checkpoint is incomplete".to_owned());
1040        }
1041        Ok(view)
1042    }
1043}
1044
1045fn validate_replacement_predecessor(
1046    record: &SessionReplacementRecord,
1047    events: &[Event],
1048) -> Result<(), String> {
1049    if record.predecessor_session_id.is_empty()
1050        || record.replacement_session_id.is_empty()
1051        || record.predecessor_session_id == record.replacement_session_id
1052        || record.predecessor_state != "failed"
1053        || !valid_digest(&record.predecessor_history_sha256)
1054        || !valid_digest(&record.candidate_sha256)
1055        || record.falsegreen_task_id.is_empty()
1056    {
1057        return Err("replacement record shape is invalid".to_owned());
1058    }
1059    if events.is_empty()
1060        || events
1061            .iter()
1062            .any(|event| event.session_id != record.predecessor_session_id)
1063    {
1064        return Err("predecessor events are not bound to the recorded session".to_owned());
1065    }
1066    if history_sha256(events).map_err(|error| error.to_string())?
1067        != record.predecessor_history_sha256
1068    {
1069        return Err("predecessor history digest is not exact".to_owned());
1070    }
1071    let terminal = events.last().ok_or("predecessor history is empty")?;
1072    if terminal.kind != EventKind::TerminalState
1073        || terminal.payload["state"] != "failed"
1074        || terminal.payload["reason"] != "falsegreen_infrastructure_failure"
1075    {
1076        return Err("predecessor terminal state is not eligible".to_owned());
1077    }
1078    let candidate_index = events
1079        .iter()
1080        .position(|event| event.sequence == record.candidate_event_sequence)
1081        .ok_or("replacement candidate sequence is not bound")?;
1082    let candidate = &events[candidate_index];
1083    if candidate.kind != EventKind::CandidateReady
1084        || candidate.payload["candidate_sha256"].as_str() != Some(record.candidate_sha256.as_str())
1085        || candidate.payload["summary"]
1086            .as_str()
1087            .is_none_or(str::is_empty)
1088    {
1089        return Err("replacement candidate event is incomplete".to_owned());
1090    }
1091    let boundary = events[..candidate_index]
1092        .iter()
1093        .rev()
1094        .find(|event| {
1095            event.kind == EventKind::Checkpoint
1096                && event.payload["checkpoint_kind"] == "turn_boundary"
1097        })
1098        .ok_or("replacement turn boundary is missing")?;
1099    if boundary.payload["candidate_sha256"].as_str() != Some(record.candidate_sha256.as_str()) {
1100        return Err("replacement turn boundary candidate is not exact".to_owned());
1101    }
1102    let git = events
1103        .iter()
1104        .find(|event| {
1105            event.sequence == record.source_git_state_sequence
1106                && event.kind == EventKind::GitState
1107                && event.sequence > candidate.sequence
1108        })
1109        .ok_or("replacement GitState sequence is not bound")?;
1110    if git.payload["workspace_state_sha256"]
1111        .as_str()
1112        .is_none_or(|value| !valid_digest(value))
1113    {
1114        return Err("replacement workspace identity is unavailable".to_owned());
1115    }
1116    let authority = events
1117        .iter()
1118        .filter(|event| {
1119            event.kind == EventKind::Checkpoint
1120                && event.payload["checkpoint_kind"] == "acceptance_authority"
1121        })
1122        .collect::<Vec<_>>();
1123    if authority.len() != 1
1124        || authority[0].payload["falsegreen_task_id"].as_str()
1125            != Some(record.falsegreen_task_id.as_str())
1126    {
1127        return Err("replacement task binding is not exact".to_owned());
1128    }
1129    if events
1130        .iter()
1131        .any(|event| event.kind == EventKind::SessionReplaced)
1132    {
1133        return Err("predecessor contains a forbidden replacement chain".to_owned());
1134    }
1135    Ok(())
1136}
1137
1138#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1139#[serde(rename_all = "snake_case")]
1140pub enum NativeSurfaceFamily {
1141    Task,
1142    FrozenScope,
1143    Job,
1144    Run,
1145    VerificationResult,
1146    Evidence,
1147    Report,
1148    FailureRecovery,
1149    ReplacementSession,
1150}
1151
1152impl NativeSurfaceFamily {
1153    #[must_use]
1154    pub const fn as_str(self) -> &'static str {
1155        match self {
1156            Self::Task => "task",
1157            Self::FrozenScope => "frozen-scope",
1158            Self::Job => "job",
1159            Self::Run => "run",
1160            Self::VerificationResult => "verification-result",
1161            Self::Evidence => "evidence",
1162            Self::Report => "report",
1163            Self::FailureRecovery => "failure-recovery",
1164            Self::ReplacementSession => "replacement-session",
1165        }
1166    }
1167}
1168
1169#[derive(Debug, Clone, PartialEq)]
1170pub struct NativeSurfaceSet {
1171    pub task: Surface,
1172    pub frozen_scope: Surface,
1173    pub job: Surface,
1174    pub run: Surface,
1175    pub verification_result: Surface,
1176    pub evidence: Surface,
1177    pub report: Surface,
1178    pub failure_recovery: Surface,
1179    pub replacement_session: Surface,
1180}
1181
1182impl NativeSurfaceSet {
1183    #[must_use]
1184    pub fn get(&self, family: NativeSurfaceFamily) -> &Surface {
1185        match family {
1186            NativeSurfaceFamily::Task => &self.task,
1187            NativeSurfaceFamily::FrozenScope => &self.frozen_scope,
1188            NativeSurfaceFamily::Job => &self.job,
1189            NativeSurfaceFamily::Run => &self.run,
1190            NativeSurfaceFamily::VerificationResult => &self.verification_result,
1191            NativeSurfaceFamily::Evidence => &self.evidence,
1192            NativeSurfaceFamily::Report => &self.report,
1193            NativeSurfaceFamily::FailureRecovery => &self.failure_recovery,
1194            NativeSurfaceFamily::ReplacementSession => &self.replacement_session,
1195        }
1196    }
1197
1198    pub fn iter(&self) -> impl Iterator<Item = (NativeSurfaceFamily, &Surface)> {
1199        [
1200            (NativeSurfaceFamily::Task, &self.task),
1201            (NativeSurfaceFamily::FrozenScope, &self.frozen_scope),
1202            (NativeSurfaceFamily::Job, &self.job),
1203            (NativeSurfaceFamily::Run, &self.run),
1204            (
1205                NativeSurfaceFamily::VerificationResult,
1206                &self.verification_result,
1207            ),
1208            (NativeSurfaceFamily::Evidence, &self.evidence),
1209            (NativeSurfaceFamily::Report, &self.report),
1210            (NativeSurfaceFamily::FailureRecovery, &self.failure_recovery),
1211            (
1212                NativeSurfaceFamily::ReplacementSession,
1213                &self.replacement_session,
1214            ),
1215        ]
1216        .into_iter()
1217    }
1218}
1219
1220#[derive(Debug, Error, Clone, PartialEq, Eq)]
1221pub enum NativeProjectionError {
1222    #[error("event-store error while projecting native state: {0}")]
1223    EventStore(String),
1224    #[error("session error while projecting native state: {0}")]
1225    Session(String),
1226    #[error("native state snapshot drifted while it was being read; refresh required")]
1227    SnapshotDrift,
1228    #[error("native state is malformed: {0}")]
1229    Malformed(String),
1230    #[error("native surface rendering failed: {0}")]
1231    Render(String),
1232}
1233
1234impl From<EventError> for NativeProjectionError {
1235    fn from(error: EventError) -> Self {
1236        Self::EventStore(error.to_string())
1237    }
1238}
1239impl From<SessionError> for NativeProjectionError {
1240    fn from(error: SessionError) -> Self {
1241        Self::Session(error.to_string())
1242    }
1243}
1244impl From<GenUiError> for NativeProjectionError {
1245    fn from(error: GenUiError) -> Self {
1246        Self::Render(error.to_string())
1247    }
1248}
1249
1250#[derive(Debug, Clone)]
1251struct Capture {
1252    session: Session,
1253    events: Vec<Event>,
1254    relation: Option<SessionReplacementRecord>,
1255    related_events: Vec<Event>,
1256}
1257
1258#[derive(Debug, Clone, PartialEq)]
1259pub struct NativeStateProjection {
1260    pub snapshot: NativeSnapshotIdentity,
1261    pub task: NativeTask,
1262    pub frozen_scope: NativeFrozenScope,
1263    pub job: NativeJob,
1264    pub run: NativeRun,
1265    pub verification: NativeVerification,
1266    pub evidence: NativeEvidence,
1267    pub report: NativeReport,
1268    pub recovery: NativeRecovery,
1269    pub replacement: NativeReplacement,
1270    surfaces: NativeSurfaceSet,
1271}
1272
1273impl NativeStateProjection {
1274    pub fn from_store(store: &EventStore, session_id: &str) -> Result<Self, NativeProjectionError> {
1275        let first = capture(store, session_id)?;
1276        let first_digest = capture_digest(&first)?;
1277        let second = capture(store, session_id)?;
1278        if first_digest != capture_digest(&second)? || first.relation != second.relation {
1279            return Err(NativeProjectionError::SnapshotDrift);
1280        }
1281        from_capture(&second)
1282    }
1283
1284    pub fn project(store: &EventStore, session_id: &str) -> Result<Self, NativeProjectionError> {
1285        Self::from_store(store, session_id)
1286    }
1287
1288    pub fn from_session(
1289        store: &EventStore,
1290        session: &Session,
1291    ) -> Result<Self, NativeProjectionError> {
1292        let projection = Self::from_store(store, &session.id)?;
1293        if projection.task.lifecycle.value.as_deref() != Some(session_state_label(session.state)) {
1294            return Err(NativeProjectionError::SnapshotDrift);
1295        }
1296        Ok(projection)
1297    }
1298
1299    #[must_use]
1300    pub fn surfaces(&self) -> &NativeSurfaceSet {
1301        &self.surfaces
1302    }
1303
1304    #[must_use]
1305    pub fn surface(&self, family: NativeSurfaceFamily) -> &Surface {
1306        self.surfaces.get(family)
1307    }
1308
1309    pub fn is_stale(&self, store: &EventStore) -> Result<bool, NativeProjectionError> {
1310        Ok(Self::from_store(store, &self.snapshot.source_session_id)?.snapshot != self.snapshot)
1311    }
1312
1313    pub fn refresh(&self, store: &EventStore) -> Result<Self, NativeProjectionError> {
1314        Self::from_store(store, &self.snapshot.source_session_id)
1315    }
1316
1317    pub fn render(
1318        &self,
1319        store: &EventStore,
1320        family: NativeSurfaceFamily,
1321        terminal_width: usize,
1322    ) -> Result<String, NativeProjectionError> {
1323        if self.is_stale(store)? {
1324            return Err(NativeProjectionError::SnapshotDrift);
1325        }
1326        let capabilities = default_negotiated_capabilities(terminal_width.max(1));
1327        let mut catalog = ActionCatalog::default();
1328        render_surface_with_capabilities_and_catalog_with_store(
1329            self.surface(family),
1330            &capabilities,
1331            &mut catalog,
1332            store,
1333        )
1334        .map_err(NativeProjectionError::from)
1335    }
1336}
1337
1338pub fn native_state_surfaces(
1339    store: &EventStore,
1340    session_id: &str,
1341) -> Result<NativeSurfaceSet, NativeProjectionError> {
1342    Ok(NativeStateProjection::from_store(store, session_id)?
1343        .surfaces
1344        .clone())
1345}
1346
1347pub fn project_native_state(
1348    store: &EventStore,
1349    session_id: &str,
1350) -> Result<NativeStateProjection, NativeProjectionError> {
1351    NativeStateProjection::from_store(store, session_id)
1352}
1353
1354pub fn render_native_surface(
1355    store: &EventStore,
1356    session_id: &str,
1357    family: NativeSurfaceFamily,
1358    terminal_width: usize,
1359) -> Result<String, NativeProjectionError> {
1360    NativeStateProjection::from_store(store, session_id)?.render(store, family, terminal_width)
1361}
1362
1363fn capture(store: &EventStore, session_id: &str) -> Result<Capture, NativeProjectionError> {
1364    let session = Session::reconstruct(store, session_id)?;
1365    let events = store.events(session_id)?;
1366    let relation = store
1367        .replacement_for_session(session_id)?
1368        .or(store.replacement_for_predecessor(session_id)?);
1369    let related_events = relation
1370        .as_ref()
1371        .map(|relation| {
1372            let related_id = if relation.predecessor_session_id == session_id {
1373                &relation.replacement_session_id
1374            } else {
1375                &relation.predecessor_session_id
1376            };
1377            store.events(related_id)
1378        })
1379        .transpose()?
1380        .unwrap_or_default();
1381    Ok(Capture {
1382        session,
1383        events,
1384        relation,
1385        related_events,
1386    })
1387}
1388
1389fn capture_digest(capture: &Capture) -> Result<String, NativeProjectionError> {
1390    let own = history_sha256(&capture.events)?;
1391    let related = (!capture.related_events.is_empty())
1392        .then(|| history_sha256(&capture.related_events))
1393        .transpose()?;
1394    serde_json::to_string(&serde_json::json!({
1395        "schema": "falsegreen.agent.native-state-snapshot.v2",
1396        "session": capture.session.id,
1397        "state": session_state_label(capture.session.state),
1398        "own": own,
1399        "related": related,
1400        "relation": capture.relation,
1401    }))
1402    .map(|value| sha256_bytes(value.as_bytes()))
1403    .map_err(|error| NativeProjectionError::Malformed(error.to_string()))
1404}
1405
1406fn snapshot_identity(capture: &Capture) -> Result<NativeSnapshotIdentity, NativeProjectionError> {
1407    let state_generation = capture
1408        .events
1409        .iter()
1410        .chain(capture.related_events.iter())
1411        .map(|event| event.sequence)
1412        .max()
1413        .unwrap_or_default();
1414    Ok(NativeSnapshotIdentity {
1415        source_session_id: capture.session.id.clone(),
1416        state_generation,
1417        digest: capture_digest(capture)?,
1418    })
1419}
1420
1421struct Facts {
1422    task: NativeTask,
1423    frozen_scope: NativeFrozenScope,
1424    job: NativeJob,
1425    run: NativeRun,
1426    verification: NativeVerification,
1427    evidence: NativeEvidence,
1428    report: NativeReport,
1429    recovery: NativeRecovery,
1430    replacement: NativeReplacement,
1431}
1432
1433fn from_capture(capture: &Capture) -> Result<NativeStateProjection, NativeProjectionError> {
1434    let snapshot = snapshot_identity(capture)?;
1435    let facts = derive_facts(capture);
1436    let surfaces = build_surfaces(&snapshot, &facts);
1437    for (_, surface) in surfaces.iter() {
1438        surface.validate().map_err(NativeProjectionError::from)?;
1439    }
1440    Ok(NativeStateProjection {
1441        snapshot,
1442        task: facts.task,
1443        frozen_scope: facts.frozen_scope,
1444        job: facts.job,
1445        run: facts.run,
1446        verification: facts.verification,
1447        evidence: facts.evidence,
1448        report: facts.report,
1449        recovery: facts.recovery,
1450        replacement: facts.replacement,
1451        surfaces,
1452    })
1453}
1454
1455fn derive_facts(capture: &Capture) -> Facts {
1456    let candidate = candidate_context(&capture.events);
1457    let source = source_context(&capture.events);
1458    let task_view = latest_typed_task(&capture.events).filter(|task| {
1459        latest_typed_scope(&capture.events).is_some_and(|scope| {
1460            task.scope_id == scope.scope_id
1461                && task.scope_digest == scope.scope_digest
1462                && task
1463                    .candidate_sha256
1464                    .as_deref()
1465                    .is_none_or(|digest| candidate.as_deref() == Some(digest))
1466                && task
1467                    .source_sha256
1468                    .as_deref()
1469                    .is_none_or(|digest| source.as_deref() == Some(digest))
1470        })
1471    });
1472    let scope_view = latest_typed_scope(&capture.events);
1473    let mut job_view = latest_typed_job(&capture.events).filter(|job| {
1474        task_view.as_ref().is_some_and(|task| {
1475            task.task_id == job.task_id
1476                && task.current_job_id.as_deref().is_none_or(|id| id == job.id)
1477                && task.candidate_sha256.as_deref().is_some_and(|digest| {
1478                    job.candidate_sha256.as_deref() == Some(digest)
1479                        && candidate.as_deref() == Some(digest)
1480                })
1481                && task.source_sha256.as_deref().is_some_and(|digest| {
1482                    job.source_sha256.as_deref() == Some(digest)
1483                        && source.as_deref() == Some(digest)
1484                })
1485        })
1486    });
1487    // Run authority is intentionally sourced only from an explicit typed
1488    // `run_authority` checkpoint. Generic claim/session/checkpoint metadata is
1489    // diagnostic context and can never manufacture a trusted Run view.
1490    let mut run_view = latest_typed_run(&capture.events).filter(|run| {
1491        job_view.as_ref().is_some_and(|job| {
1492            run.job_id == job.id
1493                && (job.run_ids.is_empty() || job.run_ids.iter().any(|id| id == &run.id))
1494        })
1495    });
1496    if job_view.as_ref().is_some_and(|job| {
1497        run_view
1498            .as_ref()
1499            .is_some_and(|run| !job.run_ids.iter().any(|id| id == &run.id))
1500    }) {
1501        run_view = None;
1502    }
1503    // A Job collection is authoritative only when every listed current Run
1504    // resolves to the same validated Run authority. Never display a loose
1505    // run alias as a trusted collection member.
1506    if job_view.as_ref().is_some_and(|job| {
1507        !job.run_ids.is_empty()
1508            && run_view
1509                .as_ref()
1510                .is_none_or(|run| job.run_ids.len() != 1 || job.run_ids[0] != run.id)
1511    }) {
1512        job_view = None;
1513    }
1514    let result_view = latest_typed_result(
1515        &capture.events,
1516        candidate.as_deref(),
1517        source.as_deref(),
1518        scope_view.as_ref(),
1519        job_view.as_ref(),
1520        run_view.as_ref(),
1521    )
1522    .filter(|result| {
1523        job_view.as_ref().is_some_and(|job| job.id == result.job_id)
1524            && run_view.as_ref().is_some_and(|run| run.id == result.run_id)
1525            && result.worker_id.as_deref().is_none_or(|worker| {
1526                run_view.as_ref().and_then(|run| run.worker_id.as_deref()) == Some(worker)
1527            })
1528            && result
1529                .assignment_generation
1530                .as_deref()
1531                .is_none_or(|generation| {
1532                    run_view
1533                        .as_ref()
1534                        .and_then(|run| run.assignment_generation.as_deref())
1535                        == Some(generation)
1536                })
1537    });
1538    let report_view = latest_typed_report(
1539        &capture.events,
1540        candidate.as_deref(),
1541        source.as_deref(),
1542        job_view.as_ref(),
1543        run_view.as_ref(),
1544        result_view.as_ref(),
1545    );
1546    let evidence = result_view.as_ref().map_or_else(
1547        || unavailable_evidence("Evidence unavailable: no validated FalseGreen result or frozen scope authority is available."),
1548        |result| {
1549            evidence_from_result(
1550                result,
1551                scope_view.as_ref(),
1552                job_view.as_ref().map(|job| job.id.as_str()),
1553                run_view.as_ref().map(|run| run.id.as_str()),
1554                source.as_deref(),
1555            )
1556        },
1557    );
1558
1559    let task_id = task_view.as_ref().map(|view| view.task_id.clone());
1560    let scope_digest = scope_view.as_ref().map(|view| view.scope_digest.clone());
1561    let task = NativeTask {
1562        task_id: task_id.as_deref().map_or_else(
1563            || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
1564            NativeField::falsegreen,
1565        ),
1566        lifecycle: task_view
1567            .as_ref()
1568            .and_then(|view| view.lifecycle.as_deref())
1569            .map_or_else(
1570                || NativeField::agent(session_state_label(capture.session.state)),
1571                NativeField::falsegreen,
1572            ),
1573        frozen: scope_view.as_ref().map_or_else(
1574            || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
1575            |_| NativeField::falsegreen("FROZEN"),
1576        ),
1577        scope_digest: scope_digest.as_deref().map_or_else(
1578            || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
1579            NativeField::falsegreen,
1580        ),
1581        scope_summary: scope_view.as_ref().map_or_else(
1582            || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
1583            |view| {
1584                NativeField::falsegreen(format!("{} frozen obligation(s)", view.obligations.len()))
1585            },
1586        ),
1587        source_sha256: task_view
1588            .as_ref()
1589            .and_then(|view| view.source_sha256.as_deref())
1590            .map_or_else(
1591                || {
1592                    source.as_deref().map_or_else(
1593                        || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
1594                        NativeField::agent,
1595                    )
1596                },
1597                NativeField::falsegreen,
1598            ),
1599        candidate_sha256: task_view
1600            .as_ref()
1601            .and_then(|view| view.candidate_sha256.as_deref())
1602            .or(candidate.as_deref())
1603            .map_or_else(
1604                || NativeField::unavailable(AuthorityClass::AuthoritativeAgentSessionState),
1605                NativeField::agent,
1606            ),
1607        current_job_id: task_view
1608            .as_ref()
1609            .and_then(|view| view.current_job_id.as_deref())
1610            .or(job_view.as_ref().map(|view| view.id.as_str()))
1611            .map_or_else(
1612                || NativeField::unavailable(AuthorityClass::AuthoritativeAgentSessionState),
1613                NativeField::agent,
1614            ),
1615    };
1616    let frozen_scope = scope_view.as_ref().map_or_else(unavailable_scope, |view| {
1617        NativeFrozenScope {
1618            scope_digest: NativeField::falsegreen(view.scope_digest.clone()),
1619            frozen: NativeField::falsegreen("FROZEN"),
1620            obligations: view
1621                .obligations
1622                .iter()
1623                .map(|(id, requirement)| NativeScopeObligation {
1624                    id: NativeField::falsegreen(id),
1625                    requirement: NativeField::falsegreen(requirement),
1626                })
1627                .collect(),
1628            distinction: "Requirement text is immutable frozen scope; evidence/result state is separate and cannot rewrite it.".to_owned(),
1629        }
1630    });
1631
1632    let mut verification = result_view
1633        .as_ref()
1634        .map_or_else(unavailable_verification, |result| {
1635            verification_from_result(result, scope_view.as_ref())
1636        });
1637    let job = job_view
1638        .as_ref()
1639        .map_or_else(unavailable_job, |view| NativeJob {
1640            id: NativeField::falsegreen(view.id.clone()),
1641            status: view.status,
1642            status_raw: NativeField::falsegreen(status_wire(view.status)),
1643            task_id: NativeField::falsegreen(view.task_id.clone()),
1644            source_sha256: optional_falsegreen(view.source_sha256.as_deref()),
1645            candidate_sha256: optional_falsegreen(view.candidate_sha256.as_deref()),
1646            entitlement_consumption: optional_agent(view.entitlement_consumption.as_deref()),
1647            run_ids: view.run_ids.iter().map(NativeField::agent).collect(),
1648            authority_decision: NativeField::unavailable(
1649                AuthorityClass::AuthoritativeFalseGreenState,
1650            ),
1651        });
1652    let run = run_view
1653        .as_ref()
1654        .map_or_else(unavailable_run, |view| NativeRun {
1655            id: NativeField::agent(view.id.clone()),
1656            worker_id: optional_agent(view.worker_id.as_deref()),
1657            job_id: NativeField::agent(view.job_id.clone()),
1658            assignment_generation: optional_agent(view.assignment_generation.as_deref()),
1659            status: view.status,
1660            status_raw: NativeField::agent(status_wire(view.status)),
1661            execution_started_at: optional_agent(view.execution_started_at.as_deref()),
1662            package_runtime_identity: optional_agent(view.package_runtime_identity.as_deref()),
1663            recovery_lease: optional_agent(view.recovery_lease.as_deref()),
1664        });
1665    let recovery = latest_typed_recovery(
1666        &capture.events,
1667        task_view.as_ref(),
1668        job_view.as_ref(),
1669        run_view.as_ref(),
1670        source.as_deref(),
1671    )
1672    .map_or_else(|| recovery_unknown(&run), |view| recovery_from_view(&view));
1673    let report = report_view
1674        .as_ref()
1675        .map_or_else(|| unavailable_report(&verification), report_from_view);
1676    if let Some(report_id) = report.id.value.as_deref() {
1677        verification.report_reference = NativeField::falsegreen(report_id);
1678    }
1679    let replacement = replacement_from_capture(capture, &verification);
1680    Facts {
1681        task,
1682        frozen_scope,
1683        job,
1684        run,
1685        verification,
1686        evidence,
1687        report,
1688        recovery,
1689        replacement,
1690    }
1691}
1692
1693fn latest_typed_task(events: &[Event]) -> Option<AuthoritativeTaskView> {
1694    unique_latest(events, |event| {
1695        checkpoint_object(
1696            event,
1697            &["task_authority", "falsegreen_task_authority"],
1698            "task",
1699        )
1700        .or_else(|| {
1701            (event.kind == EventKind::Checkpoint
1702                && event.payload["checkpoint_kind"] == "acceptance_authority")
1703                .then(|| event.payload.get("task"))
1704                .flatten()
1705                .filter(|value| value.is_object())
1706        })
1707        .or_else(|| {
1708            (event.kind == EventKind::Checkpoint
1709                && (event.payload["checkpoint_kind"] == "falsegreen_task_authority"
1710                    || (event.payload["checkpoint_kind"] == "acceptance_authority"
1711                        && event.payload.get("task_id").is_some()
1712                        && event.payload.get("scope_id").is_some())))
1713            .then_some(&event.payload)
1714        })
1715    })
1716    .and_then(|value| parse_task_view(value).ok())
1717}
1718
1719fn latest_typed_scope(events: &[Event]) -> Option<AuthoritativeFrozenScopeView> {
1720    unique_latest(events, |event| {
1721        checkpoint_object(event, &["frozen_scope_authority"], "frozen_scope")
1722            .or_else(|| {
1723                (event.kind == EventKind::Checkpoint
1724                    && event.payload["checkpoint_kind"] == "acceptance_authority")
1725                    .then(|| event.payload.get("frozen_scope"))
1726                    .flatten()
1727                    .filter(|value| value.is_object())
1728            })
1729            .or_else(|| {
1730                (event.kind == EventKind::Checkpoint
1731                    && (event.payload["checkpoint_kind"] == "frozen_scope_authority"
1732                        || (event.payload["checkpoint_kind"] == "acceptance_authority"
1733                            && event.payload.get("scope_id").is_some()
1734                            && event.payload.get("obligations").is_some())))
1735                .then_some(&event.payload)
1736            })
1737    })
1738    .and_then(|value| parse_scope_view(value).ok())
1739}
1740
1741fn latest_typed_job(events: &[Event]) -> Option<AuthoritativeJobView> {
1742    unique_latest(events, |event| {
1743        checkpoint_object(event, &["job_authority"], "job").or_else(|| {
1744            (event.kind == EventKind::Checkpoint
1745                && event.payload["checkpoint_kind"] == "job_authority"
1746                && event.payload.get("job_id").is_some())
1747            .then_some(&event.payload)
1748        })
1749    })
1750    .and_then(|value| parse_job_view(value).ok())
1751}
1752
1753fn latest_typed_run(events: &[Event]) -> Option<AuthoritativeRunView> {
1754    unique_latest(events, |event| {
1755        checkpoint_object(event, &["run_authority"], "run").or_else(|| {
1756            (event.kind == EventKind::Checkpoint
1757                && event.payload["checkpoint_kind"] == "run_authority"
1758                && event.payload.get("run_id").is_some())
1759            .then_some(&event.payload)
1760        })
1761    })
1762    .and_then(|value| parse_run_view(value).ok())
1763}
1764
1765fn latest_typed_recovery(
1766    events: &[Event],
1767    task: Option<&AuthoritativeTaskView>,
1768    job: Option<&AuthoritativeJobView>,
1769    run: Option<&AuthoritativeRunView>,
1770    source: Option<&str>,
1771) -> Option<AuthoritativeRecoveryView> {
1772    unique_latest(events, |event| {
1773        checkpoint_object(event, &["recovery_authority"], "recovery").or_else(|| {
1774            (event.kind == EventKind::Checkpoint
1775                && event.payload["checkpoint_kind"] == "recovery_authority"
1776                && event.payload.get("state").is_some())
1777            .then_some(&event.payload)
1778        })
1779    })
1780    .and_then(|value| parse_recovery_view(value, task, job, run, source).ok())
1781}
1782
1783fn parse_recovery_view(
1784    value: &Value,
1785    task: Option<&AuthoritativeTaskView>,
1786    job: Option<&AuthoritativeJobView>,
1787    run: Option<&AuthoritativeRunView>,
1788    source: Option<&str>,
1789) -> Result<AuthoritativeRecoveryView, String> {
1790    let object = value
1791        .as_object()
1792        .ok_or("recovery authority is not an object")?;
1793    reject_unknown_keys(
1794        object,
1795        &[
1796            "state",
1797            "reason",
1798            "action_available",
1799            "task_id",
1800            "falsegreen_task_id",
1801            "job_id",
1802            "run_id",
1803            "worker_id",
1804            "worker",
1805            "assignment_generation",
1806            "generation",
1807            "source_sha256",
1808            "authoritative_source_sha256",
1809            "candidate_sha256",
1810            "provenance",
1811            "authority_source",
1812        ],
1813        "recovery authority",
1814    )?;
1815    let state = match required_string(object, "state")?.as_str() {
1816        "RECOVERABLE" => RecoveryState::Recoverable,
1817        "BLOCKED" => RecoveryState::Blocked,
1818        "TERMINAL" => RecoveryState::Terminal,
1819        "UNKNOWN" => RecoveryState::Unknown,
1820        _ => return Err("unknown recovery state".to_owned()),
1821    };
1822    let task = task.ok_or("recovery authority requires Task context")?;
1823    let job = job.ok_or("recovery authority requires Job context")?;
1824    let run = run.ok_or("recovery authority requires Run context")?;
1825    let source = source.ok_or("recovery authority requires source context")?;
1826    let task_id = required_string_any(object, &["task_id", "falsegreen_task_id"])?;
1827    let job_id = required_string(object, "job_id")?;
1828    let run_id = required_string(object, "run_id")?;
1829    let worker_id = required_string_any(object, &["worker_id", "worker"])?;
1830    let assignment_generation =
1831        required_string_any(object, &["assignment_generation", "generation"])?;
1832    let source_sha256 =
1833        required_digest_any(object, &["source_sha256", "authoritative_source_sha256"])?;
1834    let candidate_sha256 = optional_digest_any(object, &["candidate_sha256"])?;
1835    let provenance = required_string_any(object, &["provenance", "authority_source"])?;
1836    if !matches!(
1837        provenance.as_str(),
1838        "canonical" | "falsegreen_core" | "falsegreen"
1839    ) {
1840        return Err("recovery provenance is not a known authority source".to_owned());
1841    }
1842    if task.task_id != task_id
1843        || job.id != job_id
1844        || run.id != run_id
1845        || run.worker_id.as_deref() != Some(worker_id.as_str())
1846        || run.assignment_generation.as_deref() != Some(assignment_generation.as_str())
1847        || source != source_sha256
1848        || candidate_sha256
1849            .as_deref()
1850            .is_some_and(|candidate| task.candidate_sha256.as_deref() != Some(candidate))
1851    {
1852        return Err("recovery authority binding conflicts with current graph".to_owned());
1853    }
1854    Ok(AuthoritativeRecoveryView {
1855        state,
1856        reason: optional_string(object, "reason"),
1857        action_available: object
1858            .get("action_available")
1859            .and_then(Value::as_bool)
1860            .unwrap_or(false),
1861        task_id,
1862        job_id,
1863        run_id,
1864        worker_id,
1865        assignment_generation,
1866        source_sha256,
1867        candidate_sha256,
1868        provenance,
1869    })
1870}
1871
1872fn unique_latest<'a, F>(events: &'a [Event], select: F) -> Option<&'a Value>
1873where
1874    F: FnMut(&'a Event) -> Option<&'a Value>,
1875{
1876    let matches = events.iter().filter_map(select).collect::<Vec<_>>();
1877    (matches.len() == 1).then(|| matches[0])
1878}
1879
1880fn checkpoint_object<'a>(event: &'a Event, kinds: &[&str], key: &str) -> Option<&'a Value> {
1881    if event.kind != EventKind::Checkpoint
1882        || !kinds
1883            .iter()
1884            .any(|kind| event.payload["checkpoint_kind"] == *kind)
1885    {
1886        return None;
1887    }
1888    event.payload.get(key).filter(|value| value.is_object())
1889}
1890
1891fn parse_task_view(value: &Value) -> Result<AuthoritativeTaskView, String> {
1892    let object = value.as_object().ok_or("task authority is not an object")?;
1893    reject_unknown_keys(
1894        object,
1895        &[
1896            "task_id",
1897            "lifecycle",
1898            "scope_id",
1899            "frozen_scope_id",
1900            "scope_digest",
1901            "frozen_scope_digest",
1902            "source_sha256",
1903            "authoritative_source_sha256",
1904            "candidate_sha256",
1905            "current_job_id",
1906            "job_id",
1907            "checkpoint_kind",
1908            "falsegreen_task_id",
1909            "task",
1910            "frozen_scope",
1911            "obligations",
1912        ],
1913        "task authority",
1914    )?;
1915    let task_id = required_string(object, "task_id")?;
1916    if !valid_identity(&task_id) {
1917        return Err("task ID is malformed".to_owned());
1918    }
1919    let lifecycle = optional_string(object, "lifecycle");
1920    if lifecycle
1921        .as_deref()
1922        .is_some_and(|value| !is_known_session_lifecycle(value))
1923    {
1924        return Err("task lifecycle is unknown".to_owned());
1925    }
1926    Ok(AuthoritativeTaskView {
1927        task_id,
1928        lifecycle,
1929        scope_id: required_string_any(object, &["scope_id", "frozen_scope_id"])?,
1930        scope_digest: required_digest_any(object, &["scope_digest", "frozen_scope_digest"])?,
1931        source_sha256: optional_digest_any(
1932            object,
1933            &["source_sha256", "authoritative_source_sha256"],
1934        )?,
1935        candidate_sha256: optional_digest_any(object, &["candidate_sha256"])?,
1936        current_job_id: optional_string_any_checked(object, &["current_job_id", "job_id"])?,
1937    })
1938}
1939
1940fn parse_scope_view(value: &Value) -> Result<AuthoritativeFrozenScopeView, String> {
1941    let object = value
1942        .as_object()
1943        .ok_or("frozen scope authority is not an object")?;
1944    reject_unknown_keys(
1945        object,
1946        &[
1947            "scope_id",
1948            "id",
1949            "scope_digest",
1950            "digest",
1951            "obligations",
1952            "checkpoint_kind",
1953            "falsegreen_task_id",
1954            "task",
1955            "frozen_scope",
1956            "job_id",
1957        ],
1958        "frozen scope authority",
1959    )?;
1960    let scope_id = required_string_any(object, &["scope_id", "id"])?;
1961    if !valid_identity(&scope_id) {
1962        return Err("frozen scope ID is malformed".to_owned());
1963    }
1964    let scope_digest = required_digest_any(object, &["scope_digest", "digest"])?;
1965    let values = object
1966        .get("obligations")
1967        .and_then(Value::as_array)
1968        .ok_or("frozen scope obligations are missing")?;
1969    if values.is_empty() {
1970        return Err("frozen scope obligations are empty".to_owned());
1971    }
1972    let mut ids = BTreeSet::new();
1973    let mut obligations = Vec::with_capacity(values.len());
1974    for value in values {
1975        let object = value
1976            .as_object()
1977            .ok_or("malformed frozen-scope obligation")?;
1978        let id = required_string_any(object, &["id", "obligation_id"])?;
1979        if !valid_identity(&id) {
1980            return Err("frozen-scope obligation ID is malformed".to_owned());
1981        }
1982        let requirement = required_string_any(object, &["requirement", "text", "description"])?;
1983        if !ids.insert(id.clone()) {
1984            return Err(format!("duplicate obligation ID {id}"));
1985        }
1986        obligations.push((id, requirement));
1987    }
1988    if scope_digest != scope_digest_for_obligations(&obligations) {
1989        return Err("frozen scope digest does not match displayed obligations".to_owned());
1990    }
1991    Ok(AuthoritativeFrozenScopeView {
1992        scope_id,
1993        scope_digest,
1994        obligations,
1995    })
1996}
1997
1998fn parse_job_view(value: &Value) -> Result<AuthoritativeJobView, String> {
1999    let object = value.as_object().ok_or("job authority is not an object")?;
2000    reject_unknown_keys(
2001        object,
2002        &[
2003            "job_id",
2004            "id",
2005            "status",
2006            "task_id",
2007            "source_sha256",
2008            "authoritative_source_sha256",
2009            "candidate_sha256",
2010            "run_ids",
2011            "entitlement_consumption",
2012            "consumption",
2013            "checkpoint_kind",
2014        ],
2015        "Job authority",
2016    )?;
2017    let id = required_string_any(object, &["job_id", "id"])?;
2018    if !valid_identity(&id) {
2019        return Err("Job ID is malformed".to_owned());
2020    }
2021    let status = parse_status_exact(required_string(object, "status")?.as_str())
2022        .ok_or("unknown job status")?;
2023    let task_id = required_string(object, "task_id")?;
2024    if !valid_identity(&task_id) {
2025        return Err("Job task ID is malformed".to_owned());
2026    }
2027    let source_sha256 =
2028        optional_digest_any(object, &["source_sha256", "authoritative_source_sha256"])?;
2029    let candidate_sha256 = optional_digest_any(object, &["candidate_sha256"])?;
2030    let run_ids = object.get("run_ids").map_or(Ok(Vec::new()), |value| {
2031        let run_ids: Vec<String> = value
2032            .as_array()
2033            .ok_or("job run_ids are malformed")?
2034            .iter()
2035            .map(|value| {
2036                value
2037                    .as_str()
2038                    .filter(|value| valid_identity(value))
2039                    .map(str::to_owned)
2040                    .ok_or_else(|| "job run ID is malformed".to_owned())
2041            })
2042            .collect::<Result<_, _>>()?;
2043        let unique = run_ids.iter().collect::<BTreeSet<_>>().len();
2044        (unique == run_ids.len())
2045            .then_some(run_ids)
2046            .ok_or_else(|| "duplicate Job run ID".to_owned())
2047    })?;
2048    Ok(AuthoritativeJobView {
2049        id,
2050        status,
2051        task_id,
2052        source_sha256,
2053        candidate_sha256,
2054        run_ids,
2055        // Agent-side Job payloads do not carry product entitlement authority.
2056        // Keep consumption Unknown until a separately validated, fully
2057        // cross-bound entitlement object is available; never infer it from a
2058        // loose alias such as `consumption: "consumed"`.
2059        entitlement_consumption: None,
2060    })
2061}
2062
2063fn parse_run_view(value: &Value) -> Result<AuthoritativeRunView, String> {
2064    let object = value.as_object().ok_or("run authority is not an object")?;
2065    reject_unknown_keys(
2066        object,
2067        &[
2068            "run_id",
2069            "id",
2070            "status",
2071            "job_id",
2072            "worker_id",
2073            "worker",
2074            "assignment_generation",
2075            "generation",
2076            "execution_started_at",
2077            "package_runtime_identity",
2078            "package_identity",
2079            "runtime_identity",
2080            "recovery_lease",
2081            "lease",
2082            "checkpoint_kind",
2083        ],
2084        "Run authority",
2085    )?;
2086    let id = required_string_any(object, &["run_id", "id"])?;
2087    if !valid_identity(&id) {
2088        return Err("Run ID is malformed".to_owned());
2089    }
2090    Ok(AuthoritativeRunView {
2091        id,
2092        status: parse_status_exact(required_string(object, "status")?.as_str())
2093            .ok_or("unknown run status")?,
2094        job_id: {
2095            let job_id = required_string(object, "job_id")?;
2096            if !valid_identity(&job_id) {
2097                return Err("Run Job ID is malformed".to_owned());
2098            }
2099            job_id
2100        },
2101        worker_id: optional_string_any_checked(object, &["worker_id", "worker"])?,
2102        assignment_generation: optional_string_any_checked(
2103            object,
2104            &["assignment_generation", "generation"],
2105        )?,
2106        execution_started_at: optional_string(object, "execution_started_at"),
2107        package_runtime_identity: optional_string_any_checked(
2108            object,
2109            &[
2110                "package_runtime_identity",
2111                "package_identity",
2112                "runtime_identity",
2113            ],
2114        )?,
2115        recovery_lease: optional_string_any_checked(object, &["recovery_lease", "lease"])?,
2116    })
2117}
2118
2119#[derive(Debug, Clone)]
2120struct ResultView {
2121    payload: Value,
2122    verdict: NativeVerdict,
2123    candidate_sha256: String,
2124    source_sha256: String,
2125    run_id: String,
2126    job_id: String,
2127    worker_id: Option<String>,
2128    assignment_generation: Option<String>,
2129    completion_authority: Option<String>,
2130    result_digest: String,
2131}
2132
2133impl AuthoritativeResultView {
2134    pub fn try_from_value(
2135        value: &Value,
2136        candidate: &str,
2137        source: Option<&str>,
2138        scope: Option<&AuthoritativeFrozenScopeView>,
2139    ) -> Result<Self, String> {
2140        if source.is_none() {
2141            return Err("trusted current source digest is required".to_owned());
2142        }
2143        let result = parse_result_view(value, Some(candidate), source, scope)?;
2144        Ok(Self {
2145            verdict: result.verdict,
2146            candidate_sha256: result.candidate_sha256,
2147            source_sha256: result.source_sha256,
2148            run_id: result.run_id,
2149            job_id: result.job_id,
2150            worker_id: result.worker_id,
2151            assignment_generation: result.assignment_generation,
2152            result_digest: result.result_digest,
2153        })
2154    }
2155
2156    pub fn from_value(
2157        value: &Value,
2158        candidate: &str,
2159        source: Option<&str>,
2160        scope: Option<&AuthoritativeFrozenScopeView>,
2161    ) -> Result<Self, String> {
2162        Self::try_from_value(value, candidate, source, scope)
2163    }
2164}
2165
2166fn latest_typed_result(
2167    events: &[Event],
2168    candidate: Option<&str>,
2169    source: Option<&str>,
2170    scope: Option<&AuthoritativeFrozenScopeView>,
2171    job: Option<&AuthoritativeJobView>,
2172    run: Option<&AuthoritativeRunView>,
2173) -> Option<ResultView> {
2174    if events
2175        .iter()
2176        .filter(|event| event.kind == EventKind::FalsegreenResult)
2177        .count()
2178        != 1
2179    {
2180        return None;
2181    }
2182    let event = events
2183        .iter()
2184        .rev()
2185        .find(|event| event.kind == EventKind::FalsegreenResult)?;
2186    let result = parse_result_view(&event.payload, candidate, source, scope).ok()?;
2187    if job.is_some_and(|job| {
2188        job.id == result.job_id && lifecycle_allows_result(job.status, result.verdict)
2189    }) && run.is_some_and(|run| {
2190        run.id == result.run_id && lifecycle_allows_result(run.status, result.verdict)
2191    }) {
2192        Some(result)
2193    } else {
2194        None
2195    }
2196}
2197
2198fn lifecycle_allows_result(status: NativeStatus, verdict: NativeVerdict) -> bool {
2199    match status {
2200        NativeStatus::Accepted => verdict == NativeVerdict::Accepted,
2201        NativeStatus::Failed => matches!(
2202            verdict,
2203            NativeVerdict::Failed | NativeVerdict::InsufficientEvidence
2204        ),
2205        NativeStatus::InsufficientEvidence => verdict == NativeVerdict::InsufficientEvidence,
2206        NativeStatus::Terminal => true,
2207        NativeStatus::Queued
2208        | NativeStatus::Claimed
2209        | NativeStatus::Running
2210        | NativeStatus::Executing
2211        | NativeStatus::Recoverable
2212        | NativeStatus::Unknown => false,
2213    }
2214}
2215
2216fn parse_result_view(
2217    payload: &Value,
2218    candidate: Option<&str>,
2219    source: Option<&str>,
2220    scope: Option<&AuthoritativeFrozenScopeView>,
2221) -> Result<ResultView, String> {
2222    let object = payload.as_object().ok_or("result is not an object")?;
2223    reject_unknown_keys(
2224        object,
2225        &[
2226            "verification",
2227            "verification_status",
2228            "candidate_sha256",
2229            "authoritative_source_sha256",
2230            "source_sha256",
2231            "run_id",
2232            "job_id",
2233            "worker_id",
2234            "worker",
2235            "assignment_generation",
2236            "generation",
2237            "completion_authority",
2238            "obligation_outcomes",
2239            "obligations",
2240            "evidence",
2241            "error",
2242            "repairable",
2243        ],
2244        "result authority",
2245    )?;
2246    let raw = object
2247        .get("verification")
2248        .and_then(Value::as_str)
2249        .ok_or("result verification is missing")?;
2250    let verdict = parse_verdict_exact(raw).ok_or("result verdict is not canonical")?;
2251    let status = object
2252        .get("verification_status")
2253        .and_then(Value::as_str)
2254        .ok_or("result verification_status is missing")?;
2255    if parse_verdict_exact(status) != Some(verdict)
2256        || !matches!(status, "accepted" | "failed" | "insufficient_evidence")
2257    {
2258        return Err("result verification fields disagree".to_owned());
2259    }
2260    let candidate_sha256 = object
2261        .get("candidate_sha256")
2262        .and_then(Value::as_str)
2263        .filter(|value| valid_digest(value))
2264        .ok_or("result candidate digest is malformed")?;
2265    if candidate != Some(candidate_sha256) {
2266        return Err("result candidate does not match current candidate".to_owned());
2267    }
2268    let expected_source = source.ok_or("current trusted source digest is unavailable")?;
2269    let source_sha256 =
2270        required_digest_any(object, &["authoritative_source_sha256", "source_sha256"])?;
2271    if source_sha256 != expected_source {
2272        return Err("result source digest conflicts with current source".to_owned());
2273    }
2274    let run_id = required_string(object, "run_id")?;
2275    let job_id = required_string(object, "job_id")?;
2276    if !valid_identity(&run_id) || !valid_identity(&job_id) {
2277        return Err("result Run/Job binding is malformed".to_owned());
2278    }
2279    for key in ["worker_id", "worker", "assignment_generation", "generation"] {
2280        if let Some(value) = object.get(key)
2281            && !value.is_null()
2282            && value.as_str().filter(|value| !value.is_empty()).is_none()
2283        {
2284            return Err(format!("result {key} binding is malformed"));
2285        }
2286    }
2287    if let Some(values) = object
2288        .get("obligation_outcomes")
2289        .or_else(|| object.get("obligations"))
2290    {
2291        let scope = scope.ok_or("result obligation outcomes have no frozen scope authority")?;
2292        let values = values
2293            .as_array()
2294            .ok_or("result obligation outcomes are malformed")?;
2295        let ids = values
2296            .iter()
2297            .filter_map(|value| {
2298                value
2299                    .get("obligation_id")
2300                    .or_else(|| value.get("id"))
2301                    .and_then(Value::as_str)
2302            })
2303            .collect::<Vec<_>>();
2304        if ids.len() != scope.obligations.len()
2305            || ids
2306                .iter()
2307                .any(|id| !scope.obligations.iter().any(|(expected, _)| expected == id))
2308            || ids.iter().collect::<BTreeSet<_>>().len() != ids.len()
2309        {
2310            return Err("result obligation outcomes do not exactly bind frozen scope".to_owned());
2311        }
2312        let mut outcome_ids = BTreeSet::new();
2313        for value in values {
2314            let outcome = value
2315                .as_object()
2316                .ok_or("result obligation outcome is malformed")?;
2317            let id = required_string_any(outcome, &["obligation_id", "id"])?;
2318            if !outcome_ids.insert(id) {
2319                return Err("duplicate result obligation outcome".to_owned());
2320            }
2321            let raw_status = required_string_any(outcome, &["status", "outcome"])?;
2322            if parse_evidence_exact(&raw_status).is_none() {
2323                return Err("result obligation outcome status is unknown".to_owned());
2324            }
2325        }
2326    }
2327    Ok(ResultView {
2328        payload: payload.clone(),
2329        verdict,
2330        candidate_sha256: candidate_sha256.to_owned(),
2331        source_sha256,
2332        run_id,
2333        job_id,
2334        worker_id: optional_string_any_checked(object, &["worker_id", "worker"])?,
2335        assignment_generation: optional_string_any_checked(
2336            object,
2337            &["assignment_generation", "generation"],
2338        )?,
2339        completion_authority: object
2340            .get("completion_authority")
2341            .map(parse_completion_authority)
2342            .transpose()?,
2343        result_digest: sha256_json(payload),
2344    })
2345}
2346
2347fn parse_completion_authority(value: &Value) -> Result<String, String> {
2348    let object = value
2349        .as_object()
2350        .ok_or("result completion authority is malformed")?;
2351    reject_unknown_keys(
2352        object,
2353        &["authority_ready", "may_claim_complete"],
2354        "result completion authority",
2355    )?;
2356    if !object.get("authority_ready").is_some_and(Value::is_boolean)
2357        || !object
2358            .get("may_claim_complete")
2359            .is_some_and(Value::is_boolean)
2360    {
2361        return Err("result completion authority is malformed".to_owned());
2362    }
2363    Ok(display_json(value))
2364}
2365
2366#[derive(Debug, Clone)]
2367struct ReportView {
2368    id: String,
2369    digest: String,
2370    candidate_sha256: String,
2371    source_sha256: String,
2372    verdict: NativeVerdict,
2373    created_at: Option<String>,
2374    provenance: String,
2375    evidence_summary: Option<String>,
2376}
2377
2378impl AuthoritativeReportView {
2379    #[allow(clippy::too_many_arguments)]
2380    pub fn try_from_value(
2381        value: &Value,
2382        candidate: &str,
2383        source: Option<&str>,
2384        job: Option<&AuthoritativeJobView>,
2385        run: Option<&AuthoritativeRunView>,
2386        result: Option<&AuthoritativeResultView>,
2387    ) -> Result<Self, String> {
2388        let result = result.ok_or("report requires a validated parent Result")?;
2389        let job = job.ok_or("report requires a validated Job binding")?;
2390        let run = run.ok_or("report requires a validated Run binding")?;
2391        let source = source.ok_or("report requires a trusted source binding")?;
2392        if result.candidate_sha256() != candidate
2393            || result.source_sha256() != source
2394            || result.job_id() != job.id()
2395            || result.run_id() != run.id()
2396        {
2397            return Err("report parent Result bindings are not exact".to_owned());
2398        }
2399        let result_view = ResultView {
2400            payload: Value::Null,
2401            verdict: result.verdict,
2402            candidate_sha256: result.candidate_sha256.clone(),
2403            source_sha256: result.source_sha256.clone(),
2404            run_id: result.run_id.clone(),
2405            job_id: result.job_id.clone(),
2406            worker_id: None,
2407            assignment_generation: None,
2408            completion_authority: None,
2409            result_digest: result.result_digest.clone(),
2410        };
2411        let report = parse_report_value(
2412            value,
2413            Some(candidate),
2414            Some(source),
2415            Some(job),
2416            Some(run),
2417            Some(&result_view),
2418        )?;
2419        let object = value
2420            .as_object()
2421            .ok_or("report authority is not an object")?;
2422        Ok(Self {
2423            id: report.id,
2424            report_digest: report.digest,
2425            candidate_sha256: report.candidate_sha256,
2426            source_sha256: report.source_sha256,
2427            verdict: report.verdict,
2428            job_id: required_string(object, "job_id")?,
2429            run_id: required_string(object, "run_id")?,
2430            result_digest: required_digest_any(object, &["result_digest"])?,
2431        })
2432    }
2433
2434    #[allow(clippy::too_many_arguments)]
2435    pub fn from_value(
2436        value: &Value,
2437        candidate: &str,
2438        source: Option<&str>,
2439        job: Option<&AuthoritativeJobView>,
2440        run: Option<&AuthoritativeRunView>,
2441        result: Option<&AuthoritativeResultView>,
2442    ) -> Result<Self, String> {
2443        Self::try_from_value(value, candidate, source, job, run, result)
2444    }
2445}
2446
2447fn latest_typed_report(
2448    events: &[Event],
2449    candidate: Option<&str>,
2450    source: Option<&str>,
2451    job: Option<&AuthoritativeJobView>,
2452    run: Option<&AuthoritativeRunView>,
2453    result: Option<&ResultView>,
2454) -> Option<ReportView> {
2455    let value = unique_latest(events, |event| {
2456        checkpoint_object(event, &["report_authority"], "report")
2457    })?;
2458    let result = result?;
2459    let job = job?;
2460    let run = run?;
2461    let source = source?;
2462    let object = value.as_object()?;
2463    reject_unknown_keys(
2464        object,
2465        &[
2466            "report_id",
2467            "id",
2468            "report_digest",
2469            "digest",
2470            "candidate_sha256",
2471            "source_sha256",
2472            "authoritative_source_sha256",
2473            "verdict",
2474            "job_id",
2475            "run_id",
2476            "result_digest",
2477            "provenance",
2478            "authority_source",
2479            "created_at",
2480            "created_at_ms",
2481            "key_evidence_summary",
2482            "summary",
2483        ],
2484        "report authority",
2485    )
2486    .ok()?;
2487    let id = required_string_any(object, &["report_id", "id"]).ok()?;
2488    if !valid_identity(&id) {
2489        return None;
2490    }
2491    let digest = required_report_digest(object).ok()?;
2492    let report_candidate = required_digest_any(object, &["candidate_sha256"]).ok()?;
2493    if candidate != Some(report_candidate.as_str()) {
2494        return None;
2495    }
2496    let report_source =
2497        required_digest_any(object, &["source_sha256", "authoritative_source_sha256"]).ok()?;
2498    if report_source != source {
2499        return None;
2500    }
2501    if result.candidate_sha256 != report_candidate
2502        || result.source_sha256 != report_source
2503        || result.job_id != job.id
2504        || result.run_id != run.id
2505    {
2506        return None;
2507    }
2508    let raw_verdict = object.get("verdict").and_then(Value::as_str)?;
2509    let verdict = parse_verdict_exact(raw_verdict)?;
2510    if result.verdict != verdict {
2511        return None;
2512    }
2513    if !matches!(raw_verdict, "accepted" | "failed" | "insufficient_evidence") {
2514        return None;
2515    }
2516    let job_id = optional_string(object, "job_id");
2517    if job_id.as_deref() != Some(job.id.as_str()) {
2518        return None;
2519    }
2520    let run_id = optional_string(object, "run_id");
2521    if run_id.as_deref() != Some(run.id.as_str()) {
2522        return None;
2523    }
2524    if object.get("result_digest").and_then(Value::as_str) != Some(result.result_digest.as_str()) {
2525        return None;
2526    }
2527    let provenance = required_string_any(object, &["provenance", "authority_source"]).ok()?;
2528    if !matches!(
2529        provenance.as_str(),
2530        "canonical" | "falsegreen_core" | "falsegreen"
2531    ) {
2532        return None;
2533    }
2534    Some(ReportView {
2535        id,
2536        digest,
2537        candidate_sha256: report_candidate,
2538        source_sha256: report_source,
2539        verdict,
2540        created_at: optional_string_any_checked(object, &["created_at", "created_at_ms"]).ok()?,
2541        provenance,
2542        evidence_summary: optional_string_any_checked(object, &["key_evidence_summary", "summary"])
2543            .ok()?,
2544    })
2545}
2546
2547fn parse_report_value(
2548    value: &Value,
2549    candidate: Option<&str>,
2550    source: Option<&str>,
2551    job: Option<&AuthoritativeJobView>,
2552    run: Option<&AuthoritativeRunView>,
2553    result: Option<&ResultView>,
2554) -> Result<ReportView, String> {
2555    let object = value
2556        .as_object()
2557        .ok_or("report authority is not an object")?;
2558    reject_unknown_keys(
2559        object,
2560        &[
2561            "report_id",
2562            "id",
2563            "report_digest",
2564            "digest",
2565            "candidate_sha256",
2566            "source_sha256",
2567            "authoritative_source_sha256",
2568            "verdict",
2569            "job_id",
2570            "run_id",
2571            "result_digest",
2572            "provenance",
2573            "authority_source",
2574            "created_at",
2575            "created_at_ms",
2576            "key_evidence_summary",
2577            "summary",
2578        ],
2579        "report authority",
2580    )?;
2581    let id = required_string_any(object, &["report_id", "id"])?;
2582    if !valid_identity(&id) {
2583        return Err("report ID is malformed".to_owned());
2584    }
2585    let digest = required_report_digest(object)?;
2586    let report_candidate = required_digest_any(object, &["candidate_sha256"])?;
2587    if candidate != Some(report_candidate.as_str()) {
2588        return Err("report candidate does not match current candidate".to_owned());
2589    }
2590    let expected_source = source.ok_or("report requires a trusted source binding")?;
2591    let report_source =
2592        required_digest_any(object, &["source_sha256", "authoritative_source_sha256"])?;
2593    if report_source != expected_source {
2594        return Err("report source does not match current source".to_owned());
2595    }
2596    let raw_verdict = object
2597        .get("verdict")
2598        .and_then(Value::as_str)
2599        .ok_or("report verdict is missing")?;
2600    let verdict = parse_verdict_exact(raw_verdict).ok_or("report verdict is not canonical")?;
2601    let result = result.ok_or("report requires a validated parent Result")?;
2602    let job = job.ok_or("report requires a validated Job binding")?;
2603    let run = run.ok_or("report requires a validated Run binding")?;
2604    if result.candidate_sha256 != report_candidate
2605        || result.source_sha256 != report_source
2606        || result.job_id != job.id
2607        || result.run_id != run.id
2608    {
2609        return Err("report parent Result bindings are not exact".to_owned());
2610    }
2611    if result.verdict != verdict {
2612        return Err("report verdict conflicts with result".to_owned());
2613    }
2614    let job_id = required_string(object, "job_id")?;
2615    if job.id != job_id {
2616        return Err("report Job binding conflicts".to_owned());
2617    }
2618    let run_id = required_string(object, "run_id")?;
2619    if run.id != run_id {
2620        return Err("report Run binding conflicts".to_owned());
2621    }
2622    let expected = required_digest_any(object, &["result_digest"])?;
2623    if expected != result.result_digest {
2624        return Err("report result binding conflicts".to_owned());
2625    }
2626    let provenance = required_string_any(object, &["provenance", "authority_source"])?;
2627    if !matches!(
2628        provenance.as_str(),
2629        "canonical" | "falsegreen_core" | "falsegreen"
2630    ) {
2631        return Err("report provenance is not a known authority source".to_owned());
2632    }
2633    Ok(ReportView {
2634        id,
2635        digest,
2636        candidate_sha256: report_candidate,
2637        source_sha256: report_source,
2638        verdict,
2639        created_at: optional_string_any_checked(object, &["created_at", "created_at_ms"])?,
2640        provenance,
2641        evidence_summary: optional_string_any_checked(
2642            object,
2643            &["key_evidence_summary", "summary"],
2644        )?,
2645    })
2646}
2647
2648fn verification_from_result(
2649    result: &ResultView,
2650    scope: Option<&AuthoritativeFrozenScopeView>,
2651) -> NativeVerification {
2652    NativeVerification {
2653        verdict: result.verdict,
2654        raw_verdict: NativeField::falsegreen(status_wire_verdict(result.verdict)),
2655        source_bound: true,
2656        candidate_sha256: NativeField::falsegreen(result.candidate_sha256.clone()),
2657        authoritative_source_sha256: Some(result.source_sha256.as_str()).map_or_else(
2658            || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2659            NativeField::falsegreen,
2660        ),
2661        result_digest: NativeField::falsegreen(result.result_digest.clone()),
2662        report_reference: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2663        completion_authority: result.completion_authority.as_deref().map_or_else(
2664            || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2665            NativeField::falsegreen,
2666        ),
2667        obligation_outcomes: parse_obligation_outcomes(&result.payload, scope),
2668        certification_note: "Canonical result is displayed exactly; this read-only surface cannot certify or override it.".to_owned(),
2669        binding_error: None,
2670    }
2671}
2672
2673fn unavailable_verification() -> NativeVerification {
2674    NativeVerification {
2675        verdict: NativeVerdict::Unknown,
2676        raw_verdict: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2677        source_bound: false,
2678        candidate_sha256: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2679        authoritative_source_sha256: NativeField::unavailable(
2680            AuthorityClass::AuthoritativeFalseGreenState,
2681        ),
2682        result_digest: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2683        report_reference: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2684        completion_authority: NativeField::unavailable(
2685            AuthorityClass::AuthoritativeFalseGreenState,
2686        ),
2687        obligation_outcomes: Vec::new(),
2688        certification_note: "FalseGreen state unavailable: no validated result authority."
2689            .to_owned(),
2690        binding_error: Some("no validated result authority".to_owned()),
2691    }
2692}
2693
2694fn unavailable_job() -> NativeJob {
2695    NativeJob {
2696        id: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2697        status: NativeStatus::Unknown,
2698        status_raw: NativeField::unavailable(AuthorityClass::DiagnosticNonAuthoritativeMetadata),
2699        task_id: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2700        source_sha256: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2701        candidate_sha256: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2702        entitlement_consumption: NativeField::unavailable(
2703            AuthorityClass::AuthoritativeAgentSessionState,
2704        ),
2705        run_ids: Vec::new(),
2706        authority_decision: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2707    }
2708}
2709
2710fn unavailable_run() -> NativeRun {
2711    NativeRun {
2712        id: NativeField::unavailable(AuthorityClass::AuthoritativeAgentSessionState),
2713        worker_id: NativeField::unavailable(AuthorityClass::AuthoritativeAgentSessionState),
2714        job_id: NativeField::unavailable(AuthorityClass::AuthoritativeAgentSessionState),
2715        assignment_generation: NativeField::unavailable(
2716            AuthorityClass::AuthoritativeAgentSessionState,
2717        ),
2718        status: NativeStatus::Unknown,
2719        status_raw: NativeField::unavailable(AuthorityClass::DiagnosticNonAuthoritativeMetadata),
2720        execution_started_at: NativeField::unavailable(
2721            AuthorityClass::AuthoritativeAgentSessionState,
2722        ),
2723        package_runtime_identity: NativeField::unavailable(
2724            AuthorityClass::AuthoritativeAgentSessionState,
2725        ),
2726        recovery_lease: NativeField::unavailable(AuthorityClass::AuthoritativeAgentSessionState),
2727    }
2728}
2729
2730fn unavailable_scope() -> NativeFrozenScope {
2731    NativeFrozenScope {
2732        scope_digest: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2733        frozen: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2734        obligations: Vec::new(),
2735        distinction:
2736            "FalseGreen frozen-scope authority unavailable; no obligation is displayed as frozen."
2737                .to_owned(),
2738    }
2739}
2740
2741fn unavailable_report(verification: &NativeVerification) -> NativeReport {
2742    NativeReport {
2743        id: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2744        report_digest: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2745        candidate_sha256: verification.candidate_sha256.clone(),
2746        source_sha256: verification.authoritative_source_sha256.clone(),
2747        verdict: NativeVerdict::Unknown,
2748        key_evidence_summary: NativeField::unavailable(
2749            AuthorityClass::AuthoritativeFalseGreenState,
2750        ),
2751        created_at: NativeField::unavailable(AuthorityClass::AuthoritativeAgentSessionState),
2752        provenance: NativeField::diagnostic(
2753            "Report: Unavailable; no real authoritative report object",
2754        ),
2755    }
2756}
2757
2758fn report_from_view(view: &ReportView) -> NativeReport {
2759    NativeReport {
2760        id: NativeField::falsegreen(view.id.clone()),
2761        report_digest: NativeField::falsegreen(view.digest.clone()),
2762        candidate_sha256: NativeField::falsegreen(view.candidate_sha256.clone()),
2763        source_sha256: Some(view.source_sha256.as_str()).map_or_else(
2764            || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2765            NativeField::falsegreen,
2766        ),
2767        verdict: view.verdict,
2768        key_evidence_summary: view.evidence_summary.as_deref().map_or_else(
2769            || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2770            NativeField::falsegreen,
2771        ),
2772        created_at: view.created_at.as_deref().map_or_else(
2773            || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2774            NativeField::falsegreen,
2775        ),
2776        provenance: Some(view.provenance.as_str()).map_or_else(
2777            || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2778            NativeField::falsegreen,
2779        ),
2780    }
2781}
2782
2783fn evidence_from_result(
2784    result: &ResultView,
2785    scope: Option<&AuthoritativeFrozenScopeView>,
2786    current_job_id: Option<&str>,
2787    current_run_id: Option<&str>,
2788    current_source: Option<&str>,
2789) -> NativeEvidence {
2790    let Some(scope) = scope else {
2791        return unavailable_evidence("Evidence unavailable: frozen scope authority is missing.");
2792    };
2793    let Some(values) = result
2794        .payload
2795        .get("evidence")
2796        .map(strict_evidence_array)
2797        .transpose()
2798        .ok()
2799        .flatten()
2800        .flatten()
2801    else {
2802        return unavailable_evidence(
2803            "Evidence unavailable: result supplied no exact evidence array.",
2804        );
2805    };
2806    let declared_rows = result
2807        .payload
2808        .get("evidence")
2809        .and_then(|value| {
2810            value
2811                .get("declared_rows")
2812                .or_else(|| value.get("declared_total"))
2813        })
2814        .and_then(Value::as_u64)
2815        .and_then(|value| usize::try_from(value).ok());
2816    if result
2817        .payload
2818        .get("evidence")
2819        .is_some_and(evidence_is_partial)
2820    {
2821        return unavailable_evidence_with_counts(
2822            "Evidence unavailable: upstream evidence set is explicitly partial or truncated.",
2823            values.len(),
2824            0,
2825            declared_rows,
2826            [0; 4],
2827        );
2828    }
2829    if values.len() > MAX_EVIDENCE_ITEMS {
2830        return unavailable_evidence_with_counts(
2831            "Evidence unavailable: upstream evidence set exceeds validation bound.",
2832            values.len(),
2833            0,
2834            declared_rows,
2835            [0; 4],
2836        );
2837    }
2838    let mut ids = BTreeSet::new();
2839    let mut duplicate_ids = Vec::new();
2840    let mut items = Vec::with_capacity(values.len().min(MAX_DISPLAY_EVIDENCE));
2841    let mut counts = [0usize; 4];
2842    let expected_job_id = Some(result.job_id.as_str()).or(current_job_id);
2843    let expected_run_id = Some(result.run_id.as_str()).or(current_run_id);
2844    let expected_source = Some(result.source_sha256.as_str()).or(current_source);
2845    let mut invalid_rows = 0usize;
2846    for (index, value) in values.iter().enumerate() {
2847        let parsed = parse_evidence_item(
2848            value,
2849            scope,
2850            expected_job_id,
2851            expected_run_id,
2852            expected_source,
2853            Some(result.result_digest.as_str()),
2854        );
2855        let Ok((id, item, state)) = parsed else {
2856            invalid_rows += 1;
2857            continue;
2858        };
2859        if !ids.insert(id.clone()) {
2860            duplicate_ids.push(id);
2861        }
2862        match state {
2863            EvidenceState::Passed => counts[0] += 1,
2864            EvidenceState::Failed => counts[1] += 1,
2865            EvidenceState::Insufficient => counts[2] += 1,
2866            EvidenceState::Unknown => counts[3] += 1,
2867        }
2868        if index < MAX_DISPLAY_EVIDENCE {
2869            items.push(item);
2870        }
2871    }
2872    duplicate_ids.sort();
2873    duplicate_ids.dedup();
2874    if !duplicate_ids.is_empty() {
2875        return NativeEvidence {
2876            // A duplicate ID invalidates the whole authority set. Keep the
2877            // exact diagnostic/counts, but do not style any row as trusted.
2878            items: Vec::new(),
2879            complete: false,
2880            duplicate_ids: duplicate_ids.clone(),
2881            unavailable_reason: Some(format!(
2882                "Duplicate evidence ID(s): {}",
2883                duplicate_ids.join(", ")
2884            )),
2885            total: values.len(),
2886            passed: counts[0],
2887            failed: counts[1],
2888            insufficient: counts[2],
2889            unknown: counts[3],
2890            observed_rows: values.len(),
2891            invalid_rows: invalid_rows + duplicate_ids.len(),
2892            declared_rows,
2893        };
2894    }
2895    if invalid_rows > 0 {
2896        return NativeEvidence {
2897            items: Vec::new(),
2898            complete: false,
2899            duplicate_ids,
2900            unavailable_reason: Some(format!(
2901                "Evidence unavailable: {invalid_rows} invalid row(s); diagnostic counts preserve observed data"
2902            )),
2903            total: values.len(),
2904            passed: counts[0],
2905            failed: counts[1],
2906            insufficient: counts[2],
2907            unknown: counts[3],
2908            observed_rows: values.len(),
2909            invalid_rows,
2910            declared_rows,
2911        };
2912    }
2913    NativeEvidence {
2914        items,
2915        complete: true,
2916        duplicate_ids,
2917        unavailable_reason: (values.len() > MAX_DISPLAY_EVIDENCE).then(|| {
2918            format!(
2919                "Showing {} of {} validated evidence items",
2920                MAX_DISPLAY_EVIDENCE,
2921                values.len()
2922            )
2923        }),
2924        total: values.len(),
2925        passed: counts[0],
2926        failed: counts[1],
2927        insufficient: counts[2],
2928        unknown: counts[3],
2929        observed_rows: values.len(),
2930        invalid_rows: 0,
2931        declared_rows,
2932    }
2933}
2934
2935fn unavailable_evidence(reason: &str) -> NativeEvidence {
2936    unavailable_evidence_with_counts(reason, 0, 0, None, [0; 4])
2937}
2938
2939fn unavailable_evidence_with_counts(
2940    reason: &str,
2941    observed_rows: usize,
2942    invalid_rows: usize,
2943    declared_rows: Option<usize>,
2944    counts: [usize; 4],
2945) -> NativeEvidence {
2946    NativeEvidence {
2947        items: Vec::new(),
2948        complete: false,
2949        duplicate_ids: Vec::new(),
2950        unavailable_reason: Some(reason.to_owned()),
2951        total: observed_rows,
2952        passed: counts[0],
2953        failed: counts[1],
2954        insufficient: counts[2],
2955        unknown: counts[3],
2956        observed_rows,
2957        invalid_rows,
2958        declared_rows,
2959    }
2960}
2961
2962fn strict_evidence_array(value: &Value) -> Result<Option<&Vec<Value>>, String> {
2963    if let Some(values) = value.as_array() {
2964        return Ok(Some(values));
2965    }
2966    let Some(object) = value.as_object() else {
2967        return Ok(None);
2968    };
2969    let mut selected: Option<&Vec<Value>> = None;
2970    for key in ["items", "evidence", "obligations"] {
2971        let Some(candidate) = object.get(key) else {
2972            continue;
2973        };
2974        let values = candidate
2975            .as_array()
2976            .ok_or_else(|| format!("evidence alias {key} is malformed"))?;
2977        if selected.is_some_and(|existing| existing != values) {
2978            return Err("conflicting evidence array aliases".to_owned());
2979        }
2980        selected = Some(values);
2981    }
2982    Ok(selected)
2983}
2984
2985fn evidence_is_partial(value: &Value) -> bool {
2986    value.get("partial").and_then(Value::as_bool) == Some(true)
2987        || value.get("truncated").and_then(Value::as_bool) == Some(true)
2988}
2989
2990fn parse_evidence_item(
2991    value: &Value,
2992    scope: &AuthoritativeFrozenScopeView,
2993    expected_job_id: Option<&str>,
2994    expected_run_id: Option<&str>,
2995    expected_source: Option<&str>,
2996    expected_result_digest: Option<&str>,
2997) -> Result<(String, NativeEvidenceItem, EvidenceState), String> {
2998    let object = value.as_object().ok_or("evidence item is not an object")?;
2999    reject_unknown_keys(
3000        object,
3001        &[
3002            "evidence_id",
3003            "id",
3004            "obligation_id",
3005            "job_id",
3006            "run_id",
3007            "source_sha256",
3008            "source_digest",
3009            "result_digest",
3010            "provenance",
3011            "source",
3012            "evidence_type",
3013            "type",
3014            "status",
3015            "state",
3016            "outcome",
3017            "detail",
3018            "value",
3019            "message",
3020            "reference",
3021            "uri",
3022            "path",
3023        ],
3024        "evidence item",
3025    )?;
3026    let id = required_string_any(object, &["evidence_id", "id"])?;
3027    if !valid_identity(&id) {
3028        return Err("evidence ID is malformed".to_owned());
3029    }
3030    let obligation = required_string_any(object, &["obligation_id"])?;
3031    if !valid_identity(&obligation) {
3032        return Err("evidence obligation ID is malformed".to_owned());
3033    }
3034    if !scope
3035        .obligations
3036        .iter()
3037        .any(|(expected, _)| expected == &obligation)
3038    {
3039        return Err("evidence obligation is not in frozen scope".to_owned());
3040    }
3041    if let Some(job_id) = object.get("job_id").and_then(Value::as_str)
3042        && expected_job_id.is_some()
3043        && expected_job_id != Some(job_id)
3044    {
3045        return Err("evidence Job binding conflicts".to_owned());
3046    }
3047    if let Some(run_id) = object.get("run_id").and_then(Value::as_str)
3048        && expected_run_id.is_some()
3049        && expected_run_id != Some(run_id)
3050    {
3051        return Err("evidence Run binding conflicts".to_owned());
3052    }
3053    if let Some(source_digest) = optional_digest_any(object, &["source_sha256", "source_digest"])?
3054        && expected_source.is_some()
3055        && expected_source != Some(source_digest.as_str())
3056    {
3057        return Err("evidence source digest conflicts".to_owned());
3058    }
3059    if let Some(result_digest) = object.get("result_digest") {
3060        let result_digest = result_digest
3061            .as_str()
3062            .filter(|value| valid_digest(value))
3063            .ok_or("evidence result digest is malformed")?;
3064        if expected_result_digest.is_some() && expected_result_digest != Some(result_digest) {
3065            return Err("evidence result digest conflicts".to_owned());
3066        }
3067    }
3068    let source = required_string_any(object, &["provenance", "source"])?;
3069    if !matches!(
3070        source.as_str(),
3071        "canonical" | "falsegreen_core" | "falsegreen"
3072    ) {
3073        return Err("evidence provenance is not a known authority source".to_owned());
3074    }
3075    let evidence_type = required_string_any(object, &["evidence_type", "type"])?;
3076    if !matches!(
3077        evidence_type.as_str(),
3078        "test" | "gate" | "artifact" | "log" | "report"
3079    ) {
3080        return Err("evidence type is unknown".to_owned());
3081    }
3082    let raw_state = required_string_any(object, &["status", "state", "outcome"])?;
3083    let state = parse_evidence_exact(&raw_state).ok_or("evidence status is unknown")?;
3084    let detail = strict_display_alias(object, &["detail", "value", "message"])?
3085        .unwrap_or_else(|| "Unavailable".to_owned());
3086    let reference = optional_string_any_checked(object, &["reference", "uri", "path"])?
3087        .unwrap_or_else(|| "Unavailable".to_owned());
3088    let item = NativeEvidenceItem {
3089        evidence_id: NativeField::falsegreen(id.clone()),
3090        obligation: NativeField::falsegreen(obligation),
3091        source: NativeField::falsegreen(source),
3092        evidence_type: NativeField::falsegreen(evidence_type),
3093        reference: NativeField::falsegreen(reference),
3094        state,
3095        detail: NativeField::falsegreen(detail),
3096    };
3097    Ok((id, item, state))
3098}
3099
3100fn recovery_unknown(run: &NativeRun) -> NativeRecovery {
3101    NativeRecovery {
3102        state: RecoveryState::Unknown,
3103        reason: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
3104        pre_begin_claim: if run.status == NativeStatus::Claimed
3105            && run.execution_started_at.value.is_none()
3106        {
3107            NativeField::agent("pre_begin")
3108        } else {
3109            NativeField::unavailable(AuthorityClass::AuthoritativeAgentSessionState)
3110        },
3111        action_available: false,
3112        distinction: "Recovery classification is unavailable without an explicit recovery-authority object; no inference is made from CLAIMED, leases, terminal reasons, or result pointers.".to_owned(),
3113    }
3114}
3115
3116fn recovery_from_view(view: &AuthoritativeRecoveryView) -> NativeRecovery {
3117    NativeRecovery {
3118        state: view.state,
3119        reason: view.reason.as_deref().map_or_else(
3120            || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
3121            NativeField::falsegreen,
3122        ),
3123        pre_begin_claim: NativeField::unavailable(AuthorityClass::AuthoritativeAgentSessionState),
3124        action_available: false,
3125        distinction: "Recovery is read-only state. No retry, recover, repair, resume, or mutation action is exposed.".to_owned(),
3126    }
3127}
3128
3129fn replacement_from_capture(
3130    capture: &Capture,
3131    verification: &NativeVerification,
3132) -> NativeReplacement {
3133    let Some(relation) = &capture.relation else {
3134        return NativeReplacement {
3135            predecessor_session: NativeField::unavailable(
3136                AuthorityClass::AuthoritativeAgentSessionState,
3137            ),
3138            replacement_session: NativeField::unavailable(
3139                AuthorityClass::AuthoritativeAgentSessionState,
3140            ),
3141            predecessor_state: NativeField::unavailable(
3142                AuthorityClass::AuthoritativeAgentSessionState,
3143            ),
3144            continuity: NativeField::diagnostic("No replacement relation for this session"),
3145            candidate_sha256: verification.candidate_sha256.clone(),
3146            workspace_state: NativeField::unavailable(
3147                AuthorityClass::AuthoritativeAgentSessionState,
3148            ),
3149            terminal_outcome: NativeField::unavailable(
3150                AuthorityClass::AuthoritativeAgentSessionState,
3151            ),
3152        };
3153    };
3154    let source_events = if relation.predecessor_session_id == capture.session.id {
3155        &capture.events
3156    } else {
3157        &capture.related_events
3158    };
3159    let replacement_events = if relation.predecessor_session_id == capture.session.id {
3160        &capture.related_events
3161    } else {
3162        &capture.events
3163    };
3164    let validated_replacement = ValidatedReplacementSessionView::try_from_record_with_related(
3165        relation,
3166        source_events,
3167        replacement_events,
3168    )
3169    .ok();
3170    let bound_git = source_events.iter().find(|event| {
3171        event.sequence == relation.source_git_state_sequence && event.kind == EventKind::GitState
3172    });
3173    let candidate_ok = source_events.iter().any(|event| {
3174        event.kind == EventKind::CandidateReady
3175            && event.sequence == relation.candidate_event_sequence
3176            && event.payload["candidate_sha256"].as_str()
3177                == Some(relation.candidate_sha256.as_str())
3178    });
3179    let workspace = bound_git
3180        .and_then(|event| event.payload.get("workspace_state_sha256"))
3181        .and_then(Value::as_str)
3182        .filter(|value| valid_digest(value));
3183    let continuity = if validated_replacement.is_some() && bound_git.is_some() && candidate_ok {
3184        NativeField::agent("candidate and exact relation-bound GitState are continuous")
3185    } else {
3186        NativeField::diagnostic(
3187            "Replacement provenance unavailable: relation-bound candidate/GitState sequence is missing or conflicting",
3188        )
3189    };
3190    let terminal = source_events
3191        .iter()
3192        .rev()
3193        .find(|event| event.kind == EventKind::TerminalState)
3194        .map(|event| display_json(&event.payload));
3195    NativeReplacement {
3196        predecessor_session: NativeField::agent(relation.predecessor_session_id.clone()),
3197        replacement_session: NativeField::agent(relation.replacement_session_id.clone()),
3198        predecessor_state: NativeField::agent(relation.predecessor_state.clone()),
3199        continuity,
3200        candidate_sha256: NativeField::agent(relation.candidate_sha256.clone()),
3201        workspace_state: workspace.map_or_else(
3202            || NativeField::unavailable(AuthorityClass::AuthoritativeAgentSessionState),
3203            NativeField::agent,
3204        ),
3205        terminal_outcome: terminal.map_or_else(
3206            || NativeField::unavailable(AuthorityClass::AuthoritativeAgentSessionState),
3207            NativeField::agent,
3208        ),
3209    }
3210}
3211
3212fn candidate_context(events: &[Event]) -> Option<String> {
3213    let mut selected: Option<String> = None;
3214    for event in events
3215        .iter()
3216        .filter(|event| event.kind == EventKind::CandidateReady)
3217    {
3218        let value = event
3219            .payload
3220            .get("candidate_sha256")
3221            .and_then(Value::as_str)
3222            .filter(|value| valid_digest(value))?;
3223        if selected
3224            .as_deref()
3225            .is_some_and(|existing| existing != value)
3226        {
3227            return None;
3228        }
3229        selected = Some(value.to_owned());
3230    }
3231    selected
3232}
3233
3234fn source_context(events: &[Event]) -> Option<String> {
3235    let mut selected: Option<String> = None;
3236    for event in events
3237        .iter()
3238        .filter(|event| event.kind == EventKind::GitState)
3239    {
3240        let object = event.payload.as_object()?;
3241        let value =
3242            optional_digest_any(object, &["source_sha256", "authoritative_source_sha256"]).ok()?;
3243        let Some(value) = value else {
3244            continue;
3245        };
3246        if selected
3247            .as_deref()
3248            .is_some_and(|existing| existing != value)
3249        {
3250            return None;
3251        }
3252        selected = Some(value);
3253    }
3254    selected
3255}
3256
3257fn build_surfaces(snapshot: &NativeSnapshotIdentity, facts: &Facts) -> NativeSurfaceSet {
3258    NativeSurfaceSet {
3259        task: task_surface(snapshot, facts),
3260        frozen_scope: scope_surface(snapshot, facts),
3261        job: job_surface(snapshot, facts),
3262        run: run_surface(snapshot, facts),
3263        verification_result: verification_surface(snapshot, facts),
3264        evidence: evidence_surface(snapshot, facts),
3265        report: report_surface(snapshot, facts),
3266        failure_recovery: recovery_surface(snapshot, facts),
3267        replacement_session: replacement_surface(snapshot, facts),
3268    }
3269}
3270
3271fn base_surface(
3272    snapshot: &NativeSnapshotIdentity,
3273    family: NativeSurfaceFamily,
3274    root: Component,
3275) -> Surface {
3276    let mut surface = Surface::new(
3277        format!(
3278            "native-{}-{}-{}",
3279            family.as_str(),
3280            safe_id(&snapshot.source_session_id),
3281            &snapshot.digest[..12]
3282        ),
3283        root,
3284    );
3285    surface
3286        .metadata
3287        .insert("surface_family".to_owned(), family.as_str().to_owned());
3288    surface.metadata.insert(
3289        "snapshot_source_session".to_owned(),
3290        snapshot.source_session_id.clone(),
3291    );
3292    surface.metadata.insert(
3293        "snapshot_state_generation".to_owned(),
3294        snapshot.state_generation.to_string(),
3295    );
3296    surface
3297        .metadata
3298        .insert("snapshot_digest".to_owned(), snapshot.digest.clone());
3299    surface
3300        .metadata
3301        .insert("execution".to_owned(), "read_only_no_actions".to_owned());
3302    surface
3303}
3304
3305fn label(name: &str, field: &NativeField, unknown: bool) -> String {
3306    format!(
3307        "{} [{}]: {}",
3308        name,
3309        field.authority.label(),
3310        field.display(unknown)
3311    )
3312}
3313
3314fn task_surface(snapshot: &NativeSnapshotIdentity, facts: &Facts) -> Surface {
3315    let entries = BTreeMap::from([
3316        ("task".to_owned(), label("Task", &facts.task.task_id, false)),
3317        (
3318            "lifecycle".to_owned(),
3319            label("Lifecycle", &facts.task.lifecycle, false),
3320        ),
3321        (
3322            "frozen".to_owned(),
3323            label("Frozen scope", &facts.task.frozen, false),
3324        ),
3325        (
3326            "scope_digest".to_owned(),
3327            label("Scope digest", &facts.task.scope_digest, false),
3328        ),
3329        (
3330            "scope".to_owned(),
3331            label("Scope", &facts.task.scope_summary, false),
3332        ),
3333        (
3334            "source".to_owned(),
3335            label("Source", &facts.task.source_sha256, false),
3336        ),
3337        (
3338            "candidate".to_owned(),
3339            label("Candidate", &facts.task.candidate_sha256, false),
3340        ),
3341        (
3342            "job".to_owned(),
3343            label("Current Job", &facts.task.current_job_id, false),
3344        ),
3345    ]);
3346    base_surface(
3347        snapshot,
3348        NativeSurfaceFamily::Task,
3349        Component {
3350            id: "task-root".to_owned(),
3351            kind: ComponentKind::Stack {
3352                children: vec![
3353                    Component {
3354                        id: "task-status".to_owned(),
3355                        kind: ComponentKind::Status {
3356                            label: "Task lifecycle".to_owned(),
3357                            value: facts.task.lifecycle.display(false),
3358                            level: StatusLevel::Info,
3359                        },
3360                    },
3361                    Component {
3362                        id: "task-facts".to_owned(),
3363                        kind: ComponentKind::KeyValue { entries },
3364                    },
3365                ],
3366            },
3367        },
3368    )
3369}
3370
3371fn scope_surface(snapshot: &NativeSnapshotIdentity, facts: &Facts) -> Surface {
3372    let mut rows = facts
3373        .frozen_scope
3374        .obligations
3375        .iter()
3376        .map(|item| {
3377            vec![
3378                item.id.display(false),
3379                item.requirement.display(false),
3380                item.requirement.authority.label().to_owned(),
3381            ]
3382        })
3383        .collect::<Vec<_>>();
3384    if rows.is_empty() {
3385        rows.push(vec![
3386            "Unavailable".to_owned(),
3387            "FalseGreen frozen-scope authority unavailable".to_owned(),
3388            facts.frozen_scope.scope_digest.authority.label().to_owned(),
3389        ]);
3390    }
3391    let entries = BTreeMap::from([
3392        (
3393            "scope_digest".to_owned(),
3394            label(
3395                "Immutable scope digest",
3396                &facts.frozen_scope.scope_digest,
3397                false,
3398            ),
3399        ),
3400        (
3401            "distinction".to_owned(),
3402            safe_text(&facts.frozen_scope.distinction),
3403        ),
3404    ]);
3405    base_surface(
3406        snapshot,
3407        NativeSurfaceFamily::FrozenScope,
3408        Component {
3409            id: "scope-root".to_owned(),
3410            kind: ComponentKind::Stack {
3411                children: vec![
3412                    Component {
3413                        id: "scope-status".to_owned(),
3414                        kind: ComponentKind::Status {
3415                            label: "Scope".to_owned(),
3416                            value: facts.frozen_scope.frozen.display(false),
3417                            level: StatusLevel::Info,
3418                        },
3419                    },
3420                    Component {
3421                        id: "scope-facts".to_owned(),
3422                        kind: ComponentKind::KeyValue { entries },
3423                    },
3424                    Component {
3425                        id: "scope-obligations".to_owned(),
3426                        kind: ComponentKind::Table {
3427                            columns: vec![
3428                                "Obligation".to_owned(),
3429                                "Requirement text".to_owned(),
3430                                "Authority".to_owned(),
3431                            ],
3432                            rows,
3433                        },
3434                    },
3435                ],
3436            },
3437        },
3438    )
3439}
3440
3441fn job_surface(snapshot: &NativeSnapshotIdentity, facts: &Facts) -> Surface {
3442    let entries = BTreeMap::from([
3443        ("job".to_owned(), label("Job", &facts.job.id, false)),
3444        ("task".to_owned(), label("Task", &facts.job.task_id, false)),
3445        (
3446            "source".to_owned(),
3447            label("Source", &facts.job.source_sha256, false),
3448        ),
3449        (
3450            "candidate".to_owned(),
3451            label("Candidate", &facts.job.candidate_sha256, false),
3452        ),
3453        (
3454            "entitlement".to_owned(),
3455            label(
3456                "Entitlement/consumption",
3457                &facts.job.entitlement_consumption,
3458                true,
3459            ),
3460        ),
3461        (
3462            "authority".to_owned(),
3463            label("Authority decision", &facts.job.authority_decision, true),
3464        ),
3465    ]);
3466    let runs = if facts.job.run_ids.is_empty() {
3467        vec![vec![
3468            "Unavailable".to_owned(),
3469            "diagnostic/non-authoritative metadata".to_owned(),
3470        ]]
3471    } else {
3472        facts
3473            .job
3474            .run_ids
3475            .iter()
3476            .map(|item| vec![item.display(false), item.authority.label().to_owned()])
3477            .collect()
3478    };
3479    base_surface(
3480        snapshot,
3481        NativeSurfaceFamily::Job,
3482        Component {
3483            id: "job-root".to_owned(),
3484            kind: ComponentKind::Stack {
3485                children: vec![
3486                    status_component(
3487                        "job-status",
3488                        "Job status",
3489                        facts.job.status,
3490                        &facts.job.status_raw,
3491                    ),
3492                    Component {
3493                        id: "job-facts".to_owned(),
3494                        kind: ComponentKind::KeyValue { entries },
3495                    },
3496                    Component {
3497                        id: "job-runs".to_owned(),
3498                        kind: ComponentKind::Table {
3499                            columns: vec!["Run".to_owned(), "Authority".to_owned()],
3500                            rows: runs,
3501                        },
3502                    },
3503                ],
3504            },
3505        },
3506    )
3507}
3508
3509fn run_surface(snapshot: &NativeSnapshotIdentity, facts: &Facts) -> Surface {
3510    let entries = BTreeMap::from([
3511        ("run".to_owned(), label("Run", &facts.run.id, false)),
3512        (
3513            "worker".to_owned(),
3514            label("Worker", &facts.run.worker_id, true),
3515        ),
3516        ("job".to_owned(), label("Job", &facts.run.job_id, false)),
3517        (
3518            "assignment_generation".to_owned(),
3519            label(
3520                "Assignment generation",
3521                &facts.run.assignment_generation,
3522                true,
3523            ),
3524        ),
3525        (
3526            "execution_started".to_owned(),
3527            label("Execution started", &facts.run.execution_started_at, true),
3528        ),
3529        (
3530            "package_runtime".to_owned(),
3531            label(
3532                "Package/runtime identity",
3533                &facts.run.package_runtime_identity,
3534                true,
3535            ),
3536        ),
3537        (
3538            "recovery_lease".to_owned(),
3539            label("Recovery/lease", &facts.run.recovery_lease, true),
3540        ),
3541    ]);
3542    base_surface(
3543        snapshot,
3544        NativeSurfaceFamily::Run,
3545        Component {
3546            id: "run-root".to_owned(),
3547            kind: ComponentKind::Stack {
3548                children: vec![
3549                    status_component(
3550                        "run-status",
3551                        "Run status",
3552                        facts.run.status,
3553                        &facts.run.status_raw,
3554                    ),
3555                    Component {
3556                        id: "run-facts".to_owned(),
3557                        kind: ComponentKind::KeyValue { entries },
3558                    },
3559                ],
3560            },
3561        },
3562    )
3563}
3564
3565fn verification_surface(snapshot: &NativeSnapshotIdentity, facts: &Facts) -> Surface {
3566    let mut entries = BTreeMap::from([
3567        (
3568            "verdict".to_owned(),
3569            format!(
3570                "VERDICT [{}]: {}",
3571                if facts.verification.source_bound {
3572                    AuthorityClass::AuthoritativeFalseGreenState.label()
3573                } else {
3574                    AuthorityClass::DiagnosticNonAuthoritativeMetadata.label()
3575                },
3576                facts.verification.verdict.label()
3577            ),
3578        ),
3579        (
3580            "raw".to_owned(),
3581            label(
3582                "Canonical raw status",
3583                &facts.verification.raw_verdict,
3584                true,
3585            ),
3586        ),
3587        (
3588            "source_bound".to_owned(),
3589            format!(
3590                "Source binding [{}]: {}",
3591                AuthorityClass::AuthoritativeFalseGreenState.label(),
3592                facts.verification.source_bound
3593            ),
3594        ),
3595        (
3596            "candidate".to_owned(),
3597            label("Candidate", &facts.verification.candidate_sha256, false),
3598        ),
3599        (
3600            "source".to_owned(),
3601            label(
3602                "Source",
3603                &facts.verification.authoritative_source_sha256,
3604                false,
3605            ),
3606        ),
3607        (
3608            "result_digest".to_owned(),
3609            label("Result digest", &facts.verification.result_digest, false),
3610        ),
3611        (
3612            "report".to_owned(),
3613            label(
3614                "Report reference",
3615                &facts.verification.report_reference,
3616                false,
3617            ),
3618        ),
3619        (
3620            "completion_authority".to_owned(),
3621            label(
3622                "Completion authority",
3623                &facts.verification.completion_authority,
3624                true,
3625            ),
3626        ),
3627    ]);
3628    if let Some(error) = &facts.verification.binding_error {
3629        entries.insert(
3630            "binding_error".to_owned(),
3631            format!(
3632                "Binding error [{}]: {}",
3633                AuthorityClass::DiagnosticNonAuthoritativeMetadata.label(),
3634                safe_text(error)
3635            ),
3636        );
3637    }
3638    let rows = if facts.verification.obligation_outcomes.is_empty() {
3639        vec![vec![
3640            "Unavailable".to_owned(),
3641            "UNKNOWN".to_owned(),
3642            "No validated obligation outcomes supplied".to_owned(),
3643        ]]
3644    } else {
3645        facts
3646            .verification
3647            .obligation_outcomes
3648            .iter()
3649            .map(|item| {
3650                vec![
3651                    item.obligation_id.display(false),
3652                    item.outcome.label().to_owned(),
3653                    item.detail.display(true),
3654                ]
3655            })
3656            .collect()
3657    };
3658    base_surface(
3659        snapshot,
3660        NativeSurfaceFamily::VerificationResult,
3661        Component {
3662            id: "verification-root".to_owned(),
3663            kind: ComponentKind::Stack {
3664                children: vec![
3665                    Component {
3666                        id: "verification-status".to_owned(),
3667                        kind: ComponentKind::Status {
3668                            label: "Canonical verification verdict".to_owned(),
3669                            value: facts.verification.verdict.label().to_owned(),
3670                            level: facts.verification.verdict.status().level(),
3671                        },
3672                    },
3673                    Component {
3674                        id: "verification-facts".to_owned(),
3675                        kind: ComponentKind::KeyValue { entries },
3676                    },
3677                    Component {
3678                        id: "verification-obligations".to_owned(),
3679                        kind: ComponentKind::Table {
3680                            columns: vec![
3681                                "Obligation".to_owned(),
3682                                "Outcome".to_owned(),
3683                                "Detail".to_owned(),
3684                            ],
3685                            rows,
3686                        },
3687                    },
3688                    Component::text("verification-note", &facts.verification.certification_note),
3689                ],
3690            },
3691        },
3692    )
3693}
3694
3695fn evidence_surface(snapshot: &NativeSnapshotIdentity, facts: &Facts) -> Surface {
3696    let mut items = facts
3697        .evidence
3698        .items
3699        .iter()
3700        .map(|item| EvidenceItem {
3701            label: format!(
3702                "{} / {} / {} [{}]",
3703                item.obligation.display(false),
3704                item.source.display(true),
3705                item.evidence_type.display(true),
3706                item.state.label()
3707            ),
3708            value: format!(
3709                "{} — {}",
3710                item.reference.display(true),
3711                item.detail.display(true)
3712            ),
3713        })
3714        .collect::<Vec<_>>();
3715    if facts.evidence.total > MAX_DISPLAY_EVIDENCE {
3716        items.push(EvidenceItem {
3717            label: "Evidence count [diagnostic/non-authoritative metadata]".to_owned(),
3718            value: format!(
3719                "Showing {} of {}; pass={}, fail={}, insufficient={}, unknown={}, observed={}, invalid={}, declared={:?}",
3720                facts.evidence.items.len(),
3721                facts.evidence.total,
3722                facts.evidence.passed,
3723                facts.evidence.failed,
3724                facts.evidence.insufficient,
3725                facts.evidence.unknown,
3726                facts.evidence.observed_rows,
3727                facts.evidence.invalid_rows,
3728                facts.evidence.declared_rows,
3729            ),
3730        });
3731    }
3732    if let Some(reason) = &facts.evidence.unavailable_reason {
3733        items.push(EvidenceItem {
3734            label: "Projection state [diagnostic/non-authoritative metadata]".to_owned(),
3735            value: reason.clone(),
3736        });
3737    }
3738    if items.is_empty() {
3739        items.push(EvidenceItem {
3740            label: "Evidence".to_owned(),
3741            value: "Unavailable".to_owned(),
3742        });
3743    }
3744    base_surface(
3745        snapshot,
3746        NativeSurfaceFamily::Evidence,
3747        Component {
3748            id: "evidence-root".to_owned(),
3749            kind: ComponentKind::Stack {
3750                children: vec![
3751                    Component {
3752                        id: "evidence-verdict".to_owned(),
3753                        kind: ComponentKind::Status {
3754                            label: "Evidence authority".to_owned(),
3755                            value: facts.verification.verdict.label().to_owned(),
3756                            level: facts.verification.verdict.status().level(),
3757                        },
3758                    },
3759                    Component {
3760                        id: "evidence-items".to_owned(),
3761                        kind: ComponentKind::Evidence {
3762                            title: "Evidence items (validated provenance)".to_owned(),
3763                            items,
3764                        },
3765                    },
3766                ],
3767            },
3768        },
3769    )
3770}
3771
3772fn report_surface(snapshot: &NativeSnapshotIdentity, facts: &Facts) -> Surface {
3773    let report_authority = if facts.report.id.value.is_some() {
3774        AuthorityClass::AuthoritativeFalseGreenState
3775    } else {
3776        AuthorityClass::DiagnosticNonAuthoritativeMetadata
3777    };
3778    let entries = BTreeMap::from([
3779        (
3780            "report".to_owned(),
3781            label("Report", &facts.report.id, false),
3782        ),
3783        (
3784            "report_digest".to_owned(),
3785            label("Report digest", &facts.report.report_digest, false),
3786        ),
3787        (
3788            "candidate".to_owned(),
3789            label("Candidate", &facts.report.candidate_sha256, false),
3790        ),
3791        (
3792            "source".to_owned(),
3793            label("Source", &facts.report.source_sha256, false),
3794        ),
3795        (
3796            "verdict".to_owned(),
3797            format!(
3798                "Verdict [{}]: {}",
3799                report_authority.label(),
3800                facts.report.verdict.label()
3801            ),
3802        ),
3803        (
3804            "evidence".to_owned(),
3805            label("Key evidence", &facts.report.key_evidence_summary, true),
3806        ),
3807        (
3808            "created".to_owned(),
3809            label("Created", &facts.report.created_at, true),
3810        ),
3811        (
3812            "provenance".to_owned(),
3813            label("Provenance", &facts.report.provenance, true),
3814        ),
3815    ]);
3816    base_surface(
3817        snapshot,
3818        NativeSurfaceFamily::Report,
3819        Component {
3820            id: "report-root".to_owned(),
3821            kind: ComponentKind::Stack {
3822                children: vec![
3823                    Component {
3824                        id: "report-status".to_owned(),
3825                        kind: ComponentKind::Status {
3826                            label: "Report verdict".to_owned(),
3827                            value: facts.report.verdict.label().to_owned(),
3828                            level: facts.report.verdict.status().level(),
3829                        },
3830                    },
3831                    Component {
3832                        id: "report-facts".to_owned(),
3833                        kind: ComponentKind::KeyValue { entries },
3834                    },
3835                ],
3836            },
3837        },
3838    )
3839}
3840
3841fn recovery_surface(snapshot: &NativeSnapshotIdentity, facts: &Facts) -> Surface {
3842    let value = match facts.recovery.state {
3843        RecoveryState::Recoverable => "RECOVERABLE",
3844        RecoveryState::Blocked => "BLOCKED",
3845        RecoveryState::Terminal => "TERMINAL",
3846        RecoveryState::Unknown => "UNKNOWN",
3847        RecoveryState::NotApplicable => "NOT APPLICABLE",
3848    };
3849    let entries = BTreeMap::from([
3850        (
3851            "reason".to_owned(),
3852            label("Reason", &facts.recovery.reason, true),
3853        ),
3854        (
3855            "claim".to_owned(),
3856            label("Pre-begin claim", &facts.recovery.pre_begin_claim, true),
3857        ),
3858        (
3859            "actions".to_owned(),
3860            "Actions [diagnostic/non-authoritative metadata]: none; read-only G4".to_owned(),
3861        ),
3862        (
3863            "distinction".to_owned(),
3864            safe_text(&facts.recovery.distinction),
3865        ),
3866    ]);
3867    base_surface(
3868        snapshot,
3869        NativeSurfaceFamily::FailureRecovery,
3870        Component {
3871            id: "recovery-root".to_owned(),
3872            kind: ComponentKind::Stack {
3873                children: vec![
3874                    Component {
3875                        id: "recovery-status".to_owned(),
3876                        kind: ComponentKind::Status {
3877                            label: "Failure/recovery state".to_owned(),
3878                            value: value.to_owned(),
3879                            level: if facts.recovery.state == RecoveryState::Terminal {
3880                                StatusLevel::Error
3881                            } else if matches!(
3882                                facts.recovery.state,
3883                                RecoveryState::Recoverable | RecoveryState::Blocked
3884                            ) {
3885                                StatusLevel::Warning
3886                            } else {
3887                                StatusLevel::Info
3888                            },
3889                        },
3890                    },
3891                    Component {
3892                        id: "recovery-facts".to_owned(),
3893                        kind: ComponentKind::KeyValue { entries },
3894                    },
3895                ],
3896            },
3897        },
3898    )
3899}
3900
3901fn replacement_surface(snapshot: &NativeSnapshotIdentity, facts: &Facts) -> Surface {
3902    let entries = BTreeMap::from([
3903        (
3904            "predecessor".to_owned(),
3905            label(
3906                "Predecessor session",
3907                &facts.replacement.predecessor_session,
3908                false,
3909            ),
3910        ),
3911        (
3912            "replacement".to_owned(),
3913            label(
3914                "Replacement session",
3915                &facts.replacement.replacement_session,
3916                false,
3917            ),
3918        ),
3919        (
3920            "predecessor_state".to_owned(),
3921            label(
3922                "Immutable predecessor state",
3923                &facts.replacement.predecessor_state,
3924                false,
3925            ),
3926        ),
3927        (
3928            "continuity".to_owned(),
3929            label("Continuity", &facts.replacement.continuity, true),
3930        ),
3931        (
3932            "candidate".to_owned(),
3933            label("Candidate", &facts.replacement.candidate_sha256, false),
3934        ),
3935        (
3936            "workspace".to_owned(),
3937            label(
3938                "Workspace identity",
3939                &facts.replacement.workspace_state,
3940                true,
3941            ),
3942        ),
3943        (
3944            "terminal".to_owned(),
3945            label(
3946                "Terminal outcome",
3947                &facts.replacement.terminal_outcome,
3948                true,
3949            ),
3950        ),
3951    ]);
3952    base_surface(
3953        snapshot,
3954        NativeSurfaceFamily::ReplacementSession,
3955        Component {
3956            id: "replacement-root".to_owned(),
3957            kind: ComponentKind::Stack {
3958                children: vec![
3959                    Component {
3960                        id: "replacement-status".to_owned(),
3961                        kind: ComponentKind::Status {
3962                            label: "Replacement continuity".to_owned(),
3963                            value: facts.replacement.continuity.display(true),
3964                            level: StatusLevel::Info,
3965                        },
3966                    },
3967                    Component {
3968                        id: "replacement-facts".to_owned(),
3969                        kind: ComponentKind::KeyValue { entries },
3970                    },
3971                    Component {
3972                        id: "replacement-timeline".to_owned(),
3973                        kind: ComponentKind::Timeline {
3974                            entries: vec![
3975                                TimelineEntry {
3976                                    label: "Predecessor".to_owned(),
3977                                    detail: facts.replacement.predecessor_session.display(false),
3978                                    state: TimelineState::Complete,
3979                                },
3980                                TimelineEntry {
3981                                    label: "Replacement".to_owned(),
3982                                    detail: facts.replacement.replacement_session.display(false),
3983                                    state: TimelineState::Active,
3984                                },
3985                            ],
3986                        },
3987                    },
3988                ],
3989            },
3990        },
3991    )
3992}
3993
3994fn status_component(id: &str, label: &str, status: NativeStatus, raw: &NativeField) -> Component {
3995    Component {
3996        id: id.to_owned(),
3997        kind: ComponentKind::Status {
3998            label: label.to_owned(),
3999            value: if raw.value.is_some() {
4000                format!("{} (raw: {})", status.label(), raw.display(true))
4001            } else {
4002                status.label().to_owned()
4003            },
4004            level: status.level(),
4005        },
4006    }
4007}
4008
4009fn required_string(object: &serde_json::Map<String, Value>, key: &str) -> Result<String, String> {
4010    object
4011        .get(key)
4012        .and_then(Value::as_str)
4013        .filter(|value| !value.is_empty())
4014        .map(str::to_owned)
4015        .ok_or_else(|| format!("missing or malformed {key}"))
4016}
4017fn required_string_any(
4018    object: &serde_json::Map<String, Value>,
4019    keys: &[&str],
4020) -> Result<String, String> {
4021    let mut selected: Option<String> = None;
4022    for key in keys {
4023        let Some(value) = object.get(*key) else {
4024            continue;
4025        };
4026        let value = value
4027            .as_str()
4028            .filter(|value| !value.is_empty())
4029            .ok_or_else(|| format!("missing or malformed {}", keys.join("/")))?;
4030        if selected.as_ref().is_some_and(|existing| existing != value) {
4031            return Err(format!("conflicting {}", keys.join("/")));
4032        }
4033        selected = Some(value.to_owned());
4034    }
4035    selected.ok_or_else(|| format!("missing or malformed {}", keys.join("/")))
4036}
4037fn optional_string(object: &serde_json::Map<String, Value>, key: &str) -> Option<String> {
4038    object
4039        .get(key)
4040        .and_then(Value::as_str)
4041        .filter(|value| !value.is_empty())
4042        .map(str::to_owned)
4043}
4044fn optional_string_any_checked(
4045    object: &serde_json::Map<String, Value>,
4046    keys: &[&str],
4047) -> Result<Option<String>, String> {
4048    let mut selected: Option<String> = None;
4049    for key in keys {
4050        let Some(value) = object.get(*key) else {
4051            continue;
4052        };
4053        let value = value
4054            .as_str()
4055            .filter(|value| !value.is_empty())
4056            .ok_or_else(|| format!("malformed {}", keys.join("/")))?;
4057        if selected.as_ref().is_some_and(|existing| existing != value) {
4058            return Err(format!("conflicting {}", keys.join("/")));
4059        }
4060        selected = Some(value.to_owned());
4061    }
4062    Ok(selected)
4063}
4064
4065fn reject_unknown_keys(
4066    object: &serde_json::Map<String, Value>,
4067    allowed: &[&str],
4068    subject: &str,
4069) -> Result<(), String> {
4070    if let Some(key) = object.keys().find(|key| !allowed.contains(&key.as_str())) {
4071        return Err(format!("{subject} contains unknown field {key}"));
4072    }
4073    Ok(())
4074}
4075
4076fn required_digest_any(
4077    object: &serde_json::Map<String, Value>,
4078    keys: &[&str],
4079) -> Result<String, String> {
4080    let value = required_string_any(object, keys)?;
4081    valid_digest(&value)
4082        .then_some(value)
4083        .ok_or_else(|| format!("malformed digest {}", keys.join("/")))
4084}
4085
4086fn required_report_digest(object: &serde_json::Map<String, Value>) -> Result<String, String> {
4087    required_digest_any(object, &["report_digest", "digest"]).map_err(|error| {
4088        if error.contains("missing or malformed") {
4089            "cryptographic report digest is missing".to_owned()
4090        } else {
4091            error
4092        }
4093    })
4094}
4095
4096fn strict_display_alias(
4097    object: &serde_json::Map<String, Value>,
4098    keys: &[&str],
4099) -> Result<Option<String>, String> {
4100    let mut selected: Option<String> = None;
4101    for key in keys {
4102        let Some(value) = object.get(*key) else {
4103            continue;
4104        };
4105        let rendered = display_json(value);
4106        if selected
4107            .as_ref()
4108            .is_some_and(|existing| existing != &rendered)
4109        {
4110            return Err(format!("conflicting {}", keys.join("/")));
4111        }
4112        selected = Some(rendered);
4113    }
4114    Ok(selected)
4115}
4116fn optional_digest_any(
4117    object: &serde_json::Map<String, Value>,
4118    keys: &[&str],
4119) -> Result<Option<String>, String> {
4120    let mut selected: Option<String> = None;
4121    let mut saw_null = false;
4122    for key in keys {
4123        let Some(value) = object.get(*key) else {
4124            continue;
4125        };
4126        let Value::String(value) = value else {
4127            if value.is_null() {
4128                if selected.is_some() {
4129                    return Err(format!("conflicting digest {}", keys.join("/")));
4130                }
4131                saw_null = true;
4132                continue;
4133            }
4134            return Err(format!("malformed digest {}", keys.join("/")));
4135        };
4136        if !valid_digest(value) {
4137            return Err(format!("malformed digest {}", keys.join("/")));
4138        }
4139        if selected.as_ref().is_some_and(|existing| existing != value) {
4140            return Err(format!("conflicting digest {}", keys.join("/")));
4141        }
4142        if saw_null {
4143            return Err(format!("conflicting digest {}", keys.join("/")));
4144        }
4145        selected = Some(value.clone());
4146    }
4147    Ok(selected)
4148}
4149fn optional_falsegreen(value: Option<&str>) -> NativeField {
4150    value.map_or_else(
4151        || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
4152        NativeField::falsegreen,
4153    )
4154}
4155fn optional_agent(value: Option<&str>) -> NativeField {
4156    value.map_or_else(
4157        || NativeField::unavailable(AuthorityClass::AuthoritativeAgentSessionState),
4158        NativeField::agent,
4159    )
4160}
4161fn parse_verdict_exact(raw: &str) -> Option<NativeVerdict> {
4162    match raw {
4163        "accepted" => Some(NativeVerdict::Accepted),
4164        "failed" => Some(NativeVerdict::Failed),
4165        "insufficient_evidence" => Some(NativeVerdict::InsufficientEvidence),
4166        _ => None,
4167    }
4168}
4169fn parse_status_exact(raw: &str) -> Option<NativeStatus> {
4170    match raw {
4171        "QUEUED" => Some(NativeStatus::Queued),
4172        "CLAIMED" => Some(NativeStatus::Claimed),
4173        "RUNNING" => Some(NativeStatus::Running),
4174        "EXECUTING" => Some(NativeStatus::Executing),
4175        "TERMINAL" => Some(NativeStatus::Terminal),
4176        "ACCEPTED" => Some(NativeStatus::Accepted),
4177        "FAILED" => Some(NativeStatus::Failed),
4178        "INSUFFICIENT_EVIDENCE" => Some(NativeStatus::InsufficientEvidence),
4179        _ => None,
4180    }
4181}
4182fn parse_evidence_exact(raw: &str) -> Option<EvidenceState> {
4183    match raw {
4184        "passed" => Some(EvidenceState::Passed),
4185        "failed" => Some(EvidenceState::Failed),
4186        "insufficient" => Some(EvidenceState::Insufficient),
4187        "unknown" => Some(EvidenceState::Unknown),
4188        _ => None,
4189    }
4190}
4191fn status_wire(status: NativeStatus) -> &'static str {
4192    match status {
4193        NativeStatus::Accepted => "ACCEPTED",
4194        NativeStatus::Failed => "FAILED",
4195        NativeStatus::InsufficientEvidence => "INSUFFICIENT_EVIDENCE",
4196        NativeStatus::Queued => "QUEUED",
4197        NativeStatus::Claimed => "CLAIMED",
4198        NativeStatus::Running => "RUNNING",
4199        NativeStatus::Executing => "EXECUTING",
4200        NativeStatus::Terminal => "TERMINAL",
4201        NativeStatus::Recoverable => "RECOVERABLE",
4202        NativeStatus::Unknown => "UNKNOWN",
4203    }
4204}
4205fn status_wire_verdict(verdict: NativeVerdict) -> &'static str {
4206    match verdict {
4207        NativeVerdict::Accepted => "accepted",
4208        NativeVerdict::Failed => "failed",
4209        NativeVerdict::InsufficientEvidence => "insufficient_evidence",
4210        NativeVerdict::Incomplete => "incomplete",
4211        NativeVerdict::Invalid => "invalid",
4212        NativeVerdict::Unknown => "unknown",
4213    }
4214}
4215fn parse_obligation_outcomes(
4216    payload: &Value,
4217    scope: Option<&AuthoritativeFrozenScopeView>,
4218) -> Vec<NativeObligationOutcome> {
4219    let Some(values) = payload
4220        .get("obligation_outcomes")
4221        .or_else(|| payload.get("obligations"))
4222        .and_then(Value::as_array)
4223    else {
4224        return Vec::new();
4225    };
4226    let Some(scope) = scope else {
4227        return Vec::new();
4228    };
4229    values
4230        .iter()
4231        .filter_map(|value| {
4232            let object = value.as_object()?;
4233            let id = required_string_any(object, &["obligation_id", "id"]).ok()?;
4234            if !scope
4235                .obligations
4236                .iter()
4237                .any(|(expected, _)| expected == &id)
4238            {
4239                return None;
4240            }
4241            let outcome = parse_evidence_exact(
4242                required_string_any(object, &["status", "outcome"])
4243                    .ok()?
4244                    .as_str(),
4245            )?;
4246            Some(NativeObligationOutcome {
4247                obligation_id: NativeField::falsegreen(id),
4248                outcome,
4249                detail: object
4250                    .get("detail")
4251                    .map(display_json)
4252                    .map(NativeField::falsegreen)
4253                    .unwrap_or_else(|| {
4254                        NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState)
4255                    }),
4256                evidence_count: object
4257                    .get("evidence_count")
4258                    .and_then(Value::as_u64)
4259                    .unwrap_or_default() as usize,
4260            })
4261        })
4262        .collect()
4263}
4264fn scope_digest_for_obligations(obligations: &[(String, String)]) -> String {
4265    let value = serde_json::json!({"obligations": obligations.iter().map(|(id, requirement)| serde_json::json!({"id": id, "requirement": requirement})).collect::<Vec<_>>()});
4266    sha256_json(&value)
4267}
4268pub fn frozen_scope_digest(obligations: &[(String, String)]) -> String {
4269    scope_digest_for_obligations(obligations)
4270}
4271fn display_json(value: &Value) -> String {
4272    safe_text(&value.to_string())
4273}
4274fn safe_text(value: &str) -> String {
4275    let value = sanitize_text(value);
4276    let mut output = String::new();
4277    for character in value.chars() {
4278        let character = if matches!(character, '\n' | '\r' | '\t') {
4279            ' '
4280        } else {
4281            character
4282        };
4283        if output.len() + character.len_utf8() > MAX_EVIDENCE_VALUE_BYTES {
4284            output.push('…');
4285            break;
4286        }
4287        output.push(character);
4288    }
4289    output
4290}
4291fn sha256_json(value: &Value) -> String {
4292    sha256_bytes(serde_json::to_string(value).unwrap_or_default().as_bytes())
4293}
4294fn sha256_bytes(value: &[u8]) -> String {
4295    format!("{:x}", Sha256::digest(value))
4296}
4297fn valid_digest(value: &str) -> bool {
4298    value.len() == 64
4299        && value
4300            .bytes()
4301            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
4302}
4303fn valid_identity(value: &str) -> bool {
4304    !value.is_empty()
4305        && value.len() <= 256
4306        && value.chars().all(|character| {
4307            !character.is_control()
4308                && !matches!(
4309                    character,
4310                    '\u{061c}'
4311                        | '\u{200b}'
4312                        | '\u{200c}'
4313                        | '\u{200d}'
4314                        | '\u{200e}'
4315                        | '\u{200f}'
4316                        | '\u{202a}'..='\u{202e}'
4317                        | '\u{2060}'
4318                        | '\u{2066}'..='\u{2069}'
4319                        | '\u{feff}'
4320                )
4321        })
4322}
4323fn safe_id(value: &str) -> String {
4324    value
4325        .chars()
4326        .filter(|character| {
4327            character.is_ascii_alphanumeric() || matches!(character, '_' | '-' | '.' | ':')
4328        })
4329        .take(64)
4330        .collect()
4331}
4332fn session_state_label(state: SessionState) -> &'static str {
4333    match state {
4334        SessionState::Initializing => "INITIALIZING",
4335        SessionState::Working => "WORKING",
4336        SessionState::WaitingForTool => "WAITING_FOR_TOOL",
4337        SessionState::CandidateReady => "CANDIDATE_READY",
4338        SessionState::Verifying => "VERIFYING",
4339        SessionState::Repairing => "REPAIRING",
4340        SessionState::Paused => "PAUSED",
4341        SessionState::AcceptedAwaitingAuthority => "ACCEPTED_AWAITING_AUTHORITY",
4342        SessionState::Completed => "COMPLETED",
4343        SessionState::Failed => "FAILED",
4344        SessionState::BudgetExhausted => "BUDGET_EXHAUSTED",
4345    }
4346}
4347
4348fn is_known_session_lifecycle(value: &str) -> bool {
4349    matches!(
4350        value,
4351        "INITIALIZING"
4352            | "WORKING"
4353            | "WAITING_FOR_TOOL"
4354            | "CANDIDATE_READY"
4355            | "VERIFYING"
4356            | "REPAIRING"
4357            | "PAUSED"
4358            | "ACCEPTED_AWAITING_AUTHORITY"
4359            | "COMPLETED"
4360            | "FAILED"
4361            | "BUDGET_EXHAUSTED"
4362    )
4363}