use noether_engine::lagrange::{
resolve_deprecated_stages, resolve_pinning, ChainEvent, CompositionGraph,
};
use noether_store::StageStore;
pub fn resolve_and_emit_diagnostics(
graph: &mut CompositionGraph,
store: &dyn StageStore,
) -> Result<(), String> {
let report =
resolve_pinning(&mut graph.root, store).map_err(|e| format!("pinning resolution: {e}"))?;
for rw in &report.rewrites {
eprintln!(
"Info: {:?}-pinned stage {} resolved to {}",
rw.pinning,
short(&rw.before),
short(&rw.after),
);
}
for w in &report.warnings {
eprintln!(
"Warning: signature {} has {} Active implementations ({}) — \
picked {} deterministically, but the store's ≤1-Active-per-\
signature invariant is violated. Deprecate the duplicates \
via `noether stage activate` / `noether store retro`.",
short(&w.signature_id),
w.active_implementation_ids.len(),
w.active_implementation_ids
.iter()
.map(|id| short(id).to_string())
.collect::<Vec<_>>()
.join(", "),
short(&w.chosen),
);
}
let dep_report = resolve_deprecated_stages(&mut graph.root, store);
for rw in &dep_report.rewrites {
eprintln!(
"Warning: stage {} is deprecated → resolved to successor {}",
short(&rw.from.0),
short(&rw.to.0),
);
}
for event in &dep_report.events {
match event {
ChainEvent::CycleDetected { stage } => {
eprintln!(
"Warning: deprecation cycle detected at stage {} — \
the graph keeps the last distinct id before the \
cycle; the store has corrupt deprecation data \
and should be repaired.",
short(&stage.0)
);
}
ChainEvent::MaxHopsExceeded { stage } => {
eprintln!(
"Warning: deprecation chain at stage {} exceeded \
the {}-hop cap — execution continues with the \
chain truncated, but the chain should be flattened \
in the store.",
short(&stage.0),
noether_engine::lagrange::MAX_DEPRECATION_HOPS,
);
}
}
}
Ok(())
}
fn short(id: &str) -> &str {
id.get(..8).unwrap_or(id)
}
#[cfg(test)]
mod tests {
use super::*;
use noether_core::effects::EffectSet;
use noether_core::stage::{
compute_signature_id, compute_stage_id, CostEstimate, SignatureId, Stage, StageId,
StageLifecycle, StageSignature,
};
use noether_core::types::NType;
use noether_engine::lagrange::{CompositionNode, Pinning};
use noether_store::MemoryStore;
use std::collections::BTreeSet;
fn make_stage(name: &str, impl_hash: &str, lifecycle: StageLifecycle) -> Stage {
let signature = StageSignature {
input: NType::Text,
output: NType::Text,
effects: EffectSet::pure(),
implementation_hash: impl_hash.into(),
};
let id = compute_stage_id(name, &signature).unwrap();
let signature_id = compute_signature_id(
name,
&signature.input,
&signature.output,
&signature.effects,
)
.unwrap();
Stage {
id,
signature_id: Some(signature_id),
signature,
capabilities: BTreeSet::new(),
cost: CostEstimate {
time_ms_p50: None,
tokens_est: None,
memory_mb: None,
},
description: "test".into(),
examples: vec![],
lifecycle,
ed25519_signature: None,
signer_public_key: None,
implementation_code: None,
implementation_language: None,
ui_style: None,
tags: vec![],
aliases: vec![],
name: Some(name.into()),
properties: Vec::new(),
}
}
#[test]
fn resolve_and_emit_handles_signature_pinning() {
let mut store = MemoryStore::new();
let stage = make_stage("sigpin_stage", "impl_hash_a", StageLifecycle::Active);
let sig_id: SignatureId = stage.signature_id.clone().unwrap();
let expected_impl = stage.id.clone();
store.put(stage).unwrap();
let mut graph = CompositionGraph::new(
"t",
CompositionNode::Stage {
id: StageId(sig_id.0.clone()),
pinning: Pinning::Signature,
config: None,
},
);
resolve_and_emit_diagnostics(&mut graph, &store).unwrap();
match &graph.root {
CompositionNode::Stage { id, .. } => {
assert_eq!(*id, expected_impl, "signature pin must rewrite to impl id");
}
other => panic!("expected Stage, got {other:?}"),
}
}
#[test]
fn composition_id_is_unstable_across_resolution() {
use noether_engine::lagrange::compute_composition_id;
let mut store = MemoryStore::new();
let stage = make_stage("sig_stable", "impl_x", StageLifecycle::Active);
let sig_id = stage.signature_id.clone().unwrap();
store.put(stage).unwrap();
let build_graph = || {
CompositionGraph::new(
"t",
CompositionNode::Stage {
id: StageId(sig_id.0.clone()),
pinning: Pinning::Signature,
config: None,
},
)
};
let pre = build_graph();
let pre_id = compute_composition_id(&pre).unwrap();
let mut post = build_graph();
resolve_and_emit_diagnostics(&mut post, &store).unwrap();
let post_id = compute_composition_id(&post).unwrap();
assert_ne!(
pre_id, post_id,
"compose.rs must compute composition_id on the pre-resolution \
graph — if these ids match, the resolver is a no-op here and \
this test needs a more aggressive rewrite, not a silent fix"
);
}
#[test]
fn short_does_not_panic_on_non_ascii() {
assert_eq!(super::short("abcdefghij"), "abcdefgh");
assert_eq!(super::short("abc"), "abc");
assert_eq!(super::short(""), "");
assert_eq!(super::short("abcdefgé"), "abcdefgé");
}
}