Files
ARCADIA-Studio/harness/experiments/text-navigation-components-20260911/component-inputs/source_symbols-baseline.rs
T
sorokin.oleg.edu 36db9356cc Add bounded definition and reference navigation for current source drafts
Preserve loaded draft bytes and model history, reject stale workspace and input events, and keep captured Escape under modal priority. Verified 181 focused Windows tests and strict desktop Clippy on exact inputs4; earlier full737 scope and component/native preparation evidence are archived separately. Native navigation acceptance remains pending.
2026-09-11 11:23:20 -07:00

1044 lines
49 KiB
Rust

//! 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;
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<AuthoredIdentity>,
}
#[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<usize>,
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<Id>,
}
#[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<usize>,
references: Range<usize>,
}
#[derive(Debug)]
pub struct Index<'a> {
snapshot: SnapshotKey,
scope: Scope,
files: Vec<SourceFile<'a>>,
definitions: Vec<Definition<'a>>,
references: Vec<Reference<'a>>,
aliases: BTreeMap<&'a str, Vec<usize>>,
uses: BTreeMap<&'a str, Vec<usize>>,
identities: BTreeMap<Id, Vec<usize>>,
}
#[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<usize>, 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<TokenSpan> {
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<Item = (Span, Span)> + '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; they cannot scale beyond the admitted input bytes.
fn preflight(files: &[FileInput<'_>], scope: Scope, limits: Limits) -> Result<Counts, BuildError> {
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));
}
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 files[..file_index].iter().any(|other| other.path.eq_ignore_ascii_case(file.path)) {
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::<String>(literal)
.ok()
.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<Self, BuildError> {
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<Lookup, QueryError> {
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<Identity, QueryError> {
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<Lookup, QueryError> {
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<References<'_, 'a>, 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<Position, QueryError> {
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<Option<Hit>, 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<Self::Item> {
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<Self::Item> {
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<TokenSpan>,
declarations: Vec<DeclarationInput>,
}
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<Id>)]) -> 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(&current, "a")), Ok(Identity::Collision { count: 2 }));
assert_eq!(current.identity(definition(&current, "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(&current, "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(&current, "a"))));
assert_eq!(anchor(&current, definition(&current, "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(&current, "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 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, .. })));
}
}