1use std::{collections::BTreeMap, fmt::Write, path::Path};
5
6use serde::Serialize;
7use serde_json::{Map, Value};
8use sha2::{Digest, Sha256};
9
10use crate::{
11 config::{ArtifactKind, EvalPayload, Fingerprint, Profile, RepoConfig},
12 scope::{Argument, RelationKind},
13 types::{ArtifactName, EvalId},
14};
15
16const FORMAT: &[u8] = b"artifactize-graph-v1\n";
18
19#[derive(Debug, Serialize)]
20#[serde(rename_all = "camelCase")]
21pub struct Structure {
22 pub artifacts: BTreeMap<ArtifactName, Artifact>,
23 pub evals: BTreeMap<EvalId, Eval>,
24 pub relations: Vec<Relation>,
25}
26
27#[derive(Debug, Serialize)]
28pub struct Artifact {
29 pub kind: ArtifactKind,
30 pub basis: Option<bool>,
31 pub fingerprint: Coverage,
32}
33
34#[derive(Debug, Serialize)]
35#[serde(tag = "kind", rename_all = "lowercase")]
36pub enum Coverage {
37 Graph,
38 Disabled,
39 Artifactsum {
40 files: Vec<crate::config::LogicalPath>,
41 ignore: Vec<String>,
42 },
43 Script {
44 command: String,
45 args: Vec<Argument>,
46 files: Vec<crate::config::LogicalPath>,
47 },
48}
49
50#[derive(Debug, Serialize)]
51#[serde(rename_all = "camelCase")]
52pub struct Eval {
53 pub target: ArtifactName,
54 pub title: String,
55 pub gate: Option<crate::config::Gate>,
56 pub criteria: Criteria,
57 pub payload: Option<EvalPayload>,
58 pub pass_schema: Option<Map<String, Value>>,
59 pub fail_schema: Option<Map<String, Value>>,
60}
61
62#[derive(Debug, Serialize)]
64#[serde(tag = "kind", rename_all = "lowercase")]
65pub enum Criteria {
66 Agent,
67 Human,
68 Runtime {
69 command: String,
70 args: Vec<Argument>,
71 },
72}
73
74#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
75pub struct Relation {
76 pub source: ArtifactName,
77 pub target: ArtifactName,
78 #[serde(flatten)]
79 pub kind: Edge,
80}
81
82#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
83#[serde(tag = "kind", rename_all = "lowercase")]
84pub enum Edge {
85 Child,
86 Instruction {
87 eval_id: EvalId,
88 name: crate::config::ReferenceName,
89 },
90 Argv {
91 eval_id: EvalId,
92 index: usize,
93 name: crate::config::ReferenceName,
94 path: crate::scope::LogicalTarget,
95 },
96}
97
98pub fn extract(config: &RepoConfig) -> Structure {
99 let artifacts = config
100 .artifacts
101 .iter()
102 .map(|(name, artifact)| {
103 let fingerprint = match &artifact.fingerprint {
104 Some(Fingerprint::Graph) => Coverage::Graph,
105 None => Coverage::Disabled,
106 Some(Fingerprint::Artifactsum { files, ignore }) => Coverage::Artifactsum {
107 files: files.clone(),
108 ignore: ignore.clone(),
109 },
110 Some(Fingerprint::Script {
111 command,
112 args,
113 files,
114 ..
115 }) => Coverage::Script {
116 command: command.clone(),
117 args: args.clone(),
118 files: files.clone(),
119 },
120 };
121 (
122 name.clone(),
123 Artifact {
124 kind: artifact.kind,
125 basis: artifact.basis,
126 fingerprint,
127 },
128 )
129 })
130 .collect();
131 let evals = config
132 .evals
133 .iter()
134 .map(|eval| {
135 let declaration = &eval.declaration;
136 let criteria = match declaration.profile() {
137 Profile::Agent { .. } => Criteria::Agent,
138 Profile::Human {} => Criteria::Human,
139 Profile::Runtime { command, args, .. } => Criteria::Runtime {
140 command: command.clone(),
141 args: args.clone(),
142 },
143 };
144 (
145 eval.id.clone(),
146 Eval {
147 target: eval.target.clone(),
148 title: declaration.title.clone(),
149 gate: declaration.gate,
150 criteria,
151 payload: declaration.payload().cloned(),
152 pass_schema: declaration.pass_schema().cloned(),
153 fail_schema: declaration.fail_schema().cloned(),
154 },
155 )
156 })
157 .collect();
158 let mut relations: Vec<_> = config
159 .relations
160 .iter()
161 .map(|relation| Relation {
162 source: relation.source.clone(),
163 target: relation.target.clone(),
164 kind: match &relation.kind {
165 RelationKind::Child { .. } => Edge::Child,
166 RelationKind::Instruction { eval_id, name } => Edge::Instruction {
167 eval_id: eval_id.clone(),
168 name: name.clone(),
169 },
170 RelationKind::Argument {
171 eval_id,
172 index,
173 name,
174 path,
175 } => Edge::Argv {
176 eval_id: eval_id.clone(),
177 index: *index,
178 name: name.clone(),
179 path: path.clone(),
180 },
181 },
182 })
183 .collect();
184 relations.sort();
185 Structure {
186 artifacts,
187 evals,
188 relations,
189 }
190}
191
192pub fn read(repo: &Path) -> Result<Structure, String> {
194 crate::config::read_workspace_declarations(repo)
195 .map(|config| extract(&config))
196 .map_err(|error| error.to_string())
197}
198
199impl Structure {
200 pub fn fingerprint(&self) -> crate::types::Fingerprint {
201 let mut value = serde_json::to_value(self).expect("graph structure is JSON");
202 value.sort_all_objects();
203 let mut digest = Sha256::new();
204 digest.update(FORMAT);
205 digest.update(serde_json::to_vec(&value).expect("graph structure is JSON"));
206 let hex: String = digest
207 .finalize()
208 .iter()
209 .map(|byte| format!("{byte:02x}"))
210 .collect();
211 format!("graph:{hex}")
212 .parse()
213 .expect("graph fingerprint is nonblank")
214 }
215
216 pub fn tree(&self) -> String {
219 let mut text = format!("Artifact graph {}\n", self.fingerprint());
220 for (name, artifact) in &self.artifacts {
221 writeln!(text, "{name} [{:?}]", artifact.kind).expect("write to String");
222 writeln!(
223 text,
224 " definition: {}",
225 serde_json::to_string(artifact).expect("Artifact is JSON")
226 )
227 .expect("write to String");
228 for (id, eval) in self.evals.iter().filter(|(_, eval)| &eval.target == name) {
229 writeln!(text, " ├─ {id}: {}", eval.title).expect("write to String");
230 writeln!(
231 text,
232 " │ criteria: {}",
233 serde_json::to_string(eval).expect("Eval is JSON")
234 )
235 .expect("write to String");
236 }
237 }
238 for relation in &self.relations {
239 writeln!(
240 text,
241 "{} → {} {}",
242 relation.source,
243 relation.target,
244 serde_json::to_string(&relation.kind).expect("relation is JSON")
245 )
246 .expect("write to String");
247 }
248 text
249 }
250}
251
252pub(crate) fn launch(
257 repo: &Path,
258 run: crate::tools::human::log::Run,
259) -> Result<(), (String, crate::tools::human::log::Run)> {
260 if let Err(error) = read(repo) {
262 return Err((error, run));
263 }
264 let program = match crate::platform::program::resolve(
265 std::ffi::OsStr::new("artifactize-desktop"),
266 repo,
267 ) {
268 Ok(program) => program,
269 Err(error) => return Err((format!("Cannot find artifactize-desktop: {error}"), run)),
270 };
271 crate::process::launch_watched(
272 program.as_os_str(),
273 &[
274 "graph".into(),
275 "--repo".into(),
276 crate::platform::path_text(repo),
277 ],
278 repo,
279 run,
280 )
281 .map_err(|(error, run)| (error.to_string(), run))
282}
283
284#[cfg(test)]
285mod tests;