1use crate::packs::{entity_name, finish, finish_soft, Section};
13use crate::{ContextCompiler, ContextPack};
14use scc_core::{
15 language_by_id, Archetype, AtlasComponent, AtlasEntrypoint, AtlasFlow, AtlasHierarchyNode,
16 AtlasInvariant, AtlasOwnershipClaim, ContractSubclass, FlowKind, SystemAtlas,
17};
18use scc_graph::TrustedGraphView;
19use std::collections::{BTreeMap, BTreeSet, HashMap};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum AtlasScope {
30 Production,
34 Full,
36}
37
38fn entity_role(view: &TrustedGraphView, e: &scc_core::Entity) -> &'static str {
44 let file: Option<String> = e
45 .attributes
46 .get("file")
47 .and_then(|v| v.as_str())
48 .map(String::from)
49 .or_else(|| {
50 e.attributes
51 .get("handler")
52 .and_then(|v| v.as_str())
53 .and_then(|h| view.entity(h))
54 .and_then(|s| {
55 s.attributes
56 .get("file")
57 .and_then(|v| v.as_str())
58 .map(String::from)
59 })
60 })
61 .or_else(|| {
62 e.attributes
63 .get("dockerfile")
64 .and_then(|v| v.as_str())
65 .map(String::from)
66 });
67 file.as_deref()
68 .and_then(scc_graph::components::path_role)
69 .unwrap_or("production")
70}
71
72fn entity_id_role(view: &TrustedGraphView, id: &str) -> &'static str {
75 view.entity(id)
76 .map(|e| entity_role(view, e))
77 .unwrap_or("production")
78}
79
80fn participants_production(view: &TrustedGraphView, ids: &[String]) -> Option<bool> {
92 let mut decided = false;
93 let mut prod = false;
94 for id in ids {
95 if let Some(e) = view.entity(id) {
96 if e.attributes.get("file").and_then(|v| v.as_str()).is_some() {
97 decided = true;
98 if entity_role(view, e) == "production" {
99 prod = true;
100 }
101 }
102 }
103 }
104 if decided {
105 Some(prod)
106 } else {
107 None
108 }
109}
110
111fn actor_production(
116 view: &TrustedGraphView,
117 comp_role_by_id: &HashMap<String, String>,
118 actor: &str,
119) -> Option<bool> {
120 if let Some(r) = comp_role_by_id.get(actor) {
121 return Some(r == "production");
122 }
123 view.entity(actor).map(|e| entity_role(view, e) == "production")
124}
125
126fn keep_flow(
131 view: &TrustedGraphView,
132 comp_role_by_id: &HashMap<String, String>,
133 scope: AtlasScope,
134 actors: &[String],
135) -> bool {
136 if scope == AtlasScope::Full {
137 return true;
138 }
139 let mut decided = false;
140 for a in actors {
141 match actor_production(view, comp_role_by_id, a) {
142 Some(true) => return true,
143 Some(false) => decided = true,
144 None => {}
145 }
146 }
147 !decided
148}
149
150fn scoped_entities<'a>(
152 view: &'a TrustedGraphView,
153 kind: &str,
154 scope: AtlasScope,
155 section: &str,
156 scoped_out: &mut BTreeMap<String, usize>,
157) -> Vec<&'a scc_core::Entity> {
158 let all = view.entities_of_kind(kind);
159 if scope == AtlasScope::Full {
160 return all;
161 }
162 let total = all.len();
163 let kept: Vec<&'a scc_core::Entity> = all
164 .into_iter()
165 .filter(|e| entity_role(view, e) == "production")
166 .collect();
167 let dropped = total - kept.len();
168 if dropped > 0 {
169 *scoped_out.entry(section.to_string()).or_default() += dropped;
170 }
171 kept
172}
173
174pub fn build_atlas(ctx: &ContextCompiler) -> SystemAtlas {
179 build_atlas_scoped(ctx, AtlasScope::Production)
180}
181
182pub fn build_atlas_scoped(ctx: &ContextCompiler, scope: AtlasScope) -> SystemAtlas {
186 let view = &ctx.view;
187 let store = ctx.store;
188 let snapshot = store.latest_snapshot().ok().flatten();
189 let repo = store.repository();
190
191 let purpose = store.meta_get("purpose").ok().flatten().unwrap_or_default();
192
193 let mut components: Vec<AtlasComponent> = Vec::new();
195 let mut data_stores: BTreeSet<String> = BTreeSet::new();
199 let mut comp_store_targets: Vec<(String, Vec<String>)> = Vec::new();
200 let mut scoped_out: BTreeMap<String, usize> = BTreeMap::new();
202 for c in view.components() {
203 let responsibility = c
207 .attributes
208 .get("responsibility")
209 .and_then(|v| v.as_array());
210 fn claim_text(r: &serde_json::Value) -> &str {
211 r.get("text").and_then(|t| t.as_str()).unwrap_or("")
212 }
213 let purpose_text = responsibility
214 .and_then(|a| {
215 a.iter()
216 .find(|r| {
217 matches!(
218 r.get("provenance").and_then(|p| p.as_str()),
219 Some("Declared") | Some("Resolved")
220 )
221 })
222 .or_else(|| a.first())
223 .map(claim_text)
224 })
225 .unwrap_or("")
226 .to_string();
227
228 let implementation_attr = c
229 .attributes
230 .get("implementation")
231 .cloned()
232 .unwrap_or(serde_json::json!({}));
233 let implementation_paths: Vec<String> = implementation_attr
234 .get("paths")
235 .and_then(|p| p.as_array())
236 .map(|a| {
237 a.iter()
238 .filter_map(|x| x.as_str().map(String::from))
239 .collect()
240 })
241 .unwrap_or_default();
242 let role = c
249 .attributes
250 .get("role")
251 .and_then(|v| v.as_str())
252 .map(String::from)
253 .unwrap_or_else(|| scc_graph::components::component_role(&implementation_paths).into());
254 let mut implementation: Vec<String> = implementation_paths.clone();
255 let mut symbols: Vec<String> = implementation_attr
256 .get("symbols")
257 .and_then(|s| s.as_array())
258 .map(|a| {
259 a.iter()
260 .filter_map(|x| x.as_str().map(String::from))
261 .collect()
262 })
263 .unwrap_or_default();
264 symbols.sort();
265 symbols.dedup();
266 implementation.extend(symbols.iter().cloned());
267
268 let mut upstream: BTreeSet<String> = BTreeSet::new();
269 let mut downstream: BTreeSet<String> = BTreeSet::new();
270 for r in view.in_pred(&c.id, scc_core::predicates::DEPENDS_ON) {
271 upstream.insert(entity_name(view, &r.subject));
272 }
273 for r in view.out_pred(&c.id, scc_core::predicates::DEPENDS_ON) {
274 downstream.insert(entity_name(view, &r.object));
275 }
276
277 let mut failure_behavior: Vec<String> = Vec::new();
278 if let Some(rs) = c.attributes.get("retries").and_then(|v| v.as_array()) {
279 failure_behavior.extend(rs.iter().filter_map(|x| x.as_str().map(String::from)));
280 }
281 for r in view.out_pred(&c.id, scc_core::predicates::CROSSES_BOUNDARY) {
282 failure_behavior.push(format!(
283 "crosses boundary -> {}",
284 entity_name(view, &r.object)
285 ));
286 }
287
288 let mut consumes: BTreeSet<String> = BTreeSet::new();
289 let mut produces: BTreeSet<String> = BTreeSet::new();
290 for pred in [
291 scc_core::predicates::READS,
292 scc_core::predicates::CONSUMES,
293 scc_core::predicates::QUERIES,
294 ] {
295 for r in view.out_pred(&c.id, pred) {
296 consumes.insert(entity_name(view, &r.object));
297 }
298 }
299 for pred in [
300 scc_core::predicates::PRODUCES,
301 scc_core::predicates::PUBLISHES,
302 scc_core::predicates::WRITES,
303 ] {
304 for r in view.out_pred(&c.id, pred) {
305 produces.insert(entity_name(view, &r.object));
306 }
307 }
308
309 let mut owns: Vec<AtlasOwnershipClaim> = Vec::new();
316 let mut my_store_targets: Vec<String> = Vec::new();
317 if let Some(oa) = c.attributes.get("owns").and_then(|v| v.as_array()) {
318 for o in oa {
319 let Some(t) = o.get("target").and_then(|v| v.as_str()) else {
320 continue;
321 };
322 let p = o.get("provenance").and_then(|v| v.as_str()).unwrap_or("");
323 let is_store_target = view
324 .entity(t)
325 .map(|e| {
326 e.kind == scc_core::kinds::DATA_STORE
327 || e.kind == scc_core::kinds::DATA_ENTITY
328 })
329 .unwrap_or(false);
330 let target_name = match view.entity(t) {
331 Some(e) if e.kind == scc_core::kinds::DATA_STORE => e.name.clone(),
332 Some(e) if e.kind == scc_core::kinds::DATA_ENTITY => e
333 .attributes
334 .get("store")
335 .and_then(|v| v.as_str())
336 .map(|s| format!("{s}.{}", e.name))
337 .unwrap_or_else(|| e.name.clone()),
338 _ => entity_name(view, t),
339 };
340 if is_store_target {
341 data_stores.insert(target_name.clone());
342 my_store_targets.push(target_name.clone());
343 }
344 owns.push(AtlasOwnershipClaim {
345 target: target_name,
346 provenance: p.to_string(),
347 });
348 }
349 }
350 let mut seen: BTreeSet<(String, String)> = BTreeSet::new();
353 owns.retain(|o| seen.insert((o.target.clone(), o.provenance.clone())));
354
355 let layer = c
359 .attributes
360 .get("layer")
361 .and_then(|v| v.as_str())
362 .unwrap_or("component")
363 .to_string();
364 let parent = c
365 .attributes
366 .get("parent")
367 .and_then(|v| v.as_str())
368 .map(String::from);
369 comp_store_targets.push((c.name.clone(), my_store_targets));
370
371 components.push(AtlasComponent {
372 name: c.name.clone(),
373 purpose: purpose_text,
374 implementation,
375 implementation_paths,
376 symbols,
377 consumes: consumes.into_iter().collect(),
378 produces: produces.into_iter().collect(),
379 upstream: upstream.into_iter().collect(),
380 downstream: downstream.into_iter().collect(),
381 failure_behavior,
382 owns,
383 layer,
384 parent,
385 role,
386 });
387 }
388 components.sort_by(|a, b| a.name.cmp(&b.name));
389
390 let comp_role: HashMap<String, String> = components
396 .iter()
397 .map(|c| (c.name.clone(), c.role.clone()))
398 .collect();
399 let comp_role_by_id: HashMap<String, String> = view
400 .components()
401 .into_iter()
402 .map(|c| {
403 let paths: Vec<String> = c
404 .attributes
405 .get("implementation")
406 .and_then(|v| v.get("paths"))
407 .and_then(|v| v.as_array())
408 .map(|a| {
409 a.iter()
410 .filter_map(|x| x.as_str().map(String::from))
411 .collect()
412 })
413 .unwrap_or_default();
414 let role = c
415 .attributes
416 .get("role")
417 .and_then(|v| v.as_str())
418 .map(String::from)
419 .unwrap_or_else(|| scc_graph::components::component_role(&paths).into());
420 (c.id.clone(), role)
421 })
422 .collect();
423 let prod_comp = |name: &str| -> bool {
424 scope == AtlasScope::Full
425 || comp_role.get(name).map(|r| r == "production").unwrap_or(true)
426 };
427 if scope == AtlasScope::Production {
428 data_stores = comp_store_targets
429 .into_iter()
430 .filter(|(name, _)| prod_comp(name))
431 .flat_map(|(_, tgts)| tgts)
432 .collect();
433 }
434
435 let mut entrypoints: Vec<AtlasEntrypoint> = Vec::new();
436 let mut seen_routes: BTreeSet<(String, String, String)> = BTreeSet::new();
440 for r in scoped_entities(view, scc_core::kinds::ROUTE, scope, "entrypoints", &mut scoped_out) {
441 let method = r
442 .attributes
443 .get("method")
444 .and_then(|v| v.as_str())
445 .unwrap_or("");
446 let path = r
447 .attributes
448 .get("path")
449 .and_then(|v| v.as_str())
450 .unwrap_or("");
451 let handler = r
452 .attributes
453 .get("handler")
454 .and_then(|v| v.as_str())
455 .unwrap_or("")
456 .to_string();
457 if !seen_routes.insert((method.to_string(), path.to_string(), handler.clone())) {
458 continue;
459 }
460 entrypoints.push(AtlasEntrypoint {
461 name: r.name.clone(),
462 kind: "route".into(),
463 trigger: format!("{method} {path}"),
464 symbol: handler,
465 });
466 }
467 for e in scoped_entities(view, scc_core::kinds::SYMBOL, scope, "entrypoints", &mut scoped_out) {
468 let Some(kinds) = e.attributes.get("entrypoints").and_then(|v| v.as_array()) else {
469 continue;
470 };
471 if kinds.is_empty() {
472 continue;
473 }
474 let kind = if kinds.iter().any(|k| k.as_str() == Some("cli-subcommand")) {
478 "cli-subcommand"
479 } else {
480 "entrypoint"
481 };
482 entrypoints.push(AtlasEntrypoint {
483 name: e.name.clone(),
484 kind: kind.into(),
485 trigger: format!("entrypoint:{}", e.name),
486 symbol: e.id.clone(),
487 });
488 }
489 let mut surface_names: BTreeSet<(String, String)> = entrypoints
497 .iter()
498 .map(|e| (e.name.clone(), e.kind.clone()))
499 .collect();
500 for s in scc_graph::flows::invocation_surfaces(view.graph) {
501 if scope == AtlasScope::Production && entity_id_role(view, &s.symbol) != "production" {
502 *scoped_out.entry("entrypoints".into()).or_default() += 1;
503 continue;
504 }
505 let name = view.name_of(&s.symbol);
506 let kind = s.kind.as_str().to_string();
507 if !surface_names.insert((name.clone(), kind.clone())) {
508 continue;
509 }
510 entrypoints.push(AtlasEntrypoint {
511 name,
512 kind,
513 trigger: s.trigger.clone(),
514 symbol: s.symbol.clone(),
515 });
516 }
517 entrypoints.sort_by(|a, b| a.name.cmp(&b.name));
518
519 let mut contracts: Vec<scc_core::Contract> = Vec::new();
521 let mut contract_seen: BTreeMap<(String, String), usize> = BTreeMap::new();
522
523 for r in scoped_entities(view, scc_core::kinds::ROUTE, scope, "contracts", &mut scoped_out) {
525 let method = r
526 .attributes
527 .get("method")
528 .and_then(|v| v.as_str())
529 .unwrap_or("");
530 let path = r
531 .attributes
532 .get("path")
533 .and_then(|v| v.as_str())
534 .unwrap_or("");
535 if path.is_empty() {
536 continue;
537 }
538 let handler = r
539 .attributes
540 .get("handler")
541 .and_then(|v| v.as_str())
542 .unwrap_or("")
543 .to_string();
544 let mut consumers: BTreeSet<String> = BTreeSet::new();
545 for pred in [
546 scc_core::predicates::HANDLES,
547 scc_core::predicates::CONSUMES,
548 scc_core::predicates::READS,
549 ] {
550 for rel in view.in_pred(&r.id, pred) {
551 consumers.insert(entity_name(view, &rel.subject));
552 }
553 }
554 push_contract(
555 &mut contracts,
556 &mut contract_seen,
557 scc_core::Contract {
558 id: r.id.clone(),
559 kind: "http".into(),
560 subclass: ContractSubclass::Http,
561 producer: handler,
562 consumers: consumers.into_iter().collect(),
563 operations: vec![format!("{method} {path}").trim().to_string()],
564 evidence: r.evidence.clone(),
565 },
566 );
567 }
568
569 for e in scoped_entities(view, scc_core::kinds::SYMBOL, scope, "contracts", &mut scoped_out) {
572 let Some(flags) = e.attributes.get("cli_flags").and_then(|v| v.as_array()) else {
573 continue;
574 };
575 let mut ops: Vec<String> = flags
576 .iter()
577 .filter_map(|f| f.as_str().map(String::from))
578 .collect();
579 ops.sort();
580 ops.dedup();
581 if ops.is_empty() {
582 continue;
583 }
584 push_contract(
585 &mut contracts,
586 &mut contract_seen,
587 scc_core::Contract {
588 id: scc_core::entity_id(
589 &store.repo_id,
590 scc_core::kinds::CONTRACT,
591 &format!("cli:{}", e.name),
592 ),
593 kind: "cli".into(),
594 subclass: ContractSubclass::Cli,
595 producer: e.id.clone(),
596 consumers: Vec::new(),
597 operations: ops,
598 evidence: e.evidence.clone(),
599 },
600 );
601 }
602
603 for t in view.entities_of_kind(scc_core::kinds::TOPIC) {
608 let mut consumers: BTreeSet<String> = BTreeSet::new();
609 let mut participant_ids: Vec<String> = Vec::new();
610 let mut any = false;
611 for pred in [
612 scc_core::predicates::PUBLISHES,
613 scc_core::predicates::SUBSCRIBES,
614 scc_core::predicates::CONSUMES,
615 ] {
616 for rel in view.in_pred(&t.id, pred) {
617 consumers.insert(entity_name(view, &rel.subject));
618 participant_ids.push(rel.subject.clone());
619 any = true;
620 }
621 }
622 if !any {
623 continue;
624 }
625 if scope == AtlasScope::Production {
626 let self_prod = t
627 .attributes
628 .get("file")
629 .and_then(|v| v.as_str())
630 .and_then(scc_graph::components::path_role)
631 .map(|r| r == "production")
632 .unwrap_or(false);
633 let parts_prod = participants_production(view, &participant_ids).unwrap_or(true);
634 if !self_prod && !parts_prod {
635 *scoped_out.entry("contracts".into()).or_default() += 1;
636 continue;
637 }
638 }
639 push_contract(
640 &mut contracts,
641 &mut contract_seen,
642 scc_core::Contract {
643 id: t.id.clone(),
644 kind: "event".into(),
645 subclass: ContractSubclass::Event,
646 producer: t.id.clone(),
647 consumers: consumers.into_iter().collect(),
648 operations: vec![t.name.clone()],
649 evidence: t.evidence.clone(),
650 },
651 );
652 }
653
654 for c in view.entities_of_kind(scc_core::kinds::CONFIGURATION) {
657 if scope == AtlasScope::Production {
658 let self_prod = c
659 .attributes
660 .get("file")
661 .and_then(|v| v.as_str())
662 .and_then(scc_graph::components::path_role)
663 .map(|r| r == "production")
664 .unwrap_or(false);
665 if !self_prod {
666 let mut party: Vec<String> = view
667 .out_pred(&c.id, scc_core::predicates::CONFIGURED_BY)
668 .into_iter()
669 .map(|r| r.object.clone())
670 .collect();
671 for pred in [
672 scc_core::predicates::READS,
673 scc_core::predicates::CONSUMES,
674 scc_core::predicates::HANDLES,
675 ] {
676 for rel in view.in_pred(&c.id, pred) {
677 party.push(rel.subject.clone());
678 }
679 }
680 if participants_production(view, &party) == Some(false) {
681 *scoped_out.entry("contracts".into()).or_default() += 1;
682 continue;
683 }
684 }
685 }
686 let mut owners: Vec<String> = view
687 .out_pred(&c.id, scc_core::predicates::CONFIGURED_BY)
688 .into_iter()
689 .map(|r| r.object.clone())
690 .collect();
691 owners.sort();
692 owners.dedup();
693 let producer = owners.first().cloned().unwrap_or_else(|| c.id.clone());
694 let mut consumers: BTreeSet<String> = BTreeSet::new();
695 for pred in [
696 scc_core::predicates::READS,
697 scc_core::predicates::CONSUMES,
698 scc_core::predicates::HANDLES,
699 ] {
700 for rel in view.in_pred(&c.id, pred) {
701 consumers.insert(entity_name(view, &rel.subject));
702 }
703 }
704 for o in &owners {
705 consumers.insert(entity_name(view, o));
706 }
707 push_contract(
708 &mut contracts,
709 &mut contract_seen,
710 scc_core::Contract {
711 id: c.id.clone(),
712 kind: "config".into(),
713 subclass: ContractSubclass::Configuration,
714 producer,
715 consumers: consumers.into_iter().collect(),
716 operations: vec![c.name.clone()],
717 evidence: c.evidence.clone(),
718 },
719 );
720 }
721
722 for e in view.entities_of_kind(scc_core::kinds::EXPORT) {
731 if scope == AtlasScope::Production && entity_role(view, e) != "production" {
732 let sym_prod = view
733 .in_pred(&e.id, scc_core::predicates::EXPORTS)
734 .into_iter()
735 .next()
736 .map(|r| entity_id_role(view, &r.subject) == "production")
737 .unwrap_or(true);
738 if !sym_prod {
739 *scoped_out.entry("contracts".into()).or_default() += 1;
740 continue;
741 }
742 }
743 let kind_attr = e
744 .attributes
745 .get("kind")
746 .and_then(|v| v.as_str())
747 .unwrap_or("");
748 if !matches!(kind_attr, "function" | "method" | "constructor") {
749 continue;
750 }
751 let symbol = view
752 .in_pred(&e.id, scc_core::predicates::EXPORTS)
753 .into_iter()
754 .next()
755 .map(|r| r.subject.clone())
756 .unwrap_or_default();
757 let mut consumers: BTreeSet<String> = BTreeSet::new();
758 for pred in [
759 scc_core::predicates::CALLS,
760 scc_core::predicates::CONSUMES,
761 scc_core::predicates::HANDLES,
762 ] {
763 for rel in view.in_pred(&symbol, pred) {
764 consumers.insert(entity_name(view, &rel.subject));
765 }
766 }
767 push_contract(
768 &mut contracts,
769 &mut contract_seen,
770 scc_core::Contract {
771 id: e.id.clone(),
772 kind: "public-api".into(),
773 subclass: ContractSubclass::PublicApi,
774 producer: symbol,
775 consumers: consumers.into_iter().collect(),
776 operations: vec![e.name.clone()],
777 evidence: e.evidence.clone(),
778 },
779 );
780 }
781
782 for ce in scoped_entities(view, scc_core::kinds::CONTRACT, scope, "contracts", &mut scoped_out) {
792 let Some(kind_attr) = ce
793 .attributes
794 .get("kind")
795 .and_then(|v| v.as_str())
796 .map(str::to_string)
797 else {
798 continue;
799 };
800 let Some(subclass) = ContractSubclass::from_kind_str(&kind_attr) else {
801 continue;
802 };
803 let producer = view
804 .in_pred(&ce.id, scc_core::predicates::REGISTERS)
805 .into_iter()
806 .next()
807 .map(|r| r.subject.clone())
808 .unwrap_or_default();
809 let mut consumers: BTreeSet<String> = BTreeSet::new();
810 for pred in [
811 scc_core::predicates::CONSUMES,
812 scc_core::predicates::READS,
813 scc_core::predicates::HANDLES,
814 ] {
815 for rel in view.in_pred(&ce.id, pred) {
816 consumers.insert(entity_name(view, &rel.subject));
817 }
818 }
819 push_contract(
820 &mut contracts,
821 &mut contract_seen,
822 scc_core::Contract {
823 id: ce.id.clone(),
824 kind: subclass.as_str().to_string(),
825 subclass,
826 producer,
827 consumers: consumers.into_iter().collect(),
828 operations: vec![ce.name.clone()],
829 evidence: ce.evidence.clone(),
830 },
831 );
832 }
833 let mut inline: Vec<(usize, String)> = Vec::new();
847 for s in scoped_entities(view, scc_core::kinds::SCHEMA, scope, "contracts", &mut scoped_out) {
848 let count = scc_graph::state::occurrence_count(view.graph, &s.id);
849 let expr = s
850 .attributes
851 .get("expr")
852 .and_then(|v| v.as_str())
853 .map(|e| e.to_string());
854 let is_inline = expr.as_deref() == Some(s.name.as_str());
855 let producer = scc_graph::state::occurrence_producer(view.graph, &s.id)
856 .unwrap_or_else(|| s.id.clone());
857 if is_inline {
858 inline.push((count, s.id.clone()));
859 continue;
860 }
861 let owner = view
862 .in_pred(&s.id, scc_core::predicates::DEFINES)
863 .into_iter()
864 .next()
865 .map(|r| r.subject.clone());
866 let mut ops: Vec<String> = vec![s.name.clone()];
867 if let Some(e) = &expr {
873 if !e.is_empty() && e != &s.name {
874 ops.push(format!("{} = {}", s.name, e));
875 }
876 }
877 let mut composed: Vec<String> = view
878 .out_pred(&s.id, scc_core::predicates::COMPOSES)
879 .into_iter()
880 .map(|r| format!("{} extends {}", s.name, entity_name(view, &r.object)))
881 .collect();
882 composed.sort();
883 composed.dedup();
884 ops.extend(composed);
885 if let Some(owner_id) = &owner {
886 let mut validated: Vec<String> = view
887 .out_pred(owner_id, scc_core::predicates::VALIDATES)
888 .into_iter()
889 .map(|_| format!("{} validates", s.name))
890 .collect();
891 validated.sort();
892 validated.dedup();
893 ops.extend(validated);
894 }
895 push_contract(
896 &mut contracts,
897 &mut contract_seen,
898 scc_core::Contract {
899 id: s.id.clone(),
900 kind: "schema".into(),
901 subclass: ContractSubclass::Schema,
902 producer,
903 consumers: Vec::new(),
904 operations: ops,
905 evidence: s.evidence.clone(),
906 },
907 );
908 }
909 inline.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
911 for (count, id) in inline.into_iter().take(40) {
912 if count < 2 {
913 continue;
914 }
915 let Some(s) = view.entity(&id) else { continue };
916 let producer = scc_graph::state::occurrence_producer(view.graph, &s.id)
917 .unwrap_or_else(|| s.id.clone());
918 push_contract(
919 &mut contracts,
920 &mut contract_seen,
921 scc_core::Contract {
922 id: s.id.clone(),
923 kind: "schema".into(),
924 subclass: ContractSubclass::Schema,
925 producer,
926 consumers: Vec::new(),
927 operations: vec![s.name.clone()],
928 evidence: s.evidence.clone(),
929 },
930 );
931 }
932 contracts.sort_by(|a, b| {
935 a.subclass
936 .as_str()
937 .cmp(b.subclass.as_str())
938 .then(a.operations.join("\u{1}").cmp(&b.operations.join("\u{1}")))
939 .then(a.producer.cmp(&b.producer))
940 });
941
942 let mut flows: Vec<AtlasFlow> = Vec::new();
945 let mut async_boundaries: BTreeSet<String> = BTreeSet::new();
946 for g in store.flow_graphs().unwrap_or_default() {
947 if g.kind != scc_core::FlowKind::Sequence {
948 continue;
949 }
950 if !keep_flow(
951 view,
952 &comp_role_by_id,
953 scope,
954 &g.nodes.iter().map(|n| n.actor.clone()).collect::<Vec<_>>(),
955 ) {
956 *scoped_out.entry("flows".into()).or_default() += 1;
957 continue;
958 }
959 let steps = project_flow_graph(view, &g, &mut async_boundaries);
960 if steps.is_empty() {
961 continue;
962 }
963 flows.push(AtlasFlow {
964 name: g.name.clone(),
965 kind: g.kind,
966 trigger: g.trigger.clone(),
967 steps,
968 });
969 }
970 for f in view.flows() {
973 if f.kind == scc_core::FlowKind::Sequence {
974 continue; }
976 if !keep_flow(
977 view,
978 &comp_role_by_id,
979 scope,
980 &f.steps.iter().map(|s| s.actor.clone()).collect::<Vec<_>>(),
981 ) {
982 *scoped_out.entry("flows".into()).or_default() += 1;
983 continue;
984 }
985 let mut steps: Vec<String> = Vec::new();
986 let mut prev_actor: Option<String> = None;
987 for s in &f.steps {
988 let actor = entity_name(view, &s.actor);
989 let mut line = if prev_actor.as_deref() == Some(actor.as_str()) {
990 format!(" -> {}", s.operation)
991 } else {
992 format!("{}: {}", actor, s.operation)
993 };
994 if s.r#async == Some(true) {
995 line.push_str(" [async]");
996 }
997 if let Some(c) = &s.condition {
998 line.push_str(&format!(" (if {c})"));
999 }
1000 if let Some(rp) = &s.retry_policy {
1001 line.push_str(&format!(" [retry: {rp}]"));
1002 }
1003 if let Some(fo) = &s.failure_outcome {
1004 line.push_str(&format!(" [fail: {fo}]"));
1005 }
1006 steps.push(line);
1007 prev_actor = Some(actor);
1008 }
1009 if f.attributes.get("signals_only").and_then(|v| v.as_bool()) == Some(true) {
1010 flows.push(AtlasFlow {
1011 name: format!("{} (LIFECYCLE SIGNALS — NOT VERIFIED TRANSITIONS)", f.name),
1012 kind: f.kind,
1013 trigger: f.trigger.clone(),
1014 steps,
1015 });
1016 } else {
1017 flows.push(AtlasFlow {
1018 name: f.name.clone(),
1019 kind: f.kind,
1020 trigger: f.trigger.clone(),
1021 steps,
1022 });
1023 }
1024 }
1025 flows.sort_by(|a, b| a.name.cmp(&b.name));
1026
1027 let invariants: Vec<AtlasInvariant> = view
1029 .invariants()
1030 .into_iter()
1031 .map(|i| AtlasInvariant {
1032 statement: i.statement,
1033 severity: i.severity,
1034 })
1035 .collect();
1036
1037 let deployment_units: Vec<String> = scoped_entities(
1039 view,
1040 scc_core::kinds::DEPLOYMENT_UNIT,
1041 scope,
1042 "deployment",
1043 &mut scoped_out,
1044 )
1045 .into_iter()
1046 .map(|e| {
1047 let img = e
1048 .attributes
1049 .get("image")
1050 .and_then(|v| v.as_str())
1051 .unwrap_or("");
1052 if img.is_empty() {
1053 e.name.clone()
1054 } else {
1055 format!("{} ({})", e.name, img)
1056 }
1057 })
1058 .collect();
1059 let external_systems: Vec<String> = scoped_entities(
1060 view,
1061 scc_core::kinds::EXTERNAL_API,
1062 scope,
1063 "external-systems",
1064 &mut scoped_out,
1065 )
1066 .into_iter()
1067 .map(|e| e.name.clone())
1068 .collect();
1069 let trust_boundaries: Vec<String> = if scope == AtlasScope::Full {
1070 scc_graph::boundaries::boundary_crossings(view.graph, store).unwrap_or_default()
1071 } else {
1072 scc_graph::boundaries::production_crossings(view.graph, store).unwrap_or_default()
1073 };
1074
1075 let mut implementation_map: BTreeMap<String, Vec<String>> = BTreeMap::new();
1077 for c in &components {
1078 implementation_map.insert(c.name.clone(), c.implementation_paths.clone());
1079 }
1080
1081 let comp_ids: Vec<String> = view
1083 .components()
1084 .into_iter()
1085 .map(|c| c.id.clone())
1086 .collect();
1087 let evidence_summary = ctx.evidence_summary(&comp_ids);
1088
1089 let mut warnings: Vec<String> = Vec::new();
1090 let stale = view.stale_paths();
1091 if snapshot.is_some() {
1092 if stale.is_empty() {
1093 warnings.push("Model is FRESH".into());
1094 } else {
1095 warnings.push(format!(
1096 "Model is stale: {} changed file(s) not yet re-indexed.",
1097 stale.len()
1098 ));
1099 }
1100 } else {
1101 warnings.push("Repository is NOT indexed — run `scc index`.".into());
1102 }
1103 warnings.extend(view.stale_warnings());
1104
1105 let freshness = if snapshot.is_none() {
1106 "NOT INDEXED".to_string()
1107 } else if stale.is_empty() {
1108 "FRESH".to_string()
1109 } else {
1110 format!("STALE ({})", stale.len())
1111 };
1112
1113 let archetype = Some(scc_graph::archetype::detect_archetype(view.graph, store));
1115
1116 let mut symbol_comp: HashMap<String, String> = HashMap::new();
1119 for c in view.components() {
1120 for r in view.out_pred(&c.id, scc_core::predicates::CONTAINS) {
1121 for sr in view.out_pred(&r.object, scc_core::predicates::CONTAINS) {
1122 symbol_comp.insert(sr.object.clone(), c.name.clone());
1123 }
1124 }
1125 }
1126 let mut state_authority = scc_graph::state::compile_state_authority(view.graph, &symbol_comp);
1127 if scope == AtlasScope::Production {
1131 for lines in state_authority.values_mut() {
1132 let before = lines.len();
1133 lines.retain(|l| {
1134 let head = l.split([' ', ':']).next().unwrap_or("");
1135 prod_comp(head)
1136 });
1137 let dropped = before - lines.len();
1138 if dropped > 0 {
1139 *scoped_out.entry("state-authority".into()).or_default() += dropped;
1140 }
1141 }
1142 }
1143
1144 let mut hierarchy: Vec<AtlasHierarchyNode> = Vec::new();
1147 for kind in [scc_core::kinds::SERVICE, scc_core::kinds::SUBSYSTEM] {
1148 for e in view.graph.entities_of_kind(kind) {
1149 let mut members: Vec<String> = view
1150 .graph
1151 .out_pred(&e.id, scc_core::predicates::CONTAINS)
1152 .into_iter()
1153 .map(|r| r.object.clone())
1154 .collect();
1155 members.sort();
1156 hierarchy.push(AtlasHierarchyNode {
1157 id: e.id.clone(),
1158 name: e.name.clone(),
1159 kind: kind.to_string(),
1160 members,
1161 });
1162 }
1163 }
1164 hierarchy.sort_by(|a, b| a.kind.cmp(&b.kind).then_with(|| a.name.cmp(&b.name)));
1165
1166 let state_claims = scc_graph::state::compile_state_claims(view.graph, &symbol_comp);
1173 for claim in state_claims {
1174 if !prod_comp(&claim.component) {
1175 *scoped_out.entry("state-authority".into()).or_default() += 1;
1176 continue;
1177 }
1178 let Some(c) = components.iter_mut().find(|c| c.name == claim.component) else {
1179 continue;
1180 };
1181 let seen_claim = (claim.target.clone(), claim.provenance.clone());
1182 if claim.verb == "reads" {
1183 continue;
1184 }
1185 if !c
1186 .owns
1187 .iter()
1188 .any(|o| o.target == seen_claim.0 && o.provenance == seen_claim.1)
1189 {
1190 c.owns.push(AtlasOwnershipClaim {
1191 target: claim.target,
1192 provenance: claim.provenance,
1193 });
1194 }
1195 }
1196 for c in &mut components {
1197 if scope == AtlasScope::Production
1198 && comp_role.get(&c.name).map(|r| r != "production").unwrap_or(false)
1199 {
1200 let n = c.owns.len();
1201 c.owns.clear();
1202 if n > 0 {
1203 *scoped_out.entry("state-authority".into()).or_default() += n;
1204 }
1205 }
1206 c.owns.sort_by(|a, b| {
1207 a.target
1208 .cmp(&b.target)
1209 .then(a.provenance.cmp(&b.provenance))
1210 });
1211 }
1212
1213 let mut public_api: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
1218 for r in view.all_rels() {
1221 if r.predicate != scc_core::predicates::EXPORTS {
1222 continue;
1223 }
1224 let Some(comp) = symbol_comp.get(&r.subject) else {
1225 continue;
1226 };
1227 if !prod_comp(comp) {
1228 *scoped_out.entry("public-api".into()).or_default() += 1;
1229 continue;
1230 }
1231 if let Some(name) = view.entity(&r.object).map(|e| e.name.clone()) {
1232 if !name.is_empty() {
1233 public_api.entry(comp.clone()).or_default().insert(name);
1234 }
1235 }
1236 }
1237 for e in view.entities_of_kind(scc_core::kinds::SYMBOL) {
1239 if e.name.is_empty() || e.name.starts_with('_') || e.name.contains('.') {
1240 continue;
1241 }
1242 if e.attributes.get("exported").and_then(|v| v.as_bool()) != Some(true) {
1243 continue;
1244 }
1245 let Some(comp) = symbol_comp.get(&e.id) else {
1246 continue;
1247 };
1248 if !prod_comp(comp) {
1249 *scoped_out.entry("public-api".into()).or_default() += 1;
1250 continue;
1251 }
1252 public_api
1253 .entry(comp.clone())
1254 .or_default()
1255 .insert(e.name.clone());
1256 }
1257 let public_api: BTreeMap<String, Vec<String>> = public_api
1258 .into_iter()
1259 .map(|(k, v)| (k, v.into_iter().collect()))
1260 .collect();
1261
1262 let mut framework_semantics: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
1265 for a in view.entities_of_kind(scc_core::kinds::ANNOTATION) {
1267 let mut rels = view.out_pred(&a.id, scc_core::predicates::ANNOTATES);
1268 rels.sort_by(|x, y| x.object.cmp(&y.object));
1269 for r in rels {
1270 if let Some(comp) = symbol_comp.get(&r.object) {
1271 if !prod_comp(comp) {
1272 *scoped_out.entry("framework".into()).or_default() += 1;
1273 continue;
1274 }
1275 let target = entity_name(view, &r.object);
1276 framework_semantics
1277 .entry(comp.clone())
1278 .or_default()
1279 .insert(format!("annotates {target} ({})", a.name));
1280 }
1281 }
1282 }
1283 for pred in [
1285 scc_core::predicates::REGISTERS,
1286 scc_core::predicates::HANDLES_CALLBACK,
1287 ] {
1288 let mut rels = view.all_rels().to_vec();
1289 rels.sort_by(|x, y| {
1290 x.subject
1291 .cmp(&y.subject)
1292 .then(x.object.cmp(&y.object))
1293 .then(x.id.cmp(&y.id))
1294 });
1295 for r in rels {
1296 if r.predicate != pred {
1297 continue;
1298 }
1299 let Some(comp) = symbol_comp.get(&r.subject) else {
1300 continue;
1301 };
1302 if !prod_comp(comp) {
1303 *scoped_out.entry("framework".into()).or_default() += 1;
1304 continue;
1305 }
1306 let target = entity_name(view, &r.object);
1307 let line = if pred == scc_core::predicates::REGISTERS {
1308 format!("registers {target}")
1309 } else {
1310 format!("handles callback {target}")
1311 };
1312 framework_semantics
1313 .entry(comp.clone())
1314 .or_default()
1315 .insert(line);
1316 }
1317 }
1318 let framework_semantics: BTreeMap<String, Vec<String>> = framework_semantics
1319 .into_iter()
1320 .map(|(k, v)| (k, v.into_iter().collect()))
1321 .collect();
1322
1323 let pipeline = build_pipeline(view, archetype);
1328
1329 let landmarks = build_landmarks(view, &public_api, &symbol_comp, scope, &comp_role, &mut scoped_out);
1332
1333 SystemAtlas {
1334 repository: repo.name,
1335 revision: snapshot
1336 .as_ref()
1337 .map(|s| s.revision.clone())
1338 .unwrap_or_else(|| "not-indexed".to_string()),
1339 indexed_at: snapshot.map(|s| s.indexed_at).unwrap_or_default(),
1340 freshness,
1341 purpose,
1342 components,
1343 entrypoints,
1344 contracts,
1345 coverage: {
1346 let mut coverage = compute_coverage(ctx);
1347 let scoped_total: usize = scoped_out.values().sum();
1348 let scope_line = if scope == AtlasScope::Full {
1349 "full (every repository role feeds the architecture sections)".to_string()
1350 } else if scoped_total == 0 {
1351 "production (no non-production architecture facts found)".to_string()
1352 } else {
1353 let parts: Vec<String> = scoped_out
1354 .iter()
1355 .map(|(k, v)| format!("{k}:{v}"))
1356 .collect();
1357 format!(
1358 "production ({} non-production facts scoped out of architecture sections: {}; components/files still list all roles)",
1359 scoped_total,
1360 parts.join(", ")
1361 )
1362 };
1363 coverage.insert("scope".to_string(), scope_line);
1364 coverage
1365 },
1366 flows,
1367 invariants,
1368 deployment_units,
1369 external_systems,
1370 trust_boundaries,
1371 async_boundaries: async_boundaries.into_iter().collect(),
1372 implementation_map,
1373 data_stores: data_stores.into_iter().collect(),
1374 archetype,
1375 state_authority,
1376 hierarchy,
1377 evidence_summary,
1378 warnings,
1379 public_api,
1380 framework_semantics,
1381 pipeline,
1382 landmarks,
1383 }
1384}
1385
1386const PIPELINE_STAGES: [(&str, &[&str]); 5] = [
1389 (
1390 "parse",
1391 &[
1392 "parse",
1393 "parser",
1394 "lexer",
1395 "lex",
1396 "tokenize",
1397 "tokeniser",
1398 "ast",
1399 ],
1400 ),
1401 ("analyze", &["analyze", "analyse", "analysis"]),
1402 (
1403 "transform",
1404 &["transform", "lower", "resolve", "resolveconfig"],
1405 ),
1406 (
1407 "generate",
1408 &["generate", "generator", "codegen", "compile", "compiler"],
1409 ),
1410 (
1411 "emit",
1412 &["emit", "print", "format", "formatdoc", "serialize"],
1413 ),
1414];
1415
1416fn build_pipeline(view: &TrustedGraphView, archetype: Option<scc_core::Archetype>) -> Vec<String> {
1422 if archetype != Some(scc_core::Archetype::CompilerLanguageTool) {
1423 return Vec::new();
1424 }
1425 let mut lines: Vec<(usize, String)> = Vec::new();
1426 let mut seen: BTreeSet<String> = BTreeSet::new();
1427 let stage_of = |name: &str| -> Option<usize> {
1428 let lower = name.to_ascii_lowercase();
1429 PIPELINE_STAGES
1430 .iter()
1431 .position(|(_, verbs)| verbs.iter().any(|v| lower.contains(v)))
1432 };
1433 for e in view.entities_of_kind(scc_core::kinds::SYMBOL) {
1435 if e.name.is_empty() {
1436 continue;
1437 }
1438 let Some(rank) = stage_of(&e.name) else {
1439 continue;
1440 };
1441 if !seen.insert(e.name.clone()) {
1442 continue;
1443 }
1444 lines.push((rank, e.name.clone()));
1445 }
1446 for f in view.entities_of_kind(scc_core::kinds::FILE) {
1449 let name = f.name.clone();
1450 let lower = name.to_ascii_lowercase();
1451 let mut rank: Option<usize> = None;
1452 for (i, (_, verbs)) in PIPELINE_STAGES.iter().enumerate() {
1453 let numbered = verbs.iter().any(|v| {
1455 lower.contains(&format!("/{v}"))
1456 || lower.split('/').any(|seg| {
1457 let seg = seg.trim_start_matches(|c: char| c.is_ascii_digit());
1458 seg.trim_start_matches(['-', '_']).starts_with(v)
1459 })
1460 });
1461 if numbered {
1462 rank = Some(i);
1463 break;
1464 }
1465 }
1466 if rank.is_none() && lower.contains("/phases/") {
1467 rank = Some(0); }
1469 if let Some(r) = rank {
1470 if seen.insert(name.clone()) {
1471 lines.push((r, name));
1472 }
1473 }
1474 }
1475 lines.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
1476 let mut out: Vec<String> = Vec::new();
1477 let mut current_stage: Option<&str> = None;
1478 for (rank, name) in lines.into_iter().take(96) {
1479 let stage = PIPELINE_STAGES[rank].0;
1480 if current_stage != Some(stage) {
1481 out.push(format!("[{}]", stage));
1482 current_stage = Some(stage);
1483 }
1484 out.push(format!(" {name}"));
1485 }
1486 out
1487}
1488
1489fn build_landmarks(
1495 view: &TrustedGraphView,
1496 public_api: &BTreeMap<String, Vec<String>>,
1497 symbol_comp: &HashMap<String, String>,
1498 scope: AtlasScope,
1499 comp_role: &HashMap<String, String>,
1500 scoped_out: &mut BTreeMap<String, usize>,
1501) -> Vec<String> {
1502 let mut out: Vec<String> = Vec::new();
1503 let mut seen: BTreeSet<String> = BTreeSet::new();
1504 let mut classes: Vec<String> = Vec::new();
1506 let mut others: Vec<String> = Vec::new();
1507 for names in public_api.values() {
1508 for n in names {
1509 let kind = view
1510 .entities_of_kind(scc_core::kinds::SYMBOL)
1511 .into_iter()
1512 .find(|e| e.name == *n)
1513 .and_then(|e| e.attributes.get("kind"))
1514 .and_then(|v| v.as_str())
1515 .unwrap_or("");
1516 let is_class = matches!(
1517 kind,
1518 "class" | "struct" | "trait" | "interface" | "enum" | "type" | "module" | "model"
1519 );
1520 if is_class {
1521 classes.push(n.clone());
1522 } else {
1523 others.push(n.clone());
1524 }
1525 }
1526 }
1527 classes.sort();
1528 others.sort();
1529 let mut pool: Vec<String> = classes;
1530 pool.extend(others);
1531 for n in pool.into_iter().take(24) {
1532 if seen.insert(n.clone()) {
1533 out.push(format!("export {}", n));
1534 }
1535 }
1536 let mut targets: Vec<String> = Vec::new();
1539 for a in view.entities_of_kind(scc_core::kinds::ANNOTATION) {
1540 for r in view.out_pred(&a.id, scc_core::predicates::ANNOTATES) {
1541 if let Some(comp) = symbol_comp.get(&r.object) {
1542 if scope == AtlasScope::Production
1543 && comp_role.get(comp).map(|r| r != "production").unwrap_or(false)
1544 {
1545 *scoped_out.entry("landmarks".into()).or_default() += 1;
1546 continue;
1547 }
1548 let name = entity_name(view, &r.object);
1549 if !name.is_empty() && !name.starts_with('_') {
1550 targets.push(format!("{name} (@{})", a.name));
1551 }
1552 let _ = comp;
1553 }
1554 }
1555 }
1556 targets.sort();
1557 targets.dedup();
1558 for t in targets.into_iter().take(16) {
1559 if seen.insert(t.clone()) {
1560 out.push(t);
1561 }
1562 }
1563 out
1564}
1565
1566fn push_contract(
1571 contracts: &mut Vec<scc_core::Contract>,
1572 seen: &mut BTreeMap<(String, String), usize>,
1573 c: scc_core::Contract,
1574) {
1575 let key = (c.subclass.as_str().to_string(), c.operations.join("\u{1}"));
1576 if let Some(&idx) = seen.get(&key) {
1577 let existing = &mut contracts[idx];
1578 for s in c.consumers {
1579 if !existing.consumers.contains(&s) {
1580 existing.consumers.push(s);
1581 }
1582 }
1583 for e in c.evidence {
1584 if !existing.evidence.contains(&e) {
1585 existing.evidence.push(e);
1586 }
1587 }
1588 existing.consumers.sort();
1589 existing.evidence.sort();
1590 return;
1591 }
1592 seen.insert(key, contracts.len());
1593 contracts.push(c);
1594}
1595
1596fn is_extractor_language(lang: &str) -> bool {
1601 language_by_id(lang).is_some_and(|c| c.extractor)
1602}
1603
1604fn compute_coverage(ctx: &ContextCompiler) -> BTreeMap<String, String> {
1610 let view = &ctx.view;
1611 let store = ctx.store;
1612 let mut out: BTreeMap<String, String> = BTreeMap::new();
1613
1614 let files = store.all_files().unwrap_or_default();
1616 let total = files.len();
1617 let parsed = files
1618 .iter()
1619 .filter(|(_, _, lang, _, _)| is_extractor_language(lang))
1620 .count();
1621 let pct = parsed
1622 .checked_mul(100)
1623 .map(|n| n / total.max(1))
1624 .unwrap_or(0);
1625 out.insert(
1626 "parsed_source_files".to_string(),
1627 format!("{pct}% ({parsed}/{total})"),
1628 );
1629
1630 let exports = view.entities_of_kind(scc_core::kinds::EXPORT);
1632 let export_edges = view
1633 .all_rels()
1634 .iter()
1635 .filter(|r| r.predicate == scc_core::predicates::EXPORTS)
1636 .count();
1637 out.insert(
1638 "exported_api".to_string(),
1639 if exports.is_empty() {
1640 "none (no EXPORTS evidence)".to_string()
1641 } else {
1642 format!(
1643 "{} export entit{} ({} EXPORTS edges)",
1644 exports.len(),
1645 if exports.len() == 1 { "y" } else { "ies" },
1646 export_edges
1647 )
1648 },
1649 );
1650
1651 let calls: Vec<&scc_core::Relationship> = view
1658 .all_rels()
1659 .into_iter()
1660 .filter(|r| r.predicate == scc_core::predicates::CALLS)
1661 .collect();
1662 let total_calls = calls.len();
1663 let lsp_resolved = calls
1664 .iter()
1665 .filter(|r| r.provenance == scc_core::Provenance::Resolved)
1666 .count();
1667 let with_target = calls
1668 .iter()
1669 .filter(|r| {
1670 matches!(
1671 r.provenance,
1672 scc_core::Provenance::Resolved | scc_core::Provenance::Extracted
1673 ) && view.entity(&r.object).is_some()
1674 })
1675 .count();
1676 let pct = with_target
1677 .checked_mul(100)
1678 .map(|n| n / total_calls.max(1))
1679 .unwrap_or(0);
1680 out.insert(
1681 "call_targets_resolved".to_string(),
1682 if pct >= 100 {
1683 format!("{pct}% ({with_target}/{total_calls}, {lsp_resolved} LSP-RESOLVED)")
1684 } else {
1685 format!("{pct}% ({with_target}/{total_calls}, {lsp_resolved} LSP-RESOLVED) — exploration still justified in unresolved regions")
1686 },
1687 );
1688
1689 let unresolved = calls
1694 .iter()
1695 .filter(|r| {
1696 r.provenance != scc_core::Provenance::Resolved
1697 && view
1698 .entity(&r.object)
1699 .map(|e| e.kind != scc_core::kinds::SYMBOL)
1700 .unwrap_or(true)
1701 })
1702 .count();
1703 out.insert(
1704 "dynamic_receivers_unresolved".to_string(),
1705 format!(
1706 "{unresolved} (calls whose target is not a local symbol; unknown-receiver calls are not persisted)"
1707 ),
1708 );
1709
1710 let surfaces = scc_graph::flows::invocation_surfaces(view.graph);
1712 let mut by_kind: BTreeMap<&str, usize> = BTreeMap::new();
1713 for s in &surfaces {
1714 *by_kind.entry(s.kind.as_str()).or_insert(0) += 1;
1715 }
1716 let summary: Vec<String> = by_kind.iter().map(|(k, v)| format!("{k} {v}")).collect();
1717 out.insert(
1718 "invocation_surfaces".to_string(),
1719 format!("{} ({})", surfaces.len(), summary.join(", ")),
1720 );
1721
1722 let known_regs = view
1724 .all_rels()
1725 .iter()
1726 .filter(|r| r.predicate == scc_core::predicates::REGISTERS)
1727 .count();
1728 out.insert(
1729 "framework_registrations_unknown".to_string(),
1730 format!("0 ({known_regs} known registrations — unknown surfaces only reported with registry evidence)"),
1731 );
1732
1733 let stale = view.stale_paths();
1735 out.insert(
1736 "stale_evidence".to_string(),
1737 if stale.is_empty() {
1738 "0 (model FRESH)".to_string()
1739 } else {
1740 format!("{} changed file(s)", stale.len())
1741 },
1742 );
1743
1744 let unparsed = files
1746 .iter()
1747 .filter(|(_, _, lang, _, _)| !is_extractor_language(lang))
1748 .count();
1749 out.insert(
1750 "unparsed_files".to_string(),
1751 format!("{unparsed} (config/docs/infra — scanned but not source-parsed)"),
1752 );
1753
1754 let epoch = store.model_epoch().unwrap_or(scc_store::ModelEpoch::zero());
1756 let gens = epoch.source
1757 + epoch.semantic
1758 + epoch.evidence
1759 + epoch.intent
1760 + epoch.runtime
1761 + epoch.derived;
1762 out.insert(
1763 "model_epoch_generations".to_string(),
1764 format!(
1765 "{gens} (source {}, semantic {}, evidence {}, intent {}, runtime {}, derived {})",
1766 epoch.source,
1767 epoch.semantic,
1768 epoch.evidence,
1769 epoch.intent,
1770 epoch.runtime,
1771 epoch.derived
1772 ),
1773 );
1774
1775 out
1776}
1777
1778pub fn render_atlas(
1784 ctx: &ContextCompiler,
1785 atlas: &SystemAtlas,
1786 budget: usize,
1787 full: bool,
1788) -> ContextPack {
1789 let mut pack = ContextPack::new("atlas", &atlas.revision);
1790 let mut sections: Vec<Section> = Vec::new();
1791
1792 let mut purpose = String::new();
1795 purpose.push_str(&format!(
1796 "ARCHETYPE: {}\n",
1797 atlas
1798 .archetype
1799 .map(|a| a.as_str())
1800 .unwrap_or(Archetype::Unknown.as_str())
1801 ));
1802 if !atlas.purpose.is_empty() {
1803 purpose.push_str(&format!(
1804 "[SYSTEM PURPOSE — from README, DOCUMENTATION not fact]\n{}\n",
1805 atlas.purpose
1806 ));
1807 }
1808 if !atlas.entrypoints.is_empty() {
1809 purpose.push_str("ENTRYPOINTS\n");
1810 const EP_RENDER_CAP: usize = 200;
1814 for e in atlas.entrypoints.iter().take(EP_RENDER_CAP) {
1815 match e.kind.as_str() {
1822 "queue" | "schedule" | "plugin" | "lifecycle" => {
1823 let label = if e.kind == "lifecycle" {
1824 e.trigger
1825 .strip_prefix("lifecycle:")
1826 .map(|a| format!("@{a}"))
1827 .unwrap_or_else(|| e.name.clone())
1828 } else {
1829 e.name.clone()
1830 };
1831 purpose.push_str(&format!(" {}: {}\n", e.kind, label));
1832 }
1833 _ => {
1834 if e.trigger == e.name {
1835 purpose.push_str(&format!(" {} [{}]\n", e.name, e.kind));
1836 } else {
1837 purpose.push_str(&format!(" {} [{}] — {}\n", e.name, e.kind, e.trigger));
1838 }
1839 }
1840 }
1841 }
1842 if atlas.entrypoints.len() > EP_RENDER_CAP {
1843 purpose.push_str(&format!(
1844 " ... +{} more (full list in the machine model)\n",
1845 atlas.entrypoints.len() - EP_RENDER_CAP
1846 ));
1847 }
1848 }
1849 sections.push(Section::new("SYSTEM PURPOSE", purpose, 10));
1850
1851 let mut arch = String::new();
1856 let mut rendered: BTreeSet<String> = BTreeSet::new(); let comp_block = |c: &AtlasComponent, indent: &str| -> String {
1858 let mut out = format!("\n{}{}", indent, c.name.to_uppercase());
1859 if !c.role.is_empty() && c.role != "production" {
1863 out.push_str(&format!(" [{}]", c.role));
1864 }
1865 if !c.purpose.is_empty() {
1866 out.push_str(&format!("\n{}Purpose: {}", indent, c.purpose));
1867 }
1868 if !c.implementation_paths.is_empty() {
1869 out.push_str(&format!(
1870 "\n{}Implementation: {}",
1871 indent,
1872 c.implementation_paths.join(", ")
1873 ));
1874 if !c.symbols.is_empty() {
1875 out.push_str(&format!(" ({} member symbols)", c.symbols.len()));
1876 }
1877 }
1878 if !c.consumes.is_empty() {
1879 out.push_str(&format!("\n{}Consumes: {}", indent, c.consumes.join(", ")));
1880 }
1881 if !c.produces.is_empty() {
1882 out.push_str(&format!("\n{}Produces: {}", indent, c.produces.join(", ")));
1883 }
1884 if !c.upstream.is_empty() {
1885 out.push_str(&format!("\n{}Upstream: {}", indent, c.upstream.join(", ")));
1886 }
1887 if !c.downstream.is_empty() {
1888 out.push_str(&format!(
1889 "\n{}Downstream: {}",
1890 indent,
1891 c.downstream.join(", ")
1892 ));
1893 }
1894 if !c.owns.is_empty() {
1895 let owned: Vec<String> = c
1896 .owns
1897 .iter()
1898 .map(|o| format!("{} ({})", o.target, o.provenance))
1899 .collect();
1900 out.push_str(&format!("\n{}Owns: {}", indent, owned.join(", ")));
1901 }
1902 out.push('\n');
1903 out
1904 };
1905 let name_of = |id: &str| -> Option<String> { ctx.view.entity(id).map(|e| e.name.clone()) };
1906 for svc in atlas.hierarchy.iter().filter(|n| n.kind == "service") {
1908 arch.push_str(&format!("\nSERVICE {}\n", svc.name.to_uppercase()));
1909 for m in &svc.members {
1910 let Some(sub) = atlas.hierarchy.iter().find(|n| &n.id == m) else {
1911 continue;
1912 };
1913 if sub.kind != "subsystem" {
1914 continue;
1915 }
1916 arch.push_str(&format!(" SUBSYSTEM {}\n", sub.name.to_uppercase()));
1917 for cm in &sub.members {
1918 if let Some(name) = name_of(cm) {
1919 rendered.insert(name.clone());
1920 if let Some(c) = atlas.components.iter().find(|c| c.name == name) {
1921 arch.push_str(&format!(" {}\n", comp_block(c, " ").trim_end()));
1922 }
1923 }
1924 }
1925 arch.push('\n');
1926 }
1927 for m in &svc.members {
1928 if atlas.hierarchy.iter().any(|n| &n.id == m) {
1929 continue; }
1931 if let Some(name) = name_of(m) {
1932 rendered.insert(name.clone());
1933 if let Some(c) = atlas.components.iter().find(|c| c.name == name) {
1934 arch.push_str(&format!(" {}\n", comp_block(c, " ").trim_end()));
1935 }
1936 }
1937 }
1938 }
1939 for sub in atlas.hierarchy.iter().filter(|n| n.kind == "subsystem") {
1941 if atlas
1942 .hierarchy
1943 .iter()
1944 .any(|n| n.kind == "service" && n.members.contains(&sub.id))
1945 {
1946 continue;
1947 }
1948 arch.push_str(&format!("\nSUBSYSTEM {}\n", sub.name.to_uppercase()));
1949 for cm in &sub.members {
1950 if let Some(name) = name_of(cm) {
1951 rendered.insert(name.clone());
1952 if let Some(c) = atlas.components.iter().find(|c| c.name == name) {
1953 arch.push_str(&format!(" {}\n", comp_block(c, " ").trim_end()));
1954 }
1955 }
1956 }
1957 arch.push('\n');
1958 }
1959 for layer in ["component", "code_region"] {
1961 for c in &atlas.components {
1962 if rendered.contains(&c.name) {
1963 continue;
1964 }
1965 if c.layer != layer {
1966 continue;
1967 }
1968 arch.push_str(&comp_block(c, ""));
1969 }
1970 }
1971 sections.push(Section::new("ARCHITECTURE", arch, 9));
1972
1973 const FLOW_RENDER_CAP: usize = 32;
1981 const FLOW_RENDER_STEP_CAP: usize = 16;
1982 let mut flows = String::new();
1983 let mut render_flows: Vec<&AtlasFlow> = atlas
1984 .flows
1985 .iter()
1986 .filter(|f| f.kind != FlowKind::Architecture)
1987 .collect();
1988 render_flows.sort_by(|a, b| b.steps.len().cmp(&a.steps.len()).then(a.name.cmp(&b.name)));
1989 for f in render_flows.into_iter().take(FLOW_RENDER_CAP) {
1990 flows.push_str(&format!("\n{} [{}]", f.name, flow_kind_str(f.kind)));
1991 if let Some(t) = &f.trigger {
1992 flows.push_str(&format!("\nTrigger: {t}"));
1993 }
1994 for s in f.steps.iter().take(FLOW_RENDER_STEP_CAP) {
1995 flows.push_str(&format!("\n{s}"));
1996 }
1997 if f.steps.len() > FLOW_RENDER_STEP_CAP {
1998 flows.push_str(&format!(
1999 "\n... +{} more steps",
2000 f.steps.len() - FLOW_RENDER_STEP_CAP
2001 ));
2002 }
2003 flows.push('\n');
2004 }
2005 sections.push(Section::new("FLOWS", flows, 9));
2006
2007 let has_state = atlas.state_authority.values().any(|v| !v.is_empty());
2014 let mut state_body = String::new();
2015 if has_state {
2016 state_body.push_str("DATA OWNERSHIP\n");
2017 }
2018 for c in &atlas.components {
2019 for o in &c.owns {
2020 state_body.push_str(&format!(
2021 "{} owns {} ({})\n",
2022 c.name, o.target, o.provenance
2023 ));
2024 }
2025 }
2026 if let Some(lines) = atlas.state_authority.get(scc_graph::state::S_PERSISTENT) {
2027 for l in lines {
2028 if l.contains("::") {
2029 state_body.push_str(&format!("{l}\n"));
2030 }
2031 }
2032 }
2033 if !atlas.data_stores.is_empty() {
2034 state_body.push_str("\nDATA STORES\n");
2035 for s in &atlas.data_stores {
2036 state_body.push_str(&format!(" {s}\n"));
2037 }
2038 }
2039 for section in [
2040 scc_graph::state::S_RUNTIME,
2041 scc_graph::state::S_REACTIVE,
2042 scc_graph::state::S_CONFIGURATION,
2043 scc_graph::state::S_CACHES,
2044 scc_graph::state::S_DERIVED,
2045 ] {
2046 if let Some(lines) = atlas.state_authority.get(section) {
2047 if !lines.is_empty() {
2048 state_body.push_str(&format!("\n{}\n", scc_graph::state::section_label(section)));
2049 for l in lines {
2050 state_body.push_str(&format!(" {l}\n"));
2051 }
2052 }
2053 }
2054 }
2055 sections.push(Section::new(
2056 if has_state {
2057 "STATE & DATA AUTHORITY"
2058 } else {
2059 "DATA OWNERSHIP"
2060 },
2061 state_body,
2062 10,
2063 ));
2064
2065 sections.push(Section::new(
2072 "CONTRACTS",
2073 if atlas.contracts.is_empty() {
2074 "(none)".into()
2075 } else {
2076 let mut lines: Vec<String> = Vec::new();
2077 for c in &atlas.contracts {
2078 let prefix = c.subclass.as_str();
2079 for op in &c.operations {
2080 lines.push(format!("{prefix}: {op}"));
2081 }
2082 }
2083 lines.sort();
2084 lines.join("\n")
2085 },
2086 9,
2087 ));
2088
2089 sections.push(Section::new(
2094 "PUBLIC API",
2095 if atlas.public_api.is_empty() {
2096 "(none)".into()
2097 } else {
2098 const API_RENDER_CAP: usize = 64;
2099 let mut lines: Vec<String> = Vec::new();
2100 for (comp, exports) in &atlas.public_api {
2101 if exports.is_empty() {
2102 continue;
2103 }
2104 let shown: Vec<&str> = exports
2105 .iter()
2106 .take(API_RENDER_CAP)
2107 .map(|s| s.as_str())
2108 .collect();
2109 let mut line = format!("{}: exports {}", comp, shown.join(", "));
2110 if exports.len() > API_RENDER_CAP {
2111 line.push_str(&format!(" (+{} more)", exports.len() - API_RENDER_CAP));
2112 }
2113 lines.push(line);
2114 }
2115 lines.join("\n")
2116 },
2117 6,
2118 ));
2119
2120 sections.push(Section::new(
2124 "FRAMEWORK SEMANTICS",
2125 if atlas.framework_semantics.is_empty() {
2126 "(none)".into()
2127 } else {
2128 const SEM_RENDER_CAP: usize = 48;
2129 let mut lines: Vec<String> = Vec::new();
2130 for (comp, facts) in &atlas.framework_semantics {
2131 for f in facts.iter().take(SEM_RENDER_CAP) {
2132 lines.push(format!("{comp}: {f}"));
2133 }
2134 if facts.len() > SEM_RENDER_CAP {
2135 lines.push(format!("{comp}: (+{} more)", facts.len() - SEM_RENDER_CAP));
2136 }
2137 }
2138 lines.join("\n")
2139 },
2140 6,
2141 ));
2142
2143 sections.push(Section::new(
2146 "PIPELINE",
2147 if atlas.pipeline.is_empty() {
2148 "(none)".into()
2149 } else {
2150 atlas.pipeline.join("\n")
2151 },
2152 6,
2153 ));
2154
2155 sections.push(Section::new(
2158 "LANDMARKS",
2159 if atlas.landmarks.is_empty() {
2160 "(none)".into()
2161 } else {
2162 let mut lines = atlas.landmarks.clone();
2163 lines.sort();
2164 lines.join("\n")
2165 },
2166 5,
2167 ));
2168
2169 let mut inv = String::new();
2171 for i in &atlas.invariants {
2172 inv.push_str(&format!(
2173 "- [{}] {}\n",
2174 severity_str(i.severity),
2175 i.statement
2176 ));
2177 }
2178 sections.push(Section::new("CRITICAL INVARIANTS", inv, 10));
2179
2180 let mut failure = String::new();
2182 for c in &atlas.components {
2183 for fb in &c.failure_behavior {
2184 failure.push_str(&format!("{}: {}\n", c.name, fb));
2185 }
2186 }
2187 sections.push(Section::new("FAILURE / RETRY", failure, 9));
2188
2189 sections.push(Section::new(
2191 "DEPLOYMENT",
2192 if atlas.deployment_units.is_empty() {
2193 "(none)".into()
2194 } else {
2195 atlas.deployment_units.join("\n")
2196 },
2197 7,
2198 ));
2199
2200 sections.push(Section::new(
2202 "TRUST BOUNDARIES",
2203 if atlas.trust_boundaries.is_empty() {
2204 "(none)".into()
2205 } else {
2206 atlas.trust_boundaries.join("\n")
2207 },
2208 7,
2209 ));
2210
2211 sections.push(Section::new(
2213 "ASYNC BOUNDARIES",
2214 if atlas.async_boundaries.is_empty() {
2215 "(none)".into()
2216 } else {
2217 atlas.async_boundaries.join("\n")
2218 },
2219 7,
2220 ));
2221
2222 sections.push(Section::new(
2224 "EXTERNAL SYSTEMS",
2225 if atlas.external_systems.is_empty() {
2226 "(none)".into()
2227 } else {
2228 atlas.external_systems.join("\n")
2229 },
2230 7,
2231 ));
2232
2233 let mut impl_map = String::new();
2235 for (name, paths) in &atlas.implementation_map {
2236 if !paths.is_empty() {
2237 impl_map.push_str(&format!("{}: {}\n", name, paths.join(", ")));
2238 }
2239 }
2240 sections.push(Section::new("IMPLEMENTATION MAP", impl_map, 6));
2241
2242 let ev: Vec<String> = atlas
2244 .evidence_summary
2245 .iter()
2246 .map(|(k, v)| format!("{v} {k}"))
2247 .collect();
2248 sections.push(Section::new(
2249 "EVIDENCE STATUS",
2250 if ev.is_empty() {
2251 "(none)".into()
2252 } else {
2253 ev.join(", ")
2254 },
2255 8,
2256 ));
2257
2258 let mut runtime = String::new();
2262 let sigs = ctx.store.trace_signatures().unwrap_or_default();
2263 if !sigs.is_empty() {
2264 runtime.push_str("OBSERVED PATH\n");
2265 for (signature, count, latency_ms, errors, _last) in sigs.into_iter().take(10) {
2267 runtime.push_str(&format!(
2268 "{signature} ({count} reqs, avg {latency_ms:.1} ms, {errors} err)\n"
2269 ));
2270 }
2271 }
2272 const THREE_WAY_KINDS: [&str; 3] = [
2273 "undeclared_observed",
2274 "declared_unobserved",
2275 "static_unobserved",
2276 ];
2277 for (_, kind, _sev, msg, _) in ctx.store.drift_findings(true).unwrap_or_default() {
2278 if !THREE_WAY_KINDS.contains(&kind.as_str()) {
2279 continue;
2280 }
2281 let label = match kind.as_str() {
2282 "undeclared_observed" => "undeclared observed",
2283 "declared_unobserved" => "declared unobserved",
2284 "static_unobserved" => "static unobserved",
2285 _ => kind.as_str(),
2286 };
2287 runtime.push_str(&format!("DRIFT {label}: {msg}\n"));
2288 }
2289 if runtime.is_empty() {
2290 runtime.push_str("(none)\n");
2291 }
2292 sections.push(Section::new("RUNTIME", runtime, 8));
2293
2294 let mut coverage = String::new();
2298 for (k, v) in &atlas.coverage {
2299 coverage.push_str(&format!("{k}: {v}\n"));
2300 }
2301 sections.push(Section::new(
2302 "MODEL COVERAGE",
2303 if coverage.is_empty() {
2304 "(none)".into()
2305 } else {
2306 coverage
2307 },
2308 7,
2309 ));
2310
2311 let warnings = atlas.warnings.clone();
2312 if full {
2313 finish_soft(&mut pack, sections, budget, warnings);
2314 } else {
2315 finish(&mut pack, sections, budget, warnings);
2316 }
2317 pack.entity_ids = comp_ids(ctx);
2318 pack
2319}
2320
2321fn project_flow_graph(
2328 view: &TrustedGraphView,
2329 g: &scc_core::FlowGraph,
2330 async_boundaries: &mut BTreeSet<String>,
2331) -> Vec<String> {
2332 let policy = view.policy();
2333 let edge_ok = |e: &scc_core::FlowEdge| -> bool {
2334 match e.provenance {
2335 None => true,
2336 Some(p) => policy.allows(p, e.confidence),
2337 }
2338 };
2339 let mut lines: Vec<String> = Vec::new();
2340 let mut visited: std::collections::BTreeSet<u32> = std::collections::BTreeSet::new();
2341 let mut queue: std::collections::VecDeque<u32> = g.entrypoints.iter().copied().collect();
2342 while let Some(n) = queue.pop_front() {
2343 if !visited.insert(n) {
2344 continue;
2345 }
2346 let Some(node) = g.nodes.get(n as usize) else {
2347 continue;
2348 };
2349 lines.push(format!(
2350 "{}: {}",
2351 entity_name(view, &node.actor),
2352 node.operation
2353 ));
2354 let mut outs: Vec<&scc_core::FlowEdge> = g
2355 .edges
2356 .iter()
2357 .filter(|e| e.from == n && edge_ok(e))
2358 .collect();
2359 outs.sort_by(|a, b| {
2360 edge_rank(a.kind)
2361 .cmp(&edge_rank(b.kind))
2362 .then(a.to.cmp(&b.to))
2363 });
2364 for e in outs {
2365 let Some(target) = g.nodes.get(e.to as usize) else {
2366 continue;
2367 };
2368 let mut line = format!(
2369 " -> {}: {}",
2370 entity_name(view, &target.actor),
2371 target.operation
2372 );
2373 match e.kind {
2374 scc_core::FlowEdgeKind::Branch => line.push_str(" (branch)"),
2375 scc_core::FlowEdgeKind::Retry => line.push_str(" [retry]"),
2376 scc_core::FlowEdgeKind::Error => line.push_str(" [error]"),
2377 scc_core::FlowEdgeKind::Async => line.push_str(" [async]"),
2378 scc_core::FlowEdgeKind::Publish => line.push_str(" [publish]"),
2379 scc_core::FlowEdgeKind::Consume => line.push_str(" [consume]"),
2380 scc_core::FlowEdgeKind::Join => line.push_str(" (join)"),
2381 scc_core::FlowEdgeKind::Fallback => line.push_str(" [fallback]"),
2382 scc_core::FlowEdgeKind::Timeout => line.push_str(" [timeout]"),
2383 scc_core::FlowEdgeKind::Compensation => line.push_str(" [compensate]"),
2384 scc_core::FlowEdgeKind::Read => line.push_str(" [read]"),
2385 scc_core::FlowEdgeKind::Write => line.push_str(" [write]"),
2386 scc_core::FlowEdgeKind::Transform => line.push_str(" [transform]"),
2387 scc_core::FlowEdgeKind::Validate => line.push_str(" [validate]"),
2388 scc_core::FlowEdgeKind::Authorize => line.push_str(" [authorize]"),
2389 scc_core::FlowEdgeKind::Cache => line.push_str(" [cache]"),
2390 scc_core::FlowEdgeKind::Invalidate => line.push_str(" [invalidate]"),
2391 _ => {}
2392 }
2393 if let Some(c) = &e.condition {
2394 line.push_str(&format!(" ({c})"));
2395 }
2396 lines.push(line);
2397 if e.kind == scc_core::FlowEdgeKind::Async {
2398 async_boundaries.insert(format!(
2399 "{} --async--> {}",
2400 entity_name(view, &node.actor),
2401 entity_name(view, &target.actor)
2402 ));
2403 }
2404 queue.push_back(e.to);
2405 }
2406 }
2407 lines
2408}
2409
2410fn edge_rank(k: scc_core::FlowEdgeKind) -> u8 {
2411 match k {
2412 scc_core::FlowEdgeKind::Next => 0,
2413 scc_core::FlowEdgeKind::Async => 1,
2414 scc_core::FlowEdgeKind::Branch => 2,
2415 scc_core::FlowEdgeKind::Join => 3,
2416 scc_core::FlowEdgeKind::Retry => 4,
2417 scc_core::FlowEdgeKind::Fallback => 5,
2418 scc_core::FlowEdgeKind::Error => 6,
2419 scc_core::FlowEdgeKind::Publish => 7,
2420 scc_core::FlowEdgeKind::Consume => 8,
2421 scc_core::FlowEdgeKind::Return => 9,
2422 scc_core::FlowEdgeKind::Timeout => 10,
2423 scc_core::FlowEdgeKind::Compensation => 11,
2424 scc_core::FlowEdgeKind::Read => 12,
2425 scc_core::FlowEdgeKind::Write => 13,
2426 scc_core::FlowEdgeKind::Transform => 14,
2427 scc_core::FlowEdgeKind::Validate => 15,
2428 scc_core::FlowEdgeKind::Authorize => 16,
2429 scc_core::FlowEdgeKind::Cache => 17,
2430 scc_core::FlowEdgeKind::Invalidate => 18,
2431 }
2432}
2433
2434fn comp_ids(ctx: &ContextCompiler) -> Vec<String> {
2436 ctx.view
2437 .components()
2438 .into_iter()
2439 .map(|c| c.id.clone())
2440 .collect()
2441}
2442
2443fn flow_kind_str(k: FlowKind) -> &'static str {
2444 match k {
2445 FlowKind::Architecture => "architecture",
2446 FlowKind::Workflow => "workflow",
2447 FlowKind::Sequence => "sequence",
2448 FlowKind::Dataflow => "dataflow",
2449 FlowKind::Lifecycle => "lifecycle",
2450 }
2451}
2452
2453fn severity_str(s: scc_core::Severity) -> &'static str {
2454 match s {
2455 scc_core::Severity::Info => "INFO",
2456 scc_core::Severity::Low => "LOW",
2457 scc_core::Severity::Medium => "MEDIUM",
2458 scc_core::Severity::High => "HIGH",
2459 scc_core::Severity::Critical => "CRITICAL",
2460 }
2461}
2462
2463#[cfg(test)]
2464mod tests {
2465 use super::*;
2466 use scc_core::{
2467 entity_id, kinds, predicates, relationship_id, symbol_id, Entity, Provenance, Relationship,
2468 };
2469 use scc_store::Store;
2470
2471 fn test_store() -> (tempfile::TempDir, Store) {
2473 let dir = tempfile::TempDir::new().unwrap();
2474 let root = dir.path().join("repo");
2475 std::fs::create_dir_all(&root).unwrap();
2476 let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
2477 let repo = "repo";
2478
2479 store
2481 .upsert_file("app.py", "h1", "python", "source", 10)
2482 .unwrap();
2483 store
2484 .upsert_file("lib.py", "h2", "python", "source", 10)
2485 .unwrap();
2486 store
2487 .upsert_file("config.json", "h3", "json", "config", 10)
2488 .unwrap();
2489
2490 let mk = |n: &str| symbol_id(repo, "app.py", n);
2492 for n in ["handler", "worker", "reader"] {
2493 let mut e = Entity::new(mk(n), kinds::SYMBOL, n);
2494 e.attr("file", serde_json::json!("app.py"));
2495 store.insert_entity(&e, &["app.py".to_string()]).unwrap();
2496 }
2497 let mut w = store.get_entity(&mk("worker")).unwrap().unwrap();
2499 w.attributes.insert(
2500 "cli_flags".into(),
2501 serde_json::json!(["--queue", "--verbose"]),
2502 );
2503 store.insert_entity(&w, &["app.py".to_string()]).unwrap();
2504
2505 let route_id = entity_id(repo, kinds::ROUTE, "GET /api/x");
2507 let mut re = Entity::new(route_id.clone(), kinds::ROUTE, "GET /api/x");
2508 re.attr("method", serde_json::json!("GET"));
2509 re.attr("path", serde_json::json!("/api/x"));
2510 re.attr("handler", serde_json::json!(mk("handler")));
2511 store.insert_entity(&re, &["app.py".to_string()]).unwrap();
2512 store
2513 .insert_relationship(
2514 &Relationship::new(
2515 relationship_id(1),
2516 mk("handler"),
2517 predicates::HANDLES,
2518 route_id,
2519 Provenance::Extracted,
2520 ),
2521 "app.py",
2522 )
2523 .unwrap();
2524
2525 let exp_id = entity_id(repo, kinds::EXPORT, "handler");
2527 let mut ex = Entity::new(exp_id.clone(), kinds::EXPORT, "handler");
2528 ex.attr("kind", serde_json::json!("function"));
2529 store.insert_entity(&ex, &["app.py".to_string()]).unwrap();
2530 store
2531 .insert_relationship(
2532 &Relationship::new(
2533 relationship_id(2),
2534 mk("handler"),
2535 predicates::EXPORTS,
2536 exp_id,
2537 Provenance::Extracted,
2538 ),
2539 "app.py",
2540 )
2541 .unwrap();
2542
2543 let cfg_id = entity_id(repo, kinds::CONFIGURATION, "DEBUG");
2545 store
2546 .insert_entity(
2547 &Entity::new(cfg_id.clone(), kinds::CONFIGURATION, "DEBUG"),
2548 &["app.py".to_string()],
2549 )
2550 .unwrap();
2551 store
2552 .insert_relationship(
2553 &Relationship::new(
2554 relationship_id(3),
2555 cfg_id,
2556 predicates::CONFIGURED_BY,
2557 mk("reader"),
2558 Provenance::Extracted,
2559 ),
2560 "app.py",
2561 )
2562 .unwrap();
2563
2564 store
2567 .insert_relationship(
2568 &Relationship::new(
2569 relationship_id(4),
2570 mk("handler"),
2571 predicates::CALLS,
2572 mk("worker"),
2573 Provenance::Extracted,
2574 ),
2575 "app.py",
2576 )
2577 .unwrap();
2578 let ext_id = entity_id(repo, kinds::EXTERNAL_API, "os");
2579 store
2580 .insert_relationship(
2581 &Relationship::new(
2582 relationship_id(5),
2583 mk("handler"),
2584 predicates::CALLS,
2585 ext_id,
2586 Provenance::Extracted,
2587 ),
2588 "app.py",
2589 )
2590 .unwrap();
2591 store
2592 .insert_relationship(
2593 &Relationship::new(
2594 relationship_id(6),
2595 mk("worker"),
2596 predicates::CALLS,
2597 mk("reader"),
2598 Provenance::Resolved,
2599 ),
2600 "app.py",
2601 )
2602 .unwrap();
2603
2604 let topic_id = entity_id(repo, kinds::TOPIC, "jobs");
2606 store
2607 .insert_entity(
2608 &Entity::new(topic_id.clone(), kinds::TOPIC, "jobs"),
2609 &["app.py".to_string()],
2610 )
2611 .unwrap();
2612 store
2613 .insert_relationship(
2614 &Relationship::new(
2615 relationship_id(7),
2616 mk("worker"),
2617 predicates::SUBSCRIBES,
2618 topic_id,
2619 Provenance::Extracted,
2620 ),
2621 "app.py",
2622 )
2623 .unwrap();
2624
2625 let _graph = scc_graph::RealityGraph::load(&store).unwrap();
2626 (dir, store)
2627 }
2628
2629 #[test]
2630 fn kinds_stringify() {
2631 assert_eq!(flow_kind_str(FlowKind::Sequence), "sequence");
2632 assert_eq!(severity_str(scc_core::Severity::Critical), "CRITICAL");
2633 }
2634
2635 #[test]
2636 fn atlas_production_scope_keeps_fixture_routes_out() {
2638 let (_dir, store) = fact_layer_store();
2642 let repo = store.repo_id.clone();
2643 let mk_route = |store: &Store, method: &str, path: &str, file: &str| {
2644 let name = format!("{method} {path}");
2645 let mut e = Entity::new(entity_id(&repo, kinds::ROUTE, &name), kinds::ROUTE, &name);
2646 e.attr("method", serde_json::json!(method));
2647 e.attr("path", serde_json::json!(path));
2648 e.attr("handler", serde_json::json!("handler"));
2649 e.attr("file", serde_json::json!(file));
2650 store.insert_entity(&e, &[file.to_string()]).unwrap();
2651 };
2652 mk_route(&store, "GET", "/api/ok", "api/app.py");
2653 mk_route(&store, "GET", "/admin", "fixtures/foo/app.py");
2654 let mut fx_comp = scc_core::Entity::new(
2655 entity_id(&repo, kinds::COMPONENT, "fx"),
2656 kinds::COMPONENT,
2657 "fx",
2658 );
2659 fx_comp.attr(
2660 "implementation",
2661 serde_json::json!({"paths": ["fixtures/foo"], "symbols": []}),
2662 );
2663 let mut api_comp = scc_core::Entity::new(
2666 entity_id(&repo, kinds::COMPONENT, "api"),
2667 kinds::COMPONENT,
2668 "api",
2669 );
2670 api_comp.attr(
2671 "implementation",
2672 serde_json::json!({"paths": ["api"], "symbols": []}),
2673 );
2674 let mut web_comp = scc_core::Entity::new(
2675 entity_id(&repo, kinds::COMPONENT, "web"),
2676 kinds::COMPONENT,
2677 "web",
2678 );
2679 web_comp.attr(
2680 "implementation",
2681 serde_json::json!({"paths": ["web"], "symbols": []}),
2682 );
2683 store.replace_components(&[api_comp, web_comp, fx_comp]).unwrap();
2684
2685 let graph = scc_graph::RealityGraph::load(&store).unwrap();
2686 let ctx = ContextCompiler::new(&store, &graph, crate::ContextSettings::default(), Vec::new());
2687 let atlas = build_atlas(&ctx);
2688 assert!(
2689 atlas.entrypoints.iter().any(|e| e.trigger == "GET /api/ok"),
2690 "production route listed: {:?}",
2691 atlas.entrypoints
2692 );
2693 assert!(
2694 !atlas.entrypoints.iter().any(|e| e.trigger == "GET /admin"),
2695 "fixture route must not be a global entrypoint: {:?}",
2696 atlas.entrypoints
2697 );
2698 assert!(
2699 !atlas.contracts.iter().any(|c| c
2700 .operations
2701 .iter()
2702 .any(|o| o == "GET /admin")),
2703 "fixture route must not be a contract: {:?}",
2704 atlas.contracts
2705 );
2706 assert!(
2707 atlas.components.iter().any(|c| c.name == "fx" && c.role == "fixture"),
2708 "fixture component stays visible, labeled: {:?}",
2709 atlas.components.iter().map(|c| (&c.name, &c.role)).collect::<Vec<_>>()
2710 );
2711 assert!(
2712 atlas.coverage.get("scope").map(|s| s.contains("production")).unwrap_or(false),
2713 "scope honesty receipt: {:?}",
2714 atlas.coverage.get("scope")
2715 );
2716 let full = build_atlas_scoped(&ctx, AtlasScope::Full);
2717 assert!(
2718 full.entrypoints.iter().any(|e| e.trigger == "GET /admin"),
2719 "full scope restores the fixture route: {:?}",
2720 full.entrypoints
2721 );
2722 }
2723
2724 #[test]
2725 fn contracts_and_coverage_from_fact_layer() {
2727 let (_dir, store) = test_store();
2728 let graph = scc_graph::RealityGraph::load(&store).unwrap();
2729 let ctx = ContextCompiler::new(
2730 &store,
2731 &graph,
2732 crate::ContextSettings::default(),
2733 Vec::new(),
2734 );
2735 let atlas = build_atlas(&ctx);
2736
2737 let kinds_found: BTreeSet<String> =
2741 atlas.contracts.iter().map(|c| c.kind.clone()).collect();
2742 assert_eq!(
2743 kinds_found,
2744 BTreeSet::from([
2745 "http".to_string(),
2746 "cli".to_string(),
2747 "config".to_string(),
2748 "event".to_string(),
2749 "public-api".to_string()
2750 ]),
2751 "contract kinds: {:?}",
2752 atlas.contracts
2753 );
2754 for c in &atlas.contracts {
2757 assert_eq!(
2758 c.kind,
2759 c.subclass.as_str(),
2760 "kind agrees with subclass: {c:?}"
2761 );
2762 }
2763 let http = atlas.contracts.iter().find(|c| c.kind == "http").unwrap();
2764 assert_eq!(http.operations, vec!["GET /api/x"]);
2765 assert_eq!(http.subclass, scc_core::ContractSubclass::Http);
2766 assert!(
2767 http.consumers.iter().any(|c| c == "handler"),
2768 "handler consumes the route: {:?}",
2769 http.consumers
2770 );
2771 let cli = atlas.contracts.iter().find(|c| c.kind == "cli").unwrap();
2772 assert_eq!(cli.operations, vec!["--queue", "--verbose"]);
2773 assert_eq!(cli.subclass, scc_core::ContractSubclass::Cli);
2774 let cfg = atlas.contracts.iter().find(|c| c.kind == "config").unwrap();
2775 assert_eq!(cfg.operations, vec!["DEBUG"]);
2776 assert_eq!(cfg.subclass, scc_core::ContractSubclass::Configuration);
2777 assert!(
2778 cfg.consumers.iter().any(|c| c == "reader"),
2779 "reader consumes DEBUG: {:?}",
2780 cfg.consumers
2781 );
2782 let api = atlas
2783 .contracts
2784 .iter()
2785 .find(|c| c.kind == "public-api")
2786 .unwrap();
2787 assert_eq!(api.operations, vec!["handler"]);
2788 assert_eq!(api.subclass, scc_core::ContractSubclass::PublicApi);
2789
2790 let lines: Vec<String> = atlas
2792 .contracts
2793 .iter()
2794 .flat_map(|c| {
2795 c.operations
2796 .iter()
2797 .map(|op| format!("{}: {}", c.subclass.as_str(), op))
2798 })
2799 .collect();
2800 for want in [
2801 "http: GET /api/x",
2802 "cli: --queue",
2803 "config: DEBUG",
2804 "event: jobs",
2805 "public-api: handler",
2806 ] {
2807 assert!(
2808 lines.contains(&want.to_string()),
2809 "missing {want}: {lines:?}"
2810 );
2811 }
2812
2813 assert_eq!(
2815 atlas.coverage.get("parsed_source_files").unwrap(),
2816 "66% (2/3)"
2817 );
2818 assert_eq!(
2819 atlas.coverage.get("unparsed_files").unwrap(),
2820 "1 (config/docs/infra — scanned but not source-parsed)"
2821 );
2822 assert!(
2823 atlas
2824 .coverage
2825 .get("exported_api")
2826 .unwrap()
2827 .starts_with("1 export entity"),
2828 "{:?}",
2829 atlas.coverage.get("exported_api")
2830 );
2831 assert_eq!(
2834 atlas.coverage.get("call_targets_resolved").unwrap(),
2835 "66% (2/3, 1 LSP-RESOLVED) — exploration still justified in unresolved regions"
2836 );
2837 assert_eq!(
2838 atlas.coverage.get("dynamic_receivers_unresolved").unwrap(),
2839 "1 (calls whose target is not a local symbol; unknown-receiver calls are not persisted)"
2840 );
2841 assert_eq!(
2844 atlas.coverage.get("invocation_surfaces").unwrap(),
2845 "4 (cli 1, http 1, public_api 1, queue 1)",
2846 "{:?}",
2847 atlas.coverage.get("invocation_surfaces")
2848 );
2849 assert_eq!(
2850 atlas.coverage.get("stale_evidence").unwrap(),
2851 "0 (model FRESH)"
2852 );
2853 assert!(atlas.coverage.contains_key("model_epoch_generations"));
2854 assert!(atlas
2855 .coverage
2856 .contains_key("framework_registrations_unknown"));
2857
2858 let ep_kinds: BTreeSet<&str> = atlas.entrypoints.iter().map(|e| e.kind.as_str()).collect();
2860 assert!(ep_kinds.contains("public_api"), "{ep_kinds:?}");
2861 assert!(ep_kinds.contains("queue"), "{ep_kinds:?}");
2862 assert!(ep_kinds.contains("http"), "http surface: {ep_kinds:?}");
2863 assert!(ep_kinds.contains("cli"), "cli surface: {ep_kinds:?}");
2864 }
2865
2866 #[test]
2874
2875 fn schema_and_reactive_render_in_atlas() {
2877 let dir = tempfile::TempDir::new().unwrap();
2878 let root = dir.path().join("repo");
2879 std::fs::create_dir_all(&root).unwrap();
2880 let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
2881 let repo = store.repo_id.clone();
2882
2883 let comp_id = entity_id(&repo, kinds::COMPONENT, "api");
2887 store
2888 .replace_components(&[scc_core::Entity::new(
2889 comp_id.clone(),
2890 kinds::COMPONENT,
2891 "api",
2892 )])
2893 .unwrap();
2894 let fid = entity_id(&repo, kinds::FILE, "api/app.py");
2895 store
2896 .insert_entity(
2897 &Entity::new(fid.clone(), kinds::FILE, "api/app.py"),
2898 &["api/app.py".to_string()],
2899 )
2900 .unwrap();
2901 store
2902 .insert_relationship(
2903 &Relationship::new(
2904 "rel:c:api",
2905 comp_id,
2906 predicates::CONTAINS,
2907 fid.clone(),
2908 Provenance::Extracted,
2909 ),
2910 "api/app.py",
2911 )
2912 .unwrap();
2913 let svc = symbol_id(&repo, "api/app.py", "UserService");
2914 store
2915 .insert_entity(
2916 &Entity::new(svc.clone(), kinds::SYMBOL, "UserService"),
2917 &["api/app.py".to_string()],
2918 )
2919 .unwrap();
2920 store
2921 .insert_relationship(
2922 &Relationship::new(
2923 "rel:f:svc",
2924 fid,
2925 predicates::CONTAINS,
2926 svc.clone(),
2927 Provenance::Extracted,
2928 ),
2929 "api/app.py",
2930 )
2931 .unwrap();
2932
2933 let base = entity_id(&repo, kinds::SCHEMA, "Base");
2936 store
2937 .insert_entity(
2938 &Entity::new(base.clone(), kinds::SCHEMA, "Base"),
2939 &["api/app.py".to_string()],
2940 )
2941 .unwrap();
2942 let user = entity_id(&repo, kinds::SCHEMA, "User");
2943 store
2944 .insert_entity(
2945 &Entity::new(user.clone(), kinds::SCHEMA, "User"),
2946 &["api/app.py".to_string()],
2947 )
2948 .unwrap();
2949 let user_occ = scc_core::occurrence_id(&repo, "User", "api/app.py", "User", 1);
2950 store
2951 .insert_entity(
2952 Entity::new(
2953 user_occ.clone(),
2954 scc_core::kinds::OCCURRENCE,
2955 "User@api/app.py@User@1",
2956 )
2957 .attr("concept", serde_json::json!(user))
2958 .attr("path", serde_json::json!("api/app.py"))
2959 .attr("owner", serde_json::json!("User"))
2960 .attr("line", serde_json::json!(1)),
2961 &["api/app.py".to_string()],
2962 )
2963 .unwrap();
2964 store
2965 .insert_relationship(
2966 &Relationship::new(
2967 "rel:occ:user",
2968 user_occ,
2969 scc_core::predicates::OCCURS,
2970 user.clone(),
2971 Provenance::Extracted,
2972 ),
2973 "api/app.py",
2974 )
2975 .unwrap();
2976 store
2977 .insert_relationship(
2978 &Relationship::new(
2979 "rel:defines",
2980 svc.clone(),
2981 predicates::DEFINES,
2982 user.clone(),
2983 Provenance::Extracted,
2984 ),
2985 "api/app.py",
2986 )
2987 .unwrap();
2988 store
2989 .insert_relationship(
2990 &Relationship::new(
2991 "rel:composes",
2992 user,
2993 predicates::COMPOSES,
2994 base,
2995 Provenance::Extracted,
2996 ),
2997 "api/app.py",
2998 )
2999 .unwrap();
3000 let target = entity_id(&repo, kinds::SYMBOL, "CreateUser");
3001 store
3002 .insert_relationship(
3003 &Relationship::new(
3004 "rel:validates",
3005 svc.clone(),
3006 predicates::VALIDATES,
3007 target,
3008 Provenance::Extracted,
3009 ),
3010 "api/app.py",
3011 )
3012 .unwrap();
3013
3014 let count = entity_id(&repo, kinds::REACTIVE, "count");
3017 store
3018 .insert_entity(
3019 &Entity::new(count.clone(), kinds::REACTIVE, "count"),
3020 &["api/app.py".to_string()],
3021 )
3022 .unwrap();
3023 let count_occ = scc_core::occurrence_id(&repo, "count", "api/app.py", "UserService", 1);
3024 store
3025 .insert_entity(
3026 Entity::new(
3027 count_occ.clone(),
3028 scc_core::kinds::OCCURRENCE,
3029 "count@api/app.py@UserService@1",
3030 )
3031 .attr("concept", serde_json::json!(count))
3032 .attr("path", serde_json::json!("api/app.py"))
3033 .attr("owner", serde_json::json!("UserService"))
3034 .attr("line", serde_json::json!(1))
3035 .attr("access", serde_json::json!("state")),
3036 &["api/app.py".to_string()],
3037 )
3038 .unwrap();
3039 store
3040 .insert_relationship(
3041 &Relationship::new(
3042 "rel:occ:count",
3043 count_occ.clone(),
3044 scc_core::predicates::OCCURS,
3045 count.clone(),
3046 Provenance::Extracted,
3047 ),
3048 "api/app.py",
3049 )
3050 .unwrap();
3051 store
3052 .insert_relationship(
3053 &Relationship::new(
3054 "rel:owns:count",
3055 svc,
3056 predicates::OWNS,
3057 count_occ,
3058 Provenance::Extracted,
3059 ),
3060 "api/app.py",
3061 )
3062 .unwrap();
3063
3064 let graph = scc_graph::RealityGraph::load(&store).unwrap();
3065 let ctx = ContextCompiler::new(
3066 &store,
3067 &graph,
3068 crate::ContextSettings::default(),
3069 Vec::new(),
3070 );
3071 let atlas = build_atlas(&ctx);
3072
3073 let schema = atlas
3077 .contracts
3078 .iter()
3079 .find(|c| {
3080 c.subclass == scc_core::ContractSubclass::Schema
3081 && c.operations.first() == Some(&"User".to_string())
3082 })
3083 .expect("schema contract for User");
3084 assert_eq!(schema.kind, "schema");
3085 assert_eq!(
3086 schema.operations,
3087 vec![
3088 "User".to_string(),
3089 "User extends Base".to_string(),
3090 "User validates".to_string()
3091 ],
3092 "{schema:?}"
3093 );
3094 assert_eq!(
3097 schema.producer, "User",
3098 "producer = owner symbol: {schema:?}"
3099 );
3100
3101 assert!(
3103 atlas
3104 .state_authority
3105 .get(scc_graph::state::S_REACTIVE)
3106 .map(|lines| lines
3107 .iter()
3108 .any(|l| l == "api owns reactive: count [state] (EXTRACTED)"))
3109 .unwrap_or(false),
3110 "{:?}",
3111 atlas.state_authority
3112 );
3113
3114 let pack = render_atlas(&ctx, &atlas, usize::MAX, false);
3116 for want in [
3117 "schema: User",
3118 "schema: User extends Base",
3119 "schema: User validates",
3120 "REACTIVE STATE",
3121 "api owns reactive: count [state] (EXTRACTED)",
3122 ] {
3123 assert!(
3124 pack.content.contains(want),
3125 "missing {want:?} in:\n{}",
3126 pack.content
3127 );
3128 }
3129 }
3130
3131 fn fact_layer_store() -> (tempfile::TempDir, Store) {
3136 let dir = tempfile::TempDir::new().unwrap();
3137 let root = dir.path().join("repo");
3138 std::fs::create_dir_all(&root).unwrap();
3139 let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
3140 let repo = store.repo_id.clone();
3141
3142 let mk_comp = |store: &Store, name: &str, file: &str| -> String {
3144 let id = entity_id(&repo, kinds::COMPONENT, name);
3145 store
3146 .insert_entity(
3147 &scc_core::Entity::new(id.clone(), kinds::COMPONENT, name),
3148 &[file.to_string()],
3149 )
3150 .unwrap();
3151 let fid = entity_id(&repo, kinds::FILE, file);
3152 store
3153 .insert_relationship(
3154 &Relationship::new(
3155 format!("rel:c:{name}"),
3156 id.clone(),
3157 predicates::CONTAINS,
3158 fid,
3159 Provenance::Extracted,
3160 ),
3161 file,
3162 )
3163 .unwrap();
3164 id
3165 };
3166 mk_comp(&store, "api", "api/app.py");
3167 mk_comp(&store, "web", "web/app.py");
3168 let mut api_comp = scc_core::Entity::new(
3173 entity_id(&repo, kinds::COMPONENT, "api"),
3174 kinds::COMPONENT,
3175 "api",
3176 );
3177 api_comp.attr(
3178 "implementation",
3179 serde_json::json!({
3180 "paths": ["api"],
3181 "symbols": ["App", "App.get", "include_router", "_secret"],
3182 }),
3183 );
3184 let mut web_comp = scc_core::Entity::new(
3185 entity_id(&repo, kinds::COMPONENT, "web"),
3186 kinds::COMPONENT,
3187 "web",
3188 );
3189 web_comp.attr(
3190 "implementation",
3191 serde_json::json!({
3192 "paths": ["web"],
3193 "symbols": ["handle_page", "on_message"],
3194 }),
3195 );
3196 store.replace_components(&[api_comp, web_comp]).unwrap();
3197 store
3198 .insert_entity(
3199 &Entity::new(
3200 entity_id(&repo, kinds::FILE, "api/app.py"),
3201 kinds::FILE,
3202 "api/app.py",
3203 ),
3204 &["api/app.py".into()],
3205 )
3206 .unwrap();
3207 store
3208 .insert_entity(
3209 &Entity::new(
3210 entity_id(&repo, kinds::FILE, "web/app.py"),
3211 kinds::FILE,
3212 "web/app.py",
3213 ),
3214 &["web/app.py".into()],
3215 )
3216 .unwrap();
3217
3218 let mk_sym = |path: &str, name: &str, attrs: serde_json::Value| -> String {
3219 let id = symbol_id(&repo, path, name);
3220 let mut e = Entity::new(id.clone(), kinds::SYMBOL, name);
3221 if let Some(obj) = attrs.as_object() {
3222 for (k, v) in obj {
3223 e.attr(k, v.clone());
3224 }
3225 }
3226 store.insert_entity(&e, &[path.to_string()]).unwrap();
3227 let fid = entity_id(&repo, kinds::FILE, path);
3228 store
3229 .insert_relationship(
3230 &Relationship::new(
3231 format!("rel:f:{path}:{name}"),
3232 fid,
3233 predicates::CONTAINS,
3234 id.clone(),
3235 Provenance::Extracted,
3236 ),
3237 path,
3238 )
3239 .unwrap();
3240 id
3241 };
3242
3243 let app_id = mk_sym(
3245 "api/app.py",
3246 "App",
3247 serde_json::json!({"kind": "class", "exported": true}),
3248 );
3249 let app_get = mk_sym(
3250 "api/app.py",
3251 "App.get",
3252 serde_json::json!({"kind": "method", "parent": "App", "exported": false}),
3253 );
3254 mk_sym(
3256 "api/app.py",
3257 "include_router",
3258 serde_json::json!({"kind": "function", "exported": true}),
3259 );
3260 mk_sym(
3261 "api/app.py",
3262 "_secret",
3263 serde_json::json!({"kind": "function", "exported": true}),
3264 );
3265 let web_handle = mk_sym(
3267 "web/app.py",
3268 "handle_page",
3269 serde_json::json!({"kind": "function", "exported": true}),
3270 );
3271
3272 let exp_id = entity_id(&repo, kinds::EXPORT, "include_router");
3274 store
3275 .insert_entity(
3276 &Entity::new(exp_id.clone(), kinds::EXPORT, "include_router"),
3277 &["api/app.py".into()],
3278 )
3279 .unwrap();
3280 let include_id = symbol_id(&repo, "api/app.py", "include_router");
3281 store
3282 .insert_relationship(
3283 &Relationship::new(
3284 relationship_id(100),
3285 include_id,
3286 predicates::EXPORTS,
3287 exp_id,
3288 Provenance::Extracted,
3289 ),
3290 "api/app.py",
3291 )
3292 .unwrap();
3293
3294 let ann_id = entity_id(&repo, kinds::ANNOTATION, "Get");
3296 store
3297 .insert_entity(
3298 &Entity::new(ann_id.clone(), kinds::ANNOTATION, "Get"),
3299 &["api/app.py".into()],
3300 )
3301 .unwrap();
3302 store
3303 .insert_relationship(
3304 &Relationship::new(
3305 relationship_id(101),
3306 ann_id,
3307 predicates::ANNOTATES,
3308 app_get.clone(),
3309 Provenance::Extracted,
3310 ),
3311 "api/app.py",
3312 )
3313 .unwrap();
3314
3315 let mw_id = entity_id(&repo, kinds::MIDDLEWARE, "RequestLogger");
3317 store
3318 .insert_entity(
3319 &Entity::new(mw_id.clone(), kinds::MIDDLEWARE, "RequestLogger"),
3320 &["api/app.py".into()],
3321 )
3322 .unwrap();
3323 store
3324 .insert_relationship(
3325 &Relationship::new(
3326 relationship_id(102),
3327 app_id.clone(),
3328 predicates::REGISTERS,
3329 mw_id,
3330 Provenance::Extracted,
3331 ),
3332 "api/app.py",
3333 )
3334 .unwrap();
3335
3336 let cb_sym = mk_sym(
3338 "web/app.py",
3339 "on_message",
3340 serde_json::json!({"kind": "function", "exported": false}),
3341 );
3342 store
3343 .insert_relationship(
3344 &Relationship::new(
3345 relationship_id(103),
3346 web_handle.clone(),
3347 predicates::HANDLES_CALLBACK,
3348 cb_sym,
3349 Provenance::Extracted,
3350 ),
3351 "web/app.py",
3352 )
3353 .unwrap();
3354
3355 let field_id = entity_id(&repo, kinds::FIELD, "App.cache");
3357 store
3358 .insert_entity(
3359 Entity::new(field_id.clone(), kinds::FIELD, "App.cache")
3360 .attr("mutable", serde_json::json!(true))
3361 .attr("owner", serde_json::json!("App")),
3362 &["api/app.py".into()],
3363 )
3364 .unwrap();
3365 store
3366 .insert_relationship(
3367 &Relationship::new(
3368 relationship_id(104),
3369 app_id.clone(),
3370 predicates::CONTAINS,
3371 field_id,
3372 Provenance::Extracted,
3373 ),
3374 "api/app.py",
3375 )
3376 .unwrap();
3377 let cfg_id = entity_id(&repo, kinds::CONFIGURATION, "DEBUG");
3378 store
3379 .insert_entity(
3380 &Entity::new(cfg_id.clone(), kinds::CONFIGURATION, "DEBUG"),
3381 &["api/app.py".into()],
3382 )
3383 .unwrap();
3384 store
3385 .insert_relationship(
3386 &Relationship::new(
3387 relationship_id(105),
3388 cfg_id,
3389 predicates::CONFIGURED_BY,
3390 app_id,
3391 Provenance::Extracted,
3392 ),
3393 "api/app.py",
3394 )
3395 .unwrap();
3396
3397 let _graph = scc_graph::RealityGraph::load(&store).unwrap();
3398 (dir, store)
3399 }
3400
3401 #[test]
3402 fn fact_layer_sections_grouped_by_component() {
3404 let (_dir, store) = fact_layer_store();
3405 let graph = scc_graph::RealityGraph::load(&store).unwrap();
3406 let ctx = ContextCompiler::new(
3407 &store,
3408 &graph,
3409 crate::ContextSettings::default(),
3410 Vec::new(),
3411 );
3412 let atlas = build_atlas(&ctx);
3413
3414 let api_exports = atlas.public_api.get("api").expect("api exports");
3418 assert!(api_exports.contains(&"App".to_string()), "{api_exports:?}");
3419 assert!(
3420 api_exports.contains(&"include_router".to_string()),
3421 "{api_exports:?}"
3422 );
3423 assert!(
3424 !api_exports.iter().any(|e| e == "_secret"),
3425 "{api_exports:?}"
3426 );
3427 let web_exports = atlas.public_api.get("web").expect("web exports");
3428 assert!(
3429 web_exports.contains(&"handle_page".to_string()),
3430 "{web_exports:?}"
3431 );
3432
3433 let api_facts = atlas.framework_semantics.get("api").expect("api facts");
3435 assert!(
3436 api_facts
3437 .iter()
3438 .any(|f| f.contains("annotates App.get (Get)")),
3439 "{api_facts:?}"
3440 );
3441 assert!(
3442 api_facts
3443 .iter()
3444 .any(|f| f.contains("registers RequestLogger")),
3445 "{api_facts:?}"
3446 );
3447 let web_facts = atlas.framework_semantics.get("web").expect("web facts");
3448 assert!(
3449 web_facts
3450 .iter()
3451 .any(|f| f.contains("handles callback on_message")),
3452 "{web_facts:?}"
3453 );
3454
3455 let api = atlas.components.iter().find(|c| c.name == "api").unwrap();
3457 assert!(
3458 api.symbols.contains(&"App".to_string()),
3459 "{:?}",
3460 api.symbols
3461 );
3462 assert!(
3463 api.symbols.contains(&"App.get".to_string()),
3464 "{:?}",
3465 api.symbols
3466 );
3467 assert!(
3468 api.implementation.contains(&"App".to_string()),
3469 "{:?}",
3470 api.implementation
3471 );
3472 assert_eq!(api.implementation_paths, vec!["api".to_string()]);
3473 assert_eq!(
3475 atlas.implementation_map.get("api").unwrap(),
3476 &vec!["api".to_string()]
3477 );
3478
3479 let api_owns: Vec<&str> = api.owns.iter().map(|o| o.target.as_str()).collect();
3482 assert!(
3483 api_owns.contains(&"App.cache"),
3484 "mutable field owns claim: {api_owns:?}"
3485 );
3486 assert!(
3487 api_owns.contains(&"DEBUG"),
3488 "config owns claim: {api_owns:?}"
3489 );
3490
3491 assert!(
3493 atlas.landmarks.len() <= 40,
3494 "landmarks bounded: {:?}",
3495 atlas.landmarks.len()
3496 );
3497 assert!(
3498 atlas.landmarks.iter().any(|l| l.contains("App")),
3499 "{:?}",
3500 atlas.landmarks
3501 );
3502
3503 let pack = render_atlas(&ctx, &atlas, usize::MAX, false);
3505 assert!(pack.content.contains("# PUBLIC API"), "{}", pack.content);
3506 assert!(
3507 pack.content.contains("# FRAMEWORK SEMANTICS"),
3508 "{}",
3509 pack.content
3510 );
3511 assert!(pack.content.contains("# LANDMARKS"), "{}", pack.content);
3512 assert!(
3513 pack.content.contains("api: exports App, include_router"),
3514 "{}",
3515 pack.content
3516 );
3517 }
3518
3519 #[test]
3520 fn pipeline_only_for_compiler_language_tool() {
3522 let (_dir, store) = fact_layer_store();
3523 let graph = scc_graph::RealityGraph::load(&store).unwrap();
3524 let ctx = ContextCompiler::new(
3525 &store,
3526 &graph,
3527 crate::ContextSettings::default(),
3528 Vec::new(),
3529 );
3530 let atlas = build_atlas(&ctx);
3531 assert!(atlas.pipeline.is_empty(), "{:?}", atlas.pipeline);
3533
3534 let parse_id = symbol_id(&store.repo_id, "api/app.py", "parse");
3536 let mut e = store
3537 .get_entity(&parse_id)
3538 .ok()
3539 .flatten()
3540 .unwrap_or_else(|| {
3541 let ent = Entity::new(parse_id.clone(), kinds::SYMBOL, "parse");
3542 store.insert_entity(&ent, &["api/app.py".into()]).unwrap();
3543 ent
3544 });
3545 e.attr("exported", serde_json::json!(true));
3546 store.insert_entity(&e, &["api/app.py".into()]).unwrap();
3547 let graph = scc_graph::RealityGraph::load(&store).unwrap();
3548 let ctx = ContextCompiler::new(
3549 &store,
3550 &graph,
3551 crate::ContextSettings::default(),
3552 Vec::new(),
3553 );
3554 let pipeline = build_pipeline(&ctx.view, Some(scc_core::Archetype::CompilerLanguageTool));
3555 assert!(
3556 pipeline.iter().any(|l| l.trim() == "parse"),
3557 "parse symbol in pipeline: {pipeline:?}"
3558 );
3559 assert!(
3560 pipeline.iter().any(|l| l == "[parse]"),
3561 "stage header: {pipeline:?}"
3562 );
3563 }
3564}