1use std::collections::HashMap;
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Artifact {
10 pub id: Uuid,
12 pub kind: ArtifactKind,
14 pub path: Option<String>,
16 pub content: Option<serde_json::Value>,
18 pub checksum: String,
20 pub created_at: DateTime<Utc>,
22}
23
24impl Artifact {
25 pub fn new(kind: ArtifactKind) -> Self {
27 Self {
28 id: Uuid::new_v4(),
29 kind,
30 path: None,
31 content: None,
32 checksum: String::new(),
33 created_at: Utc::now(),
34 }
35 }
36
37 pub fn with_path(mut self, path: String) -> Self {
39 self.path = Some(path);
40 self
41 }
42
43 pub fn with_content(mut self, content: serde_json::Value) -> Self {
45 self.content = Some(content);
46 self
47 }
48
49 pub fn with_checksum(mut self, checksum: String) -> Self {
51 self.checksum = checksum;
52 self
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
58pub enum ArtifactKind {
59 Ticket {
61 severity: Severity,
63 category: String,
65 },
66 CodeChange {
68 files: Vec<String>,
70 },
71 TestResult {
73 passed: u32,
75 failed: u32,
77 },
78 Analysis {
80 findings: Vec<Finding>,
82 },
83 Decision {
85 choice: String,
87 rationale: String,
89 },
90 Data {
92 schema: String,
94 },
95 Custom {
97 name: String,
99 },
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
104pub enum Severity {
105 Critical,
107 High,
109 Medium,
111 Low,
113 Info,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
119pub struct Finding {
120 pub title: String,
122 pub description: String,
124 pub severity: Severity,
126 pub location: Option<String>,
128 pub suggestion: Option<String>,
130}
131
132#[derive(Debug, Default)]
134pub struct ArtifactRegistry {
135 artifacts: HashMap<String, Vec<Artifact>>,
136}
137
138impl Clone for ArtifactRegistry {
139 fn clone(&self) -> Self {
140 Self {
141 artifacts: self.artifacts.clone(),
142 }
143 }
144}
145
146impl ArtifactRegistry {
147 pub fn register(&mut self, phase: String, artifact: Artifact) {
149 self.artifacts.entry(phase).or_default().push(artifact);
150 }
151
152 pub fn get_by_phase(&self, phase: &str) -> Vec<&Artifact> {
154 self.artifacts
155 .get(phase)
156 .map(|v| v.iter().collect())
157 .unwrap_or_default()
158 }
159
160 pub fn get_by_kind(&self, kind: &ArtifactKind) -> Vec<&Artifact> {
162 self.artifacts
163 .values()
164 .flatten()
165 .filter(|a| &a.kind == kind)
166 .collect()
167 }
168
169 pub fn get_latest(&self, phase: &str) -> Option<&Artifact> {
171 self.artifacts.get(phase).and_then(|v| v.last())
172 }
173
174 pub fn count(&self) -> usize {
176 self.artifacts.values().map(|v| v.len()).sum()
177 }
178
179 pub fn all(&self) -> HashMap<String, Vec<&Artifact>> {
181 self.artifacts
182 .iter()
183 .map(|(k, v)| (k.clone(), v.iter().collect()))
184 .collect()
185 }
186}
187
188#[derive(Debug, Default, Clone)]
190pub struct SharedState {
191 data: HashMap<String, serde_json::Value>,
192}
193
194impl SharedState {
195 pub fn set(&mut self, key: impl Into<String>, value: serde_json::Value) {
197 self.data.insert(key.into(), value);
198 }
199
200 pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
202 self.data.get(key)
203 }
204
205 pub fn remove(&mut self, key: &str) -> Option<serde_json::Value> {
207 self.data.remove(key)
208 }
209
210 pub fn render_template(&self, template: &str) -> String {
212 let mut result = template.to_string();
213 for (key, value) in &self.data {
214 let placeholder = format!("{{{}}}", key);
215 if let Some(s) = value.as_str() {
216 result = result.replace(&placeholder, s);
217 } else if let Ok(s) = serde_json::to_string(value) {
218 result = result.replace(&placeholder, &s);
219 }
220 }
221 result
222 }
223}
224
225pub fn parse_artifacts_from_output(output: &str) -> Vec<Artifact> {
227 let mut artifacts = Vec::new();
228
229 if let Ok(parsed) = serde_json::from_str::<Vec<serde_json::Value>>(output) {
230 for value in parsed {
231 if let Some(kind) = extract_artifact_kind(&value) {
232 let mut artifact = Artifact::new(kind);
233 if let Some(path) = value.get("path").and_then(|v| v.as_str()) {
234 artifact = artifact.with_path(path.to_string());
235 }
236 if let Some(content) = value.get("content") {
237 artifact = artifact.with_content(content.clone());
238 }
239 artifacts.push(artifact);
240 }
241 }
242 return artifacts;
243 }
244
245 if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(output) {
246 if let Some(kind) = extract_artifact_kind(&parsed) {
247 let mut artifact = Artifact::new(kind);
248 if let Some(path) = parsed.get("path").and_then(|v| v.as_str()) {
249 artifact = artifact.with_path(path.to_string());
250 }
251 if let Some(content) = parsed.get("content") {
252 artifact = artifact.with_content(content.clone());
253 }
254 artifacts.push(artifact);
255 }
256
257 if artifacts.is_empty() {
258 if let Some(arr) = parsed.get("artifacts").and_then(|v| v.as_array()) {
259 for value in arr {
260 if let Some(kind) = extract_artifact_kind(value) {
261 let mut artifact = Artifact::new(kind);
262 if let Some(path) = value.get("path").and_then(|v| v.as_str()) {
263 artifact = artifact.with_path(path.to_string());
264 }
265 if let Some(content) = value.get("content") {
266 artifact = artifact.with_content(content.clone());
267 }
268 artifacts.push(artifact);
269 }
270 }
271 }
272 }
273 }
274
275 for line in output.lines() {
276 if let Some(ticket) = parse_ticket_from_line(line) {
277 artifacts.push(ticket);
278 }
279 }
280
281 artifacts
282}
283
284fn extract_artifact_kind(value: &serde_json::Value) -> Option<ArtifactKind> {
285 let kind = value.get("kind")?.as_str()?;
286
287 match kind {
288 "ticket" => {
289 let severity = value
290 .get("severity")
291 .and_then(|v| v.as_str())
292 .map(|s| match s {
293 "critical" => Severity::Critical,
294 "high" => Severity::High,
295 "medium" => Severity::Medium,
296 "low" => Severity::Low,
297 _ => Severity::Info,
298 })
299 .unwrap_or(Severity::Info);
300
301 let category = value
302 .get("category")
303 .and_then(|v| v.as_str())
304 .unwrap_or("general")
305 .to_string();
306
307 Some(ArtifactKind::Ticket { severity, category })
308 }
309 "code_change" => {
310 let files = value
311 .get("files")
312 .and_then(|v| v.as_array())
313 .map(|arr| {
314 arr.iter()
315 .filter_map(|f| f.as_str().map(String::from))
316 .collect()
317 })
318 .unwrap_or_default();
319
320 Some(ArtifactKind::CodeChange { files })
321 }
322 "test_result" => {
323 let passed = value.get("passed").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
324 let failed = value.get("failed").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
325
326 Some(ArtifactKind::TestResult { passed, failed })
327 }
328 "analysis" => {
329 let findings = value
330 .get("findings")
331 .and_then(|v| v.as_array())
332 .map(|arr| {
333 arr.iter()
334 .filter_map(|f| {
335 Some(Finding {
336 title: f.get("title")?.as_str()?.to_string(),
337 description: f
338 .get("description")
339 .and_then(|v| v.as_str())
340 .unwrap_or("")
341 .to_string(),
342 severity: f
343 .get("severity")
344 .and_then(|v| v.as_str())
345 .map(|s| match s {
346 "critical" => Severity::Critical,
347 "high" => Severity::High,
348 "medium" => Severity::Medium,
349 "low" => Severity::Low,
350 _ => Severity::Info,
351 })
352 .unwrap_or(Severity::Info),
353 location: f
354 .get("location")
355 .and_then(|v| v.as_str())
356 .map(String::from),
357 suggestion: f
358 .get("suggestion")
359 .and_then(|v| v.as_str())
360 .map(String::from),
361 })
362 })
363 .collect()
364 })
365 .unwrap_or_default();
366
367 Some(ArtifactKind::Analysis { findings })
368 }
369 "decision" => {
370 let choice = value
371 .get("choice")
372 .and_then(|v| v.as_str())
373 .unwrap_or("unknown")
374 .to_string();
375 let rationale = value
376 .get("rationale")
377 .and_then(|v| v.as_str())
378 .unwrap_or("")
379 .to_string();
380
381 Some(ArtifactKind::Decision { choice, rationale })
382 }
383 _ => None,
384 }
385}
386
387fn parse_ticket_from_line(line: &str) -> Option<Artifact> {
388 if !line.contains("[TICKET:") {
389 return None;
390 }
391
392 let severity = if line.contains("severity=critical") {
393 Severity::Critical
394 } else if line.contains("severity=high") {
395 Severity::High
396 } else if line.contains("severity=medium") {
397 Severity::Medium
398 } else if line.contains("severity=low") {
399 Severity::Low
400 } else {
401 Severity::Info
402 };
403
404 let category = if line.contains("category=bug") {
405 "bug".to_string()
406 } else if line.contains("category=security") {
407 "security".to_string()
408 } else if line.contains("category=performance") {
409 "performance".to_string()
410 } else {
411 "general".to_string()
412 };
413
414 Some(Artifact::new(ArtifactKind::Ticket { severity, category }))
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420
421 #[test]
422 fn test_artifact_builder() {
423 let artifact = Artifact::new(ArtifactKind::CodeChange {
424 files: vec!["main.rs".to_string()],
425 })
426 .with_path("/tmp/diff.patch".to_string())
427 .with_content(serde_json::json!({"lines_added": 10}))
428 .with_checksum("abc123".to_string());
429
430 assert_eq!(
431 artifact.path.expect("artifact path should be populated"),
432 "/tmp/diff.patch"
433 );
434 assert!(artifact.content.is_some());
435 assert_eq!(artifact.checksum, "abc123");
436 }
437
438 #[test]
439 fn test_artifact_registry() {
440 let mut registry = ArtifactRegistry::default();
441
442 let artifact = Artifact::new(ArtifactKind::Ticket {
443 severity: Severity::High,
444 category: "bug".to_string(),
445 });
446
447 registry.register("qa".to_string(), artifact);
448
449 assert_eq!(registry.count(), 1);
450 assert!(registry.get_latest("qa").is_some());
451 }
452
453 #[test]
454 fn test_artifact_registry_get_by_phase() {
455 let mut registry = ArtifactRegistry::default();
456 registry.register(
457 "qa".to_string(),
458 Artifact::new(ArtifactKind::Custom {
459 name: "a".to_string(),
460 }),
461 );
462 registry.register(
463 "implement".to_string(),
464 Artifact::new(ArtifactKind::Custom {
465 name: "b".to_string(),
466 }),
467 );
468
469 assert_eq!(registry.get_by_phase("qa").len(), 1);
470 assert_eq!(registry.get_by_phase("implement").len(), 1);
471 assert_eq!(registry.get_by_phase("nonexistent").len(), 0);
472 }
473
474 #[test]
475 fn test_artifact_registry_get_by_kind() {
476 let mut registry = ArtifactRegistry::default();
477 let kind = ArtifactKind::TestResult {
478 passed: 10,
479 failed: 2,
480 };
481 registry.register("qa".to_string(), Artifact::new(kind.clone()));
482 registry.register(
483 "qa".to_string(),
484 Artifact::new(ArtifactKind::Custom {
485 name: "x".to_string(),
486 }),
487 );
488
489 let results = registry.get_by_kind(&kind);
490 assert_eq!(results.len(), 1);
491 }
492
493 #[test]
494 fn test_artifact_registry_get_latest() {
495 let mut registry = ArtifactRegistry::default();
496 assert!(registry.get_latest("qa").is_none());
497
498 registry.register(
499 "qa".to_string(),
500 Artifact::new(ArtifactKind::Custom {
501 name: "first".to_string(),
502 }),
503 );
504 registry.register(
505 "qa".to_string(),
506 Artifact::new(ArtifactKind::Custom {
507 name: "second".to_string(),
508 }),
509 );
510
511 let latest = registry
512 .get_latest("qa")
513 .expect("latest qa artifact should exist");
514 if let ArtifactKind::Custom { name } = &latest.kind {
515 assert_eq!(name, "second");
516 }
517 }
518
519 #[test]
520 fn test_artifact_registry_all() {
521 let mut registry = ArtifactRegistry::default();
522 registry.register(
523 "qa".to_string(),
524 Artifact::new(ArtifactKind::Custom {
525 name: "a".to_string(),
526 }),
527 );
528 registry.register(
529 "plan".to_string(),
530 Artifact::new(ArtifactKind::Custom {
531 name: "b".to_string(),
532 }),
533 );
534
535 let all = registry.all();
536 assert_eq!(all.len(), 2);
537 }
538
539 #[test]
540 fn test_shared_state_template() {
541 let mut state = SharedState::default();
542 state.set("name", serde_json::json!("test"));
543 state.set("count", serde_json::json!(42));
544
545 let result = state.render_template("Hello {name}, count is {count}");
546 assert_eq!(result, "Hello test, count is 42");
547 }
548
549 #[test]
550 fn test_shared_state_operations() {
551 let mut state = SharedState::default();
552 assert!(state.get("key").is_none());
553
554 state.set("key", serde_json::json!("value"));
555 assert_eq!(
556 state.get("key").expect("shared state key should exist"),
557 &serde_json::json!("value")
558 );
559
560 let removed = state.remove("key");
561 assert!(removed.is_some());
562 assert!(state.get("key").is_none());
563 }
564
565 #[test]
566 fn test_shared_state_render_non_string_json() {
567 let mut state = SharedState::default();
568 state.set("data", serde_json::json!({"nested": true}));
569
570 let result = state.render_template("result: {data}");
571 assert!(result.contains("nested"));
572 }
573
574 #[test]
575 fn test_parse_artifacts_from_output_json_object() {
576 let input = r#"{"kind":"ticket","severity":"high","category":"bug"}"#;
577 let artifacts = parse_artifacts_from_output(input);
578 assert_eq!(artifacts.len(), 1);
579 if let ArtifactKind::Ticket { severity, category } = &artifacts[0].kind {
580 assert_eq!(*severity, Severity::High);
581 assert_eq!(category, "bug");
582 } else {
583 assert!(
584 matches!(&artifacts[0].kind, ArtifactKind::Ticket { .. }),
585 "expected Ticket"
586 );
587 }
588 }
589
590 #[test]
591 fn test_parse_artifacts_from_output_nested_artifacts_array() {
592 let input = r#"{"confidence":0.4,"quality_score":0.25,"artifacts":[{"kind":"ticket","severity":"high","category":"capability","content":{"title":"qa-from-agent"}}]}"#;
593 let artifacts = parse_artifacts_from_output(input);
594 assert_eq!(artifacts.len(), 1);
595 if let ArtifactKind::Ticket { severity, category } = &artifacts[0].kind {
596 assert_eq!(*severity, Severity::High);
597 assert_eq!(category, "capability");
598 } else {
599 assert!(
600 matches!(&artifacts[0].kind, ArtifactKind::Ticket { .. }),
601 "expected Ticket from nested artifacts array"
602 );
603 }
604 }
605
606 #[test]
607 fn test_parse_artifacts_from_output_json_array() {
608 let input = r#"[{"kind":"test_result","passed":5,"failed":1},{"kind":"code_change","files":["a.rs"]}]"#;
609 let artifacts = parse_artifacts_from_output(input);
610 assert_eq!(artifacts.len(), 2);
611 }
612
613 #[test]
614 fn test_parse_artifacts_from_output_ticket_marker() {
615 let input = "some output\n[TICKET: severity=high, category=bug]\nmore output";
616 let artifacts = parse_artifacts_from_output(input);
617 assert_eq!(artifacts.len(), 1);
618 if let ArtifactKind::Ticket { severity, category } = &artifacts[0].kind {
619 assert_eq!(*severity, Severity::High);
620 assert_eq!(category, "bug");
621 }
622 }
623
624 #[test]
625 fn test_parse_artifacts_from_output_ticket_severity_levels() {
626 let levels = [
627 ("severity=critical", Severity::Critical),
628 ("severity=medium", Severity::Medium),
629 ("severity=low", Severity::Low),
630 ("severity=unknown", Severity::Info),
631 ];
632 for (marker, expected) in levels {
633 let input = format!("[TICKET: {}, category=bug]", marker);
634 let artifacts = parse_artifacts_from_output(&input);
635 assert_eq!(artifacts.len(), 1);
636 if let ArtifactKind::Ticket { severity, .. } = &artifacts[0].kind {
637 assert_eq!(*severity, expected, "failed for marker: {}", marker);
638 }
639 }
640 }
641
642 #[test]
643 fn test_parse_artifacts_from_output_ticket_categories() {
644 let categories = [
645 ("category=security", "security"),
646 ("category=performance", "performance"),
647 ("category=other", "general"),
648 ];
649 for (marker, expected) in categories {
650 let input = format!("[TICKET: severity=high, {}]", marker);
651 let artifacts = parse_artifacts_from_output(&input);
652 if let ArtifactKind::Ticket { category, .. } = &artifacts[0].kind {
653 assert_eq!(category, expected);
654 }
655 }
656 }
657
658 #[test]
659 fn test_parse_artifacts_from_output_no_artifacts() {
660 let artifacts = parse_artifacts_from_output("plain text with no markers");
661 assert!(artifacts.is_empty());
662 }
663
664 #[test]
665 fn test_extract_artifact_kind_decision() {
666 let value = serde_json::json!({
667 "kind": "decision",
668 "choice": "option_a",
669 "rationale": "better performance"
670 });
671 let kind = extract_artifact_kind(&value).expect("decision artifact should parse");
672 if let ArtifactKind::Decision { choice, rationale } = &kind {
673 assert_eq!(choice, "option_a");
674 assert_eq!(rationale, "better performance");
675 } else {
676 assert!(
677 matches!(&kind, ArtifactKind::Decision { .. }),
678 "expected Decision"
679 );
680 }
681 }
682
683 #[test]
684 fn test_extract_artifact_kind_analysis() {
685 let value = serde_json::json!({
686 "kind": "analysis",
687 "findings": [
688 {"title": "Issue 1", "severity": "high", "description": "desc"}
689 ]
690 });
691 let kind = extract_artifact_kind(&value).expect("analysis artifact should parse");
692 if let ArtifactKind::Analysis { findings } = kind {
693 assert_eq!(findings.len(), 1);
694 assert_eq!(findings[0].title, "Issue 1");
695 assert_eq!(findings[0].severity, Severity::High);
696 }
697 }
698
699 #[test]
700 fn test_extract_artifact_kind_unknown_returns_none() {
701 let value = serde_json::json!({"kind": "unknown_type"});
702 assert!(extract_artifact_kind(&value).is_none());
703 }
704
705 #[test]
706 fn test_extract_artifact_kind_missing_kind_returns_none() {
707 let value = serde_json::json!({"data": "value"});
708 assert!(extract_artifact_kind(&value).is_none());
709 }
710}