#![allow(dead_code)]
use std::collections::HashMap;
use crate::ast::*;
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 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",
];
const VALID_EPISTEMIC_LEVELS: &[&str] = &["believe", "doubt", "know", "speculate"];
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_SEVERITY_LEVELS: &[&str] = &["critical", "high", "low", "medium"];
const VALID_OTS_HOMOTOPY: &[&str] = &["deep", "shallow", "speculative"];
const VALID_MANDATE_POLICIES: &[&str] = &["coerce", "halt", "retry"];
const VALID_STORE_BACKENDS: &[&str] = &["mysql", "postgresql", "sqlite"];
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"];
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(", ")
}
#[derive(Debug)]
pub struct TypeError {
pub message: String,
pub line: u32,
pub column: u32,
}
#[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>,
}
impl<'a> TypeChecker<'a> {
pub fn new(program: &'a Program) -> Self {
TypeChecker {
program,
symbols: SymbolTable::new(),
errors: Vec::new(),
warnings: Vec::new(),
}
}
pub fn check(mut self) -> Vec<TypeError> {
self.register_declarations(&self.program.declarations);
self.check_declarations(&self.program.declarations);
self.errors
}
pub fn check_with_warnings(mut self) -> (Vec<TypeError>, Vec<TypeError>) {
self.register_declarations(&self.program.declarations);
self.check_declarations(&self.program.declarations);
(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]) {
let mut registrations: Vec<(String, String, u32, Loc)> = Vec::new();
for decl in decls {
match decl {
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::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::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(),
));
}
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::Generic(n) => {
if !n.name.is_empty() {
registrations.push((
n.name.clone(),
n.keyword.clone(),
n.loc.line,
n.loc.clone(),
));
}
}
Declaration::Epistemic(_) => {
}
Declaration::Import(_) | Declaration::Run(_) | Declaration::Let(_) => {}
}
}
for (name, kind, line, loc) in registrations {
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 check_declarations(&mut self, decls: &[Declaration]) {
for decl in decls {
match decl {
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);
self.check_declarations(&eb.body);
}
Declaration::LambdaData(n) => self.check_lambda_data(n),
Declaration::Agent(n) => self.check_agent(n),
Declaration::Shield(n) => self.check_shield(n),
Declaration::Pix(n) => self.check_pix(n),
Declaration::Psyche(n) => self.check_psyche(n),
Declaration::Corpus(n) => self.check_corpus(n),
Declaration::Dataspace(_) => {} 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::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::Import(_)
| Declaration::Type(_)
| Declaration::Let(_)
| Declaration::Generic(_) => {}
}
}
}
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,
);
}
}
}
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_tool(&mut self, node: &ToolDefinition) {
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 {
let (base, qualifier) = match e.split_once(':') {
Some((b, q)) => (b, Some(q)),
None => (e.as_str(), None),
};
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 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 check_flow(&mut self, node: &FlowDefinition) {
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);
}
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_shield(&mut self, node: &ShieldDefinition) {
for cat in &node.scan {
if !is_valid(cat, VALID_SCAN_CATEGORIES) {
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,
);
}
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,
);
}
}
}
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_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() {
self.emit(
format!(
"Corpus '{}' requires at least one document or an mcp_server (G1: D ≠ ∅)",
node.name
),
&node.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,
);
}
if let Some(v) = node.kp {
if v <= 0.0 {
self.emit(
format!("kp must be > 0.0, got {} in mandate '{}'", v, node.name),
&node.loc,
);
}
}
if let Some(v) = node.ki {
if v < 0.0 {
self.emit(
format!("ki must be >= 0.0, got {} in mandate '{}'", v, node.name),
&node.loc,
);
}
}
if let Some(v) = node.kd {
if v < 0.0 {
self.emit(
format!("kd must be >= 0.0, got {} in mandate '{}'", v, node.name),
&node.loc,
);
}
}
if let Some(v) = node.tolerance {
if v <= 0.0 || v > 1.0 {
self.emit(
format!(
"tolerance must be in (0.0, 1.0], got {} in mandate '{}'",
v, node.name
),
&node.loc,
);
}
}
if let Some(v) = node.max_steps {
if v < 1 {
self.emit(
format!(
"max_steps must be >= 1, got {} in mandate '{}'",
v, node.name
),
&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.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);
}
}
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,
),
_ => {}
}
}
}
fn check_fabric(&mut self, node: &FabricDefinition) {
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) {
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) {
for (idx, step) in role.steps.iter().enumerate() {
if !matches!(step.op.as_str(), "send" | "receive" | "loop" | "end") {
self.emit(
format!(
"Session '{session_name}' role '{}' step #{idx} has invalid op '{}'",
role.name, step.op
),
&step.loc,
);
continue;
}
if matches!(step.op.as_str(), "send" | "receive") && step.message_type.is_empty() {
self.emit(
format!(
"Session '{session_name}' role '{}' step #{idx} '{}' \
requires a message type",
role.name, step.op
),
&step.loc,
);
}
}
}
fn check_session_duality(&mut self, node: &SessionDefinition) {
let r1 = &node.roles[0];
let r2 = &node.roles[1];
if r1.steps.len() != r2.steps.len() {
self.emit(
format!(
"Session '{}' duality violation: roles '{}' ({} steps) and \
'{}' ({} steps) have different lengths",
node.name,
r1.name,
r1.steps.len(),
r2.name,
r2.steps.len()
),
&node.loc,
);
return;
}
for (i, (s1, s2)) in r1.steps.iter().zip(r2.steps.iter()).enumerate() {
if !steps_dual(s1, s2) {
self.emit(
format!(
"Session '{}' duality violation at step #{i}: '{}' has \
'{}' but '{}' has '{}' (expected the dual)",
node.name,
r1.name,
format_step(s1),
r2.name,
format_step(s2)
),
&node.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 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,
);
}
}
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 let Some(v) = node.retries {
if v < 0 {
self.emit(
format!(
"retries must be >= 0, got {} in axonendpoint '{}'",
v, node.name
),
&node.loc,
);
}
}
}
fn check_flow_steps(&mut self, steps: &[FlowStep], flow_name: &str) {
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::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 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 '{}' in navigate 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.query_expr.is_empty() {
self.emit(
"Navigate step requires a query expression".to_string(),
&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) => {
if n.navigate_ref.is_empty() {
self.emit(
"Corroborate step requires a navigate_ref".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);
}
FlowStep::Retrieve(n) => {
self.check_store_ref(&n.store_name, flow_name, &n.loc);
}
FlowStep::Mutate(n) => {
self.check_store_ref(&n.store_name, flow_name, &n.loc);
}
FlowStep::Purge(n) => {
self.check_store_ref(&n.store_name, flow_name, &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) => {
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::Emit(n) => self.check_emit(n),
FlowStep::Publish(n) => self.check_publish(n),
FlowStep::Discover(n) => self.check_discover(n),
_ => {}
}
}
}
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 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,
),
_ => {}
}
}
for listener in &node.listeners {
self.check_listen(listener, &node.name);
}
}
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,
),
_ => {}
}
} else {
self.warn(
format!(
"daemon '{}' uses string topic '{}' which is deprecated since \
Fase 13 (v1.4.x). Migrate to a typed `channel` declaration; \
string topics will be removed in v2.0 (D4).",
daemon_name, node.channel
),
&node.loc,
);
}
}
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!(
"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!(
"publish gate '{}' is a {}, not a shield",
node.shield_ref, sh_kind
),
&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 steps_dual(s1: &SessionStep, s2: &SessionStep) -> bool {
match (s1.op.as_str(), s2.op.as_str()) {
("send", "receive") | ("receive", "send") => s1.message_type == s2.message_type,
("loop", "loop") | ("end", "end") => true,
_ => false,
}
}
fn format_step(s: &SessionStep) -> String {
if matches!(s.op.as_str(), "send" | "receive") {
format!("{} {}", s.op, s.message_type)
} else {
s.op.clone()
}
}
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 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 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
}
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 const W001_CODE: &str = "axon-W001";
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)> = 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 result = implicit_transport(ae, flow, program);
updates.push((i, result));
}
}
for (i, result) in updates {
if let Declaration::AxonEndpoint(ae) = &mut program.declarations[i] {
ae.implicit_transport = 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_clean() {
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!(warns.is_empty(), "no warnings expected: {:?}", warns);
}
#[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_emits_d4_warning() {
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("deprecated since Fase 13"));
assert!(warns[0].message.contains("orders.created"));
}
#[test]
fn listen_dual_mode_only_legacy_warns() {
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(), 1, "only legacy emits a warning");
assert!(warns[0].message.contains("legacy"));
}
#[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
);
}
}