Skip to main content

code_moniker_workspace/
audit.rs

1use rustc_hash::{FxHashMap, FxHashSet};
2
3use serde::{Deserialize, Serialize};
4
5use crate::snapshot::{ReferenceRecord, WorkspaceSnapshot};
6
7// Embedded resolution audit: every reference is partitioned by decision class;
8// candidate, dynamic, and unresolved decisions are classified under mechanical
9// pattern keys. Labels are facts about the reference, never guesses at a cause.
10#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
11pub struct ResolutionAudit {
12	pub totals: AuditTotals,
13	pub clusters: Vec<AuditCluster>,
14	pub zones: Vec<AuditZone>,
15}
16
17#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
18pub struct AuditTotals {
19	pub references: usize,
20	/// Compatibility alias for `unique`.
21	pub resolved: usize,
22	pub unique: usize,
23	pub candidate: usize,
24	pub external: usize,
25	pub sdk: usize,
26	pub dependency: usize,
27	pub injected_external: usize,
28	pub unknown_external: usize,
29	pub dynamic: usize,
30	pub blocked: usize,
31	pub unresolved: usize,
32	pub explained: usize,
33	pub weak_or_unexplained: usize,
34	pub name_match_resolved: usize,
35	pub name_match_candidate: usize,
36}
37
38#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
39pub struct AuditCluster {
40	pub id: String,
41	pub pattern: AuditPattern,
42	pub count: usize,
43	pub samples: Vec<AuditSample>,
44}
45
46#[derive(Clone, Debug, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
47pub struct AuditPattern {
48	pub status: String,
49	pub reason: String,
50	pub evidence: String,
51	pub confidence: String,
52	pub kind: String,
53	pub receiver: String,
54	pub target_shape: String,
55	pub target_head: String,
56	pub srcset: String,
57}
58
59#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
60pub struct AuditSample {
61	pub file: String,
62	pub line_range: Option<(u32, u32)>,
63	pub snippet: String,
64	pub source: String,
65	pub call_name: String,
66	pub receiver: String,
67	pub target: String,
68	pub evidence: String,
69	pub constraints: Vec<String>,
70	pub candidates: Vec<String>,
71}
72
73#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
74pub struct AuditZone {
75	pub zone: String,
76	pub unresolved: usize,
77	pub dominant_pattern: String,
78}
79
80#[derive(Clone, Debug)]
81pub struct AuditOptions {
82	pub cluster_limit: usize,
83	pub sample_limit: usize,
84	pub sample_offset: usize,
85	pub zone_limit: usize,
86	pub cluster: Option<String>,
87}
88
89impl Default for AuditOptions {
90	fn default() -> Self {
91		Self {
92			cluster_limit: 20,
93			sample_limit: 3,
94			sample_offset: 0,
95			zone_limit: 10,
96			cluster: None,
97		}
98	}
99}
100
101struct AuditLookups<'a> {
102	symbols: &'a crate::snapshot::RecordTable<crate::snapshot::SymbolRecord>,
103	sources: &'a [crate::snapshot::SourceFileRecord],
104	read_index: Option<&'a crate::snapshot::LinkageReadIndex>,
105	resolved_fallback: Option<
106		FxHashMap<
107			crate::snapshot::ReferenceId,
108			(
109				crate::snapshot::ResolutionEvidence,
110				crate::snapshot::SymbolId,
111			),
112		>,
113	>,
114	name_match_resolved: FxHashSet<crate::snapshot::ReferenceId>,
115	unresolved: FxHashMap<crate::snapshot::ReferenceId, &'static str>,
116	blocked: FxHashMap<crate::snapshot::ReferenceId, &'static str>,
117	external: FxHashMap<crate::snapshot::ReferenceId, crate::snapshot::ExternalReferenceOrigin>,
118	candidates: FxHashMap<crate::snapshot::ReferenceId, &'a crate::snapshot::CandidateReference>,
119	dynamic: FxHashMap<crate::snapshot::ReferenceId, &'a crate::snapshot::DynamicReference>,
120}
121
122impl<'a> AuditLookups<'a> {
123	fn new(snapshot: &'a WorkspaceSnapshot) -> Self {
124		let read_index = snapshot.linkage.read_index.get();
125		Self {
126			symbols: &snapshot.index.symbols,
127			sources: &snapshot.index.sources,
128			read_index,
129			resolved_fallback: read_index.is_none().then(|| {
130				snapshot
131					.linkage
132					.resolved
133					.iter()
134					.map(|edge| (edge.reference, (edge.evidence, edge.target)))
135					.collect()
136			}),
137			name_match_resolved: if read_index.is_some() {
138				snapshot
139					.linkage
140					.resolved
141					.iter()
142					.filter(|edge| edge.evidence == crate::snapshot::ResolutionEvidence::NameMatch)
143					.map(|edge| edge.reference)
144					.collect()
145			} else {
146				FxHashSet::default()
147			},
148			unresolved: unresolved_reasons(&snapshot.linkage.unresolved),
149			blocked: blocked_reasons(snapshot),
150			external: external_references(&snapshot.linkage.external),
151			candidates: candidate_references(&snapshot.linkage.candidates),
152			dynamic: dynamic_references(&snapshot.linkage.dynamic),
153		}
154	}
155
156	fn symbol_identity(&self, id: crate::snapshot::SymbolId) -> Option<&'a str> {
157		self.symbols
158			.file_records(id.file())
159			.get(id.def())
160			.map(|symbol| symbol.identity.as_ref())
161	}
162
163	fn source(
164		&self,
165		id: crate::snapshot::SourceId,
166	) -> Option<&'a crate::snapshot::SourceFileRecord> {
167		self.sources.get(id.file()).filter(|source| source.id == id)
168	}
169
170	fn resolved_target(
171		&self,
172		reference: crate::snapshot::ReferenceId,
173	) -> Option<crate::snapshot::SymbolId> {
174		self.read_index
175			.and_then(|index| index.resolved_target(&reference))
176			.copied()
177			.or_else(|| {
178				self.resolved_fallback
179					.as_ref()
180					.and_then(|resolved| resolved.get(&reference))
181					.map(|(_, target)| *target)
182			})
183	}
184
185	fn is_name_match_resolved(&self, reference: crate::snapshot::ReferenceId) -> bool {
186		if self.read_index.is_some() {
187			return self.name_match_resolved.contains(&reference);
188		}
189		self.resolved_fallback
190			.as_ref()
191			.and_then(|resolved| resolved.get(&reference))
192			.is_some_and(|(evidence, _)| {
193				*evidence == crate::snapshot::ResolutionEvidence::NameMatch
194			})
195	}
196}
197
198fn unresolved_reasons(
199	references: &[crate::snapshot::UnresolvedReference],
200) -> FxHashMap<crate::snapshot::ReferenceId, &'static str> {
201	references
202		.iter()
203		.map(|item| (item.reference, item.reason.as_str()))
204		.collect()
205}
206
207fn blocked_reasons(
208	snapshot: &WorkspaceSnapshot,
209) -> FxHashMap<crate::snapshot::ReferenceId, &'static str> {
210	snapshot
211		.linkage
212		.blocked
213		.iter()
214		.chain(snapshot.linkage.manifest_blocked.iter())
215		.map(|item| (item.reference, item.reason.as_str()))
216		.collect()
217}
218
219fn external_references(
220	references: &[crate::snapshot::ExternalReference],
221) -> FxHashMap<crate::snapshot::ReferenceId, crate::snapshot::ExternalReferenceOrigin> {
222	references
223		.iter()
224		.map(|item| (item.reference, item.origin))
225		.collect()
226}
227
228fn candidate_references(
229	references: &[crate::snapshot::CandidateReference],
230) -> FxHashMap<crate::snapshot::ReferenceId, &crate::snapshot::CandidateReference> {
231	references
232		.iter()
233		.map(|candidate| (candidate.reference, candidate))
234		.collect()
235}
236
237fn dynamic_references(
238	references: &[crate::snapshot::DynamicReference],
239) -> FxHashMap<crate::snapshot::ReferenceId, &crate::snapshot::DynamicReference> {
240	references
241		.iter()
242		.map(|dynamic| (dynamic.reference, dynamic))
243		.collect()
244}
245
246struct AuditClassification {
247	status: &'static str,
248	reason: &'static str,
249	evidence: &'static str,
250	scope: &'static str,
251	candidate_targets: Vec<String>,
252}
253
254pub fn resolution_audit(
255	snapshot: &WorkspaceSnapshot,
256	prefix: &str,
257	options: AuditOptions,
258) -> ResolutionAudit {
259	let lookups = AuditLookups::new(snapshot);
260	let mut totals = AuditTotals::default();
261	let mut clusters: FxHashMap<AuditPattern, (usize, Vec<AuditSample>)> = FxHashMap::default();
262	let mut zones: FxHashMap<String, (usize, FxHashMap<String, usize>)> = FxHashMap::default();
263
264	for reference in snapshot.index.references.iter() {
265		let source = lookups
266			.symbol_identity(reference.source_symbol)
267			.unwrap_or_default();
268		if !prefix.is_empty() && !source.contains(prefix) {
269			continue;
270		}
271		totals.references += 1;
272		let Some(classification) = classify_reference(&lookups, reference, &mut totals) else {
273			continue;
274		};
275
276		let pattern = pattern_for(
277			classification.status,
278			classification.reason,
279			classification.evidence,
280			reference,
281			source,
282		);
283		let unresolved_cluster = classification.status == "unresolved";
284		let cluster_id = pattern_id(&pattern);
285		if options
286			.cluster
287			.as_deref()
288			.is_some_and(|expected| expected != cluster_id.as_str())
289		{
290			continue;
291		}
292		let entry = clusters.entry(pattern.clone()).or_default();
293		let sample_index = entry.0;
294		entry.0 += 1;
295		if sample_index >= options.sample_offset && entry.1.len() < options.sample_limit {
296			entry.1.push(sample_for(
297				reference,
298				source,
299				lookups
300					.source(reference.source)
301					.map_or("", |source| source.rel_path.as_str()),
302				lookups
303					.source(reference.source)
304					.map_or("", |source| source.text.as_str()),
305				classification,
306			));
307		}
308		if unresolved_cluster {
309			let zone = zone_of(source);
310			let slot = zones.entry(zone).or_default();
311			slot.0 += 1;
312			*slot.1.entry(pattern_label(&pattern)).or_default() += 1;
313		}
314	}
315	totals.resolved = totals.unique;
316	debug_assert_eq!(
317		totals.external,
318		totals.sdk + totals.dependency + totals.injected_external + totals.unknown_external,
319		"external compatibility total must equal its provenance buckets"
320	);
321	totals.explained = totals.unique
322		+ totals.candidate
323		+ totals.sdk
324		+ totals.dependency
325		+ totals.injected_external
326		+ totals.dynamic
327		+ totals.blocked;
328	totals.weak_or_unexplained = totals.candidate + totals.unknown_external + totals.unresolved;
329	debug_assert_eq!(
330		totals.references,
331		totals.explained + totals.unknown_external + totals.unresolved,
332		"resolution audit categories must partition every reference"
333	);
334
335	let mut clusters: Vec<AuditCluster> = clusters
336		.into_iter()
337		.map(|(pattern, (count, samples))| AuditCluster {
338			id: pattern_id(&pattern),
339			pattern,
340			count,
341			samples,
342		})
343		.collect();
344	clusters.sort_by_key(|cluster| std::cmp::Reverse(cluster.count));
345	clusters.truncate(options.cluster_limit);
346
347	let mut zones: Vec<AuditZone> = zones
348		.into_iter()
349		.map(|(zone, (unresolved, patterns))| AuditZone {
350			zone,
351			unresolved,
352			dominant_pattern: patterns
353				.into_iter()
354				.max_by_key(|(_, count)| *count)
355				.map(|(label, _)| label)
356				.unwrap_or_default(),
357		})
358		.collect();
359	zones.sort_by_key(|zone| std::cmp::Reverse(zone.unresolved));
360	zones.truncate(options.zone_limit);
361
362	ResolutionAudit {
363		totals,
364		clusters,
365		zones,
366	}
367}
368
369fn classify_reference(
370	lookups: &AuditLookups<'_>,
371	reference: &ReferenceRecord,
372	totals: &mut AuditTotals,
373) -> Option<AuditClassification> {
374	if let Some(target) = lookups.resolved_target(reference.id) {
375		if lookups.symbol_identity(target).is_none() {
376			totals.unresolved += 1;
377			return Some(AuditClassification {
378				status: "unresolved",
379				reason: "dangling_binding",
380				evidence: "linkage",
381				scope: "unknown",
382				candidate_targets: Vec::new(),
383			});
384		}
385		if !lookups.is_name_match_resolved(reference.id) {
386			totals.unique += 1;
387			return None;
388		}
389		totals.candidate += 1;
390		totals.name_match_candidate += 1;
391		return Some(AuditClassification {
392			status: "candidate",
393			reason: "weak_name_match",
394			evidence: crate::snapshot::ResolutionEvidence::NameMatch.as_str(),
395			scope: "unknown",
396			candidate_targets: Vec::new(),
397		});
398	}
399	if let Some(candidate) = lookups.candidates.get(&reference.id) {
400		totals.candidate += 1;
401		if candidate.reason == crate::snapshot::CandidateReason::WeakNameMatch {
402			totals.name_match_candidate += 1;
403		}
404		return Some(AuditClassification {
405			status: "candidate",
406			reason: candidate.reason.as_str(),
407			evidence: candidate.evidence.as_str(),
408			scope: candidate.scope.as_str(),
409			candidate_targets: candidate_identities(&candidate.targets, lookups),
410		});
411	}
412	if let Some(origin) = lookups.external.get(&reference.id) {
413		totals.external += 1;
414		match origin {
415			crate::snapshot::ExternalReferenceOrigin::Sdk => {
416				totals.sdk += 1;
417				return None;
418			}
419			crate::snapshot::ExternalReferenceOrigin::Dependency => {
420				totals.dependency += 1;
421				return None;
422			}
423			crate::snapshot::ExternalReferenceOrigin::Injected => {
424				totals.injected_external += 1;
425				return None;
426			}
427			crate::snapshot::ExternalReferenceOrigin::UnknownExternal => {
428				totals.unknown_external += 1;
429				return Some(AuditClassification {
430					status: "unknown_external",
431					reason: origin.label(),
432					evidence: "extractor",
433					scope: "external",
434					candidate_targets: Vec::new(),
435				});
436			}
437		}
438	}
439	if let Some(dynamic) = lookups.dynamic.get(&reference.id) {
440		totals.dynamic += 1;
441		return Some(AuditClassification {
442			status: "dynamic",
443			reason: dynamic.reason.as_str(),
444			evidence: "runtime",
445			scope: "runtime",
446			candidate_targets: candidate_identities(&dynamic.candidates, lookups),
447		});
448	}
449	if let Some(reason) = lookups.blocked.get(&reference.id) {
450		totals.blocked += 1;
451		return Some(AuditClassification {
452			status: "blocked",
453			reason,
454			evidence: "policy",
455			scope: "policy",
456			candidate_targets: Vec::new(),
457		});
458	}
459	totals.unresolved += 1;
460	Some(AuditClassification {
461		status: "unresolved",
462		reason: lookups
463			.unresolved
464			.get(&reference.id)
465			.copied()
466			.unwrap_or("missing_decision"),
467		evidence: "",
468		scope: "",
469		candidate_targets: Vec::new(),
470	})
471}
472
473pub fn pattern_id(pattern: &AuditPattern) -> String {
474	let mut hash = 0xcbf29ce484222325u64;
475	for byte in pattern_label(pattern).bytes() {
476		hash ^= u64::from(byte);
477		hash = hash.wrapping_mul(0x100000001b3);
478	}
479	format!("resolution-{hash:016x}")
480}
481
482pub fn pattern_label(pattern: &AuditPattern) -> String {
483	let mut label = format!("{} {}/{}", pattern.status, pattern.confidence, pattern.kind);
484	if !pattern.reason.is_empty() {
485		label.push_str(&format!(" reason:{}", pattern.reason));
486	}
487	if !pattern.evidence.is_empty() {
488		label.push_str(&format!(" evidence:{}", pattern.evidence));
489	}
490	if !pattern.receiver.is_empty() {
491		label.push_str(&format!(" recv:{}", pattern.receiver));
492	}
493	if !pattern.target_shape.is_empty() {
494		label.push_str(&format!(" shape:{}", pattern.target_shape));
495	}
496	if !pattern.target_head.is_empty() {
497		label.push_str(&format!(" head:{}", pattern.target_head));
498	}
499	if !pattern.srcset.is_empty() {
500		label.push_str(&format!(" srcset:{}", pattern.srcset));
501	}
502	label
503}
504
505fn pattern_for(
506	status: &str,
507	reason: &str,
508	evidence: &str,
509	reference: &ReferenceRecord,
510	source: &str,
511) -> AuditPattern {
512	let target = reference.target_identity.as_ref();
513	AuditPattern {
514		status: status.to_string(),
515		reason: reason.to_string(),
516		evidence: evidence.to_string(),
517		confidence: reference.confidence.clone().unwrap_or_default(),
518		kind: reference.kind.clone(),
519		receiver: receiver_class(reference).to_string(),
520		target_shape: target_shape(target),
521		target_head: target_head(target, source),
522		srcset: segment_value(target, "srcset:"),
523	}
524}
525
526fn sample_for(
527	reference: &ReferenceRecord,
528	source: &str,
529	file: &str,
530	source_text: &str,
531	classification: AuditClassification,
532) -> AuditSample {
533	AuditSample {
534		file: file.to_string(),
535		line_range: reference.line_range,
536		snippet: source_excerpt(source_text, reference.line_range),
537		source: identity_tail(source, 4),
538		call_name: reference.call_name.clone().unwrap_or_default(),
539		receiver: reference.receiver.clone().unwrap_or_default(),
540		target: identity_tail(reference.target_identity.as_ref(), 5),
541		evidence: classification.evidence.to_string(),
542		constraints: sample_constraints(
543			reference,
544			classification.reason,
545			classification.evidence,
546			classification.scope,
547		),
548		candidates: classification.candidate_targets,
549	}
550}
551
552fn source_excerpt(source: &str, line_range: Option<(u32, u32)>) -> String {
553	let Some((start, end)) = line_range else {
554		return String::new();
555	};
556	let line_count = end.saturating_sub(start).saturating_add(1).min(3) as usize;
557	let excerpt = source
558		.lines()
559		.skip(start.saturating_sub(1) as usize)
560		.take(line_count)
561		.map(str::trim)
562		.filter(|line| !line.is_empty())
563		.collect::<Vec<_>>()
564		.join(" ");
565	excerpt.chars().take(240).collect()
566}
567
568fn sample_constraints(
569	reference: &ReferenceRecord,
570	reason: &str,
571	evidence: &str,
572	scope: &str,
573) -> Vec<String> {
574	let mut constraints = vec![format!("kind:{}", reference.kind)];
575	for (label, value) in [
576		("reason", Some(reason)),
577		("evidence", Some(evidence)),
578		("scope", Some(scope)),
579		("confidence", reference.confidence.as_deref()),
580	] {
581		if let Some(value) = value.filter(|value| !value.is_empty()) {
582			constraints.push(format!("{label}:{value}"));
583		}
584	}
585	if let Some(arity) = reference.call_arity {
586		constraints.push(format!("arity:{arity}"));
587	}
588	constraints
589}
590
591fn candidate_identities(
592	candidates: &[crate::snapshot::SymbolId],
593	lookups: &AuditLookups<'_>,
594) -> Vec<String> {
595	candidates
596		.iter()
597		.filter_map(|candidate| lookups.symbol_identity(*candidate))
598		.take(8)
599		.map(|identity| identity_tail(identity, 5))
600		.collect()
601}
602
603fn receiver_class(reference: &ReferenceRecord) -> &'static str {
604	match reference.receiver.as_deref() {
605		None | Some("") => "",
606		Some("call") => "call",
607		Some("self" | "cls" | "this") => "self",
608		Some(_) => "named",
609	}
610}
611
612// Collapsed chain of segment kinds, consecutive repeats folded with `+`:
613// `srcset/lang/package+/module/path/method` reads as a target shape.
614fn target_shape(target: &str) -> String {
615	let mut kinds: Vec<&str> = Vec::new();
616	for segment in target.split('/') {
617		let Some((kind, _)) = segment.split_once(':') else {
618			continue;
619		};
620		if kind.contains('+') || kind.is_empty() {
621			continue;
622		}
623		kinds.push(kind);
624	}
625	let mut collapsed: Vec<String> = Vec::new();
626	for kind in kinds {
627		match collapsed.last_mut() {
628			Some(last) if last.trim_end_matches('+') == kind => {
629				if !last.ends_with('+') {
630					last.push('+');
631				}
632			}
633			_ => collapsed.push(kind.to_string()),
634		}
635	}
636	collapsed.join("/")
637}
638
639fn target_head(target: &str, source: &str) -> String {
640	if let Some(root) = target
641		.split('/')
642		.find_map(|segment| segment.strip_prefix("external_pkg:"))
643	{
644		return format!("external_pkg:{root}");
645	}
646	if let (Some(source_module), Some(target_module)) =
647		(module_prefix(source), module_prefix(target))
648		&& source_module == target_module
649	{
650		return "own_module".to_string();
651	}
652	String::new()
653}
654
655fn module_prefix(identity: &str) -> Option<&str> {
656	let idx = identity.find("/module:")?;
657	let rest = &identity[idx + 1..];
658	let end = rest
659		.find('/')
660		.map(|i| idx + 1 + i)
661		.unwrap_or(identity.len());
662	Some(&identity[..end])
663}
664
665fn segment_value(identity: &str, prefix: &str) -> String {
666	identity
667		.split('/')
668		.find_map(|segment| segment.strip_prefix(prefix))
669		.unwrap_or_default()
670		.to_string()
671}
672
673fn zone_of(source: &str) -> String {
674	match module_prefix(source) {
675		Some(module) => identity_tail(module, 4),
676		None => identity_tail(source, 3),
677	}
678}
679
680fn identity_tail(identity: &str, segments: usize) -> String {
681	let parts: Vec<&str> = identity
682		.split('/')
683		.filter(|part| !part.is_empty())
684		.collect();
685	let start = parts.len().saturating_sub(segments);
686	parts[start..].join("/")
687}
688
689#[cfg(test)]
690mod tests {
691	use std::sync::Arc;
692
693	use super::*;
694	use crate::snapshot::{
695		CandidateReason, CandidateReference, CandidateScope, ChangeOverlay, CodeIndex,
696		DynamicReason, DynamicReference, ExternalReference, ExternalReferenceOrigin, LinkageEdge,
697		LinkageReadIndexHandle, LinkageSnapshot, ReferenceId, ResourceGeneration, SourceCatalog,
698		SourceFileRecord, SourceId, SymbolId, SymbolRecord, UnresolvedReason, UnresolvedReference,
699		WorkspaceSnapshot, WorkspaceTimings,
700	};
701
702	#[test]
703	fn dangling_resolved_binding_is_counted_unresolved_not_unique() {
704		let generation = ResourceGeneration::new(1);
705		let source = SourceId::at(0);
706		let source_symbol = SymbolId::at(0, 0);
707		let missing_target = SymbolId::at(9, 9);
708		let mut source_record = SymbolRecord::new(source_symbol, source, "run", "function");
709		source_record.identity =
710			Arc::from("code+moniker://./lang:python/module:sample/function:run");
711		let reference = ReferenceRecord::new(
712			ReferenceId::at(0, 0),
713			source,
714			source_symbol,
715			"code+moniker://./lang:python/module:sample/method:work",
716			"method_call",
717			Some((10, 10)),
718		);
719		let resolved = vec![LinkageEdge::new(ReferenceId::at(0, 0), missing_target)];
720		let linkage = LinkageSnapshot {
721			generation,
722			index_generation: generation,
723			resolved_refs: 1,
724			candidate_refs: 0,
725			external_refs: 0,
726			dynamic_refs: 0,
727			blocked_refs: 0,
728			manifest_blocked_refs: 0,
729			unresolved_refs: 0,
730			ambiguous_refs: 0,
731			read_index: LinkageReadIndexHandle::from_edges(&resolved),
732			resolved,
733			candidates: Vec::new(),
734			external: Vec::new(),
735			dynamic: Vec::new(),
736			blocked: Vec::new(),
737			manifest_blocked: Vec::new(),
738			unresolved: Vec::new(),
739		};
740		let mut index = CodeIndex::with_references(
741			generation,
742			generation,
743			vec![source_record],
744			vec![reference],
745		);
746		index.sources.push(SourceFileRecord {
747			id: source,
748			uri: "file://sample.py".to_string(),
749			source_root: 0,
750			path: "sample.py".to_string(),
751			rel_path: "src/sample.py".to_string(),
752			anchor: "sample.py".to_string(),
753			language: "python".to_string(),
754			text: (0..20).map(|_| "value.work()\n").collect(),
755		});
756		let snapshot = WorkspaceSnapshot {
757			generation,
758			catalog: SourceCatalog::new(generation, Vec::new()),
759			index,
760			linkage,
761			changes: ChangeOverlay::new(generation, generation, generation, Vec::new()),
762			timings: WorkspaceTimings::default(),
763		};
764
765		let audit = resolution_audit(&snapshot, "lang:python", AuditOptions::default());
766
767		assert_eq!(
768			audit.totals.unique, 0,
769			"a binding to a symbol absent from the index must not count unique"
770		);
771		assert_eq!(audit.totals.unresolved, 1, "{:?}", audit.totals);
772		assert!(
773			audit
774				.clusters
775				.iter()
776				.any(|cluster| cluster.pattern.reason == "dangling_binding"),
777			"the dangling binding must cluster under its own reason: {:?}",
778			audit.clusters
779		);
780	}
781
782	#[test]
783	fn totals_partition_unique_candidate_external_dynamic_blocked_and_unresolved() {
784		let generation = ResourceGeneration::new(1);
785		let source = SourceId::at(0);
786		let source_symbol = SymbolId::at(0, 0);
787		let candidate_target = SymbolId::at(0, 1);
788		let mut source_record = SymbolRecord::new(source_symbol, source, "run", "function");
789		source_record.identity =
790			Arc::from("code+moniker://./lang:python/module:sample/function:run");
791		let mut target_record = SymbolRecord::new(candidate_target, source, "Target", "class");
792		target_record.identity =
793			Arc::from("code+moniker://./lang:python/module:sample/class:Target");
794		let references = (0..9)
795			.map(|idx| {
796				ReferenceRecord::new(
797					ReferenceId::at(0, idx),
798					source,
799					source_symbol,
800					"code+moniker://./lang:python/module:sample/method:work",
801					"method_call",
802					Some((10 + idx as u32, 10 + idx as u32)),
803				)
804				.with_metadata(
805					Some("resolved".to_string()),
806					Some("value".to_string()),
807					None,
808				)
809			})
810			.collect::<Vec<_>>();
811		let linkage = fixture_linkage(generation, candidate_target);
812		let mut index = CodeIndex::with_references(
813			generation,
814			generation,
815			vec![source_record, target_record],
816			references,
817		);
818		index.sources.push(SourceFileRecord {
819			id: source,
820			uri: "file://sample.py".to_string(),
821			source_root: 0,
822			path: "sample.py".to_string(),
823			rel_path: "src/sample.py".to_string(),
824			anchor: "sample.py".to_string(),
825			language: "python".to_string(),
826			text: (0..20).map(|_| "value.work()\n").collect(),
827		});
828		let snapshot = WorkspaceSnapshot {
829			generation,
830			catalog: SourceCatalog::new(generation, Vec::new()),
831			index,
832			linkage,
833			changes: ChangeOverlay::new(generation, generation, generation, Vec::new()),
834			timings: WorkspaceTimings::default(),
835		};
836
837		let audit = resolution_audit(&snapshot, "lang:python", AuditOptions::default());
838
839		assert_eq!(audit.totals.references, 9);
840		assert_eq!(audit.totals.unique, 1);
841		assert_eq!(audit.totals.candidate, 1);
842		assert_eq!(audit.totals.external, 4);
843		assert_eq!(audit.totals.sdk, 1);
844		assert_eq!(audit.totals.dependency, 1);
845		assert_eq!(audit.totals.injected_external, 1);
846		assert_eq!(audit.totals.unknown_external, 1);
847		assert_eq!(audit.totals.dynamic, 1);
848		assert_eq!(audit.totals.blocked, 1);
849		assert_eq!(audit.totals.unresolved, 1);
850		assert_eq!(audit.totals.explained, 7);
851		assert_eq!(audit.totals.weak_or_unexplained, 3);
852		assert_audit_clusters(&audit);
853		let mut without_read_index = snapshot.clone();
854		without_read_index.linkage.read_index = LinkageReadIndexHandle::default();
855		let fallback =
856			resolution_audit(&without_read_index, "lang:python", AuditOptions::default());
857		assert_eq!(fallback.totals.unique, audit.totals.unique);
858		assert_eq!(fallback.totals.candidate, audit.totals.candidate);
859		assert_eq!(fallback.totals.unresolved, audit.totals.unresolved);
860		let candidate_cluster = audit
861			.clusters
862			.iter()
863			.find(|cluster| cluster.pattern.status == "candidate")
864			.expect("candidate cluster");
865		let drill_down = resolution_audit(
866			&snapshot,
867			"lang:python",
868			AuditOptions {
869				cluster: Some(candidate_cluster.id.clone()),
870				sample_offset: 0,
871				sample_limit: 1,
872				..AuditOptions::default()
873			},
874		);
875		assert_eq!(drill_down.clusters.len(), 1);
876		assert_eq!(drill_down.clusters[0].id, candidate_cluster.id);
877		assert_eq!(drill_down.clusters[0].samples.len(), 1);
878	}
879
880	fn fixture_linkage(
881		generation: ResourceGeneration,
882		candidate_target: SymbolId,
883	) -> LinkageSnapshot {
884		let resolved = vec![LinkageEdge::new(ReferenceId::at(0, 0), candidate_target)];
885		let manifest_blocked = UnresolvedReference::new(
886			ReferenceId::at(0, 4),
887			"code+moniker://./lang:python/module:sample/method:work",
888			UnresolvedReason::ManifestBlocked,
889		);
890		LinkageSnapshot {
891			generation,
892			index_generation: generation,
893			resolved_refs: 1,
894			candidate_refs: 1,
895			external_refs: 4,
896			dynamic_refs: 1,
897			blocked_refs: 1,
898			manifest_blocked_refs: 1,
899			unresolved_refs: 1,
900			ambiguous_refs: 1,
901			read_index: LinkageReadIndexHandle::from_edges(&resolved),
902			resolved,
903			candidates: vec![CandidateReference::new(
904				ReferenceId::at(0, 1),
905				vec![candidate_target],
906				CandidateReason::WeakNameMatch,
907				CandidateScope::Global,
908				crate::snapshot::ResolutionEvidence::NameMatch,
909			)],
910			external: vec![
911				ExternalReference::new(
912					ReferenceId::at(0, 2),
913					"code+moniker://./external_pkg:sample/path:work",
914					ExternalReferenceOrigin::Dependency,
915				),
916				ExternalReference::new(
917					ReferenceId::at(0, 6),
918					"code+moniker://./sdk:python/path:builtins/path:print",
919					ExternalReferenceOrigin::Sdk,
920				),
921				ExternalReference::new(
922					ReferenceId::at(0, 7),
923					"code+moniker://./external_pkg:generated/path:work",
924					ExternalReferenceOrigin::Injected,
925				),
926				ExternalReference::new(
927					ReferenceId::at(0, 8),
928					"code+moniker://./external_pkg:unknown/path:work",
929					ExternalReferenceOrigin::UnknownExternal,
930				),
931			],
932			dynamic: vec![DynamicReference::new(
933				ReferenceId::at(0, 3),
934				"code+moniker://./lang:python/module:sample/method:work",
935				DynamicReason::DynamicAttribute,
936				Vec::new(),
937			)],
938			blocked: vec![manifest_blocked.clone()],
939			manifest_blocked: vec![manifest_blocked],
940			unresolved: vec![UnresolvedReference::new(
941				ReferenceId::at(0, 5),
942				"code+moniker://./lang:python/module:sample/method:work",
943				UnresolvedReason::NoCandidate,
944			)],
945		}
946	}
947
948	fn assert_audit_clusters(audit: &ResolutionAudit) {
949		assert!(audit.clusters.iter().any(|cluster| {
950			cluster.pattern.status == "candidate"
951				&& cluster.pattern.reason == "weak_name_match"
952				&& cluster.pattern.evidence == "name_match"
953				&& cluster.samples.iter().any(|sample| {
954					sample.file == "src/sample.py"
955						&& sample.line_range == Some((11, 11))
956						&& sample.snippet == "value.work()"
957						&& sample.constraints.contains(&"scope:global".to_string())
958						&& sample
959							.candidates
960							.iter()
961							.any(|candidate| candidate.ends_with("class:Target"))
962				})
963		}));
964		assert!(audit.clusters.iter().any(|cluster| {
965			cluster.pattern.status == "dynamic" && cluster.pattern.reason == "dynamic_attribute"
966		}));
967		assert!(audit.clusters.iter().any(|cluster| {
968			cluster.pattern.status == "blocked"
969				&& cluster.pattern.reason == "manifest_blocked"
970				&& cluster.pattern.evidence == "policy"
971		}));
972		assert!(audit.clusters.iter().any(|cluster| {
973			cluster.pattern.status == "unknown_external"
974				&& cluster.pattern.reason == "unknown_external"
975				&& cluster.pattern.evidence == "extractor"
976		}));
977	}
978}