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 let mut seen_ep_names: std::collections::BTreeMap<(String, String), usize> = Default::default();
1816 for e in atlas.entrypoints.iter().take(EP_RENDER_CAP) {
1817 match e.kind.as_str() {
1824 "queue" | "schedule" | "plugin" | "lifecycle" => {
1825 let label = if e.kind == "lifecycle" {
1826 e.trigger
1827 .strip_prefix("lifecycle:")
1828 .map(|a| format!("@{a}"))
1829 .unwrap_or_else(|| e.name.clone())
1830 } else {
1831 e.name.clone()
1832 };
1833 purpose.push_str(&format!(" {}: {}\n", e.kind, label));
1834 }
1835 _ => {
1836 let key = (e.name.clone(), e.kind.clone());
1842 let n = seen_ep_names.entry(key).or_insert(0);
1843 *n += 1;
1844 let label = if *n > 1 {
1845 let file = e.symbol.rsplit('/').next().unwrap_or("").split(':').next().unwrap_or("");
1846 format!("{} ({})", e.name, file)
1847 } else {
1848 e.name.clone()
1849 };
1850 if e.trigger == e.name {
1851 purpose.push_str(&format!(" {} [{}]\n", label, e.kind));
1852 } else {
1853 purpose.push_str(&format!(" {} [{}] — {}\n", label, e.kind, e.trigger));
1854 }
1855 }
1856 }
1857 }
1858 if atlas.entrypoints.len() > EP_RENDER_CAP {
1859 purpose.push_str(&format!(
1860 " ... +{} more (full list in the machine model)\n",
1861 atlas.entrypoints.len() - EP_RENDER_CAP
1862 ));
1863 }
1864 }
1865 sections.push(Section::new("SYSTEM PURPOSE", purpose, 10));
1866
1867 let mut arch = String::new();
1872 let mut rendered: BTreeSet<String> = BTreeSet::new(); let comp_block = |c: &AtlasComponent, indent: &str| -> String {
1874 let mut out = format!("\n{}{}", indent, c.name.to_uppercase());
1875 if !c.role.is_empty() && c.role != "production" {
1879 out.push_str(&format!(" [{}]", c.role));
1880 }
1881 if !c.purpose.is_empty() {
1882 out.push_str(&format!("\n{}Purpose: {}", indent, c.purpose));
1883 }
1884 if !c.implementation_paths.is_empty() {
1885 out.push_str(&format!(
1886 "\n{}Implementation: {}",
1887 indent,
1888 c.implementation_paths.join(", ")
1889 ));
1890 if !c.symbols.is_empty() {
1891 out.push_str(&format!(" ({} member symbols)", c.symbols.len()));
1892 }
1893 }
1894 if !c.consumes.is_empty() {
1895 out.push_str(&format!("\n{}Consumes: {}", indent, c.consumes.join(", ")));
1896 }
1897 if !c.produces.is_empty() {
1898 out.push_str(&format!("\n{}Produces: {}", indent, c.produces.join(", ")));
1899 }
1900 if !c.upstream.is_empty() {
1901 out.push_str(&format!("\n{}Upstream: {}", indent, c.upstream.join(", ")));
1902 }
1903 if !c.downstream.is_empty() {
1904 out.push_str(&format!(
1905 "\n{}Downstream: {}",
1906 indent,
1907 c.downstream.join(", ")
1908 ));
1909 }
1910 if !c.owns.is_empty() {
1911 let owned: Vec<String> = c
1912 .owns
1913 .iter()
1914 .map(|o| format!("{} ({})", o.target, o.provenance))
1915 .collect();
1916 out.push_str(&format!("\n{}Owns: {}", indent, owned.join(", ")));
1917 }
1918 out.push('\n');
1919 out
1920 };
1921 let name_of = |id: &str| -> Option<String> { ctx.view.entity(id).map(|e| e.name.clone()) };
1922 for svc in atlas.hierarchy.iter().filter(|n| n.kind == "service") {
1924 arch.push_str(&format!("\nSERVICE {}\n", svc.name.to_uppercase()));
1925 for m in &svc.members {
1926 let Some(sub) = atlas.hierarchy.iter().find(|n| &n.id == m) else {
1927 continue;
1928 };
1929 if sub.kind != "subsystem" {
1930 continue;
1931 }
1932 arch.push_str(&format!(" SUBSYSTEM {}\n", sub.name.to_uppercase()));
1933 for cm in &sub.members {
1934 if let Some(name) = name_of(cm) {
1935 rendered.insert(name.clone());
1936 if let Some(c) = atlas.components.iter().find(|c| c.name == name) {
1937 arch.push_str(&format!(" {}\n", comp_block(c, " ").trim_end()));
1938 }
1939 }
1940 }
1941 arch.push('\n');
1942 }
1943 for m in &svc.members {
1944 if atlas.hierarchy.iter().any(|n| &n.id == m) {
1945 continue; }
1947 if let Some(name) = name_of(m) {
1948 rendered.insert(name.clone());
1949 if let Some(c) = atlas.components.iter().find(|c| c.name == name) {
1950 arch.push_str(&format!(" {}\n", comp_block(c, " ").trim_end()));
1951 }
1952 }
1953 }
1954 }
1955 for sub in atlas.hierarchy.iter().filter(|n| n.kind == "subsystem") {
1957 if atlas
1958 .hierarchy
1959 .iter()
1960 .any(|n| n.kind == "service" && n.members.contains(&sub.id))
1961 {
1962 continue;
1963 }
1964 arch.push_str(&format!("\nSUBSYSTEM {}\n", sub.name.to_uppercase()));
1965 for cm in &sub.members {
1966 if let Some(name) = name_of(cm) {
1967 rendered.insert(name.clone());
1968 if let Some(c) = atlas.components.iter().find(|c| c.name == name) {
1969 arch.push_str(&format!(" {}\n", comp_block(c, " ").trim_end()));
1970 }
1971 }
1972 }
1973 arch.push('\n');
1974 }
1975 for layer in ["component", "code_region"] {
1977 for c in &atlas.components {
1978 if rendered.contains(&c.name) {
1979 continue;
1980 }
1981 if c.layer != layer {
1982 continue;
1983 }
1984 arch.push_str(&comp_block(c, ""));
1985 }
1986 }
1987 sections.push(Section::new("ARCHITECTURE", arch, 9));
1988
1989 const FLOW_RENDER_CAP: usize = 32;
1997 const FLOW_RENDER_STEP_CAP: usize = 16;
1998 let mut flows = String::new();
1999 let mut render_flows: Vec<&AtlasFlow> = atlas
2000 .flows
2001 .iter()
2002 .filter(|f| f.kind != FlowKind::Architecture)
2003 .collect();
2004 render_flows.sort_by(|a, b| b.steps.len().cmp(&a.steps.len()).then(a.name.cmp(&b.name)));
2005 for f in render_flows.into_iter().take(FLOW_RENDER_CAP) {
2006 flows.push_str(&format!("\n{} [{}]", f.name, flow_kind_str(f.kind)));
2007 if let Some(t) = &f.trigger {
2008 flows.push_str(&format!("\nTrigger: {t}"));
2009 }
2010 for s in f.steps.iter().take(FLOW_RENDER_STEP_CAP) {
2011 flows.push_str(&format!("\n{s}"));
2012 }
2013 if f.steps.len() > FLOW_RENDER_STEP_CAP {
2014 flows.push_str(&format!(
2015 "\n... +{} more steps",
2016 f.steps.len() - FLOW_RENDER_STEP_CAP
2017 ));
2018 }
2019 flows.push('\n');
2020 }
2021 sections.push(Section::new("FLOWS", flows, 9));
2022
2023 let has_state = atlas.state_authority.values().any(|v| !v.is_empty());
2030 let mut state_body = String::new();
2031 if has_state {
2032 state_body.push_str("DATA OWNERSHIP\n");
2033 }
2034 for c in &atlas.components {
2035 for o in &c.owns {
2036 state_body.push_str(&format!(
2037 "{} owns {} ({})\n",
2038 c.name, o.target, o.provenance
2039 ));
2040 }
2041 }
2042 if let Some(lines) = atlas.state_authority.get(scc_graph::state::S_PERSISTENT) {
2043 for l in lines {
2044 if l.contains("::") {
2045 state_body.push_str(&format!("{l}\n"));
2046 }
2047 }
2048 }
2049 if !atlas.data_stores.is_empty() {
2050 state_body.push_str("\nDATA STORES\n");
2051 for s in &atlas.data_stores {
2052 state_body.push_str(&format!(" {s}\n"));
2053 }
2054 }
2055 for section in [
2056 scc_graph::state::S_RUNTIME,
2057 scc_graph::state::S_REACTIVE,
2058 scc_graph::state::S_CONFIGURATION,
2059 scc_graph::state::S_CACHES,
2060 scc_graph::state::S_DERIVED,
2061 ] {
2062 if let Some(lines) = atlas.state_authority.get(section) {
2063 if !lines.is_empty() {
2064 state_body.push_str(&format!("\n{}\n", scc_graph::state::section_label(section)));
2065 for l in lines {
2066 state_body.push_str(&format!(" {l}\n"));
2067 }
2068 }
2069 }
2070 }
2071 sections.push(Section::new(
2072 if has_state {
2073 "STATE & DATA AUTHORITY"
2074 } else {
2075 "DATA OWNERSHIP"
2076 },
2077 state_body,
2078 10,
2079 ));
2080
2081 sections.push(Section::new(
2088 "CONTRACTS",
2089 if atlas.contracts.is_empty() {
2090 "(none)".into()
2091 } else {
2092 let mut lines: Vec<String> = Vec::new();
2093 for c in &atlas.contracts {
2094 let prefix = c.subclass.as_str();
2095 for op in &c.operations {
2096 lines.push(format!("{prefix}: {op}"));
2097 }
2098 }
2099 lines.sort();
2100 lines.join("\n")
2101 },
2102 9,
2103 ));
2104
2105 sections.push(Section::new(
2110 "PUBLIC API",
2111 if atlas.public_api.is_empty() {
2112 "(none)".into()
2113 } else {
2114 const API_RENDER_CAP: usize = 64;
2115 let mut lines: Vec<String> = Vec::new();
2116 for (comp, exports) in &atlas.public_api {
2117 if exports.is_empty() {
2118 continue;
2119 }
2120 let shown: Vec<&str> = exports
2121 .iter()
2122 .take(API_RENDER_CAP)
2123 .map(|s| s.as_str())
2124 .collect();
2125 let mut line = format!("{}: exports {}", comp, shown.join(", "));
2126 if exports.len() > API_RENDER_CAP {
2127 line.push_str(&format!(" (+{} more)", exports.len() - API_RENDER_CAP));
2128 }
2129 lines.push(line);
2130 }
2131 lines.join("\n")
2132 },
2133 6,
2134 ));
2135
2136 sections.push(Section::new(
2140 "FRAMEWORK SEMANTICS",
2141 if atlas.framework_semantics.is_empty() {
2142 "(none)".into()
2143 } else {
2144 const SEM_RENDER_CAP: usize = 48;
2145 let mut lines: Vec<String> = Vec::new();
2146 for (comp, facts) in &atlas.framework_semantics {
2147 for f in facts.iter().take(SEM_RENDER_CAP) {
2148 lines.push(format!("{comp}: {f}"));
2149 }
2150 if facts.len() > SEM_RENDER_CAP {
2151 lines.push(format!("{comp}: (+{} more)", facts.len() - SEM_RENDER_CAP));
2152 }
2153 }
2154 lines.join("\n")
2155 },
2156 6,
2157 ));
2158
2159 sections.push(Section::new(
2162 "PIPELINE",
2163 if atlas.pipeline.is_empty() {
2164 "(none)".into()
2165 } else {
2166 atlas.pipeline.join("\n")
2167 },
2168 6,
2169 ));
2170
2171 sections.push(Section::new(
2174 "LANDMARKS",
2175 if atlas.landmarks.is_empty() {
2176 "(none)".into()
2177 } else {
2178 let mut lines = atlas.landmarks.clone();
2179 lines.sort();
2180 lines.join("\n")
2181 },
2182 5,
2183 ));
2184
2185 let mut inv = String::new();
2187 for i in &atlas.invariants {
2188 inv.push_str(&format!(
2189 "- [{}] {}\n",
2190 severity_str(i.severity),
2191 i.statement
2192 ));
2193 }
2194 sections.push(Section::new("CRITICAL INVARIANTS", inv, 10));
2195
2196 let mut failure = String::new();
2198 for c in &atlas.components {
2199 for fb in &c.failure_behavior {
2200 failure.push_str(&format!("{}: {}\n", c.name, fb));
2201 }
2202 }
2203 sections.push(Section::new("FAILURE / RETRY", failure, 9));
2204
2205 sections.push(Section::new(
2207 "DEPLOYMENT",
2208 if atlas.deployment_units.is_empty() {
2209 "(none)".into()
2210 } else {
2211 atlas.deployment_units.join("\n")
2212 },
2213 7,
2214 ));
2215
2216 sections.push(Section::new(
2218 "TRUST BOUNDARIES",
2219 if atlas.trust_boundaries.is_empty() {
2220 "(none)".into()
2221 } else {
2222 atlas.trust_boundaries.join("\n")
2223 },
2224 7,
2225 ));
2226
2227 sections.push(Section::new(
2229 "ASYNC BOUNDARIES",
2230 if atlas.async_boundaries.is_empty() {
2231 "(none)".into()
2232 } else {
2233 atlas.async_boundaries.join("\n")
2234 },
2235 7,
2236 ));
2237
2238 sections.push(Section::new(
2240 "EXTERNAL SYSTEMS",
2241 if atlas.external_systems.is_empty() {
2242 "(none)".into()
2243 } else {
2244 atlas.external_systems.join("\n")
2245 },
2246 7,
2247 ));
2248
2249 let mut impl_map = String::new();
2251 for (name, paths) in &atlas.implementation_map {
2252 if !paths.is_empty() {
2253 impl_map.push_str(&format!("{}: {}\n", name, paths.join(", ")));
2254 }
2255 }
2256 sections.push(Section::new("IMPLEMENTATION MAP", impl_map, 6));
2257
2258 let ev: Vec<String> = atlas
2260 .evidence_summary
2261 .iter()
2262 .map(|(k, v)| format!("{v} {k}"))
2263 .collect();
2264 sections.push(Section::new(
2265 "EVIDENCE STATUS",
2266 if ev.is_empty() {
2267 "(none)".into()
2268 } else {
2269 ev.join(", ")
2270 },
2271 8,
2272 ));
2273
2274 let mut runtime = String::new();
2278 let sigs = ctx.store.trace_signatures().unwrap_or_default();
2279 if !sigs.is_empty() {
2280 runtime.push_str("OBSERVED PATH\n");
2281 for (signature, count, latency_ms, errors, _last) in sigs.into_iter().take(10) {
2283 runtime.push_str(&format!(
2284 "{signature} ({count} reqs, avg {latency_ms:.1} ms, {errors} err)\n"
2285 ));
2286 }
2287 }
2288 const THREE_WAY_KINDS: [&str; 3] = [
2289 "undeclared_observed",
2290 "declared_unobserved",
2291 "static_unobserved",
2292 ];
2293 for (_, kind, _sev, msg, _) in ctx.store.drift_findings(true).unwrap_or_default() {
2294 if !THREE_WAY_KINDS.contains(&kind.as_str()) {
2295 continue;
2296 }
2297 let label = match kind.as_str() {
2298 "undeclared_observed" => "undeclared observed",
2299 "declared_unobserved" => "declared unobserved",
2300 "static_unobserved" => "static unobserved",
2301 _ => kind.as_str(),
2302 };
2303 runtime.push_str(&format!("DRIFT {label}: {msg}\n"));
2304 }
2305 if runtime.is_empty() {
2306 runtime.push_str("(none)\n");
2307 }
2308 sections.push(Section::new("RUNTIME", runtime, 8));
2309
2310 let mut coverage = String::new();
2314 for (k, v) in &atlas.coverage {
2315 coverage.push_str(&format!("{k}: {v}\n"));
2316 }
2317 sections.push(Section::new(
2318 "MODEL COVERAGE",
2319 if coverage.is_empty() {
2320 "(none)".into()
2321 } else {
2322 coverage
2323 },
2324 7,
2325 ));
2326
2327 let warnings = atlas.warnings.clone();
2328 if full {
2329 finish_soft(&mut pack, sections, budget, warnings);
2330 } else {
2331 finish(&mut pack, sections, budget, warnings);
2332 }
2333 pack.entity_ids = comp_ids(ctx);
2334 pack
2335}
2336
2337fn project_flow_graph(
2344 view: &TrustedGraphView,
2345 g: &scc_core::FlowGraph,
2346 async_boundaries: &mut BTreeSet<String>,
2347) -> Vec<String> {
2348 let policy = view.policy();
2349 let edge_ok = |e: &scc_core::FlowEdge| -> bool {
2350 match e.provenance {
2351 None => true,
2352 Some(p) => policy.allows(p, e.confidence),
2353 }
2354 };
2355 let mut lines: Vec<String> = Vec::new();
2356 let mut visited: std::collections::BTreeSet<u32> = std::collections::BTreeSet::new();
2357 let mut queue: std::collections::VecDeque<u32> = g.entrypoints.iter().copied().collect();
2358 while let Some(n) = queue.pop_front() {
2359 if !visited.insert(n) {
2360 continue;
2361 }
2362 let Some(node) = g.nodes.get(n as usize) else {
2363 continue;
2364 };
2365 lines.push(format!(
2366 "{}: {}",
2367 entity_name(view, &node.actor),
2368 node.operation
2369 ));
2370 let mut outs: Vec<&scc_core::FlowEdge> = g
2371 .edges
2372 .iter()
2373 .filter(|e| e.from == n && edge_ok(e))
2374 .collect();
2375 outs.sort_by(|a, b| {
2376 edge_rank(a.kind)
2377 .cmp(&edge_rank(b.kind))
2378 .then(a.to.cmp(&b.to))
2379 });
2380 for e in outs {
2381 let Some(target) = g.nodes.get(e.to as usize) else {
2382 continue;
2383 };
2384 let mut line = format!(
2385 " -> {}: {}",
2386 entity_name(view, &target.actor),
2387 target.operation
2388 );
2389 match e.kind {
2390 scc_core::FlowEdgeKind::Branch => line.push_str(" (branch)"),
2391 scc_core::FlowEdgeKind::Retry => line.push_str(" [retry]"),
2392 scc_core::FlowEdgeKind::Error => line.push_str(" [error]"),
2393 scc_core::FlowEdgeKind::Async => line.push_str(" [async]"),
2394 scc_core::FlowEdgeKind::Publish => line.push_str(" [publish]"),
2395 scc_core::FlowEdgeKind::Consume => line.push_str(" [consume]"),
2396 scc_core::FlowEdgeKind::Join => line.push_str(" (join)"),
2397 scc_core::FlowEdgeKind::Fallback => line.push_str(" [fallback]"),
2398 scc_core::FlowEdgeKind::Timeout => line.push_str(" [timeout]"),
2399 scc_core::FlowEdgeKind::Compensation => line.push_str(" [compensate]"),
2400 scc_core::FlowEdgeKind::Read => line.push_str(" [read]"),
2401 scc_core::FlowEdgeKind::Write => line.push_str(" [write]"),
2402 scc_core::FlowEdgeKind::Transform => line.push_str(" [transform]"),
2403 scc_core::FlowEdgeKind::Validate => line.push_str(" [validate]"),
2404 scc_core::FlowEdgeKind::Authorize => line.push_str(" [authorize]"),
2405 scc_core::FlowEdgeKind::Cache => line.push_str(" [cache]"),
2406 scc_core::FlowEdgeKind::Invalidate => line.push_str(" [invalidate]"),
2407 _ => {}
2408 }
2409 if let Some(c) = &e.condition {
2410 line.push_str(&format!(" ({c})"));
2411 }
2412 lines.push(line);
2413 if e.kind == scc_core::FlowEdgeKind::Async {
2414 async_boundaries.insert(format!(
2415 "{} --async--> {}",
2416 entity_name(view, &node.actor),
2417 entity_name(view, &target.actor)
2418 ));
2419 }
2420 queue.push_back(e.to);
2421 }
2422 }
2423 lines
2424}
2425
2426fn edge_rank(k: scc_core::FlowEdgeKind) -> u8 {
2427 match k {
2428 scc_core::FlowEdgeKind::Next => 0,
2429 scc_core::FlowEdgeKind::Async => 1,
2430 scc_core::FlowEdgeKind::Branch => 2,
2431 scc_core::FlowEdgeKind::Join => 3,
2432 scc_core::FlowEdgeKind::Retry => 4,
2433 scc_core::FlowEdgeKind::Fallback => 5,
2434 scc_core::FlowEdgeKind::Error => 6,
2435 scc_core::FlowEdgeKind::Publish => 7,
2436 scc_core::FlowEdgeKind::Consume => 8,
2437 scc_core::FlowEdgeKind::Return => 9,
2438 scc_core::FlowEdgeKind::Timeout => 10,
2439 scc_core::FlowEdgeKind::Compensation => 11,
2440 scc_core::FlowEdgeKind::Read => 12,
2441 scc_core::FlowEdgeKind::Write => 13,
2442 scc_core::FlowEdgeKind::Transform => 14,
2443 scc_core::FlowEdgeKind::Validate => 15,
2444 scc_core::FlowEdgeKind::Authorize => 16,
2445 scc_core::FlowEdgeKind::Cache => 17,
2446 scc_core::FlowEdgeKind::Invalidate => 18,
2447 }
2448}
2449
2450fn comp_ids(ctx: &ContextCompiler) -> Vec<String> {
2452 ctx.view
2453 .components()
2454 .into_iter()
2455 .map(|c| c.id.clone())
2456 .collect()
2457}
2458
2459fn flow_kind_str(k: FlowKind) -> &'static str {
2460 match k {
2461 FlowKind::Architecture => "architecture",
2462 FlowKind::Workflow => "workflow",
2463 FlowKind::Sequence => "sequence",
2464 FlowKind::Dataflow => "dataflow",
2465 FlowKind::Lifecycle => "lifecycle",
2466 }
2467}
2468
2469fn severity_str(s: scc_core::Severity) -> &'static str {
2470 match s {
2471 scc_core::Severity::Info => "INFO",
2472 scc_core::Severity::Low => "LOW",
2473 scc_core::Severity::Medium => "MEDIUM",
2474 scc_core::Severity::High => "HIGH",
2475 scc_core::Severity::Critical => "CRITICAL",
2476 }
2477}
2478
2479#[cfg(test)]
2480mod tests {
2481 use super::*;
2482 use scc_core::{
2483 entity_id, kinds, predicates, relationship_id, symbol_id, Entity, Provenance, Relationship,
2484 };
2485 use scc_store::Store;
2486
2487 fn test_store() -> (tempfile::TempDir, Store) {
2489 let dir = tempfile::TempDir::new().unwrap();
2490 let root = dir.path().join("repo");
2491 std::fs::create_dir_all(&root).unwrap();
2492 let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
2493 let repo = "repo";
2494
2495 store
2497 .upsert_file("app.py", "h1", "python", "source", 10)
2498 .unwrap();
2499 store
2500 .upsert_file("lib.py", "h2", "python", "source", 10)
2501 .unwrap();
2502 store
2503 .upsert_file("config.json", "h3", "json", "config", 10)
2504 .unwrap();
2505
2506 let mk = |n: &str| symbol_id(repo, "app.py", n);
2508 for n in ["handler", "worker", "reader"] {
2509 let mut e = Entity::new(mk(n), kinds::SYMBOL, n);
2510 e.attr("file", serde_json::json!("app.py"));
2511 store.insert_entity(&e, &["app.py".to_string()]).unwrap();
2512 }
2513 let mut w = store.get_entity(&mk("worker")).unwrap().unwrap();
2515 w.attributes.insert(
2516 "cli_flags".into(),
2517 serde_json::json!(["--queue", "--verbose"]),
2518 );
2519 store.insert_entity(&w, &["app.py".to_string()]).unwrap();
2520
2521 let route_id = entity_id(repo, kinds::ROUTE, "GET /api/x");
2523 let mut re = Entity::new(route_id.clone(), kinds::ROUTE, "GET /api/x");
2524 re.attr("method", serde_json::json!("GET"));
2525 re.attr("path", serde_json::json!("/api/x"));
2526 re.attr("handler", serde_json::json!(mk("handler")));
2527 store.insert_entity(&re, &["app.py".to_string()]).unwrap();
2528 store
2529 .insert_relationship(
2530 &Relationship::new(
2531 relationship_id(1),
2532 mk("handler"),
2533 predicates::HANDLES,
2534 route_id,
2535 Provenance::Extracted,
2536 ),
2537 "app.py",
2538 )
2539 .unwrap();
2540
2541 let exp_id = entity_id(repo, kinds::EXPORT, "handler");
2543 let mut ex = Entity::new(exp_id.clone(), kinds::EXPORT, "handler");
2544 ex.attr("kind", serde_json::json!("function"));
2545 store.insert_entity(&ex, &["app.py".to_string()]).unwrap();
2546 store
2547 .insert_relationship(
2548 &Relationship::new(
2549 relationship_id(2),
2550 mk("handler"),
2551 predicates::EXPORTS,
2552 exp_id,
2553 Provenance::Extracted,
2554 ),
2555 "app.py",
2556 )
2557 .unwrap();
2558
2559 let cfg_id = entity_id(repo, kinds::CONFIGURATION, "DEBUG");
2561 store
2562 .insert_entity(
2563 &Entity::new(cfg_id.clone(), kinds::CONFIGURATION, "DEBUG"),
2564 &["app.py".to_string()],
2565 )
2566 .unwrap();
2567 store
2568 .insert_relationship(
2569 &Relationship::new(
2570 relationship_id(3),
2571 cfg_id,
2572 predicates::CONFIGURED_BY,
2573 mk("reader"),
2574 Provenance::Extracted,
2575 ),
2576 "app.py",
2577 )
2578 .unwrap();
2579
2580 store
2583 .insert_relationship(
2584 &Relationship::new(
2585 relationship_id(4),
2586 mk("handler"),
2587 predicates::CALLS,
2588 mk("worker"),
2589 Provenance::Extracted,
2590 ),
2591 "app.py",
2592 )
2593 .unwrap();
2594 let ext_id = entity_id(repo, kinds::EXTERNAL_API, "os");
2595 store
2596 .insert_relationship(
2597 &Relationship::new(
2598 relationship_id(5),
2599 mk("handler"),
2600 predicates::CALLS,
2601 ext_id,
2602 Provenance::Extracted,
2603 ),
2604 "app.py",
2605 )
2606 .unwrap();
2607 store
2608 .insert_relationship(
2609 &Relationship::new(
2610 relationship_id(6),
2611 mk("worker"),
2612 predicates::CALLS,
2613 mk("reader"),
2614 Provenance::Resolved,
2615 ),
2616 "app.py",
2617 )
2618 .unwrap();
2619
2620 let topic_id = entity_id(repo, kinds::TOPIC, "jobs");
2622 store
2623 .insert_entity(
2624 &Entity::new(topic_id.clone(), kinds::TOPIC, "jobs"),
2625 &["app.py".to_string()],
2626 )
2627 .unwrap();
2628 store
2629 .insert_relationship(
2630 &Relationship::new(
2631 relationship_id(7),
2632 mk("worker"),
2633 predicates::SUBSCRIBES,
2634 topic_id,
2635 Provenance::Extracted,
2636 ),
2637 "app.py",
2638 )
2639 .unwrap();
2640
2641 let _graph = scc_graph::RealityGraph::load(&store).unwrap();
2642 (dir, store)
2643 }
2644
2645 #[test]
2646 fn kinds_stringify() {
2647 assert_eq!(flow_kind_str(FlowKind::Sequence), "sequence");
2648 assert_eq!(severity_str(scc_core::Severity::Critical), "CRITICAL");
2649 }
2650
2651 #[test]
2652 fn atlas_production_scope_keeps_fixture_routes_out() {
2654 let (_dir, store) = fact_layer_store();
2658 let repo = store.repo_id.clone();
2659 let mk_route = |store: &Store, method: &str, path: &str, file: &str| {
2660 let name = format!("{method} {path}");
2661 let mut e = Entity::new(entity_id(&repo, kinds::ROUTE, &name), kinds::ROUTE, &name);
2662 e.attr("method", serde_json::json!(method));
2663 e.attr("path", serde_json::json!(path));
2664 e.attr("handler", serde_json::json!("handler"));
2665 e.attr("file", serde_json::json!(file));
2666 store.insert_entity(&e, &[file.to_string()]).unwrap();
2667 };
2668 mk_route(&store, "GET", "/api/ok", "api/app.py");
2669 mk_route(&store, "GET", "/admin", "fixtures/foo/app.py");
2670 let mut fx_comp = scc_core::Entity::new(
2671 entity_id(&repo, kinds::COMPONENT, "fx"),
2672 kinds::COMPONENT,
2673 "fx",
2674 );
2675 fx_comp.attr(
2676 "implementation",
2677 serde_json::json!({"paths": ["fixtures/foo"], "symbols": []}),
2678 );
2679 let mut api_comp = scc_core::Entity::new(
2682 entity_id(&repo, kinds::COMPONENT, "api"),
2683 kinds::COMPONENT,
2684 "api",
2685 );
2686 api_comp.attr(
2687 "implementation",
2688 serde_json::json!({"paths": ["api"], "symbols": []}),
2689 );
2690 let mut web_comp = scc_core::Entity::new(
2691 entity_id(&repo, kinds::COMPONENT, "web"),
2692 kinds::COMPONENT,
2693 "web",
2694 );
2695 web_comp.attr(
2696 "implementation",
2697 serde_json::json!({"paths": ["web"], "symbols": []}),
2698 );
2699 store.replace_components(&[api_comp, web_comp, fx_comp]).unwrap();
2700
2701 let graph = scc_graph::RealityGraph::load(&store).unwrap();
2702 let ctx = ContextCompiler::new(&store, &graph, crate::ContextSettings::default(), Vec::new());
2703 let atlas = build_atlas(&ctx);
2704 assert!(
2705 atlas.entrypoints.iter().any(|e| e.trigger == "GET /api/ok"),
2706 "production route listed: {:?}",
2707 atlas.entrypoints
2708 );
2709 assert!(
2710 !atlas.entrypoints.iter().any(|e| e.trigger == "GET /admin"),
2711 "fixture route must not be a global entrypoint: {:?}",
2712 atlas.entrypoints
2713 );
2714 assert!(
2715 !atlas.contracts.iter().any(|c| c
2716 .operations
2717 .iter()
2718 .any(|o| o == "GET /admin")),
2719 "fixture route must not be a contract: {:?}",
2720 atlas.contracts
2721 );
2722 assert!(
2723 atlas.components.iter().any(|c| c.name == "fx" && c.role == "fixture"),
2724 "fixture component stays visible, labeled: {:?}",
2725 atlas.components.iter().map(|c| (&c.name, &c.role)).collect::<Vec<_>>()
2726 );
2727 assert!(
2728 atlas.coverage.get("scope").map(|s| s.contains("production")).unwrap_or(false),
2729 "scope honesty receipt: {:?}",
2730 atlas.coverage.get("scope")
2731 );
2732 let full = build_atlas_scoped(&ctx, AtlasScope::Full);
2733 assert!(
2734 full.entrypoints.iter().any(|e| e.trigger == "GET /admin"),
2735 "full scope restores the fixture route: {:?}",
2736 full.entrypoints
2737 );
2738 }
2739
2740 #[test]
2741 fn contracts_and_coverage_from_fact_layer() {
2743 let (_dir, store) = test_store();
2744 let graph = scc_graph::RealityGraph::load(&store).unwrap();
2745 let ctx = ContextCompiler::new(
2746 &store,
2747 &graph,
2748 crate::ContextSettings::default(),
2749 Vec::new(),
2750 );
2751 let atlas = build_atlas(&ctx);
2752
2753 let kinds_found: BTreeSet<String> =
2757 atlas.contracts.iter().map(|c| c.kind.clone()).collect();
2758 assert_eq!(
2759 kinds_found,
2760 BTreeSet::from([
2761 "http".to_string(),
2762 "cli".to_string(),
2763 "config".to_string(),
2764 "event".to_string(),
2765 "public-api".to_string()
2766 ]),
2767 "contract kinds: {:?}",
2768 atlas.contracts
2769 );
2770 for c in &atlas.contracts {
2773 assert_eq!(
2774 c.kind,
2775 c.subclass.as_str(),
2776 "kind agrees with subclass: {c:?}"
2777 );
2778 }
2779 let http = atlas.contracts.iter().find(|c| c.kind == "http").unwrap();
2780 assert_eq!(http.operations, vec!["GET /api/x"]);
2781 assert_eq!(http.subclass, scc_core::ContractSubclass::Http);
2782 assert!(
2783 http.consumers.iter().any(|c| c == "handler"),
2784 "handler consumes the route: {:?}",
2785 http.consumers
2786 );
2787 let cli = atlas.contracts.iter().find(|c| c.kind == "cli").unwrap();
2788 assert_eq!(cli.operations, vec!["--queue", "--verbose"]);
2789 assert_eq!(cli.subclass, scc_core::ContractSubclass::Cli);
2790 let cfg = atlas.contracts.iter().find(|c| c.kind == "config").unwrap();
2791 assert_eq!(cfg.operations, vec!["DEBUG"]);
2792 assert_eq!(cfg.subclass, scc_core::ContractSubclass::Configuration);
2793 assert!(
2794 cfg.consumers.iter().any(|c| c == "reader"),
2795 "reader consumes DEBUG: {:?}",
2796 cfg.consumers
2797 );
2798 let api = atlas
2799 .contracts
2800 .iter()
2801 .find(|c| c.kind == "public-api")
2802 .unwrap();
2803 assert_eq!(api.operations, vec!["handler"]);
2804 assert_eq!(api.subclass, scc_core::ContractSubclass::PublicApi);
2805
2806 let lines: Vec<String> = atlas
2808 .contracts
2809 .iter()
2810 .flat_map(|c| {
2811 c.operations
2812 .iter()
2813 .map(|op| format!("{}: {}", c.subclass.as_str(), op))
2814 })
2815 .collect();
2816 for want in [
2817 "http: GET /api/x",
2818 "cli: --queue",
2819 "config: DEBUG",
2820 "event: jobs",
2821 "public-api: handler",
2822 ] {
2823 assert!(
2824 lines.contains(&want.to_string()),
2825 "missing {want}: {lines:?}"
2826 );
2827 }
2828
2829 assert_eq!(
2831 atlas.coverage.get("parsed_source_files").unwrap(),
2832 "66% (2/3)"
2833 );
2834 assert_eq!(
2835 atlas.coverage.get("unparsed_files").unwrap(),
2836 "1 (config/docs/infra — scanned but not source-parsed)"
2837 );
2838 assert!(
2839 atlas
2840 .coverage
2841 .get("exported_api")
2842 .unwrap()
2843 .starts_with("1 export entity"),
2844 "{:?}",
2845 atlas.coverage.get("exported_api")
2846 );
2847 assert_eq!(
2850 atlas.coverage.get("call_targets_resolved").unwrap(),
2851 "66% (2/3, 1 LSP-RESOLVED) — exploration still justified in unresolved regions"
2852 );
2853 assert_eq!(
2854 atlas.coverage.get("dynamic_receivers_unresolved").unwrap(),
2855 "1 (calls whose target is not a local symbol; unknown-receiver calls are not persisted)"
2856 );
2857 assert_eq!(
2860 atlas.coverage.get("invocation_surfaces").unwrap(),
2861 "4 (cli 1, http 1, public_api 1, queue 1)",
2862 "{:?}",
2863 atlas.coverage.get("invocation_surfaces")
2864 );
2865 assert_eq!(
2866 atlas.coverage.get("stale_evidence").unwrap(),
2867 "0 (model FRESH)"
2868 );
2869 assert!(atlas.coverage.contains_key("model_epoch_generations"));
2870 assert!(atlas
2871 .coverage
2872 .contains_key("framework_registrations_unknown"));
2873
2874 let ep_kinds: BTreeSet<&str> = atlas.entrypoints.iter().map(|e| e.kind.as_str()).collect();
2876 assert!(ep_kinds.contains("public_api"), "{ep_kinds:?}");
2877 assert!(ep_kinds.contains("queue"), "{ep_kinds:?}");
2878 assert!(ep_kinds.contains("http"), "http surface: {ep_kinds:?}");
2879 assert!(ep_kinds.contains("cli"), "cli surface: {ep_kinds:?}");
2880 }
2881
2882 #[test]
2890
2891 fn schema_and_reactive_render_in_atlas() {
2893 let dir = tempfile::TempDir::new().unwrap();
2894 let root = dir.path().join("repo");
2895 std::fs::create_dir_all(&root).unwrap();
2896 let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
2897 let repo = store.repo_id.clone();
2898
2899 let comp_id = entity_id(&repo, kinds::COMPONENT, "api");
2903 store
2904 .replace_components(&[scc_core::Entity::new(
2905 comp_id.clone(),
2906 kinds::COMPONENT,
2907 "api",
2908 )])
2909 .unwrap();
2910 let fid = entity_id(&repo, kinds::FILE, "api/app.py");
2911 store
2912 .insert_entity(
2913 &Entity::new(fid.clone(), kinds::FILE, "api/app.py"),
2914 &["api/app.py".to_string()],
2915 )
2916 .unwrap();
2917 store
2918 .insert_relationship(
2919 &Relationship::new(
2920 "rel:c:api",
2921 comp_id,
2922 predicates::CONTAINS,
2923 fid.clone(),
2924 Provenance::Extracted,
2925 ),
2926 "api/app.py",
2927 )
2928 .unwrap();
2929 let svc = symbol_id(&repo, "api/app.py", "UserService");
2930 store
2931 .insert_entity(
2932 &Entity::new(svc.clone(), kinds::SYMBOL, "UserService"),
2933 &["api/app.py".to_string()],
2934 )
2935 .unwrap();
2936 store
2937 .insert_relationship(
2938 &Relationship::new(
2939 "rel:f:svc",
2940 fid,
2941 predicates::CONTAINS,
2942 svc.clone(),
2943 Provenance::Extracted,
2944 ),
2945 "api/app.py",
2946 )
2947 .unwrap();
2948
2949 let base = entity_id(&repo, kinds::SCHEMA, "Base");
2952 store
2953 .insert_entity(
2954 &Entity::new(base.clone(), kinds::SCHEMA, "Base"),
2955 &["api/app.py".to_string()],
2956 )
2957 .unwrap();
2958 let user = entity_id(&repo, kinds::SCHEMA, "User");
2959 store
2960 .insert_entity(
2961 &Entity::new(user.clone(), kinds::SCHEMA, "User"),
2962 &["api/app.py".to_string()],
2963 )
2964 .unwrap();
2965 let user_occ = scc_core::occurrence_id(&repo, "User", "api/app.py", "User", 1);
2966 store
2967 .insert_entity(
2968 Entity::new(
2969 user_occ.clone(),
2970 scc_core::kinds::OCCURRENCE,
2971 "User@api/app.py@User@1",
2972 )
2973 .attr("concept", serde_json::json!(user))
2974 .attr("path", serde_json::json!("api/app.py"))
2975 .attr("owner", serde_json::json!("User"))
2976 .attr("line", serde_json::json!(1)),
2977 &["api/app.py".to_string()],
2978 )
2979 .unwrap();
2980 store
2981 .insert_relationship(
2982 &Relationship::new(
2983 "rel:occ:user",
2984 user_occ,
2985 scc_core::predicates::OCCURS,
2986 user.clone(),
2987 Provenance::Extracted,
2988 ),
2989 "api/app.py",
2990 )
2991 .unwrap();
2992 store
2993 .insert_relationship(
2994 &Relationship::new(
2995 "rel:defines",
2996 svc.clone(),
2997 predicates::DEFINES,
2998 user.clone(),
2999 Provenance::Extracted,
3000 ),
3001 "api/app.py",
3002 )
3003 .unwrap();
3004 store
3005 .insert_relationship(
3006 &Relationship::new(
3007 "rel:composes",
3008 user,
3009 predicates::COMPOSES,
3010 base,
3011 Provenance::Extracted,
3012 ),
3013 "api/app.py",
3014 )
3015 .unwrap();
3016 let target = entity_id(&repo, kinds::SYMBOL, "CreateUser");
3017 store
3018 .insert_relationship(
3019 &Relationship::new(
3020 "rel:validates",
3021 svc.clone(),
3022 predicates::VALIDATES,
3023 target,
3024 Provenance::Extracted,
3025 ),
3026 "api/app.py",
3027 )
3028 .unwrap();
3029
3030 let count = entity_id(&repo, kinds::REACTIVE, "count");
3033 store
3034 .insert_entity(
3035 &Entity::new(count.clone(), kinds::REACTIVE, "count"),
3036 &["api/app.py".to_string()],
3037 )
3038 .unwrap();
3039 let count_occ = scc_core::occurrence_id(&repo, "count", "api/app.py", "UserService", 1);
3040 store
3041 .insert_entity(
3042 Entity::new(
3043 count_occ.clone(),
3044 scc_core::kinds::OCCURRENCE,
3045 "count@api/app.py@UserService@1",
3046 )
3047 .attr("concept", serde_json::json!(count))
3048 .attr("path", serde_json::json!("api/app.py"))
3049 .attr("owner", serde_json::json!("UserService"))
3050 .attr("line", serde_json::json!(1))
3051 .attr("access", serde_json::json!("state")),
3052 &["api/app.py".to_string()],
3053 )
3054 .unwrap();
3055 store
3056 .insert_relationship(
3057 &Relationship::new(
3058 "rel:occ:count",
3059 count_occ.clone(),
3060 scc_core::predicates::OCCURS,
3061 count.clone(),
3062 Provenance::Extracted,
3063 ),
3064 "api/app.py",
3065 )
3066 .unwrap();
3067 store
3068 .insert_relationship(
3069 &Relationship::new(
3070 "rel:owns:count",
3071 svc,
3072 predicates::OWNS,
3073 count_occ,
3074 Provenance::Extracted,
3075 ),
3076 "api/app.py",
3077 )
3078 .unwrap();
3079
3080 let graph = scc_graph::RealityGraph::load(&store).unwrap();
3081 let ctx = ContextCompiler::new(
3082 &store,
3083 &graph,
3084 crate::ContextSettings::default(),
3085 Vec::new(),
3086 );
3087 let atlas = build_atlas(&ctx);
3088
3089 let schema = atlas
3093 .contracts
3094 .iter()
3095 .find(|c| {
3096 c.subclass == scc_core::ContractSubclass::Schema
3097 && c.operations.first() == Some(&"User".to_string())
3098 })
3099 .expect("schema contract for User");
3100 assert_eq!(schema.kind, "schema");
3101 assert_eq!(
3102 schema.operations,
3103 vec![
3104 "User".to_string(),
3105 "User extends Base".to_string(),
3106 "User validates".to_string()
3107 ],
3108 "{schema:?}"
3109 );
3110 assert_eq!(
3113 schema.producer, "User",
3114 "producer = owner symbol: {schema:?}"
3115 );
3116
3117 assert!(
3119 atlas
3120 .state_authority
3121 .get(scc_graph::state::S_REACTIVE)
3122 .map(|lines| lines
3123 .iter()
3124 .any(|l| l == "api owns reactive: count [state] (EXTRACTED)"))
3125 .unwrap_or(false),
3126 "{:?}",
3127 atlas.state_authority
3128 );
3129
3130 let pack = render_atlas(&ctx, &atlas, usize::MAX, false);
3132 for want in [
3133 "schema: User",
3134 "schema: User extends Base",
3135 "schema: User validates",
3136 "REACTIVE STATE",
3137 "api owns reactive: count [state] (EXTRACTED)",
3138 ] {
3139 assert!(
3140 pack.content.contains(want),
3141 "missing {want:?} in:\n{}",
3142 pack.content
3143 );
3144 }
3145 }
3146
3147 fn fact_layer_store() -> (tempfile::TempDir, Store) {
3152 let dir = tempfile::TempDir::new().unwrap();
3153 let root = dir.path().join("repo");
3154 std::fs::create_dir_all(&root).unwrap();
3155 let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
3156 let repo = store.repo_id.clone();
3157
3158 let mk_comp = |store: &Store, name: &str, file: &str| -> String {
3160 let id = entity_id(&repo, kinds::COMPONENT, name);
3161 store
3162 .insert_entity(
3163 &scc_core::Entity::new(id.clone(), kinds::COMPONENT, name),
3164 &[file.to_string()],
3165 )
3166 .unwrap();
3167 let fid = entity_id(&repo, kinds::FILE, file);
3168 store
3169 .insert_relationship(
3170 &Relationship::new(
3171 format!("rel:c:{name}"),
3172 id.clone(),
3173 predicates::CONTAINS,
3174 fid,
3175 Provenance::Extracted,
3176 ),
3177 file,
3178 )
3179 .unwrap();
3180 id
3181 };
3182 mk_comp(&store, "api", "api/app.py");
3183 mk_comp(&store, "web", "web/app.py");
3184 let mut api_comp = scc_core::Entity::new(
3189 entity_id(&repo, kinds::COMPONENT, "api"),
3190 kinds::COMPONENT,
3191 "api",
3192 );
3193 api_comp.attr(
3194 "implementation",
3195 serde_json::json!({
3196 "paths": ["api"],
3197 "symbols": ["App", "App.get", "include_router", "_secret"],
3198 }),
3199 );
3200 let mut web_comp = scc_core::Entity::new(
3201 entity_id(&repo, kinds::COMPONENT, "web"),
3202 kinds::COMPONENT,
3203 "web",
3204 );
3205 web_comp.attr(
3206 "implementation",
3207 serde_json::json!({
3208 "paths": ["web"],
3209 "symbols": ["handle_page", "on_message"],
3210 }),
3211 );
3212 store.replace_components(&[api_comp, web_comp]).unwrap();
3213 store
3214 .insert_entity(
3215 &Entity::new(
3216 entity_id(&repo, kinds::FILE, "api/app.py"),
3217 kinds::FILE,
3218 "api/app.py",
3219 ),
3220 &["api/app.py".into()],
3221 )
3222 .unwrap();
3223 store
3224 .insert_entity(
3225 &Entity::new(
3226 entity_id(&repo, kinds::FILE, "web/app.py"),
3227 kinds::FILE,
3228 "web/app.py",
3229 ),
3230 &["web/app.py".into()],
3231 )
3232 .unwrap();
3233
3234 let mk_sym = |path: &str, name: &str, attrs: serde_json::Value| -> String {
3235 let id = symbol_id(&repo, path, name);
3236 let mut e = Entity::new(id.clone(), kinds::SYMBOL, name);
3237 if let Some(obj) = attrs.as_object() {
3238 for (k, v) in obj {
3239 e.attr(k, v.clone());
3240 }
3241 }
3242 store.insert_entity(&e, &[path.to_string()]).unwrap();
3243 let fid = entity_id(&repo, kinds::FILE, path);
3244 store
3245 .insert_relationship(
3246 &Relationship::new(
3247 format!("rel:f:{path}:{name}"),
3248 fid,
3249 predicates::CONTAINS,
3250 id.clone(),
3251 Provenance::Extracted,
3252 ),
3253 path,
3254 )
3255 .unwrap();
3256 id
3257 };
3258
3259 let app_id = mk_sym(
3261 "api/app.py",
3262 "App",
3263 serde_json::json!({"kind": "class", "exported": true}),
3264 );
3265 let app_get = mk_sym(
3266 "api/app.py",
3267 "App.get",
3268 serde_json::json!({"kind": "method", "parent": "App", "exported": false}),
3269 );
3270 mk_sym(
3272 "api/app.py",
3273 "include_router",
3274 serde_json::json!({"kind": "function", "exported": true}),
3275 );
3276 mk_sym(
3277 "api/app.py",
3278 "_secret",
3279 serde_json::json!({"kind": "function", "exported": true}),
3280 );
3281 let web_handle = mk_sym(
3283 "web/app.py",
3284 "handle_page",
3285 serde_json::json!({"kind": "function", "exported": true}),
3286 );
3287
3288 let exp_id = entity_id(&repo, kinds::EXPORT, "include_router");
3290 store
3291 .insert_entity(
3292 &Entity::new(exp_id.clone(), kinds::EXPORT, "include_router"),
3293 &["api/app.py".into()],
3294 )
3295 .unwrap();
3296 let include_id = symbol_id(&repo, "api/app.py", "include_router");
3297 store
3298 .insert_relationship(
3299 &Relationship::new(
3300 relationship_id(100),
3301 include_id,
3302 predicates::EXPORTS,
3303 exp_id,
3304 Provenance::Extracted,
3305 ),
3306 "api/app.py",
3307 )
3308 .unwrap();
3309
3310 let ann_id = entity_id(&repo, kinds::ANNOTATION, "Get");
3312 store
3313 .insert_entity(
3314 &Entity::new(ann_id.clone(), kinds::ANNOTATION, "Get"),
3315 &["api/app.py".into()],
3316 )
3317 .unwrap();
3318 store
3319 .insert_relationship(
3320 &Relationship::new(
3321 relationship_id(101),
3322 ann_id,
3323 predicates::ANNOTATES,
3324 app_get.clone(),
3325 Provenance::Extracted,
3326 ),
3327 "api/app.py",
3328 )
3329 .unwrap();
3330
3331 let mw_id = entity_id(&repo, kinds::MIDDLEWARE, "RequestLogger");
3333 store
3334 .insert_entity(
3335 &Entity::new(mw_id.clone(), kinds::MIDDLEWARE, "RequestLogger"),
3336 &["api/app.py".into()],
3337 )
3338 .unwrap();
3339 store
3340 .insert_relationship(
3341 &Relationship::new(
3342 relationship_id(102),
3343 app_id.clone(),
3344 predicates::REGISTERS,
3345 mw_id,
3346 Provenance::Extracted,
3347 ),
3348 "api/app.py",
3349 )
3350 .unwrap();
3351
3352 let cb_sym = mk_sym(
3354 "web/app.py",
3355 "on_message",
3356 serde_json::json!({"kind": "function", "exported": false}),
3357 );
3358 store
3359 .insert_relationship(
3360 &Relationship::new(
3361 relationship_id(103),
3362 web_handle.clone(),
3363 predicates::HANDLES_CALLBACK,
3364 cb_sym,
3365 Provenance::Extracted,
3366 ),
3367 "web/app.py",
3368 )
3369 .unwrap();
3370
3371 let field_id = entity_id(&repo, kinds::FIELD, "App.cache");
3373 store
3374 .insert_entity(
3375 Entity::new(field_id.clone(), kinds::FIELD, "App.cache")
3376 .attr("mutable", serde_json::json!(true))
3377 .attr("owner", serde_json::json!("App")),
3378 &["api/app.py".into()],
3379 )
3380 .unwrap();
3381 store
3382 .insert_relationship(
3383 &Relationship::new(
3384 relationship_id(104),
3385 app_id.clone(),
3386 predicates::CONTAINS,
3387 field_id,
3388 Provenance::Extracted,
3389 ),
3390 "api/app.py",
3391 )
3392 .unwrap();
3393 let cfg_id = entity_id(&repo, kinds::CONFIGURATION, "DEBUG");
3394 store
3395 .insert_entity(
3396 &Entity::new(cfg_id.clone(), kinds::CONFIGURATION, "DEBUG"),
3397 &["api/app.py".into()],
3398 )
3399 .unwrap();
3400 store
3401 .insert_relationship(
3402 &Relationship::new(
3403 relationship_id(105),
3404 cfg_id,
3405 predicates::CONFIGURED_BY,
3406 app_id,
3407 Provenance::Extracted,
3408 ),
3409 "api/app.py",
3410 )
3411 .unwrap();
3412
3413 let _graph = scc_graph::RealityGraph::load(&store).unwrap();
3414 (dir, store)
3415 }
3416
3417 #[test]
3418 fn fact_layer_sections_grouped_by_component() {
3420 let (_dir, store) = fact_layer_store();
3421 let graph = scc_graph::RealityGraph::load(&store).unwrap();
3422 let ctx = ContextCompiler::new(
3423 &store,
3424 &graph,
3425 crate::ContextSettings::default(),
3426 Vec::new(),
3427 );
3428 let atlas = build_atlas(&ctx);
3429
3430 let api_exports = atlas.public_api.get("api").expect("api exports");
3434 assert!(api_exports.contains(&"App".to_string()), "{api_exports:?}");
3435 assert!(
3436 api_exports.contains(&"include_router".to_string()),
3437 "{api_exports:?}"
3438 );
3439 assert!(
3440 !api_exports.iter().any(|e| e == "_secret"),
3441 "{api_exports:?}"
3442 );
3443 let web_exports = atlas.public_api.get("web").expect("web exports");
3444 assert!(
3445 web_exports.contains(&"handle_page".to_string()),
3446 "{web_exports:?}"
3447 );
3448
3449 let api_facts = atlas.framework_semantics.get("api").expect("api facts");
3451 assert!(
3452 api_facts
3453 .iter()
3454 .any(|f| f.contains("annotates App.get (Get)")),
3455 "{api_facts:?}"
3456 );
3457 assert!(
3458 api_facts
3459 .iter()
3460 .any(|f| f.contains("registers RequestLogger")),
3461 "{api_facts:?}"
3462 );
3463 let web_facts = atlas.framework_semantics.get("web").expect("web facts");
3464 assert!(
3465 web_facts
3466 .iter()
3467 .any(|f| f.contains("handles callback on_message")),
3468 "{web_facts:?}"
3469 );
3470
3471 let api = atlas.components.iter().find(|c| c.name == "api").unwrap();
3473 assert!(
3474 api.symbols.contains(&"App".to_string()),
3475 "{:?}",
3476 api.symbols
3477 );
3478 assert!(
3479 api.symbols.contains(&"App.get".to_string()),
3480 "{:?}",
3481 api.symbols
3482 );
3483 assert!(
3484 api.implementation.contains(&"App".to_string()),
3485 "{:?}",
3486 api.implementation
3487 );
3488 assert_eq!(api.implementation_paths, vec!["api".to_string()]);
3489 assert_eq!(
3491 atlas.implementation_map.get("api").unwrap(),
3492 &vec!["api".to_string()]
3493 );
3494
3495 let api_owns: Vec<&str> = api.owns.iter().map(|o| o.target.as_str()).collect();
3498 assert!(
3499 api_owns.contains(&"App.cache"),
3500 "mutable field owns claim: {api_owns:?}"
3501 );
3502 assert!(
3503 api_owns.contains(&"DEBUG"),
3504 "config owns claim: {api_owns:?}"
3505 );
3506
3507 assert!(
3509 atlas.landmarks.len() <= 40,
3510 "landmarks bounded: {:?}",
3511 atlas.landmarks.len()
3512 );
3513 assert!(
3514 atlas.landmarks.iter().any(|l| l.contains("App")),
3515 "{:?}",
3516 atlas.landmarks
3517 );
3518
3519 let pack = render_atlas(&ctx, &atlas, usize::MAX, false);
3521 assert!(pack.content.contains("# PUBLIC API"), "{}", pack.content);
3522 assert!(
3523 pack.content.contains("# FRAMEWORK SEMANTICS"),
3524 "{}",
3525 pack.content
3526 );
3527 assert!(pack.content.contains("# LANDMARKS"), "{}", pack.content);
3528 assert!(
3529 pack.content.contains("api: exports App, include_router"),
3530 "{}",
3531 pack.content
3532 );
3533 }
3534
3535 #[test]
3536 fn pipeline_only_for_compiler_language_tool() {
3538 let (_dir, store) = fact_layer_store();
3539 let graph = scc_graph::RealityGraph::load(&store).unwrap();
3540 let ctx = ContextCompiler::new(
3541 &store,
3542 &graph,
3543 crate::ContextSettings::default(),
3544 Vec::new(),
3545 );
3546 let atlas = build_atlas(&ctx);
3547 assert!(atlas.pipeline.is_empty(), "{:?}", atlas.pipeline);
3549
3550 let parse_id = symbol_id(&store.repo_id, "api/app.py", "parse");
3552 let mut e = store
3553 .get_entity(&parse_id)
3554 .ok()
3555 .flatten()
3556 .unwrap_or_else(|| {
3557 let ent = Entity::new(parse_id.clone(), kinds::SYMBOL, "parse");
3558 store.insert_entity(&ent, &["api/app.py".into()]).unwrap();
3559 ent
3560 });
3561 e.attr("exported", serde_json::json!(true));
3562 store.insert_entity(&e, &["api/app.py".into()]).unwrap();
3563 let graph = scc_graph::RealityGraph::load(&store).unwrap();
3564 let ctx = ContextCompiler::new(
3565 &store,
3566 &graph,
3567 crate::ContextSettings::default(),
3568 Vec::new(),
3569 );
3570 let pipeline = build_pipeline(&ctx.view, Some(scc_core::Archetype::CompilerLanguageTool));
3571 assert!(
3572 pipeline.iter().any(|l| l.trim() == "parse"),
3573 "parse symbol in pipeline: {pipeline:?}"
3574 );
3575 assert!(
3576 pipeline.iter().any(|l| l == "[parse]"),
3577 "stage header: {pipeline:?}"
3578 );
3579 }
3580}