1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::snapshot::{ReferenceRecord, WorkspaceSnapshot};
6
7#[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 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
578fn 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}