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