Skip to main content

rustev_contract/
schema_emit.rs

1//! JSON Schema (draft 2020-12) for every public document type and version
2//! (spec 027, 3.2.2), derived from the same type definitions the bounded
3//! parser enforces. Emission is pure and deterministic: canonical JSON with
4//! sorted keys, byte-identical across runs and targets. The schemas are
5//! descriptive; the bounded parser stays the only authority on validity.
6
7use crate::bounded::ParseLimits;
8use crate::{Document, schema};
9
10/// A tagged content digest, `sha256:` and 64 lowercase hex digits.
11pub(crate) fn digest_schema() -> schemars::Schema {
12    schemars::json_schema!({
13        "type": "string",
14        "pattern": "^sha256:[0-9a-f]{64}$"
15    })
16}
17
18/// One emitted schema: the file name it is committed under and its
19/// canonical bytes.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct EmittedSchema {
22    pub file: String,
23    pub bytes: Vec<u8>,
24}
25
26/// Versions that share one wire type (judgment 1 and 2, evidence 1 and 2,
27/// and the other #76 track pairs) share its schema except for `schema`: the
28/// bounded parser reads both versions with that one type and accepts a later
29/// version's members under the earlier `schema` string, so a stricter
30/// earlier schema would describe something the parser does not enforce.
31/// The schema describes the parser; it is never stricter than it (3.2.2).
32///
33/// The JSON Schema of `T` for the document version `version`: draft
34/// 2020-12, `schema` fixed to `version`, the parse limits the bounded parser
35/// enforces recorded as `x-rustev-parse-limits` (a schema cannot express
36/// byte, depth or value-count bounds), canonical bytes.
37/// What every emitted schema says about itself (spec 027, 3.2.2).
38const COMMENT: &str = "Descriptive, derived from the types the bounded parser reads; the parser \
39is the only authority on validity. Where the parser reads two versions of a document with one \
40type, this schema admits what that type admits, including members the later version adds. \
41Byte, depth and count bounds are recorded in x-rustev-parse-limits (a string length here counts \
42characters, the parser counts bytes), and rules applied when a plan is compiled are stated in \
43field descriptions, not enforced here.";
44
45fn emit<T: schemars::JsonSchema + Document>(version: &str, part: Option<&str>) -> EmittedSchema {
46    emit_adjusted::<T>(version, part, |_| {})
47}
48
49/// [`emit`], with `adjust` applied to the derived schema before the title,
50/// comment, version and limits are set: for a document whose parser holds a
51/// version rule the shared type cannot express.
52fn emit_adjusted<T: schemars::JsonSchema + Document>(
53    version: &str,
54    part: Option<&str>,
55    adjust: impl FnOnce(&mut serde_json::Value),
56) -> EmittedSchema {
57    let generator = schemars::generate::SchemaSettings::draft2020_12().into_generator();
58    let root = generator.into_root_schema_for::<T>();
59    let mut v = serde_json::to_value(&root).expect("a schema is JSON");
60    adjust(&mut v);
61    let title = match part {
62        Some(p) => format!("{version} {p}"),
63        None => version.to_string(),
64    };
65    v["title"] = title.clone().into();
66    v["$comment"] = COMMENT.into();
67    // Every document is an object with a `schema` member; a schema without
68    // `properties` would be a defect in the derivation, never skipped. The
69    // committed-bytes test emits every document, so such a defect fails it
70    // naming the document, before any schema is committed or shipped.
71    v.get_mut("properties")
72        .and_then(|p| p.as_object_mut())
73        .unwrap_or_else(|| panic!("{title}: the derived schema has no properties"))
74        .insert(
75            "schema".into(),
76            serde_json::json!({ "type": "string", "const": version }),
77        );
78    v["x-rustev-parse-limits"] = limits(&T::LIMITS);
79    let file = format!(
80        "{}{}.schema.json",
81        version.replace('/', "-"),
82        part.map(|p| format!(".{p}")).unwrap_or_default()
83    );
84    let bytes = crate::canonical::canonical_value_bytes(&v).expect("a schema has canonical bytes");
85    EmittedSchema { file, bytes }
86}
87
88/// The members only `rustev.run/2` carries (spec 020, 3.2). The run
89/// record's parser refuses them in every other version and requires the list
90/// in version 2, so each run schema states exactly that.
91const RUN_V2_MEMBERS: [&str; 2] = ["remote_evidence", "remote_evidence_truncated"];
92
93fn run_members(version: &str) -> impl FnOnce(&mut serde_json::Value) + '_ {
94    move |v| {
95        if version != schema::RUN_V3 {
96            extraction_v1(v, "/$defs/EvidenceRecord/properties");
97            v["$defs"]["EvidenceRecord"]["properties"]
98                .as_object_mut()
99                .unwrap()
100                .remove("generation");
101            v["$defs"]["EvidenceRecord"]["properties"]
102                .as_object_mut()
103                .unwrap()
104                .remove("acquisitions");
105            v["$defs"]["StepEvidence"]["properties"]
106                .as_object_mut()
107                .unwrap()
108                .remove("constructed");
109            // Version 1 and 2 documents only; evidence/2 and run/3 keep `ranking` and `selection` (spec 039 3.13).
110            if let Some(p) = v["$defs"]["StepEvidence"]["properties"].as_object_mut() {
111                p.remove("ranking");
112                p.remove("selection");
113            }
114            v["$defs"]["StepStatus"]["oneOf"]
115                .as_array_mut()
116                .unwrap()
117                .retain(|v| v["properties"].get("skipped").is_none());
118            v["properties"]
119                .as_object_mut()
120                .unwrap()
121                .remove("acquisitions");
122            v["$defs"]["RequestRecord"]["properties"]
123                .as_object_mut()
124                .unwrap()
125                .remove("screen");
126            v["$defs"]["RequestResult"]["oneOf"]
127                .as_array_mut()
128                .unwrap()
129                .retain(|variant| variant["const"] != "withdrawn");
130            prune_defs(v);
131        }
132        if version == schema::RUN_V2 {
133            // The list is required and never null: `null` reads as absent,
134            // which the parser refuses in version 2.
135            v["required"]
136                .as_array_mut()
137                .expect("the run record has required members")
138                .push(RUN_V2_MEMBERS[0].into());
139            let list = &mut v["properties"][RUN_V2_MEMBERS[0]];
140            assert_eq!(
141                list["type"],
142                serde_json::json!(["array", "null"]),
143                "the remote evidence list is derived as an optional array"
144            );
145            list["type"] = "array".into();
146            return;
147        }
148        let props = v["properties"]
149            .as_object_mut()
150            .expect("the run record has properties");
151        for m in RUN_V2_MEMBERS {
152            props
153                .remove(m)
154                .unwrap_or_else(|| panic!("the run record has no {m} member"));
155        }
156        prune_defs(v);
157    }
158}
159
160/// Instantiation belongs only to plan 3 (spec 034, 3.10.1), as enforced
161/// by `Plan::parse`, including an explicitly null member in an earlier plan.
162fn plan_members(version: &str) -> impl FnOnce(&mut serde_json::Value) + '_ {
163    move |v| {
164        if version != schema::PLAN_V3 {
165            if let Some(variants) = v["$defs"]["PlanStepDetail"]["oneOf"].as_array_mut() {
166                variants.retain(|v| {
167                    [
168                        "generate",
169                        "extract",
170                        "accept_model_derived",
171                        "rank",
172                        "select",
173                    ]
174                    .iter()
175                    .all(|key| v["properties"].get(*key).is_none())
176                });
177            }
178            v["properties"]
179                .as_object_mut()
180                .unwrap()
181                .remove("acquisitions");
182            v["properties"]
183                .as_object_mut()
184                .expect("the plan has properties")
185                .remove("instantiation")
186                .expect("the plan has an instantiation member");
187            v["properties"]
188                .as_object_mut()
189                .unwrap()
190                .remove("screening")
191                .expect("the plan has a screening member");
192            prune_defs(v);
193        }
194    }
195}
196
197/// Keep spec 038 members out of version 1 result schemas.
198fn extraction_v1(v: &mut serde_json::Value, evidence_properties: &str) {
199    if let Some(props) = v
200        .pointer_mut(evidence_properties)
201        .and_then(|p| p.as_object_mut())
202    {
203        props.remove("extraction");
204        props.remove("acceptance");
205    }
206    for (ty, member) in [("Lineage", "producers"), ("InputEvidence", "producer")] {
207        if let Some(props) = v["$defs"][ty]["properties"].as_object_mut() {
208            props.remove(member);
209        }
210    }
211    if let Some(variants) = v["$defs"]["OutValue"]["oneOf"].as_array_mut() {
212        variants.retain(|v| v["properties"].get("attributed").is_none());
213    }
214}
215
216/// Removes every `$defs` entry no longer reachable from the schema's own
217/// members, so a version without a member does not describe its types.
218fn prune_defs(v: &mut serde_json::Value) {
219    fn refs(v: &serde_json::Value, out: &mut Vec<String>) {
220        match v {
221            serde_json::Value::Object(m) => {
222                if let Some(r) = m.get("$ref").and_then(|r| r.as_str())
223                    && let Some(name) = r.strip_prefix("#/$defs/")
224                {
225                    out.push(name.to_string());
226                }
227                m.values().for_each(|x| refs(x, out));
228            }
229            serde_json::Value::Array(a) => a.iter().for_each(|x| refs(x, out)),
230            _ => {}
231        }
232    }
233    let Some(defs) = v.get("$defs").cloned() else {
234        return;
235    };
236    let mut root = v.clone();
237    root.as_object_mut().unwrap().remove("$defs");
238    let mut pending = Vec::new();
239    refs(&root, &mut pending);
240    let mut reached = std::collections::BTreeSet::new();
241    while let Some(name) = pending.pop() {
242        if reached.insert(name.clone())
243            && let Some(d) = defs.get(&name)
244        {
245            refs(d, &mut pending);
246        }
247    }
248    let kept: serde_json::Map<String, serde_json::Value> = defs
249        .as_object()
250        .unwrap()
251        .iter()
252        .filter(|(k, _)| reached.contains(*k))
253        .map(|(k, d)| (k.clone(), d.clone()))
254        .collect();
255    v["$defs"] = kept.into();
256}
257
258fn limits(l: &ParseLimits) -> serde_json::Value {
259    serde_json::json!({
260        "max_bytes": l.max_bytes,
261        "max_depth": l.max_depth,
262        "max_string_bytes": l.max_string_bytes,
263        "max_collection_len": l.max_collection_len,
264        "max_total_values": l.max_total_values,
265        "max_number_bytes": l.max_number_bytes,
266        "allow_fractional_numbers": l.allow_fractional_numbers,
267    })
268}
269
270/// Every public document type and version, sorted by file name.
271pub fn document_schemas() -> Vec<EmittedSchema> {
272    use crate::{
273        calibration::CalibrationArtifact, definition::Definition, descriptor::BackendDescriptor,
274        eval_report::EvalReport, evidence::EvidenceRecord, execution::ExecutionPolicy,
275        judgment::Judgment, output::BackendOutputDoc, plan::Plan, reason::RuntimeReasonDoc, remote,
276        replay::ReplayBundle, request::RequestDoc, run::RunRecord, snapshot::Snapshot,
277        source::SourceDescriptor,
278    };
279    // Specs 019, 026 and 028 (R-46): the dynamic-question documents, live
280    // re-execution evidence, the redaction policy and the span line.
281    use crate::{
282        dynamic::{DynamicCapability, DynamicOutcome, DynamicQuestion},
283        live::LiveReexecution,
284        redaction::{REDACTION, RedactionPolicy},
285        trace::{TRACE, TraceLine},
286    };
287    let mut out = vec![
288        emit::<crate::explanation::Explanation>(schema::EXPLANATION, None),
289        emit::<crate::generation_result::TextResult>(schema::TEXT_RESULT, None),
290        emit::<crate::generation::PromptTemplate>(schema::PROMPT_TEMPLATE, None),
291        emit::<crate::generation::GenerateRequest>(schema::GENERATE_REQUEST, None),
292        emit::<crate::extraction_documents::ExtractRules>(schema::EXTRACT_RULES, None),
293        emit::<crate::extraction_documents::ExtractRequest>(schema::EXTRACT_REQUEST, None),
294        emit::<crate::profile::Profile>(schema::PROFILE, None),
295        emit::<crate::plan_diff::PlanDiff>(schema::PLAN_DIFF, None),
296        emit::<Definition>(schema::DEFINITION, None),
297        emit::<Definition>(schema::DEFINITION_V2, None),
298        emit_adjusted::<Plan>(schema::PLAN, None, plan_members(schema::PLAN)),
299        emit_adjusted::<Plan>(schema::PLAN_V3, None, plan_members(schema::PLAN_V3)),
300        emit::<Snapshot>(schema::SNAPSHOT, None),
301        emit::<Snapshot>(schema::SNAPSHOT_V2, None),
302        emit_adjusted::<EvidenceRecord>(schema::EVIDENCE, None, |v| {
303            extraction_v1(v, "/properties");
304            v["properties"].as_object_mut().unwrap().remove("screening");
305            v["properties"]
306                .as_object_mut()
307                .unwrap()
308                .remove("generation");
309            v["properties"]
310                .as_object_mut()
311                .unwrap()
312                .remove("acquisitions");
313            v["$defs"]["StepEvidence"]["properties"]
314                .as_object_mut()
315                .unwrap()
316                .remove("constructed");
317            // Version 1 and 2 documents only; evidence/2 and run/3 keep `ranking` and `selection` (spec 039 3.13).
318            if let Some(p) = v["$defs"]["StepEvidence"]["properties"].as_object_mut() {
319                p.remove("ranking");
320                p.remove("selection");
321            }
322            v["$defs"]["StepStatus"]["oneOf"]
323                .as_array_mut()
324                .unwrap()
325                .retain(|v| v["properties"].get("skipped").is_none());
326            prune_defs(v);
327        }),
328        emit::<EvidenceRecord>(schema::EVIDENCE_V2, None),
329        emit_adjusted::<RunRecord>(schema::RUN, None, run_members(schema::RUN)),
330        emit_adjusted::<RunRecord>(schema::RUN_V2, None, run_members(schema::RUN_V2)),
331        emit_adjusted::<RunRecord>(schema::RUN_V3, None, run_members(schema::RUN_V3)),
332        emit_adjusted::<Judgment>(schema::JUDGMENT, None, |v| {
333            extraction_v1(v, "/properties");
334            prune_defs(v);
335        }),
336        emit::<Judgment>(schema::JUDGMENT_V2, None),
337        emit::<BackendDescriptor>(schema::BACKEND, None),
338        emit::<CalibrationArtifact>(schema::CALIBRATION, None),
339        emit::<BackendOutputDoc>(schema::BACKEND_OUTPUT, None),
340        emit::<EvalReport>(schema::EVAL_REPORT, None),
341        emit::<ExecutionPolicy>(schema::EXECUTION, None),
342        emit_adjusted::<ReplayBundle>(schema::REPLAY, None, |v| {
343            for key in ["sources", "acquisition_pages"] {
344                v["properties"].as_object_mut().unwrap().remove(key);
345            }
346            v["$defs"]["SupplyEntry"]["properties"]
347                .as_object_mut()
348                .unwrap()
349                .remove("failure_context");
350            prune_defs(v);
351        }),
352        emit::<ReplayBundle>(schema::REPLAY_V2, None),
353        emit::<RequestDoc>(schema::REQUEST, None),
354        emit::<RuntimeReasonDoc>(schema::RUNTIME_REASON, None),
355        emit::<crate::acquisition::AcquisitionRequest>(schema::ACQUISITION_REQUEST, None),
356        emit::<crate::acquisition::AcquisitionPage>(schema::ACQUISITION_PAGE, None),
357        emit::<SourceDescriptor>(schema::SOURCE, None),
358        emit::<remote::DescribeRequest>(schema::REMOTE_DESCRIBE, Some("request")),
359        emit::<remote::DescribeResponse>(schema::REMOTE_DESCRIBE, Some("response")),
360        emit::<remote::InferRequest>(schema::REMOTE_INFER, Some("request")),
361        emit::<remote::InferResponse>(schema::REMOTE_INFER, Some("response")),
362        emit::<remote::CancelRequest>(schema::REMOTE_CANCEL, Some("request")),
363        emit::<remote::CancelResponse>(schema::REMOTE_CANCEL, Some("response")),
364        emit::<remote::ExchangeRecord>(schema::REMOTE_EXCHANGE, None),
365        emit::<DynamicQuestion>(schema::DYNAMIC_QUESTION, None),
366        emit::<DynamicCapability>(schema::DYNAMIC_CAPABILITY, None),
367        emit::<DynamicOutcome>(schema::DYNAMIC_OUTCOME, None),
368        emit::<LiveReexecution>(schema::LIVE_REEXECUTION, None),
369        emit::<crate::cache_entry::CacheEntry>(schema::CACHE_ENTRY, None),
370        emit::<RedactionPolicy>(REDACTION, None),
371        emit::<TraceLine>(TRACE, None),
372    ];
373    out.extend([
374        emit::<crate::remote_context::DescribeRequest>(
375            crate::remote_context::DESCRIBE,
376            Some("request"),
377        ),
378        emit::<crate::remote_context::DescribeResponse>(
379            crate::remote_context::DESCRIBE,
380            Some("response"),
381        ),
382        emit::<crate::remote_context::FetchRequest>(crate::remote_context::FETCH, Some("request")),
383        emit::<crate::remote_context::FetchResponse>(
384            crate::remote_context::FETCH,
385            Some("response"),
386        ),
387        emit::<crate::remote_context::CancelRequest>(
388            crate::remote_context::CANCEL,
389            Some("request"),
390        ),
391        emit::<crate::remote_context::CancelResponse>(
392            crate::remote_context::CANCEL,
393            Some("response"),
394        ),
395        emit::<crate::remote_context::Invalidation>(crate::remote_context::INVALIDATION, None),
396        emit::<crate::remote_context::InvalidationsRequest>(
397            crate::remote_context::INVALIDATIONS,
398            Some("request"),
399        ),
400        emit::<crate::remote_context::InvalidationsResponse>(
401            crate::remote_context::INVALIDATIONS,
402            Some("response"),
403        ),
404        emit::<crate::remote_context::ExchangeRecord>(crate::remote_context::EXCHANGE, None),
405        emit::<crate::remote_sink::DescribeRequest>(crate::remote_sink::DESCRIBE, Some("request")),
406        emit::<crate::remote_sink::DescribeResponse>(
407            crate::remote_sink::DESCRIBE,
408            Some("response"),
409        ),
410        emit::<crate::remote_sink::DeliverRequest>(crate::remote_sink::DELIVER, Some("request")),
411        emit::<crate::remote_sink::DeliverResponse>(crate::remote_sink::DELIVER, Some("response")),
412        emit::<crate::serve::Serve>(crate::serve::Serve::SCHEMA, None),
413        emit::<crate::serve::ServeConfig>(crate::serve::ServeConfig::SCHEMA, None),
414        emit::<crate::serve::ServeService>(crate::serve::ServeService::SCHEMA, None),
415        emit::<crate::serve::ServeRegister>(crate::serve::ServeRegister::SCHEMA, None),
416        emit::<crate::serve::ServeDecide>(crate::serve::ServeDecide::SCHEMA, None),
417        emit::<crate::serve::ServeDecision>(crate::serve::ServeDecision::SCHEMA, None),
418        emit::<crate::serve::ServeDecisionState>(crate::serve::ServeDecisionState::SCHEMA, None),
419        emit::<crate::serve::ServeCancel>(crate::serve::ServeCancel::SCHEMA, None),
420        emit::<crate::serve::ServeEvent>(crate::serve::ServeEvent::SCHEMA, None),
421        emit::<crate::serve::ServeRefusal>(crate::serve::ServeRefusal::SCHEMA, None),
422        emit::<crate::serve::ServeReplay>(crate::serve::ServeReplay::SCHEMA, None),
423        emit::<crate::serve::ServeEvaluate>(crate::serve::ServeEvaluate::SCHEMA, None),
424        emit::<crate::serve::ServeReadiness>(crate::serve::ServeReadiness::SCHEMA, None),
425        emit::<crate::serve::ServeShutdown>(crate::serve::ServeShutdown::SCHEMA, None),
426        emit::<crate::serve::ServeInvalidations>(crate::serve::ServeInvalidations::SCHEMA, None),
427        emit::<crate::serve::ServeInvalidationsAck>(
428            crate::serve::ServeInvalidationsAck::SCHEMA,
429            None,
430        ),
431        emit::<crate::plan_bundle::PlanBundle>(crate::plan_bundle::PlanBundle::SCHEMA, None),
432        emit::<crate::plan_bundle::PlanBundleSignature>(
433            crate::plan_bundle::PlanBundleSignature::SCHEMA,
434            None,
435        ),
436        emit::<crate::plan_bundle::PlanRegistration>(
437            crate::plan_bundle::PlanRegistration::SCHEMA,
438            None,
439        ),
440    ]);
441    out.sort_by(|a, b| a.file.cmp(&b.file));
442    out
443}