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