Skip to main content

code_moniker_workspace/snapshot/
model.rs

1use std::mem::size_of;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::time::Duration;
5
6use super::records::RecordTable;
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
9pub struct ResourceGeneration(u64);
10
11impl ResourceGeneration {
12	pub fn new(value: u64) -> Self {
13		Self(value)
14	}
15
16	pub fn value(self) -> u64 {
17		self.0
18	}
19}
20
21#[derive(Clone, Debug)]
22pub struct WorkspaceRequest {
23	pub label: String,
24	pub catalog: CatalogRequest,
25	cancellation: WorkspaceCancellation,
26	memory_source_refresh: Option<MemorySourceRefreshMetrics>,
27}
28
29impl PartialEq for WorkspaceRequest {
30	fn eq(&self, other: &Self) -> bool {
31		self.label == other.label
32			&& self.catalog == other.catalog
33			&& self.memory_source_refresh == other.memory_source_refresh
34	}
35}
36
37impl Eq for WorkspaceRequest {}
38
39#[derive(Clone, Debug, Default)]
40pub struct WorkspaceCancellation(Arc<AtomicBool>);
41
42impl WorkspaceCancellation {
43	pub fn cancel(&self) {
44		self.0.store(true, Ordering::Release);
45	}
46
47	pub fn is_cancelled(&self) -> bool {
48		self.0.load(Ordering::Acquire)
49	}
50
51	pub fn check(&self, resource: WorkspaceResource) -> WorkspaceResult<()> {
52		if self.is_cancelled() {
53			return Err(WorkspaceFailure::new(resource, "workspace build cancelled"));
54		}
55		Ok(())
56	}
57}
58
59#[derive(Clone, Copy, Debug, Eq, PartialEq)]
60pub enum CatalogRequest {
61	Refresh,
62	ReuseCurrent,
63}
64
65impl WorkspaceRequest {
66	pub fn new(label: impl Into<String>) -> Self {
67		Self {
68			label: label.into(),
69			catalog: CatalogRequest::Refresh,
70			cancellation: WorkspaceCancellation::default(),
71			memory_source_refresh: None,
72		}
73	}
74
75	pub fn with_cancellation(mut self, cancellation: WorkspaceCancellation) -> Self {
76		self.cancellation = cancellation;
77		self
78	}
79
80	pub fn cancellation(&self) -> &WorkspaceCancellation {
81		&self.cancellation
82	}
83
84	pub fn with_memory_source_refresh(mut self, refresh: MemorySourceRefreshMetrics) -> Self {
85		self.memory_source_refresh = Some(refresh);
86		self
87	}
88
89	pub fn memory_source_refresh(&self) -> Option<MemorySourceRefreshMetrics> {
90		self.memory_source_refresh
91	}
92
93	pub fn reuse_current_catalog(mut self) -> Self {
94		self.catalog = CatalogRequest::ReuseCurrent;
95		self
96	}
97
98	pub fn should_reuse_current_catalog(&self) -> bool {
99		self.catalog == CatalogRequest::ReuseCurrent
100	}
101}
102
103#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
104pub struct SourceId {
105	file: u32,
106}
107
108impl SourceId {
109	pub fn at(file: usize) -> Self {
110		Self { file: file as u32 }
111	}
112
113	pub fn parse(value: &str) -> Option<Self> {
114		let rest = value.strip_prefix("source:")?;
115		let file = rest.split(':').next()?;
116		Some(Self {
117			file: file.parse().ok()?,
118		})
119	}
120
121	pub fn file(self) -> usize {
122		self.file as usize
123	}
124}
125
126impl std::fmt::Display for SourceId {
127	fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128		write!(formatter, "source:{}", self.file)
129	}
130}
131
132#[derive(Clone, Debug, Eq, PartialEq)]
133pub struct SourceUnit {
134	pub id: SourceId,
135	pub display_name: String,
136	pub language: Option<String>,
137}
138
139impl SourceUnit {
140	pub fn new(id: SourceId, display_name: impl Into<String>) -> Self {
141		Self {
142			id,
143			display_name: display_name.into(),
144			language: None,
145		}
146	}
147
148	pub fn with_language(
149		id: SourceId,
150		display_name: impl Into<String>,
151		language: impl Into<String>,
152	) -> Self {
153		Self {
154			id,
155			display_name: display_name.into(),
156			language: Some(language.into()),
157		}
158	}
159}
160
161#[derive(Clone, Debug, Eq, PartialEq)]
162pub struct SourceCatalog {
163	pub generation: ResourceGeneration,
164	pub sources: Vec<SourceUnit>,
165}
166
167impl SourceCatalog {
168	pub fn new(generation: ResourceGeneration, mut sources: Vec<SourceUnit>) -> Self {
169		sources.shrink_to_fit();
170		Self {
171			generation,
172			sources,
173		}
174	}
175}
176
177#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
178pub struct SymbolId {
179	file: u32,
180	def: u32,
181}
182
183impl SymbolId {
184	pub fn at(file: usize, def: usize) -> Self {
185		Self {
186			file: file as u32,
187			def: def as u32,
188		}
189	}
190
191	pub fn parse(value: &str) -> Option<Self> {
192		let rest = value.strip_prefix("symbol:")?;
193		let (file, def) = rest.split_once(':')?;
194		Some(Self {
195			file: file.parse().ok()?,
196			def: def.parse().ok()?,
197		})
198	}
199
200	pub fn file(self) -> usize {
201		self.file as usize
202	}
203
204	pub fn def(self) -> usize {
205		self.def as usize
206	}
207}
208
209impl std::fmt::Display for SymbolId {
210	fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211		write!(formatter, "symbol:{}:{}", self.file, self.def)
212	}
213}
214
215#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
216pub struct SymbolLocation {
217	pub file: usize,
218	pub symbol: usize,
219}
220
221#[derive(Clone, Debug, Eq, PartialEq)]
222pub struct SymbolRecord {
223	pub id: SymbolId,
224	pub source: SourceId,
225	pub identity: Arc<str>,
226	pub name: String,
227	pub kind: String,
228	pub visibility: String,
229	pub signature: String,
230	pub call_name: Option<String>,
231	pub call_arity: Option<usize>,
232	pub navigable: bool,
233	pub line_range: Option<(u32, u32)>,
234	pub parent: Option<SymbolId>,
235}
236
237impl SymbolRecord {
238	pub fn new(
239		id: SymbolId,
240		source: SourceId,
241		name: impl Into<String>,
242		kind: impl Into<String>,
243	) -> Self {
244		Self {
245			identity: Arc::from(id.to_string()),
246			id,
247			source,
248			name: name.into(),
249			kind: kind.into(),
250			visibility: String::new(),
251			signature: String::new(),
252			call_name: None,
253			call_arity: None,
254			navigable: true,
255			line_range: None,
256			parent: None,
257		}
258	}
259}
260
261#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
262pub struct ReferenceId {
263	file: u32,
264	reference: u32,
265}
266
267impl ReferenceId {
268	pub fn at(file: usize, reference: usize) -> Self {
269		Self {
270			file: file as u32,
271			reference: reference as u32,
272		}
273	}
274
275	pub fn parse(value: &str) -> Option<Self> {
276		let rest = value.strip_prefix("reference:")?;
277		let (file, reference) = rest.split_once(':')?;
278		Some(Self {
279			file: file.parse().ok()?,
280			reference: reference.parse().ok()?,
281		})
282	}
283
284	pub fn file(self) -> usize {
285		self.file as usize
286	}
287
288	pub fn reference(self) -> usize {
289		self.reference as usize
290	}
291}
292
293impl std::fmt::Display for ReferenceId {
294	fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295		write!(formatter, "reference:{}:{}", self.file, self.reference)
296	}
297}
298
299#[derive(Clone, Debug, Eq, PartialEq)]
300pub struct ReferenceRecord {
301	pub id: ReferenceId,
302	pub source: SourceId,
303	pub source_symbol: SymbolId,
304	pub target_identity: Arc<str>,
305	pub kind: String,
306	pub call_name: Option<String>,
307	pub call_arity: Option<usize>,
308	pub confidence: Option<String>,
309	pub receiver: Option<String>,
310	pub alias: Option<String>,
311	pub line_range: Option<(u32, u32)>,
312}
313
314impl ReferenceRecord {
315	pub fn new(
316		id: ReferenceId,
317		source: SourceId,
318		source_symbol: SymbolId,
319		target_identity: impl Into<Arc<str>>,
320		kind: impl Into<String>,
321		line_range: Option<(u32, u32)>,
322	) -> Self {
323		Self {
324			id,
325			source,
326			source_symbol,
327			target_identity: target_identity.into(),
328			kind: kind.into(),
329			call_name: None,
330			call_arity: None,
331			confidence: None,
332			receiver: None,
333			alias: None,
334			line_range,
335		}
336	}
337
338	pub fn with_call_metadata(
339		mut self,
340		call_name: Option<String>,
341		call_arity: Option<usize>,
342	) -> Self {
343		self.call_name = call_name;
344		self.call_arity = call_arity;
345		self
346	}
347
348	pub fn with_metadata(
349		mut self,
350		confidence: Option<String>,
351		receiver: Option<String>,
352		alias: Option<String>,
353	) -> Self {
354		self.confidence = confidence;
355		self.receiver = receiver;
356		self.alias = alias;
357		self
358	}
359}
360
361#[derive(Clone, Debug, Eq, PartialEq)]
362pub struct SourceFileRecord {
363	pub id: SourceId,
364	pub uri: String,
365	pub source_root: usize,
366	pub path: String,
367	pub rel_path: String,
368	pub anchor: String,
369	pub language: String,
370	pub text: String,
371}
372
373#[derive(Clone, Debug, Eq, PartialEq)]
374pub struct CodeIndex {
375	pub generation: ResourceGeneration,
376	pub catalog_generation: ResourceGeneration,
377	pub identity_scheme: String,
378	pub sources: Vec<SourceFileRecord>,
379	pub symbols: RecordTable<SymbolRecord>,
380	pub references: RecordTable<ReferenceRecord>,
381	pub inventory: Arc<super::SymbolInventoryIndex>,
382	pub timings: CodeIndexTimings,
383}
384
385#[derive(Clone, Debug, Default, Eq, PartialEq)]
386pub struct CodeIndexTimings {
387	pub extract_sources: Duration,
388	pub semantic_index: Duration,
389	pub total: Duration,
390	pub extraction: Vec<ExtractionMeasurement>,
391	pub extraction_jobs: usize,
392	pub extraction_workers: usize,
393}
394
395#[derive(Clone, Debug, Default, Eq, PartialEq)]
396pub struct ExtractionMeasurement {
397	pub language: &'static str,
398	pub cache: &'static str,
399	pub files: usize,
400	pub source_bytes: usize,
401	pub duration: Duration,
402}
403
404impl CodeIndex {
405	pub fn new(
406		generation: ResourceGeneration,
407		catalog_generation: ResourceGeneration,
408		symbols: Vec<SymbolRecord>,
409	) -> Self {
410		Self::with_references(generation, catalog_generation, symbols, Vec::new())
411	}
412
413	pub fn with_references(
414		generation: ResourceGeneration,
415		catalog_generation: ResourceGeneration,
416		symbols: Vec<SymbolRecord>,
417		references: Vec<ReferenceRecord>,
418	) -> Self {
419		let sources = Vec::new();
420		let symbols = RecordTable::from_records(symbols);
421		let inventory = Arc::new(super::SymbolInventoryIndex::build(
422			generation, &sources, &symbols,
423		));
424		Self {
425			generation,
426			catalog_generation,
427			identity_scheme: crate::DEFAULT_IDENTITY_SCHEME.to_string(),
428			sources,
429			symbols,
430			references: RecordTable::from_records(references),
431			inventory,
432			timings: CodeIndexTimings::default(),
433		}
434	}
435}
436
437#[derive(Clone, Debug, Eq, PartialEq)]
438pub struct LinkageEdge {
439	pub reference: ReferenceId,
440	pub target: SymbolId,
441	pub evidence: ResolutionEvidence,
442}
443
444impl LinkageEdge {
445	pub fn new(reference: ReferenceId, target: SymbolId) -> Self {
446		Self::with_evidence(reference, target, ResolutionEvidence::ExactBinding)
447	}
448
449	pub fn with_evidence(
450		reference: ReferenceId,
451		target: SymbolId,
452		evidence: ResolutionEvidence,
453	) -> Self {
454		Self {
455			reference,
456			target,
457			evidence,
458		}
459	}
460}
461
462#[derive(Clone, Copy, Debug, Eq, PartialEq)]
463pub enum ResolutionEvidence {
464	ExactBinding,
465	LocalBinding,
466	GlobalBinding,
467	TypeConstraint,
468	Mro,
469	Injected,
470	NameMatch,
471}
472
473impl ResolutionEvidence {
474	pub fn as_str(self) -> &'static str {
475		match self {
476			Self::ExactBinding => "exact_binding",
477			Self::LocalBinding => "local_binding",
478			Self::GlobalBinding => "global_binding",
479			Self::TypeConstraint => "type_constraint",
480			Self::Mro => "mro",
481			Self::Injected => "injected",
482			Self::NameMatch => "name_match",
483		}
484	}
485
486	pub fn rank(self) -> u8 {
487		match self {
488			Self::ExactBinding => 100,
489			Self::LocalBinding | Self::TypeConstraint => 90,
490			Self::Mro => 85,
491			Self::GlobalBinding | Self::Injected => 80,
492			Self::NameMatch => 10,
493		}
494	}
495}
496
497#[derive(Clone, Copy, Debug, Eq, PartialEq)]
498pub enum CandidateReason {
499	WeakNameMatch,
500	MultipleTargets,
501	AmbiguousLookup,
502}
503
504impl CandidateReason {
505	pub fn as_str(self) -> &'static str {
506		match self {
507			Self::WeakNameMatch => "weak_name_match",
508			Self::MultipleTargets => "multiple_targets",
509			Self::AmbiguousLookup => "ambiguous_lookup",
510		}
511	}
512}
513
514#[derive(Clone, Copy, Debug, Eq, PartialEq)]
515pub enum CandidateScope {
516	Local,
517	Global,
518	Builtin,
519	Injected,
520	Unknown,
521}
522
523impl CandidateScope {
524	pub fn as_str(self) -> &'static str {
525		match self {
526			Self::Local => "local",
527			Self::Global => "global",
528			Self::Builtin => "builtin",
529			Self::Injected => "injected",
530			Self::Unknown => "unknown",
531		}
532	}
533}
534
535#[derive(Clone, Debug, Eq, PartialEq)]
536pub struct CandidateReference {
537	pub reference: ReferenceId,
538	pub targets: Vec<SymbolId>,
539	pub reason: CandidateReason,
540	pub scope: CandidateScope,
541	pub evidence: ResolutionEvidence,
542}
543
544impl CandidateReference {
545	pub fn new(
546		reference: ReferenceId,
547		targets: Vec<SymbolId>,
548		reason: CandidateReason,
549		scope: CandidateScope,
550		evidence: ResolutionEvidence,
551	) -> Self {
552		Self {
553			reference,
554			targets,
555			reason,
556			scope,
557			evidence,
558		}
559	}
560}
561
562#[derive(Clone, Copy, Debug, Eq, PartialEq)]
563pub enum DynamicReason {
564	ConditionalCompilation,
565	DynamicAttribute,
566	DescriptorOrFrameworkInjected,
567	DuckTypedCandidateSet,
568	MixinContract,
569	ExternalDependencyUnindexed,
570	RuntimeImport,
571	RuntimeMutation,
572	PreprocessorExpansion,
573	InsufficientLocalFacts,
574}
575
576impl DynamicReason {
577	pub fn as_str(self) -> &'static str {
578		match self {
579			Self::ConditionalCompilation => "conditional_compilation",
580			Self::DynamicAttribute => "dynamic_attribute",
581			Self::DescriptorOrFrameworkInjected => "descriptor_or_framework_injected",
582			Self::DuckTypedCandidateSet => "duck_typed_candidate_set",
583			Self::MixinContract => "mixin_contract",
584			Self::ExternalDependencyUnindexed => "external_dependency_unindexed",
585			Self::RuntimeImport => "runtime_import",
586			Self::RuntimeMutation => "runtime_mutation",
587			Self::PreprocessorExpansion => "preprocessor_expansion",
588			Self::InsufficientLocalFacts => "insufficient_local_facts",
589		}
590	}
591}
592
593#[derive(Clone, Debug, Eq, PartialEq)]
594pub struct DynamicReference {
595	pub reference: ReferenceId,
596	pub target_identity: Arc<str>,
597	pub reason: DynamicReason,
598	pub candidates: Vec<SymbolId>,
599}
600
601impl DynamicReference {
602	pub fn new(
603		reference: ReferenceId,
604		target_identity: impl Into<Arc<str>>,
605		reason: DynamicReason,
606		candidates: Vec<SymbolId>,
607	) -> Self {
608		Self {
609			reference,
610			target_identity: target_identity.into(),
611			reason,
612			candidates,
613		}
614	}
615}
616
617#[derive(Clone, Copy, Debug, Eq, PartialEq)]
618pub enum ExternalReferenceOrigin {
619	Sdk,
620	Dependency,
621	Injected,
622	UnknownExternal,
623}
624
625impl ExternalReferenceOrigin {
626	pub fn label(self) -> &'static str {
627		match self {
628			Self::Sdk => "sdk",
629			Self::Dependency => "dependency",
630			Self::Injected => "injected",
631			Self::UnknownExternal => "unknown_external",
632		}
633	}
634}
635
636#[derive(Clone, Debug, Eq, PartialEq)]
637pub struct ExternalReference {
638	pub reference: ReferenceId,
639	pub target_identity: Arc<str>,
640	pub origin: ExternalReferenceOrigin,
641}
642
643impl ExternalReference {
644	pub fn new(
645		reference: ReferenceId,
646		target_identity: impl Into<Arc<str>>,
647		origin: ExternalReferenceOrigin,
648	) -> Self {
649		Self {
650			reference,
651			target_identity: target_identity.into(),
652			origin,
653		}
654	}
655}
656
657/// Why linkage could not bind a reference. The taxonomy is part of the
658/// resolution contract: consumers can split external-by-design from real
659/// resolution gaps instead of reading one opaque count.
660#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
661pub enum UnresolvedReason {
662	ManifestBlocked,
663	Visibility,
664	LanguageBoundary,
665	MissingQuery,
666	NoCandidate,
667	Ambiguous,
668	UnsupportedLanguageRule,
669	IncompleteExtractorMetadata,
670}
671
672impl UnresolvedReason {
673	pub fn as_str(&self) -> &'static str {
674		match self {
675			Self::ManifestBlocked => "manifest_blocked",
676			Self::Visibility => "visibility",
677			Self::LanguageBoundary => "language_boundary",
678			Self::MissingQuery => "missing_query",
679			Self::NoCandidate => "no_candidate",
680			Self::Ambiguous => "ambiguous",
681			Self::UnsupportedLanguageRule => "unsupported_language_rule",
682			Self::IncompleteExtractorMetadata => "incomplete_extractor_metadata",
683		}
684	}
685}
686
687#[derive(Clone, Debug, Eq, PartialEq)]
688pub struct UnresolvedReference {
689	pub reference: ReferenceId,
690	pub target_identity: Arc<str>,
691	pub reason: UnresolvedReason,
692}
693
694impl UnresolvedReference {
695	pub fn new(
696		reference: ReferenceId,
697		target_identity: impl Into<Arc<str>>,
698		reason: UnresolvedReason,
699	) -> Self {
700		Self {
701			reference,
702			target_identity: target_identity.into(),
703			reason,
704		}
705	}
706}
707
708#[derive(Clone, Debug, Eq, PartialEq)]
709pub struct LinkageSnapshot {
710	pub generation: ResourceGeneration,
711	pub index_generation: ResourceGeneration,
712	pub resolved_refs: usize,
713	pub candidate_refs: usize,
714	pub external_refs: usize,
715	pub dynamic_refs: usize,
716	pub blocked_refs: usize,
717	/// Compatibility counter for manifest-policy blocks.
718	pub manifest_blocked_refs: usize,
719	pub unresolved_refs: usize,
720	/// Compatibility counter for existing consumers. Candidate references are
721	/// now stored separately and never projected as graph edges.
722	pub ambiguous_refs: usize,
723	pub resolved: Vec<LinkageEdge>,
724	pub candidates: Vec<CandidateReference>,
725	pub external: Vec<ExternalReference>,
726	pub dynamic: Vec<DynamicReference>,
727	pub blocked: Vec<UnresolvedReference>,
728	/// Compatibility view containing only `ManifestBlocked` entries.
729	pub manifest_blocked: Vec<UnresolvedReference>,
730	pub unresolved: Vec<UnresolvedReference>,
731	pub read_index: LinkageReadIndexHandle,
732}
733
734#[derive(Debug)]
735pub struct LinkageReadIndex {
736	pub(crate) incoming: rustc_hash::FxHashMap<SymbolId, Vec<ReferenceId>>,
737	pub(crate) targets: rustc_hash::FxHashMap<ReferenceId, SymbolId>,
738	pub(crate) ordinals: rustc_hash::FxHashMap<SymbolId, u32>,
739	pub(crate) symbols: rustc_hash::FxHashMap<u32, SymbolId>,
740	pub(crate) outgoing: OutgoingPathIndex,
741}
742
743type OutgoingPathIndex =
744	rustc_hash::FxHashMap<u32, rustc_hash::FxHashMap<Arc<str>, Vec<ReferenceId>>>;
745
746impl LinkageReadIndex {
747	pub(crate) fn estimated_heap_bytes(&self) -> usize {
748		let incoming = self.incoming.capacity()
749			* (size_of::<SymbolId>() + size_of::<Vec<ReferenceId>>())
750			+ self
751				.incoming
752				.values()
753				.map(|references| references.capacity() * size_of::<ReferenceId>())
754				.sum::<usize>();
755		let targets = self.targets.capacity() * (size_of::<ReferenceId>() + size_of::<SymbolId>());
756		let ordinals = self.ordinals.capacity() * (size_of::<SymbolId>() + size_of::<u32>());
757		let symbols = self.symbols.capacity() * (size_of::<u32>() + size_of::<SymbolId>());
758		let mut relation_strings = std::collections::HashSet::<(usize, usize)>::new();
759		let outgoing = self.outgoing.capacity()
760			* (size_of::<u32>() + size_of::<rustc_hash::FxHashMap<Arc<str>, Vec<ReferenceId>>>())
761			+ self
762				.outgoing
763				.values()
764				.map(|relations| {
765					relations.capacity() * (size_of::<Arc<str>>() + size_of::<Vec<ReferenceId>>())
766						+ relations
767							.iter()
768							.map(|(relation, references)| {
769								let string_bytes = if relation_strings
770									.insert((relation.as_ptr() as usize, relation.len()))
771								{
772									relation.len()
773								} else {
774									0
775								};
776								string_bytes + references.capacity() * size_of::<ReferenceId>()
777							})
778							.sum::<usize>()
779				})
780				.sum::<usize>();
781		incoming + targets + ordinals + symbols + outgoing
782	}
783
784	pub fn from_edges(edges: &[LinkageEdge]) -> Self {
785		let mut incoming = rustc_hash::FxHashMap::<SymbolId, Vec<ReferenceId>>::default();
786		let mut targets = rustc_hash::FxHashMap::<ReferenceId, SymbolId>::default();
787		for edge in edges {
788			let LinkageEdge {
789				reference, target, ..
790			} = edge.clone();
791			targets.entry(reference).or_insert(target);
792			incoming.entry(target).or_default().push(reference);
793		}
794		Self {
795			incoming,
796			targets,
797			ordinals: rustc_hash::FxHashMap::default(),
798			symbols: rustc_hash::FxHashMap::default(),
799			outgoing: rustc_hash::FxHashMap::default(),
800		}
801	}
802
803	pub(crate) fn from_edges_with_ordinals(
804		edges: &[LinkageEdge],
805		references: &RecordTable<ReferenceRecord>,
806		ordinals: impl IntoIterator<Item = (u32, SymbolId)>,
807	) -> Self {
808		let mut index = Self::from_edges(edges);
809		for (ordinal, symbol) in ordinals {
810			index.symbols.insert(ordinal, symbol);
811			index.ordinals.insert(symbol, ordinal);
812		}
813		index.outgoing = outgoing_path_index(references, &index.ordinals);
814		index
815	}
816
817	pub fn incoming(&self, symbol: &SymbolId) -> &[ReferenceId] {
818		self.incoming.get(symbol).map(Vec::as_slice).unwrap_or(&[])
819	}
820
821	pub fn resolved_target(&self, reference: &ReferenceId) -> Option<&SymbolId> {
822		self.targets.get(reference)
823	}
824
825	pub(crate) fn ordinal(&self, symbol: &SymbolId) -> Option<u32> {
826		self.ordinals.get(symbol).copied()
827	}
828
829	pub(crate) fn symbol(&self, ordinal: u32) -> Option<SymbolId> {
830		self.symbols.get(&ordinal).copied()
831	}
832
833	pub(crate) fn outgoing(&self, ordinal: u32, relation: &str) -> &[ReferenceId] {
834		self.outgoing
835			.get(&ordinal)
836			.and_then(|by_relation| by_relation.get(relation))
837			.map(Vec::as_slice)
838			.unwrap_or(&[])
839	}
840
841	pub(crate) fn outgoing_relations(&self, ordinal: u32) -> impl Iterator<Item = &str> {
842		self.outgoing
843			.get(&ordinal)
844			.into_iter()
845			.flat_map(|by_relation| by_relation.keys().map(AsRef::as_ref))
846	}
847
848	#[cfg(test)]
849	pub(crate) fn active_symbol_slots(&self) -> usize {
850		self.symbols.len()
851	}
852}
853
854fn outgoing_path_index(
855	references: &RecordTable<ReferenceRecord>,
856	ordinals: &rustc_hash::FxHashMap<SymbolId, u32>,
857) -> OutgoingPathIndex {
858	let mut index = OutgoingPathIndex::default();
859	for reference in references.iter() {
860		let Some(source_ordinal) = ordinals.get(&reference.source_symbol).copied() else {
861			continue;
862		};
863		let by_relation = index.entry(source_ordinal).or_default();
864		if let Some(outgoing) = by_relation.get_mut(reference.kind.as_str()) {
865			outgoing.push(reference.id);
866		} else {
867			by_relation.insert(Arc::from(reference.kind.as_str()), vec![reference.id]);
868		}
869	}
870	index
871}
872
873#[derive(Clone, Debug, Default)]
874pub struct LinkageReadIndexHandle(Option<Arc<LinkageReadIndex>>);
875
876impl LinkageReadIndexHandle {
877	pub(crate) fn estimated_heap_bytes(&self) -> usize {
878		self.0.as_deref().map_or(0, |index| {
879			size_of::<LinkageReadIndex>() + index.estimated_heap_bytes()
880		})
881	}
882
883	pub fn from_edges(edges: &[LinkageEdge]) -> Self {
884		Self(Some(Arc::new(LinkageReadIndex::from_edges(edges))))
885	}
886
887	pub(crate) fn from_edges_with_ordinals(
888		edges: &[LinkageEdge],
889		references: &RecordTable<ReferenceRecord>,
890		ordinals: impl IntoIterator<Item = (u32, SymbolId)>,
891	) -> Self {
892		Self(Some(Arc::new(LinkageReadIndex::from_edges_with_ordinals(
893			edges, references, ordinals,
894		))))
895	}
896
897	pub fn get(&self) -> Option<&LinkageReadIndex> {
898		self.0.as_deref()
899	}
900}
901
902impl PartialEq for LinkageReadIndexHandle {
903	fn eq(&self, _other: &Self) -> bool {
904		true
905	}
906}
907
908impl Eq for LinkageReadIndexHandle {}
909
910impl LinkageSnapshot {
911	pub fn new(
912		generation: ResourceGeneration,
913		index_generation: ResourceGeneration,
914		resolved_refs: usize,
915		unresolved_refs: usize,
916	) -> Self {
917		Self {
918			generation,
919			index_generation,
920			resolved_refs,
921			candidate_refs: 0,
922			external_refs: 0,
923			dynamic_refs: 0,
924			blocked_refs: 0,
925			manifest_blocked_refs: 0,
926			unresolved_refs,
927			ambiguous_refs: 0,
928			resolved: Vec::new(),
929			candidates: Vec::new(),
930			external: Vec::new(),
931			dynamic: Vec::new(),
932			blocked: Vec::new(),
933			manifest_blocked: Vec::new(),
934			unresolved: Vec::new(),
935			read_index: LinkageReadIndexHandle::default(),
936		}
937	}
938
939	pub fn with_refs(
940		generation: ResourceGeneration,
941		index_generation: ResourceGeneration,
942		mut resolved: Vec<LinkageEdge>,
943		mut unresolved: Vec<UnresolvedReference>,
944	) -> Self {
945		resolved.shrink_to_fit();
946		unresolved.shrink_to_fit();
947		let read_index = LinkageReadIndexHandle::from_edges(&resolved);
948		Self {
949			generation,
950			index_generation,
951			resolved_refs: resolved.len(),
952			candidate_refs: 0,
953			external_refs: 0,
954			dynamic_refs: 0,
955			blocked_refs: 0,
956			manifest_blocked_refs: 0,
957			unresolved_refs: unresolved.len(),
958			ambiguous_refs: 0,
959			resolved,
960			candidates: Vec::new(),
961			external: Vec::new(),
962			dynamic: Vec::new(),
963			blocked: Vec::new(),
964			manifest_blocked: Vec::new(),
965			unresolved,
966			read_index,
967		}
968	}
969}
970
971#[derive(Clone, Debug, Eq, PartialEq)]
972pub struct ChangeOverlay {
973	pub generation: ResourceGeneration,
974	pub catalog_generation: ResourceGeneration,
975	pub index_generation: ResourceGeneration,
976	pub scope: String,
977	pub resources: Vec<ChangeResource>,
978	pub diagnostics: Vec<String>,
979	pub changed_symbols: Vec<SymbolId>,
980	pub changes: Vec<ChangeRecord>,
981}
982
983pub struct ChangeOverlayReport {
984	pub generation: ResourceGeneration,
985	pub catalog_generation: ResourceGeneration,
986	pub index_generation: ResourceGeneration,
987	pub scope: String,
988	pub resources: Vec<ChangeResource>,
989	pub diagnostics: Vec<String>,
990	pub changes: Vec<ChangeRecord>,
991}
992
993impl ChangeOverlay {
994	pub fn new(
995		generation: ResourceGeneration,
996		catalog_generation: ResourceGeneration,
997		index_generation: ResourceGeneration,
998		mut changed_symbols: Vec<SymbolId>,
999	) -> Self {
1000		changed_symbols.shrink_to_fit();
1001		Self {
1002			generation,
1003			catalog_generation,
1004			index_generation,
1005			scope: "HEAD..worktree".to_string(),
1006			resources: Vec::new(),
1007			diagnostics: Vec::new(),
1008			changed_symbols,
1009			changes: Vec::new(),
1010		}
1011	}
1012
1013	pub fn with_records(
1014		generation: ResourceGeneration,
1015		catalog_generation: ResourceGeneration,
1016		index_generation: ResourceGeneration,
1017		mut changes: Vec<ChangeRecord>,
1018	) -> Self {
1019		changes.shrink_to_fit();
1020		let changed_symbols = changes.iter().filter_map(|change| change.symbol).fold(
1021			Vec::new(),
1022			|mut out, symbol| {
1023				if !out.contains(&symbol) {
1024					out.push(symbol);
1025				}
1026				out
1027			},
1028		);
1029		let mut changed_symbols = changed_symbols;
1030		changed_symbols.shrink_to_fit();
1031		Self {
1032			generation,
1033			catalog_generation,
1034			index_generation,
1035			scope: "HEAD..worktree".to_string(),
1036			resources: Vec::new(),
1037			diagnostics: Vec::new(),
1038			changed_symbols,
1039			changes,
1040		}
1041	}
1042
1043	pub fn from_report(report: ChangeOverlayReport) -> Self {
1044		let mut resources = report.resources;
1045		let mut diagnostics = report.diagnostics;
1046		resources.shrink_to_fit();
1047		diagnostics.shrink_to_fit();
1048		let mut overlay = Self::with_records(
1049			report.generation,
1050			report.catalog_generation,
1051			report.index_generation,
1052			report.changes,
1053		);
1054		overlay.scope = report.scope;
1055		overlay.resources = resources;
1056		overlay.diagnostics = diagnostics;
1057		overlay
1058	}
1059}
1060
1061#[derive(Clone, Debug, Eq, PartialEq)]
1062pub struct ChangeResource {
1063	pub available: bool,
1064	pub label: String,
1065	pub message: String,
1066}
1067
1068#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1069pub enum ChangeStatus {
1070	Added,
1071	Modified,
1072	Removed,
1073}
1074
1075impl ChangeStatus {
1076	pub fn label(self) -> &'static str {
1077		match self {
1078			Self::Added => "added",
1079			Self::Modified => "modified",
1080			Self::Removed => "removed",
1081		}
1082	}
1083}
1084
1085#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
1086pub struct ChangeId(String);
1087
1088impl ChangeId {
1089	pub fn new(value: impl Into<String>) -> Self {
1090		Self(value.into())
1091	}
1092
1093	pub fn as_str(&self) -> &str {
1094		&self.0
1095	}
1096}
1097
1098#[derive(Clone, Debug, Eq, PartialEq)]
1099pub struct ChangeRecord {
1100	pub id: ChangeId,
1101	pub status: ChangeStatus,
1102	pub source: Option<SourceId>,
1103	pub source_uri: Option<String>,
1104	pub symbol: Option<SymbolId>,
1105	pub identity: String,
1106	pub language: String,
1107	pub file_path: String,
1108	pub name: String,
1109	pub kind: String,
1110	pub line_range: Option<(u32, u32)>,
1111	pub hunk_count: usize,
1112}
1113
1114#[derive(Clone, Debug, Eq, PartialEq)]
1115pub struct ChangeRecordCoreFields {
1116	pub id: ChangeId,
1117	pub status: ChangeStatus,
1118	pub identity: String,
1119	pub language: String,
1120	pub file_path: String,
1121	pub name: String,
1122	pub kind: String,
1123	pub line_range: Option<(u32, u32)>,
1124	pub hunk_count: usize,
1125}
1126
1127impl ChangeRecord {
1128	pub fn new(fields: ChangeRecordCoreFields) -> Self {
1129		Self {
1130			id: fields.id,
1131			status: fields.status,
1132			source: None,
1133			source_uri: None,
1134			symbol: None,
1135			identity: fields.identity,
1136			language: fields.language,
1137			file_path: fields.file_path,
1138			name: fields.name,
1139			kind: fields.kind,
1140			line_range: fields.line_range,
1141			hunk_count: fields.hunk_count,
1142		}
1143	}
1144
1145	pub fn with_source(mut self, source: SourceId, source_uri: impl Into<String>) -> Self {
1146		self.source = Some(source);
1147		self.source_uri = Some(source_uri.into());
1148		self
1149	}
1150
1151	pub fn with_symbol(mut self, symbol: SymbolId) -> Self {
1152		self.symbol = Some(symbol);
1153		self
1154	}
1155}
1156
1157#[derive(Clone, Debug, Eq, PartialEq)]
1158pub struct WorkspaceSnapshot {
1159	pub generation: ResourceGeneration,
1160	pub catalog: SourceCatalog,
1161	pub index: CodeIndex,
1162	pub linkage: LinkageSnapshot,
1163	pub changes: ChangeOverlay,
1164	pub timings: WorkspaceTimings,
1165}
1166
1167#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1168pub struct WorkspaceTimings {
1169	pub source_catalog: Duration,
1170	pub extract_sources: Duration,
1171	pub semantic_index: Duration,
1172	pub code_index: Duration,
1173	pub linkage: Duration,
1174	pub change_overlay: Duration,
1175	pub total: Duration,
1176	pub memory_source_refresh: Option<MemorySourceRefreshMetrics>,
1177}
1178
1179#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1180pub enum MemorySourceRefreshMode {
1181	Bulk,
1182	Incremental,
1183}
1184
1185impl MemorySourceRefreshMode {
1186	pub fn as_str(self) -> &'static str {
1187		match self {
1188			Self::Bulk => "bulk",
1189			Self::Incremental => "incremental",
1190		}
1191	}
1192}
1193
1194#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1195pub struct MemorySourceRefreshMetrics {
1196	pub mode: MemorySourceRefreshMode,
1197	pub documents_total: usize,
1198	pub added: usize,
1199	pub modified: usize,
1200	pub removed: usize,
1201	pub unchanged: usize,
1202	pub extraction_jobs: usize,
1203	pub extraction_workers: usize,
1204	pub linkage_invocations: usize,
1205}
1206
1207impl MemorySourceRefreshMetrics {
1208	pub fn with_execution(mut self, index: &CodeIndex, linkage_invocations: usize) -> Self {
1209		self.extraction_jobs = index.timings.extraction_jobs;
1210		self.extraction_workers = index.timings.extraction_workers;
1211		self.linkage_invocations = linkage_invocations;
1212		self
1213	}
1214}
1215
1216#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1217pub enum WorkspaceResource {
1218	SourceCatalog,
1219	CodeIndex,
1220	LinkageSnapshot,
1221	ChangeOverlay,
1222}
1223
1224#[derive(Clone, Debug, Eq, PartialEq)]
1225pub struct WorkspaceFailure {
1226	pub resource: WorkspaceResource,
1227	pub message: String,
1228}
1229
1230impl WorkspaceFailure {
1231	pub fn new(resource: WorkspaceResource, message: impl Into<String>) -> Self {
1232		Self {
1233			resource,
1234			message: message.into(),
1235		}
1236	}
1237}
1238
1239pub type WorkspaceResult<T> = Result<T, WorkspaceFailure>;
1240
1241#[derive(Clone, Debug, Eq, PartialEq)]
1242pub enum WorkspaceTransition {
1243	Ready {
1244		generation: ResourceGeneration,
1245	},
1246	Failed {
1247		failure: WorkspaceFailure,
1248		preserved_generation: Option<ResourceGeneration>,
1249	},
1250}