Skip to main content

sim_lib_forge/
intent.rs

1use sim_citizen::CitizenField;
2use sim_citizen_derive::Citizen;
3use sim_kernel::{ContentId, Error, Expr, Result, Symbol};
4
5/// Verification and approval state for a compiled intent artifact.
6#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
7pub enum IntentStatus {
8    /// The packet is lifted and structurally accepted, but not semantically
9    /// verified.
10    #[default]
11    Candidate,
12    /// The packet has passed the semantic probes attached to the artifact.
13    Verified,
14    /// A human has approved the verified artifact for compile-once reuse.
15    Golden,
16}
17
18impl CitizenField for IntentStatus {
19    fn encode_field(&self) -> Expr {
20        Expr::Symbol(self.as_symbol())
21    }
22
23    fn decode_field_expr(expr: &Expr, field: &'static str) -> Result<Self> {
24        let Expr::Symbol(symbol) = expr else {
25            return Err(sim_citizen::field_error(
26                field,
27                "expected intent status symbol",
28            ));
29        };
30        Self::from_symbol(symbol)
31            .ok_or_else(|| sim_citizen::field_error(field, format!("unknown status {symbol}")))
32    }
33}
34
35impl IntentStatus {
36    /// Returns the stable symbol used in citizen field encodings.
37    pub fn as_symbol(self) -> Symbol {
38        match self {
39            Self::Candidate => Symbol::qualified("forge", "candidate"),
40            Self::Verified => Symbol::qualified("forge", "verified"),
41            Self::Golden => Symbol::qualified("forge", "golden"),
42        }
43    }
44
45    /// Parses a stable status symbol.
46    pub fn from_symbol(symbol: &Symbol) -> Option<Self> {
47        if *symbol == Symbol::qualified("forge", "candidate") {
48            Some(Self::Candidate)
49        } else if *symbol == Symbol::qualified("forge", "verified") {
50            Some(Self::Verified)
51        } else if *symbol == Symbol::qualified("forge", "golden") {
52            Some(Self::Golden)
53        } else {
54            None
55        }
56    }
57}
58
59/// Named, versioned wrapper around a structurally checked BRIDGE packet.
60#[derive(Clone, Debug, PartialEq, Citizen)]
61#[citizen(symbol = "forge/CompiledIntent", version = 1)]
62pub struct CompiledIntent {
63    /// Stable symbolic name for the intent.
64    pub name: Symbol,
65    /// Monotonic artifact version for this intent name.
66    pub version: u32,
67    /// Content id of the normalized prose the packet was lifted from.
68    #[citizen(with = "content_id_field")]
69    pub source: ContentId,
70    /// Content id of the BRIDGE packet used as the compiled IR.
71    #[citizen(with = "content_id_field")]
72    pub packet: ContentId,
73    /// Stable identifiers for the Check, Evidence, or Vote parts that must pass.
74    pub verifiers: Vec<Symbol>,
75    /// Content ids of probe cases used for semantic verification.
76    #[citizen(with = "content_id_vec_field")]
77    pub probes: Vec<ContentId>,
78    /// Promotion state for the artifact.
79    pub status: IntentStatus,
80    /// Optional model card content id for the compiler used during lifting.
81    #[citizen(with = "optional_content_id_field")]
82    pub compiler_card: Option<ContentId>,
83    /// Optional human approval record required for golden reuse.
84    #[citizen(with = "optional_content_id_field")]
85    pub approval: Option<ContentId>,
86}
87
88impl Default for CompiledIntent {
89    fn default() -> Self {
90        Self {
91            name: Symbol::qualified("forge", "fixture"),
92            version: 1,
93            source: fixture_content_id(1),
94            packet: fixture_content_id(2),
95            verifiers: Vec::new(),
96            probes: Vec::new(),
97            status: IntentStatus::Candidate,
98            compiler_card: None,
99            approval: None,
100        }
101    }
102}
103
104fn fixture_content_id(byte: u8) -> ContentId {
105    ContentId::from_bytes(Symbol::qualified("core", "sha256"), [byte; 32])
106}
107
108mod content_id_field {
109    use super::*;
110    use sim_value::build::entry as field;
111
112    pub(crate) fn encode(id: &ContentId) -> Expr {
113        Expr::Map(vec![
114            field("algorithm", Expr::Symbol(id.algorithm.clone())),
115            field("bytes", Expr::Bytes(id.bytes.to_vec())),
116        ])
117    }
118
119    pub(crate) fn decode(expr: &Expr) -> Result<ContentId> {
120        let Expr::Map(entries) = expr else {
121            return Err(Error::Eval("content id field must be a map".to_owned()));
122        };
123        let algorithm = required_symbol(entries, "algorithm")?;
124        let bytes = required_bytes(entries, "bytes")?;
125        Ok(ContentId::from_bytes(algorithm, bytes))
126    }
127
128    fn required_symbol(entries: &[(Expr, Expr)], name: &'static str) -> Result<Symbol> {
129        match required_entry(entries, name)? {
130            Expr::Symbol(symbol) => Ok(symbol.clone()),
131            _ => Err(sim_citizen::field_error(name, "expected symbol")),
132        }
133    }
134
135    fn required_bytes(entries: &[(Expr, Expr)], name: &'static str) -> Result<[u8; 32]> {
136        match required_entry(entries, name)? {
137            Expr::Bytes(bytes) if bytes.len() == 32 => {
138                let mut digest = [0_u8; 32];
139                digest.copy_from_slice(bytes);
140                Ok(digest)
141            }
142            Expr::Bytes(bytes) => Err(sim_citizen::field_error(
143                name,
144                format!("expected 32 digest bytes, found {}", bytes.len()),
145            )),
146            _ => Err(sim_citizen::field_error(name, "expected bytes")),
147        }
148    }
149
150    fn required_entry<'a>(entries: &'a [(Expr, Expr)], name: &'static str) -> Result<&'a Expr> {
151        let key = Expr::Symbol(Symbol::new(name.to_owned()));
152        entries
153            .iter()
154            .find_map(|(entry_key, value)| (entry_key == &key).then_some(value))
155            .ok_or_else(|| sim_citizen::field_error(name, "missing content id field"))
156    }
157}
158
159mod content_id_vec_field {
160    use super::*;
161
162    pub(crate) fn encode(ids: &[ContentId]) -> Expr {
163        Expr::List(ids.iter().map(content_id_field::encode).collect())
164    }
165
166    pub(crate) fn decode(expr: &Expr) -> Result<Vec<ContentId>> {
167        let Expr::List(items) = expr else {
168            return Err(Error::Eval(
169                "content id list field must be a list".to_owned(),
170            ));
171        };
172        items.iter().map(content_id_field::decode).collect()
173    }
174}
175
176mod optional_content_id_field {
177    use super::*;
178
179    pub(crate) fn encode(id: &Option<ContentId>) -> Expr {
180        id.as_ref()
181            .map(content_id_field::encode)
182            .unwrap_or(Expr::Nil)
183    }
184
185    pub(crate) fn decode(expr: &Expr) -> Result<Option<ContentId>> {
186        match expr {
187            Expr::Nil => Ok(None),
188            other => content_id_field::decode(other).map(Some),
189        }
190    }
191}