Merge B3: route-proof readiness, held records, strict challenge reader

ReadinessClass::of is the one class authority; recipes declare readiness.
Held records are never aborted by Idunn; cancel accepts a held pre-fence
Deploy. Silent means no byte; any byte then a stop is Refused. Soul: merge
with follow-ups, safe to install on Yggdrasil.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-29 20:34:00 +02:00
commit d32395ac99
3 changed files with 4380 additions and 331 deletions

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,7 @@ use cultnet_rs::{
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::control_plane::SequenceAdmittedReady;
use crate::control_plane::{ODIN_RENDEZVOUS_CAPABILITY, ReadinessClass, SequenceAdmittedReady};
use crate::deployment::{
CapabilityDependency, DeadlineBinding, DependencyKind, ExternalCapabilityBinding, OperatorBinding,
ServiceTransport, SourceSelectionPolicy, StartupOrder, StateDeclaration, TargetDeclaration,
@ -640,6 +640,25 @@ impl CompiledDeploymentPlan {
Ok((declaration, binding))
}
/// How this plan's target proves readiness, from its recipe's declarations
/// and its binding's route, or the typed refusal that it declares none.
/// Admission asks this before anything is sealed or installed.
pub(crate) fn readiness_class(&self) -> Result<ReadinessClass> {
let (declaration, binding) = self.parsed_inputs()?;
Ok(ReadinessClass::declared(
&declaration.target,
declaration
.provides
.iter()
.any(|provided| provided.capability == ODIN_RENDEZVOUS_CAPABILITY),
declaration.dependencies.iter().any(|dependency| {
dependency.kind == DependencyKind::SharedInfrastructure
&& dependency.capability == ODIN_RENDEZVOUS_CAPABILITY
}),
binding.route.is_some(),
)?)
}
fn recomputed_plan_id(&self) -> Result<String> {
let mut unsigned = self.clone();
unsigned.plan_id.clear();
@ -1547,6 +1566,71 @@ nodes = ["yggdrasil"]
);
}
#[test]
fn a_plan_names_how_its_target_proves_readiness_and_agrees_with_its_expected() {
use crate::control_plane::UndeclaredReadiness;
let unrouted_binding = {
let (head, tail) = BINDING.split_once("[route]").unwrap();
format!("{head}[brakes]{}", tail.split_once("[brakes]").unwrap().1)
};
let unrouted = |recipe: &str| {
recipe
.replace("route_required = true", "route_required = false")
.replace(
r#"["GAMECULT_IDUNN_CANDIDATE_BIND", "GAMECULT_IDUNN_RUNTIME_BUNDLE"]"#,
r#"["GAMECULT_IDUNN_RUNTIME_BUNDLE"]"#,
)
};
let without_odin = RECIPE.split("[[dependencies]]").next().unwrap().to_owned();
let provides_odin = |recipe: &str| {
recipe.replace("capability = \"service.runtime\"", "capability = \"odin.verse-rendezvous\"")
};
let cases: [(&str, String, &str, Option<u16>, Result<ReadinessClass, ()>); 6] = [
("depends on Odin, routed", RECIPE.to_owned(), BINDING, Some(18001), Ok(ReadinessClass::OdinCorrelated)),
("routed, no Odin", without_odin.clone(), BINDING, Some(18001), Ok(ReadinessClass::RouteProof)),
("unrouted, no Odin", unrouted(&without_odin), &unrouted_binding, None, Err(())),
("provides Odin, unrouted", provides_odin(&unrouted(&without_odin)), &unrouted_binding, None, Ok(ReadinessClass::OdinSelf)),
("depends on Odin, unrouted", unrouted(RECIPE), &unrouted_binding, None, Ok(ReadinessClass::OdinCorrelated)),
// Only the shared-infrastructure declaration is the declaration.
("requires the Odin capability, routed", RECIPE.replace("kind = \"shared-infrastructure\"", "kind = \"required\""), BINDING, Some(18001), Ok(ReadinessClass::RouteProof)),
];
for (name, recipe, binding, port, outcome) in cases {
let providers = [ready_odin_provider("odin", "odin-yggdrasil", 1)];
let plan = compile_deployment_plan(
recipe.as_bytes(),
binding.as_bytes(),
source(&recipe),
"service-incarnation-1",
port,
110,
if recipe.contains("[[dependencies]]") { &providers } else { &[] },
)
.unwrap_or_else(|error| panic!("{name}: {error:#}"));
let release = SealedRelease::new(
&plan,
vec![artifact_receipt()],
vec![external_input_receipt()],
120,
)
.unwrap();
let expected = release.expected_projection(&plan).unwrap();
match outcome {
Ok(class) => {
assert_eq!(plan.readiness_class().unwrap(), class, "{name}");
assert_eq!(ReadinessClass::of(&expected), Ok(class), "{name}");
}
Err(()) => {
let error = plan.readiness_class().expect_err(name);
let undeclared = UndeclaredReadiness {
target: "service".into(),
};
assert_eq!(error.downcast_ref::<UndeclaredReadiness>(), Some(&undeclared), "{name}");
assert_eq!(ReadinessClass::of(&expected), Err(undeclared), "{name}");
}
}
}
}
#[test]
fn expected_projection_is_sanitized_and_names_the_selected_graph() {
let plan = plan();

File diff suppressed because it is too large Load diff