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