Skip to main content

eidos_ekf/
lib.rs

1//! EKF — Eidos Knowledge Format package contract.
2//!
3//! EKF is the package boundary around Eidos, not the runtime graph. It declares partitions, source
4//! roots, policies, gates, and artifact layout. Eidos still computes the graph, retrieval confidence,
5//! trust tiers, and receipts.
6//!
7//! This crate intentionally parses only the small v0.1 manifest subset Eidos consumes today.
8//! Unsupported or malformed structure becomes diagnostics instead of implicit behavior.
9
10pub 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}