Skip to main content

scc_core/
lib.rs

1//! System IR core types.
2//!
3//! These types mirror `docs/system-ir.schema.json` exactly so that a `SystemIr`
4//! document serializes to the documented export format without translation.
5
6use serde::{Deserialize, Serialize};
7use std::collections::{BTreeMap, BTreeSet};
8
9pub const SCHEMA_VERSION: &str = "0.1.0";
10
11pub mod handles;
12pub mod identity;
13pub mod languages;
14pub mod lex;
15pub mod resolution;
16pub mod retrieval;
17
18pub use handles::{fnv1a64_hex, ContentHandle, HandleError, HandleKind};
19pub use languages::{
20    extracted_language_ids, language_by_id, language_registry, support_matrix_markdown,
21    LanguageCapability, LanguageTier, LANGUAGE_REGISTRY,
22};
23pub use lex::{
24    bm25_rank, bm25_scores, bm25_scores_with_stats, classify_query, extract_query_mentions,
25    is_exact_anchor, mention_matches_doc, path_matches_locus, ranking_arm_ids, relevance_hits,
26    relevance_hits_with_stats, route_query, subtokens, Bm25CorpusStats, LexDoc, LexField, QueryLocus,
27    QueryMention, QueryShape,
28    RankingArm, RelevanceHit, RetrievalPlan, BM25_B, BM25_K1, QUERY_MENTION_MAX_RAW, WEIGHT_BODY,
29    WEIGHT_DOC, WEIGHT_NAME, WEIGHT_PATH,
30};
31pub use resolution::{
32    choose_representation, AnalysisQuality, CallQuality, FileQuality, RecvKind,
33    RepresentationChoice, RepresentationKind, ResolutionClass,
34};
35pub use retrieval::{mean_reciprocal_rank, recall_at_k};
36
37// ---------------------------------------------------------------------------
38// Provenance
39// ---------------------------------------------------------------------------
40
41// trace:exempt reason=internal-detail
42
43/// Evidence class of a fact, per docs/SYSTEM_IR_SCHEMA.md §5.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)]
45#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
46// trace:v1 id=impl.crates-scc-core-src-lib.Provenance work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
47pub enum Provenance {
48    /// Direct syntax/configuration evidence.
49    Extracted,
50    /// Resolved through compiler/LSP/type/binding resolution.
51    Resolved,
52    /// Runtime evidence.
53    Observed,
54    /// Declared architectural intent.
55    Declared,
56    /// Heuristic/LLM claim.
57    Inferred,
58    /// Evidence no longer valid for the active revision.
59    Stale,
60}
61
62// trace:exempt reason=internal-detail
63impl Provenance {
64// trace:v1 id=impl.crates-scc-core-src-lib-provenance.as-str work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
65    pub fn as_str(&self) -> &'static str {
66        match self {
67            Provenance::Extracted => "EXTRACTED",
68            Provenance::Resolved => "RESOLVED",
69            Provenance::Observed => "OBSERVED",
70            Provenance::Declared => "DECLARED",
71            Provenance::Inferred => "INFERRED",
72            Provenance::Stale => "STALE",
73        }
74    }
75
76    /// Default confidence per docs/SYSTEM_IR_SCHEMA.md §9.
77// trace:v1 id=impl.crates-scc-core-src-lib-provenance.default-confidence work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
78    pub fn default_confidence(&self) -> f64 {
79        match self {
80            Provenance::Extracted => 1.0,
81            Provenance::Resolved => 0.98,
82            Provenance::Observed => 1.0,
83            Provenance::Declared => 1.0,
84            Provenance::Inferred => 0.7,
85            Provenance::Stale => 0.0,
86        }
87    }
88
89    /// STALE facts may never enter trusted context (only as warnings).
90// trace:v1 id=impl.crates-scc-core-src-lib-provenance.is-trusted work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
91    pub fn is_trusted(&self) -> bool {
92        !matches!(self, Provenance::Stale)
93    }
94}
95
96// ---------------------------------------------------------------------------
97// Severity / kinds
98// ---------------------------------------------------------------------------
99
100// trace:exempt reason=internal-detail
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
103#[serde(rename_all = "lowercase")]
104// trace:exempt reason=internal-detail
105// trace:v1 id=impl.crates-scc-core-src-lib.Severity work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
106pub enum Severity {
107    Info,
108    Low,
109    Medium,
110    High,
111    Critical,
112}
113
114// trace:exempt reason=internal-detail
115impl Severity {
116// trace:v1 id=impl.crates-scc-core-src-lib-severity.rank work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
117    pub fn rank(&self) -> u8 {
118        match self {
119            Severity::Info => 0,
120            Severity::Low => 1,
121            Severity::Medium => 2,
122            Severity::High => 3,
123            Severity::Critical => 4,
124        }
125    }
126}
127
128// trace:exempt reason=internal-detail
129
130/// Flow view kinds (System Atlas), per docs/SYSTEM_IR_SCHEMA.md §7.
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
132#[serde(rename_all = "lowercase")]
133// trace:exempt reason=internal-detail
134// trace:v1 id=impl.crates-scc-core-src-lib.FlowKind work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
135pub enum FlowKind {
136    Architecture,
137    Workflow,
138    Sequence,
139    Dataflow,
140    Lifecycle,
141}
142
143// trace:exempt reason=internal-detail
144impl FlowKind {
145// trace:v1 id=impl.crates-scc-core-src-lib-flowkind.as-str work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
146    pub fn as_str(&self) -> &'static str {
147        match self {
148            FlowKind::Architecture => "architecture",
149            FlowKind::Workflow => "workflow",
150            FlowKind::Sequence => "sequence",
151            FlowKind::Dataflow => "dataflow",
152            FlowKind::Lifecycle => "lifecycle",
153        }
154    }
155}
156
157// trace:v1 id=impl.crates-scc-core-src-lib.flow-kind-str work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
158pub fn flow_kind_str(k: &FlowKind) -> &'static str {
159    k.as_str()
160}
161
162/// Evidence source type.
163#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
164#[serde(rename_all = "lowercase")]
165// trace:v1 id=impl.crates-scc-core-src-lib.EvidenceType work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
166pub enum EvidenceType {
167    Source,
168    Config,
169    Runtime,
170    Test,
171    Intent,
172    History,
173}
174
175// ---------------------------------------------------------------------------
176// Archetype (Ontology phase — deterministic repo classification)
177// ---------------------------------------------------------------------------
178
179// trace:exempt reason=internal-detail
180
181/// Repository archetype, detected deterministically from graph evidence
182/// (routes, exports, cli/framework signals, deployment/workspace shape) by
183/// `scc_graph::archetype::detect_archetype`. `Unknown` is the honest
184/// fallback when no signal fires.
185#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, schemars::JsonSchema)]
186#[serde(rename_all = "snake_case")]
187// trace:exempt reason=internal-detail
188// trace:v1 id=impl.crates-scc-core-src-lib.Archetype work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
189pub enum Archetype {
190    /// HTTP routes + deployment units, no library-scale export ratio.
191    ServiceApplication,
192    /// cli-subcommand entrypoints or main fns with clap/cobra/argparse.
193    Cli,
194    /// Exported-symbol ratio over total symbols > 0.5, few/no routes.
195    LibrarySdk,
196    /// Routes + framework registrations + middleware facts.
197    WebFramework,
198    /// parse/analyze/transform/generate-style phase symbols.
199    CompilerLanguageTool,
200    /// plugin/middleware/DI registrations dominating.
201    PluginFramework,
202    /// docker/k8s/terraform manifests + deployment units, few app symbols.
203    InfrastructureProject,
204    /// workspace packages >= 3 + multiple deployment units.
205    MonorepoPlatform,
206    /// No signal fired.
207    Unknown,
208}
209
210// trace:exempt reason=internal-detail
211impl Archetype {
212// trace:v1 id=impl.crates-scc-core-src-lib-archetype.as-str work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
213    pub fn as_str(&self) -> &'static str {
214        match self {
215            Archetype::ServiceApplication => "service_application",
216            Archetype::Cli => "cli",
217            Archetype::LibrarySdk => "library_sdk",
218            Archetype::WebFramework => "web_framework",
219            Archetype::CompilerLanguageTool => "compiler_language_tool",
220            Archetype::PluginFramework => "plugin_framework",
221            Archetype::InfrastructureProject => "infrastructure_project",
222            Archetype::MonorepoPlatform => "monorepo_platform",
223            Archetype::Unknown => "unknown",
224        }
225    }
226
227// trace:v1 id=impl.crates-scc-core-src-lib-archetype.label work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
228    pub fn label(&self) -> &'static str {
229        match self {
230            Archetype::ServiceApplication => "service application",
231            Archetype::Cli => "cli",
232            Archetype::LibrarySdk => "library/sdk",
233            Archetype::WebFramework => "web framework",
234            Archetype::CompilerLanguageTool => "compiler/language tool",
235            Archetype::PluginFramework => "plugin framework",
236            Archetype::InfrastructureProject => "infrastructure project",
237            Archetype::MonorepoPlatform => "monorepo platform",
238            Archetype::Unknown => "unknown",
239        }
240    }
241
242    /// All archetypes in the deterministic tie-break precedence order
243    /// (first entry wins a score tie).
244    pub const PRECEDENCE: [Archetype; 9] = [
245        Archetype::MonorepoPlatform,
246        Archetype::InfrastructureProject,
247        Archetype::WebFramework,
248        Archetype::ServiceApplication,
249        Archetype::Cli,
250        Archetype::LibrarySdk,
251        Archetype::CompilerLanguageTool,
252        Archetype::PluginFramework,
253        Archetype::Unknown,
254    ];
255}
256
257// ---------------------------------------------------------------------------
258// Core records
259// ---------------------------------------------------------------------------
260
261#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
262// trace:v1 id=impl.crates-scc-core-src-lib.Repository work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
263pub struct Repository {
264    pub id: String,
265    pub name: String,
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub url: Option<String>,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
271// trace:v1 id=impl.crates-scc-core-src-lib.Snapshot work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
272pub struct Snapshot {
273    pub revision: String,
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub branch: Option<String>,
276    pub indexed_at: String,
277}
278
279// trace:exempt reason=internal-detail
280
281#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
282// trace:exempt reason=internal-detail
283// trace:v1 id=impl.crates-scc-core-src-lib.Entity work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
284pub struct Entity {
285    pub id: String,
286    pub kind: String,
287    pub name: String,
288    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
289    pub attributes: BTreeMap<String, serde_json::Value>,
290    #[serde(default, skip_serializing_if = "Vec::is_empty")]
291    pub evidence: Vec<String>,
292}
293
294// trace:exempt reason=internal-detail
295impl Entity {
296// trace:v1 id=impl.crates-scc-core-src-lib-entity.new work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
297    pub fn new(id: impl Into<String>, kind: impl Into<String>, name: impl Into<String>) -> Self {
298        Entity {
299            id: id.into(),
300            kind: kind.into(),
301            name: name.into(),
302            attributes: BTreeMap::new(),
303            evidence: Vec::new(),
304        }
305    }
306
307// trace:v1 id=impl.crates-scc-core-src-lib-entity.attr work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
308    pub fn attr(&mut self, key: &str, value: impl Into<serde_json::Value>) -> &mut Self {
309        self.attributes.insert(key.to_string(), value.into());
310        self
311    }
312}
313
314// trace:exempt reason=internal-detail
315
316/// One concrete occurrence of a concept (schema/reactive) in a source file.
317///
318/// Concept entities (SCHEMA/REACTIVE) are keyed globally by (kind, name) —
319/// the same `z.object({...})` in A.ts and B.ts is ONE concept. Occurrences
320/// carry per-(concept, path, owner, line) identity instead: each file's
321/// occurrence survives independently, so provenance (`sources`) and the
322/// derived occurrence count never collapse and a purge of one path never
323/// deletes an occurrence another path still has.
324#[derive(Debug, Clone, Serialize, Deserialize)]
325// trace:exempt reason=internal-detail
326// trace:v1 id=impl.crates-scc-core-src-lib.Occurrence work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
327pub struct Occurrence {
328    /// Stable entity id (see [`occurrence_id`]).
329    pub id: String,
330    /// The concept entity id this occurrence belongs to.
331    pub concept: String,
332    /// Repository-relative source path.
333    pub path: String,
334    /// Owning symbol name.
335    pub owner: String,
336    /// Deterministic site line (owning symbol's start line when the
337    /// extractor facts carry no line).
338    pub line: u32,
339}
340
341// trace:exempt reason=internal-detail
342
343#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
344// trace:exempt reason=internal-detail
345// trace:v1 id=impl.crates-scc-core-src-lib.Relationship work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
346pub struct Relationship {
347    pub id: String,
348    pub subject: String,
349    pub predicate: String,
350    pub object: String,
351    pub provenance: Provenance,
352    pub confidence: f64,
353    #[serde(default, skip_serializing_if = "Vec::is_empty")]
354    pub evidence: Vec<String>,
355    #[serde(default, skip_serializing_if = "String::is_empty")]
356    pub verified_at: String,
357}
358
359// trace:exempt reason=internal-detail
360impl Relationship {
361// trace:v1 id=impl.crates-scc-core-src-lib-relationship.new work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
362    pub fn new(
363        id: impl Into<String>,
364        subject: impl Into<String>,
365        predicate: impl Into<String>,
366        object: impl Into<String>,
367        provenance: Provenance,
368    ) -> Self {
369        Relationship {
370            id: id.into(),
371            subject: subject.into(),
372            predicate: predicate.into(),
373            object: object.into(),
374            provenance,
375            confidence: provenance.default_confidence(),
376            evidence: Vec::new(),
377            verified_at: String::new(),
378        }
379    }
380
381// trace:v1 id=impl.crates-scc-core-src-lib-relationship.with-confidence work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
382    pub fn with_confidence(mut self, c: f64) -> Self {
383        self.confidence = c;
384        self
385    }
386
387// trace:v1 id=impl.crates-scc-core-src-lib-relationship.with-evidence work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
388    pub fn with_evidence(mut self, evidence: Vec<String>) -> Self {
389        self.evidence = evidence;
390        self
391    }
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
395// trace:v1 id=impl.crates-scc-core-src-lib.FlowStep work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
396pub struct FlowStep {
397    pub id: String,
398    pub order: u32,
399    pub actor: String,
400    pub operation: String,
401    #[serde(default, skip_serializing_if = "Option::is_none")]
402    pub condition: Option<String>,
403    #[serde(default, skip_serializing_if = "Option::is_none")]
404    pub r#async: Option<bool>,
405    #[serde(default, skip_serializing_if = "Option::is_none")]
406    pub timeout_ms: Option<u64>,
407    #[serde(default, skip_serializing_if = "Option::is_none")]
408    pub retry_policy: Option<String>,
409    #[serde(default, skip_serializing_if = "Option::is_none")]
410    pub failure_outcome: Option<String>,
411    #[serde(default)]
412    pub provenance: Option<Provenance>,
413    #[serde(default, skip_serializing_if = "Vec::is_empty")]
414    pub evidence: Vec<String>,
415}
416
417#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
418// trace:v1 id=impl.crates-scc-core-src-lib.Flow work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
419pub struct Flow {
420    pub id: String,
421    pub kind: FlowKind,
422    pub name: String,
423    #[serde(default, skip_serializing_if = "Option::is_none")]
424    pub trigger: Option<String>,
425    pub steps: Vec<FlowStep>,
426    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
427    pub attributes: BTreeMap<String, serde_json::Value>,
428}
429
430#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
431// trace:v1 id=impl.crates-scc-core-src-lib.Invariant work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
432pub struct Invariant {
433    pub id: String,
434    pub statement: String,
435    pub severity: Severity,
436    #[serde(default, skip_serializing_if = "Vec::is_empty")]
437    pub scope: Vec<String>,
438    #[serde(default, skip_serializing_if = "Vec::is_empty")]
439    pub enforced_by: Vec<String>,
440    #[serde(default)]
441    pub provenance: Option<Provenance>,
442    #[serde(default, skip_serializing_if = "Vec::is_empty")]
443    pub evidence: Vec<String>,
444}
445
446// trace:exempt reason=internal-detail
447
448#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
449// trace:exempt reason=internal-detail
450// trace:v1 id=impl.crates-scc-core-src-lib.Evidence work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
451pub struct Evidence {
452    pub id: String,
453    #[serde(rename = "type")]
454    pub r#type: EvidenceType,
455    #[serde(default, skip_serializing_if = "Option::is_none")]
456    pub path: Option<String>,
457    #[serde(default, skip_serializing_if = "Option::is_none")]
458    pub symbol: Option<String>,
459    #[serde(default, skip_serializing_if = "Option::is_none")]
460    pub start_line: Option<u32>,
461    #[serde(default, skip_serializing_if = "Option::is_none")]
462    pub end_line: Option<u32>,
463    #[serde(default, skip_serializing_if = "Option::is_none")]
464    pub revision: Option<String>,
465    #[serde(default, skip_serializing_if = "Option::is_none")]
466    pub content_hash: Option<String>,
467    #[serde(default, skip_serializing_if = "Option::is_none")]
468    pub extractor: Option<String>,
469    #[serde(default, skip_serializing_if = "Option::is_none")]
470    pub extractor_version: Option<String>,
471}
472
473// trace:exempt reason=internal-detail
474impl Evidence {
475// trace:v1 id=impl.crates-scc-core-src-lib-evidence.source work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
476    pub fn source(id: impl Into<String>, path: impl Into<String>) -> Self {
477        Evidence {
478            id: id.into(),
479            r#type: EvidenceType::Source,
480            path: Some(path.into()),
481            symbol: None,
482            start_line: None,
483            end_line: None,
484            revision: None,
485            content_hash: None,
486            extractor: None,
487            extractor_version: None,
488        }
489    }
490}
491
492// ---------------------------------------------------------------------------
493// Canonical Causal FlowGraph (Wave 3 — the behavioral truth)
494// ---------------------------------------------------------------------------
495
496/// Edge kind in the canonical causal graph (P1, docs/SYSTEM_DESIGN.md §9).
497#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
498#[serde(rename_all = "lowercase")]
499// trace:v1 id=impl.crates-scc-core-src-lib.FlowEdgeKind work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
500pub enum FlowEdgeKind {
501    /// Sequential causality.
502    Next,
503    /// Alternative execution path (fanout from evidence: call fanout,
504    /// exception handlers, task spawn, runtime trace variants, declared).
505    Branch,
506    /// Failure edge to an error/exception handler or failure outcome.
507    Error,
508    /// Retry edge (back to the retried operation).
509    Retry,
510    /// Fallback edge to the degraded path.
511    /// DEFERRED (§25): no reliable extractor evidence; not produced.
512    Fallback,
513    /// Asynchronous dispatch.
514    Async,
515    /// Message/event publication.
516    Publish,
517    /// Message/event consumption.
518    Consume,
519    /// Convergence of concurrent paths.
520    Join,
521    /// Return/terminal edge.
522    /// DEFERRED: subsumed by Next topology; not produced as a kind.
523    Return,
524    /// Timeout edge.
525    /// DEFERRED (§25): no reliable extractor evidence; not produced.
526    Timeout,
527    /// Compensation/rollback edge.
528    /// DEFERRED (§25): no reliable extractor evidence; not produced.
529    Compensation,
530    /// State/data read.
531    Read,
532    /// State/data write.
533    Write,
534    /// Data transformation.
535    /// DEFERRED: subsumed by call-chain topology; not produced as a kind.
536    Transform,
537    /// Validation/schema check.
538    /// DEFERRED: no reliable extractor evidence; not produced.
539    Validate,
540    /// Authorization/policy gate.
541    /// DEFERRED: trust-boundary analysis covers this; not a flow kind.
542    Authorize,
543    /// Cache lookup.
544    /// DEFERRED: no reliable extractor evidence; not produced.
545    Cache,
546    /// Cache/state invalidation.
547    /// DEFERRED: no reliable extractor evidence; not produced.
548    Invalidate,
549}
550
551/// One operation node in the canonical flow graph. The canonical graph
552/// retains individual operations — component-level grouping (ComponentSpan)
553/// happens only at display/context time.
554#[derive(Debug, Clone, Serialize, Deserialize)]
555// trace:v1 id=impl.crates-scc-core-src-lib.FlowNode work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
556pub struct FlowNode {
557    /// Index within the graph (0-based).
558    pub id: u32,
559    /// Actor entity id (symbol or component).
560    pub actor: String,
561    /// Operation label.
562    pub operation: String,
563    #[serde(default, skip_serializing_if = "Vec::is_empty")]
564    pub evidence: Vec<String>,
565}
566
567#[derive(Debug, Clone, Serialize, Deserialize)]
568// trace:v1 id=impl.crates-scc-core-src-lib.FlowEdge work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
569pub struct FlowEdge {
570    pub from: u32,
571    pub to: u32,
572    pub kind: FlowEdgeKind,
573    #[serde(default, skip_serializing_if = "Option::is_none")]
574    pub condition: Option<String>,
575    #[serde(default)]
576    pub provenance: Option<Provenance>,
577    #[serde(default)]
578    pub confidence: f64,
579    #[serde(default, skip_serializing_if = "Vec::is_empty")]
580    pub evidence: Vec<String>,
581}
582
583/// The canonical causal representation of one flow: a graph, never a
584/// flattened linear step list. Alternate execution paths are preserved as
585/// branch edges; false sequential causality is impossible by construction.
586#[derive(Debug, Clone, Serialize, Deserialize)]
587// trace:v1 id=impl.crates-scc-core-src-lib.FlowGraph work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
588pub struct FlowGraph {
589    pub id: String,
590    pub kind: FlowKind,
591    pub name: String,
592    #[serde(default, skip_serializing_if = "Option::is_none")]
593    pub trigger: Option<String>,
594    #[serde(default)]
595    pub nodes: Vec<FlowNode>,
596    #[serde(default)]
597    pub edges: Vec<FlowEdge>,
598    /// Node indices that start the graph.
599    #[serde(default)]
600    pub entrypoints: Vec<u32>,
601    /// Node indices with no outgoing causal edge (returns).
602    #[serde(default)]
603    pub exits: Vec<u32>,
604    #[serde(default)]
605    pub provenance_summary: BTreeMap<String, usize>,
606}
607
608// ---------------------------------------------------------------------------
609// System Atlas (Wave 2 — the startup architecture artifact)
610// ---------------------------------------------------------------------------
611
612/// One architectural component in the atlas. Purpose is the highest-ranked
613/// responsibility claim; consumes/produces come from data-flow edges;
614/// upstream/downstream from dependency edges; retry/failure from extracted
615/// failure behavior.
616#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
617// trace:v1 id=impl.crates-scc-core-src-lib.AtlasComponent work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
618pub struct AtlasComponent {
619    pub name: String,
620    pub purpose: String,
621    /// Implementation facts: directory paths AND member symbol names (the
622    /// component compiler's `implementation` attribute carries both). The
623    /// structured model exposes the full fact layer; the rendered atlas
624    /// shows only [`AtlasComponent::implementation_paths`] to stay compact.
625    #[serde(default)]
626    pub implementation: Vec<String>,
627    /// The directory-path subset of `implementation` — the compact view the
628    /// rendered ARCHITECTURE block and IMPLEMENTATION MAP use.
629    #[serde(default)]
630    pub implementation_paths: Vec<String>,
631    /// Member symbols attributed to the component (compile-time fact).
632    #[serde(default)]
633    pub symbols: Vec<String>,
634    #[serde(default)]
635    pub consumes: Vec<String>,
636    #[serde(default)]
637    pub produces: Vec<String>,
638    #[serde(default)]
639    pub upstream: Vec<String>,
640    #[serde(default)]
641    pub downstream: Vec<String>,
642    #[serde(default)]
643    pub failure_behavior: Vec<String>,
644    #[serde(default)]
645    pub owns: Vec<AtlasOwnershipClaim>,
646    /// Architectural layer assigned by the hierarchy clusterer:
647    /// `code_region | component | subsystem | service`.
648    #[serde(default)]
649    pub layer: String,
650    /// Immediate container entity id (subsystem/service) for merged
651    /// members; `None` for unmerged leaves.
652    #[serde(default)]
653    pub parent: Option<String>,
654    /// Repository role from implementation paths: `production` (default),
655    /// `test`, `fixture`, `benchmark`, `example`, or `mixed`. Lets renders
656    /// scope non-production trees as structure, never production.
657    #[serde(default)]
658    pub role: String,
659}
660
661/// One hierarchical container (service or subsystem) with its direct member
662/// entity ids (component ids, or subsystem ids nested inside a service).
663/// Deterministic: `members` sorted by entity id.
664#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
665// trace:v1 id=impl.crates-scc-core-src-lib.AtlasHierarchyNode work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
666pub struct AtlasHierarchyNode {
667    /// Container entity id (`repo://…/service/…` or `repo://…/subsystem/…`).
668    pub id: String,
669    pub name: String,
670    /// `"service"` | `"subsystem"`
671    pub kind: String,
672    #[serde(default)]
673    pub members: Vec<String>,
674}
675
676/// A typed ownership claim (provenance preserved — DECLARED intent never
677/// promoted).
678#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
679// trace:v1 id=impl.crates-scc-core-src-lib.AtlasOwnershipClaim work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
680pub struct AtlasOwnershipClaim {
681    pub target: String,
682    pub provenance: String,
683}
684
685/// A condensed flow: steps collapsed to "Actor: operation" lines, with
686/// branch/async/failure markers preserved.
687#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
688// trace:v1 id=impl.crates-scc-core-src-lib.AtlasFlow work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
689pub struct AtlasFlow {
690    pub name: String,
691    pub kind: FlowKind,
692    #[serde(default, skip_serializing_if = "Option::is_none")]
693    pub trigger: Option<String>,
694    #[serde(default)]
695    pub steps: Vec<String>,
696}
697
698#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
699// trace:v1 id=impl.crates-scc-core-src-lib.AtlasEntrypoint work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
700pub struct AtlasEntrypoint {
701    pub name: String,
702    pub kind: String,
703    pub trigger: String,
704    #[serde(default)]
705    pub symbol: String,
706}
707
708#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
709// trace:v1 id=impl.crates-scc-core-src-lib.AtlasInvariant work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
710pub struct AtlasInvariant {
711    pub statement: String,
712    pub severity: Severity,
713}
714
715// trace:exempt reason=internal-detail
716
717/// First-class contract subclass (Contract ontology): the semantic contract
718/// family, derived by the extractors from general evidence (public fn
719/// signatures, builder/factory structure, event producer/consumer pairs,
720/// serializer/deserializer pairs, interface+implementations, route/flag/
721/// topic/config facts) and rendered by the atlas as per-subclass groups.
722/// The legacy `kind` string stays for back-compat; `subclass` is the typed
723/// family (`http`/`cli`/`event`/`config`/`public-api`/`extension`/
724/// `serialization`/...).
725#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default, schemars::JsonSchema)]
726#[serde(rename_all = "snake_case")]
727// trace:exempt reason=internal-detail
728// trace:v1 id=impl.crates-scc-core-src-lib.ContractSubclass work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
729pub enum ContractSubclass {
730    /// A callable contract surface (framework callback, task, annotated
731    /// handler, middleware): the framework invokes this callable.
732    CallContract,
733    /// Public API export (function/method signature surface).
734    PublicApi,
735    /// HTTP route.
736    #[default]
737    Http,
738    /// RPC method.
739    Rpc,
740    /// CLI flag / subcommand.
741    Cli,
742    /// Event (topic with producers/consumers).
743    Event,
744    /// Message / queue surface.
745    Message,
746    /// Schema definition (validation/model schema).
747    Schema,
748    /// Configuration key.
749    Configuration,
750    /// Plugin registration.
751    Plugin,
752    /// Extension point: interface + implementations.
753    Extension,
754    /// Serialization pair (serializer/deserializer around a type).
755    Serialization,
756}
757
758// trace:exempt reason=internal-detail
759impl ContractSubclass {
760    /// Render prefix used by the atlas CONTRACTS section (stable, sorted:
761    /// `http: GET /x`, `cli: --flag`, `event: user.created`, `config: DEBUG`,
762    /// `public-api: Class.method`, `extension: PluginX`, `serialization:
763    /// toJson/fromJson`).
764// trace:v1 id=impl.crates-scc-core-src-lib-contractsubclass.as-str work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
765    pub fn as_str(&self) -> &'static str {
766        match self {
767            ContractSubclass::CallContract => "call",
768            ContractSubclass::PublicApi => "public-api",
769            ContractSubclass::Http => "http",
770            ContractSubclass::Rpc => "rpc",
771            ContractSubclass::Cli => "cli",
772            ContractSubclass::Event => "event",
773            ContractSubclass::Message => "message",
774            ContractSubclass::Schema => "schema",
775            ContractSubclass::Configuration => "config",
776            ContractSubclass::Plugin => "plugin",
777            ContractSubclass::Extension => "extension",
778            ContractSubclass::Serialization => "serialization",
779        }
780    }
781
782    /// Map a contract/registration kind string to its first-class subclass.
783    /// `None` for framework-specific registration kinds (`include_router`,
784    /// `add_middleware`, ...) that stay framework semantics instead of
785    /// first-class contracts. `factory` → PublicApi and `builder` →
786    /// Configuration are the ontology's builder/factory rule.
787// trace:v1 id=impl.crates-scc-core-src-lib-contractsubclass.from-kind-str work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
788    pub fn from_kind_str(kind: &str) -> Option<ContractSubclass> {
789        Some(match kind {
790            "http" | "route" => ContractSubclass::Http,
791            "cli" => ContractSubclass::Cli,
792            "event" | "topic" => ContractSubclass::Event,
793            "config" | "configuration" | "next-config" => ContractSubclass::Configuration,
794            "factory" | "export" | "public-api" | "public_api" => ContractSubclass::PublicApi,
795            "builder" => ContractSubclass::Configuration,
796            "serialization" | "serialize" | "deserialize" => ContractSubclass::Serialization,
797            "extension" => ContractSubclass::Extension,
798            "plugin" => ContractSubclass::Plugin,
799            "rpc" => ContractSubclass::Rpc,
800            "message" | "queue" => ContractSubclass::Message,
801            "schema" => ContractSubclass::Schema,
802            "call" | "task" | "bean" | "rule" | "middleware" | "callback" => {
803                ContractSubclass::CallContract
804            }
805            _ => return None,
806        })
807    }
808}
809
810// trace:exempt reason=internal-detail
811
812/// One first-class contract in the atlas (Wave 9): a typed, evidence-backed
813/// contract surface (http/cli/event/config/annotation) with its producer
814/// symbol and the symbols that consume it. `operations` carries the concrete
815/// contract strings (route `GET /api/x`, flag `--paging`, event
816/// `user.created`, config key `DEBUG`, annotation `router.get`).
817#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
818// trace:exempt reason=internal-detail
819// trace:v1 id=impl.crates-scc-core-src-lib.Contract work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
820pub struct Contract {
821    pub id: String,
822    /// `"http" | "cli" | "event" | "config" | "annotation"`.
823    pub kind: String,
824    /// Semantic contract subclass (Contract ontology): the typed family
825    /// derived from general evidence — http/cli/event/config from
826    /// route/flag/topic/config facts, public-api from exported fn
827    /// signatures, serialization from serializer/deserializer pairs,
828    /// extension from interface+implementations, and extractor-emitted
829    /// registration kinds mapped by `ContractSubclass::from_kind_str`.
830    #[serde(default)]
831    pub subclass: ContractSubclass,
832    /// Producer entity id (handler symbol, owning symbol, topic, ...).
833    #[serde(default)]
834    pub producer: String,
835    /// Consuming entity ids (symbols with HANDLES/CONSUMES/READS edges).
836    #[serde(default)]
837    pub consumers: Vec<String>,
838    /// Concrete contract strings rendered as `{kind}: {operation}`.
839    #[serde(default)]
840    pub operations: Vec<String>,
841    #[serde(default)]
842    pub evidence: Vec<String>,
843}
844
845// trace:exempt reason=internal-detail
846impl Contract {
847// trace:v1 id=impl.crates-scc-core-src-lib-contract.new work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
848    pub fn new(
849        id: impl Into<String>,
850        kind: impl Into<String>,
851        producer: impl Into<String>,
852    ) -> Self {
853        Contract {
854            id: id.into(),
855            kind: kind.into(),
856            subclass: ContractSubclass::default(),
857            producer: producer.into(),
858            consumers: Vec::new(),
859            operations: Vec::new(),
860            evidence: Vec::new(),
861        }
862    }
863
864    /// Set the semantic subclass (builder-style; the atlas sets it on the
865    /// typed families it derives from entity kinds).
866// trace:v1 id=impl.crates-scc-core-src-lib-contract.with-subclass work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
867    pub fn with_subclass(mut self, subclass: ContractSubclass) -> Self {
868        self.subclass = subclass;
869        self
870    }
871}
872
873// trace:exempt reason=internal-detail
874
875/// How a symbol can be invoked from outside the process (Wave 9): the
876/// invocation surfaces the flow compiler seeds entrypoints from.
877#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
878#[serde(rename_all = "snake_case")]
879// trace:exempt reason=internal-detail
880// trace:v1 id=impl.crates-scc-core-src-lib.InvocationSurfaceKind work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
881pub enum InvocationSurfaceKind {
882    /// OS process spawn / executable entry.
883    Process,
884    /// HTTP route handler.
885    Http,
886    /// CLI subcommand / flag surface.
887    Cli,
888    /// Public API export (EXPORTS evidence).
889    PublicApi,
890    /// Event/topic handler.
891    Event,
892    /// Queue consumer (SUBSCRIBES evidence).
893    Queue,
894    /// Scheduled job.
895    Schedule,
896    /// Plugin/extension registration.
897    Plugin,
898    /// Framework callback (HANDLES_CALLBACK evidence).
899    FrameworkCallback,
900    /// Lifecycle callback (JUnit @Before*/@After* annotation facts).
901    Lifecycle,
902}
903
904// trace:exempt reason=internal-detail
905impl InvocationSurfaceKind {
906// trace:v1 id=impl.crates-scc-core-src-lib-invocationsurfacekind.as-str work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
907    pub fn as_str(&self) -> &'static str {
908        match self {
909            InvocationSurfaceKind::Process => "process",
910            InvocationSurfaceKind::Http => "http",
911            InvocationSurfaceKind::Cli => "cli",
912            InvocationSurfaceKind::PublicApi => "public_api",
913            InvocationSurfaceKind::Event => "event",
914            InvocationSurfaceKind::Queue => "queue",
915            InvocationSurfaceKind::Schedule => "schedule",
916            InvocationSurfaceKind::Plugin => "plugin",
917            InvocationSurfaceKind::FrameworkCallback => "framework_callback",
918            InvocationSurfaceKind::Lifecycle => "lifecycle",
919        }
920    }
921}
922
923/// One invocation surface: a symbol reachable from outside the process.
924#[derive(Debug, Clone, Serialize, Deserialize)]
925// trace:v1 id=impl.crates-scc-core-src-lib.InvocationSurface work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
926pub struct InvocationSurface {
927    pub symbol: String,
928    pub kind: InvocationSurfaceKind,
929    pub trigger: String,
930}
931
932/// The full System Atlas: structured architecture before rendering. This is
933/// the machine model handed to agents at session start (docs/SYSTEM_DESIGN.md
934/// §8, Wave 2).
935#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
936// trace:v1 id=impl.crates-scc-core-src-lib.SystemAtlas work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
937pub struct SystemAtlas {
938    pub repository: String,
939    pub revision: String,
940    pub indexed_at: String,
941    pub freshness: String,
942    pub purpose: String,
943    #[serde(default)]
944    pub components: Vec<AtlasComponent>,
945    #[serde(default)]
946    pub entrypoints: Vec<AtlasEntrypoint>,
947    #[serde(default)]
948    pub contracts: Vec<Contract>,
949    /// Explicit uncertainty/coverage map (Wave 9): section key -> line.
950    /// What the model knows AND what it does not.
951    #[serde(default)]
952    pub coverage: BTreeMap<String, String>,
953    #[serde(default)]
954    pub flows: Vec<AtlasFlow>,
955    #[serde(default)]
956    pub invariants: Vec<AtlasInvariant>,
957    #[serde(default)]
958    pub deployment_units: Vec<String>,
959    #[serde(default)]
960    pub external_systems: Vec<String>,
961    #[serde(default)]
962    pub trust_boundaries: Vec<String>,
963    #[serde(default)]
964    pub async_boundaries: Vec<String>,
965    #[serde(default)]
966    pub implementation_map: BTreeMap<String, Vec<String>>,
967    /// Data stores / data entities written by components (WRITES-derived),
968    /// rendered as a DATA STORES list under DATA OWNERSHIP.
969    #[serde(default)]
970    pub data_stores: Vec<String>,
971    /// Detected repository archetype (deterministic evidence scoring).
972    #[serde(default)]
973    pub archetype: Option<Archetype>,
974    /// STATE & DATA AUTHORITY (ontology phase): section key
975    /// (persistent|runtime|configuration|caches|derived) -> deterministic
976    /// `COMPONENT owns/reads TARGET (PROV)`-style lines.
977    #[serde(default)]
978    pub state_authority: BTreeMap<String, Vec<String>>,
979    /// Hierarchical architecture containers (services first, then
980    /// subsystems) with their direct member entity ids.
981    #[serde(default)]
982    pub hierarchy: Vec<AtlasHierarchyNode>,
983    #[serde(default)]
984    pub evidence_summary: BTreeMap<String, usize>,
985    #[serde(default)]
986    pub warnings: Vec<String>,
987    /// PUBLIC API (Wave 10 COMPILER-gap attack): component name ->
988    /// sorted public-export names (EXPORT entities + exported module-level
989    /// symbols). Rendered as `component: exports...` compact lines.
990    #[serde(default)]
991    pub public_api: BTreeMap<String, Vec<String>>,
992    /// FRAMEWORK SEMANTICS (Wave 10): component name -> sorted semantic
993    /// lines (`annotates X`, `registers Y (kind)`, `handles callback Z`)
994    /// from ANNOTATES/REGISTERS/HANDLES_CALLBACK facts.
995    #[serde(default)]
996    pub framework_semantics: BTreeMap<String, Vec<String>>,
997    /// PIPELINE (Wave 10, CompilerLanguageTool archetype): phase-named
998    /// symbols/files grouped by stage (`parse`/`analyze`/`transform`/
999    /// `generate`/`emit`/`other`), rendered as `[stage] symbol` lines.
1000    #[serde(default)]
1001    pub pipeline: Vec<String>,
1002    /// LANDMARKS (Wave 10): notable exports + annotated targets, bounded
1003    /// (~40) — the informational one-zoom-deeper symbol list.
1004    #[serde(default)]
1005    pub landmarks: Vec<String>,
1006}
1007
1008// ---------------------------------------------------------------------------
1009// Whole-document export
1010// ---------------------------------------------------------------------------
1011
1012// trace:exempt reason=internal-detail
1013
1014#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
1015// trace:exempt reason=internal-detail
1016// trace:v1 id=impl.crates-scc-core-src-lib.SystemIr work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1017pub struct SystemIr {
1018    pub schema_version: String,
1019    pub repository: Repository,
1020    pub snapshot: Snapshot,
1021    pub entities: Vec<Entity>,
1022    pub relationships: Vec<Relationship>,
1023    pub flows: Vec<Flow>,
1024    pub invariants: Vec<Invariant>,
1025    #[serde(default)]
1026    pub evidence: Vec<Evidence>,
1027}
1028
1029// trace:exempt reason=internal-detail
1030impl SystemIr {
1031// trace:v1 id=impl.crates-scc-core-src-lib-systemir.empty work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1032    pub fn empty(repository: Repository, snapshot: Snapshot) -> Self {
1033        SystemIr {
1034            schema_version: SCHEMA_VERSION.to_string(),
1035            repository,
1036            snapshot,
1037            entities: Vec::new(),
1038            relationships: Vec::new(),
1039            flows: Vec::new(),
1040            invariants: Vec::new(),
1041            evidence: Vec::new(),
1042        }
1043    }
1044}
1045
1046// ---------------------------------------------------------------------------
1047// Identifiers
1048// ---------------------------------------------------------------------------
1049
1050/// Sanitize a free-form name into a stable URI key.
1051// trace:v1 id=impl.crates-scc-core-src-lib.sanitize-key work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1052pub fn sanitize_key(input: &str) -> String {
1053    let mut out = String::with_capacity(input.len());
1054    let mut prev_dash = false;
1055    for c in input.chars() {
1056        let ok = c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' || c == '/';
1057        if ok {
1058            // keep path separators but normalize repeated dashes
1059            if c == '/' {
1060                out.push('/');
1061                prev_dash = false;
1062            } else if c == '-' || c == '_' {
1063                if !prev_dash {
1064                    out.push('-');
1065                }
1066                prev_dash = true;
1067            } else {
1068                out.push(c.to_ascii_lowercase());
1069                prev_dash = false;
1070            }
1071        } else {
1072            if !prev_dash {
1073                out.push('-');
1074            }
1075            prev_dash = true;
1076        }
1077    }
1078    while out.ends_with('-') {
1079        out.pop();
1080    }
1081    if out.is_empty() {
1082        out.push_str("unnamed");
1083    }
1084    out
1085}
1086
1087/// `repo://{repo}/{kind}/{key}` stable identifier.
1088// trace:v1 id=impl.crates-scc-core-src-lib.entity-id work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1089pub fn entity_id(repo: &str, kind: &str, key: &str) -> String {
1090    format!("repo://{}/{}/{}", sanitize_key(repo), kind, sanitize_key(key))
1091}
1092
1093/// Occurrence entity id: collision-free per (concept key, path, owner,
1094/// line) — the identity occurrences carry so shared concepts never lose
1095/// per-file provenance. Unlike [`entity_id`], the concept/path/owner
1096/// components are percent-encoded (`@`-separated), so case and separator
1097/// distinctions (`a_b` vs `a-b`) never merge distinct occurrences.
1098// trace:v1 id=impl.scc.core.occurrence work=WORK-SCC-001 satisfies=REQ-SCC-IR
1099// trace:v1 id=impl.crates-scc-core-src-lib.occurrence-id work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1100pub fn occurrence_id(repo: &str, concept: &str, path: &str, owner: &str, line: u32) -> String {
1101    format!(
1102        "repo://{}/occurrence/{}@{}@{}@{}",
1103        sanitize_key(repo),
1104        encode_component(concept),
1105        encode_component(path),
1106        encode_component(owner),
1107        line
1108    )
1109}
1110
1111/// Percent-encode a path/name component for use inside an entity id while
1112/// preserving case and common separators (`/`, `.`, `_`, `-`). Collision-free
1113/// where `sanitize_key` would risk merging distinct names.
1114// trace:v1 id=impl.crates-scc-core-src-lib.encode-component work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1115pub fn encode_component(input: &str) -> String {
1116    let mut out = String::with_capacity(input.len());
1117    for b in input.bytes() {
1118        let keep = b.is_ascii_alphanumeric() || matches!(b, b'/' | b'.' | b'_' | b'-');
1119        if keep {
1120            out.push(b as char);
1121        } else {
1122            out.push('%');
1123            out.push_str(&format!("{b:02X}"));
1124        }
1125    }
1126    out
1127}
1128
1129/// Inverse of `encode_component`: percent-decodes `%XX` sequences back to
1130/// bytes. Used by benchmark/impact tooling to map entity ids back to names.
1131// trace:v1 id=impl.crates-scc-core-src-lib.decode-component work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1132pub fn decode_component(input: &str) -> String {
1133    let bytes = input.as_bytes();
1134    let mut out = Vec::with_capacity(bytes.len());
1135    let mut i = 0;
1136    while i < bytes.len() {
1137        if bytes[i] == b'%' && i + 2 < bytes.len() {
1138            let hi = (bytes[i + 1] as char).to_digit(16);
1139            let lo = (bytes[i + 2] as char).to_digit(16);
1140            if let (Some(hi), Some(lo)) = (hi, lo) {
1141                out.push((hi * 16 + lo) as u8);
1142                i += 3;
1143                continue;
1144            }
1145        }
1146        out.push(bytes[i]);
1147        i += 1;
1148    }
1149    String::from_utf8_lossy(&out).to_string()
1150}
1151
1152/// Stable symbol id: `repo://{repo}/symbol/{encoded-file}/{encoded-name}`.
1153// trace:v1 id=impl.crates-scc-core-src-lib.symbol-id work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1154pub fn symbol_id(repo: &str, file: &str, name: &str) -> String {
1155    format!(
1156        "repo://{}/symbol/{}/{}",
1157        sanitize_key(repo),
1158        encode_component(file),
1159        encode_component(name)
1160    )
1161}
1162
1163/// Evidence id namespace: `evidence:{n}` — stable within a snapshot, assigned
1164/// by the store.
1165// trace:v1 id=impl.crates-scc-core-src-lib.evidence-id work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1166pub fn evidence_id(n: u64) -> String {
1167    format!("evidence:{n}")
1168}
1169
1170/// Relationship id: `rel:{n}` — stable within a snapshot, assigned by store.
1171// trace:v1 id=impl.crates-scc-core-src-lib.relationship-id work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1172pub fn relationship_id(n: u64) -> String {
1173    format!("rel:{n}")
1174}
1175
1176// ---------------------------------------------------------------------------
1177// Entity kind / predicate constants
1178// ---------------------------------------------------------------------------
1179
1180pub mod kinds {
1181    pub const FILE: &str = "file";
1182    pub const SYMBOL: &str = "symbol";
1183    pub const PACKAGE: &str = "package";
1184    pub const MODULE: &str = "module";
1185    pub const SYSTEM: &str = "system";
1186    pub const SUBSYSTEM: &str = "subsystem";
1187    pub const SERVICE: &str = "service";
1188    pub const COMPONENT: &str = "component";
1189    pub const DEPLOYMENT_UNIT: &str = "deployment_unit";
1190    pub const ROUTE: &str = "route";
1191    pub const ENDPOINT: &str = "endpoint";
1192    pub const EVENT: &str = "event";
1193    pub const TOPIC: &str = "topic";
1194    pub const QUEUE: &str = "queue";
1195    pub const DATA_ENTITY: &str = "data";
1196    pub const DATA_STORE: &str = "store";
1197    pub const TABLE: &str = "table";
1198    pub const COLLECTION: &str = "collection";
1199    pub const CACHE: &str = "cache";
1200    pub const EXTERNAL_SYSTEM: &str = "external_system";
1201    pub const EXTERNAL_API: &str = "external_api";
1202    pub const CONFIGURATION: &str = "configuration";
1203    pub const FEATURE_FLAG: &str = "feature_flag";
1204    pub const SECRET_REFERENCE: &str = "secret_reference";
1205    pub const CONTRACT: &str = "contract";
1206    pub const INVARIANT: &str = "invariant";
1207    pub const TEST: &str = "test";
1208    pub const TEST_SUITE: &str = "test_suite";
1209    pub const FLOW: &str = "flow";
1210    pub const WORKFLOW: &str = "workflow";
1211    pub const STATE: &str = "state";
1212    // Semantic fact layer (Wave 9): first-class representations the
1213    // extractors emit for framework/library semantics.
1214    pub const EXPORT: &str = "export";
1215    pub const ANNOTATION: &str = "annotation";
1216    pub const FIELD: &str = "field";
1217    pub const REGISTRY: &str = "registry";
1218    pub const MIDDLEWARE: &str = "middleware";
1219    pub const DI_BINDING: &str = "di_binding";
1220    pub const TRANSITION: &str = "transition";
1221    pub const RESOURCE: &str = "resource";
1222    pub const TRUST_BOUNDARY: &str = "trust_boundary";
1223    pub const SECURITY_CONTROL: &str = "security_control";
1224    pub const RUNTIME_OBSERVATION: &str = "runtime_observation";
1225    // Wave 11: first-class schema and reactive-state contracts.
1226    pub const SCHEMA: &str = "schema";
1227    pub const REACTIVE: &str = "reactive";
1228    // Occurrence layer: one entity per (concept, path, owner, line) so
1229    // shared concepts never lose per-file provenance (Wave 13).
1230    pub const OCCURRENCE: &str = "occurrence";
1231
1232    /// All entity kinds. Tests fail if a `pub const` is added above and
1233    /// omitted here.
1234    pub const ALL: &[&str] = &[
1235        FILE, SYMBOL, PACKAGE, MODULE, SYSTEM, SUBSYSTEM, SERVICE, COMPONENT, DEPLOYMENT_UNIT,
1236        ROUTE, ENDPOINT, EVENT, TOPIC, QUEUE, DATA_ENTITY, DATA_STORE, TABLE, COLLECTION, CACHE,
1237        EXTERNAL_SYSTEM, EXTERNAL_API, CONFIGURATION, FEATURE_FLAG, SECRET_REFERENCE, CONTRACT,
1238        INVARIANT, TEST, TEST_SUITE, FLOW, WORKFLOW, STATE, EXPORT, ANNOTATION, FIELD, REGISTRY,
1239        MIDDLEWARE, DI_BINDING, TRANSITION, RESOURCE, TRUST_BOUNDARY, SECURITY_CONTROL,
1240        RUNTIME_OBSERVATION, SCHEMA, REACTIVE, OCCURRENCE,
1241    ];
1242}
1243
1244pub mod predicates {
1245    pub const CONTAINS: &str = "contains";
1246    pub const IMPLEMENTS: &str = "implements";
1247    pub const INHERITS: &str = "inherits";
1248    pub const IMPORTS: &str = "imports";
1249    pub const CALLS: &str = "calls";
1250    pub const READS: &str = "reads";
1251    pub const WRITES: &str = "writes";
1252    pub const QUERIES: &str = "queries";
1253    pub const OWNS: &str = "owns";
1254    pub const PUBLISHES: &str = "publishes";
1255    pub const CONSUMES: &str = "consumes";
1256    pub const SUBSCRIBES: &str = "subscribes";
1257    pub const PRODUCES: &str = "produces";
1258    pub const TRANSFORMS: &str = "transforms";
1259    pub const VALIDATES: &str = "validates";
1260    pub const DEFINES: &str = "defines";
1261    pub const COMPOSES: &str = "composes";
1262    pub const ROUTES_TO: &str = "routes_to";
1263    pub const HANDLES: &str = "handles";
1264    pub const INVOKES: &str = "invokes";
1265    pub const DEPENDS_ON: &str = "depends_on";
1266    pub const DEPLOYED_WITH: &str = "deployed_with";
1267    pub const DEPLOYED_IN: &str = "deployed_in";
1268    pub const CONFIGURED_BY: &str = "configured_by";
1269    pub const PROTECTED_BY: &str = "protected_by";
1270    pub const CROSSES_BOUNDARY: &str = "crosses_boundary";
1271    pub const ENFORCES: &str = "enforces";
1272    pub const TESTED_BY: &str = "tested_by";
1273    pub const PARTICIPATES_IN: &str = "participates_in";
1274    pub const PRECEDES: &str = "precedes";
1275    pub const FOLLOWS: &str = "follows";
1276    pub const BRANCHES_TO: &str = "branches_to";
1277    pub const RETRIES: &str = "retries";
1278    pub const FALLS_BACK_TO: &str = "falls_back_to";
1279    pub const OBSERVED_AS: &str = "observed_as";
1280    pub const DECLARED_AS: &str = "declared_as";
1281    pub const IMPLEMENTED_BY: &str = "implemented_by";
1282    // Semantic fact layer (Wave 9)
1283    pub const EXPORTS: &str = "exports";
1284    pub const ANNOTATES: &str = "annotates";
1285    pub const REGISTERS: &str = "registers";
1286    pub const INJECTS: &str = "injects";
1287    pub const HANDLES_CALLBACK: &str = "handles_callback";
1288    pub const DECORATES: &str = "decorates";
1289    /// An occurrence entity's attachment to its concept entity
1290    /// (occurrence OCCURS concept).
1291    pub const OCCURS: &str = "occurs";
1292
1293    /// All predicates in the documented ontology. Must include every
1294    /// `pub const` in this module — `kinds`/`predicates` drift is a
1295    /// silent ranking/export bug.
1296    pub const ALL: &[&str] = &[
1297        CONTAINS, IMPLEMENTS, INHERITS, IMPORTS, CALLS, READS, WRITES, QUERIES, OWNS, PUBLISHES,
1298        CONSUMES, SUBSCRIBES, PRODUCES, TRANSFORMS, VALIDATES, DEFINES, COMPOSES, ROUTES_TO,
1299        HANDLES, INVOKES, DEPENDS_ON, DEPLOYED_WITH, DEPLOYED_IN, CONFIGURED_BY, PROTECTED_BY,
1300        CROSSES_BOUNDARY, ENFORCES, TESTED_BY, PARTICIPATES_IN, PRECEDES, FOLLOWS, BRANCHES_TO,
1301        RETRIES, FALLS_BACK_TO, OBSERVED_AS, DECLARED_AS, IMPLEMENTED_BY, EXPORTS, ANNOTATES,
1302        REGISTERS, INJECTS, HANDLES_CALLBACK, DECORATES, OCCURS,
1303    ];
1304}
1305
1306/// Authoritative kind and predicate ids. Rankers, exporters, and tests
1307/// must derive from these slices rather than a second hand-maintained list.
1308// trace:v1 id=impl.scc.core.ontology-registry work=WORK-ripwire-lessons-phase1 satisfies=REQ-ontology-single-source
1309pub fn ontology_registries() -> (&'static [&'static str], &'static [&'static str]) {
1310    (kinds::ALL, predicates::ALL)
1311}
1312
1313// ---------------------------------------------------------------------------
1314// Token budgeting
1315// ---------------------------------------------------------------------------
1316
1317/// Rough token estimate: 4 characters per token (byte-based for ASCII, but we
1318/// operate on char count which is a close approximation across scripts).
1319// trace:v1 id=impl.crates-scc-core-src-lib.estimate-tokens work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1320pub fn estimate_tokens(text: &str) -> usize {
1321    let chars = text.chars().count();
1322    chars.div_ceil(4)
1323}
1324
1325/// Hard-truncate `text` to at most `budget` tokens, preferring a clean cut at
1326/// a line boundary.
1327// trace:v1 id=impl.crates-scc-core-src-lib.truncate-to-budget work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1328pub fn truncate_to_budget(text: &str, budget: usize) -> String {
1329    if estimate_tokens(text) <= budget {
1330        return text.to_string();
1331    }
1332    let max_chars = budget.saturating_mul(4);
1333    let mut end = 0;
1334    let mut chars = 0;
1335    for (i, c) in text.char_indices() {
1336        chars += 1;
1337        if chars > max_chars {
1338            break;
1339        }
1340        end = i + c.len_utf8();
1341    }
1342    // Back off to the previous newline for a clean boundary (but keep at least
1343    // half the budget worth of content).
1344    let min_chars = max_chars / 2;
1345    let mut cut = end;
1346    if let Some(nl) = text[..end].rfind('\n') {
1347        let prefix_chars = text[..nl].chars().count();
1348        if prefix_chars >= min_chars {
1349            cut = nl;
1350        }
1351    }
1352    let mut out = text[..cut].to_string();
1353    out.push_str("\n… (truncated by token budget)");
1354    out
1355}
1356
1357// ---------------------------------------------------------------------------
1358// Misc
1359// ---------------------------------------------------------------------------
1360
1361// trace:v1 id=impl.crates-scc-core-src-lib.now-rfc3339 work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1362pub fn now_rfc3339() -> String {
1363    chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
1364}
1365
1366// ---------------------------------------------------------------------------
1367// Wave 14: System Surface Map — the actual callable code surface (Aider
1368// RepoMap equivalent built from System IR). Level 1 of the four-level
1369// context stack (docs/SYSTEM_DESIGN.md Wave 14).
1370// ---------------------------------------------------------------------------
1371
1372/// A source range: file path + 1-based inclusive line span.
1373#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
1374// trace:exempt reason=internal-detail
1375// trace:v1 id=impl.crates-scc-core-src-lib.SourceRange work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1376pub struct SourceRange {
1377    pub path: String,
1378    pub start_line: u32,
1379    pub end_line: u32,
1380}
1381
1382// trace:exempt reason=internal-detail
1383impl SourceRange {
1384// trace:exempt reason=internal-detail
1385    pub fn new(path: impl Into<String>, start_line: u32, end_line: u32) -> Self {
1386        SourceRange {
1387            path: path.into(),
1388            start_line,
1389            end_line,
1390        }
1391    }
1392}
1393
1394/// Symbol visibility as declared in source.
1395#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, schemars::JsonSchema)]
1396#[serde(rename_all = "snake_case")]
1397// trace:exempt reason=internal-detail
1398// trace:v1 id=impl.crates-scc-core-src-lib.Visibility work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1399pub enum Visibility {
1400    Public,
1401    Protected,
1402    Private,
1403    Package,
1404}
1405
1406// trace:exempt reason=internal-detail
1407impl Visibility {
1408// trace:exempt reason=internal-detail
1409    pub fn as_str(&self) -> &'static str {
1410        match self {
1411            Visibility::Public => "public",
1412            Visibility::Protected => "protected",
1413            Visibility::Private => "private",
1414            Visibility::Package => "package",
1415        }
1416    }
1417}
1418
1419/// The kind of code surface a definition exposes.
1420#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, schemars::JsonSchema)]
1421#[serde(rename_all = "snake_case")]
1422// trace:exempt reason=internal-detail
1423// trace:v1 id=impl.crates-scc-core-src-lib.SurfaceKind work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1424pub enum SurfaceKind {
1425    Function,
1426    Method,
1427    Constructor,
1428    Class,
1429    Interface,
1430    Trait,
1431    Type,
1432    Enum,
1433    Const,
1434    Module,
1435    Record,
1436}
1437
1438// trace:exempt reason=internal-detail
1439impl SurfaceKind {
1440// trace:exempt reason=internal-detail
1441    pub fn as_str(&self) -> &'static str {
1442        match self {
1443            SurfaceKind::Function => "function",
1444            SurfaceKind::Method => "method",
1445            SurfaceKind::Constructor => "constructor",
1446            SurfaceKind::Class => "class",
1447            SurfaceKind::Interface => "interface",
1448            SurfaceKind::Trait => "trait",
1449            SurfaceKind::Type => "type",
1450            SurfaceKind::Enum => "enum",
1451            SurfaceKind::Const => "const",
1452            SurfaceKind::Module => "module",
1453            SurfaceKind::Record => "record",
1454        }
1455    }
1456}
1457
1458/// One function/method parameter in structured form.
1459#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
1460// trace:exempt reason=internal-detail
1461// trace:v1 id=impl.crates-scc-core-src-lib.SemanticParameter work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1462pub struct SemanticParameter {
1463    pub name: String,
1464    #[serde(default, skip_serializing_if = "Option::is_none")]
1465    pub ty: Option<String>,
1466    /// `&self` / `self` receiver parameters.
1467    #[serde(default)]
1468    pub receiver: bool,
1469    #[serde(default, skip_serializing_if = "Option::is_none")]
1470    pub default: Option<String>,
1471    #[serde(default)]
1472    pub variadic: bool,
1473}
1474
1475/// The structured machine form of a signature — the semantic layer over
1476/// the exact source text. Benchmark matching uses this, never string
1477/// comparisons of source signatures alone.
1478#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, schemars::JsonSchema)]
1479// trace:exempt reason=internal-detail
1480// trace:v1 id=impl.crates-scc-core-src-lib.SemanticSignature work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1481pub struct SemanticSignature {
1482    pub name: String,
1483    #[serde(default, skip_serializing_if = "Option::is_none")]
1484    pub owner: Option<String>,
1485    #[serde(default)]
1486    pub visibility: Option<Visibility>,
1487    #[serde(default)]
1488    pub async_: bool,
1489    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1490    pub generic_parameters: Vec<String>,
1491    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1492    pub parameters: Vec<SemanticParameter>,
1493    #[serde(default, skip_serializing_if = "Option::is_none")]
1494    pub returns: Option<String>,
1495    /// `where` / trait-bound constraints (`T: Send + Sync`).
1496    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1497    pub constraints: Vec<String>,
1498}
1499
1500/// Why a surface entry earned its rank (explainability; `scc surface
1501/// --explain` renders this).
1502#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
1503// trace:exempt reason=internal-detail
1504// trace:v1 id=impl.crates-scc-core-src-lib.SurfaceRank work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1505pub struct SurfaceRank {
1506    pub task_ppr: f64,
1507    pub global_ppr: f64,
1508    pub lexical: f64,
1509    pub semantic: f64,
1510    pub confidence: f64,
1511    pub criticality: f64,
1512    pub change_risk: f64,
1513    pub novelty: f64,
1514    pub total: f64,
1515    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1516    pub reasons: Vec<String>,
1517}
1518
1519// trace:exempt reason=internal-detail
1520impl Default for SurfaceRank {
1521// trace:exempt reason=internal-detail
1522// trace:exempt reason=internal-detail
1523    fn default() -> Self {
1524        SurfaceRank {
1525            task_ppr: 0.0,
1526            global_ppr: 0.0,
1527            lexical: 0.0,
1528            semantic: 0.0,
1529            confidence: 0.0,
1530            criticality: 0.0,
1531            change_risk: 0.0,
1532            novelty: 0.0,
1533            total: 0.0,
1534            reasons: Vec::new(),
1535        }
1536    }
1537}
1538
1539/// One ranked definition on the system surface — a callable/typeable
1540/// reality of the architecture, with exact signatures and architectural
1541/// meaning attached.
1542#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
1543// trace:exempt reason=internal-detail
1544// trace:v1 id=impl.crates-scc-core-src-lib.SurfaceEntry work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1545pub struct SurfaceEntry {
1546    pub id: String,
1547    pub symbol_id: String,
1548    pub qualified_name: String,
1549    pub kind: SurfaceKind,
1550    pub path: String,
1551    pub range: SourceRange,
1552    /// Exact source representation of the signature.
1553    pub source_signature: String,
1554    /// Whitespace/dialect-normalized signature (dedupe/comparison/index).
1555    pub canonical_signature: String,
1556    pub semantic_signature: SemanticSignature,
1557    pub visibility: Visibility,
1558    #[serde(default)]
1559    pub exported: bool,
1560    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1561    pub modifiers: Vec<String>,
1562    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1563    pub annotations: Vec<String>,
1564    #[serde(default, skip_serializing_if = "Option::is_none")]
1565    pub component: Option<String>,
1566    #[serde(default, skip_serializing_if = "Option::is_none")]
1567    pub subsystem: Option<String>,
1568    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1569    pub flows: Vec<String>,
1570    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1571    pub contracts: Vec<String>,
1572    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1573    pub state_authorities: Vec<String>,
1574    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1575    pub invocation_surfaces: Vec<String>,
1576    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1577    pub callers: Vec<String>,
1578    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1579    pub callees: Vec<String>,
1580    /// Exact distinct fan-in/fan-out (the displayed name lists stay capped
1581    /// at 12; the counts are never truncated).
1582    #[serde(default)]
1583    pub caller_count: usize,
1584    #[serde(default)]
1585    pub callee_count: usize,
1586    /// Derived importance explanation (badges + counts + architecture
1587    /// signals). Populated by the surface pipeline; default-empty for
1588    /// entries built before ranking.
1589    #[serde(default, skip_serializing_if = "Option::is_none")]
1590    pub importance: Option<ImportanceProfile>,
1591    pub provenance: Provenance,
1592    #[serde(default)]
1593    pub confidence: f32,
1594    #[serde(default)]
1595    pub rank: SurfaceRank,
1596}
1597
1598/// A definition deliberately omitted by a token-budget cut — the artifact
1599/// never silently implies completeness.
1600/// Derived importance explanation for one surface symbol (audit item 3):
1601/// topology (exact fan-in/fan-out, never truncated), architecture signals,
1602/// and change impact — computed from data the pipeline ALREADY has. Badges
1603/// are derived labels over those numbers, not extra scoring magic: the
1604/// overall score stays `rank.total`.
1605#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
1606// trace:v1 id=impl.scc.core.importance-profile work=WORK-SI-MMMJA4G6 satisfies=REQ-SI-503JSBGP
1607pub struct ImportanceProfile {
1608    pub overall: f64,
1609    pub caller_count: usize,
1610    pub callee_count: usize,
1611    pub global_ppr: f64,
1612    pub task_ppr: f64,
1613    pub entrypoint: bool,
1614    pub exported: bool,
1615    pub flow_count: usize,
1616    pub contract_count: usize,
1617    pub state_read_count: usize,
1618    pub state_write_count: usize,
1619    pub dependent_count: usize,
1620    pub change_risk: f64,
1621    pub badges: Vec<String>,
1622}
1623
1624#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
1625// trace:exempt reason=internal-detail
1626// trace:v1 id=impl.crates-scc-core-src-lib.SurfaceOmission work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1627pub struct SurfaceOmission {
1628    pub count: usize,
1629    pub kind: String,
1630    pub reason: String,
1631}
1632
1633/// The System Surface Map: the ranked actual-API layer of a repository,
1634/// built from System IR (Level 1 of the context stack).
1635#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
1636// trace:exempt reason=internal-detail
1637// trace:v1 id=impl.crates-scc-core-src-lib.SystemSurfaceMap work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1638pub struct SystemSurfaceMap {
1639    pub repository: String,
1640    pub revision: String,
1641    pub epoch: String,
1642    pub entries: Vec<SurfaceEntry>,
1643    #[serde(default)]
1644    pub token_count: usize,
1645    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1646    pub omitted: Vec<SurfaceOmission>,
1647}
1648
1649/// The production surface render: the budget-selected subset plus honest
1650/// omission accounting (Wave 14F). `rendered_ids` are exactly the entries
1651/// the agent sees (ledger recording MUST use only these — omitted
1652/// candidates are never marked visible); `omitted_ids` are every candidate
1653/// the pipeline cut. `omissions` summarizes the cuts by kind.
1654#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
1655// trace:exempt reason=internal-detail
1656// trace:v1 id=impl.crates-scc-core-src-lib.SurfaceRenderResult work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1657pub struct SurfaceRenderResult {
1658    /// The rendered surface text (header + selected entry blocks).
1659    pub text: String,
1660    /// Entry ids actually rendered, in selection order.
1661    pub rendered_ids: Vec<String>,
1662    /// The rendered entries themselves (post hard-max pops — exactly the
1663    /// set `rendered_ids` names). Lets callers resolve rendered ids without
1664    /// recompiling the surface map. Wire-compatible: skipped when empty.
1665    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1666    pub rendered_entries: Vec<SurfaceEntry>,
1667    /// Candidate entry ids the pipeline omitted (budget/quotas/diversity).
1668    pub omitted_ids: Vec<String>,
1669    /// Per-kind omission summaries.
1670    pub omissions: Vec<SurfaceOmission>,
1671    /// Token estimate of `text`.
1672    pub token_count: usize,
1673    /// Required entries the hard-max invariant had to DROP after every
1674    /// compression level was exhausted (explicit CRITICAL omissions — the
1675    /// highest-ranked required entry is preserved whenever anything fits).
1676    /// Empty in every non-pathological render.
1677    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1678    pub critical_drops: Vec<String>,
1679}
1680
1681/// One node in the heterogeneous ranking universe (Wave 14B): any rankable
1682/// entity — symbol, component, subsystem, service, flow, contract, state,
1683/// reactive, route, topic, queue, store, schema, or file. The ranker walks
1684/// edges whose endpoints are rankable entities, so architectural importance
1685/// (flows, contracts, state) participates in PageRank directly.
1686#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1687// trace:exempt reason=internal-detail
1688// trace:v1 id=impl.crates-scc-core-src-lib.RankNode work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1689pub struct RankNode {
1690    /// The entity id (`repo://{repo}/{kind}/{key}`).
1691    pub id: String,
1692    /// Entity kind (`scc_core::kinds::*`).
1693    pub kind: String,
1694    /// Entity display name.
1695    pub name: String,
1696}
1697
1698/// A normalized reference between two symbols (Wave 14): the graph the
1699/// ranker walks. Many SCC relationships already express these concepts;
1700/// this layer normalizes them for ranking.
1701#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1702// trace:exempt reason=internal-detail
1703// trace:v1 id=impl.crates-scc-core-src-lib.ReferenceEdge work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1704pub struct ReferenceEdge {
1705    pub source_symbol: String,
1706    pub target_symbol: String,
1707    pub kind: ReferenceKind,
1708    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1709    pub locations: Vec<SourceRange>,
1710    pub provenance: Provenance,
1711    #[serde(default)]
1712    pub confidence: f32,
1713}
1714
1715#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
1716#[serde(rename_all = "snake_case")]
1717// trace:exempt reason=internal-detail
1718// trace:v1 id=impl.crates-scc-core-src-lib.ReferenceKind work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1719pub enum ReferenceKind {
1720    Read,
1721    Write,
1722    #[default]
1723    Call,
1724    TypeUse,
1725    Instantiate,
1726    Implement,
1727    Extend,
1728    Decorate,
1729    Register,
1730    Import,
1731    Export,
1732    Macro,
1733    FieldAccess,
1734    Construct,
1735}
1736
1737// trace:exempt reason=internal-detail
1738impl ReferenceKind {
1739// trace:exempt reason=internal-detail
1740    pub fn as_str(&self) -> &'static str {
1741        match self {
1742            ReferenceKind::Read => "read",
1743            ReferenceKind::Write => "write",
1744            ReferenceKind::Call => "call",
1745            ReferenceKind::TypeUse => "type_use",
1746            ReferenceKind::Instantiate => "instantiate",
1747            ReferenceKind::Implement => "implement",
1748            ReferenceKind::Extend => "extend",
1749            ReferenceKind::Decorate => "decorate",
1750            ReferenceKind::Register => "register",
1751            ReferenceKind::Import => "import",
1752            ReferenceKind::Export => "export",
1753            ReferenceKind::Macro => "macro",
1754            ReferenceKind::FieldAccess => "field_access",
1755            ReferenceKind::Construct => "construct",
1756        }
1757    }
1758}
1759
1760/// A token-optimized context candidate (Aider-style hard budget search).
1761#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1762// trace:exempt reason=internal-detail
1763// trace:v1 id=impl.crates-scc-core-src-lib.ContextItem work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1764pub struct ContextItem {
1765    pub id: String,
1766    pub value: f64,
1767    pub token_cost: usize,
1768    #[serde(default)]
1769    pub required: bool,
1770    #[serde(default, skip_serializing_if = "Option::is_none")]
1771    pub group: Option<String>,
1772}
1773
1774/// The startup/task context budget split (Wave 14 dynamic budgets).
1775#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1776// trace:v1 id=impl.crates-scc-core-src-lib.ContextBudget work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1777pub struct ContextBudget {
1778    pub total: usize,
1779    pub atlas: usize,
1780    pub surface: usize,
1781    pub task_delta: usize,
1782    pub structural_source: usize,
1783}
1784
1785// trace:exempt reason=internal-detail
1786impl Default for ContextBudget {
1787// trace:exempt reason=internal-detail
1788// trace:exempt reason=internal-detail
1789    fn default() -> Self {
1790        ContextBudget {
1791            total: 20_000,
1792            atlas: 13_000,
1793            surface: 7_000,
1794            task_delta: 3_000,
1795            structural_source: 6_000,
1796        }
1797    }
1798}
1799
1800/// Absolute ceiling on the massive-tier surface slice: a 20k-entity repo
1801/// must not hand the model an unbounded surface even under a huge total.
1802const MASSIVE_SURFACE_CAP: usize = 10_000;
1803
1804// trace:exempt reason=internal-detail  # impl grouping; adaptive below is traced
1805impl ContextBudget {
1806    /// Adaptive startup split: scale the Atlas/Surface allocation by repo
1807    /// complexity instead of the fixed 13:7 default. Tiers by entity count
1808    /// (component count escalates to `large`; flow count escalates to
1809    /// `architecture-heavy`):
1810    ///
1811    /// - tiny (`entity_count < 200`): 55/45 — a small atlas leaves room
1812    ///   for a proportionally larger surface;
1813    /// - ordinary: 60/40;
1814    /// - architecture-heavy (`flow_count > 40`): 65/35 — flows, routes and
1815    ///   contracts are Atlas content, so a flow-dense repo needs the Atlas
1816    ///   slice even when its entity count is ordinary;
1817    /// - large (`entity_count > 5000` or `component_count > 30`): 65/35 —
1818    ///   the atlas dominates;
1819    /// - massive (`entity_count > 20_000`): 70/30 with the surface slice
1820    ///   capped absolutely ([`MASSIVE_SURFACE_CAP`]) and no candidate
1821    ///   boost.
1822    ///
1823    /// Within a tier, the actual candidate pool feeds the surface share:
1824    /// every 2,000 surface candidates earns up to +5 percentage points
1825    /// (surface never over 50% of the total), so a repo whose surface map
1826    /// is genuinely large gets a proportionally larger surface slice.
1827    /// `total` is caller-supplied — adaptive scales the SPLIT, never the
1828    /// total. Deterministic and no-panic. Defaults stay for callers that
1829    /// do not adapt.
1830    // trace:v1 id=impl.scc.core.budget-adaptive work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching satisfies=REQ-adaptive-startup-budgets
1831    pub fn adaptive(
1832        total: usize,
1833        entity_count: usize,
1834        component_count: usize,
1835        flow_count: usize,
1836        surface_candidates: usize,
1837    ) -> ContextBudget {
1838        let (surface_pct, boost, cap): (f64, f64, Option<usize>) = if entity_count > 20_000 {
1839            (30.0, 0.0, Some(MASSIVE_SURFACE_CAP))
1840        } else if entity_count > 5_000 || component_count > 30 {
1841            (35.0, 5.0, None)
1842        } else if flow_count > 40 {
1843            // Architecture-heavy: flows/routes/contracts are Atlas content.
1844            // Checked BEFORE the tiny tier — a small but flow-dense repo
1845            // still needs the Atlas slice.
1846            (35.0, 5.0, None)
1847        } else if entity_count < 200 {
1848            (45.0, 5.0, None)
1849        } else {
1850            (40.0, 5.0, None)
1851        };
1852        let boost_pp = (surface_candidates / 2_000).min(boost as usize) as f64;
1853        let surface_pct = (surface_pct + boost_pp).min(50.0);
1854        let atlas_pct = 100.0 - surface_pct;
1855        let mut surface = ((total as f64) * surface_pct / 100.0).round() as usize;
1856        if let Some(c) = cap {
1857            surface = surface.min(c);
1858        }
1859        let atlas = ((total as f64) * atlas_pct / 100.0).round() as usize;
1860        let def = ContextBudget::default();
1861        ContextBudget {
1862            total,
1863            atlas,
1864            surface,
1865            task_delta: def.task_delta,
1866            structural_source: def.structural_source,
1867        }
1868    }
1869}
1870
1871/// What the agent has already seen — novelty suppression source (the
1872/// general form of Aider treating chat files specially).
1873#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1874// trace:exempt reason=internal-detail
1875// trace:v1 id=impl.crates-scc-core-src-lib.ContextLedger work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1876pub struct ContextLedger {
1877    pub model_epoch: String,
1878    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
1879    pub visible_entities: BTreeSet<String>,
1880    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
1881    pub visible_symbols: BTreeSet<String>,
1882    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
1883    pub visible_files: BTreeSet<String>,
1884    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
1885    pub visible_components: BTreeSet<String>,
1886    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
1887    pub visible_flows: BTreeSet<String>,
1888    #[serde(default, skip_serializing_if = "Option::is_none")]
1889    pub last_task: Option<String>,
1890}
1891
1892/// One structural-source unit: semantic skeleton of an implementation
1893/// slice (Level 2), with provenance back to the exact source.
1894#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1895// trace:exempt reason=internal-detail
1896// trace:v1 id=impl.crates-scc-core-src-lib.StructuralSourceUnit work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1897pub struct StructuralSourceUnit {
1898    pub path: String,
1899    /// `source: <path>:L<start>-L<end>` provenance line.
1900    pub source: String,
1901    pub representation: String,
1902    pub revision: String,
1903    pub content: String,
1904    /// Stable content handle for lazy exact-source retrieval. Empty when
1905    /// the compiler has no repo/epoch identity.
1906    #[serde(default, skip_serializing_if = "String::is_empty")]
1907    pub handle: String,
1908    /// Why EXACT vs STRUCTURAL/SIGNATURES was chosen. Empty when no
1909    /// on-disk body was available to compare.
1910    #[serde(default, skip_serializing_if = "String::is_empty")]
1911    pub representation_reason: String,
1912}
1913
1914/// The deterministic startup artifact (Atlas + Surface), hash-stable per
1915/// epoch so prompt caches hit.
1916#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1917// trace:exempt reason=internal-detail
1918// trace:v1 id=impl.crates-scc-core-src-lib.ContextArtifact work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1919pub struct ContextArtifact {
1920    pub kind: String,
1921    pub epoch: String,
1922    pub renderer_version: String,
1923    pub trust_policy: String,
1924    pub budget: ContextBudget,
1925    /// Deterministic config-only hash (epoch + renderer + policy + budget) —
1926    /// the prompt-cache key, stable per epoch. Field name kept for the JSON
1927    /// contract.
1928    pub sha256: String,
1929    /// Hash over the *actual rendered content* (config preimage + rendered
1930    /// text), so a content change that keeps the config identical still
1931    /// changes the hash (the audit's name/content mismatch fix).
1932    #[serde(default, skip_serializing_if = "String::is_empty")]
1933    pub content_hash: String,
1934    pub text: String,
1935}
1936
1937/// One task-seed resolution: task language -> SCC entities.
1938#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1939// trace:exempt reason=internal-detail
1940// trace:v1 id=impl.crates-scc-core-src-lib.TaskSeed work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1941pub struct TaskSeed {
1942    pub kind: String,
1943    pub id: String,
1944    pub weight: f64,
1945}
1946
1947#[cfg(test)]
1948mod tests {
1949    use super::*;
1950    #[test]
1951    // trace:v1 id=test.scc-core.estimate-tokens work=WORK-SI-MMMJA4G6 verifies=REQ-SI-NX53P4B7 exercises=impl.crates-scc-core-src-lib.estimate-tokens
1952    fn estimate_tokens_is_chars_ceiling() {
1953        // Authoritative estimator: chars/4 ceiling. All budget paths
1954        // (Rust packs, CLI builders, Python harness mirrors) must agree.
1955        assert_eq!(estimate_tokens(""), 0);
1956        assert_eq!(estimate_tokens("a"), 1);
1957        assert_eq!(estimate_tokens("abcd"), 1);
1958        assert_eq!(estimate_tokens("abcde"), 2);
1959        assert_eq!(estimate_tokens("fn main() { println!(\"hi\"); }"), 8);
1960        // Unicode counts code points (Rust chars), not bytes or graphemes.
1961        assert_eq!(estimate_tokens("日本語"), 1);
1962        assert_eq!(estimate_tokens("日本語テスト"), 2);
1963        // Long identifier-heavy paths stay proportional.
1964        let path = "crates/scc-context/src/surface/structural_source_selection_policy.rs";
1965        assert_eq!(estimate_tokens(path), path.chars().count().div_ceil(4));
1966    }
1967
1968    #[test]
1969// trace:v1 id=impl.crates-scc-core-src-lib-context-budget.component-encode-decode-roundtrip work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1970// trace:exempt reason=internal-detail
1971    fn component_encode_decode_roundtrip() {
1972        for name in [
1973            "Normalizer",
1974            "GET /api/transcripts/:id handler",
1975            "OrderStateMachine.advance",
1976            "test_normalization_preserves_raw",
1977            "src/app.ts",
1978            "a b%c",
1979        ] {
1980            assert_eq!(decode_component(&encode_component(name)), name, "{name}");
1981        }
1982    }
1983
1984    #[test]
1985// trace:v1 id=impl.crates-scc-core-src-lib-context-budget.component-ids-are-collision-free work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1986// trace:exempt reason=internal-detail
1987    fn component_ids_are_collision_free() {
1988        assert_ne!(
1989            encode_component("foo_bar"),
1990            encode_component("foo-bar"),
1991            "underscore and dash must not collide"
1992        );
1993    }
1994
1995// trace:exempt reason=internal-detail
1996
1997    #[test]
1998// trace:exempt reason=internal-detail
1999// trace:exempt reason=internal-detail
2000    fn occurrence_ids_are_collision_free_per_site() {
2001        // distinct paths/owners/lines/concepts never merge
2002        assert_ne!(
2003            occurrence_id("r", "expr", "a.ts", "A", 1),
2004            occurrence_id("r", "expr", "b.ts", "A", 1)
2005        );
2006        assert_ne!(
2007            occurrence_id("r", "expr", "a.ts", "A", 1),
2008            occurrence_id("r", "expr", "a.ts", "B", 1)
2009        );
2010        assert_ne!(
2011            occurrence_id("r", "expr", "a.ts", "A", 1),
2012            occurrence_id("r", "expr", "a.ts", "A", 2)
2013        );
2014        assert_ne!(
2015            occurrence_id("r", "expr1", "a.ts", "A", 1),
2016            occurrence_id("r", "expr2", "a.ts", "A", 1)
2017        );
2018        // case/separator distinctions survive (unlike sanitize_key)
2019        assert_ne!(
2020            occurrence_id("r", "expr", "a_b.ts", "A", 1),
2021            occurrence_id("r", "expr", "a-b.ts", "A", 1)
2022        );
2023        assert_ne!(
2024            occurrence_id("r", "expr", "a.ts", "Foo", 1),
2025            occurrence_id("r", "expr", "a.ts", "foo", 1)
2026        );
2027        // deterministic
2028        assert_eq!(
2029            occurrence_id("r", "expr", "a.ts", "A", 1),
2030            occurrence_id("r", "expr", "a.ts", "A", 1)
2031        );
2032    }
2033
2034// trace:exempt reason=internal-detail
2035
2036    #[test]
2037// trace:exempt reason=internal-detail
2038// trace:exempt reason=internal-detail
2039    fn occurrence_roundtrips_through_serde() {
2040        let o = Occurrence {
2041            id: occurrence_id("r", "z.object({ x: z.string() })", "src/a.ts", "make", 7),
2042            concept: entity_id("r", kinds::SCHEMA, "z.object({ x: z.string() })"),
2043            path: "src/a.ts".into(),
2044            owner: "make".into(),
2045            line: 7,
2046        };
2047        let json = serde_json::to_string(&o).unwrap();
2048        let back: Occurrence = serde_json::from_str(&json).unwrap();
2049        assert_eq!(back.id, o.id);
2050        assert_eq!(back.concept, o.concept);
2051        assert_eq!(back.line, 7);
2052    }
2053
2054// trace:exempt reason=internal-detail
2055
2056    #[test]
2057// trace:exempt reason=internal-detail
2058// trace:exempt reason=internal-detail
2059    fn adaptive_budget_scales_split_by_repo_complexity() {
2060        // tiny repo: 55/45 split — a small atlas leaves room for a
2061        // proportionally larger surface (45% share, vs the normal 40%)
2062        let tiny = ContextBudget::adaptive(20_000, 50, 2, 1, 10);
2063        let tiny_ratio = tiny.surface as f64 / tiny.total as f64;
2064        assert!(
2065            (tiny_ratio - 0.45).abs() <= 0.01,
2066            "tiny: surface share {tiny_ratio}"
2067        );
2068        assert_eq!(tiny.total, 20_000);
2069        assert!(tiny.atlas + tiny.surface <= 20_000);
2070
2071        // normal repo: 60/40
2072        let normal = ContextBudget::adaptive(20_000, 1_000, 5, 4, 10);
2073        let normal_ratio = normal.surface as f64 / normal.total as f64;
2074        assert!(
2075            (normal_ratio - 0.40).abs() <= 0.01,
2076            "normal: surface share {normal_ratio}"
2077        );
2078
2079        // large repo (component-heavy): 65/35
2080        let large = ContextBudget::adaptive(20_000, 1_000, 40, 4, 10);
2081        let large_ratio = large.surface as f64 / large.total as f64;
2082        assert!(
2083            (large_ratio - 0.35).abs() <= 0.01,
2084            "large: surface share {large_ratio}"
2085        );
2086
2087        // massive repo: 70/30 and the surface slice is absolutely capped
2088        let massive = ContextBudget::adaptive(20_000, 25_000, 60, 30, 10);
2089        assert!(massive.atlas > massive.surface);
2090        let massive_ratio = massive.surface as f64 / massive.total as f64;
2091        assert!(
2092            (massive_ratio - 0.30).abs() <= 0.01,
2093            "massive: surface share {massive_ratio}"
2094        );
2095        // the absolute cap binds under a huge total
2096        let massive_huge = ContextBudget::adaptive(100_000, 25_000, 60, 30, 10);
2097        assert_eq!(
2098            massive_huge.surface, MASSIVE_SURFACE_CAP,
2099            "massive surface absolutely capped"
2100        );
2101
2102        // tiny and large must produce different atlas/surface splits
2103        assert_ne!(
2104            tiny.atlas as f64 / tiny.surface as f64,
2105            large.atlas as f64 / large.surface as f64,
2106            "tiny vs large splits must differ"
2107        );
2108    }
2109
2110    #[test]
2111// trace:exempt reason=internal-detail
2112// trace:exempt reason=internal-detail
2113    fn adaptive_budget_candidate_pool_boosts_surface_share() {
2114        // 10k candidates: +5pp surface share (10_000 / 2_000 = 5, capped at 5)
2115        let boosted = ContextBudget::adaptive(20_000, 1_000, 5, 4, 10_000);
2116        let boosted_ratio = boosted.surface as f64 / boosted.total as f64;
2117        assert!(
2118            (boosted_ratio - 0.45).abs() <= 0.01,
2119            "boosted: surface share {boosted_ratio}"
2120        );
2121        // surface never over 50% of the total
2122        let huge = ContextBudget::adaptive(20_000, 100, 2, 1, 100_000);
2123        assert!(huge.surface <= huge.total / 2, "surface capped at 50%");
2124    }
2125
2126    #[test]
2127// trace:exempt reason=internal-detail
2128// trace:exempt reason=internal-detail
2129    fn adaptive_budget_is_deterministic_and_total_preserving() {
2130        let a = ContextBudget::adaptive(15_000, 3_000, 8, 6, 500);
2131        let b = ContextBudget::adaptive(15_000, 3_000, 8, 6, 500);
2132        assert_eq!(a, b);
2133        assert_eq!(a.total, 15_000);
2134        assert!(a.atlas + a.surface <= 15_000);
2135    }
2136
2137    #[test]
2138// trace:v1 id=impl.crates-scc-core-src-lib-context-budget.contract-kind-renders-as-operation-lines work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
2139// trace:exempt reason=internal-detail
2140    fn contract_kind_renders_as_operation_lines() {
2141        let mut c = Contract::new(
2142            "repo://repo/contract/http/get--api-x",
2143            "http",
2144            "repo://repo/symbol/main.py/handler",
2145        );
2146        c.operations.push("GET /api/x".into());
2147        c.consumers.push("repo://repo/symbol/main.py/handler".into());
2148        let lines: Vec<String> = c
2149            .operations
2150            .iter()
2151            .map(|op| format!("{}: {}", c.kind, op))
2152            .collect();
2153        assert_eq!(lines, vec!["http: GET /api/x"]);
2154    }
2155
2156    #[test]
2157// trace:v1 id=impl.crates-scc-core-src-lib-context-budget.contract-subclass-ontology-maps-and-renders work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
2158// trace:exempt reason=internal-detail
2159    fn contract_subclass_ontology_maps_and_renders() {
2160        // Render prefixes per-subclass (the atlas CONTRACTS group prefixes).
2161        assert_eq!(ContractSubclass::Http.as_str(), "http");
2162        assert_eq!(ContractSubclass::Cli.as_str(), "cli");
2163        assert_eq!(ContractSubclass::Event.as_str(), "event");
2164        assert_eq!(ContractSubclass::Configuration.as_str(), "config");
2165        assert_eq!(ContractSubclass::PublicApi.as_str(), "public-api");
2166        assert_eq!(ContractSubclass::Extension.as_str(), "extension");
2167        assert_eq!(ContractSubclass::Serialization.as_str(), "serialization");
2168        assert_eq!(ContractSubclass::CallContract.as_str(), "call");
2169        assert_eq!(ContractSubclass::Rpc.as_str(), "rpc");
2170        assert_eq!(ContractSubclass::Message.as_str(), "message");
2171        assert_eq!(ContractSubclass::Schema.as_str(), "schema");
2172        assert_eq!(ContractSubclass::Plugin.as_str(), "plugin");
2173
2174        // Derivation from contract/registration kind strings (the ontology
2175        // mapping the atlas applies to extractor-emitted facts).
2176        assert_eq!(ContractSubclass::from_kind_str("http"), Some(ContractSubclass::Http));
2177        assert_eq!(ContractSubclass::from_kind_str("route"), Some(ContractSubclass::Http));
2178        assert_eq!(ContractSubclass::from_kind_str("cli"), Some(ContractSubclass::Cli));
2179        assert_eq!(ContractSubclass::from_kind_str("event"), Some(ContractSubclass::Event));
2180        assert_eq!(
2181            ContractSubclass::from_kind_str("config"),
2182            Some(ContractSubclass::Configuration)
2183        );
2184        assert_eq!(
2185            ContractSubclass::from_kind_str("next-config"),
2186            Some(ContractSubclass::Configuration)
2187        );
2188        // builder = Configuration, factory = PublicApi (the ontology rule).
2189        assert_eq!(
2190            ContractSubclass::from_kind_str("builder"),
2191            Some(ContractSubclass::Configuration)
2192        );
2193        assert_eq!(
2194            ContractSubclass::from_kind_str("factory"),
2195            Some(ContractSubclass::PublicApi)
2196        );
2197        assert_eq!(
2198            ContractSubclass::from_kind_str("serialization"),
2199            Some(ContractSubclass::Serialization)
2200        );
2201        assert_eq!(
2202            ContractSubclass::from_kind_str("extension"),
2203            Some(ContractSubclass::Extension)
2204        );
2205        assert_eq!(ContractSubclass::from_kind_str("plugin"), Some(ContractSubclass::Plugin));
2206        assert_eq!(ContractSubclass::from_kind_str("rpc"), Some(ContractSubclass::Rpc));
2207        assert_eq!(ContractSubclass::from_kind_str("message"), Some(ContractSubclass::Message));
2208        assert_eq!(ContractSubclass::from_kind_str("schema"), Some(ContractSubclass::Schema));
2209        assert_eq!(ContractSubclass::from_kind_str("task"), Some(ContractSubclass::CallContract));
2210        // framework-specific registration kinds are NOT first-class contracts
2211        assert_eq!(ContractSubclass::from_kind_str("include_router"), None);
2212        assert_eq!(ContractSubclass::from_kind_str("add_middleware"), None);
2213
2214        // Contract carries the subclass; `new` defaults to Http, serde
2215        // roundtrip preserves it, and a missing field (legacy JSON) defaults.
2216        let mut c = Contract::new(
2217            "repo://repo/contract/http/get--api-x",
2218            "http",
2219            "repo://repo/symbol/main.py/handler",
2220        );
2221        assert_eq!(c.subclass, ContractSubclass::Http);
2222        c.subclass = ContractSubclass::Serialization;
2223        let json = serde_json::to_string(&c).unwrap();
2224        let back: Contract = serde_json::from_str(&json).unwrap();
2225        assert_eq!(back.subclass, ContractSubclass::Serialization);
2226        let legacy = serde_json::json!({
2227            "id": "repo://repo/contract/x",
2228            "kind": "http",
2229            "producer": "p",
2230            "operations": ["GET /x"],
2231            "evidence": [],
2232            "consumers": [],
2233        });
2234        let c2: Contract = serde_json::from_value(legacy).unwrap();
2235        assert_eq!(c2.subclass, ContractSubclass::Http);
2236    }
2237
2238    #[test]
2239// trace:v1 id=impl.crates-scc-core-src-lib-context-budget.invocation-surface-kinds-stringify work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
2240// trace:exempt reason=internal-detail
2241    fn invocation_surface_kinds_stringify() {
2242        assert_eq!(InvocationSurfaceKind::PublicApi.as_str(), "public_api");
2243        assert_eq!(InvocationSurfaceKind::Queue.as_str(), "queue");
2244        assert_eq!(InvocationSurfaceKind::Lifecycle.as_str(), "lifecycle");
2245        assert_eq!(InvocationSurfaceKind::FrameworkCallback.as_str(), "framework_callback");
2246        // serde roundtrip is snake_case-stable
2247        let json = serde_json::to_string(&InvocationSurfaceKind::PublicApi).unwrap();
2248        assert_eq!(json, "\"public_api\"");
2249        let back: InvocationSurfaceKind = serde_json::from_str(&json).unwrap();
2250        assert_eq!(back, InvocationSurfaceKind::PublicApi);
2251    }
2252
2253    #[test]
2254// trace:exempt reason=internal-detail
2255// trace:exempt reason=internal-detail
2256    fn surface_render_result_roundtrips_through_serde() {
2257        let r = SurfaceRenderResult {
2258            text: "SCC SYSTEM SURFACE MAP\n\n  function serve\n".into(),
2259            rendered_ids: vec!["repo://r/symbol/api.py/serve".into()],
2260            rendered_entries: vec![],
2261            omitted_ids: vec!["repo://r/symbol/api.py/internal".into()],
2262            omissions: vec![SurfaceOmission {
2263                count: 1,
2264                kind: "function".into(),
2265                reason: "token budget".into(),
2266            }],
2267            token_count: 7,
2268            critical_drops: vec![],
2269        };
2270        let json = serde_json::to_string(&r).unwrap();
2271        let back: SurfaceRenderResult = serde_json::from_str(&json).unwrap();
2272        assert_eq!(back, r);
2273        assert_eq!(back.rendered_ids[0], "repo://r/symbol/api.py/serve");
2274        assert_eq!(back.omissions[0].count, 1);
2275    }
2276
2277    #[test]
2278// trace:exempt reason=internal-detail
2279// trace:exempt reason=internal-detail
2280    fn rank_node_carries_kind_and_name() {
2281        let n = RankNode {
2282            id: "repo://r/contract/c1".into(),
2283            kind: kinds::CONTRACT.into(),
2284            name: "c1".into(),
2285        };
2286        let json = serde_json::to_string(&n).unwrap();
2287        let back: RankNode = serde_json::from_str(&json).unwrap();
2288        assert_eq!(back.kind, "contract");
2289        assert_eq!(back.name, "c1");
2290    }
2291
2292    #[test]
2293// trace:exempt reason=internal-detail
2294// trace:exempt reason=internal-detail
2295    fn context_artifact_content_hash_roundtrips_and_defaults() {
2296        // new field roundtrips
2297        let a = ContextArtifact {
2298            kind: "startup".into(),
2299            epoch: "e1".into(),
2300            renderer_version: "0.1.0".into(),
2301            trust_policy: "floor=0.85".into(),
2302            budget: ContextBudget::default(),
2303            sha256: "abc".into(),
2304            content_hash: "def".into(),
2305            text: "body".into(),
2306        };
2307        let json = serde_json::to_string(&a).unwrap();
2308        let back: ContextArtifact = serde_json::from_str(&json).unwrap();
2309        assert_eq!(back.content_hash, "def");
2310
2311        // legacy JSON without content_hash deserializes (default empty)
2312        let legacy = serde_json::json!({
2313            "kind": "startup",
2314            "epoch": "e1",
2315            "renderer_version": "0.1.0",
2316            "trust_policy": "floor=0.85",
2317            "budget": ContextBudget::default(),
2318            "sha256": "abc",
2319            "text": "body",
2320        });
2321        let c: ContextArtifact = serde_json::from_value(legacy).unwrap();
2322        assert_eq!(c.content_hash, "");
2323    }
2324
2325    #[test]
2326    // trace:v1 id=test.scc.core.predicate-registry-complete verifies=REQ-ontology-single-source exercises=impl.scc.core.ontology-registry
2327    fn predicate_and_kind_registries_are_complete() {
2328        // Mutation gate: dropping DEFINES/COMPOSES/EXPORTS from ALL used to
2329        // compile while ranking/export silently omitted those edges.
2330        let (kind_ids, predicate_ids) = ontology_registries();
2331        for required in [
2332            predicates::DEFINES,
2333            predicates::COMPOSES,
2334            predicates::EXPORTS,
2335            predicates::ANNOTATES,
2336            predicates::REGISTERS,
2337            predicates::INJECTS,
2338            predicates::HANDLES_CALLBACK,
2339            predicates::DECORATES,
2340            predicates::OCCURS,
2341        ] {
2342            assert!(
2343                predicate_ids.contains(&required),
2344                "predicates::ALL missing {required}"
2345            );
2346        }
2347        for required in [kinds::FIELD, kinds::SCHEMA, kinds::TRUST_BOUNDARY, kinds::OCCURRENCE]
2348        {
2349            assert!(kind_ids.contains(&required), "kinds::ALL missing {required}");
2350        }
2351        let mut seen = std::collections::BTreeSet::new();
2352        for p in predicate_ids {
2353            assert!(seen.insert(*p), "duplicate predicate in ALL: {p}");
2354        }
2355    }
2356}