Skip to main content

code_moniker_workspace/
audit.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::snapshot::{ReferenceRecord, WorkspaceSnapshot};
6
7// Embedded resolution audit: every unresolved reference (and every resolved
8// name-match, the false-link candidates) is classified under a mechanical
9// pattern key — the exact dimensions that drove the R4 diagnoses by hand.
10// Labels are facts about the reference, never guesses about the cause.
11#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
12pub struct ResolutionAudit {
13	pub totals: AuditTotals,
14	pub clusters: Vec<AuditCluster>,
15	pub zones: Vec<AuditZone>,
16}
17
18#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
19pub struct AuditTotals {
20	pub references: usize,
21	pub resolved: usize,
22	pub external: usize,
23	pub blocked: usize,
24	pub unresolved: usize,
25	pub name_match_resolved: usize,
26}
27
28#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
29pub struct AuditCluster {
30	pub pattern: AuditPattern,
31	pub count: usize,
32	pub samples: Vec<AuditSample>,
33}
34
35#[derive(Clone, Debug, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
36pub struct AuditPattern {
37	pub status: String,
38	pub confidence: String,
39	pub kind: String,
40	pub receiver: String,
41	pub target_shape: String,
42	pub target_head: String,
43	pub srcset: String,
44}
45
46#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
47pub struct AuditSample {
48	pub source: String,
49	pub call_name: String,
50	pub receiver: String,
51	pub target: String,
52}
53
54#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
55pub struct AuditZone {
56	pub zone: String,
57	pub unresolved: usize,
58	pub dominant_pattern: String,
59}
60
61#[derive(Clone, Copy, Debug)]
62pub struct AuditOptions {
63	pub cluster_limit: usize,
64	pub sample_limit: usize,
65	pub zone_limit: usize,
66}
67
68impl Default for AuditOptions {
69	fn default() -> Self {
70		Self {
71			cluster_limit: 20,
72			sample_limit: 3,
73			zone_limit: 10,
74		}
75	}
76}
77
78pub fn resolution_audit(
79	snapshot: &WorkspaceSnapshot,
80	prefix: &str,
81	options: AuditOptions,
82) -> ResolutionAudit {
83	let sources: HashMap<_, _> = snapshot
84		.index
85		.symbols
86		.iter()
87		.map(|symbol| (symbol.id, symbol.identity.as_ref()))
88		.collect();
89	let unresolved: HashMap<_, _> = snapshot
90		.linkage
91		.unresolved
92		.iter()
93		.map(|item| (item.reference, ()))
94		.collect();
95	let blocked: HashMap<_, _> = snapshot
96		.linkage
97		.manifest_blocked
98		.iter()
99		.map(|item| (item.reference, ()))
100		.collect();
101	let external: HashMap<_, _> = snapshot
102		.linkage
103		.external
104		.iter()
105		.map(|item| (item.reference, ()))
106		.collect();
107	let resolved: HashMap<_, _> = snapshot
108		.linkage
109		.resolved
110		.iter()
111		.map(|edge| (edge.reference, ()))
112		.collect();
113
114	let mut totals = AuditTotals::default();
115	let mut clusters: HashMap<AuditPattern, (usize, Vec<AuditSample>)> = HashMap::new();
116	let mut zones: HashMap<String, (usize, HashMap<String, usize>)> = HashMap::new();
117
118	for reference in snapshot.index.references.iter() {
119		let source = sources
120			.get(&reference.source_symbol)
121			.copied()
122			.unwrap_or_default();
123		if !prefix.is_empty() && !source.contains(prefix) {
124			continue;
125		}
126		totals.references += 1;
127		let status = if resolved.contains_key(&reference.id) {
128			totals.resolved += 1;
129			if reference.confidence.as_deref() != Some("name_match") {
130				continue;
131			}
132			totals.name_match_resolved += 1;
133			"resolved_name_match"
134		} else if external.contains_key(&reference.id) {
135			totals.external += 1;
136			continue;
137		} else if blocked.contains_key(&reference.id) {
138			totals.blocked += 1;
139			continue;
140		} else if unresolved.contains_key(&reference.id) {
141			totals.unresolved += 1;
142			"unresolved"
143		} else {
144			continue;
145		};
146
147		let pattern = pattern_for(status, reference, source);
148		let entry = clusters.entry(pattern.clone()).or_default();
149		entry.0 += 1;
150		if entry.1.len() < options.sample_limit {
151			entry.1.push(sample_for(reference, source));
152		}
153		if status == "unresolved" {
154			let zone = zone_of(source);
155			let slot = zones.entry(zone).or_default();
156			slot.0 += 1;
157			*slot.1.entry(pattern_label(&pattern)).or_default() += 1;
158		}
159	}
160
161	let mut clusters: Vec<AuditCluster> = clusters
162		.into_iter()
163		.map(|(pattern, (count, samples))| AuditCluster {
164			pattern,
165			count,
166			samples,
167		})
168		.collect();
169	clusters.sort_by_key(|cluster| std::cmp::Reverse(cluster.count));
170	clusters.truncate(options.cluster_limit);
171
172	let mut zones: Vec<AuditZone> = zones
173		.into_iter()
174		.map(|(zone, (unresolved, patterns))| AuditZone {
175			zone,
176			unresolved,
177			dominant_pattern: patterns
178				.into_iter()
179				.max_by_key(|(_, count)| *count)
180				.map(|(label, _)| label)
181				.unwrap_or_default(),
182		})
183		.collect();
184	zones.sort_by_key(|zone| std::cmp::Reverse(zone.unresolved));
185	zones.truncate(options.zone_limit);
186
187	ResolutionAudit {
188		totals,
189		clusters,
190		zones,
191	}
192}
193
194pub fn pattern_label(pattern: &AuditPattern) -> String {
195	let mut label = format!("{} {}/{}", pattern.status, pattern.confidence, pattern.kind);
196	if !pattern.receiver.is_empty() {
197		label.push_str(&format!(" recv:{}", pattern.receiver));
198	}
199	if !pattern.target_shape.is_empty() {
200		label.push_str(&format!(" shape:{}", pattern.target_shape));
201	}
202	if !pattern.target_head.is_empty() {
203		label.push_str(&format!(" head:{}", pattern.target_head));
204	}
205	if !pattern.srcset.is_empty() {
206		label.push_str(&format!(" srcset:{}", pattern.srcset));
207	}
208	label
209}
210
211fn pattern_for(status: &str, reference: &ReferenceRecord, source: &str) -> AuditPattern {
212	let target = reference.target_identity.as_ref();
213	AuditPattern {
214		status: status.to_string(),
215		confidence: reference.confidence.clone().unwrap_or_default(),
216		kind: reference.kind.clone(),
217		receiver: receiver_class(reference).to_string(),
218		target_shape: target_shape(target),
219		target_head: target_head(target, source),
220		srcset: segment_value(target, "srcset:"),
221	}
222}
223
224fn sample_for(reference: &ReferenceRecord, source: &str) -> AuditSample {
225	AuditSample {
226		source: identity_tail(source, 4),
227		call_name: reference.call_name.clone().unwrap_or_default(),
228		receiver: reference.receiver.clone().unwrap_or_default(),
229		target: identity_tail(reference.target_identity.as_ref(), 5),
230	}
231}
232
233fn receiver_class(reference: &ReferenceRecord) -> &'static str {
234	match reference.receiver.as_deref() {
235		None | Some("") => "",
236		Some("call") => "call",
237		Some("self" | "cls" | "this") => "self",
238		Some(_) => "named",
239	}
240}
241
242// Collapsed chain of segment kinds, consecutive repeats folded with `+`:
243// `srcset/lang/package+/module/path/method` reads as a target shape.
244fn target_shape(target: &str) -> String {
245	let mut kinds: Vec<&str> = Vec::new();
246	for segment in target.split('/') {
247		let Some((kind, _)) = segment.split_once(':') else {
248			continue;
249		};
250		if kind.contains('+') || kind.is_empty() {
251			continue;
252		}
253		kinds.push(kind);
254	}
255	let mut collapsed: Vec<String> = Vec::new();
256	for kind in kinds {
257		match collapsed.last_mut() {
258			Some(last) if last.trim_end_matches('+') == kind => {
259				if !last.ends_with('+') {
260					last.push('+');
261				}
262			}
263			_ => collapsed.push(kind.to_string()),
264		}
265	}
266	collapsed.join("/")
267}
268
269fn target_head(target: &str, source: &str) -> String {
270	if let Some(root) = target
271		.split('/')
272		.find_map(|segment| segment.strip_prefix("external_pkg:"))
273	{
274		return format!("external_pkg:{root}");
275	}
276	if let (Some(source_module), Some(target_module)) =
277		(module_prefix(source), module_prefix(target))
278		&& source_module == target_module
279	{
280		return "own_module".to_string();
281	}
282	String::new()
283}
284
285fn module_prefix(identity: &str) -> Option<&str> {
286	let idx = identity.find("/module:")?;
287	let rest = &identity[idx + 1..];
288	let end = rest
289		.find('/')
290		.map(|i| idx + 1 + i)
291		.unwrap_or(identity.len());
292	Some(&identity[..end])
293}
294
295fn segment_value(identity: &str, prefix: &str) -> String {
296	identity
297		.split('/')
298		.find_map(|segment| segment.strip_prefix(prefix))
299		.unwrap_or_default()
300		.to_string()
301}
302
303fn zone_of(source: &str) -> String {
304	match module_prefix(source) {
305		Some(module) => identity_tail(module, 4),
306		None => identity_tail(source, 3),
307	}
308}
309
310fn identity_tail(identity: &str, segments: usize) -> String {
311	let parts: Vec<&str> = identity
312		.split('/')
313		.filter(|part| !part.is_empty())
314		.collect();
315	let start = parts.len().saturating_sub(segments);
316	parts[start..].join("/")
317}