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