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            v["$defs"]["EvidenceRecord"]["properties"]
97                .as_object_mut()
98                .unwrap()
99                .remove("generation");
100            v["$defs"]["EvidenceRecord"]["properties"]
101                .as_object_mut()
102                .unwrap()
103                .remove("acquisitions");
104            v["$defs"]["StepEvidence"]["properties"]
105                .as_object_mut()
106                .unwrap()
107                .remove("constructed");
108            v["$defs"]["StepStatus"]["oneOf"]
109                .as_array_mut()
110                .unwrap()
111                .retain(|v| v["properties"].get("skipped").is_none());
112            v["properties"]
113                .as_object_mut()
114                .unwrap()
115                .remove("acquisitions");
116            v["$defs"]["RequestRecord"]["properties"]
117                .as_object_mut()
118                .unwrap()
119                .remove("screen");
120            v["$defs"]["RequestResult"]["oneOf"]
121                .as_array_mut()
122                .unwrap()
123                .retain(|variant| variant["const"] != "withdrawn");
124            prune_defs(v);
125        }
126        if version == schema::RUN_V2 {
127            // The list is required and never null: `null` reads as absent,
128            // which the parser refuses in version 2.
129            v["required"]
130                .as_array_mut()
131                .expect("the run record has required members")
132                .push(RUN_V2_MEMBERS[0].into());
133            let list = &mut v["properties"][RUN_V2_MEMBERS[0]];
134            assert_eq!(
135                list["type"],
136                serde_json::json!(["array", "null"]),
137                "the remote evidence list is derived as an optional array"
138            );
139            list["type"] = "array".into();
140            return;
141        }
142        let props = v["properties"]
143            .as_object_mut()
144            .expect("the run record has properties");
145        for m in RUN_V2_MEMBERS {
146            props
147                .remove(m)
148                .unwrap_or_else(|| panic!("the run record has no {m} member"));
149        }
150        prune_defs(v);
151    }
152}
153
154/// Instantiation belongs only to plan 3 (spec 034, 3.10.1), as enforced
155/// by `Plan::parse`, including an explicitly null member in an earlier plan.
156fn plan_members(version: &str) -> impl FnOnce(&mut serde_json::Value) + '_ {
157    move |v| {
158        if version != schema::PLAN_V3 {
159            if let Some(variants) = v["$defs"]["PlanStepDetail"]["oneOf"].as_array_mut() {
160                variants.retain(|v| v["properties"].get("generate").is_none());
161            }
162            v["properties"]
163                .as_object_mut()
164                .unwrap()
165                .remove("acquisitions");
166            v["properties"]
167                .as_object_mut()
168                .expect("the plan has properties")
169                .remove("instantiation")
170                .expect("the plan has an instantiation member");
171            v["properties"]
172                .as_object_mut()
173                .unwrap()
174                .remove("screening")
175                .expect("the plan has a screening member");
176            prune_defs(v);
177        }
178    }
179}
180
181/// Removes every `$defs` entry no longer reachable from the schema's own
182/// members, so a version without a member does not describe its types.
183fn prune_defs(v: &mut serde_json::Value) {
184    fn refs(v: &serde_json::Value, out: &mut Vec<String>) {
185        match v {
186            serde_json::Value::Object(m) => {
187                if let Some(r) = m.get("$ref").and_then(|r| r.as_str())
188                    && let Some(name) = r.strip_prefix("#/$defs/")
189                {
190                    out.push(name.to_string());
191                }
192                m.values().for_each(|x| refs(x, out));
193            }
194            serde_json::Value::Array(a) => a.iter().for_each(|x| refs(x, out)),
195            _ => {}
196        }
197    }
198    let Some(defs) = v.get("$defs").cloned() else {
199        return;
200    };
201    let mut root = v.clone();
202    root.as_object_mut().unwrap().remove("$defs");
203    let mut pending = Vec::new();
204    refs(&root, &mut pending);
205    let mut reached = std::collections::BTreeSet::new();
206    while let Some(name) = pending.pop() {
207        if reached.insert(name.clone())
208            && let Some(d) = defs.get(&name)
209        {
210            refs(d, &mut pending);
211        }
212    }
213    let kept: serde_json::Map<String, serde_json::Value> = defs
214        .as_object()
215        .unwrap()
216        .iter()
217        .filter(|(k, _)| reached.contains(*k))
218        .map(|(k, d)| (k.clone(), d.clone()))
219        .collect();
220    v["$defs"] = kept.into();
221}
222
223fn limits(l: &ParseLimits) -> serde_json::Value {
224    serde_json::json!({
225        "max_bytes": l.max_bytes,
226        "max_depth": l.max_depth,
227        "max_string_bytes": l.max_string_bytes,
228        "max_collection_len": l.max_collection_len,
229        "max_total_values": l.max_total_values,
230        "max_number_bytes": l.max_number_bytes,
231        "allow_fractional_numbers": l.allow_fractional_numbers,
232    })
233}
234
235/// Every public document type and version, sorted by file name.
236pub fn document_schemas() -> Vec<EmittedSchema> {
237    use crate::{
238        calibration::CalibrationArtifact, definition::Definition, descriptor::BackendDescriptor,
239        eval_report::EvalReport, evidence::EvidenceRecord, execution::ExecutionPolicy,
240        judgment::Judgment, output::BackendOutputDoc, plan::Plan, reason::RuntimeReasonDoc, remote,
241        replay::ReplayBundle, request::RequestDoc, run::RunRecord, snapshot::Snapshot,
242        source::SourceDescriptor,
243    };
244    // Specs 019, 026 and 028 (R-46): the dynamic-question documents, live
245    // re-execution evidence, the redaction policy and the span line.
246    use crate::{
247        dynamic::{DynamicCapability, DynamicOutcome, DynamicQuestion},
248        live::LiveReexecution,
249        redaction::{REDACTION, RedactionPolicy},
250        trace::{TRACE, TraceLine},
251    };
252    let mut out = vec![
253        emit::<crate::explanation::Explanation>(schema::EXPLANATION, None),
254        emit::<crate::generation_result::TextResult>(schema::TEXT_RESULT, None),
255        emit::<crate::generation::PromptTemplate>(schema::PROMPT_TEMPLATE, None),
256        emit::<crate::generation::GenerateRequest>(schema::GENERATE_REQUEST, None),
257        emit::<crate::extraction_documents::ExtractRules>(schema::EXTRACT_RULES, None),
258        emit::<crate::extraction_documents::ExtractRequest>(schema::EXTRACT_REQUEST, None),
259        emit::<crate::profile::Profile>(schema::PROFILE, None),
260        emit::<crate::plan_diff::PlanDiff>(schema::PLAN_DIFF, None),
261        emit::<Definition>(schema::DEFINITION, None),
262        emit::<Definition>(schema::DEFINITION_V2, None),
263        emit_adjusted::<Plan>(schema::PLAN, None, plan_members(schema::PLAN)),
264        emit_adjusted::<Plan>(schema::PLAN_V3, None, plan_members(schema::PLAN_V3)),
265        emit::<Snapshot>(schema::SNAPSHOT, None),
266        emit::<Snapshot>(schema::SNAPSHOT_V2, None),
267        emit_adjusted::<EvidenceRecord>(schema::EVIDENCE, None, |v| {
268            v["properties"].as_object_mut().unwrap().remove("screening");
269            v["properties"]
270                .as_object_mut()
271                .unwrap()
272                .remove("generation");
273            v["properties"]
274                .as_object_mut()
275                .unwrap()
276                .remove("acquisitions");
277            v["$defs"]["StepEvidence"]["properties"]
278                .as_object_mut()
279                .unwrap()
280                .remove("constructed");
281            v["$defs"]["StepStatus"]["oneOf"]
282                .as_array_mut()
283                .unwrap()
284                .retain(|v| v["properties"].get("skipped").is_none());
285            prune_defs(v);
286        }),
287        emit::<EvidenceRecord>(schema::EVIDENCE_V2, None),
288        emit_adjusted::<RunRecord>(schema::RUN, None, run_members(schema::RUN)),
289        emit_adjusted::<RunRecord>(schema::RUN_V2, None, run_members(schema::RUN_V2)),
290        emit_adjusted::<RunRecord>(schema::RUN_V3, None, run_members(schema::RUN_V3)),
291        emit::<Judgment>(schema::JUDGMENT, None),
292        emit::<Judgment>(schema::JUDGMENT_V2, None),
293        emit::<BackendDescriptor>(schema::BACKEND, None),
294        emit::<CalibrationArtifact>(schema::CALIBRATION, None),
295        emit::<BackendOutputDoc>(schema::BACKEND_OUTPUT, None),
296        emit::<EvalReport>(schema::EVAL_REPORT, None),
297        emit::<ExecutionPolicy>(schema::EXECUTION, None),
298        emit_adjusted::<ReplayBundle>(schema::REPLAY, None, |v| {
299            for key in ["sources", "acquisition_pages"] {
300                v["properties"].as_object_mut().unwrap().remove(key);
301            }
302            v["$defs"]["SupplyEntry"]["properties"]
303                .as_object_mut()
304                .unwrap()
305                .remove("failure_context");
306            prune_defs(v);
307        }),
308        emit::<ReplayBundle>(schema::REPLAY_V2, None),
309        emit::<RequestDoc>(schema::REQUEST, None),
310        emit::<RuntimeReasonDoc>(schema::RUNTIME_REASON, None),
311        emit::<crate::acquisition::AcquisitionRequest>(schema::ACQUISITION_REQUEST, None),
312        emit::<crate::acquisition::AcquisitionPage>(schema::ACQUISITION_PAGE, None),
313        emit::<SourceDescriptor>(schema::SOURCE, None),
314        emit::<remote::DescribeRequest>(schema::REMOTE_DESCRIBE, Some("request")),
315        emit::<remote::DescribeResponse>(schema::REMOTE_DESCRIBE, Some("response")),
316        emit::<remote::InferRequest>(schema::REMOTE_INFER, Some("request")),
317        emit::<remote::InferResponse>(schema::REMOTE_INFER, Some("response")),
318        emit::<remote::CancelRequest>(schema::REMOTE_CANCEL, Some("request")),
319        emit::<remote::CancelResponse>(schema::REMOTE_CANCEL, Some("response")),
320        emit::<remote::ExchangeRecord>(schema::REMOTE_EXCHANGE, None),
321        emit::<DynamicQuestion>(schema::DYNAMIC_QUESTION, None),
322        emit::<DynamicCapability>(schema::DYNAMIC_CAPABILITY, None),
323        emit::<DynamicOutcome>(schema::DYNAMIC_OUTCOME, None),
324        emit::<LiveReexecution>(schema::LIVE_REEXECUTION, None),
325        emit::<RedactionPolicy>(REDACTION, None),
326        emit::<TraceLine>(TRACE, None),
327    ];
328    out.extend([
329        emit::<crate::remote_context::DescribeRequest>(
330            crate::remote_context::DESCRIBE,
331            Some("request"),
332        ),
333        emit::<crate::remote_context::DescribeResponse>(
334            crate::remote_context::DESCRIBE,
335            Some("response"),
336        ),
337        emit::<crate::remote_context::FetchRequest>(crate::remote_context::FETCH, Some("request")),
338        emit::<crate::remote_context::FetchResponse>(
339            crate::remote_context::FETCH,
340            Some("response"),
341        ),
342        emit::<crate::remote_context::CancelRequest>(
343            crate::remote_context::CANCEL,
344            Some("request"),
345        ),
346        emit::<crate::remote_context::CancelResponse>(
347            crate::remote_context::CANCEL,
348            Some("response"),
349        ),
350        emit::<crate::remote_context::Invalidation>(crate::remote_context::INVALIDATION, None),
351        emit::<crate::remote_context::InvalidationsRequest>(
352            crate::remote_context::INVALIDATIONS,
353            Some("request"),
354        ),
355        emit::<crate::remote_context::InvalidationsResponse>(
356            crate::remote_context::INVALIDATIONS,
357            Some("response"),
358        ),
359        emit::<crate::remote_context::ExchangeRecord>(crate::remote_context::EXCHANGE, None),
360        emit::<crate::remote_sink::DescribeRequest>(crate::remote_sink::DESCRIBE, Some("request")),
361        emit::<crate::remote_sink::DescribeResponse>(
362            crate::remote_sink::DESCRIBE,
363            Some("response"),
364        ),
365        emit::<crate::remote_sink::DeliverRequest>(crate::remote_sink::DELIVER, Some("request")),
366        emit::<crate::remote_sink::DeliverResponse>(crate::remote_sink::DELIVER, Some("response")),
367        emit::<crate::serve::Serve>(crate::serve::Serve::SCHEMA, None),
368        emit::<crate::serve::ServeConfig>(crate::serve::ServeConfig::SCHEMA, None),
369        emit::<crate::serve::ServeService>(crate::serve::ServeService::SCHEMA, None),
370        emit::<crate::serve::ServeRegister>(crate::serve::ServeRegister::SCHEMA, None),
371        emit::<crate::serve::ServeDecide>(crate::serve::ServeDecide::SCHEMA, None),
372        emit::<crate::serve::ServeDecision>(crate::serve::ServeDecision::SCHEMA, None),
373        emit::<crate::serve::ServeDecisionState>(crate::serve::ServeDecisionState::SCHEMA, None),
374        emit::<crate::serve::ServeCancel>(crate::serve::ServeCancel::SCHEMA, None),
375        emit::<crate::serve::ServeEvent>(crate::serve::ServeEvent::SCHEMA, None),
376        emit::<crate::serve::ServeRefusal>(crate::serve::ServeRefusal::SCHEMA, None),
377        emit::<crate::serve::ServeReplay>(crate::serve::ServeReplay::SCHEMA, None),
378        emit::<crate::serve::ServeEvaluate>(crate::serve::ServeEvaluate::SCHEMA, None),
379        emit::<crate::serve::ServeReadiness>(crate::serve::ServeReadiness::SCHEMA, None),
380        emit::<crate::serve::ServeShutdown>(crate::serve::ServeShutdown::SCHEMA, None),
381        emit::<crate::serve::ServeInvalidations>(crate::serve::ServeInvalidations::SCHEMA, None),
382        emit::<crate::serve::ServeInvalidationsAck>(
383            crate::serve::ServeInvalidationsAck::SCHEMA,
384            None,
385        ),
386        emit::<crate::plan_bundle::PlanBundle>(crate::plan_bundle::PlanBundle::SCHEMA, None),
387        emit::<crate::plan_bundle::PlanBundleSignature>(
388            crate::plan_bundle::PlanBundleSignature::SCHEMA,
389            None,
390        ),
391        emit::<crate::plan_bundle::PlanRegistration>(
392            crate::plan_bundle::PlanRegistration::SCHEMA,
393            None,
394        ),
395    ]);
396    out.sort_by(|a, b| a.file.cmp(&b.file));
397    out
398}