#![allow(dead_code)]
use std::collections::HashMap;
use crate::ast::*;
use crate::session::SessionType;
use crate::epistemic;
const VALID_TONES: &[&str] = &[
"analytical",
"assertive",
"casual",
"diplomatic",
"empathetic",
"formal",
"friendly",
"precise",
];
const VALID_MEMORY_SCOPES: &[&str] = &["ephemeral", "none", "persistent", "session"];
const VALID_DEPTHS: &[&str] = &["deep", "exhaustive", "shallow", "standard"];
const VALID_EFFORT_LEVELS: &[&str] = &["high", "low", "max", "medium"];
const VALID_VIOLATION_ACTIONS: &[&str] = &["escalate", "fallback", "log", "raise", "warn"];
const VALID_RETRIEVAL_STRATEGIES: &[&str] = &["exact", "hybrid", "semantic"];
const VALID_CORPUS_RELATIONS: &[&str] = &[
"cite",
"corroborate",
"contradict",
"depend",
"elaborate",
"exemplify",
"implement",
"supersede",
];
const RESERVED_OUTPUT_TYPE_NAMES: &[&str] = &[
"any", "bool", "boolean", "bytes", "dict", "false", "float",
"int", "integer", "list", "map", "none", "null", "number",
"set", "str", "string", "true", "tuple", "void",
];
const VALID_EFFECTS: &[&str] = &[
"io",
"network",
"pure",
"random",
"storage",
"stream",
"trust",
"sensitive",
"legal",
"ots",
"web",
];
const VALID_CACHE_BACKENDS: &[&str] = &["in_process", "redis"];
const VALID_SAVANT_DEPTHS: &[&str] = &["standard", "deep", "hyper"];
const VALID_SAVANT_DIVERGENCES: &[&str] = &["low", "med", "high"];
const VALID_SYNTH_RISKS: &[&str] = &["low", "medium", "high", "critical"];
const VALID_SYNTH_LANGUAGES: &[&str] = &["rust", "c", "python"];
const VALID_SYNTH_REVIEWS: &[&str] = &["required", "none"];
const VALID_SCOPE_DEPTHS: &[&str] = &["static_artifact", "memory_dump", "live_network"];
const VALID_FORGE_MODES: &[&str] = &["combinatorial", "exploratory", "transformational"];
const VALID_SCRAPE_PROVIDERS: &[&str] = &["scrape_http", "scrape_dom", "scrape_crawl", "scrape_enrich"];
pub const VALID_TOOL_PROVIDERS: &[&str] = &[
"native",
"stub",
"stub_stream",
"http",
"mcp",
"scrape_http",
"scrape_dom",
"scrape_crawl",
"scrape_enrich",
"bash",
"agora_linkedin",
"agora_facebook",
"agora_instagram",
"agora_tiktok",
];
const VALID_SCRAPE_ENGINES: &[&str] = &["impersonate", "browser"];
const VALID_IMPERSONATE_PROFILES: &[&str] = &["chrome", "firefox", "safari", "edge"];
const VALID_EPISTEMIC_LEVELS: &[&str] = &["believe", "doubt", "know", "speculate"];
const VALID_INGEST_CLASSES: &[&str] = &["parsed", "inferred"];
const VALID_DOC_TARGETS: &[&str] = &["docx", "pptx", "xlsx"];
const VALID_DOC_PROVENANCE: &[&str] = &["none", "embedded", "signed"];
const VALID_CHART_KINDS: &[&str] = &["bar", "line", "pie", "scatter"];
const VALID_DELIVER_TARGETS: &[&str] = &["crm"];
const VALID_DELIVER_PROVENANCE: &[&str] = &["attached", "cleared"];
const VALID_DELIVER_OPS: &[&str] = &["upsert_contact", "create_deal", "add_note"];
fn doc_top_level_kinds(target: &str) -> Vec<&'static str> {
match target {
"docx" => vec!["section", "page_break"],
"pptx" => vec!["slide"],
"xlsx" => vec!["sheet"],
_ => vec![],
}
}
fn doc_allowed_child_kinds(target: &str, parent: &str) -> Vec<&'static str> {
match (target, parent) {
("docx", "") => doc_top_level_kinds("docx"),
("docx", "section") => vec![
"heading", "para", "table", "chart", "image", "toc", "page_break", "footnote",
],
("pptx", "") => doc_top_level_kinds("pptx"),
("pptx", "slide") => vec!["placeholder", "bullets", "image", "chart", "notes"],
("xlsx", "") => doc_top_level_kinds("xlsx"),
("xlsx", "sheet") => vec!["row", "formula", "range", "chart", "format"],
_ => vec![],
}
}
fn doc_allowed_fields(kind: &str) -> Vec<&'static str> {
match kind {
"section" => vec!["heading", "name"],
"heading" => vec!["text", "level"],
"para" => vec!["text", "attribute"],
"table" => vec!["columns", "rows", "attribute"],
"chart" => vec!["kind", "series", "range", "attribute"],
"image" => vec!["source", "width", "height"],
"toc" => vec!["depth"],
"page_break" => vec![],
"footnote" => vec!["text", "attribute"],
"slide" => vec!["layout"],
"placeholder" => vec!["name", "text", "attribute"],
"bullets" => vec!["items", "attribute"],
"notes" => vec!["text", "attribute"],
"sheet" => vec!["name"],
"row" => vec!["cells", "attribute"],
"formula" => vec!["cell", "expr", "attribute"],
"range" => vec!["name", "cells"],
"format" => vec!["cell", "style"],
_ => vec![],
}
}
fn doc_assertive_slot(kind: &str) -> Option<&'static str> {
match kind {
"para" | "notes" | "footnote" | "heading" => Some("text"),
"table" => Some("rows"),
"chart" => Some("series"),
"formula" => Some("expr"),
"bullets" => Some("items"),
"placeholder" => Some("text"),
"row" => Some("cells"),
_ => None,
}
}
fn is_a1_cell(s: &str) -> bool {
let s = s.trim();
if s.is_empty() {
return false;
}
let mut chars = s.chars();
let mut saw_col = false;
let mut saw_row = false;
let mut in_row = false;
for c in chars.by_ref() {
if c.is_ascii_uppercase() {
if in_row {
return false; }
saw_col = true;
} else if c.is_ascii_digit() {
if c == '0' && !saw_row {
return false; }
in_row = true;
saw_row = true;
} else {
return false;
}
}
saw_col && saw_row
}
fn is_a1_range(s: &str) -> bool {
let s = s.trim();
match s.split_once(':') {
Some((a, b)) => is_a1_cell(a) && is_a1_cell(b),
None => is_a1_cell(s),
}
}
const VALID_DERIVATIONS: &[&str] = &["aggregated", "derived", "inferred", "raw", "transformed"];
const VALID_AGENT_STRATEGIES: &[&str] = &["custom", "plan_and_execute", "react", "reflexion"];
const VALID_ON_STUCK_POLICIES: &[&str] = &["escalate", "forge", "hibernate", "retry"];
const VALID_SCAN_CATEGORIES: &[&str] = &[
"bias",
"code_injection",
"data_exfil",
"hallucination",
"jailbreak",
"model_theft",
"pii_leak",
"prompt_injection",
"social_engineering",
"toxicity",
"training_poisoning",
];
const VALID_SHIELD_STRATEGIES: &[&str] = &[
"canary",
"classifier",
"dual_llm",
"ensemble",
"pattern",
"perplexity",
];
const VALID_ON_BREACH_POLICIES: &[&str] = &[
"deflect",
"escalate",
"halt",
"quarantine",
"sanitize_and_retry",
];
const VALID_ON_OUTSIDE: &[&str] = &["skip", "defer", "warn"];
const VALID_WEEKDAYS: &[&str] = &["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const VALID_BUDGET_PERIODS: &[&str] = &["second", "minute", "hour", "day"];
const VALID_ON_EXHAUSTED: &[&str] = &["block", "defer", "shed"];
const VALID_SEVERITY_LEVELS: &[&str] = &["critical", "high", "low", "medium"];
const VALID_SIGN_ALGORITHMS: &[&str] = &["hmac_sha256"];
const VALID_UPSTREAM_TRANSPORTS: &[&str] = &["websocket"];
const VALID_UPSTREAM_AUTH_KINDS: &[&str] = &["header", "query", "signed_url"];
const VALID_UPSTREAM_ON_EXHAUSTED: &[&str] = &["fail"];
const VALID_UPSTREAM_FRAMINGS: &[&str] = &["binary", "json"];
const SHIELD_FIELD_CATALOG: &str = "scan, strategy, on_breach, severity, quarantine, \
max_retries, confidence_threshold, allow_tools, deny_tools, sandbox, redact, log, \
deflect_message, taint, compliance, sign";
const VALID_OTS_HOMOTOPY: &[&str] = &["deep", "shallow", "speculative"];
const VALID_MANDATE_POLICIES: &[&str] = &["coerce", "halt", "retry"];
pub const VALID_STORE_BACKENDS: &[&str] = &["in_memory", "postgresql", "secrets"];
const VALID_RESOURCE_KINDS: &[&str] = &["http", "https", "mysql", "postgres", "redis"];
pub(crate) fn is_config_key(key: &str) -> bool {
let mut chars = key.chars();
let head_ok = chars
.next()
.is_some_and(|c| c.is_ascii_lowercase() || c.is_ascii_digit());
let rest_ok = chars
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '_' | '.' | '-'));
head_ok && rest_ok
}
const VALID_STORE_ISOLATION: &[&str] = &["read_committed", "repeatable_read", "serializable"];
const VALID_STORE_ON_BREACH: &[&str] = &["log", "raise", "rollback"];
const VALID_ENDPOINT_METHODS: &[&str] = &["DELETE", "GET", "PATCH", "POST", "PUT", "QUERY"];
const VALID_INFERENCE_MODES: &[&str] = &["active", "passive"];
fn is_valid(value: &str, set: &[&str]) -> bool {
set.contains(&value)
}
fn valid_list(set: &[&str]) -> String {
set.join(", ")
}
fn cache_effects_are_pure_only(apply_to_effects: &[String]) -> bool {
apply_to_effects.iter().all(|e| {
let base = e.split_once(':').map(|(b, _)| b).unwrap_or(e.as_str());
base == "pure"
})
}
fn tool_is_pure(effects: &Option<crate::ast::EffectRow>) -> bool {
match effects {
Some(row) => row.effects.len() == 1 && row.effects[0] == "pure",
None => false,
}
}
fn session_has_confirm_branch(steps: &[crate::ast::SessionStep]) -> bool {
for step in steps {
if step.op == "branch" {
let has_approved = step
.branches
.iter()
.any(|b| b.label == crate::technician::CONFIRM_APPROVED_LABEL);
let has_denied = step
.branches
.iter()
.any(|b| b.label == crate::technician::CONFIRM_DENIED_LABEL);
if has_approved && has_denied {
return true;
}
}
for b in &step.branches {
if session_has_confirm_branch(&b.steps) {
return true;
}
}
}
false
}
fn is_valid_origin_glob(origin: &str) -> bool {
if origin == "*" {
return true;
}
match origin.matches('*').count() {
0 => true,
1 => match origin.find("://") {
Some(scheme_end) => origin[scheme_end + 3..].starts_with("*."),
None => false,
},
_ => false,
}
}
fn is_valid_iso_date(s: &str) -> bool {
let b = s.as_bytes();
if b.len() != 10 || b[4] != b'-' || b[7] != b'-' {
return false;
}
let digits = |lo: usize, hi: usize| b[lo..hi].iter().all(|c| c.is_ascii_digit());
if !digits(0, 4) || !digits(5, 7) || !digits(8, 10) {
return false;
}
let num = |lo: usize, hi: usize| s[lo..hi].parse::<u32>().unwrap_or(0);
let (year, month, day) = (num(0, 4), num(5, 7), num(8, 10));
if !(1..=12).contains(&month) || day < 1 {
return false;
}
let leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
let days_in_month = match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if leap => 29,
2 => 28,
_ => 0,
};
day <= days_in_month
}
#[derive(Debug)]
pub struct TypeError {
pub message: String,
pub line: u32,
pub column: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum Cardinality {
Singular(String),
Plural(String),
StreamCardinality(String),
Unit,
Disagreed,
Unknown,
Wrapped(Box<Cardinality>),
}
pub(crate) fn declared_cardinality(output_type: &str) -> Cardinality {
let t = output_type.trim();
if t.is_empty() {
return Cardinality::Unknown;
}
if t == "Unit" {
return Cardinality::Unit;
}
if t == "Any" {
return Cardinality::Disagreed;
}
if let Some(rest) = t.strip_prefix("FlowEnvelope<") {
if let Some(inner) = rest.strip_suffix('>') {
let inner_card = declared_cardinality(inner.trim());
return Cardinality::Wrapped(Box::new(inner_card));
}
}
if let Some(rest) = t.strip_prefix("List<") {
if let Some(inner) = rest.strip_suffix('>') {
return Cardinality::Plural(inner.trim().to_string());
}
}
if let Some(rest) = t.strip_prefix("Stream<") {
if let Some(inner) = rest.strip_suffix('>') {
return Cardinality::StreamCardinality(inner.trim().to_string());
}
}
Cardinality::Singular(t.to_string())
}
pub(crate) fn infer_flow_tail_cardinality(flow: &FlowDefinition) -> Cardinality {
infer_body_tail_cardinality(&flow.body)
}
fn infer_body_tail_cardinality(body: &[FlowStep]) -> Cardinality {
if body.is_empty() {
return Cardinality::Unit;
}
for step in body.iter().rev() {
match step {
FlowStep::Step(s) => return declared_cardinality(&s.output_type),
FlowStep::LambdaDataApply(n) => return declared_cardinality(&n.output_type),
FlowStep::ShieldApply(n) => return declared_cardinality(&n.output_type),
FlowStep::OtsApply(n) => return declared_cardinality(&n.output_type),
FlowStep::MandateApply(n) => return declared_cardinality(&n.output_type),
FlowStep::If(cond) => {
let then_card = infer_body_tail_cardinality(&cond.then_body);
let else_card = infer_body_tail_cardinality(&cond.else_body);
return join_cardinalities(&then_card, &else_card);
}
FlowStep::ForIn(fi) => {
let inner = infer_body_tail_cardinality(&fi.body);
return match inner {
Cardinality::Singular(t) => Cardinality::Plural(t),
Cardinality::Plural(t) => Cardinality::Plural(t),
Cardinality::StreamCardinality(t) => {
Cardinality::Plural(t)
}
Cardinality::Unit => Cardinality::Unknown,
other => other,
};
}
FlowStep::Return(r) => return infer_return_cardinality(&r.value_expr),
FlowStep::Retrieve(_) => {
return Cardinality::Plural("StoreRow".to_string());
}
FlowStep::Persist(_) | FlowStep::Mutate(_) | FlowStep::Purge(_) => {
return Cardinality::Unit;
}
FlowStep::Let(_) | FlowStep::Break(_) | FlowStep::Continue(_) => {
continue;
}
_ => return Cardinality::Unknown,
}
}
Cardinality::Unit
}
fn infer_return_cardinality(expr: &str) -> Cardinality {
let t = expr.trim();
if t.is_empty() {
return Cardinality::Unit;
}
if t.starts_with('[') && t.ends_with(']') && t.len() >= 2 {
return Cardinality::Plural(String::new());
}
if t.ends_with(']') && t.contains('[') && !t.starts_with('[') {
return Cardinality::Singular(String::new());
}
Cardinality::Unknown
}
fn join_cardinalities(a: &Cardinality, b: &Cardinality) -> Cardinality {
if matches!(a, Cardinality::Unknown) {
return b.clone();
}
if matches!(b, Cardinality::Unknown) {
return a.clone();
}
if a == b {
return a.clone();
}
let kind = |c: &Cardinality| match c {
Cardinality::Singular(_) => 0,
Cardinality::Plural(_) => 1,
Cardinality::StreamCardinality(_) => 2,
Cardinality::Unit => 3,
Cardinality::Disagreed => 4,
Cardinality::Unknown => 5,
Cardinality::Wrapped(_) => 6,
};
if kind(a) == kind(b) {
return a.clone();
}
Cardinality::Disagreed
}
#[derive(Debug, Clone)]
struct Symbol {
name: String,
kind: String,
line: u32,
}
struct SymbolTable {
symbols: HashMap<String, Symbol>,
}
impl SymbolTable {
fn new() -> Self {
SymbolTable {
symbols: HashMap::new(),
}
}
fn declare(&mut self, name: &str, kind: &str, line: u32) -> Option<String> {
if let Some(existing) = self.symbols.get(name) {
return Some(format!(
"Duplicate declaration: '{}' already defined as {} (first defined at line {})",
name, existing.kind, existing.line
));
}
self.symbols.insert(
name.to_string(),
Symbol {
name: name.to_string(),
kind: kind.to_string(),
line,
},
);
None
}
fn lookup(&self, name: &str) -> Option<&Symbol> {
self.symbols.get(name)
}
}
pub struct TypeChecker<'a> {
program: &'a Program,
symbols: SymbolTable,
errors: Vec<TypeError>,
warnings: Vec<TypeError>,
store_inline_column_sets:
std::collections::HashMap<String, crate::store_column_proof::ColumnSet>,
current_flow_params: crate::store_column_proof::FlowParamTypes,
manifest: Option<&'a crate::store_schema_manifest::Manifest>,
ext_effect_members: std::collections::HashSet<String>,
ext_scan_categories: std::collections::HashSet<String>,
json_lens_fields:
std::collections::HashMap<String, std::collections::HashMap<String, (String, String)>>,
current_flow_param_spellings: std::collections::BTreeMap<String, String>,
current_mint_bindings: std::collections::HashSet<String>,
current_epistemic_mode: String,
emitted_channels: std::collections::HashSet<String>,
secrets_backed_stores: std::collections::HashSet<String>,
module_ctx: Option<&'a ModuleCheckContext>,
imported_symbols: std::collections::BTreeMap<String, (String, String)>,
}
#[derive(Debug, Default)]
pub struct ModuleCheckContext {
pub modules: std::collections::BTreeMap<
String,
std::collections::BTreeMap<String, String>,
>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum InferType {
Int,
Float,
Bool,
Str,
Unknown,
}
impl InferType {
fn is_numeric(self) -> bool {
matches!(self, InferType::Int | InferType::Float)
}
fn label(self) -> &'static str {
match self {
InferType::Int => "Int",
InferType::Float => "Float",
InferType::Bool => "Bool",
InferType::Str => "String",
InferType::Unknown => "unknown",
}
}
fn eq_class(self) -> u8 {
match self {
InferType::Int | InferType::Float => 0,
InferType::Bool => 1,
InferType::Str => 2,
InferType::Unknown => 3,
}
}
}
fn infer_type_from_name(name: &str) -> InferType {
match name.trim_end_matches('?') {
"Int" | "Integer" | "BigInt" => InferType::Int,
"Float" | "Double" | "Number" | "Numeric" => InferType::Float,
"Bool" | "Boolean" => InferType::Bool,
"String" | "Text" => InferType::Str,
"Json" => InferType::Unknown,
_ => InferType::Unknown,
}
}
#[derive(Clone)]
enum ConstVal {
Int(i64),
Float(f64),
Bool(bool),
Str(String),
}
fn const_truthy(v: &ConstVal) -> bool {
match v {
ConstVal::Bool(b) => *b,
ConstVal::Int(i) => *i != 0,
ConstVal::Float(f) => *f != 0.0,
ConstVal::Str(s) => !s.is_empty() && s != "false" && s != "0",
}
}
fn const_as_num(v: &ConstVal) -> Option<f64> {
match v {
ConstVal::Int(i) => Some(*i as f64),
ConstVal::Float(f) => Some(*f),
_ => None,
}
}
fn const_to_str(v: &ConstVal) -> String {
match v {
ConstVal::Int(i) => i.to_string(),
ConstVal::Float(f) => f.to_string(),
ConstVal::Bool(b) => b.to_string(),
ConstVal::Str(s) => s.clone(),
}
}
fn const_fold(e: &Expr) -> Option<ConstVal> {
match e {
Expr::Lit(ExprLit::Int(i)) => Some(ConstVal::Int(*i)),
Expr::Lit(ExprLit::Float(f)) => Some(ConstVal::Float(*f)),
Expr::Lit(ExprLit::Bool(b)) => Some(ConstVal::Bool(*b)),
Expr::Lit(ExprLit::Str(s)) => Some(ConstVal::Str(s.clone())),
Expr::Unary(UnOp::Not, x) => Some(ConstVal::Bool(!const_truthy(&const_fold(x)?))),
Expr::Unary(UnOp::Neg, x) => match const_fold(x)? {
ConstVal::Int(i) => i.checked_neg().map(ConstVal::Int),
other => Some(ConstVal::Float(-const_as_num(&other)?)),
},
Expr::Binary(op, l, r) => const_binop(*op, &const_fold(l)?, &const_fold(r)?),
Expr::Let { .. } => None,
Expr::Ref(_) | Expr::Field(..) | Expr::Index(..) | Expr::Call(..) => None,
}
}
fn const_binop(op: BinOp, l: &ConstVal, r: &ConstVal) -> Option<ConstVal> {
use std::cmp::Ordering;
match op {
BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod => {
if let (ConstVal::Int(a), ConstVal::Int(b)) = (l, r) {
let res = match op {
BinOp::Add => a.checked_add(*b)?,
BinOp::Sub => a.checked_sub(*b)?,
BinOp::Mul => a.checked_mul(*b)?,
BinOp::Div => a.checked_div(*b)?,
BinOp::Mod => a.checked_rem(*b)?,
_ => unreachable!(),
};
return Some(ConstVal::Int(res));
}
let (a, b) = (const_as_num(l)?, const_as_num(r)?);
let res = match op {
BinOp::Add => a + b,
BinOp::Sub => a - b,
BinOp::Mul => a * b,
BinOp::Div if b != 0.0 => a / b,
BinOp::Mod if b != 0.0 => a % b,
_ => return None,
};
Some(ConstVal::Float(res))
}
BinOp::Eq => Some(ConstVal::Bool(const_eq(l, r))),
BinOp::Ne => Some(ConstVal::Bool(!const_eq(l, r))),
BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge => {
let ord = const_cmp(l, r)?;
Some(ConstVal::Bool(match op {
BinOp::Lt => ord == Ordering::Less,
BinOp::Le => ord != Ordering::Greater,
BinOp::Gt => ord == Ordering::Greater,
BinOp::Ge => ord != Ordering::Less,
_ => unreachable!(),
}))
}
BinOp::And => Some(ConstVal::Bool(const_truthy(l) && const_truthy(r))),
BinOp::Or => Some(ConstVal::Bool(const_truthy(l) || const_truthy(r))),
}
}
fn const_eq(l: &ConstVal, r: &ConstVal) -> bool {
if let (Some(a), Some(b)) = (const_as_num(l), const_as_num(r)) {
return a == b;
}
if let (ConstVal::Bool(a), ConstVal::Bool(b)) = (l, r) {
return a == b;
}
const_to_str(l) == const_to_str(r)
}
fn const_cmp(l: &ConstVal, r: &ConstVal) -> Option<std::cmp::Ordering> {
if let (Some(a), Some(b)) = (const_as_num(l), const_as_num(r)) {
return a.partial_cmp(&b);
}
Some(const_to_str(l).cmp(&const_to_str(r)))
}
fn bin_op_symbol(op: BinOp) -> &'static str {
match op {
BinOp::Add => "+",
BinOp::Sub => "-",
BinOp::Mul => "*",
BinOp::Div => "/",
BinOp::Mod => "%",
BinOp::Eq => "==",
BinOp::Ne => "!=",
BinOp::Lt => "<",
BinOp::Le => "<=",
BinOp::Gt => ">",
BinOp::Ge => ">=",
BinOp::And => "and",
BinOp::Or => "or",
}
}
impl<'a> TypeChecker<'a> {
pub fn new(program: &'a Program) -> Self {
TypeChecker {
program,
symbols: SymbolTable::new(),
errors: Vec::new(),
warnings: Vec::new(),
store_inline_column_sets: std::collections::HashMap::new(),
current_flow_params: crate::store_column_proof::FlowParamTypes::new(),
manifest: None,
ext_effect_members: std::collections::HashSet::new(),
ext_scan_categories: std::collections::HashSet::new(),
json_lens_fields: std::collections::HashMap::new(),
current_flow_param_spellings: std::collections::BTreeMap::new(),
current_mint_bindings: std::collections::HashSet::new(),
current_epistemic_mode: String::new(),
emitted_channels: std::collections::HashSet::new(),
secrets_backed_stores: std::collections::HashSet::new(),
module_ctx: None,
imported_symbols: std::collections::BTreeMap::new(),
}
}
pub fn set_module_context(&mut self, ctx: &'a ModuleCheckContext) {
self.module_ctx = Some(ctx);
}
pub fn with_manifest(
program: &'a Program,
manifest: &'a crate::store_schema_manifest::Manifest,
) -> Self {
TypeChecker {
program,
symbols: SymbolTable::new(),
errors: Vec::new(),
warnings: Vec::new(),
store_inline_column_sets: std::collections::HashMap::new(),
current_flow_params: crate::store_column_proof::FlowParamTypes::new(),
manifest: Some(manifest),
ext_effect_members: std::collections::HashSet::new(),
ext_scan_categories: std::collections::HashSet::new(),
json_lens_fields: std::collections::HashMap::new(),
current_flow_param_spellings: std::collections::BTreeMap::new(),
current_mint_bindings: std::collections::HashSet::new(),
current_epistemic_mode: String::new(),
emitted_channels: std::collections::HashSet::new(),
secrets_backed_stores: std::collections::HashSet::new(),
module_ctx: None,
imported_symbols: std::collections::BTreeMap::new(),
}
}
pub fn check(self) -> Vec<TypeError> {
self.check_with_warnings().0
}
fn check_resource_module_laws(&mut self, decls: &[Declaration]) {
use std::collections::BTreeMap;
let mut resources: BTreeMap<&str, &ResourceDefinition> = BTreeMap::new();
for d in decls {
if let Declaration::Resource(r) = d {
resources.insert(r.name.as_str(), r);
}
}
if resources.is_empty() {
return;
}
let mut holders: BTreeMap<&str, Vec<(String, Loc)>> = BTreeMap::new();
let mut consumers: std::collections::BTreeSet<&str> =
std::collections::BTreeSet::new();
for d in decls {
match d {
Declaration::AxonStore(s) if !s.resource_ref.is_empty() => {
holders
.entry(s.resource_ref.as_str())
.or_default()
.push((format!("axonstore '{}'", s.name), s.loc.clone()));
}
Declaration::Tool(t) if !t.resource_ref.is_empty() => {
holders
.entry(t.resource_ref.as_str())
.or_default()
.push((format!("tool '{}'", t.name), t.loc.clone()));
}
Declaration::Upstream(u) if !u.resource_ref.is_empty() => {
holders
.entry(u.resource_ref.as_str())
.or_default()
.push((format!("upstream '{}'", u.name), u.loc.clone()));
}
Declaration::Lease(l) if !l.resource_ref.is_empty() => {
consumers.insert(l.resource_ref.as_str());
}
_ => {}
}
}
for (name, res) in &resources {
let held_by = holders.get(name).map(Vec::as_slice).unwrap_or(&[]);
let consumed = held_by.is_empty() && consumers.contains(&**name);
match res.lifetime.as_str() {
"linear" => {
if held_by.is_empty() && !consumed {
self.emit(
format!(
"axon-T945 Resource '{name}' is `lifetime: linear` but NOTHING \
uses it. A linear resource must be consumed exactly once — \
zero consumers is a breach, not an omission. Give it a holder \
(`axonstore X {{ resource: {name} }}`), or a `lease` over it, \
or declare it `affine` (may go unused). Listing it in a \
`manifest` does NOT count: a manifest PROVISIONS a resource, \
it does not use one.",
),
&res.loc,
);
} else if held_by.len() > 1 {
self.emit(
format!(
"axon-T945 Resource '{name}' is `lifetime: linear` but {} \
declarations name it ({}). Linear means EXACTLY ONE holder. \
Declare it `persistent` if it is meant to be shared — but then \
say so, because a shared pool that nobody declared shared is \
how connection exhaustion arrives without a suspect.",
held_by.len(),
held_by
.iter()
.map(|(w, _)| w.as_str())
.collect::<Vec<_>>()
.join(", ")
),
&res.loc,
);
}
}
"affine" => {
if held_by.len() > 1 {
self.emit(
format!(
"axon-T945 Resource '{name}' is `lifetime: affine` but {} \
declarations name it ({}). Affine means AT MOST ONE holder: it \
may go unused, but it may not be shared. Declare it \
`persistent` to share it.",
held_by.len(),
held_by
.iter()
.map(|(w, _)| w.as_str())
.collect::<Vec<_>>()
.join(", ")
),
&res.loc,
);
}
}
_ => {}
}
}
for d in decls {
let Declaration::Manifest(m) = d else { continue };
if m.fabric_ref.is_empty() {
continue;
}
for r_name in &m.resources {
let Some(res) = resources.get(r_name.as_str()) else {
continue; };
if res.within.is_empty() {
continue; }
if res.within != m.fabric_ref {
self.emit(
format!(
"axon-T947 Manifest '{}' declares `fabric: {}` and lists resource \
'{}', which is `within: {}`. The two disagree about where '{}' \
lives. `within:` is the single source of truth — a manifest cannot \
relocate a resource by listing it.",
m.name, m.fabric_ref, r_name, res.within, r_name
),
&m.loc,
);
}
}
}
}
pub fn check_with_warnings(mut self) -> (Vec<TypeError>, Vec<TypeError>) {
self.register_declarations(&self.program.declarations);
self.index_type_fields(&self.program.declarations);
self.collect_emitted_channels(&self.program.declarations);
self.check_json_lenses(&self.program.declarations);
self.collect_and_validate_extensions(&self.program.declarations);
self.check_declarations(&self.program.declarations);
self.check_cors_cross_method_consistency(&self.program.declarations);
self.check_cache_module_laws(&self.program.declarations);
self.check_resource_module_laws(&self.program.declarations);
for d in crate::effect_check::EffectChecker::new(self.program).check() {
self.emit(d.message, &d.loc);
}
(self.errors, self.warnings)
}
fn emit(&mut self, message: String, loc: &Loc) {
self.errors.push(TypeError {
message,
line: loc.line,
column: loc.column,
});
}
fn warn(&mut self, message: String, loc: &Loc) {
self.warnings.push(TypeError {
message,
line: loc.line,
column: loc.column,
});
}
fn check_range(&mut self, value: f64, lo: f64, hi: f64, field: &str, loc: &Loc) {
if value < lo || value > hi {
self.emit(
format!("{field} must be between {lo:.1} and {hi:.1}, got {value:.1}"),
loc,
);
}
}
fn register_declarations(&mut self, decls: &[Declaration]) {
if let Some(ctx) = self.module_ctx {
for decl in decls {
let Declaration::Import(n) = decl else { continue };
if n.names.is_empty() {
continue; }
let dotted = n.module_path.join(".");
let Some(exports) = ctx.modules.get(&dotted) else {
continue; };
for name in &n.names {
let Some(kind) = exports.get(name) else {
continue; };
if let Some(prev) = self.imported_symbols.get(name) {
let (_, prev_module) = prev.clone();
if prev_module != dotted {
self.emit(
format!(
"axon-T953 import collision: '{}' is imported from both \
'{}' and '{}'. A name has exactly one provider — alias \
support does not exist; import it from one module only.",
name, prev_module, dotted
),
&n.loc,
);
}
continue;
}
if self.symbols.declare(name, kind, n.loc.line).is_none() {
self.imported_symbols
.insert(name.clone(), (kind.clone(), dotted.clone()));
}
}
}
}
let mut registrations: Vec<(String, String, u32, Loc)> = Vec::new();
for decl in decls {
match decl {
Declaration::Budget(n) => {
registrations.push((
n.name.clone(),
"budget".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Effect(n) => {
registrations.push((
n.name.clone(),
"effect".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Persona(n) => {
registrations.push((
n.name.clone(),
"persona".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Context(n) => {
registrations.push((
n.name.clone(),
"context".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Anchor(n) => {
registrations.push((
n.name.clone(),
"anchor".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Memory(n) => {
registrations.push((
n.name.clone(),
"memory".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Tool(n) => {
registrations.push((n.name.clone(), "tool".into(), n.loc.line, n.loc.clone()));
}
Declaration::Type(n) => {
registrations.push((n.name.clone(), "type".into(), n.loc.line, n.loc.clone()));
}
Declaration::Flow(n) => {
registrations.push((n.name.clone(), "flow".into(), n.loc.line, n.loc.clone()));
}
Declaration::Intent(n) => {
registrations.push((
n.name.clone(),
"intent".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::LambdaData(n) => {
registrations.push((
n.name.clone(),
"lambda_data".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Agent(n) => {
registrations.push((n.name.clone(), "agent".into(), n.loc.line, n.loc.clone()));
}
Declaration::Window(n) => {
registrations.push((
n.name.clone(),
"window".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Shield(n) => {
registrations.push((
n.name.clone(),
"shield".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Pix(n) => {
registrations.push((n.name.clone(), "pix".into(), n.loc.line, n.loc.clone()));
}
Declaration::Ledger(n) => {
registrations.push((n.name.clone(), "ledger".into(), n.loc.line, n.loc.clone()));
}
Declaration::Psyche(n) => {
registrations.push((
n.name.clone(),
"psyche".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Corpus(n) => {
registrations.push((
n.name.clone(),
"corpus".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Dataspace(n) => {
registrations.push((
n.name.clone(),
"dataspace".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Ots(n) => {
registrations.push((n.name.clone(), "ots".into(), n.loc.line, n.loc.clone()));
}
Declaration::Mandate(n) => {
registrations.push((
n.name.clone(),
"mandate".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Compute(n) => {
registrations.push((
n.name.clone(),
"compute".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Daemon(n) => {
registrations.push((
n.name.clone(),
"daemon".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::AxonStore(n) => {
registrations.push((
n.name.clone(),
"axonstore".into(),
n.loc.line,
n.loc.clone(),
));
if n.backend == "secrets" {
self.secrets_backed_stores.insert(n.name.clone());
let synthesized = crate::store_schema::secrets_metadata_schema(
n.loc.line,
n.loc.column,
);
if let Some(cs) =
crate::store_column_proof::ColumnSet::from_inline_schema(
&synthesized,
)
{
self.store_inline_column_sets.insert(n.name.clone(), cs);
}
}
if n.backend != "secrets" {
if let Some(schema) = &n.column_schema {
match schema {
crate::store_schema::StoreColumnSchema::Inline { .. } => {
if let Some(cs) =
crate::store_column_proof::ColumnSet::from_inline_schema(
schema,
)
{
self.store_inline_column_sets
.insert(n.name.clone(), cs);
}
}
crate::store_schema::StoreColumnSchema::ManifestRef {
qualified_name,
..
} => {
if let Some(manifest) = self.manifest {
if let Some(ms) = manifest.lookup(qualified_name) {
let cs =
crate::store_column_proof::ColumnSet::from_manifest_store(
ms,
);
self.store_inline_column_sets
.insert(n.name.clone(), cs);
}
}
}
crate::store_schema::StoreColumnSchema::EnvVar {
var_name,
..
} => {
if let Some(manifest) = self.manifest {
let exact_key = format!("{}.{}", var_name, n.name);
let resolved = manifest
.lookup(&exact_key)
.or_else(|| {
let suffix = format!(".{}", n.name);
for (key, store) in &manifest.stores {
if key.ends_with(&suffix) {
return Some(store);
}
}
None
});
if let Some(ms) = resolved {
let cs =
crate::store_column_proof::ColumnSet::from_manifest_store(
ms,
);
self.store_inline_column_sets
.insert(n.name.clone(), cs);
}
}
}
}
}
} }
Declaration::AxonEndpoint(n) => {
registrations.push((
n.name.clone(),
"axonendpoint".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Resource(n) => {
registrations.push((
n.name.clone(),
"resource".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Fabric(n) => {
registrations.push((
n.name.clone(),
"fabric".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Manifest(n) => {
registrations.push((
n.name.clone(),
"manifest".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Observe(n) => {
registrations.push((
n.name.clone(),
"observe".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Reconcile(n) => {
registrations.push((
n.name.clone(),
"reconcile".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Lease(n) => {
registrations.push((n.name.clone(), "lease".into(), n.loc.line, n.loc.clone()));
}
Declaration::Ensemble(n) => {
registrations.push((
n.name.clone(),
"ensemble".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Session(n) => {
registrations.push((
n.name.clone(),
"session".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Topology(n) => {
registrations.push((
n.name.clone(),
"topology".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Immune(n) => {
registrations.push((
n.name.clone(),
"immune".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Reflex(n) => {
registrations.push((
n.name.clone(),
"reflex".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Heal(n) => {
registrations.push((n.name.clone(), "heal".into(), n.loc.line, n.loc.clone()));
}
Declaration::Component(n) => {
registrations.push((
n.name.clone(),
"component".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::View(n) => {
registrations.push((n.name.clone(), "view".into(), n.loc.line, n.loc.clone()));
}
Declaration::Channel(n) => {
registrations.push((
n.name.clone(),
"channel".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Socket(n) => {
registrations.push((n.name.clone(), "socket".into(), n.loc.line, n.loc.clone()));
}
Declaration::Upstream(n) => {
registrations.push((n.name.clone(), "upstream".into(), n.loc.line, n.loc.clone()));
}
Declaration::Voice(n) => {
registrations.push((n.name.clone(), "voice".into(), n.loc.line, n.loc.clone()));
}
Declaration::Cors(n) => {
registrations.push((n.name.clone(), "cors".into(), n.loc.line, n.loc.clone()));
}
Declaration::Credential(n) => {
registrations.push((
n.name.clone(),
"credential".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Cache(n) => {
registrations.push((n.name.clone(), "cache".into(), n.loc.line, n.loc.clone()));
}
Declaration::Savant(n) => {
registrations.push((
n.name.clone(),
"savant".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Document(n) => {
registrations.push((
n.name.clone(),
"document".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Deliver(n) => {
registrations.push((
n.name.clone(),
"deliver".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Notify(n) => {
registrations.push((
n.name.clone(),
"notify".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Synth(n) => {
registrations.push((
n.name.clone(),
"synth".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Scope(n) => {
registrations.push((
n.name.clone(),
"scope".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Observable(n) => {
registrations.push((
n.name.clone(),
"observable".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Witness(n) => {
registrations.push((
n.name.clone(),
"witness".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Generic(n) => {
if !n.name.is_empty() {
registrations.push((
n.name.clone(),
n.keyword.clone(),
n.loc.line,
n.loc.clone(),
));
}
}
Declaration::Extension(n) => {
registrations.push((
n.name.clone(),
"extension".into(),
n.loc.line,
n.loc.clone(),
));
}
Declaration::Epistemic(_) => {
}
Declaration::Import(_) | Declaration::Run(_) | Declaration::Let(_) => {}
}
}
for (name, kind, line, loc) in registrations {
if let Some((imp_kind, imp_module)) = self.imported_symbols.get(&name) {
let (imp_kind, imp_module) = (imp_kind.clone(), imp_module.clone());
self.emit(
format!(
"axon-T953 '{}' collides with the {} imported from '{}'. There is no \
shadowing: rename the local {} or drop the import.",
name, imp_kind, imp_module, kind
),
&loc,
);
continue;
}
if let Some(err) = self.symbols.declare(&name, &kind, line) {
self.emit(err, &loc);
}
}
for decl in decls {
if let Declaration::Epistemic(eb) = decl {
self.register_declarations(&eb.body);
}
}
}
fn collect_and_validate_extensions(&mut self, decls: &[Declaration]) {
for decl in decls {
match decl {
Declaration::Extension(ext) => self.check_extension(ext),
Declaration::Epistemic(eb) => {
self.collect_and_validate_extensions(&eb.body)
}
_ => {}
}
}
}
fn check_extension(&mut self, ext: &ExtensionDefinition) {
match ext.category.as_str() {
"effects" => {
for m in &ext.members {
let base = m.name.split(':').next().unwrap_or(m.name.as_str());
if is_valid(base, VALID_EFFECTS) {
self.emit(
format!(
"extension '{}' effect member '{}' shadows the canonical enforceable \
base '{}'. Extensions are PROVENANCE-class only (§Fase 53 invariant \
#2: E_C ∩ E_E = ∅) — they may not redefine or qualify an enforceable \
effect base.",
ext.name, m.name, base
),
&m.loc,
);
continue;
}
if let Some(c) = m.default_confidence {
if !(0.0..=1.0).contains(&c) {
self.emit(
format!(
"extension '{}' member '{}' has default_confidence {} outside the \
valid range [0.0, 1.0]",
ext.name, m.name, c
),
&m.loc,
);
continue;
}
}
self.ext_effect_members.insert(m.name.clone());
}
}
"scan" => {
for m in &ext.members {
if is_valid(&m.name, VALID_SCAN_CATEGORIES) {
self.emit(
format!(
"extension '{}' scan member '{}' shadows a canonical scan category \
(§Fase 53 invariant #3 — no shadowing of the canonical catalog)",
ext.name, m.name
),
&m.loc,
);
continue;
}
self.ext_scan_categories.insert(m.name.clone());
}
}
other => {
self.emit(
format!(
"extension '{}' has unknown category '{}'. Valid categories: effects, scan",
ext.name, other
),
&ext.loc,
);
}
}
}
fn check_declarations(&mut self, decls: &[Declaration]) {
for decl in decls {
match decl {
Declaration::Budget(n) => {
let scope = format!("budget '{}'", n.name);
self.check_budget(n, &scope);
}
Declaration::Effect(_) => {}
Declaration::Persona(n) => self.check_persona(n),
Declaration::Context(n) => self.check_context(n),
Declaration::Anchor(n) => self.check_anchor(n),
Declaration::Memory(n) => self.check_memory(n),
Declaration::Tool(n) => self.check_tool(n),
Declaration::Flow(n) => self.check_flow(n),
Declaration::Intent(n) => self.check_intent(n),
Declaration::Run(n) => self.check_run(n),
Declaration::Epistemic(eb) => {
self.check_epistemic_mode(&eb.mode, &eb.loc);
let prev =
std::mem::replace(&mut self.current_epistemic_mode, eb.mode.clone());
self.check_declarations(&eb.body);
self.current_epistemic_mode = prev;
}
Declaration::LambdaData(n) => self.check_lambda_data(n),
Declaration::Agent(n) => self.check_agent(n),
Declaration::Shield(n) => self.check_shield(n),
Declaration::Window(n) => self.check_window(n),
Declaration::Pix(n) => self.check_pix(n),
Declaration::Ledger(n) => self.check_ledger(n),
Declaration::Psyche(n) => self.check_psyche(n),
Declaration::Corpus(n) => self.check_corpus(n),
Declaration::Dataspace(n) => self.check_dataspace(n), Declaration::Ots(n) => self.check_ots(n),
Declaration::Mandate(n) => self.check_mandate(n),
Declaration::Compute(_) => {} Declaration::Daemon(n) => self.check_daemon(n),
Declaration::AxonStore(n) => self.check_axonstore(n),
Declaration::AxonEndpoint(n) => self.check_axonendpoint(n),
Declaration::Resource(n) => self.check_resource(n),
Declaration::Fabric(n) => self.check_fabric(n),
Declaration::Manifest(n) => self.check_manifest(n),
Declaration::Observe(n) => self.check_observe(n),
Declaration::Reconcile(n) => self.check_reconcile(n),
Declaration::Lease(n) => self.check_lease(n),
Declaration::Ensemble(n) => self.check_ensemble(n),
Declaration::Session(n) => self.check_session(n),
Declaration::Topology(n) => self.check_topology(n),
Declaration::Socket(n) => self.check_socket(n),
Declaration::Upstream(n) => self.check_upstream(n),
Declaration::Voice(n) => self.check_voice(n),
Declaration::Cors(n) => self.check_cors(n),
Declaration::Cache(n) => self.check_cache(n),
Declaration::Credential(n) => self.check_credential(n),
Declaration::Savant(n) => self.check_savant(n),
Declaration::Document(n) => self.check_document(n),
Declaration::Deliver(n) => self.check_deliver(n),
Declaration::Notify(n) => self.check_notify(n), Declaration::Synth(n) => self.check_synth(n),
Declaration::Scope(n) => self.check_scope(n),
Declaration::Observable(n) => self.check_observable(n),
Declaration::Witness(n) => self.check_witness(n),
Declaration::Immune(n) => self.check_immune(n),
Declaration::Reflex(n) => self.check_reflex(n),
Declaration::Heal(n) => self.check_heal(n),
Declaration::Component(n) => self.check_component(n),
Declaration::View(n) => self.check_view(n),
Declaration::Channel(n) => self.check_channel(n),
Declaration::Extension(_) => {}
Declaration::Import(n) => self.check_import_laws(n),
Declaration::Type(_)
| Declaration::Let(_)
| Declaration::Generic(_) => {}
}
}
}
fn check_import_laws(&mut self, node: &crate::ast::ImportNode) {
let Some(ctx) = self.module_ctx else { return };
if node.module_path.first().map(|s| s.starts_with('@')).unwrap_or(false) {
self.emit(
format!(
"axon-T953 `import {}` uses the @scope form, which is RESERVED for a \
future package registry and resolves to nothing today. Import a \
project module by its root-relative path instead.",
node.module_path.join(".")
),
&node.loc,
);
return;
}
if node.names.is_empty() {
self.emit(
format!(
"axon-T953 selective import required: `import {}` names nothing and \
would flood the namespace. Name what you need: `import {}.{{A, B}}`.",
node.module_path.join("."),
node.module_path.join(".")
),
&node.loc,
);
return;
}
let dotted = node.module_path.join(".");
let Some(exports) = ctx.modules.get(&dotted) else {
self.emit(
format!(
"axon-T953 module '{}' is not part of this compilation (expected file \
'{}').",
dotted,
node.module_path.join("/") + ".axon"
),
&node.loc,
);
return;
};
for name in &node.names {
if !exports.contains_key(name) {
let mut available: Vec<&str> =
exports.keys().map(String::as_str).collect();
available.sort_unstable();
self.emit(
format!(
"axon-T953 module '{}' does not export '{}'. Its exports: [{}].",
dotted,
name,
available.join(", ")
),
&node.loc,
);
}
}
}
fn check_persona(&mut self, node: &PersonaDefinition) {
if !node.tone.is_empty() && !is_valid(&node.tone, VALID_TONES) {
self.emit(
format!(
"Unknown tone '{}' for persona '{}'. Valid tones: {}",
node.tone,
node.name,
valid_list(VALID_TONES)
),
&node.loc,
);
}
if let Some(v) = node.confidence_threshold {
self.check_range(v, 0.0, 1.0, "confidence_threshold", &node.loc);
}
}
fn check_context(&mut self, node: &ContextDefinition) {
if !node.memory_scope.is_empty() && !is_valid(&node.memory_scope, VALID_MEMORY_SCOPES) {
self.emit(
format!(
"Unknown memory scope '{}' in context '{}'. Valid: {}",
node.memory_scope,
node.name,
valid_list(VALID_MEMORY_SCOPES)
),
&node.loc,
);
}
if !node.depth.is_empty() && !is_valid(&node.depth, VALID_DEPTHS) {
self.emit(
format!(
"Unknown depth '{}' in context '{}'. Valid: {}",
node.depth,
node.name,
valid_list(VALID_DEPTHS)
),
&node.loc,
);
}
if let Some(v) = node.temperature {
self.check_range(v, 0.0, 2.0, "temperature", &node.loc);
}
if let Some(v) = node.max_tokens {
if v <= 0 {
self.emit(
format!(
"max_tokens must be positive, got {} in context '{}'",
v, node.name
),
&node.loc,
);
}
}
if let Some(tz) = &node.now_tz {
self.check_now_tz(tz, "context", &node.name, &node.loc);
}
}
fn check_now_tz(&mut self, tz: &str, surface: &str, name: &str, loc: &Loc) {
let t = tz.trim();
let tz_ok = t == "UTC" || (t.contains('/') && !t.starts_with('/') && !t.ends_with('/'));
if !tz_ok {
self.emit(
format!(
"axon-T892 {surface} '{name}' declares an invalid `now:` timezone \
'{tz}' — expected an IANA name like \"America/Bogota\" or \"UTC\". \
Time is an explicit input: say WHOSE time the cognition runs in.",
),
loc,
);
}
}
fn check_anchor(&mut self, node: &AnchorConstraint) {
if let Some(v) = node.confidence_floor {
self.check_range(v, 0.0, 1.0, "confidence_floor", &node.loc);
}
if !node.on_violation.is_empty() && !is_valid(&node.on_violation, VALID_VIOLATION_ACTIONS) {
self.emit(
format!(
"Unknown on_violation action '{}' in anchor '{}'. Valid: {}",
node.on_violation,
node.name,
valid_list(VALID_VIOLATION_ACTIONS)
),
&node.loc,
);
}
if node.on_violation == "raise" && node.on_violation_target.is_empty() {
self.emit(
format!(
"Anchor '{}' uses 'raise' but no error type specified",
node.name
),
&node.loc,
);
}
}
fn check_memory(&mut self, node: &MemoryDefinition) {
if !node.store.is_empty() && !is_valid(&node.store, VALID_MEMORY_SCOPES) {
self.emit(
format!(
"Unknown store type '{}' in memory '{}'. Valid: {}",
node.store,
node.name,
valid_list(VALID_MEMORY_SCOPES)
),
&node.loc,
);
}
if !node.retrieval.is_empty() && !is_valid(&node.retrieval, VALID_RETRIEVAL_STRATEGIES) {
self.emit(
format!(
"Unknown retrieval strategy '{}' in memory '{}'. Valid: {}",
node.retrieval,
node.name,
valid_list(VALID_RETRIEVAL_STRATEGIES)
),
&node.loc,
);
}
}
fn check_technician_tool(&mut self, node: &ToolDefinition) {
let is_technician =
node.target.is_some() || node.risk.is_some() || !node.argv.is_empty();
if !is_technician {
return;
}
if let Some(risk) = &node.risk {
if !crate::technician::VALID_RISK_LEVELS.contains(&risk.as_str()) {
self.emit(
format!(
"axon-T862 technician tool '{}' has an unknown risk class '{}' — valid: {}",
node.name,
risk,
crate::technician::VALID_RISK_LEVELS.join(", ")
),
&node.loc,
);
}
}
if let Some(target) = &node.target {
match self.symbols.lookup(target) {
None => self.emit(
format!(
"axon-T861 technician tool '{}' targets undefined socket '{}'",
node.name, target
),
&node.loc,
),
Some(sym) if sym.kind != "socket" => self.emit(
format!(
"axon-T861 '{}' is a {}, not a socket (target of technician tool '{}')",
target, sym.kind, node.name
),
&node.loc,
),
_ => {}
}
}
if node.target.is_some() && node.provider == "bash" && node.argv.is_empty() {
self.emit(
format!(
"axon-T858 technician tool '{}' binds `target:` on `provider: bash` but \
declares no `argv:` — a free-string command would reopen the injection \
surface §Fase 84 exists to close (D84.1); declare an argv template, e.g. \
`argv: [\"ping\", \"-c\", \"${{count}}\", \"${{host}}\"]`",
node.name
),
&node.loc,
);
}
let param_names: std::collections::HashSet<&str> =
node.parameters.iter().map(|p| p.name.as_str()).collect();
for tok in &node.argv {
match crate::technician::classify_argv_token(tok) {
crate::technician::ArgvToken::Placeholder(name) => {
if !param_names.contains(name.as_str()) {
self.emit(
format!(
"axon-T859 argv placeholder '${{{}}}' in technician tool '{}' is \
not a declared `parameters:` entry — every argv placeholder must \
bind to a typed argument (the §54.b interpolation discipline)",
name, node.name
),
&node.loc,
);
}
}
crate::technician::ArgvToken::Partial(t) => {
self.emit(
format!(
"axon-T859 argv element '{}' in technician tool '{}' is not a \
whole-element placeholder — a `${{param}}` must be an ENTIRE argv \
element (never fused with surrounding text like `${{x}}.txt` or \
`pre${{x}}`), so an argument can neither be split nor escape its \
slot (D84.1)",
t, node.name
),
&node.loc,
);
}
crate::technician::ArgvToken::Literal(_) => {}
}
}
if node.risk.as_deref() == Some(crate::technician::RISK_DESTRUCTIVE) {
let has_branch = node
.target
.as_ref()
.and_then(|t| self.find_socket(t))
.and_then(|sock| self.find_session(&sock.protocol))
.map(|sess| {
sess.roles
.iter()
.any(|r| session_has_confirm_branch(&r.steps))
})
.unwrap_or(false);
if !has_branch {
self.emit(
format!(
"axon-T860 technician tool '{}' is `risk: destructive` but its bound \
session offers no reachable `branch{{ approved: […], denied: […] }}` — a \
destructive command must have a human confirm/deny exit visible in the \
protocol's own shape (D84.2); add the branch, or reclassify `risk: safe`",
node.name
),
&node.loc,
);
}
}
}
fn check_tool_cache_ref(&mut self, node: &ToolDefinition) {
if node.cache.is_empty() || node.cache == "none" {
return;
}
match self.symbols.lookup(&node.cache) {
None => {
self.emit(
format!(
"axon-T864 tool '{}' references undefined cache '{}' (use `cache: none` \
to opt out of a default policy)",
node.name, node.cache
),
&node.loc,
);
return;
}
Some(sym) if sym.kind != "cache" => {
self.emit(
format!(
"axon-T864 '{}' is a {}, not a cache (referenced by tool '{}')",
node.cache, sym.kind, node.name
),
&node.loc,
);
return;
}
_ => {}
}
if !tool_is_pure(&node.effects) {
if let Some(cache_decl) = self.find_cache(&node.cache) {
if cache_decl.ttl.is_none() {
let row = node
.effects
.as_ref()
.map(|r| r.effects.join(", "))
.unwrap_or_else(|| "<none declared>".to_string());
self.emit(
format!(
"axon-T865 tool '{}' (effects <{}>, not proven `pure`) is memoised by \
cache '{}' which declares no `ttl:` — a non-deterministic result may \
not be cached forever; give '{}' a finite `ttl:`",
node.name, row, node.cache, node.cache
),
&node.loc,
);
}
}
}
}
fn check_retrieve_cache_ref(&mut self, cache_ref: &str, flow_name: &str, loc: &Loc) {
if cache_ref.is_empty() {
return;
}
match self.symbols.lookup(cache_ref) {
None => {
self.emit(
format!(
"axon-T864 retrieve in flow '{}' references undefined cache '{}'",
flow_name, cache_ref
),
loc,
);
return;
}
Some(sym) if sym.kind != "cache" => {
self.emit(
format!(
"axon-T864 '{}' is a {}, not a cache (referenced by a retrieve in flow '{}')",
cache_ref, sym.kind, flow_name
),
loc,
);
return;
}
_ => {}
}
if let Some(cache_decl) = self.find_cache(cache_ref) {
if cache_decl.ttl.is_none() {
self.emit(
format!(
"axon-T865 retrieve in flow '{}' is memoised by cache '{}' which declares \
no `ttl:` — a `retrieve` reads mutable store data, so a cached result may \
not live forever; give '{}' a finite `ttl:` (and typically an \
`invalidate_on:` channel)",
flow_name, cache_ref, cache_ref
),
loc,
);
}
}
}
fn check_scrape_tool(&mut self, node: &ToolDefinition) {
let is_scrape_provider = VALID_SCRAPE_PROVIDERS.contains(&node.provider.as_str());
let scrape = match &node.scrape {
Some(s) => s,
None => {
if is_scrape_provider {
self.check_scrape_effect_honesty(node, false);
}
return;
}
};
if !is_scrape_provider {
self.emit(
format!(
"axon-T905 tool '{}' declares a `scrape:` block but its `provider:` is \
'{}', not a web-acquisition engine. The scrape config only applies to a \
tool whose provider is one of: {}.",
node.name,
if node.provider.is_empty() { "<unset>" } else { &node.provider },
valid_list(VALID_SCRAPE_PROVIDERS)
),
&node.loc,
);
}
if let Some(engine) = &scrape.engine {
if !is_valid(engine, VALID_SCRAPE_ENGINES) {
self.emit(
format!(
"axon-T905 tool '{}' scrape `engine: {}` is not a known engine. Valid: {}.",
node.name,
engine,
valid_list(VALID_SCRAPE_ENGINES)
),
&scrape.loc,
);
}
}
if let Some(profile) = &scrape.impersonate {
if !is_valid(profile, VALID_IMPERSONATE_PROFILES) {
self.emit(
format!(
"axon-T905 tool '{}' scrape `impersonate: {}` is not a known \
fingerprint profile. Valid: {}.",
node.name,
profile,
valid_list(VALID_IMPERSONATE_PROFILES)
),
&scrape.loc,
);
}
}
let engine_is_browser = scrape.engine.as_deref() == Some("browser");
if scrape.render_wait.is_some() && !engine_is_browser {
self.emit(
format!(
"axon-T905 tool '{}' sets `render_wait:` but its `engine:` is not \
`browser` — the impersonate engine renders no JS, so a settle wait has \
no effect. Set `engine: browser` or drop `render_wait:`.",
node.name
),
&scrape.loc,
);
}
if scrape.impersonate.is_some() && engine_is_browser {
self.emit(
format!(
"axon-T905 tool '{}' sets `impersonate:` but `engine: browser` — the \
browser sidecar presents its own real fingerprint; profile impersonation \
is an `engine: impersonate` concept.",
node.name
),
&scrape.loc,
);
}
let dom_only = [
(!scrape.extract.is_empty(), "extract"),
(scrape.adaptive.is_some(), "adaptive"),
(scrape.similarity_floor.is_some(), "similarity_floor"),
];
let crawl_only = [
(!scrape.follow.is_empty(), "follow"),
(scrape.max_depth.is_some(), "max_depth"),
(scrape.max_pages.is_some(), "max_pages"),
(scrape.concurrency.is_some(), "concurrency"),
(!scrape.politeness.is_empty(), "politeness"),
(!scrape.checkpoint.is_empty(), "checkpoint"),
];
if node.provider != "scrape_dom" {
for (present, field) in dom_only {
if present {
self.emit(
format!(
"axon-T905 tool '{}' sets scrape `{}:` but its provider is '{}' — \
extraction fields (`extract`/`adaptive`/`similarity_floor`) apply \
only to `scrape_dom`.",
node.name, field, node.provider
),
&scrape.loc,
);
}
}
}
if node.provider != "scrape_crawl" {
for (present, field) in crawl_only {
if present {
self.emit(
format!(
"axon-T905 tool '{}' sets scrape `{}:` but its provider is '{}' — \
crawl fields (`follow`/`max_depth`/`max_pages`/`concurrency`/\
`politeness`/`checkpoint`) apply only to `scrape_crawl`.",
node.name, field, node.provider
),
&scrape.loc,
);
}
}
}
for spec in &scrape.extract {
let ok = match spec.split_once('=') {
Some((name, sel)) => !name.trim().is_empty() && !sel.trim().is_empty(),
None => false,
};
if !ok {
self.emit(
format!(
"axon-T906 tool '{}' scrape `extract` entry '{}' is malformed — each \
FieldSpec must be `name=selector` (e.g. `\"title=h1\"`).",
node.name, spec
),
&scrape.loc,
);
}
}
if let Some(f) = scrape.similarity_floor {
if !(0.0..=1.0).contains(&f) {
self.emit(
format!(
"axon-T907 tool '{}' scrape `similarity_floor: {}` is out of range — \
the adaptive-relocation threshold must be in [0, 1].",
node.name, f
),
&scrape.loc,
);
}
}
for (name, field) in [
(&scrape.politeness, "politeness"),
(&scrape.checkpoint, "checkpoint"),
] {
if !name.is_empty() && self.symbols.lookup(name).is_none() {
self.emit(
format!(
"axon-T909 tool '{}' scrape `{}: {}` references an undeclared name — \
it must resolve to a declared {} in this program.",
node.name,
field,
name,
if field == "politeness" { "budget" } else { "store" }
),
&scrape.loc,
);
}
}
if is_scrape_provider {
self.check_scrape_effect_honesty(node, scrape.adaptive == Some(true));
}
}
fn check_scrape_effect_honesty(&mut self, node: &ToolDefinition, adaptive: bool) {
let bases: std::collections::HashSet<String> = node
.effects
.as_ref()
.map(|e| {
e.effects
.iter()
.map(|s| s.split(':').next().unwrap_or(s).to_string())
.collect()
})
.unwrap_or_default();
let require = |this: &mut Self, base: &str, why: &str| {
if !bases.contains(base) {
this.emit(
format!(
"axon-T904 web-acquisition tool '{}' (`provider: {}`) must declare the \
`{}` effect — {}. Add it to `effects: <…>`.",
node.name, node.provider, base, why
),
&node.loc,
);
}
};
require(
self,
"web",
"web content is born epistemically Untrusted (D98.1) and cannot reach an agent's \
belief without a shield",
);
match node.provider.as_str() {
"scrape_http" | "scrape_crawl" | "scrape_enrich" => {
require(self, "network", "it performs live network I/O");
}
"scrape_dom" => {
if bases.contains("network") {
self.emit(
format!(
"axon-T904 tool '{}' (`provider: scrape_dom`) declares `network` but \
performs NO network I/O — it processes an already-fetched, already-\
tainted `page:`. Drop `network`; keep `web` (the taint is \
preserved, not re-acquired).",
node.name
),
&node.loc,
);
}
}
_ => {}
}
if adaptive && !bases.contains("storage") {
self.emit(
format!(
"axon-T904 tool '{}' sets `adaptive: true` but does not declare `storage` — \
adaptive relocation persists per-tenant selector memory (§98.h), a real \
`<storage>` effect. Add `storage` to `effects: <…>`.",
node.name
),
&node.loc,
);
}
}
fn check_tool(&mut self, node: &ToolDefinition) {
if !node.provider.is_empty() && !VALID_TOOL_PROVIDERS.contains(&node.provider.as_str()) {
self.emit(
format!(
"axon-T948 tool '{}' declares `provider: {}`, which is not a known provider. \
Expected one of: {} (or omit `provider:` for an LLM-routed tool). A provider \
the runtime does not recognise used to reach a fabricating fallthrough — on \
the primitive built so an action's result is born with an honest epistemic \
status, a typo produced an invented one.",
node.name,
node.provider,
VALID_TOOL_PROVIDERS.join(" | ")
),
&node.loc,
);
}
let rt = node.runtime.trim();
if rt.starts_with("http://") || rt.starts_with("https://") {
self.emit(
format!(
"axon-T949 tool '{}' pins an absolute `runtime: \"{}\"` — a production URL in \
source. That opens a real connection with no lifetime, no capacity, no \
shield: the channel is ungoverned. Name a `resource` instead (`tool {} {{ \
resource: <R> }}`), whose `endpoint` is a per-tenant config key (axon-T944), \
and let `runtime:` name the PATH within it. URLs never appear in source — \
the same law `axon-T850` enforces on `upstream.resolve` and `axon-T902` on \
`tool.secret`.",
node.name, node.runtime, node.name
),
&node.loc,
);
}
if !node.resource_ref.is_empty() {
match self.symbols.lookup(&node.resource_ref) {
None => self.emit(
format!(
"axon-T950 tool '{}' names resource '{}', which is not declared.",
node.name, node.resource_ref
),
&node.loc,
),
Some(sym) if sym.kind != "resource" => self.emit(
format!(
"axon-T950 tool '{}' names '{}', which is a {}, not a resource.",
node.name, node.resource_ref, sym.kind
),
&node.loc,
),
_ => {}
}
}
self.check_technician_tool(node);
self.check_scrape_tool(node);
self.check_tool_cache_ref(node);
if !node.secret.is_empty() {
let mut chars = node.secret.chars();
let head_ok = chars
.next()
.is_some_and(|c| c.is_ascii_lowercase() || c.is_ascii_digit());
let rest_ok = chars.all(|c| {
c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '_' | '.' | '-')
});
if !head_ok || !rest_ok {
self.emit(
format!(
"axon-T902 tool '{}' `secret:` value '{}' is not a config key — \
keys are lowercase dot-separated (`[a-z0-9][a-z0-9_.-]*`, no \
`/`, no `:`); credentials never appear in source. The runtime \
resolves the key against the tenant's secret custody at \
dispatch and injects the value under the reserved \
`axon_secret` request field (`rotation_without_revelation`).",
node.name, node.secret
),
&node.loc,
);
}
if node.target.is_some() {
self.emit(
format!(
"axon-T902 tool '{}' declares BOTH `target:` (technician argv \
dispatch) and `secret:` (HTTP dispatch injection) — a \
technician command has no request body to inject a secret \
into. Drop one: technician credentials belong on the machine \
end of the socket, never in the command channel.",
node.name
),
&node.loc,
);
}
}
if !node.secret_partition.is_empty() {
if node.secret.is_empty() {
self.emit(
format!(
"axon-T903 tool '{}' declares `secret_partition: {}` but no \
`secret:` — the partition appends a per-call segment to the \
secret class key, so a `secret:` (e.g. `crm.hubspot`) must be \
present for it to extend.",
node.name, node.secret_partition
),
&node.loc,
);
}
if node.target.is_some() {
self.emit(
format!(
"axon-T903 tool '{}' declares `secret_partition:` on a \
`target:`-bound technician tool — argv dispatch has no request \
body to inject a partitioned secret into (the `axon-T902` \
`secret:` exclusion, applied to its selector).",
node.name
),
&node.loc,
);
}
match node
.parameters
.iter()
.find(|p| p.name == node.secret_partition)
{
None => {
self.emit(
format!(
"axon-T903 tool '{}' declares `secret_partition: {}` but has \
no parameter named '{}'. The partition selects the custody \
entry by an argument the CALLER passes to this tool — it \
must be one of the tool's declared `parameters:` (add \
`{}: String` to `parameters:`).",
node.name,
node.secret_partition,
node.secret_partition,
node.secret_partition
),
&node.loc,
);
}
Some(p) if p.type_expr.name != "String" || p.type_expr.optional => {
self.emit(
format!(
"axon-T903 tool '{}' partition parameter '{}' has type '{}{}' \
— a `secret_partition` becomes one key segment, so it must \
be a required `String` (not optional, not numeric, not \
generic). A missing or non-scalar discriminator cannot \
address a custody entry.",
node.name,
node.secret_partition,
p.type_expr.name,
if p.type_expr.optional { "?" } else { "" }
),
&node.loc,
);
}
Some(_) => {}
}
}
if let Some(v) = node.max_results {
if v <= 0 {
self.emit(
format!(
"max_results must be positive, got {} in tool '{}'",
v, node.name
),
&node.loc,
);
}
}
if let Some(ref eff) = node.effects {
for e in &eff.effects {
if self.ext_effect_members.contains(e) {
continue;
}
let (base, qualifier) = match e.split_once(':') {
Some((b, q)) => (b, Some(q)),
None => (e.as_str(), None),
};
if base == "ingest" {
match qualifier {
Some(class) if is_valid(class, VALID_INGEST_CLASSES) => {}
other => self.emit(
format!(
"axon-T1000 tool '{}' declares `ingest:{}` — the ingest \
provenance class must be one of: {}.",
node.name,
other.unwrap_or("<none>"),
valid_list(VALID_INGEST_CLASSES)
),
&node.loc,
),
}
continue;
}
if !is_valid(base, VALID_EFFECTS) {
self.emit(
format!(
"Unknown effect '{}' in tool '{}'. Valid: {}",
e,
node.name,
valid_list(VALID_EFFECTS)
),
&node.loc,
);
continue;
}
match base {
"stream" => match qualifier {
None => self.emit(
format!(
"Effect 'stream' in tool '{}' requires a \
backpressure policy qualifier \
'stream:<policy>'. Valid policies: {}",
node.name,
valid_list(crate::stream_effect::BACKPRESSURE_CATALOG)
),
&node.loc,
),
Some(q) => {
if !is_valid(q, crate::stream_effect::BACKPRESSURE_CATALOG) {
self.emit(
format!(
"Unknown backpressure policy '{}' in tool '{}'. \
Valid: {}",
q,
node.name,
valid_list(crate::stream_effect::BACKPRESSURE_CATALOG)
),
&node.loc,
);
}
}
},
"trust" => match qualifier {
None => self.emit(
format!(
"Effect 'trust' in tool '{}' requires a proof \
qualifier 'trust:<proof>'. Valid proofs: {}",
node.name,
valid_list(crate::refinement::TRUST_CATALOG)
),
&node.loc,
),
Some(q) => {
if !is_valid(q, crate::refinement::TRUST_CATALOG) {
self.emit(
format!(
"Unknown trust proof '{}' in tool '{}'. \
Valid: {}",
q,
node.name,
valid_list(crate::refinement::TRUST_CATALOG)
),
&node.loc,
);
}
}
},
"sensitive" => {
if qualifier.is_none() {
self.emit(
format!(
"Effect 'sensitive' in tool '{}' \
requires a jurisdiction qualifier \
'sensitive:<category>' (e.g. \
'sensitive:health_data'). The \
category is adopter-defined; the \
legal basis covering it must also \
be declared via 'legal:<basis>' on \
the same tool.",
node.name,
),
&node.loc,
);
}
}
"legal" => match qualifier {
None => self.emit(
format!(
"Effect 'legal' in tool '{}' requires a \
basis qualifier 'legal:<basis>'. Valid \
bases: {}",
node.name,
valid_list(crate::legal_basis::LEGAL_BASIS_CATALOG)
),
&node.loc,
),
Some(q) => {
if !is_valid(q, crate::legal_basis::LEGAL_BASIS_CATALOG) {
self.emit(
format!(
"Unknown legal basis '{}' in tool \
'{}'. Valid: {}",
q,
node.name,
valid_list(crate::legal_basis::LEGAL_BASIS_CATALOG)
),
&node.loc,
);
}
}
},
"ots" => match qualifier {
None => self.emit(
format!(
"Effect 'ots' in tool '{}' requires a \
subkind. Expected 'ots:transform:<from>:<to>' \
or 'ots:backend:<native|ffmpeg>'.",
node.name
),
&node.loc,
),
Some(inner) => {
let (subkind, rest) = match inner.split_once(':') {
Some((a, b)) => (a, Some(b)),
None => (inner, None),
};
match subkind {
"transform" => {
let valid = rest
.and_then(|r| r.split_once(':'))
.map(|(f, t)| !f.is_empty() && !t.is_empty())
.unwrap_or(false);
if !valid {
self.emit(
format!(
"Effect 'ots:transform' in tool \
'{}' requires '<from>:<to>' \
qualifier (e.g. \
'ots:transform:mulaw8:pcm16').",
node.name
),
&node.loc,
);
}
}
"backend" => {
let qual = rest.unwrap_or("");
if !is_valid(qual, crate::ots_catalog::OTS_BACKEND_CATALOG) {
self.emit(
format!(
"Unknown OTS backend '{}' in tool '{}'. \
Valid: {}",
qual,
node.name,
valid_list(crate::ots_catalog::OTS_BACKEND_CATALOG)
),
&node.loc,
);
}
}
other => self.emit(
format!(
"Unknown 'ots' subkind '{}' in tool '{}'. \
Expected 'transform' or 'backend'.",
other, node.name
),
&node.loc,
),
}
}
},
_ => {}
}
}
if !eff.epistemic_level.is_empty()
&& !is_valid(&eff.epistemic_level, VALID_EPISTEMIC_LEVELS)
{
self.emit(
format!(
"Unknown epistemic level '{}' in tool '{}'. Valid: {}",
eff.epistemic_level,
node.name,
valid_list(VALID_EPISTEMIC_LEVELS)
),
&node.loc,
);
}
if eff.effects.iter().any(|e| e == "ingest:inferred")
&& eff.epistemic_level == "know"
{
self.emit(
format!(
"axon-T1001 tool '{}' declares `ingest:inferred` AND `epistemic:know` — \
an inferred (OCR/vision) read is a belief about pixels, not a fact about \
a file; it can never be `know` (D100.1). Its ceiling is `believe`.",
node.name
),
&node.loc,
);
}
}
if let Some(ref eff) = node.effects {
let mut sensitive_categories: Vec<&str> = Vec::new();
let mut has_legal_basis = false;
let mut legal_bases_hipaa: Vec<&str> = Vec::new();
let mut has_ffmpeg_backend = false;
for e in &eff.effects {
let (base, qual) = match e.split_once(':') {
Some((b, q)) => (b, Some(q)),
None => (e.as_str(), None),
};
if base == "sensitive" {
if let Some(q) = qual {
sensitive_categories.push(q);
}
}
if base == "legal" {
if let Some(q) = qual {
if is_valid(q, crate::legal_basis::LEGAL_BASIS_CATALOG) {
has_legal_basis = true;
if q.starts_with("HIPAA.") {
legal_bases_hipaa.push(q);
}
}
}
}
if base == "ots" {
if let Some(inner) = qual {
if let Some(("backend", backend)) = inner.split_once(':') {
if backend == "ffmpeg" {
has_ffmpeg_backend = true;
}
}
}
}
}
if !sensitive_categories.is_empty() && !has_legal_basis {
self.emit(
format!(
"Tool '{}' declares sensitive effect(s) [{}] but \
carries no 'legal:<basis>' effect. Regulated \
processing requires an explicit legal basis: {}.",
node.name,
sensitive_categories.join(", "),
valid_list(crate::legal_basis::LEGAL_BASIS_CATALOG)
),
&node.loc,
);
}
if !legal_bases_hipaa.is_empty() && has_ffmpeg_backend {
self.emit(
format!(
"Tool '{}' combines HIPAA legal basis ({}) with \
'ots:backend:ffmpeg'. ePHI MUST NOT cross the \
process boundary to a subprocess outside the \
auditable runtime. Use 'ots:backend:native' or \
register a native transformer that covers the \
required pipeline.",
node.name,
legal_bases_hipaa.join(", "),
),
&node.loc,
);
}
}
}
fn infer_expr(
&mut self,
e: &Expr,
scope: &std::collections::BTreeMap<String, String>,
loc: &Loc,
) -> InferType {
use InferType as T;
match e {
Expr::Lit(ExprLit::Int(_)) => T::Int,
Expr::Lit(ExprLit::Float(_)) => T::Float,
Expr::Lit(ExprLit::Bool(_)) => T::Bool,
Expr::Lit(ExprLit::Str(_)) => T::Str,
Expr::Let { name, value, body } => {
let bound = self.infer_expr(value, scope, loc);
let mut inner = scope.clone();
inner.insert(name.clone(), bound.label().to_string());
self.infer_expr(body, &inner, loc)
}
Expr::Ref(p) => {
if let Some((root, rest)) = p.split_once('.') {
if let Some(struct_name) =
scope.get(root).and_then(|s| Self::parse_json_lens(s))
{
let segments: Vec<&str> = rest.split('.').collect();
return self.lens_field_walk(struct_name, &segments, loc);
}
}
scope
.get(p)
.map(|n| infer_type_from_name(n))
.unwrap_or(T::Unknown)
}
Expr::Unary(UnOp::Neg, x) => {
let t = self.infer_expr(x, scope, loc);
if t != T::Unknown && !t.is_numeric() {
self.emit(
format!(
"axon-T810 unary `-` requires a numeric operand, got {}",
t.label()
),
loc,
);
}
if t.is_numeric() {
t
} else {
T::Unknown
}
}
Expr::Unary(UnOp::Not, x) => {
let t = self.infer_expr(x, scope, loc);
if t != T::Unknown && t != T::Bool {
self.emit(
format!(
"axon-T812 `not` requires a boolean operand, got {}",
t.label()
),
loc,
);
}
T::Bool
}
Expr::Call(builtin, args) => {
let extra = builtin.extra_arity();
let got_extra = args.len().saturating_sub(1);
if args.is_empty() || got_extra != extra {
self.emit(
format!(
"axon-T813 `.{}` takes {extra} argument(s), got {got_extra}",
builtin.surface()
),
loc,
);
}
let recv = args
.first()
.map(|a| self.infer_expr(a, scope, loc))
.unwrap_or(T::Unknown);
let arg_types: Vec<T> = args
.iter()
.skip(1)
.map(|a| self.infer_expr(a, scope, loc))
.collect();
let recv_is_scalar = matches!(recv, T::Int | T::Float | T::Bool);
match builtin {
Builtin::Length | Builtin::Count | Builtin::IsEmpty => {
if recv_is_scalar {
self.emit(
format!(
"axon-T814 `.{}` needs a collection or string, got {}",
builtin.surface(),
recv.label()
),
loc,
);
}
if matches!(builtin, Builtin::IsEmpty) {
T::Bool
} else {
T::Int
}
}
Builtin::IsNull => T::Bool,
Builtin::Contains => {
if recv_is_scalar {
self.emit(
format!(
"axon-T814 `.contains` needs a collection or string, got {}",
recv.label()
),
loc,
);
}
T::Bool
}
Builtin::StartsWith | Builtin::EndsWith => {
if recv != T::Unknown && recv != T::Str {
self.emit(
format!(
"axon-T814 `.{}` needs a string receiver, got {}",
builtin.surface(),
recv.label()
),
loc,
);
}
if let Some(a) = arg_types.first() {
if *a != T::Unknown && *a != T::Str {
self.emit(
format!(
"axon-T814 `.{}` argument must be a string, got {}",
builtin.surface(),
a.label()
),
loc,
);
}
}
T::Bool
}
Builtin::AsInt => T::Int,
Builtin::AsFloat => T::Float,
Builtin::AsString => T::Str,
Builtin::AsBool => T::Bool,
}
}
Expr::Field(base, field) => {
let tb = self.infer_expr(base, scope, loc);
if matches!(tb, T::Int | T::Float | T::Bool) {
self.emit(
format!(
"axon-T814 cannot access field `.{field}` of a {}",
tb.label()
),
loc,
);
}
if let Some(struct_name) = self.lens_shape_of(base, scope) {
let field_ty = self
.json_lens_fields
.get(&struct_name)
.and_then(|m| m.get(field))
.map(|(ty, _)| ty.clone());
match field_ty {
Some(ty) => return infer_type_from_name(&ty),
None => {
self.emit(
format!(
"axon-T842 the lens `Json<{struct_name}>` declares no \
field `{field}`. The shape is a checkable EXPECTATION \
— navigating an undeclared field is a likely typo \
(runtime navigation stays total → a real document's \
extra field still reads as null here). Add `{field}` \
to `type {struct_name}`, or drop the `<{struct_name}>` \
shape to navigate the open `Json` freely."
),
loc,
);
return T::Unknown;
}
}
}
T::Unknown
}
Expr::Index(base, index) => {
let tb = self.infer_expr(base, scope, loc);
let _ = self.infer_expr(index, scope, loc);
if matches!(tb, T::Int | T::Float | T::Bool) {
self.emit(
format!("axon-T814 cannot index a {} (need a collection or string)", tb.label()),
loc,
);
}
T::Unknown
}
Expr::Binary(op, l, r) => {
let tl = self.infer_expr(l, scope, loc);
let tr = self.infer_expr(r, scope, loc);
let sym = bin_op_symbol(*op);
match op {
BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod => {
if tl != T::Unknown && !tl.is_numeric() {
self.emit(
format!(
"axon-T810 left operand of `{sym}` must be numeric, got {}",
tl.label()
),
loc,
);
}
if tr != T::Unknown && !tr.is_numeric() {
self.emit(
format!(
"axon-T810 right operand of `{sym}` must be numeric, got {}",
tr.label()
),
loc,
);
}
if tl == T::Int && tr == T::Int {
T::Int
} else if tl.is_numeric() && tr.is_numeric() {
T::Float
} else {
T::Unknown
}
}
BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge => {
if tl != T::Unknown && tr != T::Unknown {
let ok =
(tl.is_numeric() && tr.is_numeric()) || (tl == T::Str && tr == T::Str);
if !ok {
self.emit(
format!(
"axon-T811 cannot order {} against {} with `{sym}` \
(ordering needs two numbers or two strings)",
tl.label(),
tr.label()
),
loc,
);
}
}
T::Bool
}
BinOp::Eq | BinOp::Ne => {
if tl != T::Unknown && tr != T::Unknown && tl.eq_class() != tr.eq_class() {
self.emit(
format!(
"axon-T811 cannot compare {} with {} using `{sym}` \
(incompatible types)",
tl.label(),
tr.label()
),
loc,
);
}
T::Bool
}
BinOp::And | BinOp::Or => {
if tl != T::Unknown && tl != T::Bool {
self.emit(
format!(
"axon-T812 left operand of `{sym}` must be boolean, got {}",
tl.label()
),
loc,
);
}
if tr != T::Unknown && tr != T::Bool {
self.emit(
format!(
"axon-T812 right operand of `{sym}` must be boolean, got {}",
tr.label()
),
loc,
);
}
T::Bool
}
}
}
}
}
fn check_flow(&mut self, node: &FlowDefinition) {
self.current_flow_params = crate::store_column_proof::FlowParamTypes::new();
for param in &node.parameters {
self.current_flow_params
.insert(param.name.clone(), param.type_expr.name.clone());
}
self.current_mint_bindings.clear();
self.current_flow_param_spellings.clear();
for param in &node.parameters {
let spelling = if param.type_expr.generic_param.is_empty() {
param.type_expr.name.clone()
} else {
format!("{}<{}>", param.type_expr.name, param.type_expr.generic_param)
};
self.current_flow_param_spellings
.insert(param.name.clone(), spelling);
}
for param in &node.parameters {
self.check_type_reference(¶m.type_expr.name, ¶m.loc);
}
if let Some(ref rt) = node.return_type {
self.check_type_reference(&rt.name, &rt.loc);
}
let mut step_names: Vec<String> = Vec::new();
for step in &node.body {
if let FlowStep::Step(s) = step {
if step_names.contains(&s.name) {
self.emit(
format!("Duplicate step name '{}' in flow '{}'", s.name, node.name),
&s.loc,
);
} else {
step_names.push(s.name.clone());
}
if let Some(v) = s.confidence_floor {
self.check_range(v, 0.0, 1.0, "confidence_floor", &s.loc);
}
}
}
self.check_flow_steps(&node.body, &node.name);
self.check_refinement_and_stream_contracts(node);
self.check_content_injection_barrier(node);
}
fn check_content_injection_barrier(&mut self, flow: &FlowDefinition) {
let mut tool_effects: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
self.collect_tool_effects(&self.program.declarations, &mut tool_effects);
let is_web_tool = |name: &str| -> bool {
tool_effects
.get(name)
.map(|effs| {
effs.iter().any(|e| {
let base = e.split(':').next().unwrap_or(e);
base == "web" || base == "ingest"
})
})
.unwrap_or(false)
};
let mut shielded_agents: std::collections::HashSet<&str> = std::collections::HashSet::new();
self.collect_shielded_agents(&self.program.declarations, &mut shielded_agents);
let mut web_producer = false;
let mut unshielded_belief = false;
let mut shield_step_present = false;
self.walk_flow_for_injection(
&flow.body,
&is_web_tool,
&shielded_agents,
&mut web_producer,
&mut unshielded_belief,
&mut shield_step_present,
);
if web_producer && unshielded_belief && !shield_step_present {
self.emit(
format!(
"axon-T908 flow '{}' acquires adversarial ingress content (a tool carrying \
the `web` effect (§98 scraping) or an `ingest:*` annotation (§100 document \
ingestion), born Untrusted) and feeds a cognitive step whose agent declares \
no shield, with no `shield` applied in the flow. Scraped/ingested content is \
adversarial: scan it before it reaches an agent's beliefs. Add a `shield <S> \
on <value>` step (scanning e.g. `prompt_injection`/`pii_leak`) before the \
reasoning step, or give the agent a `shield:` gate.",
flow.name
),
&flow.loc,
);
}
}
fn collect_shielded_agents<'d>(
&self,
decls: &'d [Declaration],
out: &mut std::collections::HashSet<&'d str>,
) {
for d in decls {
match d {
Declaration::Agent(a) if !a.shield_ref.is_empty() => {
out.insert(a.name.as_str());
}
Declaration::Epistemic(eb) => self.collect_shielded_agents(&eb.body, out),
_ => {}
}
}
}
#[allow(clippy::only_used_in_recursion)]
fn walk_flow_for_injection(
&self,
steps: &[FlowStep],
is_web_tool: &dyn Fn(&str) -> bool,
shielded_agents: &std::collections::HashSet<&str>,
web_producer: &mut bool,
unshielded_belief: &mut bool,
shield_step_present: &mut bool,
) {
for step in steps {
match step {
FlowStep::Step(s) => {
for tref in [&s.apply_ref, &s.navigate_ref] {
if !tref.is_empty() && is_web_tool(tref) {
*web_producer = true;
}
}
let is_cognitive = !s.ask.is_empty() || !s.persona_ref.is_empty();
let agent_shielded = !s.persona_ref.is_empty()
&& shielded_agents.contains(s.persona_ref.as_str());
if is_cognitive && !agent_shielded {
*unshielded_belief = true;
}
}
FlowStep::UseTool(u) => {
if is_web_tool(&u.tool_name) {
*web_producer = true;
}
}
FlowStep::ShieldApply(_) => {
*shield_step_present = true;
}
FlowStep::If(c) => {
self.walk_flow_for_injection(
&c.then_body,
is_web_tool,
shielded_agents,
web_producer,
unshielded_belief,
shield_step_present,
);
self.walk_flow_for_injection(
&c.else_body,
is_web_tool,
shielded_agents,
web_producer,
unshielded_belief,
shield_step_present,
);
}
FlowStep::ForIn(f) => {
self.walk_flow_for_injection(
&f.body,
is_web_tool,
shielded_agents,
web_producer,
unshielded_belief,
shield_step_present,
);
}
_ => {}
}
}
}
fn check_refinement_and_stream_contracts(&mut self, flow: &FlowDefinition) {
let mut uses_stream = false;
let mut uses_untrusted = false;
for param in &flow.parameters {
if crate::stream_effect::is_stream_type(¶m.type_expr.name) {
uses_stream = true;
}
if crate::refinement::is_untrusted_type(¶m.type_expr.name) {
uses_untrusted = true;
}
}
if let Some(ref rt) = flow.return_type {
if crate::stream_effect::is_stream_type(&rt.name) {
uses_stream = true;
}
}
if !uses_stream && !uses_untrusted {
return;
}
let mut tool_effects: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
self.collect_tool_effects(&self.program.declarations, &mut tool_effects);
let mut observed_backpressure = false;
let mut observed_trust_proof = false;
self.walk_flow_steps_for_effects(
&flow.body,
&tool_effects,
&mut observed_backpressure,
&mut observed_trust_proof,
);
if uses_stream && !observed_backpressure {
self.emit(
format!(
"Flow '{}' uses 'Stream<T>' in its signature but no \
reachable tool declares a 'stream:<policy>' effect. \
Every Stream<T> needs a backpressure policy: {}. \
Declare the policy on the tool that produces or \
consumes the stream (e.g. `effects: [stream:drop_oldest]`).",
flow.name,
valid_list(crate::stream_effect::BACKPRESSURE_CATALOG)
),
&flow.loc,
);
}
if uses_untrusted && !observed_trust_proof {
self.emit(
format!(
"Flow '{}' accepts 'Untrusted<T>' in its signature but \
no reachable tool declares a 'trust:<proof>' effect. \
Untrusted payloads MUST be refined via one of the \
catalogue verifiers: {}. Add the appropriate effect \
to the verifier tool (e.g. `effects: [trust:hmac]`).",
flow.name,
valid_list(crate::refinement::TRUST_CATALOG)
),
&flow.loc,
);
}
}
fn collect_tool_effects(
&self,
decls: &[Declaration],
out: &mut std::collections::HashMap<String, Vec<String>>,
) {
for d in decls {
match d {
Declaration::Tool(t) => {
if let Some(ref eff) = t.effects {
out.insert(t.name.clone(), eff.effects.clone());
}
}
Declaration::Epistemic(eb) => {
self.collect_tool_effects(&eb.body, out);
}
_ => {}
}
}
}
fn walk_flow_steps_for_effects(
&self,
steps: &[FlowStep],
tool_effects: &std::collections::HashMap<String, Vec<String>>,
observed_backpressure: &mut bool,
observed_trust_proof: &mut bool,
) {
for step in steps {
match step {
FlowStep::Step(s) => {
for tool_ref in [&s.apply_ref, &s.navigate_ref] {
if tool_ref.is_empty() {
continue;
}
if let Some(effs) = tool_effects.get(tool_ref) {
for e in effs {
let (base, qual) = match e.split_once(':') {
Some((b, q)) => (b, Some(q)),
None => (e.as_str(), None),
};
if base == "stream" {
if let Some(q) = qual {
if is_valid(q, crate::stream_effect::BACKPRESSURE_CATALOG) {
*observed_backpressure = true;
}
}
}
if base == "trust" {
if let Some(q) = qual {
if is_valid(q, crate::refinement::TRUST_CATALOG) {
*observed_trust_proof = true;
}
}
}
}
}
}
}
FlowStep::If(c) => {
self.walk_flow_steps_for_effects(
&c.then_body,
tool_effects,
observed_backpressure,
observed_trust_proof,
);
self.walk_flow_steps_for_effects(
&c.else_body,
tool_effects,
observed_backpressure,
observed_trust_proof,
);
}
FlowStep::ForIn(f) => {
self.walk_flow_steps_for_effects(
&f.body,
tool_effects,
observed_backpressure,
observed_trust_proof,
);
}
_ => {}
}
}
}
fn check_intent(&mut self, node: &IntentNode) {
if node.ask.is_empty() {
self.emit(
format!(
"Intent '{}' is missing required 'ask' field — every intent must express a question",
node.name
),
&node.loc,
);
}
if let Some(v) = node.confidence_floor {
self.check_range(v, 0.0, 1.0, "confidence_floor", &node.loc);
}
}
fn check_run(&mut self, node: &RunStatement) {
if !node.flow_name.is_empty() {
match self.symbols.lookup(&node.flow_name) {
None => self.emit(
format!("Undefined flow '{}' in run statement", node.flow_name),
&node.loc,
),
Some(sym) if sym.kind != "flow" => self.emit(
format!(
"'{}' is a {}, not a flow — only flows can be run",
node.flow_name, sym.kind
),
&node.loc,
),
_ => {}
}
}
if !node.persona.is_empty() {
match self.symbols.lookup(&node.persona) {
None => self.emit(format!("Undefined persona '{}'", node.persona), &node.loc),
Some(sym) if sym.kind != "persona" => self.emit(
format!("'{}' is a {}, not a persona", node.persona, sym.kind),
&node.loc,
),
_ => {}
}
}
if !node.context.is_empty() {
match self.symbols.lookup(&node.context) {
None => self.emit(format!("Undefined context '{}'", node.context), &node.loc),
Some(sym) if sym.kind != "context" => self.emit(
format!("'{}' is a {}, not a context", node.context, sym.kind),
&node.loc,
),
_ => {}
}
}
for anchor_name in &node.anchors {
match self.symbols.lookup(anchor_name) {
None => self.emit(format!("Undefined anchor '{}'", anchor_name), &node.loc),
Some(sym) if sym.kind != "anchor" => self.emit(
format!("'{}' is a {}, not an anchor", anchor_name, sym.kind),
&node.loc,
),
_ => {}
}
}
if !node.effort.is_empty() && !is_valid(&node.effort, VALID_EFFORT_LEVELS) {
self.emit(
format!(
"Unknown effort level '{}'. Valid: {}",
node.effort,
valid_list(VALID_EFFORT_LEVELS)
),
&node.loc,
);
}
}
fn check_lambda_data(&mut self, node: &LambdaDataDefinition) {
if node.ontology.is_empty() {
self.emit(
format!(
"lambda '{}' requires an 'ontology' field \
(Ontological Rigidity: O must classify the data domain)",
node.name
),
&node.loc,
);
}
if node.certainty < 0.0 || node.certainty > 1.0 {
self.emit(
format!(
"certainty coefficient must be in [0, 1], got {} \
(lambda '{}', Epistemic Bounding)",
node.certainty, node.name
),
&node.loc,
);
}
if !node.derivation.is_empty() && !is_valid(&node.derivation, VALID_DERIVATIONS) {
self.emit(
format!(
"Unknown derivation '{}' for lambda '{}'. Valid: {}",
node.derivation,
node.name,
valid_list(VALID_DERIVATIONS)
),
&node.loc,
);
}
if node.certainty == 1.0 && !node.derivation.is_empty() && node.derivation != "raw" {
self.emit(
format!(
"Epistemic Degradation Theorem violation: lambda '{}' \
has certainty=1.0 with derivation='{}'. \
Only 'raw' data may carry absolute certainty (c=1.0). \
Derived/inferred/aggregated data must have c < 1.0 \
(\u{2200}\u{039b}D\u{2081}\u{2218}\u{039b}D\u{2082}: c_composed \u{2264} min(c\u{2081}, c\u{2082}))",
node.name, node.derivation
),
&node.loc,
);
}
}
fn check_agent(&mut self, node: &AgentDefinition) {
if node.goal.is_empty() {
self.emit(
format!("Agent '{}' requires a 'goal' field (BDI: every agent must declare a desired objective)", node.name),
&node.loc,
);
}
for tool_name in &node.tools {
match self.symbols.lookup(tool_name) {
None => self.emit(
format!("Undefined tool '{}' in agent '{}'", tool_name, node.name),
&node.loc,
),
Some(sym) if sym.kind != "tool" => self.emit(
format!(
"'{}' is a {}, not a tool (referenced in agent '{}')",
tool_name, sym.kind, node.name
),
&node.loc,
),
_ => {}
}
}
if !node.strategy.is_empty() && !is_valid(&node.strategy, VALID_AGENT_STRATEGIES) {
self.emit(
format!(
"Unknown strategy '{}' in agent '{}'. Valid: {}",
node.strategy,
node.name,
valid_list(VALID_AGENT_STRATEGIES)
),
&node.loc,
);
}
if !node.on_stuck.is_empty() && !is_valid(&node.on_stuck, VALID_ON_STUCK_POLICIES) {
self.emit(
format!(
"Unknown on_stuck policy '{}' in agent '{}'. Valid: {}",
node.on_stuck,
node.name,
valid_list(VALID_ON_STUCK_POLICIES)
),
&node.loc,
);
}
if !node.memory_ref.is_empty() {
match self.symbols.lookup(&node.memory_ref) {
None => self.emit(
format!(
"Undefined memory '{}' in agent '{}'",
node.memory_ref, node.name
),
&node.loc,
),
Some(sym) if sym.kind != "memory" => self.emit(
format!(
"'{}' is a {}, not a memory (referenced in agent '{}')",
node.memory_ref, sym.kind, node.name
),
&node.loc,
),
_ => {}
}
}
if !node.shield_ref.is_empty() {
match self.symbols.lookup(&node.shield_ref) {
None => self.emit(
format!(
"Undefined shield '{}' in agent '{}'",
node.shield_ref, node.name
),
&node.loc,
),
Some(sym) if sym.kind != "shield" => self.emit(
format!(
"'{}' is a {}, not a shield (referenced in agent '{}')",
node.shield_ref, sym.kind, node.name
),
&node.loc,
),
_ => {}
}
}
if let Some(v) = node.max_iterations {
if v < 1 {
self.emit(
format!(
"max_iterations must be >= 1, got {} in agent '{}'",
v, node.name
),
&node.loc,
);
}
}
if let Some(v) = node.max_tokens {
if v < 0 {
self.emit(
format!(
"max_tokens must be >= 0, got {} in agent '{}'",
v, node.name
),
&node.loc,
);
}
}
if let Some(v) = node.max_cost {
if v < 0.0 {
self.emit(
format!("max_cost must be >= 0, got {} in agent '{}'", v, node.name),
&node.loc,
);
}
}
}
fn check_window(&mut self, node: &WindowDefinition) {
let tz = node.timezone.trim();
let tz_ok =
tz == "UTC" || (tz.contains('/') && !tz.starts_with('/') && !tz.ends_with('/'));
if !tz_ok {
self.emit(
format!(
"axon-T820 window '{}' has an invalid timezone '{}' — expected an IANA \
name like \"America/Bogota\" or \"UTC\"",
node.name, node.timezone
),
&node.loc,
);
}
if node.allow.is_empty() {
self.emit(
format!(
"axon-T821 window '{}' has an empty `allow:` — declare at least one \
{{ days hours }} span",
node.name
),
&node.loc,
);
}
for span in &node.allow {
if !is_valid(&span.day_start, VALID_WEEKDAYS)
|| !is_valid(&span.day_end, VALID_WEEKDAYS)
{
self.emit(
format!(
"axon-T822 window '{}' has an invalid day in `days: {}..{}` — valid: {}",
node.name,
span.day_start,
span.day_end,
valid_list(VALID_WEEKDAYS)
),
&span.loc,
);
}
if !(0..=23).contains(&span.hour_start) || !(0..=23).contains(&span.hour_end) {
self.emit(
format!(
"axon-T823 window '{}' has an out-of-range hour in `hours: {}..{}` — \
hours are 0..23",
node.name, span.hour_start, span.hour_end
),
&span.loc,
);
}
}
if !node.on_outside.is_empty() && !is_valid(&node.on_outside, VALID_ON_OUTSIDE) {
self.emit(
format!(
"axon-T824 window '{}' has an unknown on_outside policy '{}' — valid: {}",
node.name,
node.on_outside,
valid_list(VALID_ON_OUTSIDE)
),
&node.loc,
);
}
for date in &node.exclude {
if !is_valid_iso_date(date) {
self.emit(
format!(
"axon-T826 window '{}' has an invalid exclude date \"{}\" — expected a \
real ISO calendar date \"YYYY-MM-DD\" (e.g. \"2026-12-25\")",
node.name, date
),
&node.loc,
);
}
}
}
fn check_budget(&mut self, node: &BudgetBlock, daemon_name: &str) {
if node.quotas.is_empty() {
self.emit(
format!(
"axon-T834 daemon '{daemon_name}' has an empty `budget {{ }}` — declare at \
least one `rate:`/`max:` quota"
),
&node.loc,
);
}
for quota in &node.quotas {
match self.symbols.lookup("a.effect) {
None => self.emit(
format!(
"axon-T830 daemon '{daemon_name}' budget targets undefined tool \
'{}' in `on Tool({})` — must name a declared `tool`",
quota.effect, quota.effect
),
"a.loc,
),
Some(sym) if sym.kind != "tool" => self.emit(
format!(
"axon-T830 daemon '{daemon_name}' budget targets '{}', which is a {}, \
not a tool",
quota.effect, sym.kind
),
"a.loc,
),
_ => {}
}
if quota.limit <= 0 {
self.emit(
format!(
"axon-T831 daemon '{daemon_name}' budget quota on '{}' has a non-positive \
limit {} — a `{}` allowance must be > 0",
quota.effect, quota.limit, quota.kind
),
"a.loc,
);
}
if !is_valid("a.period, VALID_BUDGET_PERIODS) {
self.emit(
format!(
"axon-T832 daemon '{daemon_name}' budget quota on '{}' has an unknown \
period '{}' — valid: {}",
quota.effect,
quota.period,
valid_list(VALID_BUDGET_PERIODS)
),
"a.loc,
);
}
}
if !node.on_exhausted.is_empty() && !is_valid(&node.on_exhausted, VALID_ON_EXHAUSTED) {
self.emit(
format!(
"axon-T833 daemon '{daemon_name}' has an unknown `on_exhausted` policy '{}' — \
valid: {}",
node.on_exhausted,
valid_list(VALID_ON_EXHAUSTED)
),
&node.loc,
);
}
}
fn find_anchor(&self, name: &str) -> Option<&'a AnchorConstraint> {
self.program.declarations.iter().find_map(|d| match d {
Declaration::Anchor(a) if a.name == name => Some(a),
_ => None,
})
}
fn check_forge(&mut self, node: &ForgeBlock, flow_name: &str) {
if node.seed.trim().is_empty() {
self.emit(
format!(
"axon-T872 forge '{}' in flow '{}' has an empty `seed:` — a creative \
synthesis needs a conceptual starting point",
node.name, flow_name
),
&node.loc,
);
}
if node.output_type.trim().is_empty() {
self.emit(
format!(
"axon-T872 forge '{}' in flow '{}' has no `-> <Type>` return type",
node.name, flow_name
),
&node.loc,
);
}
if !node.mode.is_empty() && !is_valid(&node.mode, VALID_FORGE_MODES) {
self.emit(
format!(
"axon-T868 forge '{}' has unknown creativity mode '{}'. Valid: {}",
node.name,
node.mode,
valid_list(VALID_FORGE_MODES)
),
&node.loc,
);
}
if node.novelty < 0.0 || node.novelty > 1.0 {
self.emit(
format!(
"axon-T869 forge '{}' novelty {} is outside [0.0, 1.0]",
node.name, node.novelty
),
&node.loc,
);
}
if node.depth < 1 {
self.emit(
format!(
"axon-T870 forge '{}' depth {} must be ≥ 1 (at least one incubation iteration)",
node.name, node.depth
),
&node.loc,
);
}
if node.branches < 1 {
self.emit(
format!(
"axon-T870 forge '{}' branches {} must be ≥ 1 (at least one illumination branch)",
node.name, node.branches
),
&node.loc,
);
}
if !node.constraints_ref.is_empty() {
match self.symbols.lookup(&node.constraints_ref) {
None => self.emit(
format!(
"axon-T871 forge '{}' `constraints:` references undefined anchor '{}'",
node.name, node.constraints_ref
),
&node.loc,
),
Some(sym) if sym.kind != "anchor" => self.emit(
format!(
"axon-T871 '{}' is a {}, not an anchor (in forge '{}' `constraints:`)",
node.constraints_ref, sym.kind, node.name
),
&node.loc,
),
_ => {
if let Some(anchor) = self.find_anchor(&node.constraints_ref) {
if anchor.confidence_floor.is_none() {
self.emit(
format!(
"axon-T871 forge '{}' constrains on anchor '{}' which declares \
no `confidence_floor:` — the verification phase has no \
coherence gate to check against; add a `confidence_floor:` to \
'{}'",
node.name, node.constraints_ref, node.constraints_ref
),
&node.loc,
);
}
}
}
}
}
}
fn check_cache(&mut self, node: &CacheDefinition) {
if !node.backend.is_empty() && !is_valid(&node.backend, VALID_CACHE_BACKENDS) {
self.emit(
format!(
"axon-T866 unknown cache backend '{}' in cache '{}'. Valid: {}",
node.backend,
node.name,
valid_list(VALID_CACHE_BACKENDS)
),
&node.loc,
);
}
for eff in &node.apply_to_effects {
let base = eff.split_once(':').map(|(b, _)| b).unwrap_or(eff.as_str());
if !is_valid(base, VALID_EFFECTS) {
self.emit(
format!(
"axon-T867 unknown effect '{}' in cache '{}' `apply_to_effects:`. Valid: {}",
eff,
node.name,
valid_list(VALID_EFFECTS)
),
&node.loc,
);
}
}
if !cache_effects_are_pure_only(&node.apply_to_effects) && node.ttl.is_none() {
self.emit(
format!(
"axon-T865 cache '{}' widens `apply_to_effects:` beyond [pure] but declares no \
`ttl:` — a non-deterministic result may not be cached forever; add a finite \
`ttl:` (e.g. `ttl: 30s`) bounding how stale a served result may be",
node.name
),
&node.loc,
);
}
for ch in &node.invalidate_on {
match self.symbols.lookup(ch) {
None => self.emit(
format!(
"axon-T864 cache '{}' `invalidate_on:` references undefined channel '{}'",
node.name, ch
),
&node.loc,
),
Some(sym) if sym.kind != "channel" => self.emit(
format!(
"axon-T864 '{}' is a {}, not a channel (in cache '{}' `invalidate_on:`)",
ch, sym.kind, node.name
),
&node.loc,
),
_ => {}
}
}
}
fn check_credential(&mut self, node: &crate::ast::CredentialDefinition) {
if node.grants.is_empty() {
self.emit(
format!(
"axon-T893 credential '{}' declares no `grants:` — a credential that \
grants nothing can never authorize anything. Declare at least one \
capability slug (e.g. `grants: [chat.invoke]`).",
node.name
),
&node.loc,
);
}
const MAX_CREDENTIAL_TTL_SECS: u64 = 86_400; match crate::duration_literal_to_secs(&node.ttl) {
None => self.emit(
format!(
"axon-T894 credential '{}' has an invalid `ttl:` '{}' — expected a \
duration literal like `15m`, `900s`, `1h` (required field).",
node.name, node.ttl
),
&node.loc,
),
Some(0) => self.emit(
format!(
"axon-T894 credential '{}' has a zero-length `ttl:` '{}' — a bearer \
that is born expired can never authorize anything.",
node.name, node.ttl
),
&node.loc,
),
Some(secs) if secs > MAX_CREDENTIAL_TTL_SECS => self.emit(
format!(
"axon-T894 credential '{}' declares `ttl: {}` ({secs}s), above the \
24h ephemeral ceiling — a long-lived machine identity is the §81 \
service-account surface, not an ephemeral credential.",
node.name, node.ttl
),
&node.loc,
),
Some(_) => {}
}
}
fn check_cors(&mut self, node: &CorsDefinition) {
for origin in &node.allow_origins {
if !is_valid_origin_glob(origin) {
self.emit(
format!(
"axon-T854 invalid origin glob '{}' in cors '{}' — must be an exact \
origin or a single leading wildcard host label (e.g. \
\"https://*.kivi.io\"), not a full pattern",
origin, node.name
),
&node.loc,
);
}
}
let any_origin = node.allow_origins.iter().any(|o| o == "*");
if any_origin && node.allow_credentials {
self.emit(
format!(
"axon-T853 cors '{}' combines an any-origin `allow_origins: [\"*\"]` with \
`allow_credentials: true` — the CORS specification forbids this pairing \
(a browser silently REJECTS the credentialed response); narrow \
`allow_origins` to explicit origins or drop `allow_credentials`",
node.name
),
&node.loc,
);
}
for method in &node.allow_methods {
let upper = method.to_uppercase();
if !is_valid(&upper, crate::parser::AXONENDPOINT_METHOD_VALUES) {
self.emit(
format!(
"axon-T855 unknown method '{}' in cors '{}'. Valid: {}",
method,
node.name,
valid_list(crate::parser::AXONENDPOINT_METHOD_VALUES)
),
&node.loc,
);
}
}
}
fn check_cors_cross_method_consistency(&mut self, decls: &[Declaration]) {
let mut seen: std::collections::HashMap<String, (String, String)> =
std::collections::HashMap::new();
for decl in decls {
let Declaration::AxonEndpoint(ep) = decl else {
continue;
};
if ep.path.is_empty() {
continue;
}
match seen.get(&ep.path) {
None => {
seen.insert(ep.path.clone(), (ep.cors_ref.clone(), ep.name.clone()));
}
Some((first_cors_ref, first_name)) if first_cors_ref != &ep.cors_ref => {
fn describe(r: &str) -> &str {
if r.is_empty() {
"<none>"
} else {
r
}
}
self.emit(
format!(
"axon-T857 axonendpoint '{}' and '{}' share path '{}' but declare \
different `cors:` references ('{}' vs '{}') — a browser's \
preflight is per-path, not per-method; every axonendpoint on the \
same path must reference the SAME cors declaration (or all leave \
it unset)",
first_name,
ep.name,
ep.path,
describe(first_cors_ref),
describe(&ep.cors_ref),
),
&ep.loc,
);
}
Some(_) => {}
}
}
}
fn check_document(&mut self, node: &crate::ast::DocumentDefinition) {
if !is_valid(&node.target, VALID_DOC_TARGETS) {
self.emit(
format!(
"axon-T910 document '{}' has `target: {}` — a document targets one of: {}.",
node.name,
if node.target.is_empty() { "<unset>" } else { &node.target },
valid_list(VALID_DOC_TARGETS)
),
&node.loc,
);
}
if !node.provenance.is_empty() && !is_valid(&node.provenance, VALID_DOC_PROVENANCE) {
self.emit(
format!(
"axon-T911 document '{}' has `provenance: {}` — valid: {}.",
node.name,
node.provenance,
valid_list(VALID_DOC_PROVENANCE)
),
&node.loc,
);
}
if node.blocks.is_empty() {
self.emit(
format!(
"axon-T912 document '{}' has an empty body — declare at least one body block \
({} for `target: {}`).",
node.name,
doc_top_level_kinds(&node.target).join(" / "),
node.target
),
&node.loc,
);
}
let bases: std::collections::HashSet<String> = node
.effects
.as_ref()
.map(|e| {
e.effects
.iter()
.map(|s| s.split(':').next().unwrap_or(s).to_string())
.collect()
})
.unwrap_or_default();
if bases.contains("sensitive") && !bases.contains("legal") {
self.emit(
format!(
"axon-T913 document '{}' binds `sensitive:*` data but its `effects:` carries \
no `legal:<basis>` — a document is an egress boundary (D99.4); a sensitive \
value leaving the lattice into a human artifact needs a declared legal basis.",
node.name
),
&node.loc,
);
}
let epistemic_ok = matches!(self.current_epistemic_mode.as_str(), "believe" | "know");
for block in &node.blocks {
self.check_doc_block(node, block, &node.target, "", epistemic_ok);
}
}
fn dataspace_columns(&self, name: &str) -> Option<Vec<(String, String)>> {
self.program.declarations.iter().find_map(|d| match d {
Declaration::Dataspace(n) if n.name == name => Some(
n.columns
.iter()
.map(|c| (c.name.clone(), c.declared_type.clone()))
.collect(),
),
_ => None,
})
}
fn dataspace_target_error(&self, verb: &str, target: &str) -> Option<String> {
if target.is_empty() {
return Some(format!(
"axon-T930 `{verb}` names no dataspace — the data-plane form is \
`{verb} <Dataspace> …` (§108.d; these verbs are relational \
operations, not prompts)."
));
}
if self.dataspace_columns(target).is_some() {
return None;
}
let kind = self
.symbols
.lookup(target)
.map(|s| s.kind.clone())
.unwrap_or_else(|| "undeclared".to_string());
Some(format!(
"axon-T930 `{verb}` targets `{target}`, which is {} — a query verb \
reads a declared `dataspace`.",
if kind == "undeclared" {
"not declared".to_string()
} else {
format!("a {kind}")
}
))
}
fn check_dataspace(&mut self, node: &crate::ast::DataspaceDefinition) {
use crate::ast::DataspaceColumnType;
if node.columns.is_empty() {
self.emit(
format!(
"axon-T928 dataspace '{}' declares no columns. A dataspace IS its \
schema — the columnar engine materializes one typed buffer per \
declared column, and a dataspace with none can never be ingested \
into or queried. Declare at least one: `column <name>: <Type>` \
over {{Text, Int, Float, Bool, Timestamp, Json}}.",
node.name
),
&node.loc,
);
}
let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
for col in &node.columns {
if !seen.insert(col.name.as_str()) {
self.emit(
format!(
"axon-T928 dataspace '{}' declares column `{}` more than once — \
one column name maps to exactly one physical buffer.",
node.name, col.name
),
&col.loc,
);
}
}
for col in &node.columns {
if DataspaceColumnType::from_token(&col.declared_type).is_none() {
let names = DataspaceColumnType::all_canonical_names();
let suggestion = crate::smart_suggest::suggest_for(&col.declared_type, &names);
let suggest_suffix = if suggestion.is_empty() {
String::new()
} else {
format!(" {suggestion}")
};
self.emit(
format!(
"axon-T928 dataspace '{}' column `{}` has unknown type `{}`. The \
closed dataspace column-type catalog (D108.1) is {{{}}} — each \
type maps 1:1 to a physical columnar buffer layout, so the \
catalog admits no open extension.{}",
node.name,
col.name,
col.declared_type,
names.join(", "),
suggest_suffix
),
&col.loc,
);
}
}
}
fn check_notify(&mut self, node: &crate::ast::NotifyDefinition) {
if !matches!(node.channel.as_str(), "sms" | "whatsapp" | "telegram") {
self.emit(
format!(
"axon-T934 notify '{}' has `channel: {}` — the closed v1 channel catalog is {{sms, whatsapp, telegram}} (D110.5; further channels are additive §110.x surface).",
node.name,
if node.channel.is_empty() { "<unset>" } else { &node.channel }
),
&node.loc,
);
}
if !node.to_is_secret {
self.emit(
format!(
"axon-T934 notify '{}' has a literal recipient (`to: {}`). A recipient is PII and never rides source, IR, prompts or audit — declare it under §94 custody and reference the class: `to: secret(ops.oncall_phone)`. The value resolves at dispatch, tenant-scoped.",
node.name,
if node.to_secret.is_empty() { "<unset>" } else { &node.to_secret }
),
&node.loc,
);
}
if node.template.is_empty() {
self.emit(
format!("axon-T934 notify '{}' has no `template:` — a notification with nothing to say is a declaration error.", node.name),
&node.loc,
);
}
let has_web = node
.effects
.as_ref()
.map(|e| e.effects.iter().any(|x| x == "web" || x.starts_with("web:")))
.unwrap_or(false);
if !has_web {
self.emit(
format!("axon-T934 notify '{}' declares no `web` effect — a notification crosses the trust boundary over the network; declare `effects: <web>`.", node.name),
&node.loc,
);
}
let window_ok = {
let w = node.window.trim();
!w.is_empty()
&& w.len() >= 2
&& w[..w.len() - 1].chars().all(|c| c.is_ascii_digit())
&& matches!(w.chars().last(), Some('s') | Some('m') | Some('h') | Some('d'))
&& w[..w.len() - 1].parse::<u64>().map(|n| n > 0).unwrap_or(false)
};
if !window_ok {
self.emit(
format!(
"axon-T935 notify '{}' has `window: {}` — a window is MANDATORY (an unbounded interruption channel is a bug, not a default) and must be a positive duration (`30m`, `4h`, `1d`). At-most-once-per-window per recipient, enforced durably; suppressions are witnessed.",
node.name,
if node.window.is_empty() { "<unset>" } else { &node.window }
),
&node.loc,
);
}
let binds_flow_values = node.template.contains("${");
let cleared = node.provenance == "cleared";
if !node.provenance.is_empty() && !matches!(node.provenance.as_str(), "attached" | "cleared") {
self.emit(
format!("axon-T934 notify '{}' has `provenance: {}` — one of: attached (default), cleared.", node.name, node.provenance),
&node.loc,
);
}
let epistemic_ok = matches!(self.current_epistemic_mode.as_str(), "believe" | "know");
if cleared && binds_flow_values && !epistemic_ok {
self.emit(
format!(
"axon-T933 notify '{}' is `provenance: cleared` and binds flow values into a HUMAN notification. A guess reaching a person's pocket labeled as fact is assertion-laundering at its fastest — the human acts immediately (D110.2, the T920 barrier's human-egress sibling). Use `provenance: attached` (each bound value arrives with its epistemic label; a §108 envelope ref appends its evidence line), or vouch the values inside `epistemic {{ believe|know }}`.",
node.name
),
&node.loc,
);
}
}
fn check_deliver(&mut self, node: &crate::ast::DeliverDefinition) {
if !is_valid(&node.target, VALID_DELIVER_TARGETS) {
self.emit(
format!(
"axon-T921 deliver '{}' has `target: {}` — a delivery targets one of: {}.",
node.name,
if node.target.is_empty() { "<unset>" } else { &node.target },
valid_list(VALID_DELIVER_TARGETS)
),
&node.loc,
);
}
if !node.provenance.is_empty() && !is_valid(&node.provenance, VALID_DELIVER_PROVENANCE) {
self.emit(
format!(
"axon-T922 deliver '{}' has `provenance: {}` — valid: {} (empty ⇒ `attached`).",
node.name,
node.provenance,
valid_list(VALID_DELIVER_PROVENANCE)
),
&node.loc,
);
}
if node.secret.trim().is_empty() {
self.emit(
format!(
"axon-T923 deliver '{}' has no `secret:` — a CRM write must authenticate; \
name the per-tenant credential key (resolved via §94 custody at dispatch, \
never revealed to cognition).",
node.name
),
&node.loc,
);
}
let bases: std::collections::HashSet<String> = node
.effects
.as_ref()
.map(|e| {
e.effects
.iter()
.map(|s| s.split(':').next().unwrap_or(s).to_string())
.collect()
})
.unwrap_or_default();
if !bases.contains("web") {
self.emit(
format!(
"axon-T924 deliver '{}' does not declare the `web` effect — a delivery writes \
across the network trust boundary. Add `effects: <web>` (plus any \
`sensitive:<cat>`/`legal:<basis>` the delivered data carries).",
node.name
),
&node.loc,
);
}
if bases.contains("sensitive") && !bases.contains("legal") {
self.emit(
format!(
"axon-T924 deliver '{}' binds `sensitive:*` data but its `effects:` carries no \
`legal:<basis>` — delivering PII into a system of record is further \
processing (D105.6); declare the legal basis.",
node.name
),
&node.loc,
);
}
if node.ops.is_empty() {
self.emit(
format!(
"axon-T925 deliver '{}' has an empty body — declare at least one operation \
({}).",
node.name,
valid_list(VALID_DELIVER_OPS)
),
&node.loc,
);
}
let epistemic_vouched =
matches!(self.current_epistemic_mode.as_str(), "believe" | "know");
let cleared = node.provenance == "cleared";
let binds_flow_value = node
.ops
.iter()
.any(|op| op.ref_fields().next().is_some());
if cleared && binds_flow_value && !epistemic_vouched {
self.emit(
format!(
"axon-T920 deliver '{}' is `provenance: cleared` and binds a flow value into a \
CRM with no provenance. A value leaving the epistemic lattice into a system of \
record cannot be more confident than the reasoning that produced it (D105.2, \
the provenance-stripping barrier — the egress-dual of the §99 assertion-\
laundering barrier). Use `provenance: attached` (the default — each field \
lands with its level/confidence/source, a guess labeled as a guess), or, if \
you vouch the delivered values are verified facts, wrap the delivery in \
`epistemic {{ mode: believe }}` (after a `shield` + `anchor` cleared them).",
node.name
),
&node.loc,
);
}
for op in &node.ops {
if !is_valid(&op.kind, VALID_DELIVER_OPS) {
self.emit(
format!(
"axon-T925 deliver '{}' — operation `{}` is not valid for `target: {}`. \
Valid: {}.",
node.name,
op.kind,
if node.target.is_empty() { "crm" } else { &node.target },
valid_list(VALID_DELIVER_OPS)
),
&op.loc,
);
}
if !op.has_field("key") {
self.emit(
format!(
"axon-T926 deliver '{}' — operation `{}` has no `key:` — every delivery \
operation requires an idempotency key (a natural key like the contact \
email, or an adopter `external_id`) so an at-least-once retry never \
double-creates a record (D105.5).",
node.name, op.kind
),
&op.loc,
);
}
}
}
fn check_doc_block(
&mut self,
doc: &crate::ast::DocumentDefinition,
block: &crate::ast::DocBlock,
target: &str,
parent: &str,
epistemic_ok: bool,
) {
let allowed = doc_allowed_child_kinds(target, parent);
if !allowed.contains(&block.kind.as_str()) {
let where_ = if parent.is_empty() {
format!("at the top level of a `target: {target}` document")
} else {
format!("inside a `{parent}` block (`target: {target}`)")
};
self.emit(
format!(
"axon-T912 document '{}' — block `{}` is not valid {where_}. Valid here: {}.",
doc.name,
block.kind,
if allowed.is_empty() {
"(none — this block takes no children)".to_string()
} else {
allowed.join(" / ")
}
),
&block.loc,
);
}
let allowed_fields = doc_allowed_fields(&block.kind);
for (fname, _) in &block.fields {
if !allowed_fields.contains(&fname.as_str()) {
self.emit(
format!(
"axon-T914 document '{}' — `{}` is not a valid field of a `{}` block. \
Valid: {}.",
doc.name,
fname,
block.kind,
if allowed_fields.is_empty() {
"(none)".to_string()
} else {
allowed_fields.join(" / ")
}
),
&block.loc,
);
}
}
self.check_doc_block_laws(doc, block);
if let Some(slot) = doc_assertive_slot(&block.kind) {
if let Some(value) = block.field(slot) {
if let crate::ast::DocScalar::Ref(name) = value {
let attributed = block.has_field("attribute");
if !attributed && !epistemic_ok {
self.emit(
format!(
"axon-T916 document '{}' — the `{}` block binds flow value `{}` in \
its assertive `{}:` slot with no provenance. A value leaving the \
epistemic lattice into a human artifact cannot be more confident \
than the reasoning that produced it (D99.1, the assertion-\
laundering barrier). Add `attribute: <source>` (renders as a \
visible source note), wrap the document in `epistemic {{ mode: \
believe }}` if you vouch it is ≥ believe, or pass `{}` through a \
`shield` scanning `hallucination`/`pii_leak` first.",
doc.name, block.kind, name, slot, name
),
&block.loc,
);
}
}
}
}
for child in &block.children {
self.check_doc_block(doc, child, target, &block.kind, epistemic_ok);
}
}
fn check_doc_block_laws(
&mut self,
doc: &crate::ast::DocumentDefinition,
block: &crate::ast::DocBlock,
) {
use crate::ast::DocScalar;
let require = |this: &mut Self, field: &str, code: &str, why: &str| {
if !block.has_field(field) {
this.emit(
format!(
"axon-{code} document '{}' — a `{}` block requires `{field}:` ({why}).",
doc.name, block.kind
),
&block.loc,
);
}
};
match block.kind.as_str() {
"chart" => {
if let Some(DocScalar::Ref(k)) = block.field("kind") {
if !is_valid(k, VALID_CHART_KINDS) {
self.emit(
format!(
"axon-T917 document '{}' — chart `kind: {}` is outside the bounded \
v1 subset {}. SmartArt / pivots / 3-D are deferred (D99.9).",
doc.name,
k,
valid_list(VALID_CHART_KINDS)
),
&block.loc,
);
}
}
require(self, "kind", "T917", "the chart type");
require(self, "series", "T918", "the data series");
}
"table" => {
match block.field("columns") {
Some(DocScalar::List(cols)) if !cols.is_empty() => {}
_ => self.emit(
format!(
"axon-T915 document '{}' — a `table` block requires a non-empty \
`columns: [ … ]` spec (the row arity is checked against it).",
doc.name
),
&block.loc,
),
}
require(self, "rows", "T915", "the row data");
}
"formula" => {
require(self, "cell", "T919", "the target cell (A1 notation)");
require(self, "expr", "T919", "the formula expression");
if let Some(DocScalar::Text(cell)) = block.field("cell") {
if !is_a1_cell(cell) {
self.emit(
format!(
"axon-T919 document '{}' — formula `cell: \"{}\"` is not a valid A1 \
cell reference (e.g. \"B2\").",
doc.name, cell
),
&block.loc,
);
}
}
}
"range" => {
if let Some(DocScalar::Text(r)) = block.field("cells") {
if !is_a1_range(r) {
self.emit(
format!(
"axon-T919 document '{}' — `range` cells `\"{}\"` is not a valid A1 \
range (e.g. \"B2:B9\").",
doc.name, r
),
&block.loc,
);
}
}
}
"placeholder" => require(self, "name", "T920", "the layout slot name"),
"image" => require(self, "source", "T921", "the image source binding"),
"slide" => require(self, "layout", "T920", "the slide layout"),
"sheet" => require(self, "name", "T912", "the sheet tab name"),
_ => {}
}
}
fn check_savant(&mut self, node: &SavantDefinition) {
if node.domain.trim().is_empty() {
self.emit(
format!(
"axon-T873 savant '{}' declares no `domain:` — a long-horizon research \
agent needs a bounded ontological scope; add e.g. `domain: \"…\"`",
node.name
),
&node.loc,
);
}
if node.mandates.is_empty() {
self.emit(
format!(
"axon-T874 savant '{}' declares no `mandate` — add at least one \
`mandate <Name> {{ objective: \"…\", output: <Type> }}`",
node.name
),
&node.loc,
);
}
for m in &node.mandates {
if m.objective.trim().is_empty() {
self.emit(
format!(
"axon-T874 mandate '{}' in savant '{}' has an empty `objective:` — \
state the research goal the savant autonomously decomposes",
m.name, node.name
),
&m.loc,
);
}
if m.output_type.trim().is_empty() {
self.emit(
format!(
"axon-T874 mandate '{}' in savant '{}' declares no `output:` type — \
the final report must inhabit a declared type (e.g. `output: FormalReport`)",
m.name, node.name
),
&m.loc,
);
}
}
if let Some(cog) = &node.cognition {
if !cog.depth.is_empty() && !is_valid(&cog.depth, VALID_SAVANT_DEPTHS) {
self.emit(
format!(
"axon-T876 unknown savant cognition depth '{}' in savant '{}'. Valid: {}",
cog.depth,
node.name,
valid_list(VALID_SAVANT_DEPTHS)
),
&cog.loc,
);
}
if !cog.divergence.is_empty() && !is_valid(&cog.divergence, VALID_SAVANT_DIVERGENCES) {
self.emit(
format!(
"axon-T876 unknown savant cognition divergence '{}' in savant '{}'. Valid: {}",
cog.divergence,
node.name,
valid_list(VALID_SAVANT_DIVERGENCES)
),
&cog.loc,
);
}
if let Some(threshold) = cog.entropic_threshold {
if !(threshold > 0.0) {
self.emit(
format!(
"axon-T876 savant '{}' cognition `entropic_threshold: {}` must be > 0 — \
it is the Expected-Free-Energy convergence bound; a non-positive bound \
can never be reached and the loop would never terminate",
node.name, threshold
),
&cog.loc,
);
}
}
}
if let Some(mem) = &node.memory {
if !mem.backend.is_empty() {
match self.symbols.lookup(&mem.backend) {
None => self.emit(
format!(
"axon-T875 savant '{}' `memory.backend: {}` references an undefined \
store — declare a `memory` or `corpus` primitive with that name",
node.name, mem.backend
),
&mem.loc,
),
Some(sym) if sym.kind != "memory" && sym.kind != "corpus" => self.emit(
format!(
"axon-T875 savant '{}' `memory.backend: {}` resolves to a {}, not a \
`memory`/`corpus` store",
node.name, mem.backend, sym.kind
),
&mem.loc,
),
Some(_) => {}
}
}
}
match &node.budget {
None => self.emit(
format!(
"axon-T877 savant '{}' declares no `budget {{ max_iterations: N }}` — a \
long-horizon autonomous agent MUST carry an enforced compute ceiling \
(the §72 linear-budget discipline); an unbounded loop is fail-open",
node.name
),
&node.loc,
),
Some(b) => match b.max_iterations {
None => self.emit(
format!(
"axon-T877 savant '{}' `budget` declares no `max_iterations:` — the \
FEP-loop iteration ceiling is mandatory",
node.name
),
&b.loc,
),
Some(n) if n <= 0 => self.emit(
format!(
"axon-T877 savant '{}' `budget.max_iterations: {}` must be > 0",
node.name, n
),
&b.loc,
),
Some(_) => {}
},
}
for m in &node.mandates {
if let Some(sym) = self.symbols.lookup(&m.output_type) {
if sym.kind != "type" {
self.emit(
format!(
"axon-T878 mandate '{}' in savant '{}' has `output: {}` which is a {}, \
not a type — the final report must inhabit a declared `type`",
m.name, node.name, m.output_type, sym.kind
),
&m.loc,
);
}
}
}
}
fn check_synth(&mut self, node: &SynthDefinition) {
if node.target.trim().is_empty() {
self.emit(
format!(
"axon-T879 synth '{}' declares no `target:` — state the capability scope the \
synthesised tools serve (e.g. `target: \"parse geospatial datasets\"`)",
node.name
),
&node.loc,
);
}
if node.risk.is_empty() {
self.emit(
format!(
"axon-T880 synth '{}' declares no `risk:` — classify the synthesis risk. \
Valid: {}",
node.name,
valid_list(VALID_SYNTH_RISKS)
),
&node.loc,
);
} else if !is_valid(&node.risk, VALID_SYNTH_RISKS) {
self.emit(
format!(
"axon-T880 unknown synth risk '{}' in synth '{}'. Valid: {}",
node.risk,
node.name,
valid_list(VALID_SYNTH_RISKS)
),
&node.loc,
);
}
if !node.language.is_empty() && !is_valid(&node.language, VALID_SYNTH_LANGUAGES) {
self.emit(
format!(
"axon-T881 unknown synth language '{}' in synth '{}'. Valid: {}",
node.language,
node.name,
valid_list(VALID_SYNTH_LANGUAGES)
),
&node.loc,
);
}
if node.sandbox != "wasm" {
let got = if node.sandbox.is_empty() {
"no sandbox".to_string()
} else {
format!("`{}`", node.sandbox)
};
self.emit(
format!(
"axon-T882 synth '{}' must declare `sandbox: wasm` (got {}) — synthesised code \
may only run in a zero-trust WASM sandbox; there is no unsandboxed mode",
node.name, got
),
&node.loc,
);
}
if !node.review.is_empty() && !is_valid(&node.review, VALID_SYNTH_REVIEWS) {
self.emit(
format!(
"axon-T883 unknown synth review '{}' in synth '{}'. Valid: {}",
node.review,
node.name,
valid_list(VALID_SYNTH_REVIEWS)
),
&node.loc,
);
} else if node.review == "none" && (node.risk == "high" || node.risk == "critical") {
self.emit(
format!(
"axon-T883 synth '{}' is `risk: {}` but `review: none` — high/critical-risk \
synthesis MUST carry Coder/Reviewer consensus; remove `review: none`",
node.name, node.risk
),
&node.loc,
);
}
}
fn check_scope(&mut self, node: &ScopeDefinition) {
if node.targets.is_empty() {
self.emit(
format!(
"axon-T884 scope '{}' declares an empty `targets:` allowlist — a scope must \
name the specific resources the operator authorises for analysis (an empty \
allowlist would authorise nothing safely and everything dangerously)",
node.name
),
&node.loc,
);
}
if !node.depth.is_empty() && !is_valid(&node.depth, VALID_SCOPE_DEPTHS) {
self.emit(
format!(
"axon-T885 unknown scope depth '{}' in scope '{}'. Valid (least→most \
invasive): {}",
node.depth,
node.name,
valid_list(VALID_SCOPE_DEPTHS)
),
&node.loc,
);
}
if node.approver.trim().is_empty() {
self.emit(
format!(
"axon-T886 scope '{}' declares no `approver:` — name the capability whose \
holder authorised this analysis scope (e.g. `approver: requires \
\"security.lead\"`)",
node.name
),
&node.loc,
);
}
}
fn check_warden(&mut self, node: &WardenBlock, flow_name: &str) {
match self.symbols.lookup(&node.scope_ref) {
None => self.emit(
format!(
"axon-T887 warden in flow '{flow_name}' references undefined scope '{}' — a \
`warden(…) within <Scope>` must name a declared `scope` (fail-closed: with no \
authorization scope there is no analysis)",
node.scope_ref
),
&node.loc,
),
Some(sym) if sym.kind != "scope" => self.emit(
format!(
"axon-T887 warden in flow '{flow_name}' `within {}` resolves to a {}, not a \
`scope` authorization policy",
node.scope_ref, sym.kind
),
&node.loc,
),
Some(_) => {}
}
self.check_flow_steps(&node.body, flow_name);
}
fn check_cache_module_laws(&mut self, decls: &[Declaration]) {
let defaults: Vec<&CacheDefinition> = decls
.iter()
.filter_map(|d| match d {
Declaration::Cache(c) if c.default_policy => Some(c),
_ => None,
})
.collect();
if defaults.len() > 1 {
for extra in &defaults[1..] {
self.emit(
format!(
"axon-T863 more than one `cache {{ default: true }}` in this module \
('{}' and '{}'); a module has at most one default cache policy — \
remove `default: true` from all but one",
defaults[0].name, extra.name
),
&extra.loc,
);
}
}
if defaults.len() == 1 && !cache_effects_are_pure_only(&defaults[0].apply_to_effects) {
let def = defaults[0];
let base = |e: &str| e.split_once(':').map(|(b, _)| b.to_string()).unwrap_or_else(|| e.to_string());
let apply_set: Vec<String> = if def.apply_to_effects.is_empty() {
vec!["pure".to_string()]
} else {
def.apply_to_effects.iter().map(|e| base(e)).collect()
};
for decl in decls {
let Declaration::Tool(t) = decl else { continue };
if !t.cache.is_empty() {
continue;
}
let Some(row) = &t.effects else { continue };
let covered = row.effects.iter().all(|e| apply_set.contains(&base(e)));
let has_nonpure = row.effects.iter().any(|e| base(e) != "pure");
if covered && has_nonpure {
self.warn(
format!(
"axon-W013 cache '{}' has `default: true` with `apply_to_effects:` \
widened beyond [pure]; it auto-caches tool '{}' whose effect row \
<{}> is NOT proven deterministic — a stale or incorrect result may \
be served (bounded by `ttl:`). Confirm '{}' is safe to memoize, or \
set `cache: none` on it to opt out",
def.name,
t.name,
row.effects.join(", "),
t.name
),
&t.loc,
);
}
}
}
}
fn check_shield(&mut self, node: &ShieldDefinition) {
if !node.taint.is_empty() {
self.emit(
format!(
"axon-T936 shield '{}' declares `taint: {}` — a DEAD field, retracted in §111. \
It was parsed, carried into the IR, and never read by the runtime: it bought \
you nothing. Delete it. Epistemic taint is not a shield knob — external data \
is born `Untrusted` by law (axon-T908) and degrades the lattice on contact; \
to constrain what may leave, use the shield's `scan:` / `on_breach:` / \
`redact:` gates, which do run.",
node.name, node.taint
),
&node.loc,
);
}
for cat in &node.scan {
if !is_valid(cat, VALID_SCAN_CATEGORIES) && !self.ext_scan_categories.contains(cat) {
self.emit(
format!(
"Unknown scan category '{}' in shield '{}'. Valid: {}",
cat,
node.name,
valid_list(VALID_SCAN_CATEGORIES)
),
&node.loc,
);
}
}
if !node.strategy.is_empty() && !is_valid(&node.strategy, VALID_SHIELD_STRATEGIES) {
self.emit(
format!(
"Unknown strategy '{}' in shield '{}'. Valid: {}",
node.strategy,
node.name,
valid_list(VALID_SHIELD_STRATEGIES)
),
&node.loc,
);
}
match node.on_breach.as_str() {
"deflect" if node.deflect_message.is_empty() => self.emit(
format!(
"axon-T952 shield '{}' declares `on_breach: deflect` with no \
`deflect_message:` — there is nothing to emit instead of the \
candidate. Declare the canned safe reply, or use `halt`.",
node.name
),
&node.loc,
),
"quarantine" if node.quarantine.is_empty() => self.warn(
format!(
"axon-W012 shield '{}' declares `on_breach: quarantine` with no \
`quarantine:` sink — the candidate has nowhere to route, so a \
breach HALTS (fail-closed) instead of quarantining. Name the \
sink to make the candidate recoverable.",
node.name
),
&node.loc,
),
"sanitize_and_retry" if node.redact.is_empty() => self.emit(
format!(
"axon-T952 shield '{}' declares `on_breach: sanitize_and_retry` \
with no `redact:` fields — a retry that re-scans the SAME bytes \
is not sanitization. Declare the fields to mask, or use `halt`.",
node.name
),
&node.loc,
),
_ => {}
}
if !node.on_breach.is_empty() && !is_valid(&node.on_breach, VALID_ON_BREACH_POLICIES) {
self.emit(
format!(
"Unknown on_breach policy '{}' in shield '{}'. Valid: {}",
node.on_breach,
node.name,
valid_list(VALID_ON_BREACH_POLICIES)
),
&node.loc,
);
}
if !node.severity.is_empty() && !is_valid(&node.severity, VALID_SEVERITY_LEVELS) {
self.emit(
format!(
"Unknown severity '{}' in shield '{}'. Valid: {}",
node.severity,
node.name,
valid_list(VALID_SEVERITY_LEVELS)
),
&node.loc,
);
}
if let Some(v) = node.max_retries {
if v < 0 {
self.emit(
format!(
"max_retries must be >= 0, got {} in shield '{}'",
v, node.name
),
&node.loc,
);
}
}
if let Some(v) = node.confidence_threshold {
self.check_range(v, 0.0, 1.0, "confidence_threshold", &node.loc);
}
for tool in &node.allow_tools {
if node.deny_tools.contains(tool) {
self.emit(
format!(
"Tool '{}' appears in both allow_tools and deny_tools in shield '{}'",
tool, node.name
),
&node.loc,
);
}
}
if !node.sign.is_empty() && !is_valid(&node.sign, VALID_SIGN_ALGORITHMS) {
self.emit(
format!(
"axon-T846 unknown sign algorithm '{}' in shield '{}'. Valid: {}",
node.sign,
node.name,
valid_list(VALID_SIGN_ALGORITHMS)
),
&node.loc,
);
}
for (field, floc) in &node.unknown_fields {
self.warn(
format!(
"axon-W010 unknown field '{field}' in shield '{}' — the runtime \
IGNORES fields it does not know, so this line has NO effect. \
Valid shield fields: {SHIELD_FIELD_CATALOG}.",
node.name
),
floc,
);
}
let has_enforcement = !node.scan.is_empty()
|| !node.sign.is_empty()
|| !node.redact.is_empty()
|| !node.allow_tools.is_empty()
|| !node.deny_tools.is_empty()
|| node.confidence_threshold.is_some();
if !node.on_breach.is_empty() && !has_enforcement {
self.warn(
format!(
"axon-W011 shield '{}' declares `on_breach: {}` but has no \
enforcement-bearing field (scan / sign / redact / allow_tools / \
deny_tools / confidence_threshold) — the breach policy can never \
fire. Declare what the shield enforces, or remove `on_breach:`.",
node.name, node.on_breach
),
&node.loc,
);
}
}
fn check_pix(&mut self, node: &PixDefinition) {
if node.source.is_empty() {
self.emit(
format!("Pix '{}' requires a 'source' field", node.name),
&node.loc,
);
}
if let Some(v) = node.depth {
if v < 1 || v > 8 {
self.emit(
format!(
"depth must be between 1 and 8, got {} in pix '{}'",
v, node.name
),
&node.loc,
);
}
}
if let Some(v) = node.branching {
if v < 1 || v > 10 {
self.emit(
format!(
"branching must be between 1 and 10, got {} in pix '{}'",
v, node.name
),
&node.loc,
);
}
}
}
fn check_ledger(&mut self, node: &LedgerDefinition) {
if node.source.is_empty() {
self.emit(
format!("Ledger '{}' requires a 'source' field", node.name),
&node.loc,
);
}
if let Some(v) = node.depth {
if v < 1 {
self.emit(
format!(
"depth (chain retention) must be ≥ 1, got {} in ledger '{}'",
v, node.name
),
&node.loc,
);
}
}
if let Some(v) = node.branching {
if v == 1 || v < 0 {
self.emit(
format!(
"branching (Merkle factor) must be 0 (flat) or ≥ 2, got {} in ledger '{}'",
v, node.name
),
&node.loc,
);
}
}
}
fn check_psyche(&mut self, node: &PsycheDefinition) {
if node.dimensions.is_empty() {
self.emit(
format!(
"Psyche '{}' requires at least one dimension (manifold dim ≥ 1)",
node.name
),
&node.loc,
);
}
let mut seen: Vec<String> = Vec::new();
for dim in &node.dimensions {
if seen.contains(dim) {
self.emit(
format!("Duplicate dimension '{}' in psyche '{}'", dim, node.name),
&node.loc,
);
} else {
seen.push(dim.clone());
}
}
if let Some(v) = node.manifold_noise {
if v <= 0.0 || v > 1.0 {
self.emit(
format!(
"manifold_noise must be in (0.0, 1.0], got {} in psyche '{}'",
v, node.name
),
&node.loc,
);
}
}
if let Some(v) = node.manifold_momentum {
self.check_range(v, 0.0, 1.0, "manifold_momentum", &node.loc);
}
if node.safety_constraints.is_empty() {
self.emit(
format!(
"Psyche '{}' requires at least one safety_constraint",
node.name
),
&node.loc,
);
} else if !node
.safety_constraints
.iter()
.any(|c| c == "non_diagnostic")
{
self.emit(
format!("Psyche '{}' must include 'non_diagnostic' in safety_constraints (dependent type safety §4)", node.name),
&node.loc,
);
}
if !node.inference_mode.is_empty() && !is_valid(&node.inference_mode, VALID_INFERENCE_MODES)
{
self.emit(
format!(
"Unknown inference_mode '{}' in psyche '{}'. Valid: {}",
node.inference_mode,
node.name,
valid_list(VALID_INFERENCE_MODES)
),
&node.loc,
);
}
}
fn check_corpus(&mut self, node: &CorpusDefinition) {
if node.documents.is_empty() && node.mcp_server.is_empty() && node.store_source.is_none() {
self.emit(
format!(
"Corpus '{}' requires at least one document or an mcp_server (G1: D ≠ ∅)",
node.name
),
&node.loc,
);
}
if let Some(src) = &node.store_source {
self.check_corpus_store_source(node, src);
}
for r in &node.relations {
if !is_valid(&r.etype, VALID_CORPUS_RELATIONS) {
self.emit(
format!(
"Corpus '{}': unknown relation type '{}' (closed catalog: {})",
node.name,
r.etype,
VALID_CORPUS_RELATIONS.join(", ")
),
&r.loc,
);
}
if !node.documents.contains(&r.from) {
self.emit(
format!(
"Corpus '{}': relation references undeclared document '{}' (G2: edges connect corpus members)",
node.name, r.from
),
&r.loc,
);
}
if !node.documents.contains(&r.to) {
self.emit(
format!(
"Corpus '{}': relation references undeclared document '{}' (G2: edges connect corpus members)",
node.name, r.to
),
&r.loc,
);
}
if !(r.weight > 0.0 && r.weight <= 1.0) {
self.emit(
format!(
"Corpus '{}': relation weight {} must be in (0, 1] (G4)",
node.name, r.weight
),
&r.loc,
);
}
}
if node.adaptive && node.relations.is_empty() && node.store_source.is_none() {
self.emit(
format!(
"Corpus '{}': `adaptive: true` requires `relations:` — memory deforms the graph, an edgeless corpus has nothing to learn",
node.name
),
&node.loc,
);
}
}
fn check_corpus_store_source(&mut self, node: &CorpusDefinition, src: &CorpusStoreSource) {
use crate::store_schema::{StoreColumn, StoreColumnType};
let doc_store = self.find_store(&src.doc_store);
if doc_store.is_none() {
self.emit(
format!(
"Corpus '{}': documents store '{}' is not a declared axonstore",
node.name, src.doc_store
),
&src.loc,
);
}
let edge_store = self.find_store(&src.edge_store);
if edge_store.is_none() {
self.emit(
format!(
"Corpus '{}': relations store '{}' is not a declared axonstore",
node.name, src.edge_store
),
&src.loc,
);
}
let (Some(ds), Some(es)) = (doc_store, edge_store) else {
return;
};
let (Some(dcols), Some(ecols)) = (
ds.column_schema.as_ref().and_then(|s| s.inline_columns()),
es.column_schema.as_ref().and_then(|s| s.inline_columns()),
) else {
return;
};
let col_ty = |cols: &[StoreColumn], n: &str| -> Option<StoreColumnType> {
cols.iter().find(|c| c.name == n).map(|c| c.col_type)
};
let is_text_like = |t: StoreColumnType| matches!(t, StoreColumnType::Text);
let is_numeric = |t: StoreColumnType| {
matches!(
t,
StoreColumnType::Float | StoreColumnType::Double | StoreColumnType::Numeric
)
};
let id_ty = col_ty(dcols, &src.doc_id_col);
if id_ty.is_none() {
self.emit(
format!(
"Corpus '{}': documents store '{}' has no column '{}' (the document id)",
node.name, src.doc_store, src.doc_id_col
),
&src.loc,
);
}
match col_ty(dcols, &src.doc_title_col) {
None => self.emit(
format!(
"Corpus '{}': documents store '{}' has no column '{}' (the title)",
node.name, src.doc_store, src.doc_title_col
),
&src.loc,
),
Some(t) if !is_text_like(t) => self.emit(
format!(
"Corpus '{}': title column '{}' must be text-like (got {})",
node.name, src.doc_title_col, t
),
&src.loc,
),
_ => {}
}
for (label, col) in [("from", &src.edge_from_col), ("to", &src.edge_to_col)] {
match col_ty(ecols, col) {
None => self.emit(
format!(
"Corpus '{}': relations store '{}' has no column '{}' (the {} endpoint)",
node.name, src.edge_store, col, label
),
&src.loc,
),
Some(t) => {
if let Some(idt) = id_ty {
if t != idt {
self.emit(
format!(
"Corpus '{}': edge {} column '{}' type {} must match the document id column type {} (G2: edges connect corpus members)",
node.name, label, col, t, idt
),
&src.loc,
);
}
}
}
}
}
match col_ty(ecols, &src.edge_type_col) {
None => self.emit(
format!(
"Corpus '{}': relations store '{}' has no column '{}' (the edge type)",
node.name, src.edge_store, src.edge_type_col
),
&src.loc,
),
Some(t) if !is_text_like(t) => self.emit(
format!(
"Corpus '{}': edge type column '{}' must be text-like (got {})",
node.name, src.edge_type_col, t
),
&src.loc,
),
_ => {}
}
match col_ty(ecols, &src.edge_weight_col) {
None => self.emit(
format!(
"Corpus '{}': relations store '{}' has no column '{}' (the weight)",
node.name, src.edge_store, src.edge_weight_col
),
&src.loc,
),
Some(t) if !is_numeric(t) => self.emit(
format!(
"Corpus '{}': edge weight column '{}' must be numeric (got {}); ω ∈ (0,1] is enforced at runtime",
node.name, src.edge_weight_col, t
),
&src.loc,
),
_ => {}
}
}
fn check_ots(&mut self, node: &OtsDefinition) {
if node.teleology.is_empty() {
self.emit(
format!(
"OTS '{}' requires a 'teleology' field (goal required)",
node.name
),
&node.loc,
);
}
if !node.homotopy_search.is_empty() && !is_valid(&node.homotopy_search, VALID_OTS_HOMOTOPY)
{
self.emit(
format!(
"Unknown homotopy_search '{}' in OTS '{}'. Valid: {}",
node.homotopy_search,
node.name,
valid_list(VALID_OTS_HOMOTOPY)
),
&node.loc,
);
}
}
fn check_mandate(&mut self, node: &MandateDefinition) {
if node.constraint.is_empty() {
self.emit(
format!("Mandate '{}' requires a 'constraint' field (refinement type T_M = {{x ∈ Σ* | M(x) ⊢ ⊤}})", node.name),
&node.loc,
);
}
let statics = crate::stability::MandateStatics {
kp: node.kp,
ki: node.ki,
kd: node.kd,
epsilon: node.tolerance,
max_steps: node.max_steps,
drift_bound: node.drift_bound,
lipschitz: node.lipschitz,
};
for err in statics.admissibility_errors() {
self.emit(format!("mandate '{}': {}", node.name, err), &node.loc);
}
if !node.on_violation.is_empty() && !is_valid(&node.on_violation, VALID_MANDATE_POLICIES) {
self.emit(
format!(
"Unknown on_violation '{}' in mandate '{}'. Valid: {}",
node.on_violation,
node.name,
valid_list(VALID_MANDATE_POLICIES)
),
&node.loc,
);
}
}
fn check_axonstore(&mut self, node: &AxonStoreDefinition) {
if !node.resource_ref.is_empty() && !node.connection.is_empty() {
self.emit(
format!(
"axon-T946 Axonstore '{}' declares BOTH `resource: {}` and `connection:` — \
the same fact, twice, with nothing checking they agree. That duplication is \
precisely the defect `resource:` exists to end. Keep `resource:` (it also \
carries the pool size and the sharing discipline) and delete `connection:`.",
node.name, node.resource_ref
),
&node.loc,
);
}
if !node.resource_ref.is_empty() {
match self.symbols.lookup(&node.resource_ref) {
None => self.emit(
format!(
"axon-T946 Axonstore '{}' names resource '{}', which is not declared.",
node.name, node.resource_ref
),
&node.loc,
),
Some(sym) if sym.kind != "resource" => self.emit(
format!(
"axon-T946 Axonstore '{}' names '{}', which is a {}, not a resource.",
node.name, node.resource_ref, sym.kind
),
&node.loc,
),
_ => {}
}
}
if !node.backend.is_empty() && !is_valid(&node.backend, VALID_STORE_BACKENDS) {
self.emit(
format!(
"Unknown backend '{}' in axonstore '{}'. Valid: {}",
node.backend,
node.name,
valid_list(VALID_STORE_BACKENDS)
),
&node.loc,
);
}
if !node.isolation.is_empty() && !is_valid(&node.isolation, VALID_STORE_ISOLATION) {
self.emit(
format!(
"Unknown isolation '{}' in axonstore '{}'. Valid: {}",
node.isolation,
node.name,
valid_list(VALID_STORE_ISOLATION)
),
&node.loc,
);
}
if !node.on_breach.is_empty() && !is_valid(&node.on_breach, VALID_STORE_ON_BREACH) {
self.emit(
format!(
"Unknown on_breach '{}' in axonstore '{}'. Valid: {}",
node.on_breach,
node.name,
valid_list(VALID_STORE_ON_BREACH)
),
&node.loc,
);
}
if let Some(v) = node.confidence_floor {
self.check_range(v, 0.0, 1.0, "confidence_floor", &node.loc);
}
if node.backend == "secrets" {
if node.class.is_empty() {
self.emit(
format!(
"axon-T900 axonstore '{}' declares `backend: secrets` without a \
`class:` — a class-less secrets store would enumerate the \
tenant's ENTIRE secret namespace (`llm.*` included). Declare \
the secret-class prefix it may see, e.g. `class: crm` (covers \
keys under `crm.`).",
node.name
),
&node.loc,
);
} else if !crate::parser::is_valid_capability_slug(&node.class) {
self.emit(
format!(
"axon-T900 axonstore '{}' declares an invalid secret class \
'{}'. A class is a dotted lowercase prefix matching \
^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — e.g. `crm`, \
`crm.oauth`.",
node.name, node.class
),
&node.loc,
);
}
if node.column_schema.is_some() {
self.emit(
format!(
"axon-T900 axonstore '{}' declares `backend: secrets` AND an \
explicit `schema` — the metadata schema of a secrets store is \
LAW, synthesized by the compiler (key: Text, version: Int, \
created_at: Timestamptz, expires_at: Timestamptz). Drop the \
`schema` block; the secret VALUE has no column by design \
(`rotation_without_revelation`).",
node.name
),
&node.loc,
);
}
if !node.connection.is_empty()
|| !node.isolation.is_empty()
|| !node.on_breach.is_empty()
|| node.confidence_floor.is_some()
{
self.emit(
format!(
"axon-T900 axonstore '{}' declares `backend: secrets` with \
adopter-storage fields (`connection:` / `isolation:` / \
`on_breach:` / `confidence_floor:`) — a secrets store has no \
connection string and no adopter table behind it (the runtime \
binds it to the tenant's secret custody). Only `class:` and \
`capability:` apply.",
node.name
),
&node.loc,
);
}
} else if !node.class.is_empty() {
self.emit(
format!(
"axon-T900 axonstore '{}' declares `class: {}` but its backend is \
'{}' — `class:` is the secret-class prefix of a `backend: secrets` \
metadata store and has no meaning elsewhere.",
node.name,
node.class,
if node.backend.is_empty() {
"<unset>"
} else {
&node.backend
}
),
&node.loc,
);
}
}
fn check_resource(&mut self, node: &ResourceDefinition) {
if !node.lifetime.is_empty()
&& !matches!(node.lifetime.as_str(), "linear" | "affine" | "persistent")
{
self.emit(
format!(
"Invalid lifetime '{}' for resource '{}' — \
expected linear | affine | persistent",
node.lifetime, node.name
),
&node.loc,
);
}
if let Some(c) = node.certainty_floor {
if !(0.0..=1.0).contains(&c) {
self.emit(
format!(
"certainty_floor {c} for resource '{}' is out of range [0.0, 1.0]",
node.name
),
&node.loc,
);
}
}
if !node.shield_ref.is_empty() {
match self.symbols.lookup(&node.shield_ref) {
None => self.emit(
format!(
"Undefined shield '{}' in resource '{}'",
node.shield_ref, node.name
),
&node.loc,
),
Some(sym) if sym.kind != "shield" => self.emit(
format!(
"'{}' is a {}, not a shield (referenced in resource '{}')",
node.shield_ref, sym.kind, node.name
),
&node.loc,
),
_ => {}
}
}
if node.kind.is_empty() {
self.emit(
format!(
"axon-T942 Resource '{}' has no `kind:` — declare one of: {}. \
A resource with no kind names no infrastructure; the runtime cannot \
reach it, and every discipline hung on it (capacity, lifetime, lease) \
would govern nothing.",
node.name,
VALID_RESOURCE_KINDS.join(" | ")
),
&node.loc,
);
} else if !VALID_RESOURCE_KINDS.contains(&node.kind.as_str()) {
self.emit(
format!(
"axon-T942 Invalid kind '{}' for resource '{}' — expected one of: {}. \
The catalog is exactly what the runtime can REACH; a kind outside it \
is a promise with no implementation behind it.",
node.kind,
node.name,
VALID_RESOURCE_KINDS.join(" | ")
),
&node.loc,
);
}
if !node.within.is_empty() {
match self.symbols.lookup(&node.within) {
None => self.emit(
format!(
"axon-T943 Resource '{}' is `within:` fabric '{}', which is not \
declared. A resource placed in a fabric that does not exist is \
placed nowhere.",
node.name, node.within
),
&node.loc,
),
Some(sym) if sym.kind != "fabric" => self.emit(
format!(
"axon-T943 Resource '{}' is `within:` '{}', which is a {}, not a \
fabric.",
node.name, node.within, sym.kind
),
&node.loc,
),
_ => {}
}
}
if node.endpoint.is_empty() {
self.emit(
format!(
"axon-T944 Resource '{}' has no `endpoint:` — declare a per-tenant config \
key (e.g. `endpoint: db.main`). A resource with no endpoint is unreachable.",
node.name
),
&node.loc,
);
} else if !is_config_key(&node.endpoint) {
self.emit(
format!(
"axon-T944 Resource '{}' `endpoint:` value '{}' is not a config key — keys \
are lowercase dot-separated (`[a-z0-9][a-z0-9_.-]*`, no `/`, no `:`). \
URLs and credentials never appear in source (the same config-not-code law \
`axon-T850` already enforces on `upstream.resolve`, and `axon-T902` on \
`tool.secret`). A production DSN written into the program is exactly the \
shape §94 secret custody exists to refuse.",
node.name, node.endpoint
),
&node.loc,
);
}
}
fn check_fabric(&mut self, node: &FabricDefinition) {
if !node.provider.is_empty() {
if let Some(false) =
crate::substrate::region_matches_provider(&node.provider, &node.region)
{
let hint = match crate::substrate::provider_shape(&node.provider) {
crate::substrate::ProviderShape::Validated { example } => {
format!(" (a {} region looks like \"{example}\")", node.provider)
}
crate::substrate::ProviderShape::Unvalidated => String::new(),
};
self.emit(
format!(
"axon-E041 region/provider mismatch: fabric '{}' declares provider '{}' with region \"{}\"{hint}. The substrate this fabric describes does not exist, so every resource placed `within` it expects to find something that is not there.",
node.name, node.provider, node.region
),
&node.loc,
);
}
}
if let Some(z) = node.zones {
if z < 1 {
self.emit(
format!(
"Fabric '{}' has invalid zones {z} — must be >= 1",
node.name
),
&node.loc,
);
}
}
if !node.shield_ref.is_empty() {
match self.symbols.lookup(&node.shield_ref) {
None => self.emit(
format!(
"Undefined shield '{}' in fabric '{}'",
node.shield_ref, node.name
),
&node.loc,
),
Some(sym) if sym.kind != "shield" => self.emit(
format!(
"'{}' is a {}, not a shield (referenced in fabric '{}')",
node.shield_ref, sym.kind, node.name
),
&node.loc,
),
_ => {}
}
}
}
fn check_manifest(&mut self, node: &ManifestDefinition) {
if !node.fabric_ref.is_empty() && !node.compliance.is_empty() {
let substrate = self.program.declarations.iter().find_map(|d| match d {
Declaration::Fabric(f) if f.name == node.fabric_ref => {
Some((f.provider.clone(), f.region.clone()))
}
_ => None,
});
if let Some((provider, region)) = substrate {
for tag in &node.compliance {
if let Some(v) =
crate::substrate::compliance_violation(tag, &provider, ®ion)
{
self.emit(
format!(
"axon-E042 compliance/jurisdiction: manifest '{}' {v}",
node.name
),
&node.loc,
);
}
}
}
}
let mut seen: std::collections::HashSet<&String> = std::collections::HashSet::new();
for res_name in &node.resources {
if !seen.insert(res_name) {
self.emit(
format!(
"Manifest '{}' lists resource '{}' more than once \
(Linear/Separation Logic disjointness)",
node.name, res_name
),
&node.loc,
);
continue;
}
match self.symbols.lookup(res_name) {
None => self.emit(
format!(
"Manifest '{}' references undefined resource '{}'",
node.name, res_name
),
&node.loc,
),
Some(sym) if sym.kind != "resource" => self.emit(
format!(
"'{}' is a {}, not a resource (referenced in manifest '{}')",
res_name, sym.kind, node.name
),
&node.loc,
),
_ => {}
}
}
if !node.fabric_ref.is_empty() {
match self.symbols.lookup(&node.fabric_ref) {
None => self.emit(
format!(
"Manifest '{}' references undefined fabric '{}'",
node.name, node.fabric_ref
),
&node.loc,
),
Some(sym) if sym.kind != "fabric" => self.emit(
format!(
"'{}' is a {}, not a fabric (referenced in manifest '{}')",
node.fabric_ref, sym.kind, node.name
),
&node.loc,
),
_ => {}
}
}
if let Some(z) = node.zones {
if z < 1 {
self.emit(
format!(
"Manifest '{}' has invalid zones {z} — must be >= 1",
node.name
),
&node.loc,
);
}
}
}
fn check_observe(&mut self, node: &ObserveDefinition) {
if node.target.is_empty() {
self.emit(
format!(
"Observe '{}' is missing 'from <Manifest>' target",
node.name
),
&node.loc,
);
} else {
match self.symbols.lookup(&node.target) {
None => self.emit(
format!(
"Observe '{}' targets undefined manifest '{}'",
node.name, node.target
),
&node.loc,
),
Some(sym) if sym.kind != "manifest" => self.emit(
format!(
"'{}' is a {}, not a manifest (observed by '{}')",
node.target, sym.kind, node.name
),
&node.loc,
),
_ => {}
}
}
if let Some(c) = node.certainty_floor {
if !(0.0..=1.0).contains(&c) {
self.emit(
format!(
"certainty_floor {c} for observe '{}' is out of range [0.0, 1.0]",
node.name
),
&node.loc,
);
}
}
if let Some(q) = node.quorum {
if q < 1 {
self.emit(
format!(
"Observe '{}' has invalid quorum {q} — must be >= 1",
node.name
),
&node.loc,
);
}
}
if !node.on_partition.is_empty()
&& !matches!(node.on_partition.as_str(), "fail" | "shield_quarantine")
{
self.emit(
format!(
"Invalid on_partition '{}' for observe '{}' — \
expected fail | shield_quarantine",
node.on_partition, node.name
),
&node.loc,
);
}
if node.sources.is_empty() {
self.emit(
format!("Observe '{}' has empty sources: list", node.name),
&node.loc,
);
}
}
fn check_reconcile(&mut self, node: &ReconcileDefinition) {
if node.observe_ref.is_empty() {
self.emit(
format!("Reconcile '{}' is missing 'observe:' target", node.name),
&node.loc,
);
} else {
match self.symbols.lookup(&node.observe_ref) {
None => self.emit(
format!(
"Reconcile '{}' references undefined observe '{}'",
node.name, node.observe_ref
),
&node.loc,
),
Some(sym) if sym.kind != "observe" => self.emit(
format!(
"'{}' is a {}, not an observe (referenced in reconcile '{}')",
node.observe_ref, sym.kind, node.name
),
&node.loc,
),
_ => {}
}
}
if let Some(t) = node.threshold {
if !(0.0..=1.0).contains(&t) {
self.emit(
format!(
"threshold {t} for reconcile '{}' is out of range [0.0, 1.0]",
node.name
),
&node.loc,
);
}
}
if let Some(t) = node.tolerance {
if !(0.0..=1.0).contains(&t) {
self.emit(
format!(
"tolerance {t} for reconcile '{}' is out of range [0.0, 1.0]",
node.name
),
&node.loc,
);
}
}
if node.max_retries < 0 {
self.emit(
format!(
"Reconcile '{}' has invalid max_retries {} — must be >= 0",
node.name, node.max_retries
),
&node.loc,
);
}
if !node.shield_ref.is_empty() {
match self.symbols.lookup(&node.shield_ref) {
None => self.emit(
format!(
"Undefined shield '{}' in reconcile '{}'",
node.shield_ref, node.name
),
&node.loc,
),
Some(sym) if sym.kind != "shield" => self.emit(
format!(
"'{}' is a {}, not a shield (referenced in reconcile '{}')",
node.shield_ref, sym.kind, node.name
),
&node.loc,
),
_ => {}
}
}
if !node.mandate_ref.is_empty() {
match self.symbols.lookup(&node.mandate_ref) {
None => self.emit(
format!(
"Undefined mandate '{}' in reconcile '{}'",
node.mandate_ref, node.name
),
&node.loc,
),
Some(sym) if sym.kind != "mandate" => self.emit(
format!(
"'{}' is a {}, not a mandate (referenced in reconcile '{}')",
node.mandate_ref, sym.kind, node.name
),
&node.loc,
),
_ => {}
}
}
}
fn check_lease(&mut self, node: &LeaseDefinition) {
if node.resource_ref.is_empty() {
self.emit(
format!("Lease '{}' is missing 'resource:' target", node.name),
&node.loc,
);
} else {
match self.symbols.lookup(&node.resource_ref) {
None => self.emit(
format!(
"Lease '{}' references undefined resource '{}'",
node.name, node.resource_ref
),
&node.loc,
),
Some(sym) if sym.kind != "resource" => self.emit(
format!(
"'{}' is a {}, not a resource (leased by '{}')",
node.resource_ref, sym.kind, node.name
),
&node.loc,
),
_ => {}
}
}
if node.duration.is_empty() {
self.emit(
format!("Lease '{}' is missing 'duration:' field", node.name),
&node.loc,
);
}
}
fn check_ensemble(&mut self, node: &EnsembleDefinition) {
if node.observations.is_empty() {
self.emit(
format!("Ensemble '{}' has empty observations: list", node.name),
&node.loc,
);
return;
}
if node.observations.len() < 2 {
self.emit(
format!(
"Ensemble '{}' has {} observation(s); Byzantine quorum requires >= 2",
node.name,
node.observations.len()
),
&node.loc,
);
}
let mut seen: std::collections::HashSet<&String> = std::collections::HashSet::new();
for obs_name in &node.observations {
if !seen.insert(obs_name) {
self.emit(
format!(
"Ensemble '{}' lists observation '{}' more than once",
node.name, obs_name
),
&node.loc,
);
continue;
}
match self.symbols.lookup(obs_name) {
None => self.emit(
format!(
"Ensemble '{}' references undefined observation '{}'",
node.name, obs_name
),
&node.loc,
),
Some(sym) if sym.kind != "observe" => self.emit(
format!(
"'{}' is a {}, not an observe (referenced in ensemble '{}')",
obs_name, sym.kind, node.name
),
&node.loc,
),
_ => {}
}
}
if let Some(q) = node.quorum {
if q < 1 {
self.emit(
format!(
"Ensemble '{}' has invalid quorum {q} — must be >= 1",
node.name
),
&node.loc,
);
} else if (q as usize) > node.observations.len() {
self.emit(
format!(
"Ensemble '{}' quorum {q} exceeds available observations ({})",
node.name,
node.observations.len()
),
&node.loc,
);
}
}
}
fn check_session(&mut self, node: &SessionDefinition) {
if node.roles.len() != 2 {
self.emit(
format!(
"Session '{}' must declare exactly 2 roles (binary session); got {}",
node.name,
node.roles.len()
),
&node.loc,
);
} else if node.roles[0].name == node.roles[1].name {
self.emit(
format!(
"Session '{}' has duplicate role name '{}'",
node.name, node.roles[0].name
),
&node.loc,
);
}
for role in &node.roles {
self.check_session_role(&node.name, role);
}
if node.roles.len() == 2 {
self.check_session_duality(node);
}
}
fn check_session_role(&mut self, session_name: &str, role: &SessionRole) {
self.check_session_steps(session_name, &role.name, &role.steps);
}
fn check_session_steps(&mut self, session_name: &str, role_name: &str, steps: &[SessionStep]) {
for (idx, step) in steps.iter().enumerate() {
match step.op.as_str() {
"send" | "receive" => {
if step.message_type.is_empty() {
self.emit(
format!(
"Session '{session_name}' role '{role_name}' step #{idx} '{}' \
requires a message type",
step.op
),
&step.loc,
);
}
}
"loop" | "end" => {}
"resume" => {}
"interrupt" => {
if !CALL_INTERRUPT_CAUSES.contains(&step.message_type.as_str()) {
self.emit(
format!(
"Session '{session_name}' role '{role_name}' step #{idx}: \
interrupt signal '{}' is not a CallInterruptCause \
(expected one of: {})",
step.message_type,
CALL_INTERRUPT_CAUSES.join(", ")
),
&step.loc,
);
}
let has = |l: &str| step.branches.iter().any(|b| b.label == l);
if !has("body") || !has("handler") {
self.emit(
format!(
"Session '{session_name}' role '{role_name}' step #{idx}: \
interrupt requires both a body and a resumable handler"
),
&step.loc,
);
}
for b in &step.branches {
self.check_session_steps(session_name, role_name, &b.steps);
}
if let Some(h) = step.branches.iter().find(|b| b.label == "handler") {
if !handler_reaches_exit(&h.steps) {
self.emit(
format!(
"Session '{session_name}' role '{role_name}' step #{idx}: \
interrupt handler must reach `resume` or `end` \
(a two-exit construct, paper §3.5)"
),
&step.loc,
);
}
}
}
"select" | "branch" => {
if step.branches.is_empty() {
self.emit(
format!(
"Session '{session_name}' role '{role_name}' step #{idx} '{}' must \
have at least one branch",
step.op
),
&step.loc,
);
}
let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
for b in &step.branches {
if !seen.insert(b.label.as_str()) {
self.emit(
format!(
"Session '{session_name}' role '{role_name}' choice has \
duplicate branch label '{}'",
b.label
),
&b.loc,
);
}
self.check_session_steps(session_name, role_name, &b.steps);
}
}
other => {
self.emit(
format!("Session '{session_name}' role '{role_name}' step #{idx} has invalid op '{other}'"),
&step.loc,
);
}
}
}
}
fn check_session_duality(&mut self, node: &SessionDefinition) {
let t1 = lower_session_role(&node.roles[0]);
let t2 = lower_session_role(&node.roles[1]);
for (role, t) in [(&node.roles[0], &t1), (&node.roles[1], &t2)] {
if is_unguarded_recursion(t) {
self.warn(
format!(
"axon-W012 session '{}' role '{}': a leading `loop` makes the session type vacuous (μX.X) — everything after a `loop` in a sequence is unreachable, so duality and credit hold trivially, not meaningfully. Put the iteration body first and `loop` last: `[ send A, receive B, loop ]`",
node.name, role.name
),
&node.loc,
);
return;
}
}
if !t1.is_dual_to(&t2) {
self.emit(
format!(
"Session '{}' duality violation: role '{}' has the session type `{}`, \
whose dual is `{}`, but role '{}' has `{}` (expected the dual)",
node.name,
node.roles[0].name,
t1,
t1.dual(),
node.roles[1].name,
t2,
),
&node.loc,
);
}
}
fn check_socket(&mut self, node: &SocketDefinition) {
let session = if node.protocol.is_empty() {
self.emit(
format!("Socket '{}' has no `protocol:` — it must reference a declared session", node.name),
&node.loc,
);
None
} else {
match find_session_by_name(self.program, &node.protocol) {
Some(s) => Some(s),
None => {
self.emit(
format!(
"Socket '{}' protocol '{}' is not a declared session (the protocol must be a `session`)",
node.name, node.protocol
),
&node.loc,
);
None
}
}
};
let budget: Option<u64> = match node.backpressure_credit {
Some(n) if n >= 1 => Some(n as u64),
Some(n) => {
self.emit(
format!(
"Socket '{}' backpressure credit must be ≥ 1 (got {n}); a 0-credit window \
cannot type a send (§Fase 41 §4.2)",
node.name
),
&node.loc,
);
None
}
None => None, };
if let (Some(session), Some(budget)) = (session, budget) {
for role in &session.roles {
let lowered = lower_session_role(role).with_credit(budget);
if is_unguarded_recursion(&lowered) {
continue;
}
if let Err(e) = lowered.credit_analyse(budget) {
self.emit(
format!(
"Socket '{}' violates the credit-refined backpressure type of \
session '{}' role '{}': {} (D2)",
node.name, node.protocol, role.name, e
),
&node.loc,
);
}
}
}
}
fn check_upstream(&mut self, node: &UpstreamDefinition) {
if node.preset.is_some() && node.transport.is_empty() && node.protocol.is_empty() {
self.emit(
format!(
"Upstream '{}' references unknown preset '{}'. Available: {} (or fork: write the full `upstream` by hand — a preset is ordinary source, D80.5)",
node.name,
node.preset.as_deref().unwrap_or(""),
crate::upstream_presets::available()
),
&node.loc,
);
return;
}
if node.transport.is_empty() {
self.emit(
format!("Upstream '{}' has no `transport:` — v1 catalog: {}", node.name, valid_list(VALID_UPSTREAM_TRANSPORTS)),
&node.loc,
);
} else if !is_valid(&node.transport, VALID_UPSTREAM_TRANSPORTS) {
self.emit(
format!(
"Upstream '{}' transport '{}' is not in the v1 catalog: {} (gRPC/raw-TCP are named deferred scope — fase_80_upstream_design.md §5)",
node.name, node.transport, valid_list(VALID_UPSTREAM_TRANSPORTS)
),
&node.loc,
);
}
if node.auth_kind.is_empty() {
self.emit(
format!(
"Upstream '{}' has no `auth:` — every vendor handshake must be declared. Valid: {}",
node.name,
valid_list(VALID_UPSTREAM_AUTH_KINDS)
),
&node.loc,
);
} else if !is_valid(&node.auth_kind, VALID_UPSTREAM_AUTH_KINDS) {
self.emit(
format!(
"Upstream '{}' auth kind '{}' is not in the catalog: {}",
node.name,
node.auth_kind,
valid_list(VALID_UPSTREAM_AUTH_KINDS)
),
&node.loc,
);
} else {
match node.auth_kind.as_str() {
"header" | "query" if node.auth_name.is_none() => self.emit(
format!(
"Upstream '{}' auth `{}` requires a name — `{}(\"<name>\")`",
node.name, node.auth_kind, node.auth_kind
),
&node.loc,
),
"signed_url" if node.auth_name.is_some() => self.emit(
format!(
"Upstream '{}' auth `signed_url` takes no arguments — the resolved URL already carries its signature",
node.name
),
&node.loc,
),
_ => {}
}
}
if let Some(overflow) = &node.overflow {
const VALID_UPSTREAM_OVERFLOW: &[&str] = &["drop_oldest", "fail", "pause_upstream"];
if !is_valid(overflow, VALID_UPSTREAM_OVERFLOW) {
self.emit(
format!(
"Upstream '{}' overflow '{}' is not in the v1 catalog: {} (`degrade_quality` needs a declared degrader — named deferred scope, fase_80_upstream_design.md §5)",
node.name,
overflow,
valid_list(VALID_UPSTREAM_OVERFLOW)
),
&node.loc,
);
}
}
if let Some(rc) = &node.reconnect {
if rc.backoff_ms < 1 {
self.emit(
format!("Upstream '{}' reconnect backoff_ms must be ≥ 1 (got {})", node.name, rc.backoff_ms),
&node.loc,
);
}
if rc.max_attempts < 0 {
self.emit(
format!("Upstream '{}' reconnect max_attempts must be ≥ 0 (got {})", node.name, rc.max_attempts),
&node.loc,
);
}
if !is_valid(&rc.on_exhausted, VALID_UPSTREAM_ON_EXHAUSTED) {
self.emit(
format!(
"Upstream '{}' reconnect on_exhausted '{}' is not in the v1 catalog: {} (`degrade`/`park` are named deferred scope)",
node.name,
rc.on_exhausted,
valid_list(VALID_UPSTREAM_ON_EXHAUSTED)
),
&node.loc,
);
}
}
let bound_role: Option<&SessionRole> = if node.protocol.is_empty() {
self.emit(
format!("axon-T851 Upstream '{}' has no `protocol:` — it must reference a declared session", node.name),
&node.loc,
);
None
} else {
match find_session_by_name(self.program, &node.protocol) {
None => {
self.emit(
format!(
"axon-T851 Upstream '{}' protocol '{}' is not a declared session",
node.name, node.protocol
),
&node.loc,
);
None
}
Some(session) => {
if node.role.is_empty() {
self.emit(
format!(
"axon-T851 Upstream '{}' has no `role:` — declare which side of session '{}' axon plays ({})",
node.name,
node.protocol,
session.roles.iter().map(|r| r.name.as_str()).collect::<Vec<_>>().join(", ")
),
&node.loc,
);
None
} else {
match session.roles.iter().find(|r| r.name == node.role) {
Some(role) => Some(role),
None => {
self.emit(
format!(
"axon-T851 Upstream '{}' role '{}' is not a role of session '{}' (roles: {})",
node.name,
node.role,
node.protocol,
session.roles.iter().map(|r| r.name.as_str()).collect::<Vec<_>>().join(", ")
),
&node.loc,
);
None
}
}
}
}
}
};
if let Some(role) = bound_role {
match node.backpressure_credit {
Some(n) if n >= 1 => {
let lowered = lower_session_role(role).with_credit(n as u64);
if is_unguarded_recursion(&lowered) {
return;
}
if let Err(e) = lowered.credit_analyse(n as u64) {
self.emit(
format!(
"Upstream '{}' violates the credit-refined backpressure type of session '{}' role '{}': {} (D2)",
node.name, node.protocol, node.role, e
),
&node.loc,
);
}
}
Some(n) => self.emit(
format!(
"Upstream '{}' backpressure credit must be ≥ 1 (got {n}); a 0-credit window cannot type a send (§Fase 41 §4.2)",
node.name
),
&node.loc,
),
None => {}
}
}
if !node.resource_ref.is_empty() {
if !node.resolve.is_empty() {
self.emit(
format!(
"axon-T951 Upstream '{}' declares BOTH `resource: {}` AND `resolve: {}` — \
the channel's address stated twice. When the upstream rides a resource, \
its dial address derives from the resource's `endpoint` (axon-T944); a \
second address is either redundant or a contradiction, and a reader can \
no longer tell which one runs. Drop `resolve:`.",
node.name, node.resource_ref, node.resolve
),
&node.loc,
);
}
match self.symbols.lookup(&node.resource_ref) {
None => self.emit(
format!(
"axon-T951 Upstream '{}' names resource '{}', which is not declared.",
node.name, node.resource_ref
),
&node.loc,
),
Some(sym) if sym.kind != "resource" => self.emit(
format!(
"axon-T951 Upstream '{}' names '{}', which is a {}, not a resource.",
node.name, node.resource_ref, sym.kind
),
&node.loc,
),
_ => {}
}
} else {
self.check_upstream_config_key(&node.name, "resolve", &node.resolve, &node.loc);
}
self.check_upstream_config_key(&node.name, "secret", &node.secret, &node.loc);
for rule in &node.map {
if !is_valid(&rule.framing, VALID_UPSTREAM_FRAMINGS) {
self.emit(
format!(
"Upstream '{}' map rule for '{}' has framing '{}' — valid: {}",
node.name,
rule.message,
rule.framing,
valid_list(VALID_UPSTREAM_FRAMINGS)
),
&rule.loc,
);
}
if rule.tag.is_some() && (rule.direction != "send" || rule.framing != "json") {
self.emit(
format!(
"Upstream '{}' map rule for '{}': `tag` applies only to `send … as json`",
node.name, rule.message
),
&rule.loc,
);
}
if rule.when_field.is_some() && (rule.direction != "receive" || rule.framing != "json") {
self.emit(
format!(
"Upstream '{}' map rule for '{}': `when` applies only to `receive … as json`",
node.name, rule.message
),
&rule.loc,
);
}
}
if let Some(role) = bound_role {
let mut sends: Vec<String> = Vec::new();
let mut receives: Vec<String> = Vec::new();
collect_role_messages(&role.steps, &mut sends, &mut receives);
for (dir, msgs) in [("send", &sends), ("receive", &receives)] {
for msg in msgs.iter() {
let n = node
.map
.iter()
.filter(|r| r.direction == dir && &r.message == msg)
.count();
if n == 0 {
self.emit(
format!(
"axon-T849 Upstream '{}': session '{}' role '{}' {}s '{}' but `map:` has no `{}` rule for it — a message with no projection would fall through untranscoded",
node.name, node.protocol, node.role, dir, msg, dir
),
&node.loc,
);
} else if n > 1 {
self.emit(
format!(
"axon-T849 Upstream '{}': duplicate `{}` map rules for '{}' — the projection must be unambiguous",
node.name, dir, msg
),
&node.loc,
);
}
}
}
for rule in &node.map {
let known = match rule.direction.as_str() {
"send" => sends.contains(&rule.message),
_ => receives.contains(&rule.message),
};
if !known {
self.emit(
format!(
"axon-T849 Upstream '{}': map rule `{} {}` names a message session '{}' role '{}' never {}s",
node.name, rule.direction, rule.message, node.protocol, node.role, rule.direction
),
&rule.loc,
);
}
}
let receive_json: Vec<(&str, String, Option<String>)> = node
.map
.iter()
.filter(|r| r.direction == "receive" && r.framing == "json")
.map(|r| match (&r.when_field, &r.when_value) {
(None, _) => (r.message.as_str(), "type".to_string(), Some(r.message.clone())),
(Some(f), Some(v)) => (r.message.as_str(), f.clone(), Some(v.clone())),
(Some(f), None) => (r.message.as_str(), f.clone(), None),
})
.collect();
for (i, a) in receive_json.iter().enumerate() {
for b in receive_json.iter().skip(i + 1) {
if a.1 == b.1 && a.2 == b.2 {
let shape = match &a.2 {
Some(v) => format!("(\"{}\" = \"{}\")", a.1, v),
None => format!("(has \"{}\")", a.1),
};
self.emit(
format!(
"axon-T849 Upstream '{}': receive rules for '{}' and '{}' share the discriminator {} — inbound dispatch would be ambiguous",
node.name, a.0, b.0, shape
),
&node.loc,
);
}
}
}
let binary_receives = node
.map
.iter()
.filter(|r| r.direction == "receive" && r.framing == "binary")
.count();
if binary_receives > 1 {
self.emit(
format!(
"axon-T849 Upstream '{}': {} `receive … as binary` rules — binary frames carry no discriminator, so at most one is dispatchable",
node.name, binary_receives
),
&node.loc,
);
}
}
}
fn check_voice(&mut self, node: &VoiceDefinition) {
let cascaded_given = node.stt.is_some() || node.tts.is_some();
if node.realtime.is_some() && cascaded_given {
self.emit(
format!(
"axon-T852 Voice '{}' declares `realtime:` alongside `stt:`/`tts:` — one architecture per voice: cascaded (stt+tts) XOR fused (realtime)",
node.name
),
&node.loc,
);
}
if node.realtime.is_none() && (node.stt.is_none() || node.tts.is_none()) {
self.emit(
format!(
"axon-T852 Voice '{}' is incomplete — cascaded needs BOTH `stt:` and `tts:`, or declare a single fused `realtime:` leg",
node.name
),
&node.loc,
);
}
if node.interruptible && node.legal_basis.is_none() {
self.emit(
format!(
"axon-T852 Voice '{}' declares `interruptible: true` without `legal_basis:` — a barge-in-capable call parks mid-utterance residuals at rest (§79), and that retention must be governed",
node.name
),
&node.loc,
);
}
const VALID_CARRIERS: &[&str] = &["mulaw8k", "pcm16"];
if !node.carrier.is_empty() && !is_valid(&node.carrier, VALID_CARRIERS) {
self.emit(
format!(
"Voice '{}' carrier '{}' is not in the catalog: {}",
node.name,
node.carrier,
valid_list(VALID_CARRIERS)
),
&node.loc,
);
}
for (field, leg) in [("stt", &node.stt), ("tts", &node.tts), ("realtime", &node.realtime)] {
if let Some(r) = leg {
if r.contains('@') {
if crate::upstream_presets::find(r).is_none() {
self.emit(
format!(
"axon-T852 Voice '{}' `{field}:` references unknown preset '{r}'. Available: {}",
node.name,
crate::upstream_presets::available()
),
&node.loc,
);
}
} else {
match self.symbols.lookup(r) {
Some(sym) if sym.kind == "upstream" => {}
Some(sym) => self.emit(
format!(
"axon-T852 Voice '{}' `{field}:` references '{r}', which is a {} — expected a declared `upstream` or a `Preset@vN`",
node.name, sym.kind
),
&node.loc,
),
None => self.emit(
format!(
"axon-T852 Voice '{}' `{field}:` references '{r}' — not a declared `upstream` and not a `Preset@vN` from the catalog ({})",
node.name,
crate::upstream_presets::available()
),
&node.loc,
),
}
}
}
}
for (field, kind, r) in [("persona", "persona", &node.persona), ("context", "context", &node.context)] {
if let Some(name) = r {
match self.symbols.lookup(name) {
Some(sym) if sym.kind == kind => {}
_ => self.emit(
format!("Voice '{}' `{field}:` references '{name}' — not a declared {kind}", node.name),
&node.loc,
),
}
}
}
}
fn check_upstream_config_key(&mut self, upstream: &str, field: &str, key: &str, loc: &Loc) {
if key.is_empty() {
self.emit(
format!("axon-T850 Upstream '{upstream}' has no `{field}:` — declare a per-tenant config key (e.g. `upstream.vendor.{}`)",
if field == "secret" { "api_key" } else { "url" }),
loc,
);
return;
}
if !is_config_key(key) {
self.emit(
format!(
"axon-T850 Upstream '{upstream}' `{field}:` value '{key}' is not a config key — keys are lowercase dot-separated (`[a-z0-9][a-z0-9_.-]*`, no `/`, no `:`); URLs and credentials never appear in source (the same config-not-code property as `tool`, §58.g)"
),
loc,
);
}
}
fn check_topology(&mut self, node: &TopologyDefinition) {
const NODE_KINDS: &[&str] = &[
"resource",
"fabric",
"manifest",
"observe",
"axonendpoint",
"axonstore",
"daemon",
"agent",
"shield",
];
let mut seen_nodes: std::collections::HashSet<&String> = std::collections::HashSet::new();
for n in &node.nodes {
if !seen_nodes.insert(n) {
self.emit(
format!("Topology '{}' lists node '{}' more than once", node.name, n),
&node.loc,
);
continue;
}
match self.symbols.lookup(n) {
None => self.emit(
format!("Topology '{}' references undefined node '{}'", node.name, n),
&node.loc,
),
Some(sym) if !NODE_KINDS.contains(&sym.kind.as_str()) => self.emit(
format!(
"Topology '{}' node '{}' is a {} — not a valid topology entity. \
Valid kinds: {}",
node.name,
n,
sym.kind,
NODE_KINDS.join(", ")
),
&node.loc,
),
_ => {}
}
}
for edge in &node.edges {
self.check_topology_edge(&node.name, edge, &seen_nodes);
}
self.check_topology_liveness(node);
}
fn check_topology_edge(
&mut self,
topology_name: &str,
edge: &TopologyEdge,
declared_nodes: &std::collections::HashSet<&String>,
) {
if !declared_nodes.contains(&edge.source) {
self.emit(
format!(
"Topology '{topology_name}' edge source '{}' is not in the nodes list",
edge.source
),
&edge.loc,
);
}
if !declared_nodes.contains(&edge.target) {
self.emit(
format!(
"Topology '{topology_name}' edge target '{}' is not in the nodes list",
edge.target
),
&edge.loc,
);
}
if edge.source == edge.target {
self.emit(
format!(
"Topology '{topology_name}' has self-loop edge on '{}' — \
π-calculus binary sessions require two distinct endpoints",
edge.source
),
&edge.loc,
);
}
if edge.session_ref.is_empty() {
self.emit(
format!(
"Topology '{topology_name}' edge {}->{} has no session reference",
edge.source, edge.target
),
&edge.loc,
);
return;
}
match self.symbols.lookup(&edge.session_ref) {
None => self.emit(
format!(
"Topology '{topology_name}' edge {}->{} references undefined session '{}'",
edge.source, edge.target, edge.session_ref
),
&edge.loc,
),
Some(sym) if sym.kind != "session" => self.emit(
format!(
"Topology '{topology_name}' edge {}->{} session ref '{}' is a {}, not a session",
edge.source, edge.target, edge.session_ref, sym.kind
),
&edge.loc,
),
_ => {}
}
}
fn check_topology_liveness(&mut self, node: &TopologyDefinition) {
let mut adjacency: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
for edge in &node.edges {
if !edge.source.is_empty() && !edge.target.is_empty() {
adjacency
.entry(edge.source.clone())
.or_default()
.push(edge.target.clone());
}
}
let cycles = find_cycles(&adjacency);
if cycles.is_empty() {
return;
}
for cycle in cycles {
let cycle_edges = cycle_to_edges(&cycle, &node.edges);
if cycle_edges.len() == cycle.len()
&& cycle_edges.iter().all(|e| self.edge_is_receive_first(e))
{
let mut tour: Vec<String> = cycle.clone();
if let Some(first) = cycle.first() {
tour.push(first.clone());
}
self.emit(
format!(
"Topology '{}' has a static deadlock: cycle [{}] where every \
edge waits on receive — no progress is possible (Honda liveness violation)",
node.name, tour.join(" -> ")
),
&node.loc,
);
}
}
}
fn edge_is_receive_first(&self, edge: &TopologyEdge) -> bool {
let session = match find_session_by_name(self.program, &edge.session_ref) {
Some(s) => s,
None => return false,
};
let first_role = match session.roles.first() {
Some(r) => r,
None => return false,
};
first_role
.steps
.first()
.map(|s| s.op == "receive")
.unwrap_or(false)
}
fn check_immune(&mut self, node: &ImmuneDefinition) {
if node.scope.is_empty() {
self.emit(
format!(
"immune '{}' requires an explicit 'scope' (tenant | flow | global). \
No implicit default exists — blast radius must be declared (paper §8.2)",
node.name
),
&node.loc,
);
} else if !matches!(node.scope.as_str(), "tenant" | "flow" | "global") {
self.emit(
format!(
"immune '{}' has invalid scope '{}'. Valid: tenant | flow | global",
node.name, node.scope
),
&node.loc,
);
}
if node.watch.is_empty() {
self.emit(
format!(
"immune '{}' requires a non-empty 'watch' list (observables to monitor)",
node.name
),
&node.loc,
);
}
if let Some(s) = node.sensitivity {
if !(0.0..=1.0).contains(&s) {
self.emit(
format!(
"immune '{}' sensitivity must be in [0.0, 1.0], got {s}",
node.name
),
&node.loc,
);
}
}
if node.window < 1 {
self.emit(
format!(
"immune '{}' window must be >= 1, got {}",
node.name, node.window
),
&node.loc,
);
}
if !matches!(node.decay.as_str(), "exponential" | "linear" | "none") {
self.emit(
format!(
"immune '{}' has invalid decay '{}'. Valid: exponential | linear | none",
node.name, node.decay
),
&node.loc,
);
}
}
fn check_reflex(&mut self, node: &ReflexDefinition) {
if node.scope.is_empty() {
self.emit(
format!(
"reflex '{}' requires an explicit 'scope' (tenant | flow | global) — paper §8.2",
node.name
),
&node.loc,
);
} else if !matches!(node.scope.as_str(), "tenant" | "flow" | "global") {
self.emit(
format!("reflex '{}' has invalid scope '{}'", node.name, node.scope),
&node.loc,
);
}
if node.trigger.is_empty() {
self.emit(
format!("reflex '{}' requires a 'trigger: <ImmuneName>'", node.name),
&node.loc,
);
} else {
match self.symbols.lookup(&node.trigger) {
None => self.emit(
format!(
"reflex '{}' references undefined trigger '{}' (expected an immune)",
node.name, node.trigger
),
&node.loc,
),
Some(sym) if sym.kind != "immune" => self.emit(
format!(
"reflex '{}' trigger '{}' is a {}, not an immune",
node.name, node.trigger, sym.kind
),
&node.loc,
),
_ => {}
}
}
if !matches!(
node.on_level.as_str(),
"know" | "believe" | "speculate" | "doubt"
) {
self.emit(
format!(
"reflex '{}' invalid on_level '{}'. Valid: know | believe | speculate | doubt",
node.name, node.on_level
),
&node.loc,
);
}
if node.action.is_empty() {
self.emit(
format!(
"reflex '{}' requires an 'action' (drop | revoke | emit | redact | \
quarantine | terminate | alert)",
node.name
),
&node.loc,
);
} else if !matches!(
node.action.as_str(),
"drop" | "revoke" | "emit" | "redact" | "quarantine" | "terminate" | "alert"
) {
self.emit(
format!("reflex '{}' invalid action '{}'", node.name, node.action),
&node.loc,
);
}
}
fn check_heal(&mut self, node: &HealDefinition) {
if node.scope.is_empty() {
self.emit(
format!(
"heal '{}' requires an explicit 'scope' (tenant | flow | global) — paper §8.2",
node.name
),
&node.loc,
);
} else if !matches!(node.scope.as_str(), "tenant" | "flow" | "global") {
self.emit(
format!("heal '{}' has invalid scope '{}'", node.name, node.scope),
&node.loc,
);
}
if node.source.is_empty() {
self.emit(
format!("heal '{}' requires a 'source: <ImmuneName>'", node.name),
&node.loc,
);
} else {
match self.symbols.lookup(&node.source) {
None => self.emit(
format!(
"heal '{}' references undefined source '{}' (expected an immune)",
node.name, node.source
),
&node.loc,
),
Some(sym) if sym.kind != "immune" => self.emit(
format!(
"heal '{}' source '{}' is a {}, not an immune",
node.name, node.source, sym.kind
),
&node.loc,
),
_ => {}
}
}
if !matches!(
node.on_level.as_str(),
"know" | "believe" | "speculate" | "doubt"
) {
self.emit(
format!("heal '{}' invalid on_level '{}'", node.name, node.on_level),
&node.loc,
);
}
if !matches!(
node.mode.as_str(),
"audit_only" | "human_in_loop" | "adversarial"
) {
self.emit(
format!(
"heal '{}' invalid mode '{}'. Valid: audit_only | human_in_loop | \
adversarial (paper §7)",
node.name, node.mode
),
&node.loc,
);
}
if node.mode == "adversarial" && node.shield_ref.is_empty() {
self.emit(
format!(
"heal '{}' mode='adversarial' requires a 'shield' gate \
(no LLM-generated patch ships without review). \
Paper §7.3: adversarial mode needs explicit Risk Acceptance",
node.name
),
&node.loc,
);
}
if !node.shield_ref.is_empty() {
match self.symbols.lookup(&node.shield_ref) {
None => self.emit(
format!(
"heal '{}' references undefined shield '{}'",
node.name, node.shield_ref
),
&node.loc,
),
Some(sym) if sym.kind != "shield" => self.emit(
format!(
"heal '{}' shield ref '{}' is a {}, not a shield",
node.name, node.shield_ref, sym.kind
),
&node.loc,
),
_ => {}
}
}
if node.max_patches < 1 {
self.emit(
format!(
"heal '{}' max_patches must be >= 1, got {}",
node.name, node.max_patches
),
&node.loc,
);
}
}
fn check_component(&mut self, node: &ComponentDefinition) {
let rendered_type = if node.renders.is_empty() {
self.emit(
format!("component '{}' requires 'renders: <TypeName>'", node.name),
&node.loc,
);
None
} else {
match self.symbols.lookup(&node.renders) {
None => {
self.emit(
format!(
"component '{}' references undefined type '{}'",
node.name, node.renders
),
&node.loc,
);
None
}
Some(sym) if sym.kind != "type" => {
self.emit(
format!(
"component '{}' renders '{}' which is a {}, not a type",
node.name, node.renders, sym.kind
),
&node.loc,
);
None
}
Some(_) => find_type_by_name(self.program, &node.renders),
}
};
let shield_node = if node.via_shield.is_empty() {
None
} else {
match self.symbols.lookup(&node.via_shield) {
None => {
self.emit(
format!(
"component '{}' references undefined shield '{}'",
node.name, node.via_shield
),
&node.loc,
);
None
}
Some(sym) if sym.kind != "shield" => {
self.emit(
format!(
"component '{}' via_shield '{}' is a {}, not a shield",
node.name, node.via_shield, sym.kind
),
&node.loc,
);
None
}
Some(_) => find_shield_by_name(self.program, &node.via_shield),
}
};
if let Some(t) = rendered_type {
let type_kappa: std::collections::HashSet<&str> =
t.compliance.iter().map(|s| s.as_str()).collect();
if !type_kappa.is_empty() {
match shield_node {
None => self.emit(
format!(
"component '{}' renders regulated type '{}' \
(kappa = {{{}}}) but declares no 'via_shield'. \
Regulated renders require a shield that covers \
the type's kappa — Fase 9.5.",
node.name,
node.renders,
{
let mut v: Vec<&str> = type_kappa.iter().copied().collect();
v.sort();
v.join(", ")
}
),
&node.loc,
),
Some(s) => {
let shield_kappa: std::collections::HashSet<&str> =
s.compliance.iter().map(|s| s.as_str()).collect();
let mut missing: Vec<&str> =
type_kappa.difference(&shield_kappa).copied().collect();
missing.sort();
if !missing.is_empty() {
self.emit(
format!(
"component '{}' via_shield '{}' does not cover \
kappa = {{{}}} of type '{}'. Add these classes \
to the shield's 'compliance' list or pick a \
shield that already covers them.",
node.name,
node.via_shield,
missing.join(", "),
node.renders,
),
&node.loc,
);
}
}
}
}
}
if !node.on_interact.is_empty() {
match self.symbols.lookup(&node.on_interact) {
None => self.emit(
format!(
"component '{}' references undefined flow '{}'",
node.name, node.on_interact
),
&node.loc,
),
Some(sym) if sym.kind != "flow" => self.emit(
format!(
"component '{}' on_interact '{}' is a {}, not a flow",
node.name, node.on_interact, sym.kind
),
&node.loc,
),
Some(_) => {
if let Some(flow) = find_flow_by_name(self.program, &node.on_interact) {
if !rendered_type.is_none() {
if let Some(first_param) = flow.parameters.first() {
let pt = first_param.type_expr.name.as_str();
if !pt.is_empty() && pt != node.renders {
self.emit(
format!(
"component '{}' on_interact flow '{}' \
expects first parameter of type '{}', \
but component renders '{}'. Signatures \
must match — Fase 9.2 rule 2.",
node.name, node.on_interact, pt, node.renders
),
&node.loc,
);
}
}
}
}
}
}
}
}
fn check_view(&mut self, node: &ViewDefinition) {
if node.components.is_empty() {
self.emit(
format!(
"view '{}' has empty components list — a view must \
compose at least one component",
node.name
),
&node.loc,
);
return;
}
let mut seen: std::collections::HashSet<&String> = std::collections::HashSet::new();
for comp_name in &node.components {
if !seen.insert(comp_name) {
self.emit(
format!(
"view '{}' lists component '{}' more than once",
node.name, comp_name
),
&node.loc,
);
continue;
}
match self.symbols.lookup(comp_name) {
None => self.emit(
format!(
"view '{}' references undefined component '{}'",
node.name, comp_name
),
&node.loc,
),
Some(sym) if sym.kind != "component" => self.emit(
format!(
"view '{}' component ref '{}' is a {}, not a component",
node.name, comp_name, sym.kind
),
&node.loc,
),
_ => {}
}
}
}
fn first_declared_write(&self, steps: &[FlowStep]) -> Option<(&'static str, Loc)> {
for step in steps {
let hit: Option<(&'static str, Loc)> = match step {
FlowStep::Persist(s) => Some(("persist", s.loc.clone())),
FlowStep::Mutate(s) => Some(("mutate", s.loc.clone())),
FlowStep::Purge(s) => Some(("purge", s.loc.clone())),
FlowStep::Emit(s) => Some(("emit", s.loc.clone())),
FlowStep::Publish(s) => Some(("publish", s.loc.clone())),
FlowStep::Rotate(s) => Some(("rotate", s.loc.clone())),
FlowStep::Mint(s) => Some(("mint", s.loc.clone())),
FlowStep::Transact(s) => Some(("transact", s.loc.clone())),
FlowStep::Ingest(s) => Some(("ingest", s.loc.clone())),
FlowStep::If(c) => self
.first_declared_write(&c.then_body)
.or_else(|| self.first_declared_write(&c.else_body)),
FlowStep::ForIn(f) => self.first_declared_write(&f.body),
FlowStep::Par(p) => p
.branches
.iter()
.find_map(|b| self.first_declared_write(b)),
FlowStep::Warden(w) => self.first_declared_write(&w.body),
_ => None,
};
if hit.is_some() {
return hit;
}
}
None
}
fn check_query_is_safe(&mut self, node: &AxonEndpointDefinition) {
if !node.method.eq_ignore_ascii_case("QUERY") || node.execute_flow.is_empty() {
return;
}
let flow_body: Option<&Vec<FlowStep>> = self.program.declarations.iter().find_map(|d| {
match d {
Declaration::Flow(f) if f.name == node.execute_flow => Some(&f.body),
_ => None,
}
});
if let Some(body) = flow_body {
if let Some((verb, loc)) = self.first_declared_write(body) {
self.emit(
format!(
"axon-T927 axonendpoint '{}' declares `method: QUERY`, but its flow '{}' \
performs a declared write (`{}`). RFC 10008 §2: a QUERY MUST be processed \
in a SAFE and IDEMPOTENT manner — caches, proxies and clients are entitled \
to retry and cache it freely, so a QUERY that changes state is a \
correctness + security bug, not a style choice. Use `method: POST` for a \
state-changing operation, or remove the `{}` from this flow.",
node.name, node.execute_flow, verb, verb
),
&loc,
);
return;
}
}
let egress: Option<(&'static str, String)> =
self.program.declarations.iter().find_map(|d| match d {
Declaration::Deliver(x) => Some(("deliver", x.name.clone())),
Declaration::Notify(x) => Some(("notify", x.name.clone())),
Declaration::Document(x) => Some(("document", x.name.clone())),
_ => None,
});
if let Some((kind, decl_name)) = egress {
self.emit(
format!(
"axon-T927 axonendpoint '{}' declares `method: QUERY`, but this program \
declares a `{} {}` — an egress declaration FIRES for every flow the executor \
runs (it writes a CRM row / persists an artifact), so this endpoint could not \
be safe. RFC 10008 §2 requires a QUERY to change no state. Use `method: POST`, \
or move the `{}` into a program whose endpoints are not QUERY.",
node.name, kind, decl_name, kind
),
&node.loc,
);
}
}
fn check_axonendpoint(&mut self, node: &AxonEndpointDefinition) {
if !node.method.is_empty() {
let upper = node.method.to_uppercase();
if !is_valid(&upper, VALID_ENDPOINT_METHODS) {
self.emit(
format!(
"Unknown HTTP method '{}' in axonendpoint '{}'. Valid: {}",
node.method,
node.name,
valid_list(VALID_ENDPOINT_METHODS)
),
&node.loc,
);
}
}
self.check_query_is_safe(node);
if !node.backend.is_empty()
&& !is_valid(&node.backend, crate::parser::AXONENDPOINT_BACKEND_VALUES)
{
self.emit(
format!(
"Unknown backend '{}' in axonendpoint '{}'. Valid: {}",
node.backend,
node.name,
valid_list(crate::parser::AXONENDPOINT_BACKEND_VALUES)
),
&node.loc,
);
}
if node.backend.is_empty() {
self.warn(build_w003_message(&node.name), &node.loc);
}
if !node.path.is_empty() && !node.path.starts_with('/') {
self.emit(
format!(
"Path must start with '/' in axonendpoint '{}', got '{}'",
node.name, node.path
),
&node.loc,
);
}
if !node.execute_flow.is_empty() {
match self.symbols.lookup(&node.execute_flow) {
None => self.emit(
format!(
"Undefined flow '{}' in axonendpoint '{}'",
node.execute_flow, node.name
),
&node.loc,
),
Some(sym) if sym.kind != "flow" => self.emit(
format!(
"'{}' is a {}, not a flow (referenced in axonendpoint '{}')",
node.execute_flow, sym.kind, node.name
),
&node.loc,
),
_ => {}
}
}
if !node.shield_ref.is_empty() {
match self.symbols.lookup(&node.shield_ref) {
None => self.emit(
format!(
"Undefined shield '{}' in axonendpoint '{}'",
node.shield_ref, node.name
),
&node.loc,
),
Some(sym) if sym.kind != "shield" => self.emit(
format!(
"'{}' is a {}, not a shield (referenced in axonendpoint '{}')",
node.shield_ref, sym.kind, node.name
),
&node.loc,
),
_ => {}
}
}
if !node.cors_ref.is_empty() {
match self.symbols.lookup(&node.cors_ref) {
None => self.emit(
format!(
"axon-T856 undefined cors '{}' in axonendpoint '{}'",
node.cors_ref, node.name
),
&node.loc,
),
Some(sym) if sym.kind != "cors" => self.emit(
format!(
"axon-T856 '{}' is a {}, not a cors (referenced in axonendpoint '{}')",
node.cors_ref, sym.kind, node.name
),
&node.loc,
),
_ => {}
}
}
if let Some(v) = node.retries {
if v < 0 {
self.emit(
format!(
"retries must be >= 0, got {} in axonendpoint '{}'",
v, node.name
),
&node.loc,
);
}
}
if !node.execute_flow.is_empty() {
if let Some(flow) = self.find_flow(&node.execute_flow) {
for step in &flow.body {
let store_name = match step {
FlowStep::Persist(s) => &s.store_name,
FlowStep::Retrieve(s) => &s.store_name,
FlowStep::Mutate(s) => &s.store_name,
FlowStep::Purge(s) => &s.store_name,
_ => continue,
};
let Some(store) = self.find_store(store_name) else {
continue;
};
if store.capability.is_empty()
|| node.requires_capabilities.contains(&store.capability)
{
continue;
}
self.emit(
format!(
"axonendpoint '{}' executes flow '{}' which accesses \
axonstore '{}' requiring capability '{}', but '{}' \
does not grant it — add '{}' to the endpoint's \
`requires:` list (Fase 35.j Pillar IV).",
node.name,
node.execute_flow,
store_name,
store.capability,
node.name,
store.capability,
),
&node.loc,
);
}
}
}
if !node.execute_flow.is_empty() {
let covered = !node.requires_capabilities.is_empty()
|| !node.shield_ref.is_empty()
|| !node.compliance.is_empty();
if !covered && !node.public {
self.emit(
format!(
"axon-T890 axonendpoint '{}' declares no authorization \
coverage (no `requires:`, no `shield:`, no `compliance:`) \
and is not marked `public: true`. Every endpoint is a \
trust boundary — doctrine `every_boundary_is_guarded`: \
either declare a covering discipline (e.g. \
`requires: [flow.execute]`, `shield: <Name>`, or \
`compliance: [...]`) or, if the endpoint is intentionally \
uncovered, declare `public: true` so the opt-out is \
explicit and auditable. Run `axon fix` to auto-insert \
`public: true` on every currently-uncovered endpoint.",
node.name
),
&node.loc,
);
}
}
if !node.execute_flow.is_empty() {
let mut boundary_kappa: std::collections::HashSet<&str> =
std::collections::HashSet::new();
for type_ref in [node.body_type.as_str(), node.output_type.as_str()] {
let base = peel_type_constructors(type_ref);
if base.is_empty() {
continue;
}
if let Some(t) = find_type_by_name(self.program, base) {
boundary_kappa.extend(t.compliance.iter().map(|s| s.as_str()));
}
}
if !boundary_kappa.is_empty() {
let mut kappa_sorted: Vec<&str> = boundary_kappa.iter().copied().collect();
kappa_sorted.sort_unstable();
if node.shield_ref.is_empty() {
self.emit(
format!(
"axon-T957 axonendpoint '{}' carries regulated data \
(kappa = {{{}}}) across a trust boundary but declares no \
`shield:`. Regulated boundaries require a shield whose \
`compliance:` covers the type's kappa — ESK Fase 6.1 \
coverage rule. Declare `shield: <Name>` on the endpoint, \
where that shield lists at least [{}] in its \
`compliance:`. Declaring the classes on the endpoint's own \
`compliance:` does NOT cover them: that list is a label, \
the shield is the control that acts on a breach.",
node.name,
kappa_sorted.join(", "),
kappa_sorted.join(", "),
),
&node.loc,
);
} else if let Some(shield) = find_shield_by_name(self.program, &node.shield_ref) {
let shield_kappa: std::collections::HashSet<&str> =
shield.compliance.iter().map(|s| s.as_str()).collect();
let mut missing: Vec<&str> =
boundary_kappa.difference(&shield_kappa).copied().collect();
missing.sort_unstable();
if !missing.is_empty() {
self.emit(
format!(
"axon-T957 axonendpoint '{}' declares `shield: {}`, but that \
shield does not cover kappa = {{{}}} carried across the \
boundary — ESK Fase 6.1 coverage rule. Add [{}] to shield \
'{}'s `compliance:` list, or name a shield that already \
covers them.",
node.name,
node.shield_ref,
missing.join(", "),
missing.join(", "),
node.shield_ref,
),
&node.loc,
);
}
}
}
}
let e039_fired = if !node.execute_flow.is_empty()
&& !node.output_type.is_empty()
{
self.emit_e039_wire_packaging_gate(node)
} else {
false
};
if !e039_fired
&& !node.execute_flow.is_empty()
&& !node.output_type.is_empty()
{
if let Some(flow) = self.find_flow(&node.execute_flow) {
let declared = declared_cardinality(&node.output_type);
let tail = infer_flow_tail_cardinality(flow);
self.emit_cardinality_gate(node, &declared, &tail);
}
}
if !node.execute_flow.is_empty() {
let has_any_source = !node.body_type.is_empty()
|| !node.path_params.is_empty()
|| !node.query_params.is_empty();
if !has_any_source {
return;
}
let body_opt = if node.body_type.is_empty() {
None
} else {
find_type_by_name(self.program, &node.body_type)
};
if let Some(flow) = self.find_flow(&node.execute_flow) {
for param in &flow.parameters {
if param.type_expr.optional {
continue; }
let path_hit =
node.path_params.iter().any(|p| p == ¶m.name);
let query_hit = node
.query_params
.iter()
.find(|f| f.name == param.name);
let body_hit = body_opt
.and_then(|b| b.fields.iter().find(|f| f.name == param.name));
let source_count = (path_hit as usize)
+ (query_hit.is_some() as usize)
+ (body_hit.is_some() as usize);
if source_count == 0 {
let body_clause = if node.body_type.is_empty() {
"(declare a body type via `body: T` or)".to_string()
} else {
format!(
"add a field '{}: {}' to '{}', or",
param.name,
fmt_type_expr(¶m.type_expr),
node.body_type,
)
};
self.emit(
format!(
"axonendpoint '{}' executes flow '{}' whose \
required parameter '{}: {}' has no matching \
binding source. The Request Binding Contract \
(Fase 37 + 37.y D3) binds a flow parameter \
from a same-named path placeholder \
(`{{{}}}` in the `path:` string), query \
param (`query: {{ {}: {} }}`), or body \
field. Either {} add a `{{{}}}` placeholder \
to the path, or declare `{}: {}` in the \
`query: {{ … }}` block — or make the \
parameter optional (Fase 37.y D3).",
node.name,
node.execute_flow,
param.name,
fmt_type_expr(¶m.type_expr),
param.name,
param.name,
fmt_type_expr(¶m.type_expr),
body_clause,
param.name,
param.name,
fmt_type_expr(¶m.type_expr),
),
&node.loc,
);
continue;
}
if source_count > 1 {
let mut sources: Vec<&str> = Vec::new();
if path_hit {
sources.push("path");
}
if query_hit.is_some() {
sources.push("query");
}
if body_hit.is_some() {
sources.push("body");
}
let where_phrase = if sources.len() == 2 {
format!("{} and {}", sources[0], sources[1])
} else {
format!(
"{}, {}, and {}",
sources[0], sources[1], sources[2]
)
};
self.emit(
format!(
"axon-T901 axonendpoint '{}' parameter '{}' \
is declared in MORE than one binding source \
({where_phrase}). The Request Binding Contract \
forbids a name in multiple sources to keep \
the runtime binding unambiguous. Remove the \
declaration from {} of the sources so '{}' \
resolves uniquely. (Fase 37.y D4)",
node.name,
param.name,
sources.len() - 1,
param.name,
),
&node.loc,
);
continue;
}
if path_hit {
if param.type_expr.name != "Text"
|| !param.type_expr.generic_param.is_empty()
{
self.emit(
format!(
"axonendpoint '{}' parameter '{}' is bound \
from path placeholder `{{{}}}` (HTTP path \
segments are `Text` by convention), but \
the flow declares '{}: {}'. Either change \
the flow parameter to `{}: Text` and \
parse/validate inside the flow, or move \
the binding to `query: {{ {}: {} }}` if \
the type matters at the wire (Fase 37.y D3).",
node.name,
param.name,
param.name,
param.name,
fmt_type_expr(¶m.type_expr),
param.name,
param.name,
fmt_type_expr(¶m.type_expr),
),
&node.loc,
);
}
} else if let Some(qf) = query_hit {
if qf.type_expr.name != param.type_expr.name
|| qf.type_expr.generic_param
!= param.type_expr.generic_param
{
self.emit(
format!(
"axonendpoint '{}' executes flow '{}' \
whose parameter '{}' is '{}', but the \
`query: {{ … }}` block declares '{}' as \
'{}' — the types must match for the \
Request Binding Contract to bind it \
(Fase 37.y D3).",
node.name,
node.execute_flow,
param.name,
fmt_type_expr(¶m.type_expr),
qf.name,
fmt_type_expr(&qf.type_expr),
),
&node.loc,
);
}
} else if let Some(field) = body_hit {
if field.type_expr.name != param.type_expr.name
|| field.type_expr.generic_param
!= param.type_expr.generic_param
{
self.emit(
format!(
"axonendpoint '{}' executes flow '{}' whose \
parameter '{}' is '{}', but body type '{}' \
declares field '{}' as '{}' — the types \
must match for the Request Binding \
Contract to bind it (Fase 37 D2).",
node.name,
node.execute_flow,
param.name,
fmt_type_expr(¶m.type_expr),
node.body_type,
field.name,
fmt_type_expr(&field.type_expr),
),
&node.loc,
);
}
}
}
}
}
}
fn emit_e039_wire_packaging_gate(
&mut self,
node: &AxonEndpointDefinition,
) -> bool {
let effective_transport = if node.transport_explicit {
node.transport.as_str()
} else if !node.implicit_transport.is_empty() {
node.implicit_transport.as_str()
} else {
"json"
};
if effective_transport != "json" {
return false;
}
let declared = node.output_type.trim();
if declared.is_empty() {
return false;
}
if declared == "Any" {
return false;
}
if declared == "Unit" {
return false;
}
if declared.starts_with("FlowEnvelope<") && declared.ends_with('>') {
return false;
}
let tail_form = if let Some(flow) = self.find_flow(&node.execute_flow) {
let tail = infer_flow_tail_cardinality(flow);
match tail {
Cardinality::Singular(t) if !t.is_empty() => t,
Cardinality::Plural(t) if !t.is_empty() => {
format!("List<{t}>")
}
Cardinality::StreamCardinality(t) if !t.is_empty() => {
format!("Stream<{t}>")
}
Cardinality::Wrapped(_) => {
declared.to_string()
}
_ => declared.to_string(),
}
} else {
declared.to_string()
};
let suggested_envelope = format!("FlowEnvelope<{tail_form}>");
self.emit(
format!(
"axon-E039 axonendpoint '{}' declares `output: {}` with \
`transport: json` (effective), but the v2.0.0 wire \
contract requires `FlowEnvelope<T>` wrapping for every \
JSON-transport response (D12 α). The wire payload IS \
the ψ-vector envelope `⟨ontological_type, result, \
certainty, provenance_chain, …⟩`; a bare `{}` cannot \
satisfy that contract. Flow '{}' produces a `{}` tail. \
Either: \
(a) wrap the output type — `output: {}` is the \
canonical v2.0.0 declaration (the inner T is \
validated against the envelope's `result` slot \
by the D5 runtime gate); OR \
(b) change the transport — `transport: sse(axon)` \
surfaces a streaming wire (per-chunk axon.token \
events + axon.complete envelope) where bare \
`Stream<T>` / `List<T>` declarations are valid. \
See the wire-envelope contract in the docs \
(https://www.ricardovelit.com/axon-docs) for the \
ψ-vector shape. \
(Fase 39 D2 + D12 — Pure Silicon Cognition)",
node.name,
node.output_type,
node.output_type,
node.execute_flow,
tail_form,
suggested_envelope,
),
&node.loc,
);
true
}
fn emit_cardinality_gate(
&mut self,
node: &AxonEndpointDefinition,
declared: &Cardinality,
tail: &Cardinality,
) {
if let Cardinality::Wrapped(inner) = declared {
return self.emit_cardinality_gate(node, inner.as_ref(), tail);
}
if let Cardinality::Wrapped(inner) = tail {
return self.emit_cardinality_gate(node, declared, inner.as_ref());
}
match (declared, tail) {
(Cardinality::Unit, Cardinality::Unit) => {}
(Cardinality::Singular(d), Cardinality::Singular(_)) => {
let _ = d;
}
(Cardinality::Plural(_), Cardinality::Plural(_)) => {}
(Cardinality::StreamCardinality(_), Cardinality::StreamCardinality(_)) => {}
(_, Cardinality::Unknown) => {}
(Cardinality::Disagreed, _) => {
}
(Cardinality::Unknown, _) => {}
(Cardinality::Plural(decl_t), Cardinality::Singular(_)) => {
self.emit(
format!(
"axon-T9XX axonendpoint '{}' declares `output: {}` \
(plural — `List<{}>`), but flow '{}' produces a \
`{}` (singular) tail. The runtime would either \
wrap the singular in an array implicitly OR fail \
the D5 output-schema gate (Fase 32.d) depending \
on path. To make the contract explicit: \
(a) change the endpoint to `output: {}` if it \
returns a single resource (REST \
`GET /api/{{resource}}/{{id}}`-style); OR \
(b) wrap the tail in a list — `return [result]` \
or `for x in [result] {{ x }}` at the flow \
tail. \
(Fase 38.x.f D3 bilateral)",
node.name,
node.output_type,
decl_t,
node.execute_flow,
decl_t,
decl_t,
),
&node.loc,
);
}
(Cardinality::Singular(decl_t), Cardinality::Plural(_)) => {
self.emit(
format!(
"axon-T9XX axonendpoint '{}' declares `output: {}` \
(singular), but flow '{}' produces a `List<{}>` \
tail expression — the flow ends with a step or \
construct that produces a list (e.g. `retrieve` \
step, `for x in xs {{ … }}` loop, or `return \
[a, b, c]`). The runtime D5 output-schema gate \
(Fase 32.d) would reject the response as a \
shape mismatch. \
Either: \
(a) change the endpoint to `output: List<{}>` if \
it is intentionally returning a collection \
(REST `GET /api/{{resource}}`-style); OR \
(b) collapse the tail to a singular element — \
e.g. add `step Project {{ return result[0] }}` \
(or any step that emits the singular shape) \
BEFORE the implicit tail, OR add an explicit \
`return result[0]` at the end of the flow if \
the iteration is guaranteed to yield exactly \
one element. \
(Fase 38.x.f D1 — v1.39.0 narrow case preserved)",
node.name,
node.output_type,
node.execute_flow,
decl_t,
decl_t,
),
&node.loc,
);
}
(Cardinality::StreamCardinality(decl_t), Cardinality::Singular(_))
| (Cardinality::StreamCardinality(decl_t), Cardinality::Plural(_)) => {
self.emit(
format!(
"axon-T9YY axonendpoint '{}' declares `output: \
Stream<{}>` (temporal — chunks arrive over time \
on SSE), but flow '{}' produces a non-stream \
tail. These are distinct cardinality primitives: \
(a) change the endpoint to `output: {}` (or \
`List<{}>`) if you want JSON delivery at \
once, OR \
(b) change the flow tail to a step with \
`output: Stream<{}>` (e.g. `step Generate \
{{ ask: \"...\" output: Stream<{}> }}`) if \
you want SSE chunked delivery. \
(Fase 38.x.f D5 stream_cardinality_mismatch)",
node.name,
decl_t,
node.execute_flow,
decl_t,
decl_t,
decl_t,
decl_t,
),
&node.loc,
);
}
(Cardinality::Singular(_), Cardinality::StreamCardinality(strm_t))
| (Cardinality::Plural(_), Cardinality::StreamCardinality(strm_t)) => {
self.emit(
format!(
"axon-T9YY axonendpoint '{}' declares `output: {}` \
(spatial — materialized at once), but flow '{}' \
produces a `Stream<{}>` tail (temporal — chunks \
arrive over time). These are distinct \
cardinality primitives: \
(a) change the endpoint to `output: Stream<{}>` \
if you want SSE chunked delivery, OR \
(b) change the flow tail to a non-streaming step \
returning `{}` if you want JSON delivery. \
(Fase 38.x.f D5 stream_cardinality_mismatch)",
node.name,
node.output_type,
node.execute_flow,
strm_t,
strm_t,
node.output_type,
),
&node.loc,
);
}
(_, Cardinality::Disagreed) => {
self.emit(
format!(
"axon-W003 axonendpoint '{}' executes flow '{}' \
whose tail is an `if`/`else` (or `par`) where \
the branches disagree on cardinality — one \
branch returns a singular value while another \
returns a list (or stream). The endpoint's \
`output: {}` cannot satisfy both shapes \
simultaneously. Either: \
(a) align the branches — return the same \
cardinality from both; OR \
(b) declare `output: Any` to accept either \
shape (degraded type safety; the runtime \
D5 gate will not protect this endpoint); \
OR \
(c) split into two endpoints, one per branch's \
shape. \
(Fase 38.x.f D6 cardinality_disagreement_in_branches)",
node.name,
node.execute_flow,
node.output_type,
),
&node.loc,
);
}
(Cardinality::Unit, _) | (_, Cardinality::Unit) => {
}
(Cardinality::Wrapped(_), _) | (_, Cardinality::Wrapped(_)) => {
unreachable!(
"§Fase 39 D4 invariant — Wrapped is unwrapped by the \
early-return shortcut at the top of emit_cardinality_gate; \
this match arm should be unreachable."
);
}
}
}
fn find_flow(&self, name: &str) -> Option<&'a FlowDefinition> {
self.program.declarations.iter().find_map(|d| match d {
Declaration::Flow(f) if f.name == name => Some(f),
_ => None,
})
}
fn find_store(&self, name: &str) -> Option<&'a AxonStoreDefinition> {
self.program.declarations.iter().find_map(|d| match d {
Declaration::AxonStore(s) if s.name == name => Some(s),
_ => None,
})
}
fn find_socket(&self, name: &str) -> Option<&'a SocketDefinition> {
self.program.declarations.iter().find_map(|d| match d {
Declaration::Socket(s) if s.name == name => Some(s),
_ => None,
})
}
fn find_session(&self, name: &str) -> Option<&'a SessionDefinition> {
self.program.declarations.iter().find_map(|d| match d {
Declaration::Session(s) if s.name == name => Some(s),
_ => None,
})
}
fn find_cache(&self, name: &str) -> Option<&'a CacheDefinition> {
self.program.declarations.iter().find_map(|d| match d {
Declaration::Cache(c) if c.name == name => Some(c),
_ => None,
})
}
fn check_step_body_statements(&mut self, s: &crate::ast::StepNode, flow_name: &str) {
self.check_flow_steps(&s.pix_ops, flow_name);
if let Some(sb) = &s.stream {
for arm in [&sb.on_chunk, &sb.on_complete, &sb.on_error]
.into_iter()
.flatten()
{
self.check_step_body_statements(arm, flow_name);
}
self.check_flow_steps(&sb.body, flow_name);
}
}
fn check_flow_steps(&mut self, steps: &[FlowStep], flow_name: &str) {
let mut rich_lets_seen: std::collections::HashMap<String, crate::ast::Expr> =
std::collections::HashMap::new();
for step in steps {
match step {
FlowStep::ShieldApply(n) => {
if !n.shield_name.is_empty() {
match self.symbols.lookup(&n.shield_name) {
None => self.emit(
format!(
"Undefined shield '{}' in flow '{}'",
n.shield_name, flow_name
),
&n.loc,
),
Some(sym) if sym.kind != "shield" => self.emit(
format!("'{}' is a {}, not a shield", n.shield_name, sym.kind),
&n.loc,
),
_ => {}
}
}
}
FlowStep::Validate(n) => {
if !n.rule.is_empty() {
match self.symbols.lookup(&n.rule) {
None => self.emit(
format!(
"axon-T1210 `validate … against: {}` in flow '{}' names a \
schema that is not declared. The schema's fields ARE the \
constraints the response is scored against, so an \
unresolved name would score it against nothing and report \
a clean verdict for a check that never happened. Declare \
`type {} {{ … }}`.",
n.rule, flow_name, n.rule
),
&n.loc,
),
Some(sym) if sym.kind != "type" => self.emit(
format!(
"axon-T1210 `validate … against: {}` names a {}, not a \
`type`. A validation is scored against a STRUCTURE — the \
declared fields of a type — and a {} declares none.",
n.rule, sym.kind, sym.kind
),
&n.loc,
),
_ => {}
}
}
if let Some(g) = &n.guard {
if !(g.threshold > 0.0 && g.threshold <= 1.0) {
self.emit(
format!(
"axon-T1211 `if confidence < {}` can{} fire: the \
confidence is a constraint-satisfaction ratio in [0, 1], \
so the floor must satisfy 0 < θ ≤ 1. A guard that cannot \
fire is dead governance that reads as live; one that \
always fires is an unconditional retry loop disguised as \
a conditional.",
g.threshold,
if g.threshold <= 0.0 { " NEVER" } else { " ALWAYS" },
),
&g.loc,
);
}
if g.max_attempts == 0 {
self.emit(
"axon-T1212 `refine(max_attempts: 0)` promises a recovery and \
performs none — the guard would fire, refine nothing, and \
proceed below the floor it declared. Declare at least 1, or \
remove the guard."
.to_string(),
&g.loc,
);
}
}
}
FlowStep::OtsApply(n) => {
if !n.ots_name.is_empty() {
match self.symbols.lookup(&n.ots_name) {
None => self.emit(
format!("Undefined OTS '{}' in flow '{}'", n.ots_name, flow_name),
&n.loc,
),
Some(sym) if sym.kind != "ots" => self.emit(
format!("'{}' is a {}, not an OTS", n.ots_name, sym.kind),
&n.loc,
),
_ => {}
}
}
}
FlowStep::MandateApply(n) => {
if !n.mandate_name.is_empty() {
match self.symbols.lookup(&n.mandate_name) {
None => self.emit(
format!(
"Undefined mandate '{}' in flow '{}'",
n.mandate_name, flow_name
),
&n.loc,
),
Some(sym) if sym.kind != "mandate" => self.emit(
format!("'{}' is a {}, not a mandate", n.mandate_name, sym.kind),
&n.loc,
),
_ => {}
}
}
}
FlowStep::LambdaDataApply(n) => {
if !n.lambda_data_name.is_empty() {
match self.symbols.lookup(&n.lambda_data_name) {
None => self.emit(
format!(
"Undefined lambda '{}' in flow '{}'",
n.lambda_data_name, flow_name
),
&n.loc,
),
Some(sym) if sym.kind != "lambda_data" => self.emit(
format!(
"'{}' is a {}, not a lambda_data",
n.lambda_data_name, sym.kind
),
&n.loc,
),
_ => {}
}
}
if !n.output_type.is_empty()
&& RESERVED_OUTPUT_TYPE_NAMES
.contains(&n.output_type.to_ascii_lowercase().as_str())
{
self.emit(
format!(
"lambda apply output_type '{}' shadows a reserved \
primitive / built-in type name — choose a distinct \
name for the bound envelope",
n.output_type
),
&n.loc,
);
}
}
FlowStep::Let(n) => {
if let Some(e) = &n.value_ast {
rich_lets_seen.insert(n.identifier.clone(), e.clone());
}
if n.identifier.is_empty() {
self.emit(
"let binding requires an identifier".to_string(),
&n.loc,
);
} else {
if RESERVED_OUTPUT_TYPE_NAMES
.contains(&n.identifier.to_ascii_lowercase().as_str())
{
self.emit(
format!(
"let binding '{}' shadows a reserved primitive / \
built-in type name — choose a distinct identifier",
n.identifier
),
&n.loc,
);
}
if n.value_kind == "reference" && !n.value_expr.is_empty() {
let head = n.value_expr.split('.').next().unwrap_or("");
if head == n.identifier {
self.emit(
format!(
"let binding '{}' is self-referential \
(value '{}' starts with the binding name itself) — \
cannot resolve at runtime",
n.identifier, n.value_expr
),
&n.loc,
);
}
}
}
}
FlowStep::Navigate(n) => {
if !n.pix_name.is_empty() {
match self.symbols.lookup(&n.pix_name) {
None => self.emit(
format!("Undefined pix or corpus '{}' in navigate step", n.pix_name),
&n.loc,
),
Some(sym) if sym.kind != "pix" && sym.kind != "corpus" => self.emit(
format!("'{}' is a {}, not a pix or corpus", n.pix_name, sym.kind),
&n.loc,
),
_ => {}
}
}
if n.query_expr.is_empty() {
self.emit(
"Navigate step requires a query expression".to_string(),
&n.loc,
);
}
}
FlowStep::Ingest(n) => {
if n.target.is_empty() {
self.emit(
format!(
"axon-T929 ingest of `{}` names no dataspace. The governed \
form is `ingest <sourceRef> into <Dataspace> {{ format: … }}` \
— a load without a declared destination schema cannot be \
type-checked and is refused.",
n.source
),
&n.loc,
);
} else {
match self.symbols.lookup(&n.target) {
None => self.emit(
format!(
"axon-T929 ingest targets `{}`, which is not declared. \
Declare it: `dataspace {} {{ column <name>: <Type> … }}`.",
n.target, n.target
),
&n.loc,
),
Some(sym) if sym.kind != "dataspace" => self.emit(
format!(
"axon-T929 ingest targets `{}`, which is a {} — an ingest \
loads into a `dataspace` (the analytical store), not a {}.",
n.target, sym.kind, sym.kind
),
&n.loc,
),
_ => {}
}
}
if n.format.is_empty() {
self.emit(
format!(
"axon-T929 ingest into `{}` declares no `format:`. An ingest \
that does not say what it is parsing cannot be deterministic \
— declare `format: csv` or `format: json`.",
if n.target.is_empty() { "<unset>" } else { &n.target }
),
&n.loc,
);
} else if !matches!(n.format.as_str(), "csv" | "json") {
self.emit(
format!(
"axon-T929 ingest declares unknown `format: {}`. The closed \
loader catalog is {{csv, json}} (deterministic, first-party — \
the §100 posture; Parquet/Arrow-IPC are deferred §108.x surface).",
n.format
),
&n.loc,
);
}
if n.max_bytes == Some(0) || n.max_rows == Some(0) {
self.emit(
"axon-T929 ingest declares a zero limit — a bound that admits \
nothing is a declaration error, not a safety measure."
.to_string(),
&n.loc,
);
}
}
FlowStep::Grad(n) => {
if n.wrt.is_empty() {
self.emit(
format!("axon-T932 grad of `{}` names no `wrt` variable — the form is `grad <let> wrt <x>` or `grad <let> wrt [a, b]`.", n.target),
&n.loc,
);
}
match rich_lets_seen.get(&n.target) {
None => {
self.emit(
format!("axon-T932 grad targets `{0}`, which is not a PRIOR rich `let` in this flow. Bind the expression first: `let {0} = <arithmetic expression>` — grad differentiates the declared EXPRESSION, not a runtime value (a numeric derivative of a value is the approximation axon refuses to ship).", n.target),
&n.loc,
);
}
Some(expr) => {
for var in &n.wrt {
if let Err(e) = crate::expr_diff::differentiate(expr, var) {
let position = if e.path.is_empty() {
String::new()
} else {
format!(" (position: {})", e.path)
};
self.emit(
format!("axon-T931 grad of `{}` wrt `{var}`: the expression contains a non-differentiable construct — {}{position}. A gradient over it does not exist; axon does not fabricate one (no silent zeros, no finite-difference approximations). The differentiable fragment is arithmetic (+, -, *, /, negation) over Int/Float and `as_float`.", n.target, e.construct),
&n.loc,
);
}
}
}
}
}
FlowStep::Focus(n) => {
if let Some(msg) = self.dataspace_target_error("focus", &n.expression) {
self.emit(msg, &n.loc);
}
if let Some(cols) = self.dataspace_columns(&n.expression) {
for col in &n.select {
if !cols.iter().any(|(c, _)| c == col) {
self.emit(
format!(
"axon-T930 focus `select` references `{col}`, not a \
column of dataspace `{}`.",
n.expression
),
&n.loc,
);
}
}
}
}
FlowStep::Aggregate(n) => {
if let Some(msg) = self.dataspace_target_error("aggregate", &n.target) {
self.emit(msg, &n.loc);
}
if let Some(cols) = self.dataspace_columns(&n.target) {
for col in &n.group_by {
if !cols.iter().any(|(c, _)| c == col) {
self.emit(
format!(
"axon-T930 aggregate `group_by` references `{col}`, not \
a column of dataspace `{}`.",
n.target
),
&n.loc,
);
}
}
for spec in &n.compute {
let (fname, col) = match spec.find('(') {
Some(p) if spec.ends_with(')') => {
(&spec[..p], Some(spec[p + 1..spec.len() - 1].trim()))
}
None => (spec.as_str(), None),
_ => {
self.emit(
format!("axon-T930 malformed aggregate `{spec}`."),
&n.loc,
);
continue;
}
};
if !matches!(fname, "count" | "sum" | "avg" | "min" | "max") {
self.emit(
format!(
"axon-T930 unknown aggregate `{fname}` — the closed \
catalog is {{count, sum, avg, min, max}}."
),
&n.loc,
);
continue;
}
if fname != "count" && col.is_none() {
self.emit(
format!(
"axon-T930 aggregate `{fname}` requires a column: \
`{fname}(<col>)`."
),
&n.loc,
);
}
if let Some(col) = col {
match cols.iter().find(|(c, _)| c == col) {
None => self.emit(
format!(
"axon-T930 aggregate `{spec}` references `{col}`, \
not a column of dataspace `{}`.",
n.target
),
&n.loc,
),
Some((_, declared_type)) if matches!(fname, "sum" | "avg") => {
let ty = crate::ast::DataspaceColumnType::from_token(
declared_type,
);
if !matches!(
ty,
Some(crate::ast::DataspaceColumnType::Int)
| Some(crate::ast::DataspaceColumnType::Float)
) {
let dt = declared_type.clone();
self.emit(
format!(
"axon-T930 `{spec}` is defined on Int/Float; \
column `{col}` is {dt} (refusal, not \
coercion)."
),
&n.loc,
);
}
}
Some(_) => {}
}
}
}
}
}
FlowStep::Associate(n) => {
if let Some(msg) = self.dataspace_target_error("associate", &n.left) {
self.emit(msg, &n.loc);
}
if let Some(msg) = self.dataspace_target_error("associate", &n.right) {
self.emit(msg, &n.loc);
}
let ldef = self.dataspace_columns(&n.left);
let rdef = self.dataspace_columns(&n.right);
if n.using_field.is_empty() {
self.emit(
"axon-T930 associate declares no `using <column>` — an equi-join \
without a key is a cartesian product, which is refused."
.to_string(),
&n.loc,
);
} else if let (Some(l), Some(r)) = (ldef, rdef) {
let lc = l.iter().find(|(c, _)| c == &n.using_field).cloned();
let rc = r.iter().find(|(c, _)| c == &n.using_field).cloned();
match (lc, rc) {
(Some((_, lt_raw)), Some((_, rt_raw))) => {
let lt = crate::ast::DataspaceColumnType::from_token(<_raw);
let rt = crate::ast::DataspaceColumnType::from_token(&rt_raw);
if lt.is_some() && rt.is_some() && lt != rt {
self.emit(
format!(
"axon-T930 associate `using {}` joins a {} column \
against a {} column (refusal, not coercion).",
n.using_field, lt_raw, rt_raw
),
&n.loc,
);
}
}
_ => self.emit(
format!(
"axon-T930 associate `using {}` — the column must exist in \
BOTH dataspaces (`{}`, `{}`).",
n.using_field, n.left, n.right
),
&n.loc,
),
}
}
}
FlowStep::ExploreStep(n) => {
if let Some(msg) = self.dataspace_target_error("explore", &n.target) {
self.emit(msg, &n.loc);
}
}
FlowStep::Drill(n) => {
if !n.pix_name.is_empty() {
match self.symbols.lookup(&n.pix_name) {
None => self.emit(
format!("Undefined pix '{}' in drill step", n.pix_name),
&n.loc,
),
Some(sym) if sym.kind != "pix" => self.emit(
format!("'{}' is a {}, not a pix", n.pix_name, sym.kind),
&n.loc,
),
_ => {}
}
}
if n.subtree_path.is_empty() {
self.emit("Drill step requires a subtree_path".to_string(), &n.loc);
}
if n.query_expr.is_empty() {
self.emit("Drill step requires a query expression".to_string(), &n.loc);
}
}
FlowStep::Trail(n) => {
if n.navigate_ref.is_empty() {
self.emit("Trail step requires a navigate_ref".to_string(), &n.loc);
}
}
FlowStep::Corroborate(n) => {
self.emit(
format!(
"axon-T937 `corroborate {}` is RETRACTED (§111). It never corroborated \
anything: the runtime interpolated the reference's NAME into an LLM \
prompt, fetched no independent source, read no content, and computed no \
agreement metric — while instructing the model to report 'agreement \
strength'. It manufactured a warrant. Fabricated verification is worse \
than none, because it is believed. To cross-check a claim today, \
`navigate` a second corpus and compare the results explicitly, or gate \
the value behind an `anchor`.",
if n.navigate_ref.is_empty() { "<ref>" } else { &n.navigate_ref }
),
&n.loc,
);
}
FlowStep::Deliberate(n) => {
self.emit(
"axon-T939 `deliberate { … }` is REFUSED (§111). It never controlled a \
budget: the block's body is discarded at PARSE time (it shares \
`parse_block_step` with `stream`/`consensus`/`transact`, whose whole job \
is to skip the braces), so the steps you wrote inside it never reached \
the IR and never ran — while the wire reported the block complete. To \
bound compute today, use a `budget { … }` on the daemon or the declared \
`max_tokens:` / `max_time:` / `max_cost:` ceilings, which are enforced."
.to_string(),
&n.loc,
);
}
FlowStep::Consensus(n) => {
self.emit(
"axon-T940 `consensus { … }` is REFUSED (§111). There is no best-of-N: no \
votes are cast, nothing is aggregated, no candidates are generated. The \
block's body is discarded at PARSE time, so the handler never even \
receives it — a selection over an empty set silently returned nothing \
while the wire reported success. To evaluate alternatives today, run them \
explicitly (e.g. `par { … }`) and select with a `validate` or an `anchor`."
.to_string(),
&n.loc,
);
}
FlowStep::Transact(n) => {
self.emit(
"axon-T938 `transact { … }` is RETRACTED (§111). It never opened a \
transaction: the runtime set an unread marker string, the block's body \
was never lowered into the IR, and no lock was taken and nothing was \
rolled back. Writes inside it were as atomic as writes outside it — which \
is to say, not. A fabricated atomicity guarantee is worse than an absent \
one, because you only discover it on the failure path. Until real \
transactional semantics land (§111.x), issue the writes directly and make \
them idempotent, so a retry converges instead of corrupting."
.to_string(),
&n.loc,
);
}
FlowStep::DaemonStep(n) => {
if !n.daemon_ref.is_empty() {
match self.symbols.lookup(&n.daemon_ref) {
None => self.emit(
format!(
"Undefined daemon '{}' in flow '{}'",
n.daemon_ref, flow_name
),
&n.loc,
),
Some(sym) if sym.kind != "daemon" => self.emit(
format!("'{}' is a {}, not a daemon", n.daemon_ref, sym.kind),
&n.loc,
),
_ => {}
}
}
}
FlowStep::Persist(n) => {
self.check_store_ref(&n.store_name, flow_name, &n.loc);
self.check_secrets_store_write("persist", &n.store_name, flow_name, &n.loc);
self.run_38e_persist_proof(&n.store_name, &n.fields, &n.loc);
for (col, value) in &n.fields {
for binding in self.current_mint_bindings.clone() {
if value == &binding
|| value.contains(&format!("${{{binding}}}"))
|| value.contains(&format!("${binding}"))
{
self.emit(
format!(
"axon-T896 the mint binding '{binding}' flows into \
`persist` field '{col}' in flow '{flow_name}' — a \
minted credential is shown ONCE and never enters a \
store. Return it to the caller instead.",
),
&n.loc,
);
}
}
}
}
FlowStep::Retrieve(n) => {
self.check_store_ref(&n.store_name, flow_name, &n.loc);
self.run_38d_where_proof(&n.store_name, &n.where_expr, &n.loc);
self.run_67b_bounds_proof(
&n.store_name,
&n.order_by,
&n.limit_expr,
&n.loc,
);
self.run_76d_aggregate_proof(
&n.store_name,
&n.aggregate,
&n.group_by,
&n.order_by,
&n.limit_expr,
&n.loc,
);
self.check_retrieve_cache_ref(&n.cache, flow_name, &n.loc);
}
FlowStep::Mutate(n) => {
self.check_store_ref(&n.store_name, flow_name, &n.loc);
self.check_secrets_store_write("mutate", &n.store_name, flow_name, &n.loc);
self.run_38d_where_proof(&n.store_name, &n.where_expr, &n.loc);
self.run_38e_mutate_proof(&n.store_name, &n.fields, &n.loc);
}
FlowStep::Purge(n) => {
self.check_store_ref(&n.store_name, flow_name, &n.loc);
self.check_secrets_store_write("purge", &n.store_name, flow_name, &n.loc);
self.run_38d_where_proof(&n.store_name, &n.where_expr, &n.loc);
}
FlowStep::Rotate(n) => {
match self.symbols.lookup(&n.store_ref) {
None => self.emit(
format!(
"axon-T898 `rotate` targets '{}' in flow '{flow_name}', \
which is not declared — a rotation targets a \
`backend: secrets` metadata store.",
n.store_ref
),
&n.loc,
),
Some(sym) if sym.kind != "axonstore" => self.emit(
format!(
"axon-T898 `rotate` targets '{}' in flow '{flow_name}', \
but it is a {} — a rotation targets a `backend: \
secrets` metadata store.",
n.store_ref, sym.kind
),
&n.loc,
),
Some(_) if !self.secrets_backed_stores.contains(&n.store_ref) => {
self.emit(
format!(
"axon-T898 `rotate` targets the axonstore '{}' in \
flow '{flow_name}', but its backend is not \
`secrets` — rotation is the renewal of CUSTODIED \
authority (`rotation_without_revelation`); an \
adopter table has no custody behind it. Use \
`mutate` for ordinary rows, or declare a \
`backend: secrets` store for the class.",
n.store_ref
),
&n.loc,
);
}
_ => {}
}
match self.symbols.lookup(&n.tool_ref) {
None => self.emit(
format!(
"axon-T899 `rotate … with {}` in flow '{flow_name}' \
references an undeclared tool — the renewal exchange \
is performed by a declared `tool` (it receives the \
current value under the reserved `axon_rotation` \
envelope and returns the renewed one).",
n.tool_ref
),
&n.loc,
),
Some(sym) if sym.kind != "tool" => self.emit(
format!(
"axon-T899 `rotate … with {}` in flow '{flow_name}' \
references a {}, not a tool.",
n.tool_ref, sym.kind
),
&n.loc,
),
_ => {}
}
self.run_38d_where_proof(&n.store_ref, &n.where_expr, &n.loc);
}
FlowStep::ComputeApply(n) => {
if !n.compute_name.is_empty() {
match self.symbols.lookup(&n.compute_name) {
None => self.emit(
format!(
"Undefined compute '{}' in flow '{}'",
n.compute_name, flow_name
),
&n.loc,
),
Some(sym) if sym.kind != "compute" => self.emit(
format!("'{}' is a {}, not a compute", n.compute_name, sym.kind),
&n.loc,
),
_ => {}
}
}
}
FlowStep::If(n) => {
if let Some(cond) = &n.cond {
let scope = self.current_flow_param_spellings.clone();
let _ = self.infer_expr(cond, &scope, &n.loc);
if let Some(cv) = const_fold(cond) {
let always = const_truthy(&cv);
let dead = if always { "else" } else { "then" };
self.warn(
format!(
"axon-W008 condition is always {always} — the `{dead}` \
branch is unreachable (constant expression)"
),
&n.loc,
);
}
} else {
self.check_legacy_lens_path(&n.condition, &n.loc);
self.check_legacy_lens_path(&n.comparison_value, &n.loc);
for (lhs, _op, val) in &n.conditions {
self.check_legacy_lens_path(lhs, &n.loc);
self.check_legacy_lens_path(val, &n.loc);
}
}
self.check_flow_steps(&n.then_body, flow_name);
self.check_flow_steps(&n.else_body, flow_name);
}
FlowStep::ForIn(n) => {
self.check_flow_steps(&n.body, flow_name);
}
FlowStep::Mint(n) => {
match self.symbols.lookup(&n.credential_ref) {
None => self.emit(
format!(
"axon-T895 undefined credential '{}' in flow '{}' — `mint` \
requires a declared `credential {{ ttl: grants: }}` contract",
n.credential_ref, flow_name
),
&n.loc,
),
Some(sym) if sym.kind != "credential" => self.emit(
format!(
"axon-T895 '{}' is a {}, not a credential (referenced by \
`mint` in flow '{}')",
n.credential_ref, sym.kind, flow_name
),
&n.loc,
),
_ => {}
}
self.current_mint_bindings.insert(n.binding.clone());
}
FlowStep::Emit(n) => self.check_emit(n),
FlowStep::Publish(n) => self.check_publish(n),
FlowStep::Discover(n) => self.check_discover(n),
FlowStep::UseTool(n) => {
match self.symbols.lookup(&n.tool_name) {
None => self.emit(
format!("Unknown tool '{}' in flow '{}'", n.tool_name, flow_name),
&n.loc,
),
Some(sym) if sym.kind != "tool" => self.emit(
format!("'{}' is a {}, not a tool", n.tool_name, sym.kind),
&n.loc,
),
_ => {
self.check_use_tool_args(n, steps);
self.check_use_tool_scopes(n);
}
}
}
FlowStep::Step(s) => {
self.check_step_body_statements(s, flow_name);
for g in &s.guards {
if g.name.is_empty() {
continue;
}
let expected_kind =
if g.kind == "lambda" { "lambda_data" } else { g.kind.as_str() };
match self.symbols.lookup(&g.name) {
None => self.emit(
format!(
"Undefined {} '{}' in step '{}' of flow '{}'",
g.kind, g.name, s.name, flow_name
),
&g.loc,
),
Some(sym) if sym.kind != expected_kind => self.emit(
format!(
"'{}' is a {}, not a {} (step '{}' of flow '{}')",
g.name, sym.kind, g.kind, s.name, flow_name
),
&g.loc,
),
_ => {}
}
}
self.check_apply_tool(s);
self.check_apply_compute(s);
self.check_requires_context(s);
if let Some(tz) = &s.now_tz {
self.check_now_tz(tz, "step", &s.name, &s.loc);
}
}
FlowStep::Quant(q) => {
self.check_quant_header(q, flow_name);
self.check_continuous_type_invariant(&q.body, flow_name);
}
FlowStep::Yield(y) => self.emit(
format!(
"axon-E0787 `yield` in flow '{flow_name}' is only valid inside a `quant` \
block — it collapses evolved Hilbert-space amplitudes back to classical \
silicon. Move it into the enclosing `quant {{ … }}`."
),
&y.loc,
),
FlowStep::Run(n) => self.check_run(n),
FlowStep::Listen(l) if !l.body.is_empty() => {
self.check_flow_steps(&l.body, flow_name);
}
FlowStep::Forge(n) => self.check_forge(n, flow_name),
FlowStep::Warden(n) => self.check_warden(n, flow_name),
_ => {}
}
}
}
fn check_observable(&mut self, n: &ObservableDefinition) {
if n.terms.is_empty() {
self.emit(
format!(
"axon-E0785 observable '{}' has no terms — a Pauli-sum M = Σ cₖ Pₖ needs at \
least one term.",
n.name
),
&n.loc,
);
return;
}
let mut width: Option<usize> = None;
for term in &n.terms {
if term.pauli.is_empty() {
self.emit(
format!("axon-E0785 observable '{}' has an empty Pauli string.", n.name),
&term.loc,
);
continue;
}
if let Some(bad) = term.pauli.chars().find(|c| !matches!(c, 'I' | 'X' | 'Y' | 'Z')) {
self.emit(
format!(
"axon-E0785 observable '{}': Pauli string '{}' contains '{}' — the closed \
alphabet is {{I, X, Y, Z}} (one Pauli per qubit).",
n.name, term.pauli, bad
),
&term.loc,
);
}
let len = term.pauli.chars().count();
match width {
None => width = Some(len),
Some(w) if w != len => self.emit(
format!(
"axon-E0785 observable '{}': Pauli string '{}' has length {} but an \
earlier term has length {} — every term must span the same register.",
n.name, term.pauli, len, w
),
&term.loc,
),
_ => {}
}
}
if let (Some(q), Some(w)) = (n.qubits, width) {
if q as usize != w {
self.emit(
format!(
"axon-E0785 observable '{}' declares qubits: {} but its Pauli strings span \
{} qubit(s).",
n.name, q, w
),
&n.loc,
);
}
}
}
fn check_witness(&mut self, n: &WitnessDefinition) {
if n.claim.is_empty() {
self.emit(
format!(
"axon-E0790 witness '{}' has no `claim:` — name the primitive whose \
advantage you are witnessing.",
n.name
),
&n.loc,
);
}
if n.baseline.is_empty() {
self.emit(
format!(
"axon-E0790 witness '{}' has no `against:` baseline — advantage is always \
relative to a cheaper alternative (e.g. `cosine`).",
n.name
),
&n.loc,
);
}
if n.metric.is_empty() {
self.emit(
format!(
"axon-E0790 witness '{}' has no `metric:` — choose one of {{{}}}.",
n.name,
WITNESS_METRICS.join(", ")
),
&n.loc,
);
} else if !WITNESS_METRICS.contains(&n.metric.as_str()) {
self.emit(
format!(
"axon-E0790 witness '{}': metric '{}' is not in the closed catalog {{{}}}.",
n.name,
n.metric,
WITNESS_METRICS.join(", ")
),
&n.loc,
);
}
if !(n.threshold.is_finite() && n.threshold >= 0.0) {
self.emit(
format!(
"axon-E0790 witness '{}': threshold must be a finite value ≥ 0 (the minimum \
advantage that justifies the cost), got {}.",
n.name, n.threshold
),
&n.loc,
);
}
if n.data.is_empty() {
self.emit(
format!(
"axon-E0790 witness '{}' has no `data:` — advantage cannot be claimed in the \
abstract; it is witnessed on a real-data source.",
n.name
),
&n.loc,
);
}
}
fn check_quant_header(&mut self, q: &QuantBlock, flow_name: &str) {
if let Some(enc) = &q.encoding {
if enc != "amplitude" && enc != "angle" {
self.emit(
format!(
"axon-E0784 quant block in flow '{flow_name}': unknown encoding scheme \
'{enc}' — the closed set is 'amplitude' (O(log d) qubits, O(d) \
state-preparation depth) or 'angle' (O(1) depth, d=n features)."
),
&q.loc,
);
} else {
let note = if enc == "amplitude" {
"axon-W005 quant encoding 'amplitude' compresses d features into O(log d) \
qubits but costs O(d) state-preparation depth (D2); for low-depth \
robustness to scale noise, 'angle' encoding trades to O(1) depth with d=n \
features."
} else {
"axon-W005 quant encoding 'angle' has O(1) state-preparation depth but \
represents only d=n features (one per qubit, D2); for exponential feature \
compression use 'amplitude'."
};
self.warn(note.to_string(), &q.loc);
}
}
if let Some(obs) = &q.observable {
match self.symbols.lookup(obs) {
None => self.emit(
format!(
"axon-E0784 quant block in flow '{flow_name}': undefined observable \
'{obs}' — declare it with `observable {obs} {{ … }}`."
),
&q.loc,
),
Some(sym) if sym.kind != "observable" => self.emit(
format!(
"axon-E0784 quant block in flow '{flow_name}': '{obs}' is a {}, not an \
observable.",
sym.kind
),
&q.loc,
),
_ => {}
}
}
if !is_valid(&q.effect, crate::ots_catalog::QUANT_BACKEND_CATALOG) {
self.emit(
format!(
"axon-E0784 quant block in flow '{flow_name}': unknown backend '{}' — \
expected one of {} (the algebraic effect performed is \
'ots:backend:<backend>').",
q.effect,
valid_list(crate::ots_catalog::QUANT_BACKEND_CATALOG)
),
&q.loc,
);
}
if let Some(n) = q.qubits {
if n < 1 {
self.emit(
format!(
"axon-E0784 quant block in flow '{flow_name}': qubits must be >= 1, got {n}."
),
&q.loc,
);
}
}
if let Some(d) = q.depth {
if d < 1 {
self.emit(
format!(
"axon-E0784 quant block in flow '{flow_name}': circuit depth must be >= 1, \
got {d}."
),
&q.loc,
);
}
}
if let Some(b) = q.bandwidth {
if b <= 0.0 {
self.emit(
format!(
"axon-E0784 quant block in flow '{flow_name}': projected-kernel bandwidth \
must be > 0, got {b}."
),
&q.loc,
);
}
}
if let Some(r) = q.reupload {
if r < 1 {
self.emit(
format!(
"axon-E0784 quant block in flow '{flow_name}': reupload must be >= 1 \
(1 = no re-uploading; >= 2 interleaves the data encoding L times), got {r}."
),
&q.loc,
);
}
}
}
fn check_continuous_type_invariant(&mut self, body: &[FlowStep], flow_name: &str) {
for step in body {
match step {
FlowStep::Let(n) => {
if Self::quant_value_is_string_literal(&n.value_kind, &n.value_expr) {
self.emit(
format!(
"axon-E0782 Continuous Type Invariant violation in flow \
'{flow_name}': let binding '{}' inside a `quant` block holds a \
non-continuous 'String' (string literal). Discrete/conversational \
types are prohibited in the Hilbert-space scope — keep the original \
tensor via the continuous type 'SymbolicPtr[Tensor[Float32]]'.",
n.identifier
),
&n.loc,
);
} else if n.value_expr.contains(".to_string") {
self.emit(
format!(
"axon-E0782 Continuous Type Invariant violation in flow \
'{flow_name}': let binding '{}' inside a `quant` block performs an \
implicit textual conversion ('.to_string'). Textual leaks collapse \
the continuous gradient — operate on the tensor / density-matrix \
carrier instead.",
n.identifier
),
&n.loc,
);
}
if let Some(ty) = &n.type_annotation {
self.check_density_matrix_dim(ty, flow_name, &n.loc);
if matches!(ty.name.as_str(), "String" | "Text") {
self.emit(
format!(
"axon-E0782 Continuous Type Invariant violation in flow \
'{flow_name}': let binding '{}' is typed '{}' inside a `quant` \
block. Discrete/conversational types collapse the continuous \
gradient — use a continuous carrier \
('SymbolicPtr[Tensor[Float32]]', 'DensityMatrix[D]').",
n.identifier, ty.name
),
&n.loc,
);
}
}
}
FlowStep::Step(s) if !s.ask.is_empty() => {
self.emit(
format!(
"axon-E0782 Continuous Type Invariant violation in flow '{flow_name}': \
a `step` with a free-text `ask:` prompt is not permitted inside a \
`quant` block. A conversational LLM call reintroduces unstructured \
text into the Hilbert-space scope; perform cognition outside the \
`quant` block and pass only the continuous tensor in."
),
&s.loc,
);
}
FlowStep::If(n) => {
self.check_continuous_type_invariant(&n.then_body, flow_name);
self.check_continuous_type_invariant(&n.else_body, flow_name);
}
FlowStep::ForIn(n) => self.check_continuous_type_invariant(&n.body, flow_name),
FlowStep::Par(n) => {
for branch in &n.branches {
self.check_continuous_type_invariant(branch, flow_name);
}
}
FlowStep::Quant(q) => self.check_continuous_type_invariant(&q.body, flow_name),
_ => {}
}
}
}
fn check_density_matrix_dim(&mut self, ty: &TypeExpr, flow_name: &str, loc: &Loc) {
if ty.name != "DensityMatrix" {
return;
}
if let Ok(d) = ty.generic_param.trim().parse::<u64>() {
if d == 0 || (d & (d - 1)) != 0 {
self.emit(
format!(
"axon-E0786 quant block in flow '{flow_name}': DensityMatrix dimension {d} \
is not a power of two — D must equal 2ⁿ (the Hilbert-space dimension for \
n qubits, e.g. 2, 4, …, 1024)."
),
loc,
);
}
}
}
fn quant_value_is_string_literal(value_kind: &str, value_expr: &str) -> bool {
if value_kind != "literal" {
return false;
}
let v = value_expr.trim();
if v.parse::<f64>().is_ok() {
return false; }
if v == "true" || v == "false" {
return false; }
if v.starts_with('[') {
return false; }
true }
fn tool_required_scopes(&self, name: &str) -> Vec<String> {
self.program
.declarations
.iter()
.find_map(|d| match d {
Declaration::Tool(t) if t.name == name => Some(t.requires.clone()),
_ => None,
})
.unwrap_or_default()
}
fn granted_scopes(&self) -> std::collections::HashSet<String> {
let mut granted = std::collections::HashSet::new();
for d in &self.program.declarations {
match d {
Declaration::Credential(c) => granted.extend(c.grants.iter().cloned()),
Declaration::AxonEndpoint(e) => {
granted.extend(e.requires_capabilities.iter().cloned())
}
Declaration::Daemon(dm) => {
granted.extend(dm.requires_capabilities.iter().cloned())
}
_ => {}
}
}
granted
}
fn check_use_tool_scopes(&mut self, n: &UseToolStep) {
let required = self.tool_required_scopes(&n.tool_name);
if required.is_empty() {
return;
}
let granted = self.granted_scopes();
for scope in required {
if !granted.contains(&scope) {
self.emit(
format!(
"axon-T956 tool '{}' requires scope '{}', which the program's granted \
set does not cover. A tool's `requires:` scope must be held: declare a \
`credential` that grants '{}', or add it to the executing endpoint's \
`requires:` capabilities. Held scopes come from `credential.grants` \
(§92) and endpoint/daemon `requires:` capabilities (§51.x).",
n.tool_name, scope, scope
),
&n.loc,
);
}
}
}
fn check_use_tool_args(&mut self, n: &UseToolStep, steps: &[FlowStep]) {
let UseArgs::Named(pairs) = &n.args else {
return; };
let params = self.tool_parameters(&n.tool_name);
if params.is_empty() {
return; }
let mut seen = std::collections::HashSet::new();
for (name, value, value_kind) in pairs {
if !seen.insert(name.clone()) {
self.emit(
format!("Duplicate argument '{}' in call to tool '{}'", name, n.tool_name),
&n.loc,
);
continue;
}
match params.iter().find(|p| &p.0 == name) {
None => self.emit(
format!("Tool '{}' has no parameter '{}'", n.tool_name, name),
&n.loc,
),
Some((_, decl_ty, _)) => {
if value_kind == "reference" {
if let Some(src_ty) = self.resolve_reference_type(value, steps) {
let src_base =
src_ty.trim_end_matches('?').split('<').next().unwrap_or(&src_ty);
if !Self::tool_arg_types_align(src_base, decl_ty) {
self.emit(
format!(
"Type mismatch for parameter '{}' of tool '{}': expected {}, got {} (from reference '{}')",
name, n.tool_name, decl_ty, src_ty, value
),
&n.loc,
);
}
}
} else if let Some(val_ty) = Self::infer_arg_literal_type(value) {
if !Self::tool_arg_types_align(&val_ty, decl_ty) {
self.emit(
format!(
"Type mismatch for parameter '{}' of tool '{}': expected {}, got {}",
name, n.tool_name, decl_ty, val_ty
),
&n.loc,
);
}
}
}
}
}
for (pname, _ty, optional) in ¶ms {
if !optional && !pairs.iter().any(|(name, _, _)| name == pname) {
self.emit(
format!("Missing required argument '{}' for tool '{}'", pname, n.tool_name),
&n.loc,
);
}
}
}
fn resolve_reference_type(&self, reference: &str, steps: &[FlowStep]) -> Option<String> {
if let Some(t) = self.current_flow_params.get(reference) {
return Some(t.to_string());
}
let step_name = reference.strip_suffix(".output").unwrap_or(reference);
steps.iter().find_map(|s| match s {
FlowStep::Step(st) if st.name == step_name && !st.output_type.is_empty() => {
Some(st.output_type.clone())
}
FlowStep::UseTool(u) if u.tool_name == step_name => {
self.tool_output_type(&u.tool_name)
}
_ => None,
})
}
fn tool_output_type(&self, tool_name: &str) -> Option<String> {
self.program.declarations.iter().find_map(|d| match d {
Declaration::Tool(t) if t.name == tool_name => t.output_type.clone(),
_ => None,
})
}
fn tool_parameters(&self, tool_name: &str) -> Vec<(String, String, bool)> {
self.program
.declarations
.iter()
.find_map(|d| match d {
Declaration::Tool(t) if t.name == tool_name => Some(t),
_ => None,
})
.map(|t| {
t.parameters
.iter()
.map(|p| {
let mut ty = p.type_expr.name.clone();
if !p.type_expr.generic_param.is_empty() {
ty = format!("{}<{}>", ty, p.type_expr.generic_param);
}
(p.name.clone(), ty, p.type_expr.optional)
})
.collect()
})
.unwrap_or_default()
}
fn infer_arg_literal_type(value: &str) -> Option<String> {
if value == "true" || value == "false" {
return Some("Bool".to_string());
}
if value.contains('.') && value.parse::<f64>().is_ok() {
return Some("Float".to_string());
}
let digits = value.strip_prefix('-').unwrap_or(value);
if !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit()) {
return Some("Int".to_string());
}
None
}
fn tool_arg_types_align(value_ty: &str, decl_ty: &str) -> bool {
let base = decl_ty
.trim_end_matches('?')
.split('<')
.next()
.unwrap_or(decl_ty);
base == "Any" || base == value_ty || (base == "Float" && value_ty == "Int")
}
fn check_apply_tool(&mut self, step: &StepNode) {
if step.apply_ref.is_empty() {
return;
}
match self.symbols.lookup(&step.apply_ref) {
Some(sym) if sym.kind == "tool" => {}
_ => return, }
let params = self.tool_parameters(&step.apply_ref);
if params.is_empty() {
return; }
self.warn(build_w004_message(&step.apply_ref, ¶ms), &step.loc);
}
fn check_apply_compute(&mut self, step: &StepNode) {
if step.apply_ref.is_empty() {
return;
}
if let Some(sym) = self.symbols.lookup(&step.apply_ref) {
if sym.kind == "compute" {
self.warn(build_w006_message(&step.apply_ref, &step.name), &step.loc);
}
}
}
fn check_requires_context(&mut self, step: &StepNode) {
let Some(n) = step.requires_context else {
return;
};
if n == 0 {
self.emit(
format!(
"axon-T809 step '{}' declares `requires_context: 0` — a context \
requirement must be a positive token count (or omit it to use the \
backend default).",
step.name
),
&step.loc,
);
} else if n > MAX_KNOWN_CONTEXT_WINDOW {
self.emit(
format!(
"axon-T809 step '{}' declares `requires_context: {n}`, which exceeds \
the largest known model context window ({MAX_KNOWN_CONTEXT_WINDOW} \
tokens) — no model could satisfy it. Lower the requirement.",
step.name
),
&step.loc,
);
}
}
fn check_secrets_store_write(
&mut self,
verb: &str,
store_name: &str,
flow_name: &str,
loc: &Loc,
) {
if !store_name.is_empty() && self.secrets_backed_stores.contains(store_name) {
self.emit(
format!(
"axon-T897 `{verb}` targets the secrets store '{store_name}' in \
flow '{flow_name}' — a `backend: secrets` store is a READ-ONLY \
metadata view over the tenant's secret custody \
(`rotation_without_revelation`). Custody is written only by the \
tenant-secrets API (seed) and by `rotate … with <Tool>` (renewal); \
`retrieve` is the only verb that reads it."
),
loc,
);
}
}
fn check_store_ref(&mut self, store_name: &str, flow_name: &str, loc: &Loc) {
if !store_name.is_empty() {
match self.symbols.lookup(store_name) {
None => self.emit(
format!(
"Undefined axonstore '{}' in flow '{}'",
store_name, flow_name
),
loc,
),
Some(sym) if sym.kind != "axonstore" => self.emit(
format!("'{}' is a {}, not an axonstore", store_name, sym.kind),
loc,
),
_ => {}
}
}
}
fn run_38d_where_proof(&mut self, store_name: &str, where_expr: &str, loc: &Loc) {
if store_name.is_empty() || where_expr.trim().is_empty() {
return;
}
let cs = match self.store_inline_column_sets.get(store_name) {
Some(cs) => cs.clone(),
None => return, };
let errors = crate::store_column_proof::check_filter(
where_expr,
&cs,
&self.current_flow_params,
(loc.line, loc.column),
);
for err in errors {
self.emit(err.message, loc);
}
}
fn run_67b_bounds_proof(
&mut self,
store_name: &str,
order_by: &str,
limit_expr: &str,
loc: &Loc,
) {
if store_name.is_empty()
|| (order_by.trim().is_empty() && limit_expr.trim().is_empty())
{
return;
}
let cs = self.store_inline_column_sets.get(store_name).cloned();
let errors = crate::store_column_proof::check_bounds(
order_by,
limit_expr,
cs.as_ref(),
&self.current_flow_params,
(loc.line, loc.column),
);
for err in errors {
self.emit(err.message, loc);
}
}
fn run_76d_aggregate_proof(
&mut self,
store_name: &str,
aggregate: &str,
group_by: &str,
order_by: &str,
limit_expr: &str,
loc: &Loc,
) {
if store_name.is_empty()
|| (aggregate.trim().is_empty() && group_by.trim().is_empty())
{
return;
}
let cs = self.store_inline_column_sets.get(store_name).cloned();
let errors = crate::store_column_proof::check_aggregate(
aggregate,
group_by,
order_by,
limit_expr,
cs.as_ref(),
(loc.line, loc.column),
);
for err in errors {
self.emit(err.message, loc);
}
}
fn run_38e_persist_proof(
&mut self,
store_name: &str,
fields: &[(String, String)],
loc: &Loc,
) {
if store_name.is_empty() || fields.is_empty() {
return;
}
let cs = match self.store_inline_column_sets.get(store_name) {
Some(cs) => cs.clone(),
None => return,
};
let errors = crate::store_column_proof::check_persist_fields(
fields,
&cs,
&self.current_flow_params,
(loc.line, loc.column),
);
for err in errors {
self.emit(err.message, loc);
}
}
fn run_38e_mutate_proof(
&mut self,
store_name: &str,
fields: &[(String, String)],
loc: &Loc,
) {
if store_name.is_empty() || fields.is_empty() {
return;
}
let cs = match self.store_inline_column_sets.get(store_name) {
Some(cs) => cs.clone(),
None => return,
};
let errors = crate::store_column_proof::check_mutate_fields(
fields,
&cs,
&self.current_flow_params,
(loc.line, loc.column),
);
for err in errors {
self.emit(err.message, loc);
}
}
fn check_json_lenses(&mut self, decls: &[Declaration]) {
for decl in decls {
match decl {
Declaration::Type(t) => {
for f in &t.fields {
self.check_json_lens_annotation(&f.type_expr);
}
}
Declaration::Flow(fl) => {
for p in &fl.parameters {
self.check_json_lens_annotation(&p.type_expr);
}
if let Some(rt) = &fl.return_type {
self.check_json_lens_annotation(rt);
}
}
Declaration::AxonStore(s) => {
if let Some(crate::store_schema::StoreColumnSchema::Inline {
columns,
..
}) = &s.column_schema
{
for c in columns {
if let Some(shape) = &c.json_shape {
let loc = Loc {
line: c.line,
column: c.column,
};
self.validate_json_shape(shape, &loc);
}
}
}
}
Declaration::Epistemic(eb) => self.check_json_lenses(&eb.body),
_ => {}
}
}
}
fn check_json_lens_annotation(&mut self, ty: &TypeExpr) {
if ty.name == "Json" && !ty.generic_param.is_empty() {
self.validate_json_shape(&ty.generic_param, &ty.loc);
}
}
fn validate_json_shape(&mut self, shape: &str, loc: &Loc) {
let t = shape.trim();
let is_declared_struct = self
.symbols
.lookup(t)
.map_or(false, |s| s.kind == "type");
if is_declared_struct {
return;
}
let why = match self.symbols.lookup(t) {
Some(sym) => format!("`{t}` is a {}, not a `type`", sym.kind),
None => format!("`{t}` is not declared"),
};
self.emit(
format!(
"axon-T840 the shape lens `Json<{t}>` requires `{t}` to be a \
declared `type` (a struct whose fields are the document's \
expected shape), but {why}. Declare `type {t} {{ … }}`, or \
use open `Json` (no shape) when the document's shape is not \
known — open navigation stays total either way."
),
loc,
);
}
fn collect_emitted_channels(&mut self, decls: &[Declaration]) {
fn walk(steps: &[FlowStep], out: &mut std::collections::HashSet<String>) {
for step in steps {
match step {
FlowStep::Emit(e) => {
out.insert(e.channel_ref.clone());
}
FlowStep::If(c) => {
walk(&c.then_body, out);
walk(&c.else_body, out);
}
FlowStep::ForIn(f) => walk(&f.body, out),
FlowStep::Listen(l) => walk(&l.body, out),
_ => {}
}
}
}
let mut out = std::collections::HashSet::new();
for decl in decls {
match decl {
Declaration::Flow(f) => walk(&f.body, &mut out),
Declaration::Daemon(d) => {
for l in &d.listeners {
walk(&l.body, &mut out);
}
}
Declaration::Epistemic(eb) => {
self.collect_emitted_channels(&eb.body);
}
_ => {}
}
}
self.emitted_channels.extend(out);
}
fn index_type_fields(&mut self, decls: &[Declaration]) {
for decl in decls {
match decl {
Declaration::Type(t) => {
let mut fields = std::collections::HashMap::new();
for f in &t.fields {
fields.insert(
f.name.clone(),
(f.type_expr.name.clone(), f.type_expr.generic_param.clone()),
);
}
self.json_lens_fields.insert(t.name.clone(), fields);
}
Declaration::Epistemic(eb) => self.index_type_fields(&eb.body),
_ => {}
}
}
}
fn parse_json_lens(spelling: &str) -> Option<String> {
let s = spelling.trim().trim_end_matches('?').trim();
let inner = s.strip_prefix("Json<")?.strip_suffix('>')?.trim();
if inner.is_empty() {
None
} else {
Some(inner.to_string())
}
}
fn lens_shape_of(
&self,
e: &Expr,
scope: &std::collections::BTreeMap<String, String>,
) -> Option<String> {
match e {
Expr::Ref(name) => Self::parse_json_lens(scope.get(name)?),
Expr::Field(base, field) => {
let parent = self.lens_shape_of(base, scope)?;
let (ty, generic) = self.json_lens_fields.get(&parent)?.get(field)?;
if ty == "Json" && !generic.is_empty() {
Some(generic.clone())
} else if self.json_lens_fields.contains_key(ty) {
Some(ty.clone())
} else {
None
}
}
_ => None,
}
}
fn lens_field_walk(
&mut self,
mut struct_name: String,
segments: &[&str],
loc: &Loc,
) -> InferType {
use InferType as T;
for (i, f) in segments.iter().enumerate() {
let entry = self
.json_lens_fields
.get(&struct_name)
.and_then(|m| m.get(*f))
.map(|(ty, g)| (ty.clone(), g.clone()));
let (ty, generic) = match entry {
Some(e) => e,
None => {
self.emit(
format!(
"axon-T842 the lens `Json<{struct_name}>` declares no field \
`{f}`. The shape is a checkable EXPECTATION — navigating an \
undeclared field is a likely typo (runtime navigation stays \
total → a real document's extra field still reads as null \
here). Add `{f}` to `type {struct_name}`, or drop the shape \
to navigate the open `Json` freely."
),
loc,
);
return T::Unknown;
}
};
if i == segments.len() - 1 {
return infer_type_from_name(&ty);
}
if ty == "Json" && !generic.is_empty() {
struct_name = generic;
} else if self.json_lens_fields.contains_key(&ty) {
struct_name = ty;
} else {
return T::Unknown;
}
}
T::Unknown
}
fn check_legacy_lens_path(&mut self, path: &str, loc: &Loc) {
let p = path.trim();
let (root, rest) = match p.split_once('.') {
Some(rt) => rt,
None => return,
};
let struct_name = match self
.current_flow_param_spellings
.get(root)
.and_then(|s| Self::parse_json_lens(s))
{
Some(s) => s,
None => return,
};
let segments: Vec<&str> = rest.split('.').collect();
let _ = self.lens_field_walk(struct_name, &segments, loc);
}
fn check_type_reference(&self, type_name: &str, _loc: &Loc) -> bool {
if type_name.is_empty() {
return true;
}
let builtin = epistemic::builtin_types();
if builtin.contains(type_name) {
return true;
}
if self
.symbols
.lookup(type_name)
.map_or(false, |s| s.kind == "type")
{
return true;
}
true
}
fn check_epistemic_mode(&mut self, mode: &str, loc: &Loc) {
const VALID_EPISTEMIC_MODES: &[&str] = &["believe", "doubt", "know", "speculate"];
if !mode.is_empty() && !is_valid(mode, VALID_EPISTEMIC_MODES) {
self.emit(
format!(
"Unknown epistemic mode '{}'. Valid: {}",
mode,
valid_list(VALID_EPISTEMIC_MODES)
),
loc,
);
}
}
fn check_channel(&mut self, node: &ChannelDefinition) {
if node.name.is_empty() {
self.emit("channel requires a name".to_string(), &node.loc);
}
if node.message.is_empty() {
self.emit(
"channel requires a `message:` schema type".to_string(),
&node.loc,
);
} else {
self.validate_channel_message_type(&node.message, &node.loc);
}
if !node.shield_ref.is_empty() {
match self.symbols.lookup(&node.shield_ref) {
None => self.emit(
format!(
"channel '{}' references undefined shield '{}'",
node.name, node.shield_ref
),
&node.loc,
),
Some(sym) if sym.kind != "shield" => self.emit(
format!(
"channel '{}' shield '{}' is a {}, not a shield",
node.name, node.shield_ref, sym.kind
),
&node.loc,
),
_ => {}
}
}
}
fn validate_channel_message_type(&mut self, spelling: &str, _loc: &Loc) {
let s = spelling.trim();
if s.starts_with("Channel<") && s.ends_with('>') {
let inner = &s["Channel<".len()..s.len() - 1];
self.validate_channel_message_type(inner, _loc);
return;
}
}
fn check_daemon(&mut self, node: &DaemonDefinition) {
if !node.shield_ref.is_empty() {
match self.symbols.lookup(&node.shield_ref) {
None => self.emit(
format!(
"daemon '{}' references undefined shield '{}'",
node.name, node.shield_ref
),
&node.loc,
),
Some(sym) if sym.kind != "shield" => self.emit(
format!(
"daemon '{}' shield '{}' is a {}, not a shield",
node.name, node.shield_ref, sym.kind
),
&node.loc,
),
_ => {}
}
}
if !node.window_ref.is_empty() {
match self.symbols.lookup(&node.window_ref) {
None => self.emit(
format!(
"axon-T825 daemon '{}' binds undefined window '{}' — \
`window:` must name a declared `window` primitive.",
node.name, node.window_ref
),
&node.loc,
),
Some(sym) if sym.kind != "window" => self.emit(
format!(
"axon-T825 daemon '{}' `window:` references '{}', which is a {}, \
not a window.",
node.name, node.window_ref, sym.kind
),
&node.loc,
),
_ => {}
}
}
if let Some(budget) = &node.budget {
self.check_budget(budget, &node.name);
}
for listener in &node.listeners {
self.check_listen(listener, &node.name);
}
let has_cron = node
.listeners
.iter()
.any(|l| crate::cron::cron_expr(&l.channel).is_some());
if has_cron && node.requires_capabilities.is_empty() {
self.emit(
format!(
"axon-E0791 daemon '{}' has a cron-scheduled listener but declares no \
`requires:` capability scope — a standing scheduled privilege must be \
explicit. Add `requires: [<cap>, …]` (e.g. `requires: [flow.execute]`) so \
each run executes under a least-privilege principal.",
node.name
),
&node.loc,
);
}
}
fn warn_daemon_listener_never_fires(&mut self, daemon_name: &str, channel: &str, loc: &Loc) {
if self.emitted_channels.contains(channel) {
return;
}
self.warn(
format!(
"axon-W009 daemon '{daemon_name}' listens on '{channel}', but NOTHING \
emits to it — there is no `emit {channel}(…)` anywhere in the program, so \
this listener can NEVER fire (it waits for an event no producer raises). \
Add a producer (`emit {channel}(payload)` in a flow), or remove the \
listener. (§74 delivers a listener that HAS a producer.)"
),
loc,
);
}
fn check_listen(&mut self, node: &ListenStep, daemon_name: &str) {
if node.channel_is_ref {
match self.symbols.lookup(&node.channel) {
None => self.emit(
format!(
"daemon '{}' listens on undefined channel '{}'",
daemon_name, node.channel
),
&node.loc,
),
Some(sym) if sym.kind != "channel" => self.emit(
format!(
"daemon '{}' listen target '{}' is a {}, not a channel",
daemon_name, node.channel, sym.kind
),
&node.loc,
),
_ => self.warn_daemon_listener_never_fires(daemon_name, &node.channel, &node.loc),
}
} else if let Some(expr) = crate::cron::cron_expr(&node.channel) {
match crate::cron::CronSchedule::parse(expr) {
Err(e) => self.emit(
format!(
"axon-E0789 daemon '{}' has a malformed cron schedule \
'{}': {}",
daemon_name, node.channel, e
),
&node.loc,
),
Ok(_) => {
if node.body.is_empty() {
self.emit(
format!(
"axon-E0792 daemon '{}' cron listener '{}' has no \
handler body — a scheduled trigger with no work \
is a no-op; add a `{{ … }}` body with the steps \
to run on each tick",
daemon_name, node.channel
),
&node.loc,
);
}
}
}
} else {
self.warn_daemon_listener_never_fires(daemon_name, &node.channel, &node.loc);
}
if !node.body.is_empty() {
self.check_flow_steps(&node.body, daemon_name);
}
}
fn check_emit(&mut self, node: &EmitStatement) {
if node.channel_ref.is_empty() {
self.emit("emit requires a channel reference".to_string(), &node.loc);
return;
}
let kind = match self.symbols.lookup(&node.channel_ref) {
None => {
self.emit(
format!("emit references undefined channel '{}'", node.channel_ref),
&node.loc,
);
return;
}
Some(sym) => sym.kind.clone(),
};
if kind != "channel" {
self.emit(
format!(
"emit target '{}' is a {}, not a channel",
node.channel_ref, kind
),
&node.loc,
);
return;
}
if node.value_ref.is_empty() {
self.emit(
format!("emit on channel '{}' requires a value", node.channel_ref),
&node.loc,
);
return;
}
if node.value_ref.contains('.') {
return;
}
let outer_msg = self.find_channel_message(&node.channel_ref);
if let Some(outer) = outer_msg {
if outer.starts_with("Channel<") && outer.ends_with('>') {
let inner = &outer["Channel<".len()..outer.len() - 1];
let value_kind = self
.symbols
.lookup(&node.value_ref)
.map(|s| s.kind.clone())
.unwrap_or_default();
if value_kind != "channel" {
self.emit(
format!(
"emit on '{}' carries '{}' but value '{}' is not a \
channel handle (mobility violation, Chan-Mobility paper §3.2)",
node.channel_ref, outer, node.value_ref
),
&node.loc,
);
return;
}
let value_msg = self
.find_channel_message(&node.value_ref)
.unwrap_or_default();
if value_msg != inner {
self.emit(
format!(
"emit on '{}' expects Channel<{}> but '{}' carries \
Channel<{}> (second-order schema mismatch)",
node.channel_ref, inner, node.value_ref, value_msg
),
&node.loc,
);
}
}
}
}
fn check_publish(&mut self, node: &PublishStatement) {
if node.channel_ref.is_empty() {
self.emit(
"publish requires a channel reference".to_string(),
&node.loc,
);
return;
}
if node.shield_ref.is_empty() {
self.emit(
format!(
"publish '{}' requires a shield gate (D8 — capability \
extrusion is shield-mediated)",
node.channel_ref
),
&node.loc,
);
return;
}
let ch_kind = match self.symbols.lookup(&node.channel_ref) {
None => {
self.emit(
format!(
"publish references undefined channel '{}'",
node.channel_ref
),
&node.loc,
);
return;
}
Some(sym) => sym.kind.clone(),
};
if ch_kind != "channel" {
self.emit(
format!(
"publish target '{}' is a {}, not a channel",
node.channel_ref, ch_kind
),
&node.loc,
);
return;
}
let sh_kind = match self.symbols.lookup(&node.shield_ref) {
None => {
self.emit(
format!(
"axon-T847 publish '{}' references undefined shield '{}'",
node.channel_ref, node.shield_ref
),
&node.loc,
);
return;
}
Some(sym) => sym.kind.clone(),
};
if sh_kind != "shield" {
self.emit(
format!(
"axon-T847 publish gate '{}' is a {}, not a shield",
node.shield_ref, sh_kind
),
&node.loc,
);
return;
}
if let Some(shield) = find_shield_by_name(self.program, &node.shield_ref) {
if !shield.sign.is_empty() {
let persistence = self
.find_channel_persistence(&node.channel_ref)
.unwrap_or_default();
if persistence != "persistent_axonstore" {
self.emit(
format!(
"axon-T848 publish '{}' within '{}' declares SIGNED egress \
(`sign: {}`), but the channel's persistence is '{}' — signed \
egress requires `persistence: persistent_axonstore` so the \
delivery promise survives a restart (it inherits the durable \
outbox's at-least-once). Declare the channel durable, or \
publish within a non-signing shield.",
node.channel_ref,
node.shield_ref,
shield.sign,
if persistence.is_empty() {
"ephemeral (default)"
} else {
persistence.as_str()
},
),
&node.loc,
);
}
}
}
}
fn check_discover(&mut self, node: &DiscoverStatement) {
if node.capability_ref.is_empty() {
self.emit(
"discover requires a channel reference".to_string(),
&node.loc,
);
return;
}
if node.alias.is_empty() {
self.emit(
"discover requires an `as <alias>` binding".to_string(),
&node.loc,
);
return;
}
let kind = match self.symbols.lookup(&node.capability_ref) {
None => {
self.emit(
format!(
"discover references undefined channel '{}'",
node.capability_ref
),
&node.loc,
);
return;
}
Some(sym) => sym.kind.clone(),
};
if kind != "channel" {
self.emit(
format!(
"discover target '{}' is a {}, not a channel",
node.capability_ref, kind
),
&node.loc,
);
return;
}
let shield = self.find_channel_shield(&node.capability_ref);
if shield.as_deref().unwrap_or("").is_empty() {
self.emit(
format!(
"discover '{}' is not publishable: its channel definition \
declares no shield (D8 — only shield-gated channels can \
be discovered)",
node.capability_ref
),
&node.loc,
);
}
}
fn find_channel_message(&self, name: &str) -> Option<String> {
for decl in &self.program.declarations {
if let Declaration::Channel(c) = decl {
if c.name == name {
return Some(c.message.clone());
}
}
}
None
}
fn find_channel_shield(&self, name: &str) -> Option<String> {
for decl in &self.program.declarations {
if let Declaration::Channel(c) = decl {
if c.name == name {
return Some(c.shield_ref.clone());
}
}
}
None
}
fn find_channel_persistence(&self, name: &str) -> Option<String> {
for decl in &self.program.declarations {
if let Declaration::Channel(c) = decl {
if c.name == name {
return Some(c.persistence.clone());
}
}
}
None
}
}
fn lower_session_role(role: &SessionRole) -> SessionType {
let body = lower_session_steps(&role.steps);
if steps_contain_loop(&role.steps) {
SessionType::rec("X", body)
} else {
body
}
}
fn lower_session_steps(steps: &[SessionStep]) -> SessionType {
let Some((first, rest)) = steps.split_first() else {
return SessionType::End;
};
match first.op.as_str() {
"send" => SessionType::send(first.message_type.clone(), lower_session_steps(rest)),
"receive" => SessionType::recv(first.message_type.clone(), lower_session_steps(rest)),
"loop" => SessionType::var("X"),
"end" => SessionType::End,
"select" => SessionType::select(branch_types(&first.branches)),
"branch" => SessionType::branch(branch_types(&first.branches)),
"interrupt" => {
let find = |label: &str| {
first
.branches
.iter()
.find(|b| b.label == label)
.map(|b| lower_session_steps(&b.steps))
.unwrap_or(SessionType::End)
};
SessionType::Interrupt {
signal: crate::session::Payload::new(first.message_type.clone()),
body: Box::new(find("body")),
handler: Box::new(find("handler")),
}
}
"resume" => SessionType::Resume,
_ => lower_session_steps(rest),
}
}
fn branch_types(branches: &[SessionBranch]) -> impl Iterator<Item = (String, SessionType)> + '_ {
branches.iter().map(|b| (b.label.clone(), lower_session_steps(&b.steps)))
}
pub const CALL_INTERRUPT_CAUSES: &[&str] =
&["CallerSpeech", "Dtmf", "SilenceTimeout", "AgentFault"];
fn handler_reaches_exit(steps: &[SessionStep]) -> bool {
match steps.last() {
Some(s) if s.op == "resume" || s.op == "end" => true,
Some(s) if matches!(s.op.as_str(), "select" | "branch") => {
!s.branches.is_empty() && s.branches.iter().all(|b| handler_reaches_exit(&b.steps))
}
_ => false,
}
}
fn steps_contain_loop(steps: &[SessionStep]) -> bool {
steps.iter().any(|s| {
s.op == "loop"
|| (matches!(s.op.as_str(), "select" | "branch" | "interrupt")
&& s.branches.iter().any(|b| steps_contain_loop(&b.steps)))
})
}
fn find_cycles(adjacency: &std::collections::HashMap<String, Vec<String>>) -> Vec<Vec<String>> {
let mut color: std::collections::HashMap<String, &'static str> =
std::collections::HashMap::new();
let mut stack: Vec<String> = Vec::new();
let mut cycles: Vec<Vec<String>> = Vec::new();
fn visit(
n: &str,
adjacency: &std::collections::HashMap<String, Vec<String>>,
color: &mut std::collections::HashMap<String, &'static str>,
stack: &mut Vec<String>,
cycles: &mut Vec<Vec<String>>,
) {
color.insert(n.to_string(), "gray");
stack.push(n.to_string());
let targets = adjacency.get(n).cloned().unwrap_or_default();
for tgt in targets {
match color.get(&tgt).copied() {
Some("gray") => {
if let Some(idx) = stack.iter().position(|s| s == &tgt) {
cycles.push(stack[idx..].to_vec());
}
}
None => visit(&tgt, adjacency, color, stack, cycles),
_ => {}
}
}
stack.pop();
color.insert(n.to_string(), "black");
}
let keys: Vec<String> = adjacency.keys().cloned().collect();
for src in keys {
if !color.contains_key(&src) {
visit(&src, adjacency, &mut color, &mut stack, &mut cycles);
}
}
cycles
}
fn cycle_to_edges<'a>(cycle: &[String], edges: &'a [TopologyEdge]) -> Vec<&'a TopologyEdge> {
let n = cycle.len();
let mut result = Vec::with_capacity(n);
for i in 0..n {
let src = &cycle[i];
let tgt = &cycle[(i + 1) % n];
if let Some(e) = edges.iter().find(|e| &e.source == src && &e.target == tgt) {
result.push(e);
}
}
result
}
fn is_unguarded_recursion(t: &SessionType) -> bool {
matches!(t, SessionType::Rec(name, body)
if matches!(body.as_ref(), SessionType::Var(v) if v == name))
}
fn collect_role_messages(steps: &[SessionStep], sends: &mut Vec<String>, receives: &mut Vec<String>) {
for step in steps {
match step.op.as_str() {
"send" => {
if !sends.contains(&step.message_type) {
sends.push(step.message_type.clone());
}
}
"receive" => {
if !receives.contains(&step.message_type) {
receives.push(step.message_type.clone());
}
}
"select" | "branch" | "interrupt" => {
for b in &step.branches {
collect_role_messages(&b.steps, sends, receives);
}
}
_ => {}
}
}
}
fn find_session_by_name<'a>(program: &'a Program, name: &str) -> Option<&'a SessionDefinition> {
for decl in &program.declarations {
if let Declaration::Session(s) = decl {
if s.name == name {
return Some(s);
}
}
}
None
}
fn fmt_type_expr(t: &TypeExpr) -> String {
let mut s = t.name.clone();
if !t.generic_param.is_empty() {
s.push('<');
s.push_str(&t.generic_param);
s.push('>');
}
if t.optional {
s.push('?');
}
s
}
fn peel_type_constructors(type_ref: &str) -> &str {
let mut t = type_ref.trim();
t = t.strip_suffix('?').unwrap_or(t).trim();
loop {
let peeled = ["FlowEnvelope<", "List<", "Stream<"].iter().find_map(|ctor| {
t.strip_prefix(*ctor)
.and_then(|rest| rest.strip_suffix('>'))
.map(|inner| inner.trim())
});
match peeled {
Some(inner) => t = inner.strip_suffix('?').unwrap_or(inner).trim(),
None => return t,
}
}
}
fn find_type_by_name<'a>(program: &'a Program, name: &str) -> Option<&'a TypeDefinition> {
for decl in &program.declarations {
if let Declaration::Type(t) = decl {
if t.name == name {
return Some(t);
}
}
}
None
}
fn find_shield_by_name<'a>(program: &'a Program, name: &str) -> Option<&'a ShieldDefinition> {
for decl in &program.declarations {
if let Declaration::Shield(s) = decl {
if s.name == name {
return Some(s);
}
}
}
None
}
fn find_flow_by_name<'a>(program: &'a Program, name: &str) -> Option<&'a FlowDefinition> {
for decl in &program.declarations {
if let Declaration::Flow(f) = decl {
if f.name == name {
return Some(f);
}
}
}
None
}
fn tool_has_stream_effect(program: &Program, tool_name: &str) -> bool {
if tool_name.is_empty() {
return false;
}
for decl in &program.declarations {
if let Declaration::Tool(t) = decl {
if t.name == tool_name {
if let Some(ref effects) = t.effects {
return effects.effects.iter().any(|e| e.starts_with("stream:"));
}
return false;
}
}
}
false
}
fn flow_has_stream_output(flow: &FlowDefinition) -> bool {
for step in &flow.body {
if let FlowStep::Step(s) = step {
let out = s.output_type.trim();
if out.starts_with("Stream<") && out.ends_with('>') {
return true;
}
}
}
false
}
fn use_tool_step_name(u: &UseToolStep) -> &str {
&u.tool_name
}
pub fn flow_quant_effects(flow: &FlowDefinition) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
collect_quant_effects(&flow.body, &mut out);
out
}
fn collect_quant_effects(steps: &[FlowStep], out: &mut Vec<String>) {
for step in steps {
match step {
FlowStep::Quant(q) => {
let slug = crate::ots_catalog::quant_effect_slug(&q.effect);
if !out.contains(&slug) {
out.push(slug);
}
collect_quant_effects(&q.body, out);
}
FlowStep::If(c) => {
collect_quant_effects(&c.then_body, out);
collect_quant_effects(&c.else_body, out);
}
FlowStep::ForIn(f) => collect_quant_effects(&f.body, out),
FlowStep::Par(p) => {
for branch in &p.branches {
collect_quant_effects(branch, out);
}
}
_ => {}
}
}
}
pub fn flow_uses_streaming_tool(flow: &FlowDefinition, program: &Program) -> bool {
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
for step in &flow.body {
match step {
FlowStep::UseTool(u) => {
let tn = use_tool_step_name(u);
if !tn.is_empty()
&& seen.insert(tn.to_string())
&& tool_has_stream_effect(program, tn)
{
return true;
}
}
FlowStep::Step(s) => {
if !s.apply_ref.is_empty()
&& seen.insert(s.apply_ref.clone())
&& tool_has_stream_effect(program, &s.apply_ref)
{
return true;
}
}
_ => {}
}
}
false
}
pub fn produces_stream(flow: &FlowDefinition, program: &Program) -> bool {
flow_has_stream_output(flow) || flow_uses_streaming_tool(flow, program)
}
pub fn implicit_transport(
endpoint: &AxonEndpointDefinition,
flow: Option<&FlowDefinition>,
program: &Program,
) -> String {
if endpoint.transport_explicit {
return match endpoint.transport.as_str() {
"ndjson" => "sse".to_string(),
"sse" | "json" => endpoint.transport.clone(),
_ => "json".to_string(),
};
}
match flow {
Some(f) if produces_stream(f, program) => "sse".to_string(),
_ => "json".to_string(),
}
}
pub fn resolve_effective_dialect(
transport_dialect: &str,
has_algebraic_stream_effect: bool,
) -> String {
if !transport_dialect.is_empty() {
return transport_dialect.to_string();
}
if has_algebraic_stream_effect {
return "openai".to_string();
}
"axon".to_string()
}
pub const W001_CODE: &str = "axon-W001";
pub const W003_CODE: &str = "axon-W003";
pub const W004_CODE: &str = "axon-W004";
fn build_w004_message(tool_name: &str, params: &[(String, String, bool)]) -> String {
let tool = tool_name;
let call = params
.iter()
.map(|(n, _, _)| format!("{n} = …"))
.collect::<Vec<_>>()
.join(", ");
format!(
"warning[{W004_CODE}]: `apply: {tool}` runs '{tool}' as a COGNITIVE step \
backend — the step executes as an LLM reasoning call and the model decides \
stochastically whether to invoke the tool; it is NOT a deterministic \
dispatch, and `given:` is not splatted at runtime. '{tool}' declares a typed \
`parameters:` schema, so for a deterministic, schema-validated, real dispatch \
use the flow-level form `use {tool}({call})` (with `provider: http`/`mcp` + a \
wired endpoint). See axon://logic/dispatch_vs_cognition."
)
}
pub const W006_CODE: &str = "axon-W006";
pub const WITNESS_METRICS: &[&str] = &[
"geometric_difference",
"kernel_target_alignment",
"ranking_lift",
"outcome_lift",
];
pub const MAX_KNOWN_CONTEXT_WINDOW: u32 = 1_048_576;
fn build_w006_message(compute_ref: &str, step_name: &str) -> String {
format!(
"warning[{W006_CODE}]: step '{step_name}' applies compute '{compute_ref}' — \
`compute` / `apply:` does NOT select an LLM model (a `compute {{ model: … }}` \
field is dropped at lowering and has no runtime effect). To choose the model \
for this step, declare its capability need with `requires_context: <tokens>` \
(the resolver picks the smallest model whose context window fits, or fails \
closed at deploy), or set the deployment model (e.g. `AXON_DAEMON_MODEL`). \
See §Fase 68 capability-aware model resolution."
)
}
fn build_w003_message(endpoint_name: &str) -> String {
format!(
"warning[{W003_CODE}]: axonendpoint '{endpoint_name}' declares \
no `backend:` — its execution backend is resolved at request \
time down the Fase 36 precedence ladder (server default → \
environment-available providers). If none resolves the \
endpoint fails with a structured HTTP 503; it never silently \
runs the no-op `stub`. Declare `backend: <provider>` to pin \
the model, or `backend: auto` to make the reliance on ladder \
resolution explicit and silence this warning."
)
}
fn describe_stream_origin(flow: &FlowDefinition, program: &Program) -> String {
for step in &flow.body {
if let FlowStep::Step(s) = step {
let out = s.output_type.trim();
if out.starts_with("Stream<") && out.ends_with('>') {
return format!("step '{}' has `output: {}`", s.name, s.output_type);
}
}
}
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
for step in &flow.body {
match step {
FlowStep::Step(s) => {
if !s.apply_ref.is_empty() && seen.insert(s.apply_ref.clone()) {
if let Some(policy) = tool_stream_policy(program, &s.apply_ref) {
return format!(
"step '{}' applies tool '{}' with effects `<{}>`",
s.name, s.apply_ref, policy
);
}
}
}
FlowStep::UseTool(u) => {
let tn = use_tool_step_name(u);
if !tn.is_empty() && seen.insert(tn.to_string()) {
if let Some(policy) = tool_stream_policy(program, tn) {
return format!(
"tool '{}' is used directly with effects `<{}>`",
tn, policy
);
}
}
}
_ => {}
}
}
"its declared algebraic effects".to_string()
}
fn tool_stream_policy(program: &Program, tool_name: &str) -> Option<String> {
for decl in &program.declarations {
if let Declaration::Tool(t) = decl {
if t.name == tool_name {
if let Some(ref effects) = t.effects {
for e in &effects.effects {
if e.starts_with("stream:") {
return Some(e.clone());
}
}
}
return None;
}
}
}
None
}
fn build_w001_message(endpoint: &AxonEndpointDefinition, flow: &FlowDefinition, program: &Program) -> String {
let origin = describe_stream_origin(flow, program);
format!(
"warning[{}]: implicit `transport: sse` inferred from stream \
effects on axonendpoint '{}' (flow '{}' produces a stream \
via {}). Declare `transport: sse` to silence this warning \
and lock in SSE behavior, or `transport: json` to opt out \
and keep the legacy JSON wire format. When \
`strict_type_driven_transport: true`, this endpoint emits \
SSE on /v1/execute by default.",
W001_CODE, endpoint.name, endpoint.execute_flow, origin
)
}
pub fn compute_implicit_transport_warnings(program: &Program) -> Vec<TypeError> {
let mut warnings: Vec<TypeError> = Vec::new();
let mut flow_indices: HashMap<String, usize> = HashMap::new();
for (i, decl) in program.declarations.iter().enumerate() {
if let Declaration::Flow(f) = decl {
flow_indices.insert(f.name.clone(), i);
}
}
for decl in &program.declarations {
let ae = match decl {
Declaration::AxonEndpoint(ae) => ae,
_ => continue,
};
if ae.transport_explicit {
continue;
}
if ae.implicit_transport != "sse" {
continue;
}
let flow = match flow_indices.get(&ae.execute_flow) {
Some(&fi) => match &program.declarations[fi] {
Declaration::Flow(f) => f,
_ => continue,
},
None => continue,
};
warnings.push(TypeError {
message: build_w001_message(ae, flow, program),
line: ae.loc.line,
column: ae.loc.column,
});
}
warnings
}
pub fn compute_implicit_transports(program: &mut Program) {
let mut flow_indices: HashMap<String, usize> = HashMap::new();
for (i, decl) in program.declarations.iter().enumerate() {
if let Declaration::Flow(f) = decl {
flow_indices.insert(f.name.clone(), i);
}
}
let mut updates: Vec<(usize, String, bool)> = Vec::new();
for (i, decl) in program.declarations.iter().enumerate() {
if let Declaration::AxonEndpoint(ae) = decl {
let flow = flow_indices.get(&ae.execute_flow).and_then(|&fi| {
if let Declaration::Flow(f) = &program.declarations[fi] {
Some(f)
} else {
None
}
});
let transport_result = implicit_transport(ae, flow, program);
let algebraic_result = match flow {
Some(f) => flow_uses_streaming_tool(f, program),
None => false,
};
updates.push((i, transport_result, algebraic_result));
}
}
for (i, transport_result, algebraic_result) in updates {
if let Declaration::AxonEndpoint(ae) = &mut program.declarations[i] {
ae.implicit_transport = transport_result;
ae.has_algebraic_stream_effect = algebraic_result;
}
}
}
#[cfg(test)]
mod fase13_typecheck_tests {
use super::*;
use crate::lexer::Lexer;
use crate::parser::Parser;
fn check_with_warnings(src: &str) -> (Vec<TypeError>, Vec<TypeError>) {
let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
let prog = Parser::new(tokens).parse().expect("parse");
TypeChecker::new(&prog).check_with_warnings()
}
fn check_errors(src: &str) -> Vec<TypeError> {
check_with_warnings(src).0
}
#[test]
fn channel_with_valid_shield_clean() {
let src = r#"
type Order { id: String }
shield Gate { scan: [pii_leak] }
channel C { message: Order shield: Gate }
"#;
assert!(check_errors(src).is_empty());
}
#[test]
fn channel_undefined_shield_rejected() {
let src = "channel C { message: Order shield: NotDefined }";
let errs = check_errors(src);
assert!(
errs.iter()
.any(|e| e.message.contains("undefined shield 'NotDefined'")),
"got: {:?}",
errs
);
}
#[test]
fn channel_shield_wrong_kind_rejected() {
let src = r#"
type NotAShield { x: String }
channel C { message: Order shield: NotAShield }
"#;
let errs = check_errors(src);
assert!(
errs.iter().any(|e| e.message.contains("not a shield")),
"got: {:?}",
errs
);
}
#[test]
fn emit_undefined_channel_rejected() {
let src = "flow f() -> O { emit Bogus(payload) }";
let errs = check_errors(src);
assert!(
errs.iter()
.any(|e| e.message.contains("undefined channel 'Bogus'")),
"got: {:?}",
errs
);
}
#[test]
fn emit_target_wrong_kind_rejected() {
let src = r#"
type Order { id: String }
flow f() -> O { emit Order(payload) }
"#;
let errs = check_errors(src);
assert!(
errs.iter().any(|e| e.message.contains("not a channel")),
"got: {:?}",
errs
);
}
#[test]
fn emit_mobility_schema_mismatch_rejected() {
let src = r#"
type Order { id: String }
type Other { y: String }
channel Wrong { message: Other }
channel Outer { message: Channel<Order> }
flow f() -> O { emit Outer(Wrong) }
"#;
let errs = check_errors(src);
assert!(
errs.iter()
.any(|e| e.message.contains("second-order schema mismatch")),
"got: {:?}",
errs
);
}
#[test]
fn publish_undefined_shield_rejected() {
let src = r#"
channel C { message: Order }
flow f() -> Cap { publish C within MissingShield }
"#;
let errs = check_errors(src);
assert!(
errs.iter()
.any(|e| e.message.contains("undefined shield 'MissingShield'")),
"got: {:?}",
errs
);
}
#[test]
fn discover_unpublishable_channel_rejected() {
let src = r#"
type Order { id: String }
channel C { message: Order }
flow f() -> O { discover C as ch }
"#;
let errs = check_errors(src);
assert!(
errs.iter().any(|e| e.message.contains("not publishable")),
"got: {:?}",
errs
);
}
#[test]
fn listen_typed_channel_warns_never_fires() {
let src = r#"
type Order { id: String }
channel C { message: Order }
daemon D() {
goal: "x"
listen C as ev { }
}
"#;
let (errs, warns) = check_with_warnings(src);
assert!(errs.is_empty(), "errors: {:?}", errs);
assert_eq!(warns.len(), 1, "the typed-channel listener warns it never fires");
assert!(warns[0].message.contains("axon-W009"), "{:?}", warns);
assert!(warns[0].message.contains("NEVER fire") && warns[0].message.contains("emit"));
}
#[test]
fn listen_typed_undefined_rejected() {
let src = r#"
daemon D() {
goal: "x"
listen NoSuchChannel as ev { }
}
"#;
let errs = check_errors(src);
assert!(
errs.iter().any(|e| e.message.contains("undefined channel")),
"got: {:?}",
errs
);
}
#[test]
fn listen_string_topic_warns_never_fires() {
let src = r#"
daemon D() {
goal: "x"
listen "orders.created" as ev { }
}
"#;
let (errs, warns) = check_with_warnings(src);
assert!(errs.is_empty(), "no errors expected: {:?}", errs);
assert_eq!(warns.len(), 1);
assert!(warns[0].message.contains("axon-W009"));
assert!(warns[0].message.contains("orders.created"));
assert!(warns[0].message.contains("NEVER fire"));
assert!(!warns[0].message.contains("deprecated since Fase 13"));
}
#[test]
fn listen_both_non_cron_listeners_warn_never_fires() {
let src = r#"
type Order { id: String }
channel C { message: Order }
daemon Mixed() {
goal: "x"
listen C as canonical { }
listen "legacy" as legacy_ev { }
}
"#;
let (errs, warns) = check_with_warnings(src);
assert!(errs.is_empty(), "no errors expected: {:?}", errs);
assert_eq!(warns.len(), 2, "both non-cron listeners warn they never fire");
assert!(warns.iter().all(|w| w.message.contains("axon-W009")));
}
#[test]
fn listen_with_a_producer_does_not_warn() {
let src = r#"
type Order { id: String }
channel C { message: Order }
flow Produce(id: String) -> Unit { emit C(id) }
daemon D() {
requires: [flow.execute]
listen C as ev { probe p }
}
"#;
let (errs, warns) = check_with_warnings(src);
assert!(errs.is_empty(), "errors: {:?}", errs);
assert!(
!warns.iter().any(|w| w.message.contains("axon-W009")),
"a listener WITH a producer must not warn (§74 delivers it): {:?}",
warns
);
}
#[test]
fn listen_cron_schedule_does_not_warn_never_fires() {
let src = r#"
flow Tick() -> Unit { probe p }
daemon Sched() {
requires: [flow.execute]
listen "cron:*/5 * * * *" as t { run Tick() }
}
"#;
let (errs, warns) = check_with_warnings(src);
assert!(errs.is_empty(), "no errors: {:?}", errs);
assert!(
!warns.iter().any(|w| w.message.contains("axon-W009")),
"a cron listener must NOT get the never-fires warning: {:?}",
warns
);
}
#[test]
fn emit_dotted_value_ref_does_not_trip_mobility_check() {
let src = r#"
channel Inner { message: Bytes qos: at_least_once }
channel Outer { message: Channel<Bytes> qos: at_least_once }
flow f() -> Out {
emit Outer(Build.handle)
}
"#;
let errs = check_errors(src);
let mobility = errs
.iter()
.filter(|e| {
e.message.contains("second-order schema mismatch")
|| e.message.contains("not a channel handle")
})
.count();
assert_eq!(
mobility, 0,
"dotted access must not trip mobility check; got: {:?}",
errs
);
}
#[test]
fn emit_bare_identifier_mobility_check_still_runs() {
let src = r#"
channel Inner { message: Bytes qos: at_least_once }
channel Wrong { message: Integer qos: at_least_once }
channel Outer { message: Channel<Bytes> qos: at_least_once }
flow f() -> Out {
emit Outer(Wrong)
}
"#;
let errs = check_errors(src);
assert!(
errs.iter()
.any(|e| e.message.contains("second-order schema mismatch")),
"expected mobility violation for bare-id ref, got: {:?}",
errs
);
}
}
#[cfg(test)]
mod fase35j_capability_tests {
use super::*;
use crate::lexer::Lexer;
use crate::parser::Parser;
fn check_errors(src: &str) -> Vec<TypeError> {
let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
let prog = Parser::new(tokens).parse().expect("parse");
TypeChecker::new(&prog).check()
}
fn mentions_capability(errs: &[TypeError]) -> bool {
errs.iter().any(|e| e.message.contains("requiring capability"))
}
#[test]
fn endpoint_must_grant_a_gated_store_capability() {
let src = r#"
axonstore tenants {
backend: postgresql
connection: "env:DB"
capability: "tenant.read"
}
flow GetTenants() -> Unit {
retrieve tenants { where: "id = 1" }
}
axonendpoint Ep { method: GET path: "/t" execute: GetTenants }
"#;
assert!(
mentions_capability(&check_errors(src)),
"an endpoint that does not grant the store's capability \
must fail the compositional check"
);
}
#[test]
fn endpoint_granting_the_capability_type_checks_clean() {
let src = r#"
axonstore tenants {
backend: postgresql
connection: "env:DB"
capability: "tenant.read"
}
flow GetTenants() -> Unit {
retrieve tenants { where: "id = 1" }
}
axonendpoint Ep {
method: GET path: "/t" execute: GetTenants
requires: [tenant.read]
}
"#;
assert!(
!mentions_capability(&check_errors(src)),
"an endpoint that grants the capability must type-check clean"
);
}
#[test]
fn ungated_store_needs_no_endpoint_grant() {
let src = r#"
axonstore kvstore { backend: postgresql connection: "env:DB" }
flow Fetch() -> Unit {
retrieve kvstore { where: "k = 1" }
}
axonendpoint Ep { method: GET path: "/c" execute: Fetch }
"#;
assert!(
!mentions_capability(&check_errors(src)),
"a store with no `capability:` requires no endpoint grant"
);
}
#[test]
fn malformed_capability_slug_is_a_parse_error() {
let src = r#"axonstore s { backend: postgresql connection: "env:DB" capability: "Tenant.Read" }"#;
let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
assert!(
Parser::new(tokens).parse().is_err(),
"an uppercase capability slug must be rejected at parse time"
);
}
}
#[cfg(test)]
mod fase37y_d3_d4_tests {
use super::*;
use crate::lexer::Lexer;
use crate::parser::Parser;
fn check_errors(src: &str) -> Vec<TypeError> {
let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
let prog = Parser::new(tokens).parse().expect("parse");
TypeChecker::new(&prog).check()
}
#[test]
fn d3_path_only_param_passes_d2_totality() {
let src = r#"
type SecretWriteRequest { value: Text }
type WriteResult { ok: Bool }
axonendpoint write_secret {
method: POST
path: "/api/tenants/{tenant_id}/secrets/{secret_name}"
body: SecretWriteRequest
execute: WriteSecret
}
flow WriteSecret(tenant_id: Text, secret_name: Text, value: Text) -> WriteResult {
step Echo { reason: "ok" output: WriteResult }
}
"#;
let errs = check_errors(src);
let binding_errs: Vec<&TypeError> = errs
.iter()
.filter(|e| {
e.message.contains("tenant_id")
|| e.message.contains("secret_name")
|| e.message.contains("Request Binding")
})
.collect();
assert!(
binding_errs.is_empty(),
"D3 — path-only params must satisfy D2 totality. Got: {binding_errs:#?}"
);
}
#[test]
fn d3_query_only_param_passes_d2_totality() {
let src = r#"
type UserList { count: Int }
axonendpoint list_users {
method: GET
path: "/api/users"
query: { status: Text }
execute: ListUsers
}
flow ListUsers(status: Text) -> UserList {
step Build { reason: "list" output: UserList }
}
"#;
let errs = check_errors(src);
let binding_errs: Vec<&TypeError> = errs
.iter()
.filter(|e| e.message.contains("status") || e.message.contains("Request Binding"))
.collect();
assert!(
binding_errs.is_empty(),
"D3 — query-only params must satisfy D2 totality. Got: {binding_errs:#?}"
);
}
#[test]
fn d3_mixed_path_query_body_coverage_passes() {
let src = r#"
type CreateRequest { content: Text }
type CreateResult { id: Uuid }
axonendpoint create_item {
method: POST
path: "/api/orgs/{org_id}/items"
query: { dry_run: Bool? }
body: CreateRequest
execute: CreateItem
}
flow CreateItem(org_id: Text, dry_run: Bool?, content: Text) -> CreateResult {
step Build { reason: "create" output: CreateResult }
}
"#;
let errs = check_errors(src);
let binding_errs: Vec<&TypeError> = errs
.iter()
.filter(|e| e.message.contains("Request Binding") || e.message.contains("axon-T901"))
.collect();
assert!(
binding_errs.is_empty(),
"D3 — mixed coverage must satisfy D2. Got: {binding_errs:#?}"
);
}
#[test]
fn d3_missing_param_extended_hint_names_all_three_sources() {
let src = r#"
type Empty { ok: Bool }
axonendpoint x {
method: POST
path: "/api/x"
body: Empty
execute: X
}
flow X(missing: Text) -> Empty {
step S { reason: "x" output: Empty }
}
"#;
let errs = check_errors(src);
let hint = errs.iter().find(|e| e.message.contains("missing")).expect(
"missing-binding error must surface",
);
assert!(hint.message.contains("path placeholder"), "hint names path: {}", hint.message);
assert!(hint.message.contains("query"), "hint names query: {}", hint.message);
assert!(hint.message.contains("body"), "hint names body: {}", hint.message);
}
#[test]
fn d4_t901_collision_path_and_body() {
let src = r#"
type SecretWriteRequest { tenant_id: Text, value: Text }
type WriteResult { ok: Bool }
axonendpoint write {
method: POST
path: "/api/tenants/{tenant_id}"
body: SecretWriteRequest
execute: Write
}
flow Write(tenant_id: Text, value: Text) -> WriteResult {
step S { reason: "x" output: WriteResult }
}
"#;
let errs = check_errors(src);
let t901 = errs.iter().find(|e| e.message.contains("axon-T901"));
assert!(
t901.is_some(),
"D4 — path+body collision must emit axon-T901. Errors: {errs:#?}"
);
let msg = &t901.unwrap().message;
assert!(msg.contains("path and body"), "names both sources: {msg}");
assert!(msg.contains("tenant_id"), "names the colliding param: {msg}");
}
#[test]
fn d4_t901_collision_path_and_query() {
let src = r#"
type Empty { ok: Bool }
axonendpoint x {
method: GET
path: "/api/users/{id}"
query: { id: Text }
execute: X
}
flow X(id: Text) -> Empty {
step S { reason: "x" output: Empty }
}
"#;
let errs = check_errors(src);
let t901 = errs.iter().find(|e| e.message.contains("axon-T901"));
assert!(t901.is_some(), "D4 — path+query collision. Errs: {errs:#?}");
assert!(
t901.unwrap().message.contains("path and query"),
"names path AND query"
);
}
#[test]
fn d4_t901_collision_query_and_body() {
let src = r#"
type Req { status: Text }
type Empty { ok: Bool }
axonendpoint x {
method: POST
path: "/api/x"
query: { status: Text }
body: Req
execute: X
}
flow X(status: Text) -> Empty {
step S { reason: "x" output: Empty }
}
"#;
let errs = check_errors(src);
let t901 = errs.iter().find(|e| e.message.contains("axon-T901"));
assert!(t901.is_some(), "D4 — query+body collision. Errs: {errs:#?}");
assert!(
t901.unwrap().message.contains("query and body"),
"names query AND body"
);
}
#[test]
fn d4_t901_collision_triple_source() {
let src = r#"
type Req { id: Text }
type Empty { ok: Bool }
axonendpoint x {
method: POST
path: "/api/{id}"
query: { id: Text }
body: Req
execute: X
}
flow X(id: Text) -> Empty {
step S { reason: "x" output: Empty }
}
"#;
let errs = check_errors(src);
let t901 = errs.iter().find(|e| e.message.contains("axon-T901"));
assert!(t901.is_some(), "D4 — triple collision. Errs: {errs:#?}");
let msg = &t901.unwrap().message;
assert!(
msg.contains("path, query, and body"),
"names all three sources with Oxford comma: {msg}"
);
assert!(
msg.contains("Remove the declaration from 2 of the sources"),
"explicit count of removals needed: {msg}"
);
}
#[test]
fn d3_path_param_typed_non_text_emits_error() {
let src = r#"
type Empty { ok: Bool }
axonendpoint x {
method: GET
path: "/api/users/{id}"
execute: X
}
flow X(id: Uuid) -> Empty {
step S { reason: "x" output: Empty }
}
"#;
let errs = check_errors(src);
let type_err = errs.iter().find(|e| {
e.message.contains("path placeholder") && e.message.contains("Text")
});
assert!(
type_err.is_some(),
"path-binding type mismatch must surface. Errs: {errs:#?}"
);
}
#[test]
fn d3_query_param_type_mismatch_emits_error() {
let src = r#"
type Empty { ok: Bool }
axonendpoint x {
method: GET
path: "/api/x"
query: { limit: Int }
execute: X
}
flow X(limit: Text) -> Empty {
step S { reason: "x" output: Empty }
}
"#;
let errs = check_errors(src);
let type_err = errs.iter().find(|e| {
e.message.contains("query: {") && e.message.contains("Int")
});
assert!(
type_err.is_some(),
"query-binding type mismatch must surface. Errs: {errs:#?}"
);
}
#[test]
fn d5_body_only_endpoint_legacy_behavior_intact() {
let src_passes = r#"
type Req { value: Text }
type Empty { ok: Bool }
axonendpoint x {
method: POST
path: "/api/x"
body: Req
execute: X
}
flow X(value: Text) -> Empty {
step S { reason: "x" output: Empty }
}
"#;
let errs = check_errors(src_passes);
assert!(
errs.iter().all(|e| !e.message.contains("Request Binding")
&& !e.message.contains("axon-T901")),
"D5 — body-only happy path passes unchanged. Errs: {errs:#?}"
);
}
#[test]
fn d3_kivi_secret_write_passes_post_37y() {
let src = r#"
type SecretWriteRequest { value: Text }
type WriteResult { ok: Bool }
axonendpoint write_secret {
method: POST
path: "/api/tenants/{tenant_id}/secrets/{secret_name}"
query: { dry_run: Bool?, overwrite: Bool? }
body: SecretWriteRequest
execute: WriteSecret
}
flow WriteSecret(
tenant_id: Text,
secret_name: Text,
dry_run: Bool?,
overwrite: Bool?,
value: Text
) -> WriteResult {
step S { reason: "x" output: WriteResult }
}
"#;
let errs = check_errors(src);
let binding_errs: Vec<&TypeError> = errs
.iter()
.filter(|e| {
e.message.contains("Request Binding") || e.message.contains("axon-T901")
})
.collect();
assert!(
binding_errs.is_empty(),
"Kivi corpus must pass post-37.y. Got: {binding_errs:#?}"
);
}
}
#[cfg(test)]
mod fase38xe_cardinality_tests {
use super::*;
use crate::lexer::Lexer;
use crate::parser::Parser;
fn check_errors(src: &str) -> Vec<TypeError> {
let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
let prog = Parser::new(tokens).parse().expect("parse");
TypeChecker::new(&prog).check()
}
#[test]
fn retrieve_tail_with_singular_output_emits_t9xx() {
let src = r#"
type TenantRecord { id: Text }
axonstore tenants { backend: in_memory }
axonendpoint get_tenant {
method: GET
path: "/api/tenants/{tenant_id}"
output: TenantRecord
execute: GetTenant
}
flow GetTenant(tenant_id: Text) -> TenantRecord {
retrieve tenants { where: "id = ${tenant_id}" as: result }
}
"#;
let errs = check_errors(src);
let e039: Vec<&TypeError> = errs
.iter()
.filter(|e| e.message.contains("axon-E039"))
.collect();
assert!(
!e039.is_empty(),
"§Fase 39.e (D12 α) — a bare-singular `output: T` on a \
`transport: json` endpoint with retrieve-tail MUST emit \
`axon-E039`. All errors: {errs:#?}"
);
let err = e039[0];
assert!(
err.message.contains("FlowEnvelope<List<StoreRow>>")
|| err.message.contains("FlowEnvelope<List<TenantRecord>>"),
"§39.e — the E039 hint MUST suggest a canonical \
FlowEnvelope wrapping around the inferred tail \
cardinality (List<StoreRow> from the IR retrieve-step \
taxonomy today; List<TenantRecord> if inference is \
refined). Got: {}",
err.message
);
assert!(
err.message.contains("transport: sse"),
"§39.e — the E039 hint MUST also name the sse migration \
alternative. Got: {}",
err.message
);
let t9xx: Vec<&TypeError> = errs
.iter()
.filter(|e| e.message.contains("axon-T9XX"))
.collect();
assert!(
t9xx.is_empty(),
"§39.e — when E039 fires, T9XX MUST be suppressed (single \
canonical diagnostic with the right answer). Got: {t9xx:#?}"
);
}
#[test]
fn retrieve_tail_with_list_output_passes() {
let src = r#"
type TenantRecord { id: Text }
axonstore tenants { backend: in_memory }
axonendpoint list_tenants {
method: GET
path: "/api/tenants"
output: List<TenantRecord>
execute: ListTenants
}
flow ListTenants() -> List<TenantRecord> {
retrieve tenants { where: "1 = 1" as: result }
}
"#;
let errs = check_errors(src);
let t9xx: Vec<&TypeError> = errs
.iter()
.filter(|e| e.message.contains("axon-T9XX"))
.collect();
assert!(
t9xx.is_empty(),
"§Fase 38.x.e D1 — a retrieve-tail flow with `output: \
List<T>` is the well-formed case. No T9XX should fire. \
Got: {t9xx:#?}"
);
}
#[test]
fn step_tail_with_singular_output_passes() {
let src = r#"
type WriteResult { ok: Bool }
axonendpoint write_secret {
method: POST
path: "/api/secrets"
output: WriteResult
execute: WriteSecret
}
flow WriteSecret() -> WriteResult {
step Echo { reason: "ok" output: WriteResult }
}
"#;
let errs = check_errors(src);
let t9xx: Vec<&TypeError> = errs
.iter()
.filter(|e| e.message.contains("axon-T9XX"))
.collect();
assert!(
t9xx.is_empty(),
"§Fase 38.x.e D1 — a step-tail flow with matching singular \
output is well-formed; no cardinality mismatch. Got: \
{t9xx:#?}"
);
}
#[test]
fn no_output_declared_skips_gate() {
let src = r#"
type TenantRecord { id: Text }
axonstore tenants { backend: in_memory }
axonendpoint get_tenant_loose {
method: GET
path: "/api/tenants/{tenant_id}"
execute: GetTenantLoose
}
flow GetTenantLoose(tenant_id: Text) -> Unit {
retrieve tenants { where: "id = ${tenant_id}" as: result }
}
"#;
let errs = check_errors(src);
let t9xx: Vec<&TypeError> = errs
.iter()
.filter(|e| e.message.contains("axon-T9XX"))
.collect();
assert!(
t9xx.is_empty(),
"§Fase 38.x.e D1 — endpoint with no `output:` declared \
skips the cardinality gate (honest scope). Got: {t9xx:#?}"
);
}
#[test]
fn stream_output_skips_gate() {
let src = r#"
type Token { text: Text }
axonendpoint stream_chat {
method: POST
path: "/api/stream"
output: Stream<Token>
execute: StreamChat
}
flow StreamChat() -> Stream<Token> {
step Generate { ask: "stream" output: Stream<Token> }
}
"#;
let errs = check_errors(src);
let t9xx: Vec<&TypeError> = errs
.iter()
.filter(|e| e.message.contains("axon-T9XX"))
.collect();
assert!(
t9xx.is_empty(),
"§Fase 38.x.e D1 — Stream<T> output skips the gate \
(v1.39.0 honest scope). Got: {t9xx:#?}"
);
}
}
#[cfg(test)]
mod fase39a_flow_envelope_tests {
use super::*;
use crate::lexer::Lexer;
use crate::parser::Parser;
fn check_errors(src: &str) -> Vec<TypeError> {
let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
let prog = Parser::new(tokens).parse().expect("parse");
TypeChecker::new(&prog).check()
}
#[test]
fn fase39a_flow_envelope_of_singular_is_wrapped_singular() {
let card = declared_cardinality("FlowEnvelope<TenantRecord>");
match card {
Cardinality::Wrapped(inner) => {
assert_eq!(
*inner,
Cardinality::Singular("TenantRecord".to_string()),
"§39.a §1 — inner must be Singular(\"TenantRecord\")"
);
}
other => panic!(
"§39.a §1 — FlowEnvelope<TenantRecord> must yield \
Wrapped(Singular(...)). Got: {other:?}"
),
}
}
#[test]
fn fase39a_flow_envelope_of_list_is_wrapped_plural() {
let card = declared_cardinality("FlowEnvelope<List<TenantRecord>>");
match card {
Cardinality::Wrapped(inner) => {
assert_eq!(
*inner,
Cardinality::Plural("TenantRecord".to_string()),
"§39.a §1 acceptance — inner must be Plural(\"TenantRecord\") \
(the canonical retrieve-tail shape Kivi reported)"
);
}
other => panic!(
"§39.a §1 acceptance — FlowEnvelope<List<TenantRecord>> must \
yield Wrapped(Plural(\"TenantRecord\")). Got: {other:?}"
),
}
}
#[test]
fn fase39a_flow_envelope_of_stream_is_wrapped_stream() {
let card = declared_cardinality("FlowEnvelope<Stream<Token>>");
match card {
Cardinality::Wrapped(inner) => {
assert_eq!(
*inner,
Cardinality::StreamCardinality("Token".to_string()),
"§39.a §1 — inner must be StreamCardinality(\"Token\")"
);
}
other => panic!(
"§39.a §1 — FlowEnvelope<Stream<Token>> must yield \
Wrapped(StreamCardinality(...)). Got: {other:?}"
),
}
}
#[test]
fn fase39a_flow_envelope_of_any_is_wrapped_disagreed() {
let card = declared_cardinality("FlowEnvelope<Any>");
match card {
Cardinality::Wrapped(inner) => {
assert_eq!(
*inner,
Cardinality::Disagreed,
"§39.a §1 — FlowEnvelope<Any> inner must be Disagreed"
);
}
other => panic!("§39.a §1 — got: {other:?}"),
}
}
#[test]
fn fase39a_nested_flow_envelope_is_doubly_wrapped() {
let card = declared_cardinality("FlowEnvelope<FlowEnvelope<TenantRecord>>");
match card {
Cardinality::Wrapped(outer_inner) => match outer_inner.as_ref() {
Cardinality::Wrapped(inner_inner) => {
assert_eq!(
**inner_inner,
Cardinality::Singular("TenantRecord".to_string()),
"§39.a §1 — nested wrap inner must be Singular"
);
}
other => panic!(
"§39.a §1 — nested wrap outer.inner must be Wrapped. \
Got: {other:?}"
),
},
other => panic!("§39.a §1 — got: {other:?}"),
}
}
#[test]
fn fase39a_list_without_envelope_still_plural() {
let card = declared_cardinality("List<TenantRecord>");
assert_eq!(
card,
Cardinality::Plural("TenantRecord".to_string()),
"§39.a §2 — List<T> backwards-compat: still Plural"
);
}
#[test]
fn fase39a_stream_without_envelope_still_stream() {
let card = declared_cardinality("Stream<Token>");
assert_eq!(
card,
Cardinality::StreamCardinality("Token".to_string()),
"§39.a §2 — Stream<T> backwards-compat: still StreamCardinality"
);
}
#[test]
fn fase39a_bare_type_still_singular() {
let card = declared_cardinality("TenantRecord");
assert_eq!(
card,
Cardinality::Singular("TenantRecord".to_string()),
"§39.a §2 — bare type backwards-compat: still Singular"
);
}
#[test]
fn fase39a_parser_accepts_flow_envelope_of_list() {
let src = r#"
type TenantRecord { id: Text }
axonstore tenants { backend: in_memory }
axonendpoint get_all {
method: GET
path: "/api/tenants"
output: FlowEnvelope<List<TenantRecord>>
execute: GetAll
}
flow GetAll() -> Unit {
retrieve tenants { where: "" as: result }
}
"#;
let errs = check_errors(src);
let parse_errs: Vec<&TypeError> = errs
.iter()
.filter(|e| {
e.message.contains("Expected")
|| e.message.contains("Unexpected token")
|| e.message.contains("syntax")
})
.collect();
assert!(
parse_errs.is_empty(),
"§39.a §3 — FlowEnvelope<List<TenantRecord>> MUST parse cleanly. \
Got parse-class errors: {parse_errs:#?}"
);
}
#[test]
fn fase39a_wrapped_plural_matches_plural_tail_silent() {
let src = r#"
type TenantRecord { id: Text }
axonstore tenants { backend: in_memory }
axonendpoint get_all {
method: GET
path: "/api/tenants"
output: FlowEnvelope<List<TenantRecord>>
execute: GetAll
}
flow GetAll() -> List<TenantRecord> {
retrieve tenants { where: "" as: result }
}
"#;
let errs = check_errors(src);
let cardinality_errs: Vec<&TypeError> = errs
.iter()
.filter(|e| {
e.message.contains("axon-T9XX")
|| e.message.contains("axon-T9YY")
|| e.message.contains("axon-W003")
})
.collect();
assert!(
cardinality_errs.is_empty(),
"§39.a §4 — FlowEnvelope<List<T>> declared against Plural \
tail MUST silent-pass the cardinality gate (the wrap \
unwraps transparently). Got: {cardinality_errs:#?}"
);
}
#[test]
fn fase39e_bare_singular_with_json_transport_emits_e039() {
let src = r#"
type TenantRecord { id: Text }
axonstore tenants { backend: in_memory }
axonendpoint get_tenant {
method: GET
path: "/api/tenants/{id}"
output: TenantRecord
execute: GetTenant
}
flow GetTenant(id: Text) -> TenantRecord {
step Echo { reason: "x" output: TenantRecord }
}
"#;
let errs = check_errors(src);
let e039: Vec<&TypeError> = errs
.iter()
.filter(|e| e.message.contains("axon-E039"))
.collect();
assert!(!e039.is_empty(), "§39.e §1 — bare T MUST fire E039. Got: {errs:#?}");
assert!(
e039[0].message.contains("`output: TenantRecord`"),
"§39.e §1 — diagnostic MUST name the declared bare type. Got: {}",
e039[0].message
);
assert!(
e039[0].message.contains("FlowEnvelope<"),
"§39.e §1 — diagnostic MUST suggest FlowEnvelope wrapping. \
Got: {}",
e039[0].message
);
assert!(
e039[0].message.contains("transport: sse"),
"§39.e §1 — diagnostic MUST mention sse migration alternative. \
Got: {}",
e039[0].message
);
assert!(
e039[0].message.contains("D12"),
"§39.e §1 — diagnostic MUST reference the D12 α \
ratification anchor. Got: {}",
e039[0].message
);
}
#[test]
fn fase39e_bare_list_with_json_transport_emits_e039() {
let src = r#"
type TenantRecord { id: Text }
axonstore tenants { backend: in_memory }
axonendpoint list_tenants {
method: GET
path: "/api/tenants"
output: List<TenantRecord>
execute: ListTenants
}
flow ListTenants() -> List<TenantRecord> {
retrieve tenants { where: "1=1" as: result }
}
"#;
let errs = check_errors(src);
let e039: Vec<&TypeError> = errs
.iter()
.filter(|e| e.message.contains("axon-E039"))
.collect();
assert!(
!e039.is_empty(),
"§39.e §2 — bare `List<T>` MUST fire E039. Got: {errs:#?}"
);
assert!(
e039[0].message.contains("`output: List<TenantRecord>`"),
"§39.e §2 — diagnostic MUST name the declared bare List<T>. \
Got: {}",
e039[0].message
);
assert!(
e039[0].message.contains("FlowEnvelope<List<"),
"§39.e §2 — diagnostic MUST suggest FlowEnvelope<List<...>>. \
Got: {}",
e039[0].message
);
}
#[test]
fn fase39e_bare_stream_with_json_transport_emits_e039() {
let src = r#"
type Token { text: Text }
axonendpoint stream_chat {
method: POST
path: "/api/stream"
output: Stream<Token>
execute: StreamChat
}
flow StreamChat() -> Stream<Token> {
step Generate { ask: "stream" output: Stream<Token> }
}
"#;
let errs = check_errors(src);
let e039_or_silent =
errs.iter().filter(|e| e.message.contains("axon-E039")).count();
let t9yy: Vec<&TypeError> = errs
.iter()
.filter(|e| e.message.contains("axon-T9YY"))
.collect();
assert!(
e039_or_silent <= 1,
"§39.e §3 — Stream<T> bare emits at most ONE diagnostic \
(E039 or silent if implicit_transport=sse). Got count: \
{e039_or_silent}, t9yy: {t9yy:#?}, all: {errs:#?}"
);
}
#[test]
fn fase39e_flow_envelope_singular_passes_clean() {
let src = r#"
type WriteResult { ok: Bool }
axonendpoint write_secret {
method: POST
path: "/api/secrets"
output: FlowEnvelope<WriteResult>
execute: WriteSecret
}
flow WriteSecret() -> WriteResult {
step Echo { reason: "ok" output: WriteResult }
}
"#;
let errs = check_errors(src);
let wire_errs: Vec<&TypeError> = errs
.iter()
.filter(|e| {
e.message.contains("axon-E039")
|| e.message.contains("axon-T9XX")
|| e.message.contains("axon-T9YY")
})
.collect();
assert!(
wire_errs.is_empty(),
"§39.e §4 — FlowEnvelope<T> singular happy path MUST be \
clean. Got: {wire_errs:#?}"
);
}
#[test]
fn fase39e_flow_envelope_list_passes_clean() {
let src = r#"
type TenantRecord { id: Text }
axonstore tenants { backend: in_memory }
axonendpoint list_tenants {
method: GET
path: "/api/tenants"
output: FlowEnvelope<List<TenantRecord>>
execute: ListTenants
}
flow ListTenants() -> List<TenantRecord> {
retrieve tenants { where: "1=1" as: result }
}
"#;
let errs = check_errors(src);
let wire_errs: Vec<&TypeError> = errs
.iter()
.filter(|e| {
e.message.contains("axon-E039")
|| e.message.contains("axon-T9XX")
|| e.message.contains("axon-T9YY")
})
.collect();
assert!(
wire_errs.is_empty(),
"§39.e §5 — FlowEnvelope<List<T>> over a List<T>-tail flow \
is the canonical migration. Got: {wire_errs:#?}"
);
}
#[test]
fn fase39e_flow_envelope_any_passes_clean() {
let src = r#"
type X { f: Text }
axonendpoint p {
method: POST
path: "/api/p"
output: Any
execute: F
}
flow F() -> X {
step S { reason: "x" output: X }
}
"#;
let errs = check_errors(src);
let wire_errs: Vec<&TypeError> = errs
.iter()
.filter(|e| e.message.contains("axon-E039"))
.collect();
assert!(
wire_errs.is_empty(),
"§39.e §6 — `output: Any` is the universal-accept escape \
hatch; E039 MUST NOT fire. Got: {wire_errs:#?}"
);
}
#[test]
fn fase39e_sse_transport_exempts_from_e039() {
let src = r#"
type Token { text: Text }
axonendpoint stream_chat {
method: POST
path: "/api/stream"
transport: sse
output: Stream<Token>
execute: StreamChat
}
flow StreamChat() -> Stream<Token> {
step Generate { ask: "stream" output: Stream<Token> }
}
"#;
let errs = check_errors(src);
let e039: Vec<&TypeError> = errs
.iter()
.filter(|e| e.message.contains("axon-E039"))
.collect();
assert!(
e039.is_empty(),
"§39.e §7 — explicit `transport: sse` MUST exempt the \
endpoint from E039 (the SSE wire has its own event \
family per D9). Got: {e039:#?}"
);
}
#[test]
fn fase39e_no_output_declared_skips_e039() {
let src = r#"
type TenantRecord { id: Text }
axonstore tenants { backend: in_memory }
axonendpoint loose {
method: GET
path: "/api/loose"
execute: GetLoose
}
flow GetLoose() -> Unit {
retrieve tenants { where: "1=1" as: result }
}
"#;
let errs = check_errors(src);
let wire_errs: Vec<&TypeError> = errs
.iter()
.filter(|e| e.message.contains("axon-E039"))
.collect();
assert!(
wire_errs.is_empty(),
"§39.e §8 — empty `output:` MUST skip E039 (D9 \
backwards-compat). Got: {wire_errs:#?}"
);
}
#[test]
fn fase39e_unit_output_skips_e039() {
let src = r#"
type X { f: Text }
axonendpoint noop {
method: POST
path: "/api/noop"
output: Unit
execute: F
}
flow F() -> Unit {
step S { reason: "x" output: Unit }
}
"#;
let errs = check_errors(src);
let e039: Vec<&TypeError> = errs
.iter()
.filter(|e| e.message.contains("axon-E039"))
.collect();
assert!(
e039.is_empty(),
"§39.e §9 — `output: Unit` MUST be exempt from E039. \
Got: {e039:#?}"
);
}
#[test]
fn fase39e_nested_flow_envelope_passes_clean() {
let src = r#"
type X { f: Text }
axonendpoint p {
method: POST
path: "/api/p"
output: FlowEnvelope<FlowEnvelope<X>>
execute: F
}
flow F() -> X {
step S { reason: "x" output: X }
}
"#;
let errs = check_errors(src);
let e039: Vec<&TypeError> = errs
.iter()
.filter(|e| e.message.contains("axon-E039"))
.collect();
assert!(
e039.is_empty(),
"§39.e §10 — nested FlowEnvelope<FlowEnvelope<X>> is \
semantically degenerate but NOT an E039 (any \
`FlowEnvelope<...>` declaration passes the wrapping \
mandate). Got: {e039:#?}"
);
}
#[test]
fn fase39a_wrapped_singular_vs_plural_tail_still_warns() {
let src = r#"
type TenantRecord { id: Text }
axonstore tenants { backend: in_memory }
axonendpoint get_one {
method: GET
path: "/api/tenants/{id}"
output: FlowEnvelope<TenantRecord>
execute: GetOne
}
flow GetOne(id: Text) -> TenantRecord {
retrieve tenants { where: "id = ${id}" as: result }
}
"#;
let errs = check_errors(src);
let t9xx: Vec<&TypeError> = errs
.iter()
.filter(|e| e.message.contains("axon-T9XX"))
.collect();
assert!(
!t9xx.is_empty(),
"§39.a §4 — Wrapped(Singular) against Plural tail MUST \
surface axon-T9XX through the unwrap (the wrap is \
transparent to the cardinality contract). Errors: {errs:#?}"
);
}
}
#[cfg(test)]
mod fase41b_session_lowering_tests {
use super::*;
fn step(op: &str, ty: &str) -> SessionStep {
SessionStep { op: op.into(), message_type: ty.into(), ..Default::default() }
}
fn role(name: &str, steps: Vec<SessionStep>) -> SessionRole {
SessionRole { name: name.into(), steps, ..Default::default() }
}
#[test]
fn lowers_send_receive_end_to_session_type() {
let r = role("client", vec![step("send", "T"), step("receive", "U"), step("end", "")]);
assert_eq!(lower_session_role(&r), SessionType::send("T", SessionType::recv("U", SessionType::End)));
}
#[test]
fn lowers_terminal_loop_to_mu_recursion() {
let r = role("p", vec![step("send", "T"), step("loop", "")]);
assert_eq!(lower_session_role(&r), SessionType::rec("X", SessionType::send("T", SessionType::var("X"))));
}
#[test]
fn dual_recursive_roles_satisfy_the_connection_law() {
let client = lower_session_role(&role("c", vec![step("send", "T"), step("loop", "")]));
let server = lower_session_role(&role("s", vec![step("receive", "T"), step("loop", "")]));
assert!(client.is_dual_to(&server));
assert!(server.is_dual_to(&client)); }
#[test]
fn non_dual_roles_are_rejected() {
let a = lower_session_role(&role("a", vec![step("send", "T"), step("end", "")]));
let same = lower_session_role(&role("b", vec![step("send", "T"), step("end", "")]));
assert!(!a.is_dual_to(&same));
let wrong = lower_session_role(&role("c", vec![step("receive", "WRONG"), step("end", "")]));
assert!(!a.is_dual_to(&wrong));
}
}
#[cfg(test)]
mod fase41b_socket_tests {
use super::*;
use crate::lexer::Lexer;
use crate::parser::Parser;
const SESSION: &str =
"session Chat { client: [send Msg, receive Token, end] server: [receive Msg, send Token, end] }";
fn parse_prog(src: &str) -> Program {
let toks = Lexer::new(src, "<t>").tokenize().expect("lex");
Parser::new(toks).parse().expect("parse")
}
fn errors(src: &str) -> Vec<TypeError> {
TypeChecker::new(&parse_prog(src)).check()
}
#[test]
fn socket_parses_into_ast_fields() {
let prog = parse_prog(&format!(
"{SESSION}\nsocket ChatWS {{ protocol: Chat, backpressure: credit(64), reconnect: cognitive_state, legal_basis: legitimate_interest }}"
));
let sock = prog
.declarations
.iter()
.find_map(|d| if let Declaration::Socket(s) = d { Some(s) } else { None })
.expect("socket parsed");
assert_eq!(sock.name, "ChatWS");
assert_eq!(sock.protocol, "Chat");
assert_eq!(sock.backpressure_credit, Some(64));
assert!(sock.reconnect);
assert_eq!(sock.legal_basis.as_deref(), Some("legitimate_interest"));
}
#[test]
fn socket_referencing_a_declared_session_has_no_socket_error() {
let errs = errors(&format!("{SESSION}\nsocket ChatWS {{ protocol: Chat, backpressure: credit(64) }}"));
assert!(!errs.iter().any(|e| e.message.contains("Socket")), "unexpected socket error: {errs:?}");
}
#[test]
fn socket_with_undeclared_protocol_is_rejected() {
let errs = errors("socket ChatWS { protocol: DoesNotExist }");
assert!(errs.iter().any(|e| e.message.contains("not a declared session")), "{errs:?}");
}
#[test]
fn socket_with_zero_credit_window_is_rejected() {
let errs = errors(&format!("{SESSION}\nsocket ChatWS {{ protocol: Chat, backpressure: credit(0) }}"));
assert!(errs.iter().any(|e| e.message.contains("credit must be")), "{errs:?}");
}
}
#[cfg(test)]
mod fase41b_choice_tests {
use super::*;
use crate::lexer::Lexer;
use crate::parser::Parser;
use std::collections::BTreeMap;
fn parse_prog(src: &str) -> Program {
let toks = Lexer::new(src, "<t>").tokenize().expect("lex");
Parser::new(toks).parse().expect("parse")
}
fn session<'a>(p: &'a Program, name: &str) -> &'a SessionDefinition {
p.declarations
.iter()
.find_map(|d| match d {
Declaration::Session(s) if s.name == name => Some(s),
_ => None,
})
.expect("session declared")
}
fn role_of<'a>(s: &'a SessionDefinition, name: &str) -> &'a SessionRole {
s.roles.iter().find(|r| r.name == name).expect("role")
}
const CHOICE: &str = "session Negotiate {\n\
client: [select { ask: [send Query, receive Answer, end], quit: [end] }]\n\
server: [branch { ask: [receive Query, send Answer, end], quit: [end] }]\n\
}";
#[test]
fn select_branch_steps_parse_with_nested_arms() {
let p = parse_prog(CHOICE);
let s = session(&p, "Negotiate");
let client = role_of(s, "client");
assert_eq!(client.steps.len(), 1);
assert_eq!(client.steps[0].op, "select");
let labels: Vec<_> = client.steps[0].branches.iter().map(|b| b.label.as_str()).collect();
assert_eq!(labels, vec!["ask", "quit"]);
let ask = &client.steps[0].branches[0];
assert_eq!(ask.steps.iter().map(|s| s.op.as_str()).collect::<Vec<_>>(), vec!["send", "receive", "end"]);
}
#[test]
fn select_lowers_to_session_type_select() {
let p = parse_prog(CHOICE);
let client = lower_session_role(role_of(session(&p, "Negotiate"), "client"));
let mut arms = BTreeMap::new();
arms.insert("ask".to_string(), SessionType::send("Query", SessionType::recv("Answer", SessionType::End)));
arms.insert("quit".to_string(), SessionType::End);
assert_eq!(client, SessionType::Select(arms));
}
#[test]
fn select_is_dual_to_matching_branch() {
let p = parse_prog(CHOICE);
let s = session(&p, "Negotiate");
let client = lower_session_role(role_of(s, "client"));
let server = lower_session_role(role_of(s, "server"));
assert!(client.is_dual_to(&server));
assert!(server.is_dual_to(&client));
}
#[test]
fn choice_session_typechecks_clean() {
let errs = TypeChecker::new(&parse_prog(CHOICE)).check();
assert!(
!errs.iter().any(|e| e.message.contains("not dual") || e.message.contains("Session")),
"unexpected session error: {errs:?}"
);
}
#[test]
fn choice_with_duplicate_labels_is_rejected() {
let src = "session Bad {\n\
client: [select { ask: [end], ask: [end] }]\n\
server: [branch { ask: [end] }]\n\
}";
let errs = TypeChecker::new(&parse_prog(src)).check();
assert!(errs.iter().any(|e| e.message.contains("duplicate") || e.message.contains("label")), "{errs:?}");
}
#[test]
fn empty_choice_is_rejected() {
let src = "session Bad {\n\
client: [select { }]\n\
server: [branch { }]\n\
}";
let errs = TypeChecker::new(&parse_prog(src)).check();
assert!(errs.iter().any(|e| e.message.contains("at least one") || e.message.contains("branch")), "{errs:?}");
}
}
#[cfg(test)]
mod fase41c_credit_tests {
use super::*;
use crate::lexer::Lexer;
use crate::parser::Parser;
fn parse_prog(src: &str) -> Program {
let toks = Lexer::new(src, "<t>").tokenize().expect("lex");
Parser::new(toks).parse().expect("parse")
}
fn errors(src: &str) -> Vec<TypeError> {
TypeChecker::new(&parse_prog(src)).check()
}
fn has(errs: &[TypeError], needle: &str) -> bool {
errs.iter().any(|e| e.message.contains(needle))
}
const BURST_SESSION: &str =
"session Burst { client: [send A, send B, end] server: [receive A, receive B, end] }";
#[test]
fn credit_window_within_budget_is_accepted() {
let errs = errors(&format!(
"{BURST_SESSION}\nsocket S {{ protocol: Burst, backpressure: credit(2) }}"
));
assert!(!has(&errs, "credit-refined"), "unexpected credit error: {errs:?}");
assert!(!has(&errs, "violates"), "{errs:?}");
}
#[test]
fn burst_overflow_is_rejected() {
let errs = errors(&format!(
"{BURST_SESSION}\nsocket S {{ protocol: Burst, backpressure: credit(1) }}"
));
assert!(has(&errs, "credit-window overflow"), "expected burst overflow, got: {errs:?}");
assert!(has(&errs, "send-burst of 2"), "expected burst=2 detail, got: {errs:?}");
assert!(has(&errs, "credit(1)"), "expected budget=1 detail, got: {errs:?}");
let server_errs: Vec<_> = errs
.iter()
.filter(|e| e.message.contains("role 'server'") && e.message.contains("credit-refined"))
.collect();
assert!(server_errs.is_empty(), "server role should be clean: {server_errs:?}");
}
#[test]
fn unsustainable_loop_is_rejected_at_any_budget() {
let src = "session Drain {\n\
client: [send A, send B, receive Ack, loop]\n\
server: [receive A, receive B, send Ack, loop]\n\
}\nsocket S { protocol: Drain, backpressure: credit(100) }";
let errs = errors(src);
assert!(has(&errs, "unsustainable"), "expected loop unsustainability, got: {errs:?}");
assert!(has(&errs, "2 - 1 > 0"), "expected Δ detail, got: {errs:?}");
}
#[test]
fn balanced_loop_is_accepted_at_minimal_budget() {
let src = "session Pingpong {\n\
client: [send A, receive Ack, loop]\n\
server: [receive A, send Ack, loop]\n\
}\nsocket S { protocol: Pingpong, backpressure: credit(1) }";
let errs = errors(src);
assert!(!has(&errs, "credit-refined"), "{errs:?}");
assert!(!has(&errs, "unsustainable"), "{errs:?}");
}
#[test]
fn choice_arms_are_each_checked_under_budget() {
let src = "session Choice {\n\
client: [select { ask: [send Q, send R, end], quit: [end] }]\n\
server: [branch { ask: [receive Q, receive R, end], quit: [end] }]\n\
}\nsocket S { protocol: Choice, backpressure: credit(1) }";
let errs = errors(src);
assert!(has(&errs, "credit-window overflow"), "ask arm must overflow: {errs:?}");
let src_ok = src.replace("credit(1)", "credit(2)");
assert!(!has(&errors(&src_ok), "credit-refined"), "credit(2) should fit");
}
#[test]
fn no_backpressure_annotation_skips_credit_analysis() {
let errs = errors(&format!(
"{BURST_SESSION}\nsocket S {{ protocol: Burst }}"
));
assert!(!has(&errs, "credit-refined"), "{errs:?}");
assert!(!has(&errs, "violates"), "{errs:?}");
}
#[test]
fn zero_credit_still_caught_as_a_separate_diagnostic() {
let errs = errors(&format!(
"{BURST_SESSION}\nsocket S {{ protocol: Burst, backpressure: credit(0) }}"
));
assert!(has(&errs, "credit must be"), "41.b ≥ 1 check still fires: {errs:?}");
assert!(!has(&errs, "credit-window overflow"), "no overflow-walking on bad budget: {errs:?}");
}
}