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}
529
530impl CandidateTraceDisposition {
531    const fn code(self) -> u8 {
532        match self {
533            Self::InterruptedBeforeEvaluation => 1,
534            Self::Evaluated => 2,
535            Self::NotDoable => 3,
536            Self::RejectedByHardImprovement => 4,
537            Self::RejectedByScoreImprovement => 5,
538            Self::AcceptorRejected => 6,
539            Self::ForagerIgnored => 7,
540            Self::Selected => 8,
541            Self::Applied => 9,
542        }
543    }
544
545    const fn is_terminal(self) -> bool {
546        matches!(
547            self,
548            Self::InterruptedBeforeEvaluation
549                | Self::NotDoable
550                | Self::RejectedByHardImprovement
551                | Self::RejectedByScoreImprovement
552                | Self::AcceptorRejected
553                | Self::ForagerIgnored
554                | Self::Applied
555        )
556    }
557}
558
559/// Opaque handle for one retained pull. It exists only after the recorder
560/// accepted capacity, so trace-off and overflow paths cannot allocate or
561/// update per-candidate diagnostic state.
562#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
563pub(crate) struct CandidateTracePullToken {
564    ordinal: u64,
565}
566
567/// A descriptor/entity location that a construction candidate was generated
568/// for.  It is absent for local-search candidates.
569#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
570pub struct CandidateTraceConstructionTarget {
571    pub descriptor_index: usize,
572    pub entity_index: usize,
573}
574
575/// Canonical, owned identity for one traced candidate.
576///
577/// This is deliberately structured rather than debug-formatted or pointer
578/// based. The grammar supports a leaf operation with its own logical scope
579/// and an ordered composite of independently scoped child identities. That
580/// means grouped and cartesian moves do not have to flatten cross-descriptor
581/// edits into a misleading parent scope. The framing preserves every field
582/// and every list boundary, so a trace consumer can compare exact candidate
583/// ordering without relying on a process-local hash.
584#[derive(Debug, Clone, PartialEq, Eq)]
585pub enum CandidateTraceIdentity {
586    /// One logical operation scoped to a descriptor and, when applicable, a
587    /// planning variable.
588    Operation(CandidateTraceOperationIdentity),
589    /// An ordered composition of logical child operations. The composite has
590    /// no implicit descriptor/variable scope; every child carries its own.
591    Composite(CandidateTraceCompositeIdentity),
592}
593
594/// Canonical identity for an explicit non-cursor operation performed by a
595/// specialized list construction/search engine.
596///
597/// The operation name is framed verbatim (never hashed), and components are
598/// ordered raw coordinates chosen by that engine's actual trial loop.
599#[derive(Debug, Clone, PartialEq, Eq)]
600pub struct CandidateTraceOperationIdentity {
601    pub descriptor_index: usize,
602    /// Present for a move-family identity and absent for a phase-local
603    /// specialized operation that has no variable scope.
604    pub variable_name: Option<String>,
605    /// Explicit family/operation grammar token, never a pointer, debug value,
606    /// or tabu-derived hash.
607    pub operation: String,
608    pub components: Vec<CandidateTraceCoordinate>,
609}
610
611/// Canonical identity for a compound/cartesian move.
612///
613/// `operation` describes composition semantics (for example
614/// `compound_scalar` or `cartesian_product`) while `children` preserves the
615/// exact generation/execution order. Children may themselves be composites.
616#[derive(Debug, Clone, PartialEq, Eq)]
617pub struct CandidateTraceCompositeIdentity {
618    pub operation: String,
619    pub children: Vec<CandidateTraceIdentity>,
620}
621
622/// One explicitly framed logical-coordinate component.
623///
624/// `Absent` is distinct from every integer value, so an unassigned scalar
625/// target never needs a magic sentinel. `Bytes` is reserved for a declared
626/// pure typed-value codec; it is never populated from `Debug` output.
627#[derive(Debug, Clone, PartialEq, Eq, Hash)]
628pub enum CandidateTraceCoordinate {
629    Unsigned(u64),
630    Absent,
631    /// A declared logical token such as a variable name or operation-local
632    /// label. It is framed as text, never debug-rendered or hashed.
633    Text(String),
634    /// A declared pure typed-value codec output. This is intentionally
635    /// distinct from [`Self::Text`] and must never be populated from debug or
636    /// tabu metadata.
637    Bytes(Vec<u8>),
638}
639
640impl From<u64> for CandidateTraceCoordinate {
641    fn from(value: u64) -> Self {
642        Self::Unsigned(value)
643    }
644}
645
646impl From<usize> for CandidateTraceCoordinate {
647    fn from(value: usize) -> Self {
648        Self::Unsigned(value as u64)
649    }
650}
651
652impl From<Option<usize>> for CandidateTraceCoordinate {
653    fn from(value: Option<usize>) -> Self {
654        value.map_or(Self::Absent, Self::from)
655    }
656}
657
658impl From<String> for CandidateTraceCoordinate {
659    fn from(value: String) -> Self {
660        Self::Text(value)
661    }
662}
663
664impl From<&str> for CandidateTraceCoordinate {
665    fn from(value: &str) -> Self {
666        Self::Text(value.to_owned())
667    }
668}
669
670impl CandidateTraceIdentity {
671    pub fn operation<I, T>(
672        descriptor_index: usize,
673        operation: impl Into<String>,
674        components: I,
675    ) -> Self
676    where
677        I: IntoIterator<Item = T>,
678        T: Into<CandidateTraceCoordinate>,
679    {
680        Self::Operation(CandidateTraceOperationIdentity {
681            descriptor_index,
682            variable_name: None,
683            operation: operation.into(),
684            components: components.into_iter().map(Into::into).collect(),
685        })
686    }
687
688    pub fn logical_move<I, T>(
689        descriptor_index: usize,
690        variable_name: impl Into<String>,
691        family: impl Into<String>,
692        coordinates: I,
693    ) -> Self
694    where
695        I: IntoIterator<Item = T>,
696        T: Into<CandidateTraceCoordinate>,
697    {
698        Self::Operation(CandidateTraceOperationIdentity {
699            descriptor_index,
700            variable_name: Some(variable_name.into()),
701            operation: family.into(),
702            components: coordinates.into_iter().map(Into::into).collect(),
703        })
704    }
705
706    /// Builds an ordered composite identity without inventing a shared scope
707    /// for child operations that may target different descriptors/variables.
708    pub fn composite(
709        operation: impl Into<String>,
710        children: impl IntoIterator<Item = CandidateTraceIdentity>,
711    ) -> Self {
712        Self::Composite(CandidateTraceCompositeIdentity {
713            operation: operation.into(),
714            children: children.into_iter().collect(),
715        })
716    }
717
718    pub fn canonical_bytes(&self) -> Vec<u8> {
719        let mut out = Vec::new();
720        self.append_canonical_bytes(&mut out);
721        out
722    }
723
724    fn append_canonical_bytes(&self, out: &mut Vec<u8>) {
725        match self {
726            Self::Operation(identity) => {
727                out.push(0x4f);
728                append_usize(out, identity.descriptor_index);
729                match &identity.variable_name {
730                    Some(variable_name) => {
731                        append_bool(out, true);
732                        append_string(out, variable_name);
733                    }
734                    None => append_bool(out, false),
735                }
736                append_string(out, &identity.operation);
737                append_coordinate_list(out, &identity.components);
738            }
739            Self::Composite(identity) => {
740                out.push(0x43);
741                append_string(out, &identity.operation);
742                append_len(out, identity.children.len());
743                for child in &identity.children {
744                    child.append_canonical_bytes(out);
745                }
746            }
747        }
748    }
749}
750
751/// One ordered candidate pull retained by an enabled trace.
752#[derive(Debug, Clone, PartialEq, Eq)]
753pub struct CandidatePullTelemetry {
754    /// Monotonic zero-based ordinal across all instrumented engine pulls.
755    pub ordinal: u64,
756    pub source: CandidateTraceSource,
757    pub phase_index: usize,
758    pub phase_type: String,
759    pub step_index: u64,
760    pub selector_index: Option<usize>,
761    /// Source-local cursor/trial coordinate reported by the engine.
762    ///
763    /// This is useful when inspecting one implementation, but it is not a
764    /// cross-representation comparison key: specialized construction loops
765    /// can reuse local coordinates across owners or passes. Consumers compare
766    /// the globally monotonic [`Self::ordinal`] together with `identity`.
767    pub candidate_index: usize,
768    pub construction_target: Option<CandidateTraceConstructionTarget>,
769    /// `None` explicitly means this move family did not provide a canonical
770    /// logical-coordinate identity. Such a trace is not qualifying evidence.
771    pub identity: Option<CandidateTraceIdentity>,
772    /// Ordered engine decisions for this captured pull. A terminal detail
773    /// trace must finish every retained pull with a terminal disposition.
774    pub dispositions: Vec<CandidateTraceDisposition>,
775}
776
777impl CandidatePullTelemetry {
778    fn append_canonical_bytes(&self, out: &mut Vec<u8>) {
779        out.push(0x45);
780        append_u64(out, self.ordinal);
781        out.push(self.source.code());
782        append_usize(out, self.phase_index);
783        append_string(out, &self.phase_type);
784        append_u64(out, self.step_index);
785        append_option_usize(out, self.selector_index);
786        append_usize(out, self.candidate_index);
787        match self.construction_target {
788            Some(target) => {
789                append_bool(out, true);
790                append_usize(out, target.descriptor_index);
791                append_usize(out, target.entity_index);
792            }
793            None => append_bool(out, false),
794        }
795        match &self.identity {
796            Some(identity) => {
797                append_bool(out, true);
798                identity.append_canonical_bytes(out);
799            }
800            None => append_bool(out, false),
801        }
802        append_len(out, self.dispositions.len());
803        for disposition in &self.dispositions {
804            out.push(disposition.code());
805        }
806    }
807
808    fn has_terminal_disposition(&self) -> bool {
809        self.dispositions
810            .last()
811            .is_some_and(|disposition| disposition.is_terminal())
812    }
813}
814
815/// Bounded trace emitted in final/pause snapshots for an enabled run.
816#[derive(Debug, Clone, PartialEq, Eq)]
817pub struct CandidateTraceTelemetry {
818    pub header: CandidateTraceHeader,
819    pub max_entries: usize,
820    /// All instrumented pulls, including pulls beyond `max_entries`.
821    pub total_pulls: u64,
822    /// The retained ordered prefix.  This is never larger than `max_entries`.
823    pub pulls: Vec<CandidatePullTelemetry>,
824    /// True when the retained prefix is not the full pull stream.
825    pub truncated: bool,
826    /// Stable checksum of the retained canonical prefix only.
827    pub prefix_digest: CandidateTraceDigest,
828    /// Number of retained pulls without an explicit logical-coordinate
829    /// identity. A non-zero value makes the trace non-qualifying.
830    pub unencoded_identity_count: u64,
831    /// A staged runtime may know only a provisional phase plan while it is
832    /// paused.  The executor supplies the complete plan once at terminal
833    /// completion; do not infer one from configuration or observed pulls.
834    resolved_phase_plan_finalized: bool,
835    next_phase_index: usize,
836}
837
838impl CandidateTraceTelemetry {
839    pub(crate) fn new(header: CandidateTraceHeader, max_entries: usize) -> Self {
840        Self {
841            header,
842            max_entries,
843            total_pulls: 0,
844            // Do not reserve the configured ceiling up front: a diagnostic
845            // cap can be intentionally large while a short run pulls only a
846            // handful of candidates.
847            pulls: Vec::new(),
848            truncated: false,
849            prefix_digest: CandidateTraceDigest::empty(),
850            unencoded_identity_count: 0,
851            resolved_phase_plan_finalized: false,
852            next_phase_index: 0,
853        }
854    }
855
856    /// Installs the caller-supplied terminal resolved plan exactly once.
857    ///
858    /// This deliberately updates only phase-plan provenance and its derived
859    /// fields. Configuration, execution policy, input provenance, and
860    /// qualified-run attestation remain untouched. In particular, this method
861    /// never attempts to reconstruct a plan from candidate pulls or a
862    /// partially executed configuration.
863    pub(crate) fn finalize_resolved_phase_plan(
864        &mut self,
865        resolved_phase_plan: CandidateTracePhasePlan,
866    ) {
867        assert!(
868            !self.resolved_phase_plan_finalized,
869            "candidate trace resolved phase plan may be finalized only once"
870        );
871        let resolved_phase_plan_digest =
872            CandidateTraceDigest::of_bytes(&resolved_phase_plan.canonical_bytes());
873        let resolved_phase_plan_complete = resolved_phase_plan.is_complete();
874        self.header.resolved_phase_plan = resolved_phase_plan;
875        self.header.resolved_phase_plan_digest = resolved_phase_plan_digest;
876        self.header.resolved_phase_plan_complete = resolved_phase_plan_complete;
877        self.resolved_phase_plan_finalized = true;
878    }
879
880    pub fn is_complete(&self) -> bool {
881        !self.truncated
882            && self.total_pulls == self.pulls.len() as u64
883            && self.unencoded_identity_count == 0
884            && self
885                .pulls
886                .iter()
887                .all(CandidatePullTelemetry::has_terminal_disposition)
888    }
889
890    /// Whether this trace has complete *engine execution* provenance.
891    ///
892    /// This is intentionally separate from [`Self::is_complete`]: a bounded
893    /// pull stream can be internally complete while the caller has not yet
894    /// supplied a verifiable model/instance/initial-state attestation or a
895    /// phase has correctly declared its graph opaque.
896    pub fn has_complete_execution_provenance(&self) -> bool {
897        self.header.execution_policy_complete && self.header.resolved_phase_plan_complete
898    }
899
900    pub fn provenance_status(&self) -> CandidateTraceProvenanceStatus {
901        let input_provenance = match self.header.input_provenance.as_ref() {
902            None => CandidateTraceInputProvenanceStatus::Absent,
903            Some(_) => CandidateTraceInputProvenanceStatus::ExternallyAttested,
904        };
905        CandidateTraceProvenanceStatus {
906            execution_policy_complete: self.header.execution_policy_complete,
907            resolved_phase_plan_complete: self.header.resolved_phase_plan_complete,
908            input_provenance,
909            qualification: self
910                .header
911                .qualified_run_provenance
912                .as_ref()
913                .map_or(CandidateTraceQualificationStatus::NotRequested, |_| {
914                    CandidateTraceQualificationStatus::Qualified
915                }),
916        }
917    }
918
919    /// Returns the immutable attestation only when the caller explicitly
920    /// requested qualified diagnostics. A normal trace with optional digests
921    /// remains normal and never gets silently upgraded.
922    pub fn require_qualified_run_provenance(
923        &self,
924    ) -> Result<&QualifiedCandidateTraceRunProvenance, CandidateTraceQualificationError> {
925        self.header
926            .qualified_run_provenance
927            .as_ref()
928            .ok_or(CandidateTraceQualificationError::QualificationNotRequested)
929    }
930
931    pub(crate) fn prepare_pull(&mut self) -> CandidateTraceRecordDecision {
932        let ordinal = self.total_pulls;
933        self.total_pulls = self.total_pulls.saturating_add(1);
934        if self.pulls.len() < self.max_entries {
935            CandidateTraceRecordDecision::Capture { ordinal }
936        } else {
937            self.truncated = true;
938            CandidateTraceRecordDecision::Overflow
939        }
940    }
941
942    pub(crate) fn begin_phase(&mut self) -> usize {
943        let phase_index = self.next_phase_index;
944        self.next_phase_index = self.next_phase_index.saturating_add(1);
945        phase_index
946    }
947
948    pub(crate) fn push_prepared(
949        &mut self,
950        pull: CandidatePullTelemetry,
951    ) -> CandidateTracePullToken {
952        debug_assert!(self.pulls.len() < self.max_entries);
953        debug_assert_eq!(pull.ordinal, self.total_pulls.saturating_sub(1));
954        if pull.identity.is_none() {
955            self.unencoded_identity_count = self.unencoded_identity_count.saturating_add(1);
956        }
957        let token = CandidateTracePullToken {
958            ordinal: pull.ordinal,
959        };
960        self.pulls.push(pull);
961        token
962    }
963
964    pub(crate) fn record_disposition(
965        &mut self,
966        token: CandidateTracePullToken,
967        disposition: CandidateTraceDisposition,
968    ) {
969        let Some(pull) = self.pulls.get_mut(token.ordinal as usize) else {
970            debug_assert!(false, "candidate trace token must point at a retained pull");
971            return;
972        };
973        debug_assert_eq!(pull.ordinal, token.ordinal);
974        pull.dispositions.push(disposition);
975    }
976
977    /// Takes a coherent diagnostic copy and refreshes the canonical prefix
978    /// checksum after all mutable disposition transitions. Normal progress
979    /// snapshots never call this because trace detail is deliberately
980    /// detached from control-plane telemetry.
981    pub(crate) fn snapshot(&self) -> Self {
982        let mut snapshot = self.clone();
983        snapshot.prefix_digest = CandidateTraceDigest::empty();
984        for pull in &snapshot.pulls {
985            let mut bytes = Vec::new();
986            pull.append_canonical_bytes(&mut bytes);
987            snapshot.prefix_digest.update(&bytes);
988        }
989        snapshot
990    }
991}
992
993#[derive(Debug, Clone, Copy, PartialEq, Eq)]
994pub(crate) enum CandidateTraceRecordDecision {
995    Disabled,
996    Capture { ordinal: u64 },
997    Overflow,
998}
999
1000fn append_bool(out: &mut Vec<u8>, value: bool) {
1001    out.push(u8::from(value));
1002}
1003
1004fn append_len(out: &mut Vec<u8>, value: usize) {
1005    append_u64(
1006        out,
1007        u64::try_from(value).expect("candidate trace lengths must fit in u64"),
1008    );
1009}
1010
1011fn append_usize(out: &mut Vec<u8>, value: usize) {
1012    append_len(out, value);
1013}
1014
1015fn append_u64(out: &mut Vec<u8>, value: u64) {
1016    out.extend_from_slice(&value.to_le_bytes());
1017}
1018
1019fn append_option_usize(out: &mut Vec<u8>, value: Option<usize>) {
1020    match value {
1021        Some(value) => {
1022            append_bool(out, true);
1023            append_usize(out, value);
1024        }
1025        None => append_bool(out, false),
1026    }
1027}
1028
1029fn append_optional_external_digest(out: &mut Vec<u8>, value: Option<CandidateTraceExternalDigest>) {
1030    match value {
1031        Some(value) => {
1032            append_bool(out, true);
1033            value.append_canonical_bytes(out);
1034        }
1035        None => append_bool(out, false),
1036    }
1037}
1038
1039fn append_string(out: &mut Vec<u8>, value: &str) {
1040    append_len(out, value.len());
1041    out.extend_from_slice(value.as_bytes());
1042}
1043
1044fn append_coordinate_list(out: &mut Vec<u8>, values: &[CandidateTraceCoordinate]) {
1045    append_len(out, values.len());
1046    for value in values {
1047        match value {
1048            CandidateTraceCoordinate::Unsigned(value) => {
1049                out.push(1);
1050                append_u64(out, *value);
1051            }
1052            CandidateTraceCoordinate::Absent => out.push(2),
1053            CandidateTraceCoordinate::Text(value) => {
1054                out.push(3);
1055                append_string(out, value);
1056            }
1057            CandidateTraceCoordinate::Bytes(value) => {
1058                out.push(4);
1059                append_len(out, value.len());
1060                out.extend_from_slice(value);
1061            }
1062        }
1063    }
1064}