Skip to main content

onetaskgraph_core/
schema.rs

1//! The JSON Schema bundle both SDKs are generated from.
2//!
3//! It is emitted by `onetaskgraph schema` rather than committed, so the schema a
4//! consumer generates against can never drift from the types this binary actually
5//! serialises: they are the same types.
6
7use std::collections::BTreeMap;
8
9use onetaskgraph_plugin_api::{
10    Capabilities, Comment, DependencyEdge, DependencyEndpoint, DependencyKind, Direction, Document,
11    DocumentQuery, Health, ItemKind, Label, Location, NewComment, Page, PageRequest, Priority,
12    Project, ProjectQuery, Repository, SourceError, SourceName, Status, StatusCategory, Task,
13    TaskQuery, TaskRef, TaskUpdate, TaskUpdateOutcome, TextFields, UpdatedField,
14};
15use schemars::{Schema, schema_for};
16use serde_json::{Value, json};
17
18use crate::config::{EffectiveConfig, Origin, OutputFormat, Setting};
19use crate::registry::registry;
20use crate::secrets::{CredentialLayer, ResolvedCredential, SecretsReport};
21use crate::template::{
22    ItemType, RenderedTemplate, TemplateProvenance, TemplateVariable, TemplateVariables,
23    VariableType,
24};
25use crate::{
26    CommentList, CopyAction, CopyOutcome, CopyReport, DeletedComment, Delivered, DeliveryOutcome,
27    Failure, FailureClass, FailureDocument, GlobalId, MetadataSet, PageToken, Predicate, Qualified,
28    QualifiedEdge, QualifiedEndpoint, QueryPlan, QueryResponse, Regenerated, SearchHit, SearchKind,
29    SourceFailure, SourceListing, SourcePlan, TaskContentSet, TaskDetail, TaskPrioritySet,
30    TaskStatusSet, TaskUpdated, TemplateAnswers,
31};
32
33/// The bundle's own version, bumped whenever any root's schema changes — added, removed,
34/// renamed, **or altered inside**.
35///
36/// Consumers generate code from this document, so the version is part of the
37/// contract rather than a convenience: an SDK can refuse a bundle it was not
38/// generated against instead of silently emitting the wrong models. A property added to an
39/// existing root is a new field in both SDKs' generated models exactly as a new root is a
40/// new model, which is why the reach is the whole document rather than the set of names.
41///
42/// What each version brought is what `git log` answers; what this number owes a reader is
43/// that it moves whenever [`schema_bundle`] below emits a different document. The golden
44/// that holds it to that is `PUBLISHED_BUNDLES` in `tests/engine.rs`, which records every
45/// root's schema by digest from this version on.
46pub const SCHEMA_BUNDLE_VERSION: u32 = 23;
47
48/// Every contract root, keyed by name, plus each registered plugin's config schema.
49#[must_use]
50pub fn schema_bundle() -> Value {
51    let mut roots: BTreeMap<&'static str, Schema> = BTreeMap::new();
52
53    roots.insert("Task", schema_for!(Task));
54    roots.insert("Project", schema_for!(Project));
55    roots.insert("Document", schema_for!(Document));
56    // A root of its own although both `Task` and `Project` reach it inside their own
57    // definitions, for the reason `TextFields` is one: a consumer acts on a location by
58    // asking which of the two keys is present, so the shape it switches on has to be
59    // nameable rather than only reachable.
60    roots.insert("Location", schema_for!(Location));
61    roots.insert("Label", schema_for!(Label));
62    roots.insert("Status", schema_for!(Status));
63    roots.insert("StatusCategory", schema_for!(StatusCategory));
64    // A root of its own although `Task` reaches it, for the reason `StatusCategory` is one:
65    // `task priority set` and `task list --priority` take one by name.
66    roots.insert("Priority", schema_for!(Priority));
67    roots.insert("SourceName", schema_for!(SourceName));
68    roots.insert("DependencyEdge", schema_for!(DependencyEdge));
69    roots.insert("DependencyEndpoint", schema_for!(DependencyEndpoint));
70    roots.insert("QualifiedEndpoint", schema_for!(QualifiedEndpoint));
71    roots.insert("ItemKind", schema_for!(ItemKind));
72    roots.insert("Repository", schema_for!(Repository));
73    roots.insert("DependencyKind", schema_for!(DependencyKind));
74    roots.insert("Direction", schema_for!(Direction));
75    // Roots of their own although both are reachable inside `TaskQuery`'s definitions,
76    // which is enough for a generator and not enough for a reconciliation: the command
77    // line spells both deliberately differently (`both` for `title-or-content`, `task`
78    // for `tasks`), so a variant added to either would leave the command line quietly
79    // unable to name it. A root apiece gives that gate one document to read.
80    roots.insert("TextFields", schema_for!(TextFields));
81    roots.insert("SearchKind", schema_for!(SearchKind));
82
83    roots.insert("TaskQuery", schema_for!(TaskQuery));
84    roots.insert("ProjectQuery", schema_for!(ProjectQuery));
85    roots.insert("DocumentQuery", schema_for!(DocumentQuery));
86    roots.insert("PageRequest", schema_for!(PageRequest));
87    roots.insert("PageOfTask", schema_for!(Page<Task>));
88    roots.insert("PageOfProject", schema_for!(Page<Project>));
89    roots.insert("PageOfDocument", schema_for!(Page<Document>));
90    roots.insert("PageOfLabel", schema_for!(Page<Label>));
91    roots.insert("PageOfDependencyEdge", schema_for!(Page<DependencyEdge>));
92
93    roots.insert("Capabilities", schema_for!(Capabilities));
94    roots.insert("Health", schema_for!(Health));
95    roots.insert("SourceError", schema_for!(SourceError));
96
97    roots.insert("GlobalId", schema_for!(GlobalId));
98    roots.insert("PageToken", schema_for!(PageToken));
99    roots.insert("QueryPlan", schema_for!(QueryPlan));
100    roots.insert("SourcePlan", schema_for!(SourcePlan));
101    roots.insert("Predicate", schema_for!(Predicate));
102    roots.insert("SourceFailure", schema_for!(SourceFailure));
103    // What a command writes to standard output when it exits `1` under machine output,
104    // and the two types inside it a caller branches on by name.
105    roots.insert("FailureDocument", schema_for!(FailureDocument));
106    roots.insert("Failure", schema_for!(Failure));
107    roots.insert("FailureClass", schema_for!(FailureClass));
108    roots.insert("QualifiedTask", schema_for!(Qualified<Task>));
109    roots.insert("QualifiedProject", schema_for!(Qualified<Project>));
110    roots.insert("QualifiedDocument", schema_for!(Qualified<Document>));
111    roots.insert("QualifiedLabel", schema_for!(Qualified<Label>));
112    roots.insert("QualifiedEdge", schema_for!(QualifiedEdge));
113    roots.insert("SearchHit", schema_for!(SearchHit));
114    roots.insert("SourceListing", schema_for!(SourceListing));
115    roots.insert("SourceListings", schema_for!(Vec<SourceListing>));
116    roots.insert(
117        "QueryResponseOfQualifiedTask",
118        schema_for!(QueryResponse<Qualified<Task>>),
119    );
120    roots.insert(
121        "QueryResponseOfQualifiedProject",
122        schema_for!(QueryResponse<Qualified<Project>>),
123    );
124    roots.insert(
125        "QueryResponseOfQualifiedDocument",
126        schema_for!(QueryResponse<Qualified<Document>>),
127    );
128    roots.insert(
129        "QueryResponseOfQualifiedLabel",
130        schema_for!(QueryResponse<Qualified<Label>>),
131    );
132    roots.insert(
133        "QueryResponseOfQualifiedEdge",
134        schema_for!(QueryResponse<QualifiedEdge>),
135    );
136    roots.insert(
137        "QueryResponseOfSearchHit",
138        schema_for!(QueryResponse<SearchHit>),
139    );
140
141    // The comment roots: the contract's own `Comment` and `NewComment`, which cross the
142    // plugin protocol, and the three shapes the comment verbs and `task show` answer with.
143    roots.insert("Comment", schema_for!(Comment));
144    roots.insert("NewComment", schema_for!(NewComment));
145    roots.insert("PageOfComment", schema_for!(Page<Comment>));
146    roots.insert("CommentList", schema_for!(CommentList));
147    roots.insert("DeletedComment", schema_for!(DeletedComment));
148    roots.insert("TaskDetail", schema_for!(TaskDetail));
149
150    // What `task status set` answers with, and the per-task entries it and a copy report for
151    // the delivered tasks they kept in step — plus the entry type both of a task's lists hold.
152    roots.insert("TaskRef", schema_for!(TaskRef));
153    roots.insert("TaskStatusSet", schema_for!(TaskStatusSet));
154    roots.insert("Delivered", schema_for!(Delivered));
155    roots.insert("DeliveryOutcome", schema_for!(DeliveryOutcome));
156
157    // What `task priority set` and `task content set` answer with.
158    roots.insert("TaskPrioritySet", schema_for!(TaskPrioritySet));
159    roots.insert("TaskContentSet", schema_for!(TaskContentSet));
160
161    // The targeted update: what a caller names, what `task update` answers with and the
162    // field vocabulary both report in, and the outcome a source answers it with across the
163    // plugin protocol.
164    roots.insert("TaskUpdate", schema_for!(TaskUpdate));
165    roots.insert("TaskUpdated", schema_for!(TaskUpdated));
166    roots.insert("UpdatedField", schema_for!(UpdatedField));
167    roots.insert("TaskUpdateOutcome", schema_for!(TaskUpdateOutcome));
168
169    // What `task`, `project` and `document metadata set` answer with.
170    roots.insert("MetadataSet", schema_for!(MetadataSet));
171
172    roots.insert("CopyReport", schema_for!(CopyReport));
173    roots.insert("CopyOutcome", schema_for!(CopyOutcome));
174    roots.insert("CopyAction", schema_for!(CopyAction));
175
176    // What `template variables` and `template render` answer with, and the declaration and
177    // the two vocabularies inside the first, which a caller branches on by name.
178    // llmlint: ignore-block[code_lands_in_the_domain_that_owns_it] This table is the one
179    // document both SDKs are generated from, and every verb's answer is a root of it by
180    // design — `SCHEMA_BUNDLE_VERSION` and `PUBLISHED_BUNDLES` hold it whole. A template root
181    // registered anywhere else would be one no SDK is generated against.
182    roots.insert("TemplateVariables", schema_for!(TemplateVariables));
183    roots.insert("TemplateVariable", schema_for!(TemplateVariable));
184    roots.insert("VariableType", schema_for!(VariableType));
185    roots.insert("ItemType", schema_for!(ItemType));
186    roots.insert("RenderedTemplate", schema_for!(RenderedTemplate));
187    // What `task render` and `document render` answer with, what `task answers` and
188    // `document answers` answer with, and the `onetaskgraph.template` entry an item rendered
189    // from a template records — named, because a caller checking a hand edit or a changed
190    // template reads its hashes by name.
191    roots.insert("Regenerated", schema_for!(Regenerated));
192    roots.insert("TemplateAnswers", schema_for!(TemplateAnswers));
193    roots.insert("TemplateProvenance", schema_for!(TemplateProvenance));
194    // llmlint: ignore-end[code_lands_in_the_domain_that_owns_it]
195
196    roots.insert("EffectiveConfig", schema_for!(EffectiveConfig));
197    roots.insert("Setting", schema_for!(Setting));
198    roots.insert("Origin", schema_for!(Origin));
199    roots.insert("OutputFormat", schema_for!(OutputFormat));
200    roots.insert("SecretsReport", schema_for!(SecretsReport));
201    roots.insert("ResolvedCredential", schema_for!(ResolvedCredential));
202    roots.insert("CredentialLayer", schema_for!(CredentialLayer));
203
204    let plugins: BTreeMap<String, Schema> = registry()
205        .iter()
206        .map(|plugin| (plugin.kind().to_owned(), plugin.config_schema()))
207        .collect();
208
209    json!({
210        "version": SCHEMA_BUNDLE_VERSION,
211        "roots": roots,
212        "plugin_config": plugins,
213    })
214}