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 #[serde(skip_serializing_if = "Vec::is_empty", default)]
156 requires: Vec<String>,
157 },
158 Resource {
159 resource_kind: String,
160 #[serde(skip_serializing_if = "Option::is_none", default)]
161 capacity: Option<i64>,
162 lifetime: String,
163 },
164 Other {
165 other_kind: String,
166 },
167}
168
169impl ExportSignature {
170 pub fn kind(&self) -> &str {
173 match self {
174 ExportSignature::Persona { .. } => "persona",
175 ExportSignature::Anchor { .. } => "anchor",
176 ExportSignature::Flow { .. } => "flow",
177 ExportSignature::Shield { .. } => "shield",
178 ExportSignature::Tool { .. } => "tool",
179 ExportSignature::Resource { .. } => "resource",
180 ExportSignature::Other { other_kind } => other_kind,
181 }
182 }
183}
184
185fn type_spelling(t: &TypeExpr) -> String {
186 let mut s = t.name.clone();
187 if !t.generic_param.is_empty() {
188 s.push('<');
189 s.push_str(&t.generic_param);
190 s.push('>');
191 }
192 if t.optional {
193 s.push('?');
194 }
195 s
196}
197
198#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
204pub struct CognitiveInterface {
205 pub format_version: u32,
206 pub module: String,
208 pub epistemic_floor: EpistemicFloor,
209 pub content_hash: String,
211 pub interface_hash: String,
213 pub exports: BTreeMap<String, ExportSignature>,
215}
216
217impl CognitiveInterface {
218 pub fn to_axi_json(&self) -> String {
222 serde_json::to_string_pretty(self).expect("interface serializes")
223 }
224
225 pub fn from_axi_json(s: &str) -> Option<CognitiveInterface> {
226 serde_json::from_str(s).ok()
227 }
228}
229
230pub fn generate_interface(
232 module: &ModulePath,
233 program: &Program,
234 source: &str,
235) -> CognitiveInterface {
236 let mut exports: BTreeMap<String, ExportSignature> = BTreeMap::new();
237
238 fn collect(decls: &[Declaration], exports: &mut BTreeMap<String, ExportSignature>) {
239 for decl in decls {
240 if let Declaration::Epistemic(eb) = decl {
241 collect(&eb.body, exports);
242 continue;
243 }
244 let Some((name, kind, _loc)) = declaration_surface(decl) else {
245 continue;
246 };
247 if name.is_empty() {
248 continue;
249 }
250 let sig = match decl {
251 Declaration::Persona(n) => ExportSignature::Persona {
252 domain: n.domain.clone(),
253 tone: n.tone.clone(),
254 confidence_threshold: n.confidence_threshold,
255 },
256 Declaration::Anchor(n) => {
257 let mut basis = String::new();
258 basis.push_str(&n.require);
259 basis.push('\u{1f}');
260 basis.push_str(&n.reject.join("\u{1f}"));
261 basis.push('\u{1f}');
262 basis.push_str(&n.enforce);
263 basis.push('\u{1f}');
264 basis.push_str(&n.unknown_response);
265 basis.push('\u{1f}');
266 if let Some(cf) = n.confidence_floor {
267 basis.push_str(&format!("{cf:.6}"));
268 }
269 basis.push('\u{1f}');
270 basis.push_str(&n.on_violation_target);
271 ExportSignature::Anchor {
272 constraint_hash: sha256_hex(basis.as_bytes()),
273 on_violation: n.on_violation.clone(),
274 }
275 }
276 Declaration::Flow(n) => ExportSignature::Flow {
277 params: n
278 .parameters
279 .iter()
280 .map(|p| (p.name.clone(), type_spelling(&p.type_expr)))
281 .collect(),
282 output_type: n
283 .return_type
284 .as_ref()
285 .map(type_spelling)
286 .unwrap_or_default(),
287 step_count: n.body.len(),
288 },
289 Declaration::Shield(n) => ExportSignature::Shield {
290 scan: n.scan.clone(),
291 on_breach: n.on_breach.clone(),
292 },
293 Declaration::Tool(n) => ExportSignature::Tool {
294 effects: n
295 .effects
296 .as_ref()
297 .map(|e| e.effects.clone())
298 .unwrap_or_default(),
299 risk: n.risk.clone(),
300 provider: n.provider.clone(),
301 requires: n.requires.clone(),
302 },
303 Declaration::Resource(n) => ExportSignature::Resource {
304 resource_kind: n.kind.clone(),
305 capacity: n.capacity,
306 lifetime: n.lifetime.clone(),
307 },
308 _ => ExportSignature::Other { other_kind: kind },
309 };
310 exports.insert(name, sig);
311 }
312 }
313 collect(&program.declarations, &mut exports);
314
315 let mut interface = CognitiveInterface {
316 format_version: AXI_FORMAT_VERSION,
317 module: module.dotted(),
318 epistemic_floor: compute_floor(program),
319 content_hash: sha256_hex(source.as_bytes()),
320 interface_hash: String::new(),
321 exports,
322 };
323 let mut hashed = interface.clone();
329 hashed.content_hash = String::new();
330 interface.interface_hash = sha256_hex(hashed.to_axi_json().as_bytes());
331 interface
332}
333
334#[derive(Debug, Default)]
342pub struct ModuleRegistry {
343 modules: BTreeMap<ModulePath, CognitiveInterface>,
344}
345
346impl ModuleRegistry {
347 pub fn new() -> Self {
348 Self::default()
349 }
350
351 pub fn register(&mut self, path: ModulePath, interface: CognitiveInterface) {
352 self.modules.insert(path, interface);
353 }
354
355 pub fn interface(&self, path: &ModulePath) -> Option<&CognitiveInterface> {
356 self.modules.get(path)
357 }
358
359 pub fn export_kind(&self, path: &ModulePath, name: &str) -> Option<&str> {
361 self.modules
362 .get(path)
363 .and_then(|i| i.exports.get(name))
364 .map(|sig| sig.kind())
365 }
366
367 pub fn len(&self) -> usize {
368 self.modules.len()
369 }
370
371 pub fn is_empty(&self) -> bool {
372 self.modules.is_empty()
373 }
374
375 pub fn iter(&self) -> impl Iterator<Item = (&ModulePath, &CognitiveInterface)> {
376 self.modules.iter()
377 }
378}
379
380#[cfg(test)]
385mod tests {
386 use super::*;
387 use crate::lexer::Lexer;
388 use crate::parser::Parser;
389
390 fn parse(source: &str) -> Program {
391 let tokens = Lexer::new(source, "test.axon").tokenize().expect("lex");
392 Parser::new(tokens).parse().expect("parse")
393 }
394
395 fn mp(dotted: &str) -> ModulePath {
396 ModulePath(dotted.split('.').map(str::to_string).collect())
397 }
398
399 const SECURITY: &str = r#"
400persona Expert {
401 domain: ["medicine", "diagnostics"]
402 tone: precise
403 confidence_threshold: 0.9
404}
405
406anchor NoHallucination {
407 require: source_citation
408 confidence_floor: 0.75
409 on_violation: raise AnchorBreachError
410}
411"#;
412
413 #[test]
414 fn floor_anchor_means_know() {
415 assert_eq!(compute_floor(&parse(SECURITY)), EpistemicFloor::Know);
416 }
417
418 #[test]
419 fn floor_shield_means_believe() {
420 let p = parse("shield S { scan: [pii_leak] on_breach: halt }\n");
421 assert_eq!(compute_floor(&p), EpistemicFloor::Believe);
422 }
423
424 #[test]
425 fn floor_unspecified_without_evidence() {
426 let p = parse("persona P { domain: [\"x\"] }\n");
427 assert_eq!(compute_floor(&p), EpistemicFloor::Unspecified);
428 }
429
430 #[test]
431 fn interface_hides_description_but_carries_signature() {
432 let p = parse(SECURITY);
433 let i = generate_interface(&mp("axon.security"), &p, SECURITY);
434 let json = i.to_axi_json();
435 assert!(json.contains("\"diagnostics\""));
436 assert!(json.contains("constraint_hash"));
437 assert!(!json.contains("source_citation"), "anchor text must be hidden");
438 assert_eq!(i.exports.len(), 2);
439 }
440
441 #[test]
442 fn interface_hash_stable_under_comment_edit() {
443 let with_comment = format!("// a comment\n{SECURITY}");
444 let p1 = parse(SECURITY);
445 let p2 = parse(&with_comment);
446 let i1 = generate_interface(&mp("m"), &p1, SECURITY);
447 let i2 = generate_interface(&mp("m"), &p2, &with_comment);
448 assert_ne!(i1.content_hash, i2.content_hash);
449 assert_eq!(i1.interface_hash, i2.interface_hash, "early-cutoff precondition");
450 }
451
452 #[test]
453 fn interface_hash_changes_when_surface_changes() {
454 let changed = SECURITY.replace("0.9", "0.8");
455 let i1 = generate_interface(&mp("m"), &parse(SECURITY), SECURITY);
456 let i2 = generate_interface(&mp("m"), &parse(&changed), &changed);
457 assert_ne!(i1.interface_hash, i2.interface_hash);
458 }
459
460 #[test]
461 fn roundtrip_axi_json() {
462 let i = generate_interface(&mp("m"), &parse(SECURITY), SECURITY);
463 let back = CognitiveInterface::from_axi_json(&i.to_axi_json()).unwrap();
464 assert_eq!(i, back);
465 }
466
467 #[test]
468 fn hash_determinism() {
469 let a = generate_interface(&mp("m"), &parse(SECURITY), SECURITY);
470 let b = generate_interface(&mp("m"), &parse(SECURITY), SECURITY);
471 assert_eq!(a.interface_hash, b.interface_hash);
472 assert_eq!(a.content_hash, b.content_hash);
473 }
474}