//! Uncompiled experiment: current-text symbol queries, with no model or editor mutation. //! //! The caller supplies one immutable snapshot of current text, full lexer tokens, //! and syntax-only declaration role spans. This module does not lex, parse //! Arcadia declarations, lint, compile, allocate model IDs, discover includes, //! or read an accepted Project. //! Its matches are authored lexical uses, not typed semantic dependency edges. use oa_core::language::{Span, TokenClass, TokenSpan}; use oa_core::Id; use std::collections::{BTreeMap, BTreeSet}; use std::ops::Range; /// Fresh editor/workspace session identity plus a revision changed for EVERY /// indexed input change, including membership, outline and validation evidence. /// Reusing this key for different input violates the caller contract. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SnapshotKey { pub workspace: Id, pub revision: u64, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Membership { Included, Detached, Unverified, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Evidence { /// The caller verified these exact bytes in the current source scope. Validated, /// Current editor bytes; no successful semantic validation is claimed. Draft, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SyntaxCoverage { /// The tolerant walk considered the entire file, even if its text is invalid. FullFile, /// A stopped/budget-limited outline cannot masquerade as a complete result. Incomplete, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PartialReason { IncludesUnverified, MissingFiles, UnappliedWorkspace, UnsupportedBindings, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Scope { CompleteProject, Partial(PartialReason), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Domain { Included, Loaded, } impl Domain { fn contains(self, membership: Membership) -> bool { self == Self::Loaded || membership == Membership::Included } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct AuthoredIdentity { pub id: Id, /// Whole quoted JSON string token in the declaration header, not a UUID /// mentioned in a body, history record, comment or display name. pub literal_span: Span, } /// These header roles must come from the coordinated core outline seam. The /// existing public DeclarationSpan alone lacks alias/kind/ID literal spans. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct DeclarationInput { pub span: Span, pub kind_span: Span, pub alias_span: Span, pub identity: Option, } #[derive(Debug, Clone, Copy)] pub struct FileInput<'a> { pub path: &'a str, pub text: &'a str, /// Exact lexer output for text, in order and covering the entire file. pub tokens: &'a [TokenSpan], /// Tolerant outline in alias-source order. Whole spans may nest. pub declarations: &'a [DeclarationInput], pub syntax: SyntaxCoverage, pub membership: Membership, pub evidence: Evidence, } /// Explicit experiment limits; production must select its own shared budgets. /// Caller-owned text/tokens/outline must already have bounded extraction. #[derive(Debug, Clone, Copy)] pub struct Limits { pub files: usize, pub source_bytes: usize, pub path_bytes: usize, pub tokens: usize, pub records: usize, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Resource { Files, SourceBytes, PathBytes, Tokens, Records, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum InputProblem { NonCanonicalPath, DuplicateOrCaseCollidingPath, InconsistentCompleteScope, IncompleteSyntax, TokenCoverage, TokenBounds, DeclarationBounds, DeclarationOrder, HeaderRoleToken, InvalidAlias, IdentityLiteral, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum BuildError { /// No partial Index is returned. This is not a zero-result query. Limit { resource: Resource, limit: usize, observed_at_least: usize, }, Input { file: usize, record: Option, problem: InputProblem, }, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Location { pub file: usize, pub span: Span, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct DefinitionHandle { snapshot: SnapshotKey, index: usize, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ReferenceHandle { snapshot: SnapshotKey, index: usize, } /// Carries only an authored identity, never an identity generated by compilation. /// It may be resolved across revisions of the same workspace. Resolution still /// checks global loaded-file uniqueness every time. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct AuthoredAnchor { workspace: Id, id: Id, } impl AuthoredAnchor { pub fn id(self) -> Id { self.id } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Identity { Missing, Unique(AuthoredAnchor), Collision { count: usize }, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Lookup { Missing, Unique(DefinitionHandle), Ambiguous { count: usize }, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Hit { Definition(DefinitionHandle), Reference(ReferenceHandle), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum QueryError { StaleSnapshot, WrongWorkspace, InvalidHandle, InvalidFile, InvalidBytePosition, OutsideDomain, AmbiguousAlias { count: usize }, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct QueryScope { pub declared: Scope, pub domain: Domain, /// True only for the complete included project, not loaded detached files. pub project_complete: bool, pub all_selected_files_validated: bool, pub selected_files: usize, pub detached_files: usize, pub unverified_files: usize, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Position { pub line: usize, /// Zero-based UTF-8 byte column. The editor adapter must convert explicitly /// if it needs character/UTF-16 columns; CRLF bytes are never normalized. pub byte_column: usize, } #[derive(Debug)] pub struct Definition<'a> { pub alias: &'a str, pub kind: &'a str, pub declaration: Location, pub alias_location: Location, pub authored_id: Option, } #[derive(Debug)] pub struct Reference<'a> { pub alias: &'a str, pub at: Location, pub alias_location: Location, } #[derive(Debug)] pub struct SourceFile<'a> { pub path: &'a str, pub text: &'a str, pub membership: Membership, pub evidence: Evidence, definitions: Range, references: Range, } #[derive(Debug)] pub struct Index<'a> { snapshot: SnapshotKey, scope: Scope, files: Vec>, definitions: Vec>, references: Vec>, aliases: BTreeMap<&'a str, Vec>, uses: BTreeMap<&'a str, Vec>, identities: BTreeMap>, } #[derive(Debug, Clone, Copy)] struct Counts { definitions: usize, references: usize, } fn charge(total: &mut usize, value: usize, limit: usize, resource: Resource) -> Result<(), BuildError> { let next = total.checked_add(value).ok_or(BuildError::Limit { resource, limit, observed_at_least: usize::MAX, })?; if next > limit { return Err(BuildError::Limit { resource, limit, observed_at_least: next, }); } *total = next; Ok(()) } fn input_error(file: usize, record: Option, problem: InputProblem) -> BuildError { BuildError::Input { file, record, problem } } fn valid_span(text: &str, span: Span) -> bool { span.start < span.end && span.end <= text.len() && text.is_char_boundary(span.start) && text.is_char_boundary(span.end) } fn contains(outer: Span, inner: Span) -> bool { outer.start <= inner.start && inner.end <= outer.end } fn token_at(tokens: &[TokenSpan], span: Span) -> Option { let index = tokens.binary_search_by_key(&span.start, |token| token.span.start).ok()?; let token = tokens[index]; (token.span == span).then_some(token) } fn word(class: TokenClass) -> bool { matches!(class, TokenClass::Identifier | TokenClass::Keyword) } /// Only a live punctuation @ followed by a legal alias outside trivia qualifies. /// Trivia can separate @ and alias, but its bytes never become a clickable hit. fn reference_spans<'a>(file: FileInput<'a>) -> impl Iterator + 'a { file.tokens.iter().enumerate().filter_map(move |(index, token)| { if token.class != TokenClass::Punctuation || &file.text[token.span.start..token.span.end] != "@" { return None; } let next = file.tokens[index + 1..].iter().find(|next| { !matches!(next.class, TokenClass::Whitespace | TokenClass::Comment) })?; let alias = &file.text[next.span.start..next.span.end]; (word(next.class) && oa_core::valid_alias(alias)).then_some((token.span, next.span)) }) } /// Runs all aggregate limits and structural checks before allocating Index maps /// or vectors. Path normalization/quoted-ID decoding may allocate bounded scratch /// strings. Scratch storage is proportional to admitted input bytes and file /// count, including Unicode expansion and collection overhead. fn preflight(files: &[FileInput<'_>], scope: Scope, limits: Limits) -> Result { let mut file_count = 0; charge(&mut file_count, files.len(), limits.files, Resource::Files)?; let mut bytes = 0; let mut path_bytes = 0; let mut tokens = 0; let mut records = 0; for file in files { charge(&mut bytes, file.text.len(), limits.source_bytes, Resource::SourceBytes)?; charge(&mut path_bytes, file.path.len(), limits.path_bytes, Resource::PathBytes)?; charge(&mut tokens, file.tokens.len(), limits.tokens, Resource::Tokens)?; charge(&mut records, file.declarations.len(), limits.records, Resource::Records)?; } let definitions = records; if scope == Scope::CompleteProject && (!files.iter().any(|file| file.membership == Membership::Included) || files.iter().any(|file| file.membership == Membership::Unverified)) { return Err(input_error(0, None, InputProblem::InconsistentCompleteScope)); } // Scratch storage is allocated only after aggregate file/path admission. // Each admitted path is folded once, retaining core's Unicode policy. let mut folded_paths = BTreeSet::new(); for (file_index, file) in files.iter().copied().enumerate() { if file.syntax != SyntaxCoverage::FullFile { return Err(input_error(file_index, None, InputProblem::IncompleteSyntax)); } if oa_core::source_project::resolve_include("", file.path).as_deref() != Ok(file.path) { return Err(input_error(file_index, None, InputProblem::NonCanonicalPath)); } if !folded_paths.insert(file.path.to_lowercase()) { return Err(input_error(file_index, None, InputProblem::DuplicateOrCaseCollidingPath)); } let mut end = 0; for (token_index, token) in file.tokens.iter().enumerate() { if !valid_span(file.text, token.span) { return Err(input_error(file_index, Some(token_index), InputProblem::TokenBounds)); } if token.span.start != end { return Err(input_error(file_index, Some(token_index), InputProblem::TokenCoverage)); } end = token.span.end; } if end != file.text.len() { return Err(input_error(file_index, None, InputProblem::TokenCoverage)); } let mut previous_alias_end = 0; for (declaration_index, declaration) in file.declarations.iter().enumerate() { let problem = |problem| input_error(file_index, Some(declaration_index), problem); if !valid_span(file.text, declaration.span) || !valid_span(file.text, declaration.kind_span) || !valid_span(file.text, declaration.alias_span) || declaration.kind_span.start != declaration.span.start || !contains(declaration.span, declaration.kind_span) || !contains(declaration.span, declaration.alias_span) || declaration.alias_span.start < declaration.kind_span.end { return Err(problem(InputProblem::DeclarationBounds)); } if declaration.alias_span.start < previous_alias_end { return Err(problem(InputProblem::DeclarationOrder)); } previous_alias_end = declaration.alias_span.end; if !token_at(file.tokens, declaration.kind_span).is_some_and(|token| word(token.class)) || !token_at(file.tokens, declaration.alias_span).is_some_and(|token| word(token.class)) { return Err(problem(InputProblem::HeaderRoleToken)); } let alias = &file.text[declaration.alias_span.start..declaration.alias_span.end]; if !oa_core::valid_alias(alias) { return Err(problem(InputProblem::InvalidAlias)); } if let Some(identity) = declaration.identity { if !valid_span(file.text, identity.literal_span) || !contains(declaration.span, identity.literal_span) || identity.literal_span.start < declaration.alias_span.end || token_at(file.tokens, identity.literal_span).map(|token| token.class) != Some(TokenClass::String) { return Err(problem(InputProblem::IdentityLiteral)); } let literal = &file.text[identity.literal_span.start..identity.literal_span.end]; let decoded = serde_json::from_str::(literal) .ok() .filter(|value| oa_core::language::has_uuid_header_shape(value)) .and_then(|value| Id::parse_str(&value).ok()); if decoded != Some(identity.id) { return Err(problem(InputProblem::IdentityLiteral)); } } } // Token bounds and coverage are proven before reference_spans slices. for _ in reference_spans(file) { charge(&mut records, 1, limits.records, Resource::Records)?; } } Ok(Counts { definitions, references: records - definitions }) } impl<'a> Index<'a> { pub fn build( snapshot: SnapshotKey, files: &[FileInput<'a>], scope: Scope, limits: Limits, ) -> Result { let counts = preflight(files, scope, limits)?; let mut index = Self { snapshot, scope, files: Vec::with_capacity(files.len()), definitions: Vec::with_capacity(counts.definitions), references: Vec::with_capacity(counts.references), aliases: BTreeMap::new(), uses: BTreeMap::new(), identities: BTreeMap::new(), }; for (file_index, file) in files.iter().copied().enumerate() { let definition_start = index.definitions.len(); let reference_start = index.references.len(); for declaration in file.declarations { let alias = &file.text[declaration.alias_span.start..declaration.alias_span.end]; let number = index.definitions.len(); index.aliases.entry(alias).or_default().push(number); if let Some(identity) = declaration.identity { index.identities.entry(identity.id).or_default().push(number); } index.definitions.push(Definition { alias, kind: &file.text[declaration.kind_span.start..declaration.kind_span.end], declaration: Location { file: file_index, span: declaration.span }, alias_location: Location { file: file_index, span: declaration.alias_span }, authored_id: declaration.identity.map(|identity| identity.id), }); } for (at, alias_span) in reference_spans(file) { let alias = &file.text[alias_span.start..alias_span.end]; index.uses.entry(alias).or_default().push(index.references.len()); index.references.push(Reference { alias, at: Location { file: file_index, span: at }, alias_location: Location { file: file_index, span: alias_span }, }); } index.files.push(SourceFile { path: file.path, text: file.text, membership: file.membership, evidence: file.evidence, definitions: definition_start..index.definitions.len(), references: reference_start..index.references.len(), }); } Ok(index) } pub fn snapshot(&self) -> SnapshotKey { self.snapshot } pub fn source(&self, file: usize) -> Result<&SourceFile<'a>, QueryError> { self.files.get(file).ok_or(QueryError::InvalidFile) } pub fn scope(&self, domain: Domain) -> QueryScope { let selected_files = self.files.iter().filter(|file| domain.contains(file.membership)).count(); QueryScope { declared: self.scope, domain, project_complete: self.scope == Scope::CompleteProject && domain == Domain::Included, all_selected_files_validated: selected_files != 0 && self.files.iter().filter(|file| domain.contains(file.membership)) .all(|file| file.evidence == Evidence::Validated), selected_files, detached_files: self.files.iter().filter(|file| file.membership == Membership::Detached).count(), unverified_files: self.files.iter().filter(|file| file.membership == Membership::Unverified).count(), } } fn definition_handle(&self, index: usize) -> DefinitionHandle { DefinitionHandle { snapshot: self.snapshot, index } } fn reference_handle(&self, index: usize) -> ReferenceHandle { ReferenceHandle { snapshot: self.snapshot, index } } pub fn definition(&self, handle: DefinitionHandle) -> Result<&Definition<'a>, QueryError> { if handle.snapshot != self.snapshot { return Err(QueryError::StaleSnapshot); } self.definitions.get(handle.index).ok_or(QueryError::InvalidHandle) } pub fn reference(&self, handle: ReferenceHandle) -> Result<&Reference<'a>, QueryError> { if handle.snapshot != self.snapshot { return Err(QueryError::StaleSnapshot); } self.references.get(handle.index).ok_or(QueryError::InvalidHandle) } pub fn definitions_for_alias<'i>(&'i self, alias: &str, domain: Domain) -> Definitions<'i, 'a> { Definitions { index: self, indices: self.aliases.get(alias).map_or(&[], Vec::as_slice).iter(), domain } } pub fn definitions_for_id(&self, id: Id, domain: Domain) -> Definitions<'_, 'a> { Definitions { index: self, indices: self.identities.get(&id).map_or(&[], Vec::as_slice).iter(), domain } } pub fn references_for_alias<'i>(&'i self, alias: &str, domain: Domain) -> References<'i, 'a> { References { index: self, indices: self.uses.get(alias).map_or(&[], Vec::as_slice).iter(), domain } } pub fn resolve_reference(&self, handle: ReferenceHandle, domain: Domain) -> Result { let reference = self.reference(handle)?; if !domain.contains(self.files[reference.at.file].membership) { return Err(QueryError::OutsideDomain); } let mut definitions = self.definitions_for_alias(reference.alias, domain); let Some(first) = definitions.next() else { return Ok(Lookup::Missing); }; let count = 1 + definitions.count(); Ok(if count == 1 { Lookup::Unique(first) } else { Lookup::Ambiguous { count } }) } pub fn identity(&self, handle: DefinitionHandle) -> Result { let Some(id) = self.definition(handle)?.authored_id else { return Ok(Identity::Missing); }; let count = self.identities[&id].len(); Ok(if count == 1 { Identity::Unique(AuthoredAnchor { workspace: self.snapshot.workspace, id }) } else { Identity::Collision { count } }) } pub fn resolve_anchor(&self, anchor: AuthoredAnchor, domain: Domain) -> Result { if anchor.workspace != self.snapshot.workspace { return Err(QueryError::WrongWorkspace); } let Some(candidates) = self.identities.get(&anchor.id) else { return Ok(Lookup::Missing); }; // A detached duplicate must not silently confer identity uniqueness. if candidates.len() != 1 { return Ok(Lookup::Ambiguous { count: candidates.len() }); } let candidate = candidates[0]; Ok(if domain.contains(self.files[self.definitions[candidate].declaration.file].membership) { Lookup::Unique(self.definition_handle(candidate)) } else { Lookup::Missing }) } /// Exact authored alias-use projection for a unique current declaration in /// the requested domain. Duplicate aliases require candidate inspection; /// this API never attributes all matching uses to the first declaration. pub fn references_to(&self, handle: DefinitionHandle, domain: Domain) -> Result, QueryError> { let definition = self.definition(handle)?; if !domain.contains(self.files[definition.declaration.file].membership) { return Err(QueryError::OutsideDomain); } let count = self.definitions_for_alias(definition.alias, domain).count(); if count != 1 { return Err(QueryError::AmbiguousAlias { count }); } Ok(self.references_for_alias(definition.alias, domain)) } pub fn position(&self, file: usize, offset: usize) -> Result { let source = self.source(file)?; if offset > source.text.len() || !source.text.is_char_boundary(offset) { return Err(QueryError::InvalidBytePosition); } let prefix = &source.text[..offset]; Ok(Position { line: prefix.bytes().filter(|byte| *byte == b'\n').count(), byte_column: prefix.rsplit_once('\n').map_or(prefix.len(), |(_, tail)| tail.len()), }) } pub fn hit(&self, file: usize, offset: usize) -> Result, QueryError> { let source = self.source(file)?; if offset > source.text.len() || !source.text.is_char_boundary(offset) { return Err(QueryError::InvalidBytePosition); } let definitions = &self.definitions[source.definitions.clone()]; let before = definitions.partition_point(|definition| definition.alias_location.span.start <= offset); if before != 0 && offset < definitions[before - 1].alias_location.span.end { return Ok(Some(Hit::Definition(self.definition_handle(source.definitions.start + before - 1)))); } let references = &self.references[source.references.clone()]; let before = references.partition_point(|reference| reference.at.span.start <= offset); if before != 0 { let reference = &references[before - 1]; if offset < reference.at.span.end || (reference.alias_location.span.start <= offset && offset < reference.alias_location.span.end) { return Ok(Some(Hit::Reference(self.reference_handle(source.references.start + before - 1)))); } } Ok(None) } } pub struct Definitions<'i, 'a> { index: &'i Index<'a>, indices: std::slice::Iter<'i, usize>, domain: Domain, } impl Iterator for Definitions<'_, '_> { type Item = DefinitionHandle; fn next(&mut self) -> Option { for &number in self.indices.by_ref() { let definition = &self.index.definitions[number]; if self.domain.contains(self.index.files[definition.declaration.file].membership) { return Some(self.index.definition_handle(number)); } } None } } pub struct References<'i, 'a> { index: &'i Index<'a>, indices: std::slice::Iter<'i, usize>, domain: Domain, } impl References<'_, '_> { pub fn scope(&self) -> QueryScope { self.index.scope(self.domain) } } impl Iterator for References<'_, '_> { type Item = ReferenceHandle; fn next(&mut self) -> Option { for &number in self.indices.by_ref() { let reference = &self.index.references[number]; if self.domain.contains(self.index.files[reference.at.file].membership) { return Some(self.index.reference_handle(number)); } } None } } #[cfg(test)] mod tests { use super::*; const A: Id = Id::from_u128(1); const B: Id = Id::from_u128(2); const C: Id = Id::from_u128(3); struct Fixture { path: String, text: String, tokens: Vec, declarations: Vec, } impl Fixture { /// Test-only literal role proof. Production never searches declaration /// headers: its caller supplies the coordinated syntax outline roles. fn new(path: &str, text: String, roles: &[(&str, &str, Option)]) -> Self { let tokens = oa_core::language::tokenize(&text); let declarations = roles.iter().map(|&(kind, alias, id)| { let header = format!("{kind} {alias} "); let start = text.find(&header).expect("authored fixture header"); let end = text[start..].find('}').map_or(text.len(), |end| start + end + 1); let alias_start = start + kind.len() + 1; DeclarationInput { span: Span { start, end }, kind_span: Span { start, end: start + kind.len() }, alias_span: Span { start: alias_start, end: alias_start + alias.len() }, identity: id.map(|id| { let literal = format!("\"{id}\""); let literal_start = start + text[start..end].find(&literal).expect("authored fixture UUID"); AuthoredIdentity { id, literal_span: Span { start: literal_start, end: literal_start + literal.len() } } }), } }).collect(); Self { path: path.into(), text, tokens, declarations } } fn input(&self, membership: Membership, evidence: Evidence) -> FileInput<'_> { FileInput { path: &self.path, text: &self.text, tokens: &self.tokens, declarations: &self.declarations, syntax: SyntaxCoverage::FullFile, membership, evidence } } fn included(&self) -> FileInput<'_> { self.input(Membership::Included, Evidence::Draft) } } fn snapshot(revision: u64) -> SnapshotKey { SnapshotKey { workspace: Id::from_u128(1000), revision } } fn limits() -> Limits { Limits { files: 16, source_bytes: 1024 * 1024, path_bytes: 8192, tokens: 200_000, records: 100_000 } } fn index<'a>(files: &[FileInput<'a>]) -> Index<'a> { Index::build(snapshot(1), files, Scope::CompleteProject, limits()).unwrap() } fn definition(index: &Index<'_>, alias: &str) -> DefinitionHandle { let mut found = index.definitions_for_alias(alias, Domain::Loaded); let result = found.next().expect("fixture definition"); assert!(found.next().is_none(), "fixture expects unique alias"); result } fn anchor(index: &Index<'_>, handle: DefinitionHandle) -> AuthoredAnchor { match index.identity(handle).unwrap() { Identity::Unique(anchor) => anchor, identity => panic!("fixture expects unique authored identity: {identity:?}"), } } #[test] fn repeated_live_uses_are_distinct_and_strings_comments_history_are_excluded() { let fixture = Fixture::new("project.arcadia", format!( "oa_actor tank \"Same display\" id \"{A}\" {{}}\n\ oa_actor tank_extra \"Same display\" id \"{B}\" {{}}\n\ oa_interaction flow \"Flow\" id \"{C}\" {{ source @tank; target @tank; other @tank_extra; \ description \"@tank @tank_extra\"; historical uuid \"{A}\"; }}\n\ // @tank @tank_extra and UUID {A}\n" ), &[("oa_actor", "tank", Some(A)), ("oa_actor", "tank_extra", Some(B)), ("oa_interaction", "flow", Some(C))]); let original = (fixture.text.clone(), fixture.tokens.clone(), fixture.declarations.clone()); let index = index(&[fixture.included()]); let tank = definition(&index, "tank"); let uses: Vec<_> = index.references_to(tank, Domain::Included).unwrap().collect(); assert_eq!(uses.len(), 2); assert_ne!(uses[0], uses[1]); assert_eq!(index.references_for_alias("tank_extra", Domain::Loaded).count(), 1); for usage in uses { let record = index.reference(usage).unwrap(); assert_eq!(&fixture.text[record.at.span.start..record.at.span.end], "@"); assert_eq!(&fixture.text[record.alias_location.span.start..record.alias_location.span.end], "tank"); assert_eq!(index.resolve_reference(usage, Domain::Included), Ok(Lookup::Unique(tank))); assert_eq!(index.hit(0, record.alias_location.span.start), Ok(Some(Hit::Reference(usage)))); } for ignored in [fixture.text.find("Same display").unwrap(), fixture.text.find("// @tank").unwrap() + 3, fixture.text.find("\"@tank @tank_extra\"").unwrap() + 1, fixture.text.find("historical uuid").unwrap() + "historical uuid \"".len()] { assert_eq!(index.hit(0, ignored), Ok(None)); } assert_eq!(index.hit(0, fixture.text.len()), Ok(None)); assert_eq!(original, (fixture.text.clone(), fixture.tokens.clone(), fixture.declarations.clone())); } #[test] fn comment_separated_reference_keeps_crlf_unicode_and_disjoint_hit_spans() { let fixture = Fixture::new("parts/actors.arcadia", format!( "oa_actor a \"救助 🚒\" id \"{A}\" {{}}\r\nsource @ // 注释 @a\r\n a; target @a;\r\n" ), &[("oa_actor", "a", Some(A))]); let index = index(&[fixture.included()]); let uses: Vec<_> = index.references_for_alias("a", Domain::Loaded).collect(); assert_eq!(uses.len(), 2); let first = index.reference(uses[0]).unwrap(); assert_eq!(index.position(0, first.at.span.start), Ok(Position { line: 1, byte_column: 7 })); assert_eq!(index.position(0, first.alias_location.span.start), Ok(Position { line: 2, byte_column: 2 })); assert_eq!(index.hit(0, first.at.span.start), Ok(Some(Hit::Reference(uses[0])))); assert_eq!(index.hit(0, first.alias_location.span.start), Ok(Some(Hit::Reference(uses[0])))); for byte in [first.at.span.end, fixture.text.find("注释").unwrap(), fixture.text.find("// 注释 @a").unwrap() + "// 注释 ".len(), first.alias_location.span.end] { assert_eq!(index.hit(0, byte), Ok(None)); } let unicode_inside = fixture.text.find('救').unwrap() + 1; assert_eq!(index.hit(0, unicode_inside), Err(QueryError::InvalidBytePosition)); assert_eq!(index.position(0, unicode_inside), Err(QueryError::InvalidBytePosition)); assert_eq!(index.hit(0, fixture.text.len() + 1), Err(QueryError::InvalidBytePosition)); assert_eq!(index.hit(1, 0), Err(QueryError::InvalidFile)); assert!(index.source(0).unwrap().text.contains("\r\n")); } #[test] fn legal_keyword_classified_alias_is_not_discarded() { let fixture = Fixture::new("project.arcadia", format!( "oa_actor oa_activity \"Participant\" id \"{A}\" {{}}\nsource @oa_activity; source @parent;" ), &[("oa_actor", "oa_activity", Some(A))]); assert!(oa_core::valid_alias("oa_activity")); assert!(!oa_core::valid_alias("parent")); assert_eq!(token_at(&fixture.tokens, fixture.declarations[0].alias_span).unwrap().class, TokenClass::Keyword); let index = index(&[fixture.included()]); let found = definition(&index, "oa_activity"); let usage = index.references_for_alias("oa_activity", Domain::Included).next().unwrap(); assert_eq!(index.resolve_reference(usage, Domain::Included), Ok(Lookup::Unique(found))); assert_eq!(index.references_for_alias("parent", Domain::Loaded).count(), 0); assert_eq!(index.hit(0, fixture.declarations[0].alias_span.start), Ok(Some(Hit::Definition(found)))); } #[test] fn duplicate_aliases_are_candidates_without_false_reference_attribution() { let first = Fixture::new("project.arcadia", format!("oa_actor a \"Same display\" id \"{A}\" {{}}\nsource @a;"), &[("oa_actor", "a", Some(A))]); let second = Fixture::new("parts/other.arcadia", format!("oa_actor a \"Same display\" id \"{B}\" {{}}\nsource @a;"), &[("oa_actor", "a", Some(B))]); let index = index(&[first.included(), second.included()]); let candidates: Vec<_> = index.definitions_for_alias("a", Domain::Included).collect(); assert_eq!(candidates.len(), 2); assert_eq!(index.definition(candidates[0]).unwrap().declaration.file, 0); assert_eq!(index.definition(candidates[1]).unwrap().declaration.file, 1); assert_ne!(anchor(&index, candidates[0]), anchor(&index, candidates[1])); assert!(matches!(index.references_to(candidates[0], Domain::Included), Err(QueryError::AmbiguousAlias { count: 2 }))); let uses: Vec<_> = index.references_for_alias("a", Domain::Included).collect(); assert_eq!(uses.len(), 2); for usage in uses { assert_eq!(index.resolve_reference(usage, Domain::Included), Ok(Lookup::Ambiguous { count: 2 })); } } #[test] fn detached_uuid_clone_disables_anchor_even_for_included_query() { let first = Fixture::new("project.arcadia", format!("oa_actor a \"First\" id \"{A}\" {{}}"), &[("oa_actor", "a", Some(A))]); let original = index(&[first.included()]); let old_anchor = anchor(&original, definition(&original, "a")); let detached = Fixture::new("retained.arcadia", format!("oa_actor old \"Retained\" id \"{A}\" {{}}"), &[("oa_actor", "old", Some(A))]); let current = Index::build(snapshot(2), &[first.included(), detached.input(Membership::Detached, Evidence::Draft)], Scope::CompleteProject, limits()).unwrap(); assert_eq!(current.identity(definition(¤t, "a")), Ok(Identity::Collision { count: 2 })); assert_eq!(current.identity(definition(¤t, "old")), Ok(Identity::Collision { count: 2 })); assert_eq!(current.resolve_anchor(old_anchor, Domain::Included), Ok(Lookup::Ambiguous { count: 2 })); assert_eq!(current.resolve_anchor(old_anchor, Domain::Loaded), Ok(Lookup::Ambiguous { count: 2 })); assert_eq!(current.definitions_for_alias("old", Domain::Included).count(), 0); assert_eq!(current.definitions_for_id(A, Domain::Included).count(), 1); assert_eq!(current.definitions_for_id(A, Domain::Loaded).count(), 2); } #[test] fn reused_alias_cannot_steal_old_identity_and_old_locations_are_stale() { let first = Fixture::new("project.arcadia", format!("oa_actor a \"First\" id \"{A}\" {{}}\nsource @a;"), &[("oa_actor", "a", Some(A))]); let original = index(&[first.included()]); let old_definition = definition(&original, "a"); let old_reference = original.references_for_alias("a", Domain::Included).next().unwrap(); let old_anchor = anchor(&original, old_definition); let edited = Fixture::new("project.arcadia", format!( "oa_actor b \"First renamed\" id \"{A}\" {{}}\noa_actor a \"New actor\" id \"{B}\" {{}}\nsource @a; target @b;" ), &[("oa_actor", "b", Some(A)), ("oa_actor", "a", Some(B))]); let current = Index::build(snapshot(2), &[edited.included()], Scope::CompleteProject, limits()).unwrap(); assert_eq!(current.definition(old_definition).map(|definition| definition.alias), Err(QueryError::StaleSnapshot)); assert_eq!(current.reference(old_reference).map(|reference| reference.alias), Err(QueryError::StaleSnapshot)); assert_eq!(current.resolve_anchor(old_anchor, Domain::Included), Ok(Lookup::Unique(definition(¤t, "b")))); let new_use = current.references_for_alias("a", Domain::Included).next().unwrap(); assert_eq!(current.resolve_reference(new_use, Domain::Included), Ok(Lookup::Unique(definition(¤t, "a")))); assert_eq!(anchor(¤t, definition(¤t, "a")).id(), B); let other_workspace = Index::build(SnapshotKey { workspace: Id::from_u128(2000), revision: 2 }, &[edited.included()], Scope::CompleteProject, limits()).unwrap(); assert_eq!(other_workspace.resolve_anchor(old_anchor, Domain::Loaded), Err(QueryError::WrongWorkspace)); assert_eq!(other_workspace.definition(definition(¤t, "b")).map(|definition| definition.alias), Err(QueryError::StaleSnapshot)); let deleted = Fixture::new("project.arcadia", format!("oa_actor a \"New actor\" id \"{B}\" {{}}"), &[("oa_actor", "a", Some(B))]); let after_delete = Index::build(snapshot(3), &[deleted.included()], Scope::CompleteProject, limits()).unwrap(); assert_eq!(after_delete.resolve_anchor(old_anchor, Domain::Loaded), Ok(Lookup::Missing)); } #[test] fn missing_ids_produce_only_current_snapshot_locations_without_stabilization() { let fixture = Fixture::new("project.arcadia", format!("oa_actor a \"No UUID\" {{ historical uuid \"{A}\"; }}\r\nsource @a;"), &[("oa_actor", "a", None)]); let original = fixture.text.clone(); let index = index(&[fixture.included()]); let declaration = definition(&index, "a"); assert_eq!(index.identity(declaration), Ok(Identity::Missing)); assert_eq!(index.definition(declaration).unwrap().authored_id, None); let usage = index.references_to(declaration, Domain::Included).unwrap().next().unwrap(); assert_eq!(index.resolve_reference(usage, Domain::Included), Ok(Lookup::Unique(declaration))); assert_eq!(fixture.text, original); assert_eq!(index.source(0).unwrap().text.as_bytes(), original.as_bytes()); assert!(!index.source(0).unwrap().text.contains(" id ")); assert_eq!(index.hit(0, fixture.text.find(&A.to_string()).unwrap()), Ok(None)); } #[test] fn scope_provenance_and_detached_alias_collision_are_explicit() { let included = Fixture::new("project.arcadia", format!("oa_actor a \"Included\" id \"{A}\" {{}}\nsource @a;"), &[("oa_actor", "a", Some(A))]); let detached = Fixture::new("old.arcadia", format!("oa_actor a \"Detached\" id \"{B}\" {{}}\nsource @a;"), &[("oa_actor", "a", Some(B))]); let unknown = Fixture::new("new.arcadia", "source @a;".into(), &[]); let index = Index::build(snapshot(1), &[ included.input(Membership::Included, Evidence::Validated), detached.input(Membership::Detached, Evidence::Draft), unknown.input(Membership::Unverified, Evidence::Draft), ], Scope::Partial(PartialReason::IncludesUnverified), limits()).unwrap(); assert_eq!(index.references_for_alias("a", Domain::Included).count(), 1); assert_eq!(index.references_for_alias("a", Domain::Loaded).count(), 3); let scope = index.references_for_alias("a", Domain::Included).scope(); assert_eq!(scope.declared, Scope::Partial(PartialReason::IncludesUnverified)); assert!(!scope.project_complete); assert!(scope.all_selected_files_validated); assert_eq!((scope.selected_files, scope.detached_files, scope.unverified_files), (1, 1, 1)); assert!(!index.scope(Domain::Loaded).all_selected_files_validated); let uses: Vec<_> = index.references_for_alias("a", Domain::Loaded).collect(); assert!(matches!(index.resolve_reference(uses[0], Domain::Included), Ok(Lookup::Unique(_)))); assert_eq!(index.resolve_reference(uses[0], Domain::Loaded), Ok(Lookup::Ambiguous { count: 2 })); assert_eq!(index.resolve_reference(uses[1], Domain::Included), Err(QueryError::OutsideDomain)); assert_eq!(index.resolve_reference(uses[2], Domain::Included), Err(QueryError::OutsideDomain)); assert_eq!(index.source(2).unwrap().membership, Membership::Unverified); assert!(matches!(Index::build(snapshot(1), &[included.included(), unknown.input(Membership::Unverified, Evidence::Draft)], Scope::CompleteProject, limits()), Err(BuildError::Input { problem: InputProblem::InconsistentCompleteScope, .. }))); } #[test] fn incomplete_draft_is_lexical_and_error_token_does_not_leak_references() { let fixture = Fixture::new("project.arcadia", "oa_actor a \"Draft\" {}\nsource @a; ? \"unfinished @a".into(), &[("oa_actor", "a", None)]); assert!(fixture.tokens.iter().any(|token| token.class == TokenClass::Error)); let index = Index::build(snapshot(1), &[fixture.included()], Scope::Partial(PartialReason::UnappliedWorkspace), limits()).unwrap(); let matches = index.references_to(definition(&index, "a"), Domain::Included).unwrap(); assert!(!matches.scope().project_complete); assert!(!matches.scope().all_selected_files_validated); assert_eq!(matches.count(), 1); assert_eq!(index.hit(0, fixture.text.rfind("@a").unwrap()), Ok(None)); } #[test] fn header_metadata_must_match_current_token_boundaries_and_authored_id() { let mut fixture = Fixture::new("project.arcadia", format!("oa_actor alpha \"救助\" id \"{A}\" {{}}"), &[("oa_actor", "alpha", Some(A))]); let original = fixture.declarations[0]; fixture.declarations[0].alias_span.start += 1; assert!(matches!(Index::build(snapshot(1), &[fixture.included()], Scope::CompleteProject, limits()), Err(BuildError::Input { problem: InputProblem::HeaderRoleToken, .. }))); fixture.declarations[0] = original; fixture.declarations[0].identity.as_mut().unwrap().id = B; assert!(matches!(Index::build(snapshot(1), &[fixture.included()], Scope::CompleteProject, limits()), Err(BuildError::Input { problem: InputProblem::IdentityLiteral, .. }))); fixture.declarations[0] = original; fixture.declarations[0].alias_span = Span { start: fixture.text.find('救').unwrap() + 1, end: fixture.text.find('救').unwrap() + 3 }; assert!(matches!(Index::build(snapshot(1), &[fixture.included()], Scope::CompleteProject, limits()), Err(BuildError::Input { problem: InputProblem::DeclarationBounds, .. }))); fixture.declarations[0] = original; fixture.declarations[0].span.end = fixture.text.len() + 1; assert!(matches!(Index::build(snapshot(1), &[fixture.included()], Scope::CompleteProject, limits()), Err(BuildError::Input { problem: InputProblem::DeclarationBounds, .. }))); } #[test] fn truncated_or_gapped_tokens_refuse_even_when_project_scope_is_partial() { let mut fixture = Fixture::new("project.arcadia", "oa_actor a \"Text\" {}\nsource @a;".into(), &[("oa_actor", "a", None)]); let final_token = fixture.tokens.pop().unwrap(); assert!(matches!(Index::build(snapshot(1), &[fixture.included()], Scope::Partial(PartialReason::MissingFiles), limits()), Err(BuildError::Input { problem: InputProblem::TokenCoverage, .. }))); fixture.tokens.push(final_token); fixture.tokens.remove(1); assert!(matches!(Index::build(snapshot(1), &[fixture.included()], Scope::CompleteProject, limits()), Err(BuildError::Input { problem: InputProblem::TokenCoverage, .. }))); let mut unicode = Fixture::new("project.arcadia", "// 救助\n".into(), &[]); unicode.tokens[0].span.end = unicode.text.find('救').unwrap() + 1; assert!(matches!(Index::build(snapshot(1), &[unicode.included()], Scope::CompleteProject, limits()), Err(BuildError::Input { problem: InputProblem::TokenBounds, .. }))); } #[test] fn stopped_outline_is_not_a_successful_empty_query() { let fixture = Fixture::new("project.arcadia", "oa_actor a \"Text\" {}\nsource @a;".into(), &[("oa_actor", "a", None)]); let mut stopped = fixture.included(); stopped.declarations = &[]; stopped.syntax = SyntaxCoverage::Incomplete; assert!(matches!(Index::build(snapshot(1), &[stopped], Scope::Partial(PartialReason::MissingFiles), limits()), Err(BuildError::Input { problem: InputProblem::IncompleteSyntax, .. }))); } #[test] fn nested_declaration_extents_do_not_turn_body_text_into_definition_hits() { let mut fixture = Fixture::new("project.arcadia", "oa_entity outer \"Outer\" {\n oa_actor inner \"Inner\" {}\n}\nsource @inner;".into(), &[("oa_entity", "outer", None), ("oa_actor", "inner", None)]); // Supply the actual outer whole span; nesting does not change alias order. fixture.declarations[0].span.end = fixture.text.rfind('}').unwrap() + 1; let index = index(&[fixture.included()]); let inner = definition(&index, "inner"); assert_eq!(index.hit(0, fixture.declarations[1].alias_span.start), Ok(Some(Hit::Definition(inner)))); assert_eq!(index.hit(0, fixture.text.find("\"Inner\"").unwrap() + 1), Ok(None)); let usage = index.references_for_alias("inner", Domain::Included).next().unwrap(); assert_eq!(index.resolve_reference(usage, Domain::Included), Ok(Lookup::Unique(inner))); } #[test] fn path_identity_rejects_normalization_and_case_collisions() { let mut first = Fixture::new("project.arcadia", "source @a;".into(), &[]); let second = Fixture::new("PROJECT.arcadia", "source @b;".into(), &[]); assert!(matches!(Index::build(snapshot(1), &[first.included(), second.included()], Scope::CompleteProject, limits()), Err(BuildError::Input { problem: InputProblem::DuplicateOrCaseCollidingPath, .. }))); assert!(matches!(Index::build(snapshot(1), &[first.included(), first.included()], Scope::CompleteProject, limits()), Err(BuildError::Input { problem: InputProblem::DuplicateOrCaseCollidingPath, .. }))); for invalid in ["./project.arcadia", "parts/../project.arcadia", "../project.arcadia", "C:/project.arcadia", "parts\\project.arcadia"] { first.path = invalid.into(); assert!(matches!(Index::build(snapshot(1), &[first.included()], Scope::CompleteProject, limits()), Err(BuildError::Input { problem: InputProblem::NonCanonicalPath, .. })), "{invalid}"); } } #[test] fn unicode_path_collisions_match_core_lowercase_policy() { for (first_path, second_path) in [("Ä.arcadia", "ä.arcadia"), ("ΟΣ/model.arcadia", "ος/model.arcadia")] { let first = Fixture::new(first_path, "source @a;".into(), &[]); let second = Fixture::new(second_path, "source @b;".into(), &[]); assert_eq!(first_path.to_lowercase(), second_path.to_lowercase()); assert!(!first_path.eq_ignore_ascii_case(second_path)); assert!(matches!(Index::build(snapshot(1), &[first.included(), second.included()], Scope::CompleteProject, limits()), Err(BuildError::Input { problem: InputProblem::DuplicateOrCaseCollidingPath, .. }))); } } #[test] fn noncanonical_header_identity_cannot_enter_through_query_metadata() { for raw in [ "00000000000000000000000000000001", "{00000000-0000-0000-0000-000000000001}", "urn:uuid:00000000-0000-0000-0000-000000000001", ] { let literal = format!("\"{raw}\""); let source = format!("oa_actor a \"A\" id {literal} {{}}\nsource @a;"); let mut fixture = Fixture::new("project.arcadia", source.clone(), &[("oa_actor", "a", None)]); let start = source.find(&literal).unwrap(); // This fixture has one explicit empty body. A brace inside the // quoted UUID must not become the test's declaration end. let body_start = source.find("{}\n").unwrap(); fixture.declarations[0].span.end = body_start + 2; assert!(start + literal.len() <= body_start); // An adapter passes None from the corrected outline; lexical // definition/reference navigation survives without an ID anchor. { let admitted = index(&[fixture.included()]); let d = definition(&admitted, "a"); assert_eq!(admitted.identity(d), Ok(Identity::Missing)); assert_eq!(admitted.references_to(d, Domain::Included).unwrap().count(), 1); assert_eq!(admitted.source(0).unwrap().text, source); } // A forged/stale caller cannot put the UUID crate's permissive // interpretation back into an authored anchor via the raw API. fixture.declarations[0].identity = Some(AuthoredIdentity { id: A, literal_span: Span { start, end: start + literal.len() }, }); assert!(matches!(Index::build(snapshot(1), &[fixture.included()], Scope::CompleteProject, limits()), Err(BuildError::Input { problem: InputProblem::IdentityLiteral, .. }))); } } #[test] fn query_header_admission_preserves_uppercase_and_json_escape_bytes() { let expected = Id::from_u128(0xabcdef); let uppercase = expected.to_string().to_uppercase(); for raw in [uppercase.clone(), format!("\\u0030{}", &uppercase[1..])] { let literal = format!("\"{raw}\""); let source = format!("oa_actor a \"A\" id {literal} {{}}\nsource @a;"); let mut fixture = Fixture::new("project.arcadia", source.clone(), &[("oa_actor", "a", None)]); let start = source.find(&literal).unwrap(); fixture.declarations[0].identity = Some(AuthoredIdentity { id: expected, literal_span: Span { start, end: start + literal.len() }, }); let admitted = index(&[fixture.included()]); assert_eq!(anchor(&admitted, definition(&admitted, "a")).id(), expected); assert_eq!(admitted.source(0).unwrap().text.as_bytes(), source.as_bytes()); } } #[test] fn admitted_unicode_path_budget_keeps_original_policy_after_single_fold() { let fixture = Fixture::new("İ.arcadia", String::new(), &[]); assert!(fixture.path.to_lowercase().len() > fixture.path.len()); let exact = Limits { files: 1, source_bytes: 0, path_bytes: fixture.path.len(), tokens: 0, records: 0 }; let admitted = Index::build(snapshot(1), &[fixture.included()], Scope::CompleteProject, exact).unwrap(); assert_eq!(admitted.source(0).unwrap().path, "İ.arcadia"); // PathBytes continues to count authored input. One lowercase key's // Unicode expansion is bounded scratch, not a new path rejection rule. } #[test] fn every_budget_admits_boundary_and_returns_no_index_one_below() { let fixture = Fixture::new("project.arcadia", "oa_actor a \"Text\" {}\nsource @a; target @a;".into(), &[("oa_actor", "a", None)]); let exact = Limits { files: 1, source_bytes: fixture.text.len(), path_bytes: fixture.path.len(), tokens: fixture.tokens.len(), records: 3 }; assert!(Index::build(snapshot(1), &[fixture.included()], Scope::CompleteProject, exact).is_ok()); for resource in [Resource::Files, Resource::SourceBytes, Resource::PathBytes, Resource::Tokens, Resource::Records] { let mut reduced = exact; match resource { Resource::Files => reduced.files -= 1, Resource::SourceBytes => reduced.source_bytes -= 1, Resource::PathBytes => reduced.path_bytes -= 1, Resource::Tokens => reduced.tokens -= 1, Resource::Records => reduced.records -= 1, } assert!(matches!(Index::build(snapshot(1), &[fixture.included()], Scope::CompleteProject, reduced), Err(BuildError::Limit { resource: actual, .. }) if actual == resource)); } // Aggregate bytes/tokens/records, not a per-file budget reset. let second = Fixture::new("other.arcadia", "source @a;".into(), &[]); let mut aggregate = limits(); aggregate.records = 3; assert!(matches!(Index::build(snapshot(1), &[fixture.included(), second.included()], Scope::CompleteProject, aggregate), Err(BuildError::Limit { resource: Resource::Records, observed_at_least: 4, .. }))); let mut maximum = usize::MAX; assert!(matches!(charge(&mut maximum, 1, usize::MAX, Resource::SourceBytes), Err(BuildError::Limit { resource: Resource::SourceBytes, .. }))); assert_eq!(maximum, usize::MAX); } #[test] fn declaration_order_and_duplicate_roles_refuse_instead_of_corrupting_hit_search() { let mut fixture = Fixture::new("project.arcadia", "oa_actor a \"First\" {}\noa_actor b \"Second\" {}".into(), &[("oa_actor", "a", None), ("oa_actor", "b", None)]); fixture.declarations.reverse(); assert!(matches!(Index::build(snapshot(1), &[fixture.included()], Scope::CompleteProject, limits()), Err(BuildError::Input { problem: InputProblem::DeclarationOrder, .. }))); fixture.declarations.reverse(); fixture.declarations.insert(1, fixture.declarations[0]); assert!(matches!(Index::build(snapshot(1), &[fixture.included()], Scope::CompleteProject, limits()), Err(BuildError::Input { problem: InputProblem::DeclarationOrder, .. }))); } }