Skip to main content

scc_core/
resolution.rs

1//! Receiver classification, resolution honesty, and representation choice.
2//!
3//! These types are the extraction/resolution ontology. They do **not**
4//! replace System Atlas entities, Reality Graph kinds, or provenance.
5//! They describe how a reference was captured and how confidently it
6//! was bound — so agents can see incomplete truth instead of silent
7//! false certainty.
8
9use serde::{Deserialize, Serialize};
10
11/// How the callee's receiver was written at the call site.
12///
13/// Captured during extraction (or recovered from the callee string) so
14/// resolution can distinguish `this.method()` from `other.method()`
15/// without re-parsing. Field-chain receivers are classified rather than
16/// silently treated as the first identifier.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
18#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
19// trace:v1 id=impl.scc.core.recv-kind work=WORK-ripwire-lessons-phase1 satisfies=REQ-receiver-aware-resolution
20pub enum RecvKind {
21    None,
22    This,
23    #[serde(rename = "SELF")]
24    SelfRecv,
25    NamedVariable,
26    TypeQualified,
27    FieldOfThis,
28    FieldOfSelf,
29    FieldOfVariable,
30    Super,
31    StaticType,
32    #[default]
33    Unknown,
34}
35
36// trace:exempt reason=internal-detail
37impl RecvKind {
38    // trace:exempt reason=internal-detail
39    pub fn as_str(self) -> &'static str {
40        match self {
41            RecvKind::None => "NONE",
42            RecvKind::This => "THIS",
43            RecvKind::SelfRecv => "SELF",
44            RecvKind::NamedVariable => "NAMED_VARIABLE",
45            RecvKind::TypeQualified => "TYPE_QUALIFIED",
46            RecvKind::FieldOfThis => "FIELD_OF_THIS",
47            RecvKind::FieldOfSelf => "FIELD_OF_SELF",
48            RecvKind::FieldOfVariable => "FIELD_OF_VARIABLE",
49            RecvKind::Super => "SUPER",
50            RecvKind::StaticType => "STATIC_TYPE",
51            RecvKind::Unknown => "UNKNOWN_RECEIVER",
52        }
53    }
54
55    /// Receivers whose method must be a sibling of the enclosing type.
56    // trace:exempt reason=internal-detail
57    pub fn is_instance_self(self) -> bool {
58        matches!(self, RecvKind::This | RecvKind::SelfRecv)
59    }
60
61    /// Receivers that walk through an intermediate field. One-hop
62    /// `self.x.m()` / `this.x.m()` may pin via a unique field type; longer
63    /// chains stay unresolved and must not pretend the intermediate name
64    /// is the method.
65    // trace:exempt reason=internal-detail
66    pub fn is_field_chain(self) -> bool {
67        matches!(
68            self,
69            RecvKind::FieldOfThis | RecvKind::FieldOfSelf | RecvKind::FieldOfVariable
70        )
71    }
72}
73
74/// Outcome of native (non-LSP/SCIP) reference resolution.
75///
76/// `UnresolvedLikelyInternal` and `ConfirmedExternal` are distinct:
77/// failing to find a target is not evidence that the target lives outside
78/// the repository.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
80#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
81// trace:v1 id=impl.scc.core.resolution-class work=WORK-ripwire-lessons-phase1 satisfies=REQ-resolution-honesty-gauges
82pub enum ResolutionClass {
83    ResolvedInternal,
84    UnresolvedLikelyInternal,
85    ConfirmedExternal,
86    #[default]
87    Unknown,
88}
89
90// trace:exempt reason=internal-detail
91impl ResolutionClass {
92    // trace:exempt reason=internal-detail
93    pub fn as_str(self) -> &'static str {
94        match self {
95            ResolutionClass::ResolvedInternal => "resolved_internal",
96            ResolutionClass::UnresolvedLikelyInternal => "unresolved_likely_internal",
97            ResolutionClass::ConfirmedExternal => "confirmed_external",
98            ResolutionClass::Unknown => "unknown",
99        }
100    }
101}
102
103/// Compact analyzer-health summary. Concise by default; counts are floors
104/// of what this index actually observed.
105/// Ordinal confidence ladder for call resolution. These are NOT calibrated
106/// probabilities — no experiment measured them. They encode a deliberate
107/// ranking (exact pins outrank typed-receiver matches outrank lexical
108/// fallbacks), and every production emit site must use these names instead
109/// of bare literals so the ranking stays reviewable in one place.
110// trace:exempt reason=const-data
111pub mod confidence {
112    /// LSP definition resolution.
113    pub const LSP_EXACT: f64 = 0.99;
114    /// SCIP definition resolution (compiler-exact, same tier as LSP).
115    pub const SCIP_EXACT: f64 = 0.99;
116    /// Unique pin through a structural rule: bare-local callable, typed
117    /// receiver, field type, Rule-1 enclosing class, Rule-3 include file,
118    /// or fn-alias binding. Same tier as definition exactness: narrowing
119    /// converged on exactly one target.
120    pub const UNIQUE_PIN: f64 = 0.99;
121    /// `self`/`this` method found on a sibling of the enclosing class.
122    pub const SIBLING_METHOD: f64 = 0.98;
123    /// Namespace-qualified member pin (`ns.member`, `pkg.Symbol`).
124    pub const NAMESPACE_PIN: f64 = 0.97;
125    /// Imported member pin (binding resolved through an import).
126    pub const IMPORTED_MEMBER: f64 = 0.95;
127    /// Typed-receiver tier: field-type and Rule-1 enclosing-class matches.
128    /// Shares its value with unique pins; kept as a name so call sites
129    /// state which rule family produced the edge.
130    pub const TYPED_RECEIVER: f64 = 0.9;
131    /// Confirmed external target (`external:` import, seeded root).
132    pub const CONFIRMED_EXTERNAL: f64 = 0.8;
133    /// Strong same-system edge below definition-exactness: bridge stitch,
134    /// state-authority write. Structural, not pinned by a resolver rule.
135    pub const STRONG_LINK: f64 = 0.8;
136    /// Seeded field-chain root (object known, method not pinned).
137    pub const SEEDED_ROOT: f64 = 0.55;
138    /// No rule fired; recorded for coverage honesty, never an edge.
139    pub const UNRESOLVED_FALLBACK: f64 = 0.5;
140    /// Likely-internal target that no rule could pin.
141    pub const UNRESOLVED_LIKELY_INTERNAL: f64 = 0.4;
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, schemars::JsonSchema)]
145// trace:v1 id=impl.scc.core.analysis-quality work=WORK-ripwire-lessons-phase1 satisfies=REQ-resolution-honesty-gauges
146pub struct AnalysisQuality {
147    pub calls: CallQuality,
148    pub files: FileQuality,
149    #[serde(default, skip_serializing_if = "is_zero")]
150    pub matched_doc_mentions: u32,
151    #[serde(default, skip_serializing_if = "is_zero")]
152    pub unmatched_doc_mentions: u32,
153}
154
155// trace:exempt reason=internal-detail
156fn is_zero(n: &u32) -> bool {
157    *n == 0
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, schemars::JsonSchema)]
161// trace:v1 id=impl.scc.core.call-quality work=WORK-SI-MMMJA4G6 satisfies=REQ-SI-NX53P4B7
162pub struct CallQuality {
163    pub resolved: u32,
164    #[serde(default, skip_serializing_if = "is_zero")]
165    pub precise: u32,
166    #[serde(default, skip_serializing_if = "is_zero")]
167    pub heuristic: u32,
168    pub likely_internal_unresolved: u32,
169    pub external: u32,
170    #[serde(default, skip_serializing_if = "is_zero")]
171    pub unknown: u32,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, schemars::JsonSchema)]
175// trace:v1 id=impl.scc.core.file-quality work=WORK-SI-MMMJA4G6 satisfies=REQ-SI-NX53P4B7
176pub struct FileQuality {
177    pub parsed: u32,
178    #[serde(default, skip_serializing_if = "is_zero")]
179    pub unsupported: u32,
180}
181
182// trace:exempt reason=internal-detail
183impl AnalysisQuality {
184// trace:v1 id=impl.scc.core.analysis-quality.record-call work=WORK-SI-MMMJA4G6 satisfies=REQ-SI-NX53P4B7
185    pub fn record_call(&mut self, class: ResolutionClass, precise: bool) {
186        match class {
187            ResolutionClass::ResolvedInternal => {
188                self.calls.resolved += 1;
189                if precise {
190                    self.calls.precise += 1;
191                } else {
192                    self.calls.heuristic += 1;
193                }
194            }
195            ResolutionClass::UnresolvedLikelyInternal => {
196                self.calls.likely_internal_unresolved += 1
197            }
198            ResolutionClass::ConfirmedExternal => self.calls.external += 1,
199            ResolutionClass::Unknown => self.calls.unknown += 1,
200        }
201    }
202
203    /// One-line machine-readable summary for context packs.
204    // trace:exempt reason=internal-detail
205    pub fn merge(&mut self, other: &AnalysisQuality) {
206        self.calls.resolved += other.calls.resolved;
207        self.calls.precise += other.calls.precise;
208        self.calls.heuristic += other.calls.heuristic;
209        self.calls.likely_internal_unresolved += other.calls.likely_internal_unresolved;
210        self.calls.external += other.calls.external;
211        self.calls.unknown += other.calls.unknown;
212        self.files.parsed += other.files.parsed;
213        self.files.unsupported += other.files.unsupported;
214        self.matched_doc_mentions += other.matched_doc_mentions;
215        self.unmatched_doc_mentions += other.unmatched_doc_mentions;
216    }
217
218    // trace:exempt reason=internal-detail
219    pub fn compact_line(&self) -> String {
220        let mut line = format!(
221            "calls: resolved={} precise={} heuristic={} likely_internal_unresolved={} external={} | files: parsed={} unsupported={}",
222            self.calls.resolved,
223            self.calls.precise,
224            self.calls.heuristic,
225            self.calls.likely_internal_unresolved,
226            self.calls.external,
227            self.files.parsed,
228            self.files.unsupported,
229        );
230        if self.matched_doc_mentions > 0 || self.unmatched_doc_mentions > 0 {
231            line.push_str(&format!(
232                " | mentions: matched={} unmatched={}",
233                self.matched_doc_mentions, self.unmatched_doc_mentions
234            ));
235        }
236        line
237    }
238}
239
240/// Body representation selected under the exact-source-dominance rule:
241/// never spend more tokens to be clever.
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
243#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
244// trace:v1 id=impl.scc.core.representation-kind work=WORK-SI-MMMJA4G6 satisfies=REQ-SI-503JSBGP
245pub enum RepresentationKind {
246    Exact,
247    Structural,
248    Signatures,
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252// trace:v1 id=impl.scc.core.representation-choice work=WORK-SI-MMMJA4G6 satisfies=REQ-SI-503JSBGP
253pub struct RepresentationChoice {
254    pub kind: RepresentationKind,
255    pub reason: String,
256    pub exact_cost: usize,
257    pub structural_cost: usize,
258}
259
260/// If exact source costs no more than the generated skeleton, serve exact.
261// trace:v1 id=impl.scc.core.choose-representation work=WORK-ripwire-lessons-phase1 satisfies=REQ-exact-source-dominance
262pub fn choose_representation(exact_cost: usize, structural_cost: usize) -> RepresentationChoice {
263    if exact_cost <= structural_cost {
264        RepresentationChoice {
265            kind: RepresentationKind::Exact,
266            reason: "exact_body_cheaper_than_structural".into(),
267            exact_cost,
268            structural_cost,
269        }
270    } else {
271        let saved = structural_cost
272            .saturating_mul(100)
273            .checked_div(exact_cost)
274            .map(|pct| 100usize.saturating_sub(pct))
275            .unwrap_or(0);
276        RepresentationChoice {
277            kind: RepresentationKind::Structural,
278            reason: format!("structural_saved_{saved}_percent"),
279            exact_cost,
280            structural_cost,
281        }
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    // trace:v1 id=test.scc.core.exact-source-dominance verifies=REQ-exact-source-dominance exercises=impl.scc.core.choose-representation
291    fn exact_wins_when_not_more_expensive() {
292        let c = choose_representation(40, 40);
293        assert_eq!(c.kind, RepresentationKind::Exact);
294        assert_eq!(c.reason, "exact_body_cheaper_than_structural");
295        let cheaper = choose_representation(10, 80);
296        assert_eq!(cheaper.kind, RepresentationKind::Exact);
297    }
298
299    #[test]
300    // trace:v1 id=test.scc.core.structural-wins verifies=REQ-exact-source-dominance exercises=impl.scc.core.choose-representation
301    fn structural_wins_when_it_saves_tokens() {
302        let c = choose_representation(100, 29);
303        assert_eq!(c.kind, RepresentationKind::Structural);
304        assert!(c.reason.contains("structural_saved_"), "{}", c.reason);
305    }
306
307    #[test]
308    // trace:v1 id=test.scc.core.analysis-quality-buckets verifies=REQ-resolution-honesty-gauges exercises=impl.scc.core.analysis-quality
309    fn record_call_buckets_are_honest() {
310        let mut q = AnalysisQuality::default();
311        q.record_call(ResolutionClass::ResolvedInternal, true);
312        q.record_call(ResolutionClass::ResolvedInternal, false);
313        q.record_call(ResolutionClass::ConfirmedExternal, false);
314        q.record_call(ResolutionClass::UnresolvedLikelyInternal, false);
315        q.record_call(ResolutionClass::Unknown, false);
316        assert_eq!(q.calls.resolved, 2);
317        assert_eq!(q.calls.precise, 1);
318        assert_eq!(q.calls.heuristic, 1);
319        assert_eq!(q.calls.external, 1);
320        assert_eq!(q.calls.likely_internal_unresolved, 1);
321        assert_eq!(q.calls.unknown, 1);
322        assert!(q.compact_line().contains("precise=1"));
323        assert!(q.compact_line().contains("external=1"));
324    }
325}