1use rustev_contract::Document;
4use rustev_contract::definition::Definition;
5use rustev_contract::execution::ExecutionPolicy;
6use rustev_contract::limits::{DEFINITION_V1, DESCRIPTOR_V1, PLAN_V1};
7use rustev_contract::plan::{BoundCalibration, Plan, PlanExecution, PlanStepDetail};
8use rustev_core::compile::{Category, ShortfallReason};
9use rustev_core::{Compiled, LoadError, Refusal, compile, compile_with};
10use serde_json::{Value, json};
11
12use crate::deps::{self, DepArgs, DepFail, Deps, RulesHook};
13use crate::host::Host;
14use crate::out::{Done, Out, Res, code};
15
16pub fn category_name(c: Category) -> &'static str {
18 match c {
19 Category::Parse => "parse",
20 Category::UnknownOperator => "unknown_operator",
21 Category::KindMismatch => "kind_mismatch",
22 Category::Cycle => "cycle",
23 Category::NoCapableBackend => "no_capable_backend",
24 Category::UncalibratedThreshold => "uncalibrated_threshold",
25 Category::Budget => "budget",
26 Category::UnhandledUnresolved => "unhandled_unresolved",
27 Category::InvalidFallback => "invalid_fallback",
28 Category::CalibrationBinding => "calibration_binding",
29 Category::InvalidDefinition => "invalid_definition",
30 Category::InvalidExecution => "invalid_execution",
31 Category::GradeRequirement => "grade_requirement",
32 Category::UncorroboratedHint => "uncorroborated_hint",
33 Category::SourceBinding => "source_binding",
34 Category::UncoveredProvenance => "uncovered_provenance",
36 Category::CallerCannotAct => "caller_cannot_act",
37 Category::InvalidScreening => "invalid_screening",
38 Category::UnscreenedText => "unscreened_text",
39 Category::InvalidAcquisition => "invalid_acquisition",
40 Category::SourceMapping => "source_mapping",
41 Category::InvalidGeneration => "invalid_generation",
42 Category::Template => "template",
43 Category::ModelOutputFlow => "model_output_flow",
44 }
45}
46
47fn reason_json(r: &ShortfallReason) -> Value {
48 match r {
49 ShortfallReason::ScreenProfile => json!({"kind": "screen_profile"}),
50 ShortfallReason::ScreenTextLimit { max, needed } => {
51 json!({"kind": "screen_text_limit", "max": max, "needed": needed})
52 }
53 ShortfallReason::ScreenInputExcess => json!({"kind": "screen_input_excess"}),
54 ShortfallReason::OutputLimit { max, needed } => {
55 json!({"kind": "output_limit", "max": max, "needed": needed})
56 }
57 ShortfallReason::Language { required } => json!({"kind": "language", "required": required}),
58 ShortfallReason::UnknownBackend => json!({"kind": "unknown_backend"}),
59 ShortfallReason::Operation(op) => json!({"kind": "operation", "operation": op}),
60 ShortfallReason::OutputKind { offered, required } => {
61 json!({"kind": "output_kind", "offered": offered, "required": required})
62 }
63 ShortfallReason::Options { max, needed } => {
64 json!({"kind": "options", "max": max, "needed": needed})
65 }
66 ShortfallReason::InputLimit { max, needed } => {
67 json!({"kind": "input_limit", "max": max, "needed": needed})
68 }
69 ShortfallReason::Determinism { offered, required } => {
70 json!({"kind": "determinism", "offered": offered, "required": required})
71 }
72 ShortfallReason::ArtifactPin { pinned, offered } => {
73 json!({"kind": "artifact_pin", "pinned": pinned, "offered": offered})
74 }
75 }
76}
77
78pub fn refused(command: &str, r: &Refusal) -> Done {
80 let shortfalls: Vec<Value> = r
81 .shortfalls
82 .iter()
83 .map(|s| {
84 json!({
85 "backend_id": s.backend_id,
86 "reasons": s.reasons.iter().map(reason_json).collect::<Vec<_>>(),
87 })
88 })
89 .collect();
90 Done::new(
91 code::REFUSED,
92 Out::new(command, "refused")
93 .set("category", &(r.category as u8))
94 .set("category_name", category_name(r.category))
95 .set("subject", &r.subject)
96 .set("detail", &r.detail)
97 .set("shortfalls", &shortfalls),
98 )
99}
100
101fn parse_refusal(subject: String, detail: String) -> Refusal {
102 Refusal {
103 category: Category::Parse,
104 subject,
105 detail,
106 shortfalls: vec![],
107 }
108}
109
110pub fn load_refused(command: &str, e: &LoadError) -> Done {
112 let out = Out::new(command, "refused").set("detail", &e.to_string());
113 let out = match e {
114 LoadError::CompilerChanged {
115 recorded_compiler,
116 current_compiler,
117 recorded_registry,
118 current_registry,
119 } => out.set("load_error", "compiler_changed").set(
120 "identities",
121 &json!({
122 "recorded_compiler": recorded_compiler,
123 "current_compiler": current_compiler,
124 "recorded_registry": recorded_registry,
125 "current_registry": current_registry,
126 }),
127 ),
128 LoadError::MissingDescriptor {
129 backend_id,
130 descriptor,
131 } => out
132 .set("load_error", "missing_descriptor")
133 .set("backend_id", backend_id)
134 .set("descriptor", descriptor),
135 LoadError::MissingCalibration { step, calibration } => out
136 .set("load_error", "missing_calibration")
137 .set("step", step)
138 .set("calibration", calibration),
139 LoadError::PlanMismatch { refusal } => {
140 let out = out.set("load_error", "plan_mismatch");
141 match refusal {
142 Some(r) => out.set(
143 "refusal",
144 &json!({
145 "category": r.category as u8,
146 "category_name": category_name(r.category),
147 "subject": r.subject,
148 "detail": r.detail,
149 }),
150 ),
151 None => out,
152 }
153 }
154 };
155 Done::new(code::REFUSED, out)
156}
157
158pub fn dep_failed(command: &str, e: DepFail) -> Done {
160 match e {
161 DepFail::Io(e) => Done::io(command, e),
162 DepFail::Parse { subject, detail } => refused(command, &parse_refusal(subject, detail)),
163 DepFail::Rules { path, detail } => Done::invalid(command, &path, detail),
164 }
165}
166
167pub fn read_plan(command: &str, host: &dyn Host, path: &str) -> Result<Plan, Done> {
169 let bytes = host
170 .read(path, PLAN_V1.max_bytes)
171 .map_err(|e| Done::io(command, e))?;
172 #[cfg(not(feature = "parameterized"))]
173 rustev_core::unparameterized::plan_bytes(&bytes).map_err(|r| refused(command, &r))?;
174 Plan::parse(&bytes).map_err(|e| {
175 refused(
176 command,
177 &parse_refusal(format!("plan:{path}"), e.to_string()),
178 )
179 })
180}
181
182pub fn load_plan(command: &str, plan: &Plan, deps: &Deps) -> Result<Compiled, Done> {
184 Compiled::load_checked(plan, &deps.descriptors, &deps.calibrations)
185 .map_err(|e| load_refused(command, &e))
186}
187
188#[derive(Debug, Clone, Copy)]
190pub struct CompileArgs<'a> {
191 pub definition: &'a str,
192 pub deps: DepArgs<'a>,
193 pub execution: Option<&'a str>,
194}
195
196fn compile_definition(
197 command: &str,
198 host: &dyn Host,
199 args: CompileArgs<'_>,
200 rules: &mut RulesHook<'_>,
201) -> Result<Compiled, Done> {
202 let def_path = args.definition;
203 let bytes = host
204 .read(def_path, DEFINITION_V1.max_bytes)
205 .map_err(|e| Done::io(command, e))?;
206 compile_definition_bytes(command, host, args, rules, &bytes)
207}
208
209pub(crate) fn compile_definition_bytes(
210 command: &str,
211 host: &dyn Host,
212 args: CompileArgs<'_>,
213 rules: &mut RulesHook<'_>,
214 bytes: &[u8],
215) -> Result<Compiled, Done> {
216 #[cfg(not(feature = "parameterized"))]
219 rustev_core::unparameterized::definition(bytes).map_err(|r| refused(command, &r))?;
220 let def = Definition::parse(bytes)
221 .map_err(|e| refused(command, &parse_refusal("definition".into(), e.to_string())))?;
222 let deps = deps::load(host, args.deps, rules).map_err(|e| dep_failed(command, e))?;
223 let execution = match args.execution {
224 None => None,
225 Some(path) => {
226 let bytes = host
227 .read(path, DESCRIPTOR_V1.max_bytes)
228 .map_err(|e| Done::io(command, e))?;
229 Some(ExecutionPolicy::parse(&bytes).map_err(|e| {
230 refused(
231 command,
232 &parse_refusal(format!("execution:{path}"), e.to_string()),
233 )
234 })?)
235 }
236 };
237 match &execution {
238 None => compile(&def, &deps.descriptors, &deps.calibrations),
239 Some(x) => compile_with(&def, &deps.descriptors, &deps.calibrations, x),
240 }
241 .map_err(|r| refused(command, &r))
242}
243
244pub fn check(
246 command: &str,
247 host: &dyn Host,
248 args: CompileArgs<'_>,
249 rules: &mut RulesHook<'_>,
250) -> Res {
251 let c = compile_definition(command, host, args, rules)?;
252 Ok(checked(command, &c))
253}
254
255pub(crate) fn checked(command: &str, c: &Compiled) -> Done {
256 Done::ok(Out::new(command, "ok").set("plan_id", &c.id))
257}
258
259pub fn compile_cmd(
261 command: &str,
262 host: &dyn Host,
263 args: CompileArgs<'_>,
264 rules: &mut RulesHook<'_>,
265 out: &str,
266) -> Res {
267 host.ensure_absent(out).map_err(|e| Done::io(command, e))?;
268 let c = compile_definition(command, host, args, rules)?;
269 publish_compiled(command, host, &c, out)
270}
271
272pub(crate) fn publish_compiled(command: &str, host: &dyn Host, c: &Compiled, out: &str) -> Res {
273 host.create(out, &c.canonical())
274 .map_err(|e| Done::io(command, e))?;
275 Ok(Done::ok(
276 Out::new(command, "compiled")
277 .set("plan_id", &c.id)
278 .set("out", out),
279 ))
280}
281
282fn step_json(step: &rustev_contract::plan::PlanStep) -> Value {
283 let detail = match &step.detail {
284 PlanStepDetail::Exact { op, version } => {
285 json!({"kind": "exact", "op": op, "version": version})
286 }
287 PlanStepDetail::Semantic(b) => json!({
288 "kind": "semantic",
289 "backend_id": b.backend_id,
290 "artifact": b.artifact,
291 "descriptor": b.descriptor,
292 "output": b.output,
293 "requires": b.requires,
294 "normalization": b.normalization,
295 "calibration": match &b.calibration {
296 BoundCalibration::None => json!("none"),
297 BoundCalibration::Bound { id, .. } => json!(id),
298 },
299 "fallback_taken": b.fallback_taken,
300 "max_requests": b.max_requests,
301 }),
302 PlanStepDetail::Unsupported { capability } => {
303 json!({"kind": "unsupported", "capability": capability})
304 }
305 PlanStepDetail::Generate(_) => json!({"kind": "generate"}),
306 PlanStepDetail::Corroborate => json!({"kind": "corroborate"}),
307 PlanStepDetail::Acquire => json!({"kind": "acquire"}),
308 PlanStepDetail::Widen => json!({"kind": "widen"}),
309 };
310 json!({"id": step.id, "derivation": step.derivation, "detail": detail})
311}
312
313pub fn show(
315 command: &str,
316 host: &dyn Host,
317 plan: &str,
318 deps_args: DepArgs<'_>,
319 rules: &mut RulesHook<'_>,
320) -> Res {
321 let path = plan;
322 let plan = read_plan(command, host, plan)?;
323 let out = instantiation(command, path, Out::new(command, "ok"), &plan)?;
326 let deps = deps::load(host, deps_args, rules).map_err(|e| dep_failed(command, e))?;
327 let c = load_plan(command, &plan, &deps)?;
328 let plan = &c.plan;
329 let (execution, fallbacks) = match &plan.execution {
330 PlanExecution::None => (json!("none"), vec![]),
331 PlanExecution::Declared(d) => (
332 json!(d.id),
333 d.fallbacks
334 .iter()
335 .map(|f| {
336 json!({
337 "step": f.step,
338 "target": f.target,
339 "backend_id": f.backend_id,
340 "artifact": f.artifact,
341 "descriptor": f.descriptor,
342 "output": f.output,
343 })
344 })
345 .collect(),
346 ),
347 };
348 let def = &plan.definition;
349 let out = out
350 .set("plan_id", &c.id)
351 .set("compiler", &plan.compiler)
352 .set("registry", &plan.registry)
353 .set("definition_id", &plan.definition_id)
354 .set(
355 "definition",
356 &json!({"package": def.package, "name": def.name, "version": def.version}),
357 )
358 .set("execution", &execution)
359 .set("fallbacks", &fallbacks)
360 .set("order", &plan.order)
361 .set(
362 "steps",
363 &plan.steps.iter().map(step_json).collect::<Vec<_>>(),
364 );
365 Ok(Done::ok(actionability(out, plan)))
366}
367
368fn actionability(out: Out, plan: &rustev_contract::plan::Plan) -> Out {
372 let (Some(a), Some(table)) = (&plan.definition.actionability, &plan.actionability) else {
373 return out;
374 };
375 out.set("caller_controlled", &a.caller_controlled)
376 .set(
377 "checks",
378 &table
379 .checks
380 .iter()
381 .map(|c| {
382 json!({
383 "id": c.check,
384 "subject": c.subject,
385 "branches": c.branches.iter().map(|b| json!({
386 "classes": b.classes,
387 "consequence": b.consequence,
388 "party": b.party,
389 })).collect::<Vec<_>>(),
390 })
391 })
392 .collect::<Vec<_>>(),
393 )
394 .set(
395 "constraint_groups",
396 &a.constraint_groups
397 .iter()
398 .map(|g| json!({"name": g.name, "checks": g.checks}))
399 .collect::<Vec<_>>(),
400 )
401 .set(
402 "remediations",
403 &a.remediations
404 .iter()
405 .map(|r| json!({"code": r.code, "for": r.for_party}))
406 .collect::<Vec<_>>(),
407 )
408 .set(
409 "questions",
410 &a.questions.iter().map(|q| &q.id).collect::<Vec<_>>(),
411 )
412}
413
414fn instantiation(command: &str, path: &str, out: Out, plan: &Plan) -> Result<Out, Done> {
416 let Some(i) = &plan.instantiation else {
417 return Ok(out);
418 };
419 let version = i.template.get("version").and_then(Value::as_str);
420 let version = version
421 .filter(|v| v.parse::<rustev_contract::version::Version>().is_ok())
422 .ok_or_else(|| {
423 refused(
424 command,
425 &parse_refusal(
426 format!("plan:{path}"),
427 "instantiation.template.version must be a string containing a valid definition version (MAJOR.MINOR.PATCH without leading zeros or suffixes)".into(),
428 ),
429 )
430 })?;
431 Ok(out.set("plan_schema", &plan.schema).set(
432 "instantiation",
433 &json!({
434 "template_id": i.template_id, "template_version": version,
435 "variant": i.variant, "chain": i.profile,
436 "parameters": i.parameters.iter().map(|p| json!({
437 "name": p.name, "value": p.value, "source": p.source,
438 "site_count": p.sites.len(),
439 })).collect::<Vec<_>>(),
440 }),
441 ))
442}