Skip to main content

scc_context/
atlas.rs

1//! System Atlas compiler (Wave 2): the full-system architecture artifact
2//! injected into coding agents at session start (docs/SYSTEM_DESIGN.md §8).
3//!
4//! Builds a structured [`scc_core::SystemAtlas`] from the trusted view, then
5//! renders it as compact structured text. The atlas is the product: the
6//! agent should know the architecture *before* its first coding task.
7//!
8//! Trust contract: every fact comes from the TrustedGraphView — STALE facts
9//! are excluded and surfaced as warnings; low-confidence inference is
10//! excluded unless `include_low_confidence_inference` is set.
11
12use 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/// Semantic compilation scope: which repository roles feed the
22/// architecture sections (entrypoints, contracts, state authority,
23/// flows, boundaries, deployment). Components and files ALWAYS list
24/// every role (labeled) — structure stays visible; only the inferred
25/// architecture is scoped, so a fixture route never becomes a global
26/// entrypoint and a benchmark DB writer never owns production state.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28// trace:v1 id=impl.scc.atlas-scope work=WORK-SI-MMMJA4G6 satisfies=REQ-SI-503JSBGP
29pub enum AtlasScope {
30    /// Default: production-role evidence only, plus test relationships
31    /// that explain production (TESTED_BY). Counts of scoped-out facts
32    /// land in the coverage map under `scope`.
33    Production,
34    /// Everything: benchmark/fixture archaeology, benchmark tasks.
35    Full,
36}
37
38/// Repository role of one entity: its `file` attribute (routes, symbols,
39/// contracts carry it), else its handler symbol's file, else a manifest
40/// pointer (`dockerfile`). Entities without any placement evidence are
41/// production — never drop facts we cannot place.
42// trace:exempt reason=internal-detail
43fn 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
72/// Role of an entity id (`production` when unknown — see [`entity_role`]).
73// trace:exempt reason=internal-detail
74fn 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
80/// Scope-filtered entity iteration: architecture sections use this
81/// instead of `view.entities_of_kind`. Structural kinds are never
82/// filtered by callers (components/files list every role, labeled).
83/// Dropped counts accumulate per section for the honesty receipt.
84/// Participant verdict for unattributed hubs (topics, configurations):
85/// `None` when no participant carries placement evidence (kept — never
86/// drop facts we cannot place), else whether ANY participant is production.
87/// A topic whose publishers/subscribers are ALL fixture files is fixture
88/// chatter, not architecture — even though the topic entity itself has no
89/// file attribute.
90// trace:exempt reason=internal-detail
91fn 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
111/// Actor placement: component actors resolve through the component
112/// role map, symbol actors through entity file evidence. `None` means
113/// unplaceable (kept — never drop flows we cannot place).
114// trace:exempt reason=internal-detail
115fn 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
126/// Flow keep rule: a flow with at least one production actor is
127/// architecture; a flow whose actors are ALL placed outside production
128/// is fixture/test choreography. Unplaceable flows are kept.
129// trace:exempt reason=internal-detail
130fn 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
150// trace:exempt reason=internal-detail
151fn 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
174/// Structured atlas compilation — pure data, no rendering. Default
175/// scope is [`AtlasScope::Production`]: fixture/test/benchmark evidence
176/// labels components but never feeds architecture sections.
177// trace:v1 id=impl.scc.atlas work=WORK-SCC-001 satisfies=REQ-state-function-access,REQ-SCC-CTX
178pub fn build_atlas(ctx: &ContextCompiler) -> SystemAtlas {
179    build_atlas_scoped(ctx, AtlasScope::Production)
180}
181
182/// [`build_atlas`] with an explicit scope. `Full` restores the unfiltered
183/// compilation for benchmark/fixture archaeology (`scc atlas --full`).
184// trace:v1 id=impl.scc.atlas-scoped work=WORK-SI-MMMJA4G6 satisfies=REQ-SCC-CTX
185pub 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    // ---- components ----
194    let mut components: Vec<AtlasComponent> = Vec::new();
195    // data stores / data entities written by component symbols (WRITES-derived).
196    // Collected per component so the global DATA STORES list can scope to
197    // production components: a benchmark DB writer never owns production state.
198    let mut data_stores: BTreeSet<String> = BTreeSet::new();
199    let mut comp_store_targets: Vec<(String, Vec<String>)> = Vec::new();
200    // Honesty receipt: per-section counts of architecture facts scoped out.
201    let mut scoped_out: BTreeMap<String, usize> = BTreeMap::new();
202    for c in view.components() {
203        // Purpose prefers evidence-backed claims (Declared, then Resolved)
204        // over bare inference; the trust view already strips low-confidence
205        // claims, and this keeps the render honest about what remains.
206        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        // the full implementation fact: directory paths AND member symbol
243        // names (the component compiler attributes every contained symbol).
244        // The structured model carries both; the render shows only the
245        // paths (`implementation_paths`) to stay compact.
246        // Role first: owns/data-stores scoping below needs it before the
247        // AtlasComponent literal is built.
248        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        // Ownership claims come from the component compiler's `owns` attr
310        // (write-edge derived + declared intent, provenance preserved).
311        // Claims targeting data-store / data-entity entities additionally
312        // surface in the atlas DATA STORES list, using the full store
313        // reference (`db.users`) so data entities stay attributed to their
314        // store.
315        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        // defensive dedupe: the same (target, provenance) pair may repeat
351        // across claims
352        let mut seen: BTreeSet<(String, String)> = BTreeSet::new();
353        owns.retain(|o| seen.insert((o.target.clone(), o.provenance.clone())));
354
355        // Ontology phase: hierarchical layer + immediate container (set by
356        // the component compiler's clusterer; defaulted for pre-ontology
357        // components so grouping stays total and deterministic).
358        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    // ---- scope: production component set ----
391    // Component roles are presentation AND scope: every component lists
392    // (structure stays visible), but owns/data-stores/public-api/framework
393    // sections only admit production components by default. Unknown
394    // components are kept — never drop facts we cannot place.
395    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    // Exact duplicates (same method+path+handler from overlapping evidence)
437    // collapse to one line; distinct handlers stay visible so genuinely
438    // ambiguous routes are never hidden.
439    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        // extractor contract: entrypoint kinds are strings ("main-guard",
475        // "cli-subcommand", ...); cli-subcommand entrypoints render as
476        // `name [cli-subcommand]` instead of the generic kind
477        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    // Wave 9: invocation-surface seeds (public exports → public_api, queue
490    // consumers → queue, framework callbacks → framework_callback,
491    // lifecycle callbacks → lifecycle, event handlers → event). Additive and
492    // deterministic (invocation_surfaces sorts its output). Deduped by
493    // (name, kind): a symbol that is several surfaces at once (exported AND
494    // callback registrar) renders under each kind — the atlas has no
495    // unique-id constraint.
496    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    // ---- contracts (Wave 9: first-class, typed) ----
520    let mut contracts: Vec<scc_core::Contract> = Vec::new();
521    let mut contract_seen: BTreeMap<(String, String), usize> = BTreeMap::new();
522
523    // http: ROUTE entities (producer = handler symbol)
524    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    // cli: SYMBOL entities carrying `cli_flags: ["--flag", ...]` attrs
570    // (producer = the owning symbol)
571    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    // event: TOPIC entities with PUBLISHES/SUBSCRIBES edges (producer = the
604    // topic; consumers = the publishing/subscribing symbols). Participant-
605    // scoped: an unattributed topic is judged by its publishers, not kept
606    // by default — fixture topics never reach the architecture sections.
607    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    // config: CONFIGURATION entities (producer = the owning symbol via
655    // CONFIGURED_BY; consumers = READS edges + the configured-by symbols)
656    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    // subclass contracts: CONTRACT entities carrying a first-class
723    // registration kind (serialization/extension/plugin/rpc/message/schema/
724    // call/factory/builder/...). Framework-specific registration kinds
725    // (`include_router`, `add_middleware`, ...) map to None and stay
726    // public-api: EXPORT entities whose export kind is a callable signature
727    // (function/method/constructor) — the "public fn signature" surface.
728    // The EXPORT entity's symbol is the EXPORTS edge subject; consumers are
729    // the symbols that call it.
730    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    // subclass contracts: CONTRACT entities carrying a first-class
783    // registration kind (serialization/extension/plugin/rpc/message/schema/
784    // call/factory/builder/...). Framework-specific registration kinds
785    // (`include_router`, `add_middleware`, ...) map to None and stay
786    // framework semantics (FRAMEWORK SEMANTICS), never first-class
787    // contracts. Annotations are per-symbol framework semantics too — they
788    // render under FRAMEWORK SEMANTICS, not here. Producer = the
789    // registering symbol (REGISTERS subject); consumers = symbols consuming
790    // the surface.
791    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    // schema: SCHEMA concepts (Wave 11/13) — the schema name, its composed
834    // parents (COMPOSES edges, schema→parent) and validation lines (the
835    // defining owner's VALIDATES edges) render per-subclass with the
836    // `schema:` prefix: `schema: User`, `schema: User extends Base`,
837    // `schema: User validates`. Producer = the most frequent occurrence
838    // owner (a symbol, never the concept/expr itself).
839    //
840    // Inline constructions (name == expr, the `z.object({...})` test and
841    // handler forms) are frequency-capped: only the *repeated* DSL surface
842    // (count >= 2, top 40 by count) renders — one-off test schemas are
843    // noise, not architecture, and would flood the contracts layer. The
844    // count is DERIVED from the live OCCURRENCE entities — never a stored,
845    // write-time-mutated counter.
846    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        // the defining expression (`z.object({ name: z.string() })`)
868        // renders as `schema: <name> = <expr>` when the extractor
869        // captured one — the concrete code form a human would quote.
870        // Inline constructions use the expression itself as the name
871        // (`schema: z.object({...})`); never render `X = X`.
872        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    // repeated inline DSL forms (count desc, id asc for determinism)
910    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    // deterministic order for the machine model: per-subclass groups,
933    // then by operation, then producer
934    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    // ---- flows: SEQUENCES project from the canonical FlowGraph (P1 §18);
943    // the old linear flows table is never the atlas's sequence source ----
944    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    // derived views (workflow/dataflow/lifecycle) still come from the
971    // derived compilers — but lifecycle is SIGNALS, never authoritative
972    for f in view.flows() {
973        if f.kind == scc_core::FlowKind::Sequence {
974            continue; // sequences come from the canonical graphs
975        }
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    // ---- invariants ----
1028    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    // ---- deployment / externals / trust boundaries ----
1038    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    // ---- implementation map ----
1076    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    // ---- evidence + warnings + freshness ----
1082    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    // ---- Ontology phase: archetype + STATE & DATA AUTHORITY + hierarchy ----
1114    let archetype = Some(scc_graph::archetype::detect_archetype(view.graph, store));
1115
1116    // symbol -> component name over the *stored* components (the state
1117    // compiler attributes ownership per component from the fact layer)
1118    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    // State lines read `{comp} owns/reads …` / `{comp}::{sym} …`: the
1128    // leading component attributes the claim. Non-production components
1129    // never own production state in the default scope.
1130    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    // hierarchical containers: services first, then subsystems; members are
1145    // direct member entity ids (component ids or nested subsystem ids)
1146    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    // ---- STATE & DATA AUTHORITY: structured bridge ----
1167    // The state compiler's per-component claims (mutable fields, STATE/
1168    // REGISTRY entities, configuration targets, topics, middleware/registry
1169    // registrations, store writes) become component `owns` claims too, so
1170    // the state fact layer is part of the machine model — not just rendered
1171    // text. Provenance preserved; deduped by (target, provenance).
1172    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    // ---- PUBLIC API (Wave 10): exports grouped by component ----
1214    // EXPORT entities (extractor-emitted public-export facts) plus symbols
1215    // the extractor statically marked `exported: true` at module level.
1216    // Grouped by the exporting symbol's component.
1217    let mut public_api: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
1218    // EXPORT entities: the exporting symbol is the EXPORTS relationship
1219    // subject; its name matches the export entity name.
1220    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    // exported module-level symbols (`exported: true`, no `.` in the name)
1238    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    // ---- FRAMEWORK SEMANTICS (Wave 10): annotations / registrations /
1263    // callbacks grouped by component ----
1264    let mut framework_semantics: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
1265    // annotations: ANNOTATION entity ANNOTATES target symbol
1266    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    // REGISTERS + HANDLES_CALLBACK: symbol -> target
1284    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    // ---- PIPELINE (Wave 10): phase-named symbols grouped by stage ----
1324    // Rendered only for the CompilerLanguageTool archetype: symbols whose
1325    // name contains a phase verb, plus phase-named files (`1-parse`-style
1326    // stage directories). Grouped by stage; bounded.
1327    let pipeline = build_pipeline(view, archetype);
1328
1329    // ---- LANDMARKS (Wave 10): notable exports + annotated targets,
1330    // bounded (~40) ----
1331    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
1386/// Phase-stage verbs for the PIPELINE section (CompilerLanguageTool
1387/// archetype): a symbol whose name contains a stage verb is a phase symbol.
1388const 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
1416/// PIPELINE (Wave 10): phase-named symbols grouped by stage, plus
1417/// phase-named file paths (`1-parse`-style stage dirs). Only rendered for
1418/// the CompilerLanguageTool archetype. Deterministic: sorted by
1419/// (stage-rank, name); bounded to keep the section compact.
1420// trace:exempt reason=internal-detail
1421fn 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    // phase-named symbols (module-level and method symbols)
1434    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    // phase-named files: `phases/1-parse/index.js` or a `1-parse`-style
1447    // directory segment, or a path segment containing a stage verb
1448    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            // digit-prefixed stage dirs: `1-parse`, `2-analyze`, `3-transform`
1454            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); // compiler phase tree without a matched verb
1468        }
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
1489/// LANDMARKS (Wave 10): notable exports + annotated targets, bounded (~40).
1490/// Exports: the component-sorted public API, preferring classes then
1491/// functions, capped. Annotated targets: symbols an ANNOTATION/REGISTERS
1492/// fact targets (framework-decorated code). Deterministic: sorted.
1493// trace:exempt reason=internal-detail
1494fn 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    // notable exports: exported classes first, then other exports, capped
1505    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    // annotated targets (framework-decorated symbols), capped.
1537    // Component-scoped: a test-only decorator target is not a landmark.
1538    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
1566/// Push one contract, merging consumers/evidence when the same
1567/// (subclass, operations) surface was already recorded (e.g. the same CLI
1568/// flag owned by two symbols). Deterministic: consumers/evidence stay
1569/// sorted.
1570fn 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
1596/// Languages with a real extractor come from `LANGUAGE_REGISTRY`. Files in
1597/// any other language are scanned but never parsed — the honest `unparsed`
1598/// remainder of the coverage map.
1599// trace:exempt reason=internal-detail
1600fn is_extractor_language(lang: &str) -> bool {
1601    language_by_id(lang).is_some_and(|c| c.extractor)
1602}
1603
1604/// Deterministic model-coverage facts (Wave 9): what the model knows AND
1605/// what it does not. Every line is computed from the trusted view + store —
1606/// no heuristics, no fabrication; when a quantity is unobservable the line
1607/// says so explicitly.
1608// trace:exempt reason=internal-detail
1609fn 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    // ---- parsed source files % ----
1615    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    // ---- exported API identified ----
1631    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    // ---- call targets resolved % ----
1652    // RESOLVED (compiler/LSP proof) + EXTRACTED calls with a target that
1653    // resolves to an existing entity (symbol or external API), over every
1654    // stored CALLS edge. Unresolved calls are never persisted, so the
1655    // interesting limit is the LSP-vs-candidate split plus the
1656    // external/dynamic receiver count below.
1657    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    // ---- dynamic receivers unresolved ----
1690    // Calls whose target is not a local symbol (external/dynamic receivers)
1691    // are stored with the external target; unknown-receiver calls are not
1692    // persisted at all — reported honestly as such.
1693    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    // ---- invocation surfaces ----
1711    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    // ---- framework registrations unknown ----
1723    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    // ---- stale evidence ----
1734    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    // ---- unparsed files ----
1745    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    // ---- model epoch generations ----
1755    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
1778/// Render the atlas as compact structured text (agent-facing).
1779/// `full` is the human `--unbounded` mode: soft legacy render that may
1780/// exceed the budget (reported, never silent). Agent/MCP paths always
1781/// pass false.
1782// trace:v1 id=impl.scc.atlas.render work=WORK-SCC-001 satisfies=REQ-state-function-access,REQ-SCC-CTX
1783pub 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    // SYSTEM PURPOSE (never cut); the ARCHETYPE header is the ontology
1793    // phase's one-line classification of the repository.
1794    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        // bounded render: the full structured list stays in the machine
1811        // model; the agent-facing artifact caps the listing (a framework
1812        // repo can have thousands of export surfaces).
1813        const EP_RENDER_CAP: usize = 200;
1814        for e in atlas.entrypoints.iter().take(EP_RENDER_CAP) {
1815            // Framework-surface kinds render as compact `kind: name` lines
1816            // (`queue: consume_order`, `schedule: daily_job`,
1817            // `plugin: register_hook`, `lifecycle: @BeforeAll` — the
1818            // lifecycle line names the hook annotation). Classic
1819            // http/cli/public_api/route/entrypoint lines keep the
1820            // `name [kind] — trigger` form.
1821            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    // ARCHITECTURE (component blocks), grouped by layer: services first,
1852    // then subsystems, then unmerged components, code-regions last — with
1853    // `parent` indentation under service/subsystem headers. The flat block
1854    // format is preserved inside each group.
1855    let mut arch = String::new();
1856    let mut rendered: BTreeSet<String> = BTreeSet::new(); // names under containers
1857    let comp_block = |c: &AtlasComponent, indent: &str| -> String {
1858        let mut out = format!("\n{}{}", indent, c.name.to_uppercase());
1859        // Role scoping at the render boundary: test/fixture/benchmark
1860        // trees stay visible as structure but are never mistaken for
1861        // production architecture. Production renders unlabeled.
1862        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    // services first: nested subsystems, then directly-contained components
1907    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; // subsystems rendered above
1930            }
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    // standalone subsystems (not nested inside a service)
1940    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    // unmerged components (evidence-backed), then bare code regions
1960    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    // PRIMARY FLOWS (never cut); the architecture view is the ARCHITECTURE
1974    // section itself — skip it here to avoid duplicating the system. The
1975    // rendered section is bounded so a chain-rich repo's FLOWS view stays
1976    // compact: the deepest flows (most step lines) render first, capped at
1977    // FLOW_RENDER_CAP flows and FLOW_RENDER_STEP_CAP lines each. The
1978    // machine model (`atlas.flows`) carries the full inventory, so the
1979    // structured behavior layer is unaffected by the render cap.
1980    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    // STATE & DATA AUTHORITY (never cut): six subsections — DATA
2008    // OWNERSHIP (persistent: the write-derived + declared owns claims and
2009    // the DATA STORES list), RUNTIME STATE, REACTIVE STATE,
2010    // CONFIGURATION, CACHES, DERIVED / REGISTRIES. Falls back to the
2011    // legacy DATA OWNERSHIP title when the state compiler found no state
2012    // at all.
2013    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    // CONTRACTS (never cut) — rendered as per-subclass groups: one
2066    // `{subclass}: {operation}` line per operation, sorted so each subclass
2067    // family (http/cli/event/config/public-api/extension/serialization/...)
2068    // clusters together. Preserves the classic contract strings (route
2069    // `GET /api/x`, flag `--paging`, event `user.created`, config key
2070    // `DEBUG`) so pre-Wave-9 consumers keep matching.
2071    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    // PUBLIC API (Wave 10): exports grouped by component — compact
2090    // `component: exports A, B, C` lines from the semantic fact layer
2091    // (EXPORT entities + exported module-level symbols). Per-component
2092    // render is bounded (the structured model carries the full list).
2093    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    // FRAMEWORK SEMANTICS (Wave 10): annotations on targets,
2121    // route/bean/middleware registrations, lifecycle callbacks — grouped
2122    // by component. Per-component render is bounded.
2123    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    // PIPELINE (Wave 10, CompilerLanguageTool archetype): phase-named
2144    // symbols grouped by stage.
2145    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    // LANDMARKS (Wave 10, priority 5 — bounded ~40): notable exports and
2156    // annotated targets, one zoom level deeper than the component list.
2157    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    // CRITICAL INVARIANTS (never cut)
2170    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    // FAILURE / RETRY (never cut)
2181    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    // DEPLOYMENT
2190    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    // TRUST BOUNDARIES
2201    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    // ASYNC BOUNDARIES
2212    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    // EXTERNAL SYSTEMS
2223    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    // IMPLEMENTATION MAP
2234    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    // EVIDENCE STATUS
2243    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    // RUNTIME (Wave 6): observed trace-path signatures + three-way drift
2259    // (declared vs static vs observed). Priority 8 — droppable before any
2260    // critical section, so a tight budget never hides invariants.
2261    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        // store order: (count DESC, signature) — deterministic top-10
2266        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    // MODEL COVERAGE (Wave 9): the explicit uncertainty/coverage map — what
2295    // the model knows AND what it does not. Priority 7: droppable before
2296    // any critical section, so a tight budget never hides invariants.
2297    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
2321/// Project one canonical FlowGraph into ordered step lines for the atlas.
2322/// Walks from the entrypoints along POLICY-ALLOWED edges (the trust view's
2323/// provenance policy applies to derived edges too), marking edge kinds:
2324/// branch / retry / error / async / publish / consume / join. Never
2325/// flattens alternate paths into false sequential causality.
2326// trace:exempt reason=internal-detail
2327fn 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
2434// trace:exempt reason=internal-detail
2435fn 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    // trace:exempt reason=internal-detail
2472    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        // files: 2 parsed python + 1 unparsed json
2480        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        // symbols
2491        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        // cli flags on worker
2498        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        // route with handler
2506        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        // export: handler EXPORTS export(handler, function)
2526        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        // configuration DEBUG configured-by reader
2544        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        // calls: one EXTRACTED to a local symbol, one EXTRACTED to a missing
2565        // external target, one RESOLVED (LSP proof)
2566        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        // topic jobs + worker SUBSCRIBES
2605        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    // trace:v1 id=test.scc.context.atlas-production-scope verifies=REQ-SCC-CTX exercises=impl.scc.atlas-scoped
2637    fn atlas_production_scope_keeps_fixture_routes_out() {
2638        // The pollution case: fixtures/foo/app.py exposes GET /admin. The
2639        // default atlas must not list it as an entrypoint or contract, the
2640        // fixture component must still list (labeled), and --full restores it.
2641        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        // Re-list components: fact_layer_store set [api, web]; rebuild the
2664        // same two plus fx (replace_components takes the full list).
2665        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    // trace:exempt reason=internal-detail
2726    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        // contract subclasses: http (route), cli (flags), config (DEBUG),
2738        // event (topic jobs), public-api (exported function signature) —
2739        // no annotations in this fixture
2740        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        // every contract carries the typed subclass, and the machine-model
2755        // kind agrees with the subclass render prefix
2756        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        // rendered CONTRACTS lines are `{subclass}: {operation}` per group
2791        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        // coverage map: honest, deterministic numbers from the store
2814        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        // 3 calls: 2 with existing targets (worker symbol, reader symbol),
2832        // 1 external target with no entity → 66%, 1 LSP-RESOLVED
2833        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        // invocation surfaces: public_api (handler) + queue (worker) +
2842        // http (handler via route) + cli (worker cli_flags)
2843        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        // atlas entrypoints carry the surface kinds
2859        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    // trace:exempt reason=internal-detail
2867
2868    /// Wave 11: schema contracts (SCHEMA entities + DEFINES/COMPOSES/
2869    /// VALIDATES edges) render under CONTRACTS with the `schema:` prefix,
2870    /// and reactive state (REACTIVE entities + OWNS edges) renders under
2871    /// STATE & DATA AUTHORITY's REACTIVE STATE subsection, attributed to
2872    /// the owning symbol's component.
2873    #[test]
2874
2875    // trace:exempt reason=internal-detail
2876    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        // component api (api/app.py) with symbol UserService — components
2884        // live in the `components` table (RealityGraph::load reads
2885        // store.components())
2886        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        // schema User (UserService DEFINES it, COMPOSES Base, VALIDATES
2934        // CreateUser) with one occurrence owned by the User symbol
2935        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        // reactive state count: one concept, one occurrence owned by
3015        // UserService (Wave 13 — identity is per concept/path/owner/line)
3016        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        // schema contract: name + composition + validation operations
3074        // (find the User schema — the Base schema is a plain name-only
3075        // contract)
3076        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        // Wave 13 (e): the producer is the occurrence owner symbol — never
3095        // the concept/expr itself
3096        assert_eq!(
3097            schema.producer, "User",
3098            "producer = owner symbol: {schema:?}"
3099        );
3100
3101        // reactive state attributed to the owning symbol's component
3102        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        // rendered atlas lines
3115        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    /// A repo with component-attributed symbols exercising the Wave 10
3132    /// fact-layer sections: exported classes/methods, module exports,
3133    /// annotations, registrations, callbacks, and state facts.
3134    // trace:exempt reason=internal-detail
3135    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        // components api (api/app.py) and web (web/app.py)
3143        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        // components live in the `components` table (RealityGraph::load
3169        // reads store.components()) — replace with the full list, carrying
3170        // the component compiler's `implementation` fact (paths + member
3171        // symbols).
3172        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        // exported class `App` with public method `App.get` (framework class)
3244        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        // exported module-level function + underscore-private one
3255        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        // web component symbol
3266        let web_handle = mk_sym(
3267            "web/app.py",
3268            "handle_page",
3269            serde_json::json!({"kind": "function", "exported": true}),
3270        );
3271
3272        // EXPORT entity for include_router (EXPORTS edge)
3273        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        // annotation: @Get on App.get
3295        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        // registration: App registers middleware
3316        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        // callback: web_handle HANDLES_CALLBACK on_message (a SYMBOL target)
3337        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        // state facts: mutable field CONTAINS-ed by App + config
3356        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    // trace:exempt reason=internal-detail
3403    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        // PUBLIC API grouped by component: api has App (exported class) +
3415        // include_router (export entity + module export); web has
3416        // handle_page. `_secret` is excluded (leading underscore).
3417        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        // FRAMEWORK SEMANTICS: annotation, registration, callback per comp
3434        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        // component implementation carries member symbols; paths stay pure
3456        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        // paths-only in the implementation map (compact render)
3474        assert_eq!(
3475            atlas.implementation_map.get("api").unwrap(),
3476            &vec!["api".to_string()]
3477        );
3478
3479        // STATE & DATA AUTHORITY structured bridge: mutable field + config
3480        // targets surface as component owns claims
3481        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        // LANDMARKS bounded: exports (App, include_router, handle_page)
3492        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        // rendered atlas carries the new section headers
3504        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    // trace:exempt reason=internal-detail
3521    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        // not a compiler repo → no pipeline lines
3532        assert!(atlas.pipeline.is_empty(), "{:?}", atlas.pipeline);
3533
3534        // phase-named symbols group by stage when the archetype fires
3535        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}