Skip to main content

solverforge_solver/stats/
candidate_trace.rs

1//! Bounded, core-owned candidate-pull diagnostics.
2//!
3//! This module intentionally records candidates where the solver engine
4//! consumes them, rather than decorating selectors or reconstructing a second
5//! cursor.  A trace is therefore a faithful ordered prefix of work that
6//! reached construction or search evaluation.
7
8use super::{
9    CandidateTraceQualificationError, CandidateTraceQualificationStatus,
10    QualifiedCandidateTraceRunProvenance,
11};
12
13/// Wire-format version for [`CandidateTraceHeader`] and every framed value in
14/// this module.
15pub const CANDIDATE_TRACE_FORMAT_VERSION: u32 = 3;
16
17/// Stable, non-cryptographic digest used to quickly reject mismatched trace
18/// provenance or prefixes.
19///
20/// The framed configuration, resolved plan, and retained entries remain the
21/// source of truth.  This value is a deterministic convenience checksum, not
22/// a collision-resistant proof.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub struct CandidateTraceDigest {
25    pub first: u64,
26    pub second: u64,
27}
28
29impl CandidateTraceDigest {
30    const FIRST_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
31    const FIRST_PRIME: u64 = 0x0000_0100_0000_01b3;
32    const SECOND_OFFSET: u64 = 0x9e37_79b9_7f4a_7c15;
33    const SECOND_MULTIPLIER: u64 = 0xd6e8_feb8_6659_fd93;
34
35    pub const fn empty() -> Self {
36        Self {
37            first: Self::FIRST_OFFSET,
38            second: Self::SECOND_OFFSET,
39        }
40    }
41
42    pub fn of_bytes(bytes: &[u8]) -> Self {
43        let mut digest = Self::empty();
44        digest.update(bytes);
45        digest
46    }
47
48    pub fn update(&mut self, bytes: &[u8]) {
49        for &byte in bytes {
50            self.first ^= u64::from(byte);
51            self.first = self.first.wrapping_mul(Self::FIRST_PRIME);
52
53            self.second ^= u64::from(byte).wrapping_add(0x9d);
54            self.second = self
55                .second
56                .rotate_left(13)
57                .wrapping_mul(Self::SECOND_MULTIPLIER)
58                .wrapping_add(0x9e37_79b9);
59        }
60    }
61}
62
63impl Default for CandidateTraceDigest {
64    fn default() -> Self {
65        Self::empty()
66    }
67}
68
69/// A resolved solver phase plan used as candidate-trace provenance.
70///
71/// `opaque` is explicit rather than silently falling back to a type name.
72/// Consumers must reject an incomplete plan when proving work equivalence.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct CandidateTracePhasePlan {
75    pub kind: String,
76    /// Structured, sorted effective settings for this resolved node.  Keys
77    /// are unique, which makes the canonical framing unambiguous and prevents
78    /// a display-only label from hiding material selector/acceptor/forager or
79    /// termination differences.
80    pub attributes: Vec<CandidateTracePhaseAttribute>,
81    pub opaque: bool,
82    pub children: Vec<Self>,
83}
84
85/// One canonical resolved-plan attribute.
86#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
87pub struct CandidateTracePhaseAttribute {
88    pub key: String,
89    pub value: String,
90}
91
92impl CandidateTracePhasePlan {
93    pub fn known<K, V, I>(kind: impl Into<String>, attributes: I, children: Vec<Self>) -> Self
94    where
95        I: IntoIterator<Item = (K, V)>,
96        K: Into<String>,
97        V: Into<String>,
98    {
99        let mut attributes = attributes
100            .into_iter()
101            .map(|(key, value)| CandidateTracePhaseAttribute {
102                key: key.into(),
103                value: value.into(),
104            })
105            .collect::<Vec<_>>();
106        attributes.sort();
107        assert!(
108            attributes.windows(2).all(|pair| pair[0].key != pair[1].key),
109            "candidate trace phase-plan attributes must have unique keys"
110        );
111        Self {
112            kind: kind.into(),
113            attributes,
114            opaque: false,
115            children,
116        }
117    }
118
119    pub fn opaque(kind: impl Into<String>) -> Self {
120        Self {
121            kind: kind.into(),
122            attributes: Vec::new(),
123            opaque: true,
124            children: Vec::new(),
125        }
126    }
127
128    pub fn is_complete(&self) -> bool {
129        !self.opaque && self.children.iter().all(Self::is_complete)
130    }
131
132    pub fn canonical_bytes(&self) -> Vec<u8> {
133        let mut out = Vec::new();
134        self.append_canonical_bytes(&mut out);
135        out
136    }
137
138    fn append_canonical_bytes(&self, out: &mut Vec<u8>) {
139        out.push(0x50);
140        append_string(out, &self.kind);
141        append_bool(out, self.opaque);
142        append_len(out, self.attributes.len());
143        for attribute in &self.attributes {
144            append_string(out, &attribute.key);
145            append_string(out, &attribute.value);
146        }
147        append_len(out, self.children.len());
148        for child in &self.children {
149            child.append_canonical_bytes(out);
150        }
151    }
152}
153
154/// The execution policy the solver actually installed for this run.
155///
156/// Configuration input is useful provenance, but it is not itself the
157/// execution policy: the configured entrypoint can inject a fallback time
158/// limit, discard an invalid score target, or compose a criterion with a
159/// time guard.  This value records that resolved result explicitly.  A
160/// generic [`crate::solver::Solver`] whose termination is supplied as an
161/// arbitrary Rust value is intentionally opaque unless its caller supplies a
162/// canonical policy through the internal configured-run seam.
163#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct CandidateTraceExecutionPolicy {
165    pub kind: String,
166    /// Sorted, unique material policy attributes.  These identify the
167    /// installed termination composition rather than a display-only summary.
168    pub attributes: Vec<CandidateTracePhaseAttribute>,
169    pub opaque: bool,
170}
171
172impl CandidateTraceExecutionPolicy {
173    pub fn known<K, V, I>(kind: impl Into<String>, attributes: I) -> Self
174    where
175        I: IntoIterator<Item = (K, V)>,
176        K: Into<String>,
177        V: Into<String>,
178    {
179        let mut attributes = attributes
180            .into_iter()
181            .map(|(key, value)| CandidateTracePhaseAttribute {
182                key: key.into(),
183                value: value.into(),
184            })
185            .collect::<Vec<_>>();
186        attributes.sort();
187        assert!(
188            attributes.windows(2).all(|pair| pair[0].key != pair[1].key),
189            "candidate trace execution-policy attributes must have unique keys"
190        );
191        Self {
192            kind: kind.into(),
193            attributes,
194            opaque: false,
195        }
196    }
197
198    pub fn opaque(kind: impl Into<String>) -> Self {
199        Self::opaque_with_attributes(kind, std::iter::empty::<(String, String)>())
200    }
201
202    /// Records observable policy fields while explicitly declining to claim
203    /// complete policy provenance.  This is used by the generic `Solver`
204    /// entrypoint: `with_time_limit` is observable and must be retained, but
205    /// the caller's arbitrary Rust termination remains uninspectable.
206    pub fn opaque_with_attributes<K, V, I>(kind: impl Into<String>, attributes: I) -> Self
207    where
208        I: IntoIterator<Item = (K, V)>,
209        K: Into<String>,
210        V: Into<String>,
211    {
212        let mut attributes = attributes
213            .into_iter()
214            .map(|(key, value)| CandidateTracePhaseAttribute {
215                key: key.into(),
216                value: value.into(),
217            })
218            .collect::<Vec<_>>();
219        attributes.sort();
220        assert!(
221            attributes.windows(2).all(|pair| pair[0].key != pair[1].key),
222            "candidate trace execution-policy attributes must have unique keys"
223        );
224        Self {
225            kind: kind.into(),
226            attributes,
227            opaque: true,
228        }
229    }
230
231    pub const fn is_complete(&self) -> bool {
232        !self.opaque
233    }
234
235    pub fn canonical_bytes(&self) -> Vec<u8> {
236        let mut out = Vec::new();
237        out.push(0x58);
238        append_string(&mut out, &self.kind);
239        append_bool(&mut out, self.opaque);
240        append_len(&mut out, self.attributes.len());
241        for attribute in &self.attributes {
242            append_string(&mut out, &attribute.key);
243            append_string(&mut out, &attribute.value);
244        }
245        out
246    }
247}
248
249/// Digest value supplied by an external fixture or integration boundary.
250///
251/// SolverForge transports this value; it does not derive it by traversing a
252/// model, calling user callbacks, or serializing a working solution. That
253/// keeps opt-in diagnostics from changing model work or callback delivery.
254#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
255pub struct CandidateTraceExternalDigest {
256    pub bytes: [u8; 32],
257}
258
259impl CandidateTraceExternalDigest {
260    pub const fn sha256(bytes: [u8; 32]) -> Self {
261        Self { bytes }
262    }
263
264    fn append_canonical_bytes(&self, out: &mut Vec<u8>) {
265        // The fixed 32-byte representation is SHA-256 by contract. Keep the
266        // algorithm tag framed so a future explicit algorithm does not
267        // silently compare as equivalent.
268        out.push(1);
269        out.extend_from_slice(&self.bytes);
270    }
271}
272
273/// Origin of supplied run-input provenance.
274#[derive(Debug, Clone, PartialEq, Eq)]
275pub struct CandidateTraceInputAttestation {
276    producer: String,
277}
278
279impl CandidateTraceInputAttestation {
280    pub fn external(producer: impl Into<String>) -> Self {
281        let producer = producer.into();
282        assert!(
283            !producer.is_empty(),
284            "candidate trace external provenance producer must not be empty"
285        );
286        Self { producer }
287    }
288
289    pub fn external_producer(&self) -> &str {
290        &self.producer
291    }
292
293    fn append_canonical_bytes(&self, out: &mut Vec<u8>) {
294        out.push(1);
295        append_string(out, &self.producer);
296    }
297}
298
299/// Immutable caller-supplied context for a diagnostic run.
300///
301/// The three required digests identify the compiled schema/model, source
302/// instance, and imported initial working state. Optional core-tree and build
303/// digests let a harness pin the executable source as well. This is an
304/// attestation hook, not an assertion by SolverForge that two foreign models
305/// have equivalent semantics; consumers must inspect [`Self::attestation`]
306/// and independently verify external inputs before qualifying a comparison.
307#[derive(Debug, Clone, PartialEq, Eq)]
308pub struct CandidateTraceInputProvenance {
309    pub schema_digest: CandidateTraceExternalDigest,
310    pub instance_digest: CandidateTraceExternalDigest,
311    pub initial_state_digest: CandidateTraceExternalDigest,
312    pub core_tree_digest: Option<CandidateTraceExternalDigest>,
313    pub build_digest: Option<CandidateTraceExternalDigest>,
314    pub attestation: CandidateTraceInputAttestation,
315}
316
317impl CandidateTraceInputProvenance {
318    pub fn externally_attested(
319        schema_digest: CandidateTraceExternalDigest,
320        instance_digest: CandidateTraceExternalDigest,
321        initial_state_digest: CandidateTraceExternalDigest,
322        producer: impl Into<String>,
323    ) -> Self {
324        Self {
325            schema_digest,
326            instance_digest,
327            initial_state_digest,
328            core_tree_digest: None,
329            build_digest: None,
330            attestation: CandidateTraceInputAttestation::external(producer),
331        }
332    }
333
334    pub fn with_core_tree_digest(mut self, digest: CandidateTraceExternalDigest) -> Self {
335        self.core_tree_digest = Some(digest);
336        self
337    }
338
339    pub fn with_build_digest(mut self, digest: CandidateTraceExternalDigest) -> Self {
340        self.build_digest = Some(digest);
341        self
342    }
343
344    fn canonical_bytes(&self) -> Vec<u8> {
345        let mut out = Vec::new();
346        out.push(0x49);
347        self.schema_digest.append_canonical_bytes(&mut out);
348        self.instance_digest.append_canonical_bytes(&mut out);
349        self.initial_state_digest.append_canonical_bytes(&mut out);
350        append_optional_external_digest(&mut out, self.core_tree_digest);
351        append_optional_external_digest(&mut out, self.build_digest);
352        self.attestation.append_canonical_bytes(&mut out);
353        out
354    }
355}
356
357/// Trust state of optional caller-supplied run-input provenance.
358#[derive(Debug, Clone, Copy, PartialEq, Eq)]
359pub enum CandidateTraceInputProvenanceStatus {
360    Absent,
361    ExternallyAttested,
362}
363
364/// Separate execution/input provenance status for benchmark qualification.
365///
366/// This intentionally does not collapse into `CandidateTraceTelemetry::is_complete()`.
367/// External attestation remains separate from engine execution-plan
368/// completeness.
369#[derive(Debug, Clone, Copy, PartialEq, Eq)]
370pub struct CandidateTraceProvenanceStatus {
371    pub execution_policy_complete: bool,
372    pub resolved_phase_plan_complete: bool,
373    pub input_provenance: CandidateTraceInputProvenanceStatus,
374    pub qualification: CandidateTraceQualificationStatus,
375}
376
377impl CandidateTraceProvenanceStatus {
378    pub const fn has_complete_execution_plan(self) -> bool {
379        self.execution_policy_complete && self.resolved_phase_plan_complete
380    }
381}
382
383/// Immutable provenance emitted once for an opt-in candidate trace.
384#[derive(Debug, Clone, PartialEq, Eq)]
385pub struct CandidateTraceHeader {
386    pub format_version: u32,
387    /// Canonical TOML from the exact `SolverConfig` supplied to the run.
388    ///
389    /// This is deliberately named *configured input*, not "effective
390    /// configuration": effective execution behavior is represented by
391    /// [`Self::execution_policy`] and [`Self::resolved_phase_plan`].
392    pub configured_input: String,
393    pub configured_input_digest: CandidateTraceDigest,
394    /// Actual resolved solver policy, including entrypoint defaults injected
395    /// by the configured runtime path.
396    pub execution_policy: CandidateTraceExecutionPolicy,
397    pub execution_policy_digest: CandidateTraceDigest,
398    /// False means the execution policy came from an arbitrary generic Rust
399    /// termination and cannot qualify as exact work-equivalence evidence.
400    pub execution_policy_complete: bool,
401    /// Optional immutable model/instance/initial-state provenance supplied by
402    /// the entrypoint.
403    pub input_provenance: Option<CandidateTraceInputProvenance>,
404    pub input_provenance_digest: Option<CandidateTraceDigest>,
405    /// Present only when the caller explicitly requested a qualified
406    /// diagnostic run. It repeats the exact immutable attestation rather than
407    /// requiring consumers to infer qualification from optional fields.
408    pub qualified_run_provenance: Option<QualifiedCandidateTraceRunProvenance>,
409    pub resolved_phase_plan: CandidateTracePhasePlan,
410    pub resolved_phase_plan_digest: CandidateTraceDigest,
411    /// False means at least one phase supplied only opaque provenance and
412    /// cannot qualify as an exact work-equivalence comparison.
413    pub resolved_phase_plan_complete: bool,
414}
415
416impl CandidateTraceHeader {
417    pub fn new(
418        configured_input: String,
419        execution_policy: CandidateTraceExecutionPolicy,
420        resolved_phase_plan: CandidateTracePhasePlan,
421        input_provenance: Option<CandidateTraceInputProvenance>,
422    ) -> Self {
423        let configured_input_digest = CandidateTraceDigest::of_bytes(configured_input.as_bytes());
424        let execution_policy_digest =
425            CandidateTraceDigest::of_bytes(&execution_policy.canonical_bytes());
426        let execution_policy_complete = execution_policy.is_complete();
427        let input_provenance_digest = input_provenance
428            .as_ref()
429            .map(|provenance| CandidateTraceDigest::of_bytes(&provenance.canonical_bytes()));
430        let resolved_phase_plan_digest =
431            CandidateTraceDigest::of_bytes(&resolved_phase_plan.canonical_bytes());
432        let resolved_phase_plan_complete = resolved_phase_plan.is_complete();
433        Self {
434            format_version: CANDIDATE_TRACE_FORMAT_VERSION,
435            configured_input,
436            configured_input_digest,
437            execution_policy,
438            execution_policy_digest,
439            execution_policy_complete,
440            input_provenance,
441            input_provenance_digest,
442            qualified_run_provenance: None,
443            resolved_phase_plan,
444            resolved_phase_plan_digest,
445            resolved_phase_plan_complete,
446        }
447    }
448
449    pub fn new_qualified(
450        configured_input: String,
451        execution_policy: CandidateTraceExecutionPolicy,
452        resolved_phase_plan: CandidateTracePhasePlan,
453        qualified_run_provenance: QualifiedCandidateTraceRunProvenance,
454    ) -> Self {
455        let mut header = Self::new(
456            configured_input,
457            execution_policy,
458            resolved_phase_plan,
459            Some(qualified_run_provenance.input_provenance().clone()),
460        );
461        header.qualified_run_provenance = Some(qualified_run_provenance);
462        header
463    }
464}
465
466/// The engine path that consumed a candidate.
467#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
468pub enum CandidateTraceSource {
469    Construction,
470    LocalSearch,
471    VariableNeighborhoodDescent,
472    KOpt,
473    ListRoundRobinConstruction,
474    ListCheapestInsertionTrial,
475    ListRegretInsertionTrial,
476    ListClarkeWrightSavings,
477    ListClarkeWrightMerge,
478    ListClarkeWrightCompletionInsertion,
479    ListKOptReconnection,
480    ListRegretOwnerAppend,
481}
482
483impl CandidateTraceSource {
484    const fn code(self) -> u8 {
485        match self {
486            Self::Construction => 1,
487            Self::LocalSearch => 2,
488            Self::VariableNeighborhoodDescent => 3,
489            Self::KOpt => 4,
490            Self::ListRoundRobinConstruction => 5,
491            Self::ListCheapestInsertionTrial => 6,
492            Self::ListRegretInsertionTrial => 7,
493            Self::ListClarkeWrightSavings => 8,
494            Self::ListClarkeWrightMerge => 9,
495            Self::ListClarkeWrightCompletionInsertion => 10,
496            Self::ListKOptReconnection => 11,
497            Self::ListRegretOwnerAppend => 12,
498        }
499    }
500}
501
502/// One state transition for a retained candidate-pull entry.
503///
504/// The events are recorded in engine order. This lets a bounded trace prove
505/// not only which candidates were pulled, but which one was evaluated,
506/// rejected, selected, and actually applied.
507#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
508pub enum CandidateTraceDisposition {
509    /// The engine stopped after the pull and before it evaluated this
510    /// candidate.
511    InterruptedBeforeEvaluation,
512    /// The candidate reached evaluation.
513    Evaluated,
514    /// Evaluation determined that the candidate was not doable.
515    NotDoable,
516    /// A hard-improvement requirement rejected the evaluated candidate.
517    RejectedByHardImprovement,
518    /// A score-improvement requirement rejected the evaluated candidate.
519    RejectedByScoreImprovement,
520    /// The configured acceptor rejected the evaluated candidate.
521    AcceptorRejected,
522    /// The candidate was accepted/evaluated but lost to a forager choice.
523    ForagerIgnored,
524    /// The candidate was selected for commit.
525    Selected,
526    /// The selected candidate was applied to the working solution.
527    Applied,
528    /// Parallel evaluation pulled this candidate ahead of the sequential
529    /// decision point; the step ended before the candidate was consumed.
530    DiscardedSpeculative,
531}
532
533impl CandidateTraceDisposition {
534    const fn code(self) -> u8 {
535        match self {
536            Self::InterruptedBeforeEvaluation => 1,
537            Self::Evaluated => 2,
538            Self::NotDoable => 3,
539            Self::RejectedByHardImprovement => 4,
540            Self::RejectedByScoreImprovement => 5,
541            Self::AcceptorRejected => 6,
542            Self::ForagerIgnored => 7,
543            Self::Selected => 8,
544            Self::Applied => 9,
545            Self::DiscardedSpeculative => 10,
546        }
547    }
548
549    const fn is_terminal(self) -> bool {
550        matches!(
551            self,
552            Self::InterruptedBeforeEvaluation
553                | Self::NotDoable
554                | Self::RejectedByHardImprovement
555                | Self::RejectedByScoreImprovement
556                | Self::AcceptorRejected
557                | Self::ForagerIgnored
558                | Self::Applied
559                | Self::DiscardedSpeculative
560        )
561    }
562}
563
564/// Opaque handle for one retained pull. It exists only after the recorder
565/// accepted capacity, so trace-off and overflow paths cannot allocate or
566/// update per-candidate diagnostic state.
567#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
568pub(crate) struct CandidateTracePullToken {
569    ordinal: u64,
570}
571
572/// A descriptor/entity location that a construction candidate was generated
573/// for.  It is absent for local-search candidates.
574#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
575pub struct CandidateTraceConstructionTarget {
576    pub descriptor_index: usize,
577    pub entity_index: usize,
578}
579
580/// Canonical, owned identity for one traced candidate.
581///
582/// This is deliberately structured rather than debug-formatted or pointer
583/// based. The grammar supports a leaf operation with its own logical scope
584/// and an ordered composite of independently scoped child identities. That
585/// means grouped and cartesian moves do not have to flatten cross-descriptor
586/// edits into a misleading parent scope. The framing preserves every field
587/// and every list boundary, so a trace consumer can compare exact candidate
588/// ordering without relying on a process-local hash.
589#[derive(Debug, Clone, PartialEq, Eq)]
590pub enum CandidateTraceIdentity {
591    /// One logical operation scoped to a descriptor and, when applicable, a
592    /// planning variable.
593    Operation(CandidateTraceOperationIdentity),
594    /// An ordered composition of logical child operations. The composite has
595    /// no implicit descriptor/variable scope; every child carries its own.
596    Composite(CandidateTraceCompositeIdentity),
597}
598
599/// Canonical identity for an explicit non-cursor operation performed by a
600/// specialized list construction/search engine.
601///
602/// The operation name is framed verbatim (never hashed), and components are
603/// ordered raw coordinates chosen by that engine's actual trial loop.
604#[derive(Debug, Clone, PartialEq, Eq)]
605pub struct CandidateTraceOperationIdentity {
606    pub descriptor_index: usize,
607    /// Present for a move-family identity and absent for a phase-local
608    /// specialized operation that has no variable scope.
609    pub variable_name: Option<String>,
610    /// Explicit family/operation grammar token, never a pointer, debug value,
611    /// or tabu-derived hash.
612    pub operation: String,
613    pub components: Vec<CandidateTraceCoordinate>,
614}
615
616/// Canonical identity for a compound/cartesian move.
617///
618/// `operation` describes composition semantics (for example
619/// `compound_scalar` or `cartesian_product`) while `children` preserves the
620/// exact generation/execution order. Children may themselves be composites.
621#[derive(Debug, Clone, PartialEq, Eq)]
622pub struct CandidateTraceCompositeIdentity {
623    pub operation: String,
624    pub children: Vec<CandidateTraceIdentity>,
625}
626
627/// One explicitly framed logical-coordinate component.
628///
629/// `Absent` is distinct from every integer value, so an unassigned scalar
630/// target never needs a magic sentinel. `Bytes` is reserved for a declared
631/// pure typed-value codec; it is never populated from `Debug` output.
632#[derive(Debug, Clone, PartialEq, Eq, Hash)]
633pub enum CandidateTraceCoordinate {
634    Unsigned(u64),
635    Absent,
636    /// A declared logical token such as a variable name or operation-local
637    /// label. It is framed as text, never debug-rendered or hashed.
638    Text(String),
639    /// A declared pure typed-value codec output. This is intentionally
640    /// distinct from [`Self::Text`] and must never be populated from debug or
641    /// tabu metadata.
642    Bytes(Vec<u8>),
643}
644
645impl From<u64> for CandidateTraceCoordinate {
646    fn from(value: u64) -> Self {
647        Self::Unsigned(value)
648    }
649}
650
651impl From<usize> for CandidateTraceCoordinate {
652    fn from(value: usize) -> Self {
653        Self::Unsigned(value as u64)
654    }
655}
656
657impl From<Option<usize>> for CandidateTraceCoordinate {
658    fn from(value: Option<usize>) -> Self {
659        value.map_or(Self::Absent, Self::from)
660    }
661}
662
663impl From<String> for CandidateTraceCoordinate {
664    fn from(value: String) -> Self {
665        Self::Text(value)
666    }
667}
668
669impl From<&str> for CandidateTraceCoordinate {
670    fn from(value: &str) -> Self {
671        Self::Text(value.to_owned())
672    }
673}
674
675impl CandidateTraceIdentity {
676    pub fn operation<I, T>(
677        descriptor_index: usize,
678        operation: impl Into<String>,
679        components: I,
680    ) -> Self
681    where
682        I: IntoIterator<Item = T>,
683        T: Into<CandidateTraceCoordinate>,
684    {
685        Self::Operation(CandidateTraceOperationIdentity {
686            descriptor_index,
687            variable_name: None,
688            operation: operation.into(),
689            components: components.into_iter().map(Into::into).collect(),
690        })
691    }
692
693    pub fn logical_move<I, T>(
694        descriptor_index: usize,
695        variable_name: impl Into<String>,
696        family: impl Into<String>,
697        coordinates: I,
698    ) -> Self
699    where
700        I: IntoIterator<Item = T>,
701        T: Into<CandidateTraceCoordinate>,
702    {
703        Self::Operation(CandidateTraceOperationIdentity {
704            descriptor_index,
705            variable_name: Some(variable_name.into()),
706            operation: family.into(),
707            components: coordinates.into_iter().map(Into::into).collect(),
708        })
709    }
710
711    /// Builds an ordered composite identity without inventing a shared scope
712    /// for child operations that may target different descriptors/variables.
713    pub fn composite(
714        operation: impl Into<String>,
715        children: impl IntoIterator<Item = CandidateTraceIdentity>,
716    ) -> Self {
717        Self::Composite(CandidateTraceCompositeIdentity {
718            operation: operation.into(),
719            children: children.into_iter().collect(),
720        })
721    }
722
723    pub fn canonical_bytes(&self) -> Vec<u8> {
724        let mut out = Vec::new();
725        self.append_canonical_bytes(&mut out);
726        out
727    }
728
729    fn append_canonical_bytes(&self, out: &mut Vec<u8>) {
730        match self {
731            Self::Operation(identity) => {
732                out.push(0x4f);
733                append_usize(out, identity.descriptor_index);
734                match &identity.variable_name {
735                    Some(variable_name) => {
736                        append_bool(out, true);
737                        append_string(out, variable_name);
738                    }
739                    None => append_bool(out, false),
740                }
741                append_string(out, &identity.operation);
742                append_coordinate_list(out, &identity.components);
743            }
744            Self::Composite(identity) => {
745                out.push(0x43);
746                append_string(out, &identity.operation);
747                append_len(out, identity.children.len());
748                for child in &identity.children {
749                    child.append_canonical_bytes(out);
750                }
751            }
752        }
753    }
754}
755
756/// One ordered candidate pull retained by an enabled trace.
757#[derive(Debug, Clone, PartialEq, Eq)]
758pub struct CandidatePullTelemetry {
759    /// Monotonic zero-based ordinal across all instrumented engine pulls.
760    pub ordinal: u64,
761    pub source: CandidateTraceSource,
762    pub phase_index: usize,
763    pub phase_type: String,
764    pub step_index: u64,
765    pub selector_index: Option<usize>,
766    /// Source-local cursor/trial coordinate reported by the engine.
767    ///
768    /// This is useful when inspecting one implementation, but it is not a
769    /// cross-representation comparison key: specialized construction loops
770    /// can reuse local coordinates across owners or passes. Consumers compare
771    /// the globally monotonic [`Self::ordinal`] together with `identity`.
772    pub candidate_index: usize,
773    pub construction_target: Option<CandidateTraceConstructionTarget>,
774    /// `None` explicitly means this move family did not provide a canonical
775    /// logical-coordinate identity. Such a trace is not qualifying evidence.
776    pub identity: Option<CandidateTraceIdentity>,
777    /// Ordered engine decisions for this captured pull. A terminal detail
778    /// trace must finish every retained pull with a terminal disposition.
779    pub dispositions: Vec<CandidateTraceDisposition>,
780}
781
782impl CandidatePullTelemetry {
783    fn append_canonical_bytes(&self, out: &mut Vec<u8>) {
784        out.push(0x45);
785        append_u64(out, self.ordinal);
786        out.push(self.source.code());
787        append_usize(out, self.phase_index);
788        append_string(out, &self.phase_type);
789        append_u64(out, self.step_index);
790        append_option_usize(out, self.selector_index);
791        append_usize(out, self.candidate_index);
792        match self.construction_target {
793            Some(target) => {
794                append_bool(out, true);
795                append_usize(out, target.descriptor_index);
796                append_usize(out, target.entity_index);
797            }
798            None => append_bool(out, false),
799        }
800        match &self.identity {
801            Some(identity) => {
802                append_bool(out, true);
803                identity.append_canonical_bytes(out);
804            }
805            None => append_bool(out, false),
806        }
807        append_len(out, self.dispositions.len());
808        for disposition in &self.dispositions {
809            out.push(disposition.code());
810        }
811    }
812
813    fn has_terminal_disposition(&self) -> bool {
814        self.dispositions
815            .last()
816            .is_some_and(|disposition| disposition.is_terminal())
817    }
818}
819
820/// Bounded trace emitted in final/pause snapshots for an enabled run.
821#[derive(Debug, Clone, PartialEq, Eq)]
822pub struct CandidateTraceTelemetry {
823    pub header: CandidateTraceHeader,
824    pub max_entries: usize,
825    /// All instrumented pulls, including pulls beyond `max_entries`.
826    pub total_pulls: u64,
827    /// The retained ordered prefix.  This is never larger than `max_entries`.
828    pub pulls: Vec<CandidatePullTelemetry>,
829    /// True when the retained prefix is not the full pull stream.
830    pub truncated: bool,
831    /// Stable checksum of the retained canonical prefix only.
832    pub prefix_digest: CandidateTraceDigest,
833    /// Number of retained pulls without an explicit logical-coordinate
834    /// identity. A non-zero value makes the trace non-qualifying.
835    pub unencoded_identity_count: u64,
836    /// A staged runtime may know only a provisional phase plan while it is
837    /// paused.  The executor supplies the complete plan once at terminal
838    /// completion; do not infer one from configuration or observed pulls.
839    resolved_phase_plan_finalized: bool,
840    next_phase_index: usize,
841}
842
843impl CandidateTraceTelemetry {
844    pub(crate) fn new(header: CandidateTraceHeader, max_entries: usize) -> Self {
845        Self {
846            header,
847            max_entries,
848            total_pulls: 0,
849            // Do not reserve the configured ceiling up front: a diagnostic
850            // cap can be intentionally large while a short run pulls only a
851            // handful of candidates.
852            pulls: Vec::new(),
853            truncated: false,
854            prefix_digest: CandidateTraceDigest::empty(),
855            unencoded_identity_count: 0,
856            resolved_phase_plan_finalized: false,
857            next_phase_index: 0,
858        }
859    }
860
861    /// Installs the caller-supplied terminal resolved plan exactly once.
862    ///
863    /// This deliberately updates only phase-plan provenance and its derived
864    /// fields. Configuration, execution policy, input provenance, and
865    /// qualified-run attestation remain untouched. In particular, this method
866    /// never attempts to reconstruct a plan from candidate pulls or a
867    /// partially executed configuration.
868    pub(crate) fn finalize_resolved_phase_plan(
869        &mut self,
870        resolved_phase_plan: CandidateTracePhasePlan,
871    ) {
872        assert!(
873            !self.resolved_phase_plan_finalized,
874            "candidate trace resolved phase plan may be finalized only once"
875        );
876        let resolved_phase_plan_digest =
877            CandidateTraceDigest::of_bytes(&resolved_phase_plan.canonical_bytes());
878        let resolved_phase_plan_complete = resolved_phase_plan.is_complete();
879        self.header.resolved_phase_plan = resolved_phase_plan;
880        self.header.resolved_phase_plan_digest = resolved_phase_plan_digest;
881        self.header.resolved_phase_plan_complete = resolved_phase_plan_complete;
882        self.resolved_phase_plan_finalized = true;
883    }
884
885    pub fn is_complete(&self) -> bool {
886        !self.truncated
887            && self.total_pulls == self.pulls.len() as u64
888            && self.unencoded_identity_count == 0
889            && self
890                .pulls
891                .iter()
892                .all(CandidatePullTelemetry::has_terminal_disposition)
893    }
894
895    /// Whether this trace has complete *engine execution* provenance.
896    ///
897    /// This is intentionally separate from [`Self::is_complete`]: a bounded
898    /// pull stream can be internally complete while the caller has not yet
899    /// supplied a verifiable model/instance/initial-state attestation or a
900    /// phase has correctly declared its graph opaque.
901    pub fn has_complete_execution_provenance(&self) -> bool {
902        self.header.execution_policy_complete && self.header.resolved_phase_plan_complete
903    }
904
905    pub fn provenance_status(&self) -> CandidateTraceProvenanceStatus {
906        let input_provenance = match self.header.input_provenance.as_ref() {
907            None => CandidateTraceInputProvenanceStatus::Absent,
908            Some(_) => CandidateTraceInputProvenanceStatus::ExternallyAttested,
909        };
910        CandidateTraceProvenanceStatus {
911            execution_policy_complete: self.header.execution_policy_complete,
912            resolved_phase_plan_complete: self.header.resolved_phase_plan_complete,
913            input_provenance,
914            qualification: self
915                .header
916                .qualified_run_provenance
917                .as_ref()
918                .map_or(CandidateTraceQualificationStatus::NotRequested, |_| {
919                    CandidateTraceQualificationStatus::Qualified
920                }),
921        }
922    }
923
924    /// Returns the immutable attestation only when the caller explicitly
925    /// requested qualified diagnostics. A normal trace with optional digests
926    /// remains normal and never gets silently upgraded.
927    pub fn require_qualified_run_provenance(
928        &self,
929    ) -> Result<&QualifiedCandidateTraceRunProvenance, CandidateTraceQualificationError> {
930        self.header
931            .qualified_run_provenance
932            .as_ref()
933            .ok_or(CandidateTraceQualificationError::QualificationNotRequested)
934    }
935
936    pub(crate) fn prepare_pull(&mut self) -> CandidateTraceRecordDecision {
937        let ordinal = self.total_pulls;
938        self.total_pulls = self.total_pulls.saturating_add(1);
939        if self.pulls.len() < self.max_entries {
940            CandidateTraceRecordDecision::Capture { ordinal }
941        } else {
942            self.truncated = true;
943            CandidateTraceRecordDecision::Overflow
944        }
945    }
946
947    pub(crate) fn begin_phase(&mut self) -> usize {
948        let phase_index = self.next_phase_index;
949        self.next_phase_index = self.next_phase_index.saturating_add(1);
950        phase_index
951    }
952
953    pub(crate) fn push_prepared(
954        &mut self,
955        pull: CandidatePullTelemetry,
956    ) -> CandidateTracePullToken {
957        debug_assert!(self.pulls.len() < self.max_entries);
958        debug_assert_eq!(pull.ordinal, self.total_pulls.saturating_sub(1));
959        if pull.identity.is_none() {
960            self.unencoded_identity_count = self.unencoded_identity_count.saturating_add(1);
961        }
962        let token = CandidateTracePullToken {
963            ordinal: pull.ordinal,
964        };
965        self.pulls.push(pull);
966        token
967    }
968
969    pub(crate) fn record_disposition(
970        &mut self,
971        token: CandidateTracePullToken,
972        disposition: CandidateTraceDisposition,
973    ) {
974        let Some(pull) = self.pulls.get_mut(token.ordinal as usize) else {
975            debug_assert!(false, "candidate trace token must point at a retained pull");
976            return;
977        };
978        debug_assert_eq!(pull.ordinal, token.ordinal);
979        pull.dispositions.push(disposition);
980    }
981
982    /// Takes a coherent diagnostic copy and refreshes the canonical prefix
983    /// checksum after all mutable disposition transitions. Normal progress
984    /// snapshots never call this because trace detail is deliberately
985    /// detached from control-plane telemetry.
986    pub(crate) fn snapshot(&self) -> Self {
987        let mut snapshot = self.clone();
988        snapshot.prefix_digest = CandidateTraceDigest::empty();
989        for pull in &snapshot.pulls {
990            let mut bytes = Vec::new();
991            pull.append_canonical_bytes(&mut bytes);
992            snapshot.prefix_digest.update(&bytes);
993        }
994        snapshot
995    }
996}
997
998#[derive(Debug, Clone, Copy, PartialEq, Eq)]
999pub(crate) enum CandidateTraceRecordDecision {
1000    Disabled,
1001    Capture { ordinal: u64 },
1002    Overflow,
1003}
1004
1005fn append_bool(out: &mut Vec<u8>, value: bool) {
1006    out.push(u8::from(value));
1007}
1008
1009fn append_len(out: &mut Vec<u8>, value: usize) {
1010    append_u64(
1011        out,
1012        u64::try_from(value).expect("candidate trace lengths must fit in u64"),
1013    );
1014}
1015
1016fn append_usize(out: &mut Vec<u8>, value: usize) {
1017    append_len(out, value);
1018}
1019
1020fn append_u64(out: &mut Vec<u8>, value: u64) {
1021    out.extend_from_slice(&value.to_le_bytes());
1022}
1023
1024fn append_option_usize(out: &mut Vec<u8>, value: Option<usize>) {
1025    match value {
1026        Some(value) => {
1027            append_bool(out, true);
1028            append_usize(out, value);
1029        }
1030        None => append_bool(out, false),
1031    }
1032}
1033
1034fn append_optional_external_digest(out: &mut Vec<u8>, value: Option<CandidateTraceExternalDigest>) {
1035    match value {
1036        Some(value) => {
1037            append_bool(out, true);
1038            value.append_canonical_bytes(out);
1039        }
1040        None => append_bool(out, false),
1041    }
1042}
1043
1044fn append_string(out: &mut Vec<u8>, value: &str) {
1045    append_len(out, value.len());
1046    out.extend_from_slice(value.as_bytes());
1047}
1048
1049fn append_coordinate_list(out: &mut Vec<u8>, values: &[CandidateTraceCoordinate]) {
1050    append_len(out, values.len());
1051    for value in values {
1052        match value {
1053            CandidateTraceCoordinate::Unsigned(value) => {
1054                out.push(1);
1055                append_u64(out, *value);
1056            }
1057            CandidateTraceCoordinate::Absent => out.push(2),
1058            CandidateTraceCoordinate::Text(value) => {
1059                out.push(3);
1060                append_string(out, value);
1061            }
1062            CandidateTraceCoordinate::Bytes(value) => {
1063                out.push(4);
1064                append_len(out, value.len());
1065                out.extend_from_slice(value);
1066            }
1067        }
1068    }
1069}