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