1use std::collections::BTreeMap;
32
33use serde::{Deserialize, Serialize};
34
35use crate::ast::{declaration_surface, Declaration, Program, TypeExpr};
36use crate::module_resolver::ModulePath;
37use crate::store_schema_manifest::sha256_hex;
38
39pub const AXI_FORMAT_VERSION: u32 = 1;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
50#[serde(rename_all = "lowercase")]
51pub enum EpistemicFloor {
52 Unspecified = 0,
53 Speculate = 1,
54 Doubt = 2,
55 Believe = 3,
56 Know = 4,
57}
58
59impl EpistemicFloor {
60 pub fn as_str(self) -> &'static str {
61 match self {
62 EpistemicFloor::Unspecified => "unspecified",
63 EpistemicFloor::Speculate => "speculate",
64 EpistemicFloor::Doubt => "doubt",
65 EpistemicFloor::Believe => "believe",
66 EpistemicFloor::Know => "know",
67 }
68 }
69
70 pub fn rank(self) -> u8 {
71 self as u8
72 }
73
74 fn from_mode(mode: &str) -> EpistemicFloor {
75 match mode {
76 "know" => EpistemicFloor::Know,
77 "believe" => EpistemicFloor::Believe,
78 "doubt" => EpistemicFloor::Doubt,
79 "speculate" => EpistemicFloor::Speculate,
80 _ => EpistemicFloor::Unspecified,
81 }
82 }
83}
84
85pub fn compute_floor(program: &Program) -> EpistemicFloor {
89 fn walk(decls: &[Declaration], floor: &mut EpistemicFloor) {
90 for decl in decls {
91 let candidate = match decl {
92 Declaration::Anchor(_) => EpistemicFloor::Know,
93 Declaration::Shield(_) => EpistemicFloor::Believe,
94 Declaration::Epistemic(eb) => {
95 let level = EpistemicFloor::from_mode(&eb.mode);
96 walk(&eb.body, floor);
97 level
98 }
99 _ => EpistemicFloor::Unspecified,
100 };
101 if candidate > *floor {
102 *floor = candidate;
103 }
104 }
105 }
106 let mut floor = EpistemicFloor::Unspecified;
107 walk(&program.declarations, &mut floor);
108 floor
109}
110
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
122#[serde(tag = "kind", rename_all = "lowercase")]
123pub enum ExportSignature {
124 Persona {
125 domain: Vec<String>,
126 tone: String,
127 #[serde(skip_serializing_if = "Option::is_none", default)]
128 confidence_threshold: Option<f64>,
129 },
130 Anchor {
131 constraint_hash: String,
134 on_violation: String,
135 },
136 Flow {
137 params: Vec<(String, String)>,
138 output_type: String,
139 step_count: usize,
140 },
141 Shield {
142 scan: Vec<String>,
143 on_breach: String,
144 },
145 Tool {
146 effects: Vec<String>,
147 #[serde(skip_serializing_if = "Option::is_none", default)]
148 risk: Option<String>,
149 provider: String,
150 },
151 Resource {
152 resource_kind: String,
153 #[serde(skip_serializing_if = "Option::is_none", default)]
154 capacity: Option<i64>,
155 lifetime: String,
156 },
157 Other {
158 other_kind: String,
159 },
160}
161
162impl ExportSignature {
163 pub fn kind(&self) -> &str {
166 match self {
167 ExportSignature::Persona { .. } => "persona",
168 ExportSignature::Anchor { .. } => "anchor",
169 ExportSignature::Flow { .. } => "flow",
170 ExportSignature::Shield { .. } => "shield",
171 ExportSignature::Tool { .. } => "tool",
172 ExportSignature::Resource { .. } => "resource",
173 ExportSignature::Other { other_kind } => other_kind,
174 }
175 }
176}
177
178fn type_spelling(t: &TypeExpr) -> String {
179 let mut s = t.name.clone();
180 if !t.generic_param.is_empty() {
181 s.push('<');
182 s.push_str(&t.generic_param);
183 s.push('>');
184 }
185 if t.optional {
186 s.push('?');
187 }
188 s
189}
190
191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
197pub struct CognitiveInterface {
198 pub format_version: u32,
199 pub module: String,
201 pub epistemic_floor: EpistemicFloor,
202 pub content_hash: String,
204 pub interface_hash: String,
206 pub exports: BTreeMap<String, ExportSignature>,
208}
209
210impl CognitiveInterface {
211 pub fn to_axi_json(&self) -> String {
215 serde_json::to_string_pretty(self).expect("interface serializes")
216 }
217
218 pub fn from_axi_json(s: &str) -> Option<CognitiveInterface> {
219 serde_json::from_str(s).ok()
220 }
221}
222
223pub fn generate_interface(
225 module: &ModulePath,
226 program: &Program,
227 source: &str,
228) -> CognitiveInterface {
229 let mut exports: BTreeMap<String, ExportSignature> = BTreeMap::new();
230
231 fn collect(decls: &[Declaration], exports: &mut BTreeMap<String, ExportSignature>) {
232 for decl in decls {
233 if let Declaration::Epistemic(eb) = decl {
234 collect(&eb.body, exports);
235 continue;
236 }
237 let Some((name, kind, _loc)) = declaration_surface(decl) else {
238 continue;
239 };
240 if name.is_empty() {
241 continue;
242 }
243 let sig = match decl {
244 Declaration::Persona(n) => ExportSignature::Persona {
245 domain: n.domain.clone(),
246 tone: n.tone.clone(),
247 confidence_threshold: n.confidence_threshold,
248 },
249 Declaration::Anchor(n) => {
250 let mut basis = String::new();
251 basis.push_str(&n.require);
252 basis.push('\u{1f}');
253 basis.push_str(&n.reject.join("\u{1f}"));
254 basis.push('\u{1f}');
255 basis.push_str(&n.enforce);
256 basis.push('\u{1f}');
257 basis.push_str(&n.unknown_response);
258 basis.push('\u{1f}');
259 if let Some(cf) = n.confidence_floor {
260 basis.push_str(&format!("{cf:.6}"));
261 }
262 basis.push('\u{1f}');
263 basis.push_str(&n.on_violation_target);
264 ExportSignature::Anchor {
265 constraint_hash: sha256_hex(basis.as_bytes()),
266 on_violation: n.on_violation.clone(),
267 }
268 }
269 Declaration::Flow(n) => ExportSignature::Flow {
270 params: n
271 .parameters
272 .iter()
273 .map(|p| (p.name.clone(), type_spelling(&p.type_expr)))
274 .collect(),
275 output_type: n
276 .return_type
277 .as_ref()
278 .map(type_spelling)
279 .unwrap_or_default(),
280 step_count: n.body.len(),
281 },
282 Declaration::Shield(n) => ExportSignature::Shield {
283 scan: n.scan.clone(),
284 on_breach: n.on_breach.clone(),
285 },
286 Declaration::Tool(n) => ExportSignature::Tool {
287 effects: n
288 .effects
289 .as_ref()
290 .map(|e| e.effects.clone())
291 .unwrap_or_default(),
292 risk: n.risk.clone(),
293 provider: n.provider.clone(),
294 },
295 Declaration::Resource(n) => ExportSignature::Resource {
296 resource_kind: n.kind.clone(),
297 capacity: n.capacity,
298 lifetime: n.lifetime.clone(),
299 },
300 _ => ExportSignature::Other { other_kind: kind },
301 };
302 exports.insert(name, sig);
303 }
304 }
305 collect(&program.declarations, &mut exports);
306
307 let mut interface = CognitiveInterface {
308 format_version: AXI_FORMAT_VERSION,
309 module: module.dotted(),
310 epistemic_floor: compute_floor(program),
311 content_hash: sha256_hex(source.as_bytes()),
312 interface_hash: String::new(),
313 exports,
314 };
315 let mut hashed = interface.clone();
321 hashed.content_hash = String::new();
322 interface.interface_hash = sha256_hex(hashed.to_axi_json().as_bytes());
323 interface
324}
325
326#[derive(Debug, Default)]
334pub struct ModuleRegistry {
335 modules: BTreeMap<ModulePath, CognitiveInterface>,
336}
337
338impl ModuleRegistry {
339 pub fn new() -> Self {
340 Self::default()
341 }
342
343 pub fn register(&mut self, path: ModulePath, interface: CognitiveInterface) {
344 self.modules.insert(path, interface);
345 }
346
347 pub fn interface(&self, path: &ModulePath) -> Option<&CognitiveInterface> {
348 self.modules.get(path)
349 }
350
351 pub fn export_kind(&self, path: &ModulePath, name: &str) -> Option<&str> {
353 self.modules
354 .get(path)
355 .and_then(|i| i.exports.get(name))
356 .map(|sig| sig.kind())
357 }
358
359 pub fn len(&self) -> usize {
360 self.modules.len()
361 }
362
363 pub fn is_empty(&self) -> bool {
364 self.modules.is_empty()
365 }
366
367 pub fn iter(&self) -> impl Iterator<Item = (&ModulePath, &CognitiveInterface)> {
368 self.modules.iter()
369 }
370}
371
372#[cfg(test)]
377mod tests {
378 use super::*;
379 use crate::lexer::Lexer;
380 use crate::parser::Parser;
381
382 fn parse(source: &str) -> Program {
383 let tokens = Lexer::new(source, "test.axon").tokenize().expect("lex");
384 Parser::new(tokens).parse().expect("parse")
385 }
386
387 fn mp(dotted: &str) -> ModulePath {
388 ModulePath(dotted.split('.').map(str::to_string).collect())
389 }
390
391 const SECURITY: &str = r#"
392persona Expert {
393 domain: ["medicine", "diagnostics"]
394 tone: precise
395 confidence_threshold: 0.9
396}
397
398anchor NoHallucination {
399 require: source_citation
400 confidence_floor: 0.75
401 on_violation: raise AnchorBreachError
402}
403"#;
404
405 #[test]
406 fn floor_anchor_means_know() {
407 assert_eq!(compute_floor(&parse(SECURITY)), EpistemicFloor::Know);
408 }
409
410 #[test]
411 fn floor_shield_means_believe() {
412 let p = parse("shield S { scan: [pii_leak] on_breach: halt }\n");
413 assert_eq!(compute_floor(&p), EpistemicFloor::Believe);
414 }
415
416 #[test]
417 fn floor_unspecified_without_evidence() {
418 let p = parse("persona P { domain: [\"x\"] }\n");
419 assert_eq!(compute_floor(&p), EpistemicFloor::Unspecified);
420 }
421
422 #[test]
423 fn interface_hides_description_but_carries_signature() {
424 let p = parse(SECURITY);
425 let i = generate_interface(&mp("axon.security"), &p, SECURITY);
426 let json = i.to_axi_json();
427 assert!(json.contains("\"diagnostics\""));
428 assert!(json.contains("constraint_hash"));
429 assert!(!json.contains("source_citation"), "anchor text must be hidden");
430 assert_eq!(i.exports.len(), 2);
431 }
432
433 #[test]
434 fn interface_hash_stable_under_comment_edit() {
435 let with_comment = format!("// a comment\n{SECURITY}");
436 let p1 = parse(SECURITY);
437 let p2 = parse(&with_comment);
438 let i1 = generate_interface(&mp("m"), &p1, SECURITY);
439 let i2 = generate_interface(&mp("m"), &p2, &with_comment);
440 assert_ne!(i1.content_hash, i2.content_hash);
441 assert_eq!(i1.interface_hash, i2.interface_hash, "early-cutoff precondition");
442 }
443
444 #[test]
445 fn interface_hash_changes_when_surface_changes() {
446 let changed = SECURITY.replace("0.9", "0.8");
447 let i1 = generate_interface(&mp("m"), &parse(SECURITY), SECURITY);
448 let i2 = generate_interface(&mp("m"), &parse(&changed), &changed);
449 assert_ne!(i1.interface_hash, i2.interface_hash);
450 }
451
452 #[test]
453 fn roundtrip_axi_json() {
454 let i = generate_interface(&mp("m"), &parse(SECURITY), SECURITY);
455 let back = CognitiveInterface::from_axi_json(&i.to_axi_json()).unwrap();
456 assert_eq!(i, back);
457 }
458
459 #[test]
460 fn hash_determinism() {
461 let a = generate_interface(&mp("m"), &parse(SECURITY), SECURITY);
462 let b = generate_interface(&mp("m"), &parse(SECURITY), SECURITY);
463 assert_eq!(a.interface_hash, b.interface_hash);
464 assert_eq!(a.content_hash, b.content_hash);
465 }
466}