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    /// Plugin-contributed facts preserved verbatim (§106): provider ->
1028    /// the ids this export carries from that provider. Core consumers
1029    /// ignore this section; extension-aware consumers resolve it.
1030    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1031    pub extensions: BTreeMap<String, Vec<String>>,
1032}
1033
1034// trace:exempt reason=internal-detail
1035impl SystemIr {
1036// trace:v1 id=impl.crates-scc-core-src-lib-systemir.empty work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1037    pub fn empty(repository: Repository, snapshot: Snapshot) -> Self {
1038        SystemIr {
1039            schema_version: SCHEMA_VERSION.to_string(),
1040            repository,
1041            snapshot,
1042            entities: Vec::new(),
1043            relationships: Vec::new(),
1044            flows: Vec::new(),
1045            invariants: Vec::new(),
1046            evidence: Vec::new(),
1047            extensions: BTreeMap::new(),
1048        }
1049    }
1050}
1051
1052// ---------------------------------------------------------------------------
1053// Identifiers
1054// ---------------------------------------------------------------------------
1055
1056/// Sanitize a free-form name into a stable URI key.
1057// trace:v1 id=impl.crates-scc-core-src-lib.sanitize-key work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1058pub fn sanitize_key(input: &str) -> String {
1059    let mut out = String::with_capacity(input.len());
1060    let mut prev_dash = false;
1061    for c in input.chars() {
1062        let ok = c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' || c == '/';
1063        if ok {
1064            // keep path separators but normalize repeated dashes
1065            if c == '/' {
1066                out.push('/');
1067                prev_dash = false;
1068            } else if c == '-' || c == '_' {
1069                if !prev_dash {
1070                    out.push('-');
1071                }
1072                prev_dash = true;
1073            } else {
1074                out.push(c.to_ascii_lowercase());
1075                prev_dash = false;
1076            }
1077        } else {
1078            if !prev_dash {
1079                out.push('-');
1080            }
1081            prev_dash = true;
1082        }
1083    }
1084    while out.ends_with('-') {
1085        out.pop();
1086    }
1087    if out.is_empty() {
1088        out.push_str("unnamed");
1089    }
1090    out
1091}
1092
1093/// `repo://{repo}/{kind}/{key}` stable identifier.
1094// trace:v1 id=impl.crates-scc-core-src-lib.entity-id work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1095pub fn entity_id(repo: &str, kind: &str, key: &str) -> String {
1096    format!("repo://{}/{}/{}", sanitize_key(repo), kind, sanitize_key(key))
1097}
1098
1099/// Occurrence entity id: collision-free per (concept key, path, owner,
1100/// line) — the identity occurrences carry so shared concepts never lose
1101/// per-file provenance. Unlike [`entity_id`], the concept/path/owner
1102/// components are percent-encoded (`@`-separated), so case and separator
1103/// distinctions (`a_b` vs `a-b`) never merge distinct occurrences.
1104// trace:v1 id=impl.scc.core.occurrence work=WORK-SCC-001 satisfies=REQ-SCC-IR
1105// trace:v1 id=impl.crates-scc-core-src-lib.occurrence-id work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1106pub fn occurrence_id(repo: &str, concept: &str, path: &str, owner: &str, line: u32) -> String {
1107    format!(
1108        "repo://{}/occurrence/{}@{}@{}@{}",
1109        sanitize_key(repo),
1110        encode_component(concept),
1111        encode_component(path),
1112        encode_component(owner),
1113        line
1114    )
1115}
1116
1117/// Percent-encode a path/name component for use inside an entity id while
1118/// preserving case and common separators (`/`, `.`, `_`, `-`). Collision-free
1119/// where `sanitize_key` would risk merging distinct names.
1120// trace:v1 id=impl.crates-scc-core-src-lib.encode-component work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1121pub fn encode_component(input: &str) -> String {
1122    let mut out = String::with_capacity(input.len());
1123    for b in input.bytes() {
1124        let keep = b.is_ascii_alphanumeric() || matches!(b, b'/' | b'.' | b'_' | b'-');
1125        if keep {
1126            out.push(b as char);
1127        } else {
1128            out.push('%');
1129            out.push_str(&format!("{b:02X}"));
1130        }
1131    }
1132    out
1133}
1134
1135/// Inverse of `encode_component`: percent-decodes `%XX` sequences back to
1136/// bytes. Used by benchmark/impact tooling to map entity ids back to names.
1137// trace:v1 id=impl.crates-scc-core-src-lib.decode-component work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1138pub fn decode_component(input: &str) -> String {
1139    let bytes = input.as_bytes();
1140    let mut out = Vec::with_capacity(bytes.len());
1141    let mut i = 0;
1142    while i < bytes.len() {
1143        if bytes[i] == b'%' && i + 2 < bytes.len() {
1144            let hi = (bytes[i + 1] as char).to_digit(16);
1145            let lo = (bytes[i + 2] as char).to_digit(16);
1146            if let (Some(hi), Some(lo)) = (hi, lo) {
1147                out.push((hi * 16 + lo) as u8);
1148                i += 3;
1149                continue;
1150            }
1151        }
1152        out.push(bytes[i]);
1153        i += 1;
1154    }
1155    String::from_utf8_lossy(&out).to_string()
1156}
1157
1158/// Stable symbol id: `repo://{repo}/symbol/{encoded-file}/{encoded-name}`.
1159// trace:v1 id=impl.crates-scc-core-src-lib.symbol-id work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1160pub fn symbol_id(repo: &str, file: &str, name: &str) -> String {
1161    format!(
1162        "repo://{}/symbol/{}/{}",
1163        sanitize_key(repo),
1164        encode_component(file),
1165        encode_component(name)
1166    )
1167}
1168
1169/// Evidence id namespace: `evidence:{n}` — stable within a snapshot, assigned
1170/// by the store.
1171// trace:v1 id=impl.crates-scc-core-src-lib.evidence-id work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1172pub fn evidence_id(n: u64) -> String {
1173    format!("evidence:{n}")
1174}
1175
1176/// Relationship id: `rel:{n}` — stable within a snapshot, assigned by store.
1177// trace:v1 id=impl.crates-scc-core-src-lib.relationship-id work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1178pub fn relationship_id(n: u64) -> String {
1179    format!("rel:{n}")
1180}
1181
1182// ---------------------------------------------------------------------------
1183// Entity kind / predicate constants
1184// ---------------------------------------------------------------------------
1185
1186pub mod kinds {
1187    pub const FILE: &str = "file";
1188    pub const SYMBOL: &str = "symbol";
1189    pub const PACKAGE: &str = "package";
1190    pub const MODULE: &str = "module";
1191    pub const SYSTEM: &str = "system";
1192    pub const SUBSYSTEM: &str = "subsystem";
1193    pub const SERVICE: &str = "service";
1194    pub const COMPONENT: &str = "component";
1195    pub const DEPLOYMENT_UNIT: &str = "deployment_unit";
1196    pub const ROUTE: &str = "route";
1197    pub const ENDPOINT: &str = "endpoint";
1198    pub const EVENT: &str = "event";
1199    pub const TOPIC: &str = "topic";
1200    pub const QUEUE: &str = "queue";
1201    pub const DATA_ENTITY: &str = "data";
1202    pub const DATA_STORE: &str = "store";
1203    pub const TABLE: &str = "table";
1204    pub const COLLECTION: &str = "collection";
1205    pub const CACHE: &str = "cache";
1206    pub const EXTERNAL_SYSTEM: &str = "external_system";
1207    pub const EXTERNAL_API: &str = "external_api";
1208    pub const CONFIGURATION: &str = "configuration";
1209    pub const FEATURE_FLAG: &str = "feature_flag";
1210    pub const SECRET_REFERENCE: &str = "secret_reference";
1211    pub const CONTRACT: &str = "contract";
1212    pub const INVARIANT: &str = "invariant";
1213    pub const TEST: &str = "test";
1214    pub const TEST_SUITE: &str = "test_suite";
1215    pub const FLOW: &str = "flow";
1216    pub const WORKFLOW: &str = "workflow";
1217    pub const STATE: &str = "state";
1218    // Semantic fact layer (Wave 9): first-class representations the
1219    // extractors emit for framework/library semantics.
1220    pub const EXPORT: &str = "export";
1221    pub const ANNOTATION: &str = "annotation";
1222    pub const FIELD: &str = "field";
1223    pub const REGISTRY: &str = "registry";
1224    pub const MIDDLEWARE: &str = "middleware";
1225    pub const DI_BINDING: &str = "di_binding";
1226    pub const TRANSITION: &str = "transition";
1227    pub const RESOURCE: &str = "resource";
1228    pub const TRUST_BOUNDARY: &str = "trust_boundary";
1229    pub const SECURITY_CONTROL: &str = "security_control";
1230    pub const RUNTIME_OBSERVATION: &str = "runtime_observation";
1231    // Wave 11: first-class schema and reactive-state contracts.
1232    pub const SCHEMA: &str = "schema";
1233    pub const REACTIVE: &str = "reactive";
1234    // Occurrence layer: one entity per (concept, path, owner, line) so
1235    // shared concepts never lose per-file provenance (Wave 13).
1236    pub const OCCURRENCE: &str = "occurrence";
1237
1238    /// All entity kinds. Tests fail if a `pub const` is added above and
1239    /// omitted here.
1240    pub const ALL: &[&str] = &[
1241        FILE, SYMBOL, PACKAGE, MODULE, SYSTEM, SUBSYSTEM, SERVICE, COMPONENT, DEPLOYMENT_UNIT,
1242        ROUTE, ENDPOINT, EVENT, TOPIC, QUEUE, DATA_ENTITY, DATA_STORE, TABLE, COLLECTION, CACHE,
1243        EXTERNAL_SYSTEM, EXTERNAL_API, CONFIGURATION, FEATURE_FLAG, SECRET_REFERENCE, CONTRACT,
1244        INVARIANT, TEST, TEST_SUITE, FLOW, WORKFLOW, STATE, EXPORT, ANNOTATION, FIELD, REGISTRY,
1245        MIDDLEWARE, DI_BINDING, TRANSITION, RESOURCE, TRUST_BOUNDARY, SECURITY_CONTROL,
1246        RUNTIME_OBSERVATION, SCHEMA, REACTIVE, OCCURRENCE,
1247    ];
1248}
1249
1250pub mod predicates {
1251    pub const CONTAINS: &str = "contains";
1252    pub const IMPLEMENTS: &str = "implements";
1253    pub const INHERITS: &str = "inherits";
1254    pub const IMPORTS: &str = "imports";
1255    pub const CALLS: &str = "calls";
1256    pub const READS: &str = "reads";
1257    pub const WRITES: &str = "writes";
1258    pub const QUERIES: &str = "queries";
1259    pub const OWNS: &str = "owns";
1260    pub const PUBLISHES: &str = "publishes";
1261    pub const CONSUMES: &str = "consumes";
1262    pub const SUBSCRIBES: &str = "subscribes";
1263    pub const PRODUCES: &str = "produces";
1264    pub const TRANSFORMS: &str = "transforms";
1265    pub const VALIDATES: &str = "validates";
1266    pub const DEFINES: &str = "defines";
1267    pub const COMPOSES: &str = "composes";
1268    pub const ROUTES_TO: &str = "routes_to";
1269    pub const HANDLES: &str = "handles";
1270    pub const INVOKES: &str = "invokes";
1271    pub const DEPENDS_ON: &str = "depends_on";
1272    pub const DEPLOYED_WITH: &str = "deployed_with";
1273    pub const DEPLOYED_IN: &str = "deployed_in";
1274    pub const CONFIGURED_BY: &str = "configured_by";
1275    pub const PROTECTED_BY: &str = "protected_by";
1276    pub const CROSSES_BOUNDARY: &str = "crosses_boundary";
1277    pub const ENFORCES: &str = "enforces";
1278    pub const TESTED_BY: &str = "tested_by";
1279    pub const PARTICIPATES_IN: &str = "participates_in";
1280    pub const PRECEDES: &str = "precedes";
1281    pub const FOLLOWS: &str = "follows";
1282    pub const BRANCHES_TO: &str = "branches_to";
1283    pub const RETRIES: &str = "retries";
1284    pub const FALLS_BACK_TO: &str = "falls_back_to";
1285    pub const OBSERVED_AS: &str = "observed_as";
1286    pub const DECLARED_AS: &str = "declared_as";
1287    pub const IMPLEMENTED_BY: &str = "implemented_by";
1288    // Semantic fact layer (Wave 9)
1289    pub const EXPORTS: &str = "exports";
1290    pub const ANNOTATES: &str = "annotates";
1291    pub const REGISTERS: &str = "registers";
1292    pub const INJECTS: &str = "injects";
1293    pub const HANDLES_CALLBACK: &str = "handles_callback";
1294    pub const DECORATES: &str = "decorates";
1295    /// An occurrence entity's attachment to its concept entity
1296    /// (occurrence OCCURS concept).
1297    pub const OCCURS: &str = "occurs";
1298
1299    /// All predicates in the documented ontology. Must include every
1300    /// `pub const` in this module — `kinds`/`predicates` drift is a
1301    /// silent ranking/export bug.
1302    pub const ALL: &[&str] = &[
1303        CONTAINS, IMPLEMENTS, INHERITS, IMPORTS, CALLS, READS, WRITES, QUERIES, OWNS, PUBLISHES,
1304        CONSUMES, SUBSCRIBES, PRODUCES, TRANSFORMS, VALIDATES, DEFINES, COMPOSES, ROUTES_TO,
1305        HANDLES, INVOKES, DEPENDS_ON, DEPLOYED_WITH, DEPLOYED_IN, CONFIGURED_BY, PROTECTED_BY,
1306        CROSSES_BOUNDARY, ENFORCES, TESTED_BY, PARTICIPATES_IN, PRECEDES, FOLLOWS, BRANCHES_TO,
1307        RETRIES, FALLS_BACK_TO, OBSERVED_AS, DECLARED_AS, IMPLEMENTED_BY, EXPORTS, ANNOTATES,
1308        REGISTERS, INJECTS, HANDLES_CALLBACK, DECORATES, OCCURS,
1309    ];
1310}
1311
1312/// Authoritative kind and predicate ids. Rankers, exporters, and tests
1313/// must derive from these slices rather than a second hand-maintained list.
1314// trace:v1 id=impl.scc.core.ontology-registry work=WORK-ripwire-lessons-phase1 satisfies=REQ-ontology-single-source
1315pub fn ontology_registries() -> (&'static [&'static str], &'static [&'static str]) {
1316    (kinds::ALL, predicates::ALL)
1317}
1318
1319// ---------------------------------------------------------------------------
1320// Token budgeting
1321// ---------------------------------------------------------------------------
1322
1323/// Rough token estimate: 4 characters per token (byte-based for ASCII, but we
1324/// operate on char count which is a close approximation across scripts).
1325// trace:v1 id=impl.crates-scc-core-src-lib.estimate-tokens work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1326pub fn estimate_tokens(text: &str) -> usize {
1327    let chars = text.chars().count();
1328    chars.div_ceil(4)
1329}
1330
1331/// Hard-truncate `text` to at most `budget` tokens, preferring a clean cut at
1332/// a line boundary.
1333// 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
1334pub fn truncate_to_budget(text: &str, budget: usize) -> String {
1335    if estimate_tokens(text) <= budget {
1336        return text.to_string();
1337    }
1338    let max_chars = budget.saturating_mul(4);
1339    let mut end = 0;
1340    let mut chars = 0;
1341    for (i, c) in text.char_indices() {
1342        chars += 1;
1343        if chars > max_chars {
1344            break;
1345        }
1346        end = i + c.len_utf8();
1347    }
1348    // Back off to the previous newline for a clean boundary (but keep at least
1349    // half the budget worth of content).
1350    let min_chars = max_chars / 2;
1351    let mut cut = end;
1352    if let Some(nl) = text[..end].rfind('\n') {
1353        let prefix_chars = text[..nl].chars().count();
1354        if prefix_chars >= min_chars {
1355            cut = nl;
1356        }
1357    }
1358    let mut out = text[..cut].to_string();
1359    out.push_str("\n… (truncated by token budget)");
1360    out
1361}
1362
1363// ---------------------------------------------------------------------------
1364// Misc
1365// ---------------------------------------------------------------------------
1366
1367// trace:v1 id=impl.crates-scc-core-src-lib.now-rfc3339 work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1368pub fn now_rfc3339() -> String {
1369    chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
1370}
1371
1372// ---------------------------------------------------------------------------
1373// Wave 14: System Surface Map — the actual callable code surface (Aider
1374// RepoMap equivalent built from System IR). Level 1 of the four-level
1375// context stack (docs/SYSTEM_DESIGN.md Wave 14).
1376// ---------------------------------------------------------------------------
1377
1378/// A source range: file path + 1-based inclusive line span.
1379#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
1380// trace:exempt reason=internal-detail
1381// trace:v1 id=impl.crates-scc-core-src-lib.SourceRange work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1382pub struct SourceRange {
1383    pub path: String,
1384    pub start_line: u32,
1385    pub end_line: u32,
1386}
1387
1388// trace:exempt reason=internal-detail
1389impl SourceRange {
1390// trace:exempt reason=internal-detail
1391    pub fn new(path: impl Into<String>, start_line: u32, end_line: u32) -> Self {
1392        SourceRange {
1393            path: path.into(),
1394            start_line,
1395            end_line,
1396        }
1397    }
1398}
1399
1400/// Symbol visibility as declared in source.
1401#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, schemars::JsonSchema)]
1402#[serde(rename_all = "snake_case")]
1403// trace:exempt reason=internal-detail
1404// trace:v1 id=impl.crates-scc-core-src-lib.Visibility work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1405pub enum Visibility {
1406    Public,
1407    Protected,
1408    Private,
1409    Package,
1410}
1411
1412// trace:exempt reason=internal-detail
1413impl Visibility {
1414// trace:exempt reason=internal-detail
1415    pub fn as_str(&self) -> &'static str {
1416        match self {
1417            Visibility::Public => "public",
1418            Visibility::Protected => "protected",
1419            Visibility::Private => "private",
1420            Visibility::Package => "package",
1421        }
1422    }
1423}
1424
1425/// The kind of code surface a definition exposes.
1426#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, schemars::JsonSchema)]
1427#[serde(rename_all = "snake_case")]
1428// trace:exempt reason=internal-detail
1429// trace:v1 id=impl.crates-scc-core-src-lib.SurfaceKind work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1430pub enum SurfaceKind {
1431    Function,
1432    Method,
1433    Constructor,
1434    Class,
1435    Interface,
1436    Trait,
1437    Type,
1438    Enum,
1439    Const,
1440    Module,
1441    Record,
1442}
1443
1444// trace:exempt reason=internal-detail
1445impl SurfaceKind {
1446// trace:exempt reason=internal-detail
1447    pub fn as_str(&self) -> &'static str {
1448        match self {
1449            SurfaceKind::Function => "function",
1450            SurfaceKind::Method => "method",
1451            SurfaceKind::Constructor => "constructor",
1452            SurfaceKind::Class => "class",
1453            SurfaceKind::Interface => "interface",
1454            SurfaceKind::Trait => "trait",
1455            SurfaceKind::Type => "type",
1456            SurfaceKind::Enum => "enum",
1457            SurfaceKind::Const => "const",
1458            SurfaceKind::Module => "module",
1459            SurfaceKind::Record => "record",
1460        }
1461    }
1462}
1463
1464/// One function/method parameter in structured form.
1465#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
1466// trace:exempt reason=internal-detail
1467// trace:v1 id=impl.crates-scc-core-src-lib.SemanticParameter work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1468pub struct SemanticParameter {
1469    pub name: String,
1470    #[serde(default, skip_serializing_if = "Option::is_none")]
1471    pub ty: Option<String>,
1472    /// `&self` / `self` receiver parameters.
1473    #[serde(default)]
1474    pub receiver: bool,
1475    #[serde(default, skip_serializing_if = "Option::is_none")]
1476    pub default: Option<String>,
1477    #[serde(default)]
1478    pub variadic: bool,
1479}
1480
1481/// The structured machine form of a signature — the semantic layer over
1482/// the exact source text. Benchmark matching uses this, never string
1483/// comparisons of source signatures alone.
1484#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, schemars::JsonSchema)]
1485// trace:exempt reason=internal-detail
1486// trace:v1 id=impl.crates-scc-core-src-lib.SemanticSignature work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1487pub struct SemanticSignature {
1488    pub name: String,
1489    #[serde(default, skip_serializing_if = "Option::is_none")]
1490    pub owner: Option<String>,
1491    #[serde(default)]
1492    pub visibility: Option<Visibility>,
1493    #[serde(default)]
1494    pub async_: bool,
1495    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1496    pub generic_parameters: Vec<String>,
1497    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1498    pub parameters: Vec<SemanticParameter>,
1499    #[serde(default, skip_serializing_if = "Option::is_none")]
1500    pub returns: Option<String>,
1501    /// `where` / trait-bound constraints (`T: Send + Sync`).
1502    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1503    pub constraints: Vec<String>,
1504}
1505
1506/// Why a surface entry earned its rank (explainability; `scc surface
1507/// --explain` renders this).
1508#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
1509// trace:exempt reason=internal-detail
1510// trace:v1 id=impl.crates-scc-core-src-lib.SurfaceRank work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1511pub struct SurfaceRank {
1512    pub task_ppr: f64,
1513    pub global_ppr: f64,
1514    pub lexical: f64,
1515    pub semantic: f64,
1516    pub confidence: f64,
1517    pub criticality: f64,
1518    pub change_risk: f64,
1519    pub novelty: f64,
1520    pub total: f64,
1521    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1522    pub reasons: Vec<String>,
1523}
1524
1525// trace:exempt reason=internal-detail
1526impl Default for SurfaceRank {
1527// trace:exempt reason=internal-detail
1528// trace:exempt reason=internal-detail
1529    fn default() -> Self {
1530        SurfaceRank {
1531            task_ppr: 0.0,
1532            global_ppr: 0.0,
1533            lexical: 0.0,
1534            semantic: 0.0,
1535            confidence: 0.0,
1536            criticality: 0.0,
1537            change_risk: 0.0,
1538            novelty: 0.0,
1539            total: 0.0,
1540            reasons: Vec::new(),
1541        }
1542    }
1543}
1544
1545/// One ranked definition on the system surface — a callable/typeable
1546/// reality of the architecture, with exact signatures and architectural
1547/// meaning attached.
1548#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
1549// trace:exempt reason=internal-detail
1550// trace:v1 id=impl.crates-scc-core-src-lib.SurfaceEntry work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1551pub struct SurfaceEntry {
1552    pub id: String,
1553    pub symbol_id: String,
1554    pub qualified_name: String,
1555    pub kind: SurfaceKind,
1556    pub path: String,
1557    pub range: SourceRange,
1558    /// Exact source representation of the signature.
1559    pub source_signature: String,
1560    /// Whitespace/dialect-normalized signature (dedupe/comparison/index).
1561    pub canonical_signature: String,
1562    pub semantic_signature: SemanticSignature,
1563    pub visibility: Visibility,
1564    #[serde(default)]
1565    pub exported: bool,
1566    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1567    pub modifiers: Vec<String>,
1568    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1569    pub annotations: Vec<String>,
1570    #[serde(default, skip_serializing_if = "Option::is_none")]
1571    pub component: Option<String>,
1572    #[serde(default, skip_serializing_if = "Option::is_none")]
1573    pub subsystem: Option<String>,
1574    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1575    pub flows: Vec<String>,
1576    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1577    pub contracts: Vec<String>,
1578    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1579    pub state_authorities: Vec<String>,
1580    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1581    pub invocation_surfaces: Vec<String>,
1582    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1583    pub callers: Vec<String>,
1584    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1585    pub callees: Vec<String>,
1586    /// Exact distinct fan-in/fan-out (the displayed name lists stay capped
1587    /// at 12; the counts are never truncated).
1588    #[serde(default)]
1589    pub caller_count: usize,
1590    #[serde(default)]
1591    pub callee_count: usize,
1592    /// Derived importance explanation (badges + counts + architecture
1593    /// signals). Populated by the surface pipeline; default-empty for
1594    /// entries built before ranking.
1595    #[serde(default, skip_serializing_if = "Option::is_none")]
1596    pub importance: Option<ImportanceProfile>,
1597    pub provenance: Provenance,
1598    #[serde(default)]
1599    pub confidence: f32,
1600    #[serde(default)]
1601    pub rank: SurfaceRank,
1602}
1603
1604/// A definition deliberately omitted by a token-budget cut — the artifact
1605/// never silently implies completeness.
1606/// Derived importance explanation for one surface symbol (audit item 3):
1607/// topology (exact fan-in/fan-out, never truncated), architecture signals,
1608/// and change impact — computed from data the pipeline ALREADY has. Badges
1609/// are derived labels over those numbers, not extra scoring magic: the
1610/// overall score stays `rank.total`.
1611#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
1612// trace:v1 id=impl.scc.core.importance-profile work=WORK-SI-MMMJA4G6 satisfies=REQ-SI-503JSBGP
1613pub struct ImportanceProfile {
1614    pub overall: f64,
1615    pub caller_count: usize,
1616    pub callee_count: usize,
1617    pub global_ppr: f64,
1618    pub task_ppr: f64,
1619    pub entrypoint: bool,
1620    pub exported: bool,
1621    pub flow_count: usize,
1622    pub contract_count: usize,
1623    pub state_read_count: usize,
1624    pub state_write_count: usize,
1625    pub dependent_count: usize,
1626    pub change_risk: f64,
1627    pub badges: Vec<String>,
1628}
1629
1630#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
1631// trace:exempt reason=internal-detail
1632// trace:v1 id=impl.crates-scc-core-src-lib.SurfaceOmission work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1633pub struct SurfaceOmission {
1634    pub count: usize,
1635    pub kind: String,
1636    pub reason: String,
1637}
1638
1639/// The System Surface Map: the ranked actual-API layer of a repository,
1640/// built from System IR (Level 1 of the context stack).
1641#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
1642// trace:exempt reason=internal-detail
1643// trace:v1 id=impl.crates-scc-core-src-lib.SystemSurfaceMap work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1644pub struct SystemSurfaceMap {
1645    pub repository: String,
1646    pub revision: String,
1647    pub epoch: String,
1648    pub entries: Vec<SurfaceEntry>,
1649    #[serde(default)]
1650    pub token_count: usize,
1651    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1652    pub omitted: Vec<SurfaceOmission>,
1653}
1654
1655/// The production surface render: the budget-selected subset plus honest
1656/// omission accounting (Wave 14F). `rendered_ids` are exactly the entries
1657/// the agent sees (ledger recording MUST use only these — omitted
1658/// candidates are never marked visible); `omitted_ids` are every candidate
1659/// the pipeline cut. `omissions` summarizes the cuts by kind.
1660#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
1661// trace:exempt reason=internal-detail
1662// trace:v1 id=impl.crates-scc-core-src-lib.SurfaceRenderResult work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1663pub struct SurfaceRenderResult {
1664    /// The rendered surface text (header + selected entry blocks).
1665    pub text: String,
1666    /// Entry ids actually rendered, in selection order.
1667    pub rendered_ids: Vec<String>,
1668    /// The rendered entries themselves (post hard-max pops — exactly the
1669    /// set `rendered_ids` names). Lets callers resolve rendered ids without
1670    /// recompiling the surface map. Wire-compatible: skipped when empty.
1671    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1672    pub rendered_entries: Vec<SurfaceEntry>,
1673    /// Candidate entry ids the pipeline omitted (budget/quotas/diversity).
1674    pub omitted_ids: Vec<String>,
1675    /// Per-kind omission summaries.
1676    pub omissions: Vec<SurfaceOmission>,
1677    /// Token estimate of `text`.
1678    pub token_count: usize,
1679    /// Required entries the hard-max invariant had to DROP after every
1680    /// compression level was exhausted (explicit CRITICAL omissions — the
1681    /// highest-ranked required entry is preserved whenever anything fits).
1682    /// Empty in every non-pathological render.
1683    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1684    pub critical_drops: Vec<String>,
1685}
1686
1687/// One node in the heterogeneous ranking universe (Wave 14B): any rankable
1688/// entity — symbol, component, subsystem, service, flow, contract, state,
1689/// reactive, route, topic, queue, store, schema, or file. The ranker walks
1690/// edges whose endpoints are rankable entities, so architectural importance
1691/// (flows, contracts, state) participates in PageRank directly.
1692#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1693// trace:exempt reason=internal-detail
1694// trace:v1 id=impl.crates-scc-core-src-lib.RankNode work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1695pub struct RankNode {
1696    /// The entity id (`repo://{repo}/{kind}/{key}`).
1697    pub id: String,
1698    /// Entity kind (`scc_core::kinds::*`).
1699    pub kind: String,
1700    /// Entity display name.
1701    pub name: String,
1702}
1703
1704/// A normalized reference between two symbols (Wave 14): the graph the
1705/// ranker walks. Many SCC relationships already express these concepts;
1706/// this layer normalizes them for ranking.
1707#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1708// trace:exempt reason=internal-detail
1709// trace:v1 id=impl.crates-scc-core-src-lib.ReferenceEdge work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1710pub struct ReferenceEdge {
1711    pub source_symbol: String,
1712    pub target_symbol: String,
1713    pub kind: ReferenceKind,
1714    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1715    pub locations: Vec<SourceRange>,
1716    pub provenance: Provenance,
1717    #[serde(default)]
1718    pub confidence: f32,
1719}
1720
1721#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
1722#[serde(rename_all = "snake_case")]
1723// trace:exempt reason=internal-detail
1724// trace:v1 id=impl.crates-scc-core-src-lib.ReferenceKind work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1725pub enum ReferenceKind {
1726    Read,
1727    Write,
1728    #[default]
1729    Call,
1730    TypeUse,
1731    Instantiate,
1732    Implement,
1733    Extend,
1734    Decorate,
1735    Register,
1736    Import,
1737    Export,
1738    Macro,
1739    FieldAccess,
1740    Construct,
1741}
1742
1743// trace:exempt reason=internal-detail
1744impl ReferenceKind {
1745// trace:exempt reason=internal-detail
1746    pub fn as_str(&self) -> &'static str {
1747        match self {
1748            ReferenceKind::Read => "read",
1749            ReferenceKind::Write => "write",
1750            ReferenceKind::Call => "call",
1751            ReferenceKind::TypeUse => "type_use",
1752            ReferenceKind::Instantiate => "instantiate",
1753            ReferenceKind::Implement => "implement",
1754            ReferenceKind::Extend => "extend",
1755            ReferenceKind::Decorate => "decorate",
1756            ReferenceKind::Register => "register",
1757            ReferenceKind::Import => "import",
1758            ReferenceKind::Export => "export",
1759            ReferenceKind::Macro => "macro",
1760            ReferenceKind::FieldAccess => "field_access",
1761            ReferenceKind::Construct => "construct",
1762        }
1763    }
1764}
1765
1766/// A token-optimized context candidate (Aider-style hard budget search).
1767#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1768// trace:exempt reason=internal-detail
1769// trace:v1 id=impl.crates-scc-core-src-lib.ContextItem work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1770pub struct ContextItem {
1771    pub id: String,
1772    pub value: f64,
1773    pub token_cost: usize,
1774    #[serde(default)]
1775    pub required: bool,
1776    #[serde(default, skip_serializing_if = "Option::is_none")]
1777    pub group: Option<String>,
1778}
1779
1780/// The startup/task context budget split (Wave 14 dynamic budgets).
1781#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1782// trace:v1 id=impl.crates-scc-core-src-lib.ContextBudget work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1783pub struct ContextBudget {
1784    pub total: usize,
1785    pub atlas: usize,
1786    pub surface: usize,
1787    pub task_delta: usize,
1788    pub structural_source: usize,
1789}
1790
1791// trace:exempt reason=internal-detail
1792impl Default for ContextBudget {
1793// trace:exempt reason=internal-detail
1794// trace:exempt reason=internal-detail
1795    fn default() -> Self {
1796        ContextBudget {
1797            total: 20_000,
1798            atlas: 13_000,
1799            surface: 7_000,
1800            task_delta: 3_000,
1801            structural_source: 6_000,
1802        }
1803    }
1804}
1805
1806/// Absolute ceiling on the massive-tier surface slice: a 20k-entity repo
1807/// must not hand the model an unbounded surface even under a huge total.
1808const MASSIVE_SURFACE_CAP: usize = 10_000;
1809
1810// trace:exempt reason=internal-detail  # impl grouping; adaptive below is traced
1811impl ContextBudget {
1812    /// Adaptive startup split: scale the Atlas/Surface allocation by repo
1813    /// complexity instead of the fixed 13:7 default. Tiers by entity count
1814    /// (component count escalates to `large`; flow count escalates to
1815    /// `architecture-heavy`):
1816    ///
1817    /// - tiny (`entity_count < 200`): 55/45 — a small atlas leaves room
1818    ///   for a proportionally larger surface;
1819    /// - ordinary: 60/40;
1820    /// - architecture-heavy (`flow_count > 40`): 65/35 — flows, routes and
1821    ///   contracts are Atlas content, so a flow-dense repo needs the Atlas
1822    ///   slice even when its entity count is ordinary;
1823    /// - large (`entity_count > 5000` or `component_count > 30`): 65/35 —
1824    ///   the atlas dominates;
1825    /// - massive (`entity_count > 20_000`): 70/30 with the surface slice
1826    ///   capped absolutely ([`MASSIVE_SURFACE_CAP`]) and no candidate
1827    ///   boost.
1828    ///
1829    /// Within a tier, the actual candidate pool feeds the surface share:
1830    /// every 2,000 surface candidates earns up to +5 percentage points
1831    /// (surface never over 50% of the total), so a repo whose surface map
1832    /// is genuinely large gets a proportionally larger surface slice.
1833    /// `total` is caller-supplied — adaptive scales the SPLIT, never the
1834    /// total. Deterministic and no-panic. Defaults stay for callers that
1835    /// do not adapt.
1836    // 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
1837    pub fn adaptive(
1838        total: usize,
1839        entity_count: usize,
1840        component_count: usize,
1841        flow_count: usize,
1842        surface_candidates: usize,
1843    ) -> ContextBudget {
1844        let (surface_pct, boost, cap): (f64, f64, Option<usize>) = if entity_count > 20_000 {
1845            (30.0, 0.0, Some(MASSIVE_SURFACE_CAP))
1846        } else if entity_count > 5_000 || component_count > 30 {
1847            (35.0, 5.0, None)
1848        } else if flow_count > 40 {
1849            // Architecture-heavy: flows/routes/contracts are Atlas content.
1850            // Checked BEFORE the tiny tier — a small but flow-dense repo
1851            // still needs the Atlas slice.
1852            (35.0, 5.0, None)
1853        } else if entity_count < 200 {
1854            (45.0, 5.0, None)
1855        } else {
1856            (40.0, 5.0, None)
1857        };
1858        let boost_pp = (surface_candidates / 2_000).min(boost as usize) as f64;
1859        let surface_pct = (surface_pct + boost_pp).min(50.0);
1860        let atlas_pct = 100.0 - surface_pct;
1861        let mut surface = ((total as f64) * surface_pct / 100.0).round() as usize;
1862        if let Some(c) = cap {
1863            surface = surface.min(c);
1864        }
1865        let atlas = ((total as f64) * atlas_pct / 100.0).round() as usize;
1866        let def = ContextBudget::default();
1867        ContextBudget {
1868            total,
1869            atlas,
1870            surface,
1871            task_delta: def.task_delta,
1872            structural_source: def.structural_source,
1873        }
1874    }
1875}
1876
1877/// What the agent has already seen — novelty suppression source (the
1878/// general form of Aider treating chat files specially).
1879#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1880// trace:exempt reason=internal-detail
1881// trace:v1 id=impl.crates-scc-core-src-lib.ContextLedger work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1882pub struct ContextLedger {
1883    pub model_epoch: String,
1884    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
1885    pub visible_entities: BTreeSet<String>,
1886    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
1887    pub visible_symbols: BTreeSet<String>,
1888    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
1889    pub visible_files: BTreeSet<String>,
1890    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
1891    pub visible_components: BTreeSet<String>,
1892    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
1893    pub visible_flows: BTreeSet<String>,
1894    #[serde(default, skip_serializing_if = "Option::is_none")]
1895    pub last_task: Option<String>,
1896}
1897
1898/// One structural-source unit: semantic skeleton of an implementation
1899/// slice (Level 2), with provenance back to the exact source.
1900#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1901// trace:exempt reason=internal-detail
1902// trace:v1 id=impl.crates-scc-core-src-lib.StructuralSourceUnit work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1903pub struct StructuralSourceUnit {
1904    pub path: String,
1905    /// `source: <path>:L<start>-L<end>` provenance line.
1906    pub source: String,
1907    pub representation: String,
1908    pub revision: String,
1909    pub content: String,
1910    /// Stable content handle for lazy exact-source retrieval. Empty when
1911    /// the compiler has no repo/epoch identity.
1912    #[serde(default, skip_serializing_if = "String::is_empty")]
1913    pub handle: String,
1914    /// Why EXACT vs STRUCTURAL/SIGNATURES was chosen. Empty when no
1915    /// on-disk body was available to compare.
1916    #[serde(default, skip_serializing_if = "String::is_empty")]
1917    pub representation_reason: String,
1918}
1919
1920/// The deterministic startup artifact (Atlas + Surface), hash-stable per
1921/// epoch so prompt caches hit.
1922#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1923// trace:exempt reason=internal-detail
1924// trace:v1 id=impl.crates-scc-core-src-lib.ContextArtifact work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1925pub struct ContextArtifact {
1926    pub kind: String,
1927    pub epoch: String,
1928    pub renderer_version: String,
1929    pub trust_policy: String,
1930    pub budget: ContextBudget,
1931    /// Deterministic config-only hash (epoch + renderer + policy + budget) —
1932    /// the prompt-cache key, stable per epoch. Field name kept for the JSON
1933    /// contract.
1934    pub sha256: String,
1935    /// Hash over the *actual rendered content* (config preimage + rendered
1936    /// text), so a content change that keeps the config identical still
1937    /// changes the hash (the audit's name/content mismatch fix).
1938    #[serde(default, skip_serializing_if = "String::is_empty")]
1939    pub content_hash: String,
1940    pub text: String,
1941}
1942
1943/// One task-seed resolution: task language -> SCC entities.
1944#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1945// trace:exempt reason=internal-detail
1946// trace:v1 id=impl.crates-scc-core-src-lib.TaskSeed work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching
1947pub struct TaskSeed {
1948    pub kind: String,
1949    pub id: String,
1950    pub weight: f64,
1951}
1952
1953#[cfg(test)]
1954mod tests {
1955    use super::*;
1956    #[test]
1957    // 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
1958    fn estimate_tokens_is_chars_ceiling() {
1959        // Authoritative estimator: chars/4 ceiling. All budget paths
1960        // (Rust packs, CLI builders, Python harness mirrors) must agree.
1961        assert_eq!(estimate_tokens(""), 0);
1962        assert_eq!(estimate_tokens("a"), 1);
1963        assert_eq!(estimate_tokens("abcd"), 1);
1964        assert_eq!(estimate_tokens("abcde"), 2);
1965        assert_eq!(estimate_tokens("fn main() { println!(\"hi\"); }"), 8);
1966        // Unicode counts code points (Rust chars), not bytes or graphemes.
1967        assert_eq!(estimate_tokens("日本語"), 1);
1968        assert_eq!(estimate_tokens("日本語テスト"), 2);
1969        // Long identifier-heavy paths stay proportional.
1970        let path = "crates/scc-context/src/surface/structural_source_selection_policy.rs";
1971        assert_eq!(estimate_tokens(path), path.chars().count().div_ceil(4));
1972    }
1973
1974    #[test]
1975// 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
1976// trace:exempt reason=internal-detail
1977    fn component_encode_decode_roundtrip() {
1978        for name in [
1979            "Normalizer",
1980            "GET /api/transcripts/:id handler",
1981            "OrderStateMachine.advance",
1982            "test_normalization_preserves_raw",
1983            "src/app.ts",
1984            "a b%c",
1985        ] {
1986            assert_eq!(decode_component(&encode_component(name)), name, "{name}");
1987        }
1988    }
1989
1990    #[test]
1991// 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
1992// trace:exempt reason=internal-detail
1993    fn component_ids_are_collision_free() {
1994        assert_ne!(
1995            encode_component("foo_bar"),
1996            encode_component("foo-bar"),
1997            "underscore and dash must not collide"
1998        );
1999    }
2000
2001// trace:exempt reason=internal-detail
2002
2003    #[test]
2004// trace:exempt reason=internal-detail
2005// trace:exempt reason=internal-detail
2006    fn occurrence_ids_are_collision_free_per_site() {
2007        // distinct paths/owners/lines/concepts never merge
2008        assert_ne!(
2009            occurrence_id("r", "expr", "a.ts", "A", 1),
2010            occurrence_id("r", "expr", "b.ts", "A", 1)
2011        );
2012        assert_ne!(
2013            occurrence_id("r", "expr", "a.ts", "A", 1),
2014            occurrence_id("r", "expr", "a.ts", "B", 1)
2015        );
2016        assert_ne!(
2017            occurrence_id("r", "expr", "a.ts", "A", 1),
2018            occurrence_id("r", "expr", "a.ts", "A", 2)
2019        );
2020        assert_ne!(
2021            occurrence_id("r", "expr1", "a.ts", "A", 1),
2022            occurrence_id("r", "expr2", "a.ts", "A", 1)
2023        );
2024        // case/separator distinctions survive (unlike sanitize_key)
2025        assert_ne!(
2026            occurrence_id("r", "expr", "a_b.ts", "A", 1),
2027            occurrence_id("r", "expr", "a-b.ts", "A", 1)
2028        );
2029        assert_ne!(
2030            occurrence_id("r", "expr", "a.ts", "Foo", 1),
2031            occurrence_id("r", "expr", "a.ts", "foo", 1)
2032        );
2033        // deterministic
2034        assert_eq!(
2035            occurrence_id("r", "expr", "a.ts", "A", 1),
2036            occurrence_id("r", "expr", "a.ts", "A", 1)
2037        );
2038    }
2039
2040// trace:exempt reason=internal-detail
2041
2042    #[test]
2043// trace:exempt reason=internal-detail
2044// trace:exempt reason=internal-detail
2045    fn occurrence_roundtrips_through_serde() {
2046        let o = Occurrence {
2047            id: occurrence_id("r", "z.object({ x: z.string() })", "src/a.ts", "make", 7),
2048            concept: entity_id("r", kinds::SCHEMA, "z.object({ x: z.string() })"),
2049            path: "src/a.ts".into(),
2050            owner: "make".into(),
2051            line: 7,
2052        };
2053        let json = serde_json::to_string(&o).unwrap();
2054        let back: Occurrence = serde_json::from_str(&json).unwrap();
2055        assert_eq!(back.id, o.id);
2056        assert_eq!(back.concept, o.concept);
2057        assert_eq!(back.line, 7);
2058    }
2059
2060// trace:exempt reason=internal-detail
2061
2062    #[test]
2063// trace:exempt reason=internal-detail
2064// trace:exempt reason=internal-detail
2065    fn adaptive_budget_scales_split_by_repo_complexity() {
2066        // tiny repo: 55/45 split — a small atlas leaves room for a
2067        // proportionally larger surface (45% share, vs the normal 40%)
2068        let tiny = ContextBudget::adaptive(20_000, 50, 2, 1, 10);
2069        let tiny_ratio = tiny.surface as f64 / tiny.total as f64;
2070        assert!(
2071            (tiny_ratio - 0.45).abs() <= 0.01,
2072            "tiny: surface share {tiny_ratio}"
2073        );
2074        assert_eq!(tiny.total, 20_000);
2075        assert!(tiny.atlas + tiny.surface <= 20_000);
2076
2077        // normal repo: 60/40
2078        let normal = ContextBudget::adaptive(20_000, 1_000, 5, 4, 10);
2079        let normal_ratio = normal.surface as f64 / normal.total as f64;
2080        assert!(
2081            (normal_ratio - 0.40).abs() <= 0.01,
2082            "normal: surface share {normal_ratio}"
2083        );
2084
2085        // large repo (component-heavy): 65/35
2086        let large = ContextBudget::adaptive(20_000, 1_000, 40, 4, 10);
2087        let large_ratio = large.surface as f64 / large.total as f64;
2088        assert!(
2089            (large_ratio - 0.35).abs() <= 0.01,
2090            "large: surface share {large_ratio}"
2091        );
2092
2093        // massive repo: 70/30 and the surface slice is absolutely capped
2094        let massive = ContextBudget::adaptive(20_000, 25_000, 60, 30, 10);
2095        assert!(massive.atlas > massive.surface);
2096        let massive_ratio = massive.surface as f64 / massive.total as f64;
2097        assert!(
2098            (massive_ratio - 0.30).abs() <= 0.01,
2099            "massive: surface share {massive_ratio}"
2100        );
2101        // the absolute cap binds under a huge total
2102        let massive_huge = ContextBudget::adaptive(100_000, 25_000, 60, 30, 10);
2103        assert_eq!(
2104            massive_huge.surface, MASSIVE_SURFACE_CAP,
2105            "massive surface absolutely capped"
2106        );
2107
2108        // tiny and large must produce different atlas/surface splits
2109        assert_ne!(
2110            tiny.atlas as f64 / tiny.surface as f64,
2111            large.atlas as f64 / large.surface as f64,
2112            "tiny vs large splits must differ"
2113        );
2114    }
2115
2116    #[test]
2117// trace:exempt reason=internal-detail
2118// trace:exempt reason=internal-detail
2119    fn adaptive_budget_candidate_pool_boosts_surface_share() {
2120        // 10k candidates: +5pp surface share (10_000 / 2_000 = 5, capped at 5)
2121        let boosted = ContextBudget::adaptive(20_000, 1_000, 5, 4, 10_000);
2122        let boosted_ratio = boosted.surface as f64 / boosted.total as f64;
2123        assert!(
2124            (boosted_ratio - 0.45).abs() <= 0.01,
2125            "boosted: surface share {boosted_ratio}"
2126        );
2127        // surface never over 50% of the total
2128        let huge = ContextBudget::adaptive(20_000, 100, 2, 1, 100_000);
2129        assert!(huge.surface <= huge.total / 2, "surface capped at 50%");
2130    }
2131
2132    #[test]
2133// trace:exempt reason=internal-detail
2134// trace:exempt reason=internal-detail
2135    fn adaptive_budget_is_deterministic_and_total_preserving() {
2136        let a = ContextBudget::adaptive(15_000, 3_000, 8, 6, 500);
2137        let b = ContextBudget::adaptive(15_000, 3_000, 8, 6, 500);
2138        assert_eq!(a, b);
2139        assert_eq!(a.total, 15_000);
2140        assert!(a.atlas + a.surface <= 15_000);
2141    }
2142
2143    #[test]
2144// 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
2145// trace:exempt reason=internal-detail
2146    fn contract_kind_renders_as_operation_lines() {
2147        let mut c = Contract::new(
2148            "repo://repo/contract/http/get--api-x",
2149            "http",
2150            "repo://repo/symbol/main.py/handler",
2151        );
2152        c.operations.push("GET /api/x".into());
2153        c.consumers.push("repo://repo/symbol/main.py/handler".into());
2154        let lines: Vec<String> = c
2155            .operations
2156            .iter()
2157            .map(|op| format!("{}: {}", c.kind, op))
2158            .collect();
2159        assert_eq!(lines, vec!["http: GET /api/x"]);
2160    }
2161
2162    #[test]
2163// 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
2164// trace:exempt reason=internal-detail
2165    fn contract_subclass_ontology_maps_and_renders() {
2166        // Render prefixes per-subclass (the atlas CONTRACTS group prefixes).
2167        assert_eq!(ContractSubclass::Http.as_str(), "http");
2168        assert_eq!(ContractSubclass::Cli.as_str(), "cli");
2169        assert_eq!(ContractSubclass::Event.as_str(), "event");
2170        assert_eq!(ContractSubclass::Configuration.as_str(), "config");
2171        assert_eq!(ContractSubclass::PublicApi.as_str(), "public-api");
2172        assert_eq!(ContractSubclass::Extension.as_str(), "extension");
2173        assert_eq!(ContractSubclass::Serialization.as_str(), "serialization");
2174        assert_eq!(ContractSubclass::CallContract.as_str(), "call");
2175        assert_eq!(ContractSubclass::Rpc.as_str(), "rpc");
2176        assert_eq!(ContractSubclass::Message.as_str(), "message");
2177        assert_eq!(ContractSubclass::Schema.as_str(), "schema");
2178        assert_eq!(ContractSubclass::Plugin.as_str(), "plugin");
2179
2180        // Derivation from contract/registration kind strings (the ontology
2181        // mapping the atlas applies to extractor-emitted facts).
2182        assert_eq!(ContractSubclass::from_kind_str("http"), Some(ContractSubclass::Http));
2183        assert_eq!(ContractSubclass::from_kind_str("route"), Some(ContractSubclass::Http));
2184        assert_eq!(ContractSubclass::from_kind_str("cli"), Some(ContractSubclass::Cli));
2185        assert_eq!(ContractSubclass::from_kind_str("event"), Some(ContractSubclass::Event));
2186        assert_eq!(
2187            ContractSubclass::from_kind_str("config"),
2188            Some(ContractSubclass::Configuration)
2189        );
2190        assert_eq!(
2191            ContractSubclass::from_kind_str("next-config"),
2192            Some(ContractSubclass::Configuration)
2193        );
2194        // builder = Configuration, factory = PublicApi (the ontology rule).
2195        assert_eq!(
2196            ContractSubclass::from_kind_str("builder"),
2197            Some(ContractSubclass::Configuration)
2198        );
2199        assert_eq!(
2200            ContractSubclass::from_kind_str("factory"),
2201            Some(ContractSubclass::PublicApi)
2202        );
2203        assert_eq!(
2204            ContractSubclass::from_kind_str("serialization"),
2205            Some(ContractSubclass::Serialization)
2206        );
2207        assert_eq!(
2208            ContractSubclass::from_kind_str("extension"),
2209            Some(ContractSubclass::Extension)
2210        );
2211        assert_eq!(ContractSubclass::from_kind_str("plugin"), Some(ContractSubclass::Plugin));
2212        assert_eq!(ContractSubclass::from_kind_str("rpc"), Some(ContractSubclass::Rpc));
2213        assert_eq!(ContractSubclass::from_kind_str("message"), Some(ContractSubclass::Message));
2214        assert_eq!(ContractSubclass::from_kind_str("schema"), Some(ContractSubclass::Schema));
2215        assert_eq!(ContractSubclass::from_kind_str("task"), Some(ContractSubclass::CallContract));
2216        // framework-specific registration kinds are NOT first-class contracts
2217        assert_eq!(ContractSubclass::from_kind_str("include_router"), None);
2218        assert_eq!(ContractSubclass::from_kind_str("add_middleware"), None);
2219
2220        // Contract carries the subclass; `new` defaults to Http, serde
2221        // roundtrip preserves it, and a missing field (legacy JSON) defaults.
2222        let mut c = Contract::new(
2223            "repo://repo/contract/http/get--api-x",
2224            "http",
2225            "repo://repo/symbol/main.py/handler",
2226        );
2227        assert_eq!(c.subclass, ContractSubclass::Http);
2228        c.subclass = ContractSubclass::Serialization;
2229        let json = serde_json::to_string(&c).unwrap();
2230        let back: Contract = serde_json::from_str(&json).unwrap();
2231        assert_eq!(back.subclass, ContractSubclass::Serialization);
2232        let legacy = serde_json::json!({
2233            "id": "repo://repo/contract/x",
2234            "kind": "http",
2235            "producer": "p",
2236            "operations": ["GET /x"],
2237            "evidence": [],
2238            "consumers": [],
2239        });
2240        let c2: Contract = serde_json::from_value(legacy).unwrap();
2241        assert_eq!(c2.subclass, ContractSubclass::Http);
2242    }
2243
2244    #[test]
2245// 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
2246// trace:exempt reason=internal-detail
2247    fn invocation_surface_kinds_stringify() {
2248        assert_eq!(InvocationSurfaceKind::PublicApi.as_str(), "public_api");
2249        assert_eq!(InvocationSurfaceKind::Queue.as_str(), "queue");
2250        assert_eq!(InvocationSurfaceKind::Lifecycle.as_str(), "lifecycle");
2251        assert_eq!(InvocationSurfaceKind::FrameworkCallback.as_str(), "framework_callback");
2252        // serde roundtrip is snake_case-stable
2253        let json = serde_json::to_string(&InvocationSurfaceKind::PublicApi).unwrap();
2254        assert_eq!(json, "\"public_api\"");
2255        let back: InvocationSurfaceKind = serde_json::from_str(&json).unwrap();
2256        assert_eq!(back, InvocationSurfaceKind::PublicApi);
2257    }
2258
2259    #[test]
2260// trace:exempt reason=internal-detail
2261// trace:exempt reason=internal-detail
2262    fn surface_render_result_roundtrips_through_serde() {
2263        let r = SurfaceRenderResult {
2264            text: "SCC SYSTEM SURFACE MAP\n\n  function serve\n".into(),
2265            rendered_ids: vec!["repo://r/symbol/api.py/serve".into()],
2266            rendered_entries: vec![],
2267            omitted_ids: vec!["repo://r/symbol/api.py/internal".into()],
2268            omissions: vec![SurfaceOmission {
2269                count: 1,
2270                kind: "function".into(),
2271                reason: "token budget".into(),
2272            }],
2273            token_count: 7,
2274            critical_drops: vec![],
2275        };
2276        let json = serde_json::to_string(&r).unwrap();
2277        let back: SurfaceRenderResult = serde_json::from_str(&json).unwrap();
2278        assert_eq!(back, r);
2279        assert_eq!(back.rendered_ids[0], "repo://r/symbol/api.py/serve");
2280        assert_eq!(back.omissions[0].count, 1);
2281    }
2282
2283    #[test]
2284// trace:exempt reason=internal-detail
2285// trace:exempt reason=internal-detail
2286    fn rank_node_carries_kind_and_name() {
2287        let n = RankNode {
2288            id: "repo://r/contract/c1".into(),
2289            kind: kinds::CONTRACT.into(),
2290            name: "c1".into(),
2291        };
2292        let json = serde_json::to_string(&n).unwrap();
2293        let back: RankNode = serde_json::from_str(&json).unwrap();
2294        assert_eq!(back.kind, "contract");
2295        assert_eq!(back.name, "c1");
2296    }
2297
2298    #[test]
2299// trace:exempt reason=internal-detail
2300// trace:exempt reason=internal-detail
2301    fn context_artifact_content_hash_roundtrips_and_defaults() {
2302        // new field roundtrips
2303        let a = ContextArtifact {
2304            kind: "startup".into(),
2305            epoch: "e1".into(),
2306            renderer_version: "0.1.0".into(),
2307            trust_policy: "floor=0.85".into(),
2308            budget: ContextBudget::default(),
2309            sha256: "abc".into(),
2310            content_hash: "def".into(),
2311            text: "body".into(),
2312        };
2313        let json = serde_json::to_string(&a).unwrap();
2314        let back: ContextArtifact = serde_json::from_str(&json).unwrap();
2315        assert_eq!(back.content_hash, "def");
2316
2317        // legacy JSON without content_hash deserializes (default empty)
2318        let legacy = serde_json::json!({
2319            "kind": "startup",
2320            "epoch": "e1",
2321            "renderer_version": "0.1.0",
2322            "trust_policy": "floor=0.85",
2323            "budget": ContextBudget::default(),
2324            "sha256": "abc",
2325            "text": "body",
2326        });
2327        let c: ContextArtifact = serde_json::from_value(legacy).unwrap();
2328        assert_eq!(c.content_hash, "");
2329    }
2330
2331    #[test]
2332    // trace:v1 id=test.scc.core.predicate-registry-complete verifies=REQ-ontology-single-source exercises=impl.scc.core.ontology-registry
2333    fn predicate_and_kind_registries_are_complete() {
2334        // Mutation gate: dropping DEFINES/COMPOSES/EXPORTS from ALL used to
2335        // compile while ranking/export silently omitted those edges.
2336        let (kind_ids, predicate_ids) = ontology_registries();
2337        for required in [
2338            predicates::DEFINES,
2339            predicates::COMPOSES,
2340            predicates::EXPORTS,
2341            predicates::ANNOTATES,
2342            predicates::REGISTERS,
2343            predicates::INJECTS,
2344            predicates::HANDLES_CALLBACK,
2345            predicates::DECORATES,
2346            predicates::OCCURS,
2347        ] {
2348            assert!(
2349                predicate_ids.contains(&required),
2350                "predicates::ALL missing {required}"
2351            );
2352        }
2353        for required in [kinds::FIELD, kinds::SCHEMA, kinds::TRUST_BOUNDARY, kinds::OCCURRENCE]
2354        {
2355            assert!(kind_ids.contains(&required), "kinds::ALL missing {required}");
2356        }
2357        let mut seen = std::collections::BTreeSet::new();
2358        for p in predicate_ids {
2359            assert!(seen.insert(*p), "duplicate predicate in ALL: {p}");
2360        }
2361    }
2362}