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