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.
130 lines
7.1 KiB
Diff
130 lines
7.1 KiB
Diff
--- a/.local/text-navigation-probe-20260911/source_symbols.rs
|
|
+++ b/.local/text-navigation-probe-20260911/source_symbols.rs
|
|
@@ -7,9 +7,9 @@
|
|
//! 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::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.
|
|
@@ -357,16 +357,19 @@
|
|
|| 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 files[..file_index].iter().any(|other| other.path.eq_ignore_ascii_case(file.path)) {
|
|
+ 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() {
|
|
@@ -417,8 +420,9 @@
|
|
}
|
|
let literal = &file.text[identity.literal_span.start..identity.literal_span.end];
|
|
let decoded = serde_json::from_str::<String>(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));
|
|
}
|
|
@@ -1005,8 +1009,84 @@
|
|
}
|
|
}
|
|
|
|
#[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());
|