feat(source): add bounded read-only mixed filesystem discovery

This commit is contained in:
2026-09-24 21:57:16 -07:00
parent f83f3bf45b
commit 3fd87bd8be
8 changed files with 739 additions and 31 deletions
+1
View File
@@ -5,6 +5,7 @@ mod semantic_source;
mod source3_bridge;
// Mixed source8 is exposed only through the separate borrowed read-only checker.
mod analysis_source;
pub(crate) use analysis_source::MixedDiscovery;
pub use analysis_source::{CheckedMixed, check_mixed_bundle};
#[path = "migration_v2/source.rs"]
mod v2_source;
@@ -118,3 +118,5 @@ pub fn parse(source: &str, budget: &mut Budget) -> Result<Binding, Error> {
#[cfg(test)]
mod tests;
pub(crate) use mixed::discovery::MixedDiscovery;
@@ -395,30 +395,15 @@ fn parse_impl<'s>(
let failure = |error| located(path, source, ordinary_error(error));
let tokens = lex::tokens(source, &mut budget).map_err(failure)?;
let metadata_before = budget.producer_entries();
let mut parser = Parser {
let (ordinary, state) = parse_unit(
source,
tokens: Vec::new(),
cursor: 0,
value_depth: 0,
limits: limits.model,
mixed: Some(State {
source,
tokens: &tokens,
budget: &mut budget,
model: &mut model,
typed: Vec::new(),
value_ends: BTreeMap::new(),
lexical_owners: Vec::new(),
pending_projection: 0,
reserved_values: 0,
reserved_owners: 0,
}),
};
let ordinary = parser
.unit(path == &bundle.entry, true)
.map_err(|e| located(path, source, e))?;
let state = parser.mixed.take().expect("private parser state");
drop(parser);
&tokens,
&mut budget,
&mut model,
limits.model,
path == &bundle.entry,
)
.map_err(|e| located(path, source, e))?;
// Projected ordinary AST nodes/fields are bounded before constructing a
// second position representation. Count references as slots, not syntax.
let projection_slots = if entry_producers {
@@ -564,3 +549,40 @@ fn ordinary_position_slots(unit: &SourceUnit) -> usize {
mod tests;
pub(super) mod project;
/// Shared grammar setup only. Complete parsing still retains/projects its state;
/// discovery drops that state after moving includes without a projection.
fn parse_unit<'a>(
source: &'a str,
tokens: &'a [lex::Token],
budget: &'a mut Budget,
model: &'a mut ModelCounts,
limits: ParseLimits,
entry: bool,
) -> Result<(SourceUnit, State<'a>), Error> {
let mut parser = Parser {
source,
tokens: Vec::new(),
cursor: 0,
value_depth: 0,
limits,
mixed: Some(State {
source,
tokens,
budget,
model,
typed: Vec::new(),
value_ends: BTreeMap::new(),
lexical_owners: Vec::new(),
pending_projection: 0,
reserved_values: 0,
reserved_owners: 0,
}),
};
let ordinary = parser.unit(entry, true)?;
let state = parser.mixed.take().expect("mixed parser state");
drop(parser);
Ok((ordinary, state))
}
pub(crate) mod discovery;
@@ -0,0 +1,99 @@
//! A separate conservative discovery phase; no model admission or edit positions.
use super::*;
use crate::source_project::{IncludeLocation, SourceLocation, resolve_include};
pub(crate) struct MixedDiscovery {
budget: Budget,
model: ModelCounts,
failed: Option<Vec<SourceDiagnostic>>,
}
impl MixedDiscovery {
pub(crate) fn new(limits: ProjectLimits) -> Result<Self, String> {
let mut budget = Budget::new(source_profile(limits)).map_err(|e| e.message)?;
budget.enable_text_producers(0).map_err(|e| e.message)?;
budget.enable_entry_producers(0, 0).map_err(|e| e.message)?;
Ok(Self {
budget,
model: ModelCounts {
limits,
graph: 0,
records: 0,
includes: 0,
},
failed: None,
})
}
pub(crate) fn inspect(
&mut self,
path: &str,
source: &str,
entry: bool,
) -> Result<Vec<IncludeLocation>, Vec<SourceDiagnostic>> {
if let Some(failed) = &self.failed {
return Err(failed.clone());
}
let result = self.inspect_unit(path, source, entry);
if let Err(failed) = &result {
self.failed = Some(failed.clone());
}
result
}
fn inspect_unit(
&mut self,
path: &str,
source: &str,
entry: bool,
) -> Result<Vec<IncludeLocation>, Vec<SourceDiagnostic>> {
let failure = |error| located(path, source, ordinary_error(error));
// Conservative per-inspected-file reservation, before its scanner/parser.
self.budget
.reserve_metadata(1, Span::default())
.map_err(failure)?;
let tokens = lex::tokens(source, &mut self.budget).map_err(failure)?;
let limits = self.model.limits.model;
let (ordinary, state) = parse_unit(
source,
&tokens,
&mut self.budget,
&mut self.model,
limits,
entry,
)
.map_err(|e| located(path, source, e))?;
// No unit-drop refunds: remaining reservations are cumulative phase work.
// In-parser quoted/UUID/key temporary release behavior is unchanged.
drop(state);
let mut includes = Vec::new();
for (raw, span) in ordinary.includes {
let target = resolve_include(path, &raw).map_err(|message| {
let mut diagnostic = language::diagnostic(
source,
Error {
code: "invalid_path",
offset: span.start,
message,
},
);
diagnostic.span = span;
vec![SourceDiagnostic {
path: path.into(),
diagnostic,
related: Vec::new(),
}]
})?;
includes.push(IncludeLocation {
location: SourceLocation {
path: path.into(),
span,
},
target,
});
}
Ok(includes)
}
}
#[cfg(test)]
mod tests;
@@ -0,0 +1,144 @@
use super::*;
const MAIN: &str = "arcadia 8;\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\ninclude \"part.arcadia\";\n}\n";
const PART: &str = "include \"last.arcadia\";\r\n";
const LAST: &str = "//é\r\n";
#[test]
fn mixed_discovery_accumulates_work_and_reservations_after_unit_drop() {
let limits = ProjectLimits {
model: ParseLimits {
max_source_bytes: 4096,
max_records: 1,
max_graph_objects: 3,
max_step_references: 17,
},
max_files: 7,
max_include_depth: 9,
max_include_edges: 11,
};
let mut phase = MixedDiscovery::new(limits).unwrap();
let profile = phase.budget.limits();
assert_eq!(
(
profile.bytes,
profile.tokens,
profile.entries,
profile.references,
profile.frames,
profile.numeric_units
),
(4096, 4096, 262144, 17, 256, 50_000_000)
);
assert_eq!(phase.budget.producer_text(), Some((0, 0)));
assert_eq!(phase.budget.producer_entries(), Some(0));
let includes = phase.inspect("main.arcadia", MAIN, true).unwrap();
assert_eq!(includes.len(), 1);
assert_eq!(includes[0].target, "part.arcadia");
drop(includes);
assert_eq!(phase.budget.producer_text(), Some((20, 0)));
assert_eq!(phase.budget.producer_entries(), Some(1));
assert_eq!(phase.budget.usage.entries, 2);
let includes = phase.inspect("part.arcadia", PART, false).unwrap();
assert_eq!(includes[0].target, "last.arcadia");
drop(includes);
assert_eq!(phase.budget.producer_text(), Some((32, 0)));
assert!(
phase
.inspect("last.arcadia", LAST, false)
.unwrap()
.is_empty()
);
assert_eq!(
(
phase.budget.usage.bytes,
phase.budget.usage.tokens,
phase.budget.usage.entries,
phase.budget.usage.references
),
(132, 31, 3, 0)
);
assert_eq!(phase.budget.producer_entries(), Some(3));
assert_eq!(phase.budget.producer_text(), Some((32, 0)));
assert_eq!(phase.budget.numeric_units(), 0);
assert_eq!(
(phase.model.records, phase.model.graph, phase.model.includes),
(1, 0, 2)
);
assert_eq!(phase.model.limits.max_files, 7);
assert_eq!(phase.model.limits.max_include_depth, 9);
assert_eq!(phase.model.limits.max_include_edges, 11);
assert_eq!(phase.model.limits.model.max_records, 1);
assert_eq!(phase.model.limits.model.max_graph_objects, 3);
}
#[test]
fn mixed_discovery_cumulative_entry_boundary_and_terminal_original_failure() {
// Deliberate private constructor fixture: fresh policy, never a widened owner.
for cap in [6, 5] {
let mut phase = MixedDiscovery::new(ProjectLimits::default()).unwrap();
phase.budget = Budget::new(Limits {
entries: cap,
bytes: 4096,
..source_profile(ProjectLimits::default())
})
.unwrap();
phase.budget.enable_text_producers(0).unwrap();
phase.budget.enable_entry_producers(0, 0).unwrap();
phase.inspect("main.arcadia", MAIN, true).unwrap();
phase.inspect("part.arcadia", PART, false).unwrap();
let last = phase.inspect("last.arcadia", LAST, false);
if cap == 6 {
assert!(last.unwrap().is_empty());
} else {
let errors = last.unwrap_err();
assert_eq!(errors[0].path, "last.arcadia");
assert_eq!(errors[0].diagnostic.span, Span { start: 0, end: 1 });
assert!(errors[0].diagnostic.message.contains("entry limit"));
let usage = phase.budget.usage;
assert_eq!(
phase
.inspect("later.arcadia", "malformed", false)
.unwrap_err(),
errors
);
assert_eq!(phase.budget.usage, usage);
}
}
let mut phase = MixedDiscovery::new(ProjectLimits::default()).unwrap();
let errors = phase
.inspect("broken.arcadia", "arcadia 8; project p \"P\" {}", true)
.unwrap_err();
assert_eq!(
errors[0].diagnostic.message,
"Mixed source8 requires explicit UUIDs."
);
let usage = phase.budget.usage;
assert_eq!(
phase.inspect("later.arcadia", LAST, false).unwrap_err(),
errors
);
assert_eq!(phase.budget.usage, usage);
}
#[test]
fn mixed_discovery_numeric_and_reference_work_is_not_restarted_per_unit() {
let mut phase = MixedDiscovery::new(ProjectLimits::default()).unwrap();
for (alias, number) in [("f", 2), ("g", 3)] {
let source = format!(
"calculation {alias} \"C\" id \"00000000-0000-0000-0000-{number:012}\" {{ input x id \"00000000-0000-0000-0000-000000000001\" : Number; result Number; body $x + 1; }}"
);
let before = phase.budget.numeric_units();
phase
.inspect(&format!("{alias}.arcadia"), &source, false)
.unwrap();
if number == 2 {
assert!(phase.budget.numeric_units() > 0);
} else {
assert_eq!(phase.budget.numeric_units(), 2 * before);
}
assert_eq!(phase.budget.usage.references, number - 1);
assert_eq!(phase.budget.producer_text().unwrap().1, 0);
}
assert_eq!(phase.model.records, 2);
}
+82 -8
View File
@@ -379,6 +379,59 @@ pub fn expect_new_file(
Ok(())
}
/// Captured mixed source bytes and discovery diagnostics, without save authority.
/// Handle discovery errors before checking the complete bundle. This is not an
/// atomic snapshot against another process and never performs journal recovery.
pub struct MixedSourceSnapshot {
bundle: SourceBundle,
diagnostics: Vec<SourceDiagnostic>,
}
impl MixedSourceSnapshot {
pub fn bundle(&self) -> &SourceBundle {
&self.bundle
}
pub fn diagnostics(&self) -> &[SourceDiagnostic] {
&self.diagnostics
}
}
/// Discover explicit-ID mixed source8 through the shared grammar without writes.
/// Complete semantic checking remains `language::check_mixed_bundle`; discovery
/// alone does not admit a model. Source bytes are capped before disk reads.
pub fn load_mixed_source(
entry: &Path,
mut limits: ProjectLimits,
) -> Result<MixedSourceSnapshot, SourceFileError> {
limits.model.max_source_bytes = limits.model.max_source_bytes.min(64 * 1024 * 1024);
let (root, entry) = root_entry(entry)?;
let snapshot = DiskProject {
root,
bundle: SourceBundle {
entry,
files: BTreeMap::new(),
},
expectations: BTreeMap::new(),
reachable: BTreeSet::new(),
diagnostics: Vec::new(),
};
let mut discovery = language::MixedDiscovery::new(limits).map_err(SourceFileError::Limit)?;
let captured = discover(snapshot, limits, DiscoveryMode::Mixed(&mut discovery))?;
drop(discovery);
let DiskProject {
bundle,
diagnostics,
..
} = captured;
Ok(MixedSourceSnapshot {
bundle,
diagnostics,
})
}
enum DiscoveryMode<'a> {
Ordinary,
Mixed(&'a mut language::MixedDiscovery),
}
/// Merge editor overlays and discover only files absent from retained buffers.
/// Existing drafts (including detached ones) and their disk baselines survive.
pub fn discover_missing(
@@ -390,6 +443,14 @@ pub fn discover_missing(
result.bundle.files.extend(overlays.clone());
result.reachable.clear();
result.diagnostics.clear();
discover(result, limits, DiscoveryMode::Ordinary)
}
fn discover(
mut result: DiskProject,
limits: ProjectLimits,
mut mode: DiscoveryMode<'_>,
) -> Result<DiskProject, SourceFileError> {
check_snapshot(&result, limits)?;
let mut stack = vec![(result.bundle.entry.clone(), 0usize, None::<SourceLocation>)];
let mut edges = 0usize;
@@ -492,12 +553,20 @@ pub fn discover_missing(
}
}
}
match inspect_includes(
&path,
&result.bundle.files[&path],
path == result.bundle.entry,
limits,
) {
let includes = match &mut mode {
DiscoveryMode::Ordinary => inspect_includes(
&path,
&result.bundle.files[&path],
path == result.bundle.entry,
limits,
),
DiscoveryMode::Mixed(discovery) => discovery.inspect(
&path,
&result.bundle.files[&path],
path == result.bundle.entry,
),
};
match includes {
Ok(includes) => {
edges = edges.saturating_add(includes.len());
if edges > limits.max_include_edges {
@@ -517,7 +586,12 @@ pub fn discover_missing(
.map(|include| (include.target, depth + 1, Some(include.location))),
);
}
Err(diagnostics) => result.diagnostics.extend(diagnostics),
Err(diagnostics) => {
result.diagnostics.extend(diagnostics);
if matches!(mode, DiscoveryMode::Mixed(_)) {
break;
}
}
}
}
check_snapshot(&result, limits)?;
@@ -525,7 +599,7 @@ pub fn discover_missing(
&result.root,
result.expectations.keys().chain(result.bundle.files.keys()),
)?;
if result.diagnostics.is_empty() {
if result.diagnostics.is_empty() && matches!(mode, DiscoveryMode::Ordinary) {
match compile_bundle(&result.reachable_bundle(), limits) {
Ok(compiled) => result.diagnostics = compiled.diagnostics,
Err(diagnostics) => result.diagnostics = diagnostics,
+318
View File
@@ -0,0 +1,318 @@
//! Actual filesystem discovery followed by the public borrowed complete checker.
use oa_core::{analysis as a, language, source_files::*, source_project::*, *};
use std::{collections::BTreeMap, fs};
fn id(n: u128) -> Id {
Id::from_u128(n)
}
fn document(body: &str) -> String {
format!(
"arcadia 8;\nproject p \"Project\" id \"{}\" {{\n{body}\n}}\n",
id(1)
)
}
fn write(files: &[(&str, String)]) -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
for (path, text) in files {
fs::write(dir.path().join(path), text).unwrap();
}
dir
}
fn unchanged(dir: &tempfile::TempDir, files: &[(&str, String)]) {
assert_eq!(fs::read_dir(dir.path()).unwrap().count(), files.len());
for (path, text) in files {
assert_eq!(fs::read(dir.path().join(path)).unwrap(), text.as_bytes());
}
}
fn calculation(alias: &str, n: u128, body: a::Expr) -> a::Definition {
a::Definition {
id: id(n),
alias: alias.into(),
name: "Calculation".into(),
description: String::new(),
body: a::DefinitionBody::Calculation(a::CalculationDefinition {
parameters: vec![a::Parameter {
id: a::ParameterId(id(1)),
name: "x".into(),
value_type: a::AnalysisType::Scalar(a::ScalarType::Number),
}],
result: a::AnalysisType::Scalar(a::ScalarType::Number),
body: a::Binding::Expression(body),
}),
}
}
fn calc(alias: &str, n: u128, body: &str) -> String {
format!(
"calculation {alias} \"Calculation\" id \"{}\" {{ input x id \"{}\" : Number; result Number; body {body}; }}",
id(n),
id(1)
)
}
#[test]
fn mixed_files_complete_model_nested_ordinary_scoped_cross_file_and_original_bytes() {
let files = [
(
"main.arcadia",
document(
"// include \"ignored.arcadia\"; 海\ninclude \"f.arcadia\"; include \"g.arcadia\";",
),
),
(
"f.arcadia",
format!(
"{}\noa_entity team \"Team\" id \"{}\" {{ oa_actor crew \"Crew\" id \"{}\" {{}} }}\r\n",
calc("f", 2, "@g(x: $x)"),
id(4),
id(5)
),
),
("g.arcadia", calc("g", 3, "$x")),
];
let dir = write(&files);
let snapshot =
load_mixed_source(&dir.path().join("main.arcadia"), ProjectLimits::default()).unwrap();
assert!(
snapshot.diagnostics().is_empty(),
"{:?}",
snapshot.diagnostics()
);
assert_eq!(snapshot.bundle().entry, "main.arcadia");
assert_eq!(
snapshot.bundle().files,
files
.iter()
.map(|(p, t)| (p.to_string(), t.clone()))
.collect()
);
let mut expected = Project::empty();
expected.id = id(1);
expected.alias = "p".into();
expected.name = "Project".into();
expected.definitions.insert(
id(2),
calculation(
"f",
2,
a::Expr::Call {
definition: a::DefinitionRef::Local(id(3)),
arguments: BTreeMap::from([(
a::ParameterId(id(1)),
a::Expr::Parameter(a::ParameterId(id(1))),
)]),
},
),
);
expected.definitions.insert(
id(3),
calculation("g", 3, a::Expr::Parameter(a::ParameterId(id(1)))),
);
for (n, alias, name, kind, parent) in [
(4, "team", "Team", Kind::Entity, None),
(5, "crew", "Crew", Kind::Actor, Some(id(4))),
] {
expected.elements.insert(
id(n),
Element {
id: id(n),
alias: alias.into(),
name: name.into(),
description: String::new(),
parent,
body: ElementBody::new(kind, Layer::Oa).unwrap(),
},
);
}
let checked =
language::check_mixed_bundle(snapshot.bundle(), ProjectLimits::default()).unwrap();
assert_eq!(checked.project(), &expected);
assert!(checked.warnings().is_empty());
drop(checked);
drop(snapshot);
unchanged(&dir, &files);
}
#[test]
fn mixed_files_terminal_syntax_preserves_first_path_and_still_reports_journal() {
let files = [
(
"main.arcadia",
document("include \"bad.arcadia\"; include \"later.arcadia\";"),
),
("bad.arcadia", "oa_entity missing \"Missing\" {}".into()),
("later.arcadia", "must not parse".into()),
];
let dir = write(&files);
fs::write(
dir.path().join(".arcadia-save-main.arcadia.journal"),
b"unrecovered",
)
.unwrap();
let snapshot =
load_mixed_source(&dir.path().join("main.arcadia"), ProjectLimits::default()).unwrap();
assert_eq!(snapshot.diagnostics().len(), 2);
assert_eq!(snapshot.diagnostics()[0].path, "bad.arcadia");
assert_eq!(
snapshot.diagnostics()[0].diagnostic.message,
"Mixed source8 requires explicit UUIDs."
);
assert_eq!(snapshot.diagnostics()[1].diagnostic.code, "pending_save");
assert!(!snapshot.bundle().files.contains_key("later.arcadia"));
assert_eq!(
fs::read(dir.path().join("later.arcadia")).unwrap(),
b"must not parse"
);
assert_eq!(
fs::read(dir.path().join(".arcadia-save-main.arcadia.journal")).unwrap(),
b"unrecovered"
);
for (path, text) in &files {
assert_eq!(fs::read(dir.path().join(path)).unwrap(), text.as_bytes());
}
assert_eq!(fs::read_dir(dir.path()).unwrap().count(), 4);
}
#[test]
fn mixed_files_identity_classes_and_malformed_units_are_discovery_errors() {
let valid = calc("f", 2, "$x");
let cases = [
document("").replace(&format!(" id \"{}\"", id(1)), ""),
document("oa_entity e \"E\" {}"),
document(&valid.replace(&format!("id \"{}\"", id(2)), "")),
document(&valid.replace(&format!("input x id \"{}\"", id(1)), "input x")),
document("calculation malformed"),
];
for (i, text) in cases.into_iter().enumerate() {
let files = [("main.arcadia", text)];
let dir = write(&files);
let snapshot =
load_mixed_source(&dir.path().join("main.arcadia"), ProjectLimits::default()).unwrap();
assert_eq!(snapshot.diagnostics().len(), 1);
assert_eq!(snapshot.diagnostics()[0].path, "main.arcadia");
if i < 4 {
assert_eq!(
snapshot.diagnostics()[0].diagnostic.message,
"Mixed source8 requires explicit UUIDs."
);
} else {
assert_eq!(snapshot.diagnostics()[0].diagnostic.code, "invalid_source");
}
unchanged(&dir, &files);
}
}
#[test]
fn mixed_files_include_failures_and_cycles_do_not_become_partial_admission() {
for (include, code) in [
("missing.arcadia", "missing_include"),
("../escape.arcadia", "invalid_path"),
] {
let files = [("main.arcadia", document(&format!("include \"{include}\";")))];
let dir = write(&files);
let snapshot =
load_mixed_source(&dir.path().join("main.arcadia"), ProjectLimits::default()).unwrap();
assert!(
snapshot
.diagnostics()
.iter()
.any(|d| d.diagnostic.code == code)
);
unchanged(&dir, &files);
}
let files = [
("main.arcadia", document("include \"part.arcadia\";")),
("part.arcadia", "include \"part.arcadia\";".into()),
];
let dir = write(&files);
let snapshot =
load_mixed_source(&dir.path().join("main.arcadia"), ProjectLimits::default()).unwrap();
assert!(snapshot.diagnostics().is_empty());
let errors = match language::check_mixed_bundle(snapshot.bundle(), ProjectLimits::default()) {
Ok(_) => panic!("cycle admitted"),
Err(errors) => errors,
};
assert!(errors.iter().any(|d| d.diagnostic.code == "include_cycle"));
unchanged(&dir, &files);
}
#[test]
fn mixed_files_limits_encoding_nonregular_and_physical_aliases() {
let files = [
("main.arcadia", document("include \"part.arcadia\";")),
("part.arcadia", "include \"last.arcadia\";\r\n".into()),
("last.arcadia", "//é\r\n".into()),
];
let dir = write(&files);
let entry = dir.path().join("main.arcadia");
for (kind, cap) in [("files", 3), ("depth", 2), ("edges", 2), ("bytes", 132)] {
for allowed in [cap, cap - 1] {
let mut limits = ProjectLimits::default();
match kind {
"files" => limits.max_files = allowed,
"depth" => limits.max_include_depth = allowed,
"edges" => limits.max_include_edges = allowed,
_ => limits.model.max_source_bytes = allowed,
};
let result = load_mixed_source(&entry, limits);
if allowed == cap {
assert!(result.unwrap().diagnostics().is_empty(), "{kind}");
} else {
match result {
Err(_) => {}
Ok(snapshot) => assert!(!snapshot.diagnostics().is_empty(), "{kind}"),
}
}
unchanged(&dir, &files);
}
}
fs::write(dir.path().join("last.arcadia"), [0xff]).unwrap();
let snapshot = load_mixed_source(&entry, ProjectLimits::default()).unwrap();
assert!(
snapshot
.diagnostics()
.iter()
.any(|d| d.diagnostic.code == "source_encoding")
);
fs::remove_file(dir.path().join("last.arcadia")).unwrap();
fs::create_dir(dir.path().join("last.arcadia")).unwrap();
let snapshot = load_mixed_source(&entry, ProjectLimits::default()).unwrap();
assert!(
snapshot
.diagnostics()
.iter()
.any(|d| d.diagnostic.code == "invalid_path")
);
let collision = write(&[
(
"main.arcadia",
document("include \"part.arcadia\"; include \"PART.arcadia\";"),
),
("part.arcadia", "// case alias\n".into()),
]);
assert!(matches!(
load_mixed_source(
&collision.path().join("main.arcadia"),
ProjectLimits::default()
),
Err(SourceFileError::Path(_))
));
let aliases = write(&[
(
"main.arcadia",
document("include \"a.arcadia\"; include \"b.arcadia\";"),
),
("a.arcadia", "// same physical file\n".into()),
]);
fs::hard_link(
aliases.path().join("a.arcadia"),
aliases.path().join("b.arcadia"),
)
.unwrap();
assert!(matches!(
load_mixed_source(
&aliases.path().join("main.arcadia"),
ProjectLimits::default()
),
Err(SourceFileError::Path(_))
));
}
@@ -0,0 +1,48 @@
# Mixed CLI core discovery implementation
Base bab6f96a; selected root contract f810fb40. This contribution owns core only.
source_files::load_mixed_source returns opaque MixedSourceSnapshot with immutable
bundle/diagnostics views. It consumes a fresh private DiskProject after final
checks and drops root/expectations/reachability; no reachable-bundle clone, model,
index, save or recovery authority escapes. The byte allowance is clamped to the
existing64MiB ceiling before disk reads; other caller fields are preserved.
The existing discovery loop has a private borrowed ordinary/mixed selector.
Ordinary overlays, aggregation, limits, final compilation and journal behavior
are preserved. Mixed selects a shared Budget/ModelCounts phase and skips ordinary
compilation. Syntax/budget failure records the original file diagnostic and stops
parsing later siblings, while final disk/physical/journal checks still execute.
Disk/encoding/path failures retain existing classifications and safeguards.
Both producer modes start once at zero. Each inspected file reserves its phase
slot before scanning. Bytes/tokens/references/numeric/syntax/model work accumulate
across units. Remaining text/value/owner reservations remain conservative after
unit destruction; in-parser temporary/UUID/key releases are unchanged. No edit
positions or complete model are constructed during discovery. Final public
semantic checking is a separate pass with the original caller policy.
A narrow shared parse_unit helper centralizes exactly the existing Parser setup,
unit(entry,true) call, state extraction and Parser drop. Complete mixed parsing
keeps all subsequent metadata/projection/traversal/accounting operations. Source
review finds no semantic blocker; helper-frame effects still require parent's
actual existing stack gates and prologue comparison, not a source-only claim.
Three internal discovery tests cover exact132-byte/31-token/3-event chain totals,
conservative text32/metadata3 after unit drops, all temporary balances, original
policy capture, cumulative6/5 entry boundary with no later-file remapping, syntax
failure terminality and positive cumulative numeric/reference work. The tight
entry profile is explicitly a fresh internal fixture, not a public policy knob.
Five external mixed_source_files tests exercise real filesystem capture and
independently authored complete model equality (nested ordinary participants,
cross-file calculations and scoped parameters), original bytes, four missing-ID
classes, malformed syntax, unparsed later sibling plus preserved pending journal,
missing/escaping/cyclic includes, file/byte/depth/edge boundaries, invalid UTF-8,
nonregular files, case collisions and hard-link aliases. CLI worker separately
owns OA/SA rich command fixtures/report/actual executable tests and documentation.
No old test body or expected outcome was changed.
Only owned Rust files were formatted directly with skip_children=true. No Cargo,
Rust compiler, rustdoc, CLI execution or remote operation ran in this contribution.
Parent owns all runtime, production ordinary-regression, stack, lint and combined
source acceptance. Existing depth/platform/accounting limitations remain intact.