1pub const MANIFEST_FILE: &str = "EKF.yaml";
11pub const SUPPORTED_EKF_VERSION: &str = "0.1";
12
13mod map;
14mod model;
15mod paths;
16mod resolve;
17mod validate;
18
19pub use map::{parse_manifest, parse_manifest_file};
20pub use model::*;
21pub use resolve::resolve_package;
22pub use validate::gate_intent;
23
24#[cfg(test)]
25mod tests {
26 use std::path::{Path, PathBuf};
27
28 use super::*;
29
30 #[test]
31 fn parses_v01_manifest_subset() {
32 let (manifest, diagnostics) = parse_manifest(
33 r#"
34ekf_version: "0.1"
35name: eidos-engine
36partitions:
37 - id: docs.core
38 title: Core Docs
39 source: aun
40 roots: ["docs/"]
41 role: knowledge
42 tags: [docs, core]
43 - id: code.rust
44 source: glyph
45 roots: ["crates/"]
46 languages: ["rust"]
47sources:
48 docs: ["docs/"]
49 skills:
50 - skills/
51 code:
52 - path: "crates/"
53 source: glyph
54 languages: ["rust", "python"]
55 evals: ["evals/"]
56trust:
57 default_authority: human
58 gold_ground_threshold: 3
59gates:
60 route:
61 min_p_at_1: 0.96
62relations:
63 produces:
64 forward: creates
65 reverse: created by
66 context_score: 34
67"#,
68 );
69
70 assert!(diagnostics.is_empty(), "{diagnostics:#?}");
71 assert_eq!(manifest.ekf_version.as_deref(), Some("0.1"));
72 assert_eq!(manifest.name.as_deref(), Some("eidos-engine"));
73 assert_eq!(manifest.partitions.len(), 2);
74 assert_eq!(manifest.partitions[0].id, "docs.core");
75 assert_eq!(manifest.partitions[0].source, "aun");
76 assert_eq!(manifest.partitions[0].roots, vec!["docs/"]);
77 assert_eq!(manifest.partitions[0].role.as_deref(), Some("knowledge"));
78 assert_eq!(manifest.partitions[0].tags, vec!["docs", "core"]);
79 assert_eq!(manifest.partitions[1].source, "glyph");
80 assert_eq!(manifest.partitions[1].languages, vec!["rust"]);
81 assert_eq!(manifest.sources.docs, vec!["docs/"]);
82 assert_eq!(manifest.sources.skills, vec!["skills/"]);
83 assert_eq!(manifest.sources.code.len(), 1);
84 assert_eq!(manifest.sources.code[0].path, "crates/");
85 assert_eq!(manifest.sources.code[0].source.as_deref(), Some("glyph"));
86 assert_eq!(manifest.sources.code[0].languages, vec!["rust", "python"]);
87 assert_eq!(
88 manifest.gates["route"]["min_p_at_1"], "0.96",
89 "gate value should be retained as a scalar string"
90 );
91 assert_eq!(manifest.trust["gold_ground_threshold"], "3");
92 assert_eq!(manifest.relations["produces"]["forward"], "creates");
93 assert_eq!(manifest.relations["produces"]["reverse"], "created by");
94 assert_eq!(manifest.relations["produces"]["context_score"], "34");
95 }
96
97 #[test]
98 fn four_space_indentation_parses_identically_to_two_space() {
99 let two_space = r#"
100ekf_version: "0.1"
101name: eidos-engine
102partitions:
103 - id: docs.core
104 title: Core Docs
105 source: aun
106 roots: ["docs/"]
107 tags: [docs, core]
108sources:
109 docs: ["docs/"]
110 code:
111 - path: "crates/"
112 source: glyph
113 languages: ["rust"]
114trust:
115 gold_ground_threshold: 3
116gates:
117 route:
118 min_p_at_1: 0.96
119relations:
120 produces:
121 forward: creates
122"#;
123 let four_space = r#"
124ekf_version: "0.1"
125name: eidos-engine
126partitions:
127 - id: docs.core
128 title: Core Docs
129 source: aun
130 roots: ["docs/"]
131 tags: [docs, core]
132sources:
133 docs: ["docs/"]
134 code:
135 - path: "crates/"
136 source: glyph
137 languages: ["rust"]
138trust:
139 gold_ground_threshold: 3
140gates:
141 route:
142 min_p_at_1: 0.96
143relations:
144 produces:
145 forward: creates
146"#;
147
148 let (two, two_diagnostics) = parse_manifest(two_space);
149 let (four, four_diagnostics) = parse_manifest(four_space);
150 assert!(two_diagnostics.is_empty(), "{two_diagnostics:#?}");
151 assert!(four_diagnostics.is_empty(), "{four_diagnostics:#?}");
152 assert_eq!(two, four);
153 assert_eq!(two.partitions.len(), 1, "partitions must not be dropped");
154 }
155
156 #[test]
157 fn flow_and_block_lists_are_interchangeable() {
158 let flow = r#"
159ekf_version: "0.1"
160partitions:
161 - id: code.rust
162 source: glyph
163 roots: ["crates/", "tools/"]
164 languages: [rust, python]
165 tags: [code]
166"#;
167 let block = r#"
168ekf_version: "0.1"
169partitions:
170 - id: code.rust
171 source: glyph
172 roots:
173 - "crates/"
174 - "tools/"
175 languages:
176 - rust
177 - python
178 tags:
179 - code
180"#;
181
182 let (from_flow, flow_diagnostics) = parse_manifest(flow);
183 let (from_block, block_diagnostics) = parse_manifest(block);
184 assert!(flow_diagnostics.is_empty(), "{flow_diagnostics:#?}");
185 assert!(block_diagnostics.is_empty(), "{block_diagnostics:#?}");
186 assert_eq!(from_flow, from_block);
187 assert_eq!(from_flow.partitions[0].roots, vec!["crates/", "tools/"]);
188 assert_eq!(from_flow.partitions[0].languages, vec!["rust", "python"]);
189 }
190
191 #[test]
192 fn single_and_double_quoted_scalars_unquote() {
193 let (manifest, diagnostics) = parse_manifest(
194 r#"
195ekf_version: '0.1'
196name: "eidos-engine"
197partitions:
198 - id: 'docs.core'
199 title: "Core Docs"
200 source: aun
201 roots: ['docs/', "extra docs/"]
202"#,
203 );
204
205 assert!(diagnostics.is_empty(), "{diagnostics:#?}");
206 assert_eq!(manifest.ekf_version.as_deref(), Some("0.1"));
207 assert_eq!(manifest.name.as_deref(), Some("eidos-engine"));
208 assert_eq!(manifest.partitions[0].id, "docs.core");
209 assert_eq!(manifest.partitions[0].title.as_deref(), Some("Core Docs"));
210 assert_eq!(manifest.partitions[0].roots, vec!["docs/", "extra docs/"]);
211 }
212
213 #[test]
214 fn invalid_yaml_yields_diagnostic_and_default_manifest() {
215 let (manifest, diagnostics) = parse_manifest("gates: [unclosed\n\t: flow");
216
217 assert_eq!(manifest, Manifest::default());
218 assert_eq!(diagnostics.len(), 1, "{diagnostics:#?}");
219 assert_eq!(diagnostics[0].kind, "yaml_error");
220 assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Warning);
221 }
222
223 #[test]
224 fn gate_and_trust_scalars_keep_raw_text() {
225 let (manifest, diagnostics) = parse_manifest(
226 r#"
227ekf_version: "0.1"
228trust:
229 gold_ground_threshold: 5
230gates:
231 route:
232 min_p_at_1: 0.96
233 min_partition_match_rate: 1.0
234 dogfood:
235 min_dogfood_score: 0.90
236 enabled: true
237"#,
238 );
239
240 assert!(diagnostics.is_empty(), "{diagnostics:#?}");
241 assert_eq!(manifest.trust["gold_ground_threshold"], "5");
242 assert_eq!(manifest.gates["route"]["min_p_at_1"], "0.96");
243 assert_eq!(manifest.gates["route"]["min_partition_match_rate"], "1.0");
244 assert_eq!(manifest.gates["dogfood"]["min_dogfood_score"], "0.90");
245 assert_eq!(manifest.gates["dogfood"]["enabled"], "true");
246 }
247
248 #[test]
249 fn resolves_partitions_as_package_compartments() {
250 let tmp = tempfile::tempdir().unwrap();
251 for dir in ["knowledge", "src", "package-evals"] {
252 std::fs::create_dir_all(tmp.path().join(dir)).unwrap();
253 }
254 std::fs::write(
255 tmp.path().join(MANIFEST_FILE),
256 r#"
257ekf_version: "0.1"
258name: partitioned-package
259partitions:
260 - id: knowledge.core
261 title: Core Knowledge
262 source: aun
263 roots: ["knowledge"]
264 role: knowledge
265 - id: code.app
266 source: glyph
267 roots: ["src"]
268 languages: ["rust"]
269 - id: eval.route
270 source: eval
271 roots: ["package-evals"]
272 role: route
273"#,
274 )
275 .unwrap();
276
277 let config = resolve_package(PackageRequest {
278 start_dir: Some(tmp.path().to_path_buf()),
279 ..Default::default()
280 });
281
282 assert!(config.report.manifest_found);
283 assert_eq!(
284 config.report.resolved.docs.as_deref(),
285 Some(tmp.path().join("knowledge").as_path())
286 );
287 assert_eq!(
288 config.report.resolved.code.as_deref(),
289 Some(tmp.path().join("src").as_path())
290 );
291 assert_eq!(config.report.partitions.len(), 3);
292 assert_eq!(config.report.partitions[0].id, "knowledge.core");
293 assert_eq!(config.report.partitions[0].status, "compiled");
294 assert_eq!(
295 config.report.partitions[0].roots,
296 vec![tmp.path().join("knowledge")]
297 );
298 assert_eq!(config.report.partitions[1].languages, vec!["rust"]);
299 assert_eq!(config.report.partitions[2].status, "eval");
300 assert!(
301 config
302 .report
303 .resolved
304 .source_roots
305 .iter()
306 .any(|root| { root.kind == "docs" && root.path == tmp.path().join("knowledge") })
307 );
308 assert!(
309 config
310 .report
311 .resolved
312 .source_roots
313 .iter()
314 .any(|root| { root.kind == "code" && root.path == tmp.path().join("src") })
315 );
316 assert!(
317 config.report.resolved.source_roots.iter().any(|root| {
318 root.kind == "evals" && root.path == tmp.path().join("package-evals")
319 })
320 );
321 }
322
323 #[test]
324 fn diagnoses_invalid_partitions() {
325 let tmp = tempfile::tempdir().unwrap();
326 std::fs::write(
327 tmp.path().join(MANIFEST_FILE),
328 r#"
329ekf_version: "0.1"
330partitions:
331 - id: broken
332 source: spaceship
333 roots: ["missing"]
334 - id: broken
335 source: aun
336 - source: glyph
337 roots: ["missing-code"]
338"#,
339 )
340 .unwrap();
341
342 let config = resolve_package(PackageRequest {
343 start_dir: Some(tmp.path().to_path_buf()),
344 ..Default::default()
345 });
346
347 assert!(config.report.diagnostics.iter().any(|diagnostic| {
348 diagnostic.kind == "unsupported_partition_source"
349 && diagnostic.message.contains("spaceship")
350 }));
351 assert!(
352 config
353 .report
354 .diagnostics
355 .iter()
356 .any(|diagnostic| diagnostic.kind == "duplicate_partition")
357 );
358 assert!(
359 config
360 .report
361 .diagnostics
362 .iter()
363 .any(|diagnostic| diagnostic.kind == "partition_missing_id")
364 );
365 assert!(
366 config
367 .report
368 .diagnostics
369 .iter()
370 .any(|diagnostic| diagnostic.kind == "partition_missing_roots")
371 );
372 assert!(
373 config
374 .report
375 .diagnostics
376 .iter()
377 .any(|diagnostic| diagnostic.kind == "missing_source_root")
378 );
379 }
380
381 #[test]
382 fn resolves_trust_policy_and_reports_invalid_values() {
383 let tmp = tempfile::tempdir().unwrap();
384 std::fs::write(
385 tmp.path().join(MANIFEST_FILE),
386 r#"
387ekf_version: "0.1"
388trust:
389 gold_ground_threshold: 1
390 stale_after_days: nope
391 agent_accepted_ttl_days: 30
392 mystery: true
393"#,
394 )
395 .unwrap();
396
397 let config = resolve_package(PackageRequest {
398 start_dir: Some(tmp.path().to_path_buf()),
399 ..Default::default()
400 });
401
402 assert_eq!(config.report.trust_policy.gold_ground_threshold, 1);
403 assert_eq!(config.report.trust_policy.agent_accepted_ttl_days, Some(30));
404 assert_eq!(config.report.trust_policy.stale_after_days, None);
405 assert!(
406 config
407 .report
408 .diagnostics
409 .iter()
410 .any(|d| d.kind == "invalid_trust_policy"
411 && d.message.contains("stale_after_days"))
412 );
413 assert!(
414 config
415 .report
416 .diagnostics
417 .iter()
418 .any(|d| d.kind == "unknown_trust_field" && d.message.contains("mystery"))
419 );
420 }
421
422 #[test]
423 fn accepts_partition_eval_gate_metrics() {
424 let tmp = tempfile::tempdir().unwrap();
425 std::fs::write(
426 tmp.path().join(MANIFEST_FILE),
427 r#"
428ekf_version: "0.1"
429gates:
430 coverage:
431 min_partition_judged_cases: 1
432 min_vocabulary_gap_judged_cases: 1
433 min_no_vocabulary_gap_judged_cases: 1
434 min_vocab_editable_target_judged_cases: 1
435 min_vocab_non_editable_target_judged_cases: 1
436 min_omitted_context_judged_cases: 1
437 min_omitted_compound_anchor_judged_cases: 1
438 graph:
439 max_unpartitioned_compiled_nodes: 0
440 context:
441 min_partition_match_rate: 1.0
442 min_vocabulary_gap_match_rate: 1.0
443 min_no_vocabulary_gap_match_rate: 1.0
444 min_vocab_editable_target_match_rate: 1.0
445 min_vocab_non_editable_target_match_rate: 1.0
446 min_omitted_context_match_rate: 1.0
447 min_omitted_compound_anchor_match_rate: 1.0
448 dogfood:
449 min_partition_match_rate: 1.0
450"#,
451 )
452 .unwrap();
453
454 let config = resolve_package(PackageRequest {
455 start_dir: Some(tmp.path().to_path_buf()),
456 ..Default::default()
457 });
458
459 assert!(
460 !config
461 .report
462 .diagnostics
463 .iter()
464 .any(|diagnostic| diagnostic.kind == "unknown_gate_metric"),
465 "partition gate metrics should be accepted: {:?}",
466 config.report.diagnostics
467 );
468 }
469
470 #[test]
471 fn resolves_relation_profiles_and_reports_invalid_values() {
472 let tmp = tempfile::tempdir().unwrap();
473 std::fs::write(
474 tmp.path().join(MANIFEST_FILE),
475 r#"
476ekf_version: "0.1"
477relations:
478 produces:
479 forward: creates
480 reverse: created by
481 context_score: 34
482 domain_kinds: [doc, section]
483 range_kinds: [function, type]
484 blocks:
485 forward: blocks
486 reverse: blocked by
487 context_score: nope
488 searchable: maybe
489 traversable: false
490 domain_kinds: [doc, bogus]
491 unknown: value
492"#,
493 )
494 .unwrap();
495
496 let config = resolve_package(PackageRequest {
497 start_dir: Some(tmp.path().to_path_buf()),
498 ..Default::default()
499 });
500
501 let produces = &config.report.relation_profiles["produces"];
502 assert_eq!(produces.forward_phrase, "creates");
503 assert_eq!(produces.reverse_phrase, "created by");
504 assert_eq!(produces.context_score, 34);
505 assert_eq!(produces.domain_kinds, vec!["doc", "section"]);
506 assert_eq!(produces.range_kinds, vec!["function", "type"]);
507
508 let supports = &config.report.relation_profiles["supports"];
509 assert_eq!(supports.forward_phrase, "supports");
510 assert_eq!(supports.reverse_phrase, "supported by");
511 assert_eq!(supports.context_score, 34);
512 assert!(supports.searchable);
513 assert!(supports.traversable);
514
515 let asserts = &config.report.relation_profiles["asserts"];
516 assert_eq!(asserts.forward_phrase, "asserts");
517 assert_eq!(asserts.reverse_phrase, "asserted by");
518
519 let qualifies = &config.report.relation_profiles["qualifies"];
520 assert_eq!(qualifies.forward_phrase, "qualifies");
521 assert_eq!(qualifies.reverse_phrase, "qualified by");
522
523 let blocks = &config.report.relation_profiles["blocks"];
524 assert_eq!(blocks.forward_phrase, "blocks");
525 assert_eq!(blocks.reverse_phrase, "blocked by");
526 assert_eq!(blocks.context_score, 4);
527 assert!(!blocks.traversable);
528 assert!(blocks.searchable);
529 assert_eq!(blocks.domain_kinds, vec!["doc"]);
530
531 assert!(
532 config.report.diagnostics.iter().any(
533 |d| d.kind == "invalid_relation_profile" && d.message.contains("context_score")
534 )
535 );
536 assert!(
537 config
538 .report
539 .diagnostics
540 .iter()
541 .any(|d| d.kind == "invalid_relation_profile" && d.message.contains("searchable"))
542 );
543 assert!(
544 config
545 .report
546 .diagnostics
547 .iter()
548 .any(|d| d.kind == "unknown_relation_field")
549 );
550 assert!(config.report.diagnostics.iter().any(|d| {
551 d.kind == "invalid_relation_profile" && d.message.contains("unsupported kind 'bogus'")
552 }));
553 }
554
555 #[test]
556 fn resolves_manifest_roots_and_reports_missing_sources() {
557 let tmp = tempfile::tempdir().unwrap();
558 std::fs::create_dir_all(tmp.path().join("docs")).unwrap();
559 std::fs::write(
560 tmp.path().join(MANIFEST_FILE),
561 r#"
562ekf_version: "0.1"
563name: package
564sources:
565 docs: ["docs/"]
566 code:
567 - path: "missing-code/"
568 source: glyph
569"#,
570 )
571 .unwrap();
572
573 let config = resolve_package(PackageRequest {
574 start_dir: Some(tmp.path().to_path_buf()),
575 ..Default::default()
576 });
577
578 assert!(config.report.manifest_found);
579 assert_eq!(config.report.resolved.vault.as_deref(), Some(tmp.path()));
580 assert_eq!(
581 config.report.resolved.docs.as_deref(),
582 Some(tmp.path().join("docs").as_path())
583 );
584 assert!(
585 config
586 .report
587 .resolved
588 .source_roots
589 .iter()
590 .any(|root| root.kind == "docs" && root.status == "compiled")
591 );
592 assert!(
593 config
594 .report
595 .resolved
596 .source_roots
597 .iter()
598 .any(|root| root.kind == "code" && root.status == "compiled")
599 );
600 assert!(
601 config
602 .report
603 .diagnostics
604 .iter()
605 .any(|d| d.kind == "missing_source_root" && d.message.contains("missing-code"))
606 );
607 }
608
609 #[test]
610 fn explicit_relative_manifest_uses_current_package_root() {
611 let tmp = tempfile::tempdir().unwrap();
612 std::fs::write(
613 tmp.path().join(MANIFEST_FILE),
614 r#"
615ekf_version: "0.1"
616name: package
617sources:
618 docs: ["docs/"]
619"#,
620 )
621 .unwrap();
622 let old = std::env::current_dir().unwrap();
623 std::env::set_current_dir(tmp.path()).unwrap();
624
625 let config = resolve_package(PackageRequest {
626 manifest: Some(PathBuf::from(MANIFEST_FILE)),
627 ..Default::default()
628 });
629
630 std::env::set_current_dir(old).unwrap();
631 assert_eq!(config.report.package_root.as_deref(), Some(Path::new(".")));
632 assert_eq!(
633 config.report.resolved.vault.as_deref(),
634 Some(Path::new("."))
635 );
636 assert!(
637 config
638 .report
639 .resolved
640 .source_roots
641 .iter()
642 .any(|root| root.kind == "vault" && root.path == Path::new("."))
643 );
644 }
645
646 #[test]
647 fn infers_zero_config_roots_without_manifest() {
648 let tmp = tempfile::tempdir().unwrap();
649 std::fs::create_dir_all(tmp.path().join("docs")).unwrap();
650
651 let config = resolve_package(PackageRequest {
652 start_dir: Some(tmp.path().to_path_buf()),
653 ..Default::default()
654 });
655
656 assert!(!config.report.manifest_found);
657 assert_eq!(config.report.resolved.vault.as_deref(), Some(tmp.path()));
658 assert_eq!(
659 config.report.resolved.docs.as_deref(),
660 Some(tmp.path().join("docs").as_path())
661 );
662 assert_eq!(config.report.resolved.code.as_deref(), Some(tmp.path()));
663 assert!(
664 config
665 .report
666 .resolved
667 .source_roots
668 .iter()
669 .any(|root| root.kind == "vault" && root.status == "compiled")
670 );
671 assert!(
672 config
673 .report
674 .resolved
675 .source_roots
676 .iter()
677 .any(|root| root.kind == "code" && root.status == "compiled")
678 );
679 }
680
681 #[test]
682 fn cli_overrides_manifest_roots() {
683 let tmp = tempfile::tempdir().unwrap();
684 let override_docs = tmp.path().join("override-docs");
685 std::fs::create_dir_all(&override_docs).unwrap();
686 std::fs::write(
687 tmp.path().join(MANIFEST_FILE),
688 r#"
689ekf_version: "0.1"
690sources:
691 docs: ["docs/"]
692"#,
693 )
694 .unwrap();
695
696 let config = resolve_package(PackageRequest {
697 start_dir: Some(tmp.path().to_path_buf()),
698 docs: Some(override_docs.clone()),
699 ..Default::default()
700 });
701
702 assert_eq!(config.report.resolved.docs, Some(override_docs));
703 assert!(
704 config
705 .report
706 .resolved
707 .source_roots
708 .iter()
709 .any(|root| root.kind == "docs" && root.status == "compiled")
710 );
711 assert!(
712 !config
713 .report
714 .resolved
715 .source_roots
716 .iter()
717 .any(|root| root.path.ends_with("docs") && root.status == "declared"),
718 "CLI docs override should replace manifest docs roots in the package report"
719 );
720 }
721
722 #[test]
723 fn reports_legacy_workflows_source_warning() {
724 let tmp = tempfile::tempdir().unwrap();
725 std::fs::create_dir_all(tmp.path().join("workflows")).unwrap();
726 std::fs::write(
727 tmp.path().join(MANIFEST_FILE),
728 r#"
729ekf_version: "0.1"
730sources:
731 workflows: ["workflows"]
732"#,
733 )
734 .unwrap();
735
736 let config = resolve_package(PackageRequest {
737 start_dir: Some(tmp.path().to_path_buf()),
738 ..Default::default()
739 });
740
741 assert!(
742 config
743 .report
744 .diagnostics
745 .iter()
746 .any(|diagnostic| diagnostic.kind == "legacy_workflows_source")
747 );
748 assert!(
749 config
750 .report
751 .resolved
752 .source_roots
753 .iter()
754 .any(|root| root.kind == "workflows" && root.status == "planning")
755 );
756 }
757
758 #[test]
759 fn reports_all_v01_package_source_planes() {
760 let tmp = tempfile::tempdir().unwrap();
761 for dir in [
762 "knowledge",
763 "skills",
764 "agents",
765 "capabilities",
766 "brief-profiles",
767 "workflows",
768 "evals",
769 "src",
770 ] {
771 std::fs::create_dir_all(tmp.path().join(dir)).unwrap();
772 }
773 std::fs::write(
774 tmp.path().join(MANIFEST_FILE),
775 r#"
776ekf_version: "0.1"
777sources:
778 docs: ["knowledge"]
779 skills: ["skills"]
780 agents: ["agents"]
781 capabilities: ["capabilities"]
782 brief_profiles: ["brief-profiles"]
783 workflows: ["workflows"]
784 evals: ["evals"]
785 code:
786 - path: "src"
787 source: glyph
788"#,
789 )
790 .unwrap();
791
792 let config = resolve_package(PackageRequest {
793 start_dir: Some(tmp.path().to_path_buf()),
794 ..Default::default()
795 });
796 let roots = &config.report.resolved.source_roots;
797
798 for kind in [
799 "vault",
800 "docs",
801 "skills",
802 "agents",
803 "capabilities",
804 "brief_profiles",
805 "workflows",
806 "evals",
807 "code",
808 ] {
809 assert!(
810 roots.iter().any(|root| root.kind == kind),
811 "missing EKF source root kind {kind}: {roots:#?}"
812 );
813 }
814 assert!(
815 roots
816 .iter()
817 .any(|root| root.kind == "docs" && root.status == "compiled")
818 );
819 assert!(
820 roots
821 .iter()
822 .any(|root| root.kind == "brief_profiles" && root.status == "planning")
823 );
824 assert!(
825 roots
826 .iter()
827 .any(|root| root.kind == "workflows" && root.status == "planning")
828 );
829 assert!(
830 roots
831 .iter()
832 .any(|root| root.kind == "evals" && root.status == "eval")
833 );
834 }
835}