Skip to main content

infotheory/spec/document/
mod.rs

1//! Canonical top-level specification documents for planner runs and tuning.
2
3#[cfg(feature = "tuner")]
4use super::core::CompiledCompressionBackend;
5use super::core::{CanonicalBytes, CompiledRateBackend};
6use super::{
7    SpecEnvironment, SpecError, SpecResult, compression_backend_to_json_value,
8    parse_compression_backend_json, parse_rate_backend_json, rate_backend_to_json_value,
9};
10use crate::aixi::common::ObservationKeyMode;
11use crate::aixi::common::resolve_random_seed;
12use crate::api::{CompressionBackend, RateBackend};
13use crate::spec::CanonicalJson;
14use std::fmt;
15use std::path::Path;
16use std::sync::Arc;
17
18/// Schema version for canonical top-level spec documents.
19pub const SPEC_DOCUMENT_SCHEMA_VERSION: u32 = 1;
20
21const DOCUMENT_MAGIC: &[u8; 4] = b"itsd";
22const DOCUMENT_BINARY_VERSION: u8 = 1;
23#[cfg(feature = "tuner")]
24const TUNE_CANONICALIZATION_CLASSIFICATION_VERSION: &str = "bounds-v1";
25
26#[cfg(feature = "tuner")]
27pub(crate) const CANDIDATE_EXTERNAL_ASSET_FORBIDDEN: &str = "candidate_external_asset_forbidden";
28
29#[cfg(feature = "tuner")]
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31pub(crate) enum TuneInvalidReason {
32    CandidateExternalAssetForbidden,
33    CandidateOutOfBounds,
34    CandidateCompileError,
35    InvalidActionIndex,
36    InapplicableAction,
37}
38
39#[cfg(feature = "tuner")]
40impl TuneInvalidReason {
41    pub(crate) const fn as_str(self) -> &'static str {
42        match self {
43            Self::CandidateExternalAssetForbidden => CANDIDATE_EXTERNAL_ASSET_FORBIDDEN,
44            Self::CandidateOutOfBounds => "candidate_out_of_bounds",
45            Self::CandidateCompileError => "candidate_compile_error",
46            Self::InvalidActionIndex => "invalid_action_index",
47            Self::InapplicableAction => "inapplicable_action",
48        }
49    }
50}
51
52mod binary;
53mod io;
54mod parser;
55mod pipeline;
56mod serializer;
57mod types;
58
59use binary::{builtin_environment_name, observation_key_mode_name};
60#[cfg(feature = "vm")]
61use binary::{
62    shared_memory_policy_name, vm_fuzz_mutator_name, vm_observation_policy_name,
63    vm_observation_stream_mode_name, vm_payload_encoding_name,
64};
65pub use io::load_spec_document;
66pub use types::*;
67
68impl ValidatedPlannerRunSpec {
69    /// Canonical validated planner-run spec.
70    pub fn canonical_spec(&self) -> &PlannerRunSpec {
71        self.canonical_spec.as_ref()
72    }
73
74    /// Deterministic binary representation of the canonical planner-run spec.
75    pub fn canonical_bytes(&self) -> &CanonicalBytes {
76        &self.canonical_bytes
77    }
78
79    /// Compile the validated planner-run spec into resolved assets and compiled backends.
80    pub fn compile(&self) -> SpecResult<CompiledPlannerRunSpec> {
81        pipeline::compile_validated_planner_run_spec(self)
82    }
83}
84
85#[cfg(feature = "tuner")]
86impl ValidatedTuneSpec {
87    /// Canonical validated tune spec.
88    pub fn canonical_spec(&self) -> &TuneSpec {
89        self.canonical_spec.as_ref()
90    }
91
92    /// Deterministic binary representation of the canonical tune spec.
93    pub fn canonical_bytes(&self) -> &CanonicalBytes {
94        &self.canonical_bytes
95    }
96
97    /// Compile the validated tune request into resolved assets and compiled backends.
98    pub fn compile(&self) -> SpecResult<CompiledTuneSpec> {
99        pipeline::compile_validated_tune_spec(self)
100    }
101}
102
103impl ParsedSpecDocument {
104    /// Parsed document before validation.
105    pub fn document(&self) -> &SpecDocument {
106        &self.document
107    }
108
109    /// Base directory captured at parse time.
110    pub fn base_dir(&self) -> &Path {
111        self.base_dir.as_path()
112    }
113
114    /// Consume this stage wrapper and return the parsed document.
115    pub fn into_document(self) -> SpecDocument {
116        self.document
117    }
118
119    /// Validate this parsed document in the captured parse environment.
120    pub fn validate(self) -> SpecResult<ValidatedSpecDocument> {
121        let env = SpecEnvironment::new(self.base_dir);
122        self.document.validate_in(&env)
123    }
124
125    /// Validate and compile this parsed document in the captured parse environment.
126    pub fn compile(self) -> SpecResult<CompiledSpecDocument> {
127        self.validate()?.compile()
128    }
129}
130
131impl ValidatedSpecDocument {
132    /// Deterministic canonical bytes for this validated document.
133    pub fn canonical_bytes(&self) -> &CanonicalBytes {
134        match self {
135            Self::PlannerRun(validated) => validated.canonical_bytes(),
136            #[cfg(feature = "tuner")]
137            Self::Tune(validated) => validated.canonical_bytes(),
138            Self::RateBackend(validated) => validated.canonical_bytes(),
139            Self::CompressionBackend(validated) => validated.canonical_bytes(),
140        }
141    }
142
143    /// Compile this validated document into runtime-ready form.
144    pub fn compile(&self) -> SpecResult<CompiledSpecDocument> {
145        match self {
146            Self::PlannerRun(validated) => {
147                Ok(CompiledSpecDocument::PlannerRun(validated.compile()?))
148            }
149            #[cfg(feature = "tuner")]
150            Self::Tune(validated) => Ok(CompiledSpecDocument::Tune(validated.compile()?)),
151            Self::RateBackend(validated) => {
152                Ok(CompiledSpecDocument::RateBackend(validated.compile()?))
153            }
154            Self::CompressionBackend(validated) => Ok(CompiledSpecDocument::CompressionBackend(
155                validated.compile()?,
156            )),
157        }
158    }
159}
160
161impl CompiledSpecDocument {
162    /// Deterministic canonical bytes for this compiled document.
163    pub fn canonical_bytes(&self) -> &CanonicalBytes {
164        match self {
165            Self::PlannerRun(compiled) => compiled.canonical_bytes(),
166            #[cfg(feature = "tuner")]
167            Self::Tune(compiled) => compiled.canonical_bytes(),
168            Self::RateBackend(compiled) => compiled.canonical_bytes(),
169            Self::CompressionBackend(compiled) => compiled.canonical_bytes(),
170        }
171    }
172}
173
174impl PlannerRunSpec {
175    /// Validate this planner-run spec and return its canonical binary encoding.
176    pub fn validate_in(&self, env: &SpecEnvironment) -> SpecResult<ValidatedPlannerRunSpec> {
177        let canonical = pipeline::canonicalize_planner_run(self, env)?;
178        Ok(ValidatedPlannerRunSpec {
179            canonical_bytes: CanonicalBytes::from(binary::encode_spec_document_payload(
180                &SpecDocument::PlannerRun(canonical.clone()),
181            )),
182            canonical_spec: Arc::new(canonical),
183            base_dir: env.base_dir().to_path_buf(),
184        })
185    }
186
187    /// Validate this planner-run spec using the default compilation environment.
188    pub fn validate(&self) -> SpecResult<ValidatedPlannerRunSpec> {
189        self.validate_in(&SpecEnvironment::default())
190    }
191
192    /// Validate and compile this planner-run spec using the supplied environment.
193    pub fn compile_in(&self, env: &SpecEnvironment) -> SpecResult<CompiledPlannerRunSpec> {
194        pipeline::compile_planner_run_spec(self, env.base_dir())
195    }
196
197    /// Validate and compile this planner-run spec using the default environment.
198    pub fn compile(&self) -> SpecResult<CompiledPlannerRunSpec> {
199        self.compile_in(&SpecEnvironment::default())
200    }
201}
202
203impl EnvironmentSpec {
204    /// Validate and canonicalize this environment spec against the supplied asset bindings.
205    pub fn validate_in(
206        &self,
207        assets: &[AssetBinding],
208        env: &SpecEnvironment,
209    ) -> SpecResult<EnvironmentSpec> {
210        pipeline::canonicalize_environment_spec(self, assets, env)
211    }
212
213    /// Validate and canonicalize this environment spec using the default environment.
214    pub fn validate(&self, assets: &[AssetBinding]) -> SpecResult<EnvironmentSpec> {
215        self.validate_in(assets, &SpecEnvironment::default())
216    }
217
218    /// Stable canonical kind name for this environment variant.
219    ///
220    /// Used by tooling that prints canonical names without dispatching on the
221    /// payload of each variant.
222    pub fn kind_str(&self) -> &'static str {
223        match self {
224            Self::Builtin { .. } => "builtin",
225            #[cfg(feature = "vm")]
226            Self::NyxVm(_) => "vm",
227        }
228    }
229}
230
231impl BuiltinEnvironmentSpec {
232    /// Stable canonical name string for this builtin environment.
233    ///
234    /// This is the canonical document/CLI identifier used in serialized specs;
235    /// it does not change when new builtins are added.
236    pub fn canonical_name(&self) -> &'static str {
237        match self {
238            Self::TunerBridge => "tuner_bridge",
239            Self::CoinFlip => "coin_flip",
240            Self::BiasedRockPaperScissor => "biased_rock_paper_scissor",
241            Self::KuhnPoker => "kuhn_poker",
242            Self::ExtendedTiger => "extended_tiger",
243            Self::TicTacToe => "tic_tac_toe",
244            Self::Blackjack => "blackjack",
245            Self::Platformer => "platformer",
246        }
247    }
248}
249
250#[cfg(feature = "tuner")]
251impl TuneSpec {
252    /// Validate this tune request and return its canonical binary encoding.
253    pub fn validate_in(&self, env: &SpecEnvironment) -> SpecResult<ValidatedTuneSpec> {
254        let canonical = pipeline::canonicalize_tune_spec(self, env)?;
255        Ok(ValidatedTuneSpec {
256            canonical_bytes: CanonicalBytes::from(binary::encode_spec_document_payload(
257                &SpecDocument::Tune(canonical.clone()),
258            )),
259            canonical_spec: Arc::new(canonical),
260            base_dir: env.base_dir().to_path_buf(),
261        })
262    }
263
264    /// Validate this tune request using the default compilation environment.
265    pub fn validate(&self) -> SpecResult<ValidatedTuneSpec> {
266        self.validate_in(&SpecEnvironment::default())
267    }
268
269    /// Validate and compile this tune request using the supplied environment.
270    pub fn compile_in(&self, env: &SpecEnvironment) -> SpecResult<CompiledTuneSpec> {
271        pipeline::compile_tune_spec(self, env.base_dir())
272    }
273
274    /// Validate and compile this tune request using the default environment.
275    pub fn compile(&self) -> SpecResult<CompiledTuneSpec> {
276        self.compile_in(&SpecEnvironment::default())
277    }
278}
279
280impl SpecDocument {
281    /// Encode this document in the versioned binary document envelope.
282    pub fn to_binary(&self) -> Vec<u8> {
283        binary::encode_spec_document_payload(self)
284    }
285
286    /// Parse a canonical JSON document from a raw JSON value.
287    pub fn parse_json_value(value: &serde_json::Value, base_dir: &Path) -> SpecResult<Self> {
288        parser::parse_spec_document_json_value(value, base_dir)
289    }
290
291    /// Parse a canonical JSON document and return the parsed-stage wrapper.
292    pub fn parse_json_value_staged(
293        value: &serde_json::Value,
294        base_dir: &Path,
295    ) -> SpecResult<ParsedSpecDocument> {
296        let document = Self::parse_json_value(value, base_dir)?;
297        Ok(ParsedSpecDocument {
298            document,
299            base_dir: base_dir.to_path_buf(),
300        })
301    }
302
303    /// Decode a binary spec document.
304    pub fn from_binary(bytes: &[u8], base_dir: &Path) -> SpecResult<Self> {
305        binary::decode_spec_document(bytes, base_dir)
306    }
307
308    /// Decode a binary spec document and return the parsed-stage wrapper.
309    pub fn from_binary_staged(bytes: &[u8], base_dir: &Path) -> SpecResult<ParsedSpecDocument> {
310        let document = Self::from_binary(bytes, base_dir)?;
311        Ok(ParsedSpecDocument {
312            document,
313            base_dir: base_dir.to_path_buf(),
314        })
315    }
316
317    /// Validate this top-level document in the supplied environment.
318    pub fn validate_in(&self, env: &SpecEnvironment) -> SpecResult<ValidatedSpecDocument> {
319        match self {
320            Self::PlannerRun(spec) => Ok(ValidatedSpecDocument::PlannerRun(spec.validate_in(env)?)),
321            #[cfg(feature = "tuner")]
322            Self::Tune(spec) => Ok(ValidatedSpecDocument::Tune(spec.validate_in(env)?)),
323            Self::RateBackend(backend) => Ok(ValidatedSpecDocument::RateBackend(
324                backend.validate_in(env)?,
325            )),
326            Self::CompressionBackend(backend) => Ok(ValidatedSpecDocument::CompressionBackend(
327                backend.validate_in(env)?,
328            )),
329        }
330    }
331
332    /// Validate this top-level document using the default compilation environment.
333    pub fn validate(&self) -> SpecResult<ValidatedSpecDocument> {
334        self.validate_in(&SpecEnvironment::default())
335    }
336
337    /// Validate and compile this top-level document in the supplied environment.
338    pub fn compile_in(&self, env: &SpecEnvironment) -> SpecResult<CompiledSpecDocument> {
339        self.validate_in(env)?.compile()
340    }
341
342    /// Validate and compile this top-level document using the default environment.
343    pub fn compile(&self) -> SpecResult<CompiledSpecDocument> {
344        self.compile_in(&SpecEnvironment::default())
345    }
346
347    /// Stable canonical kind name (matches the document's `"kind"` JSON field).
348    pub fn kind_str(&self) -> &'static str {
349        match self {
350            Self::PlannerRun(_) => "planner_run",
351            #[cfg(feature = "tuner")]
352            Self::Tune(_) => "tune",
353            Self::RateBackend(_) => "rate_backend",
354            Self::CompressionBackend(_) => "compression_backend",
355        }
356    }
357}
358
359impl CanonicalJson for PlannerRunSpec {
360    fn to_canonical_json_value(&self) -> SpecResult<serde_json::Value> {
361        serializer::planner_run_to_json_value(self)
362    }
363}
364
365#[cfg(feature = "tuner")]
366impl CanonicalJson for TuneSpec {
367    fn to_canonical_json_value(&self) -> SpecResult<serde_json::Value> {
368        serializer::tune_spec_to_json_value(self)
369    }
370}
371
372impl CanonicalJson for SpecDocument {
373    fn to_canonical_json_value(&self) -> SpecResult<serde_json::Value> {
374        serializer::spec_document_to_json_value(self)
375    }
376}
377
378impl CompiledPlannerController {
379    /// Compiled predictor backend used by this planner controller.
380    pub fn predictor(&self) -> &CompiledRateBackend {
381        match self {
382            Self::McAixi { predictor, .. } => predictor,
383            Self::AiqiDiscounted { predictor, .. } => predictor,
384            #[cfg(feature = "aixi")]
385            Self::AiqiWarmstartExactJh { predictor, .. } => predictor,
386        }
387    }
388
389    /// Stable canonical kind name string for this controller variant.
390    pub fn kind_str(&self) -> &'static str {
391        match self {
392            Self::McAixi { .. } => "mc_aixi",
393            Self::AiqiDiscounted { .. } => "aiqi_discounted",
394            #[cfg(feature = "aixi")]
395            Self::AiqiWarmstartExactJh { .. } => "aiqi_warmstart_exact_jh",
396        }
397    }
398
399    /// Human-readable predictor backend label.
400    ///
401    /// Backend-local algorithm parameters (such as ROSA's `max_order`) are
402    /// derived from the backend's variant directly.
403    pub fn backend_label(&self) -> String {
404        self.predictor().display_label()
405    }
406}
407
408impl CompiledPlannerRunSpec {
409    /// Canonical planner-run spec used to build this compiled form.
410    pub fn canonical_spec(&self) -> &PlannerRunSpec {
411        self.canonical_spec.as_ref()
412    }
413
414    /// Deterministic canonical bytes for the planner-run document.
415    pub fn canonical_bytes(&self) -> &CanonicalBytes {
416        &self.canonical_bytes
417    }
418
419    /// Resolved assets used when compiling the planner-run document.
420    pub fn resolved_assets(&self) -> &[ResolvedAssetBinding] {
421        self.resolved_assets.as_ref()
422    }
423
424    /// Canonical planner interface metadata.
425    pub fn interface(&self) -> &PlannerInterfaceSpec {
426        &self.interface
427    }
428
429    /// Operational runtime controls.
430    pub fn runtime(&self) -> &PlannerRuntimeSpec {
431        &self.runtime
432    }
433
434    /// Returns the canonical resolved planner runtime seed.
435    pub fn resolved_random_seed(&self) -> u64 {
436        resolve_random_seed(self.runtime.random_seed)
437    }
438
439    /// Compiled planner controller.
440    pub fn controller(&self) -> &CompiledPlannerController {
441        &self.controller
442    }
443
444    /// Number of bits required to encode agent actions.
445    pub fn action_bits(&self) -> usize {
446        self.action_bits
447    }
448}
449
450#[cfg(feature = "tuner")]
451impl CompiledTuneSpec {
452    /// Canonical tune request used to build this compiled form.
453    pub fn canonical_spec(&self) -> &TuneSpec {
454        self.canonical_spec.as_ref()
455    }
456
457    /// Deterministic canonical bytes for the tune request document.
458    pub fn canonical_bytes(&self) -> &CanonicalBytes {
459        &self.canonical_bytes
460    }
461
462    /// Base directory used to resolve relative paths during tune compilation.
463    pub fn base_dir(&self) -> &Path {
464        self.base_dir.as_path()
465    }
466
467    /// Resolved assets used when compiling the tune request.
468    pub fn resolved_assets(&self) -> &[ResolvedAssetBinding] {
469        self.resolved_assets.as_ref()
470    }
471
472    /// Compiled baseline candidate used by the future tuner.
473    pub fn baseline_candidate(&self) -> &CompiledCompressionBackend {
474        &self.baseline_candidate
475    }
476
477    /// Runtime-selectable compiled controller settings.
478    pub fn controller(&self) -> &CompiledTuneController {
479        &self.controller
480    }
481
482    /// Stable identifier for the current canonicalization classification rules.
483    pub fn candidate_canonicalization_version(&self) -> &'static str {
484        self.candidate_canonicalization_version
485    }
486
487    /// Canonical byte length of the baseline candidate model code.
488    pub fn baseline_candidate_model_bytes(&self) -> usize {
489        self.baseline_candidate.canonical_bytes().len()
490    }
491}
492
493#[cfg(feature = "vm")]
494fn canonicalize_vm_observation_policy_name(name: &str) -> SpecResult<VmObservationPolicySpec> {
495    match name {
496        "from_guest" => Ok(VmObservationPolicySpec::FromGuest),
497        "output_hash" => Ok(VmObservationPolicySpec::OutputHash),
498        "raw_output" => Ok(VmObservationPolicySpec::RawOutput),
499        "shared_memory" => Ok(VmObservationPolicySpec::SharedMemory),
500        other => Err(SpecError::new(format!(
501            "unknown VM observation_policy '{other}'"
502        ))),
503    }
504}
505
506#[cfg(feature = "vm")]
507fn canonicalize_vm_observation_stream_mode_name(
508    name: &str,
509) -> SpecResult<VmObservationStreamModeSpec> {
510    match name {
511        "pad_truncate" => Ok(VmObservationStreamModeSpec::PadTruncate),
512        "pad" => Ok(VmObservationStreamModeSpec::Pad),
513        "truncate" => Ok(VmObservationStreamModeSpec::Truncate),
514        other => Err(SpecError::new(format!(
515            "unknown VM observation_stream_mode '{other}'"
516        ))),
517    }
518}
519
520#[cfg(feature = "vm")]
521fn canonicalize_vm_payload_encoding(
522    name: &str,
523    field_name: &str,
524) -> SpecResult<VmPayloadEncodingSpec> {
525    match name {
526        "utf8" => Ok(VmPayloadEncodingSpec::Utf8),
527        "hex" => Ok(VmPayloadEncodingSpec::Hex),
528        other => Err(SpecError::new(format!(
529            "unknown VM payload encoding '{other}' for {field_name}"
530        ))),
531    }
532}
533
534#[cfg(feature = "vm")]
535fn canonicalize_vm_fuzz_mutator_name(name: &str) -> SpecResult<VmFuzzMutatorSpec> {
536    match name {
537        "flip_bit" => Ok(VmFuzzMutatorSpec::FlipBit),
538        "flip_byte" => Ok(VmFuzzMutatorSpec::FlipByte),
539        "insert_byte" => Ok(VmFuzzMutatorSpec::InsertByte),
540        "delete_byte" => Ok(VmFuzzMutatorSpec::DeleteByte),
541        "splice_seed" => Ok(VmFuzzMutatorSpec::SpliceSeed),
542        "reset_seed" => Ok(VmFuzzMutatorSpec::ResetSeed),
543        "havoc" => Ok(VmFuzzMutatorSpec::Havoc),
544        other => Err(SpecError::new(format!("unknown VM fuzz mutator '{other}'"))),
545    }
546}
547
548impl fmt::Debug for ValidatedPlannerRunSpec {
549    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
550        f.debug_struct("ValidatedPlannerRunSpec")
551            .field("canonical_bytes_len", &self.canonical_bytes.len())
552            .finish()
553    }
554}
555
556#[cfg(feature = "tuner")]
557impl fmt::Debug for ValidatedTuneSpec {
558    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
559        f.debug_struct("ValidatedTuneSpec")
560            .field("canonical_bytes_len", &self.canonical_bytes.len())
561            .finish()
562    }
563}
564
565#[cfg(test)]
566mod tests;