diff --git a/crates/oa-core/src/language/analysis_source/mixed/carried.rs b/crates/oa-core/src/language/analysis_source/mixed/carried.rs index 3c07e1d8..c57de87c 100644 --- a/crates/oa-core/src/language/analysis_source/mixed/carried.rs +++ b/crates/oa-core/src/language/analysis_source/mixed/carried.rs @@ -1,7 +1,8 @@ -//! Fixture-constructible carried phase authority, not an emitted-output result. +//! Complete pending-backed authority and fixture-only carried snapshots. //! Checks existing logical phase totals; does not meter every parser allocation. use super::*; use project::accounting::{Ledger, Slots}; +use project::ordinary_index::rename::emission::PendingRename; struct SnapshotPolicy { limits: Limits, @@ -15,6 +16,7 @@ struct RetainedSnapshot { retained: Slots, } enum SnapshotBacking<'s> { + Pending(&'s PendingRename<'s>), Borrowed { bundle: &'s SourceBundle, policy: SnapshotPolicy, @@ -27,6 +29,7 @@ pub(super) struct CarriedInput<'s> { backing: SnapshotBacking<'s>, } enum GuardBacking<'s> { + Pending(&'s PendingRename<'s>), Borrowed, Retained(&'s RetainedSnapshot), } @@ -37,8 +40,26 @@ pub(super) struct CarriedGuard<'s> { backing: GuardBacking<'s>, } impl<'s> CarriedInput<'s> { + pub(super) fn from_pending<'t: 's>(pending: &'s PendingRename<'t>) -> Self { + Self { + backing: SnapshotBacking::Pending(pending), + } + } fn into_parts(self) -> (&'s SourceBundle, SnapshotPolicy, CarriedGuard<'s>) { match self.backing { + SnapshotBacking::Pending(pending) => { + let (limits, project_limits) = pending.carried_policy(); + ( + pending.carried_bundle(), + SnapshotPolicy { + limits, + project_limits, + }, + CarriedGuard { + backing: GuardBacking::Pending(pending), + }, + ) + } SnapshotBacking::Borrowed { bundle, policy } => ( bundle, policy, @@ -62,6 +83,7 @@ impl<'s> CarriedInput<'s> { impl CarriedGuard<'_> { pub(super) fn slots(&self) -> Slots { match &self.backing { + GuardBacking::Pending(pending) => pending.carried_slots(), GuardBacking::Borrowed => Slots::default(), GuardBacking::Retained(owner) => owner.retained, } @@ -111,9 +133,8 @@ fn parse_text_producers( ) } -/// Explicit retained text and entry producer fixture route. -#[cfg(test)] -fn parse_entry_producers( +/// Combined producer route, selected only by sealed pending or fixture authority. +pub(super) fn parse_entry_producers( input: CarriedInput<'_>, ) -> Result, Vec> { let (bundle, policy, guard) = input.into_parts(); diff --git a/crates/oa-core/src/language/analysis_source/mixed/project/ordinary_index/rename/emission.rs b/crates/oa-core/src/language/analysis_source/mixed/project/ordinary_index/rename/emission.rs index a02552f2..2ffefa65 100644 --- a/crates/oa-core/src/language/analysis_source/mixed/project/ordinary_index/rename/emission.rs +++ b/crates/oa-core/src/language/analysis_source/mixed/project/ordinary_index/rename/emission.rs @@ -1,4 +1,5 @@ -//! Owned output only. No parser, extraction or accepted-result conversion. +//! Owned unverified emission; consumed actual-output verification is a private child. +pub(in crate::language::analysis_source) mod acceptance; use super::*; use crate::language::analysis_source::budget::Limits; use crate::source_project::ProjectLimits; diff --git a/crates/oa-core/src/language/analysis_source/mixed/project/ordinary_index/rename/emission/acceptance.rs b/crates/oa-core/src/language/analysis_source/mixed/project/ordinary_index/rename/emission/acceptance.rs new file mode 100644 index 00000000..da5e53cc --- /dev/null +++ b/crates/oa-core/src/language/analysis_source/mixed/project/ordinary_index/rename/emission/acceptance.rs @@ -0,0 +1,175 @@ +//! Actual-byte verification and owning result; no installation or mutable extraction. +use super::*; +use crate::language::analysis_source::budget::Usage; +use crate::language::analysis_source::mixed::{carried, project}; +use crate::source_project::SourceDiagnostic; + +pub(in crate::language::analysis_source) struct AcceptedRename { + bundle: SourceBundle, + project: Project, + warnings: Vec, + limits: Limits, + project_limits: ProjectLimits, + retained: Slots, + verification_usage: Usage, + verification_numeric_units: u64, +} +impl AcceptedRename { + pub fn bundle(&self) -> &SourceBundle { + &self.bundle + } + pub fn project(&self) -> &Project { + &self.project + } + pub fn warnings(&self) -> &[SourceDiagnostic] { + &self.warnings + } + pub fn policy(&self) -> (Limits, ProjectLimits) { + (self.limits, self.project_limits) + } + pub fn retained(&self) -> Slots { + self.retained + } + pub fn verification_work(&self) -> (Usage, u64) { + (self.verification_usage, self.verification_numeric_units) + } +} +impl PendingRename<'_> { + pub(in crate::language::analysis_source::mixed) fn carried_bundle(&self) -> &SourceBundle { + &self.bundle + } + pub(in crate::language::analysis_source::mixed) fn carried_policy( + &self, + ) -> (Limits, ProjectLimits) { + (self.limits, self.project_limits) + } + pub(in crate::language::analysis_source::mixed) fn carried_slots(&self) -> Slots { + self.retained + } + pub fn verify(self) -> Result { + let (project, warnings, retained, verification_usage, verification_numeric_units) = { + let syntax = carried::parse_entry_producers(carried::CarriedInput::from_pending(&self)) + .map_err(|diagnostics| Failure { diagnostics })?; + let mut resolved = project::resolve(syntax)?; + let equal = equal_target(&mut resolved, self.desired)?; + if !equal { + return Err(Failure::typed( + &resolved.syntax, + None, + error( + Span::default(), + "Verified output differs from the complete rename target.", + ), + )); + } + let retained = warning_inventory(&resolved.warnings) + .and_then(|warnings| self.retained.add(warnings, Span::default())) + .and_then(|retained| { + check_destination(&resolved.syntax.budget, resolved.storage.live, retained)?; + Ok(retained) + }) + .map_err(|e| Failure::typed(&resolved.syntax, None, e))?; + let usage = resolved.syntax.budget.usage; + let numeric = resolved.syntax.budget.numeric_units(); + let project::ResolvedMixedProject { + syntax, + project, + warnings, + expressions, + occurrences, + static_limits: _, + static_usage: _, + storage: _, + } = resolved; + // The full live ledger remains intact through the checks above. + // Only scalar destination accounting escapes these actual drops. + drop(expressions); + drop(occurrences); + drop(syntax); + (project, warnings, retained, usage, numeric) + }; + Ok(AcceptedRename { + bundle: self.bundle, + project, + warnings, + limits: self.limits, + project_limits: self.project_limits, + retained, + verification_usage, + verification_numeric_units, + }) + } +} +// Every actual Expr root is lowered into this arena. The old pending expression +// count cannot authenticate parsed topology. Keep this boundary independently +// testable without changing the inherited policy or instrumenting comparison. +fn equal_target( + resolved: &mut project::ResolvedMixedProject<'_>, + desired: &Project, +) -> Result { + let frontier = Slots::entries(resolved.expressions.len()); + resolved + .storage + .reserve(&resolved.syntax.budget, frontier, Span::default()) + .map_err(|e| Failure::typed(&resolved.syntax, None, e))?; + let equal = resolved.project.eq(desired); + resolved.storage.release(frontier); + Ok(equal) +} +fn check_destination(budget: &Budget, live: Slots, retained: Slots) -> Result<(), TypedError> { + let at = Span::default(); + if retained.entries > live.entries + || retained.occurrences > live.occurrences + || retained.text > live.text + { + return Err(error( + at, + "Accepted inventory exceeds live verification storage.", + )); + } + budget.lowering_entries(retained.entries, at)?; + budget.lowering_occurrences(retained.occurrences, at)?; + budget.index_bytes(retained.text, at) +} +fn warning_inventory(warnings: &[SourceDiagnostic]) -> Result { + let mut inventory = Slots::default(); + for SourceDiagnostic { + path, + diagnostic, + related, + } in warnings + { + let language::Diagnostic { + severity, + code, + object_id: _, + field, + line: _, + column: _, + message, + span: _, + } = diagnostic; + if !related.is_empty() || *severity == validation::Severity::Error { + return Err(error(Span::default(), "Unexpected accepted warning shape.")); + } + let text = [ + path.len(), + code.len(), + field.as_ref().map_or(0, String::len), + message.len(), + ] + .into_iter() + .try_fold(0, |n, k| add(n, k, Span::default()))?; + inventory = inventory.add( + Slots { + entries: 1, + occurrences: 0, + text, + }, + Span::default(), + )?; + } + Ok(inventory) +} +#[cfg(test)] +mod tests; diff --git a/crates/oa-core/src/language/analysis_source/mixed/project/ordinary_index/rename/emission/acceptance/tests.rs b/crates/oa-core/src/language/analysis_source/mixed/project/ordinary_index/rename/emission/acceptance/tests.rs new file mode 100644 index 00000000..385bb9f3 --- /dev/null +++ b/crates/oa-core/src/language/analysis_source/mixed/project/ordinary_index/rename/emission/acceptance/tests.rs @@ -0,0 +1,807 @@ +use super::*; +use crate::language::{ParseLimits, analysis_source::mixed::parse_with_profile}; +use crate::{Layer, analysis as a}; +use project::tests::{id, rich_bundle, rich_model, single}; +const MISMATCH: &str = "Verified output differs from the complete rename target."; +const NEXT: &str = + "arcadia 8;\nproject next \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\n\n}\n"; +const UNNAMED: &str = + "arcadia 8;\nproject next \"\" id \"00000000-0000-0000-0000-000000000001\" {\n\n}\n"; +fn profile(entries: usize, bytes: usize) -> Limits { + Limits { + entries, + bytes, + ..Limits::source8(ParseLimits::UNLIMITED) + } +} +fn limited(b: &SourceBundle, limits: Limits, policy: ProjectLimits) -> AuthoredMixedProject<'_> { + project::resolve(parse_with_profile(b, policy, limits).unwrap()) + .unwrap() + .index() + .unwrap() + .index_ordinary() + .unwrap() +} +fn authored(b: &SourceBundle) -> AuthoredMixedProject<'_> { + limited(b, profile(100_000, 1_000_000), ProjectLimits::default()) +} +fn expected(name: &str) -> Project { + Project { + schema_version: 8, + id: id(1), + alias: "next".into(), + name: name.into(), + description: String::new(), + definitions: BTreeMap::new(), + elements: BTreeMap::new(), + relations: BTreeMap::new(), + diagrams: BTreeMap::new(), + artifact_links: BTreeMap::new(), + traces: BTreeMap::new(), + } +} +fn failure(result: Result) -> Failure { + match result { + Ok(_) => panic!("unexpected verification success"), + Err(e) => e, + } +} +fn diagnostic(failure: &Failure, message: &str, start: usize, end: usize) { + assert_eq!(failure.diagnostics.len(), 1); + let d = &failure.diagnostics[0]; + assert_eq!(d.path, "main.arcadia"); + assert_eq!(d.diagnostic.code, "invalid_source"); + assert_eq!(d.diagnostic.message, message); + assert_eq!(d.diagnostic.span, Span { start, end }); + assert!(d.related.is_empty()); +} +fn same_policy(accepted: &AcceptedRename, limits: Limits, policy: ProjectLimits) { + let (a, b) = accepted.policy(); + assert_eq!( + [a.bytes, a.tokens, a.entries, a.references, a.frames], + [ + limits.bytes, + limits.tokens, + limits.entries, + limits.references, + limits.frames + ] + ); + assert_eq!(a.numeric_units, limits.numeric_units); + assert_eq!( + [ + b.max_files, + b.max_include_depth, + b.max_include_edges, + b.model.max_source_bytes, + b.model.max_graph_objects, + b.model.max_records, + b.model.max_step_references + ], + [ + policy.max_files, + policy.max_include_depth, + policy.max_include_edges, + policy.model.max_source_bytes, + policy.model.max_graph_objects, + policy.model.max_records, + policy.model.max_step_references + ] + ); +} +#[test] +fn actual_minimal_bytes_full_model_and_late_decode_boundary() { + for bytes in [152, 151] { + let b = single(""); + let before = b.clone(); + let limits = profile(17, bytes); + let source = limited(&b, limits, ProjectLimits::default()); + assert_eq!(source.indexed.resolved.syntax.budget.usage.bytes, 78); + let desired = expected("Project"); + let pending = source + .plan_rename(&desired) + .unwrap() + .emit_changed_unverified(&b) + .unwrap(); + assert_eq!(pending.bundle.files["main.arcadia"], NEXT); + let result = pending.verify(); + if bytes == 152 { + let accepted = result.unwrap(); + assert_eq!( + accepted.bundle(), + &SourceBundle { + entry: "main.arcadia".into(), + files: BTreeMap::from([("main.arcadia".into(), NEXT.into())]) + } + ); + assert_eq!(accepted.project(), &expected("Project")); + assert!(accepted.warnings().is_empty()); + assert_eq!( + accepted.retained(), + Slots { + entries: 1, + occurrences: 0, + text: 105 + } + ); + assert_eq!( + accepted.verification_work(), + ( + Usage { + bytes: 81, + tokens: 19, + entries: 1, + references: 0, + deepest_frame: 0 + }, + 0 + ) + ); + same_policy(&accepted, limits, ProjectLimits::default()); + } else { + let e = failure(result); + diagnostic(&e, "Source8 index/output byte limit exceeded.", 37, 38); + assert_eq!( + ( + e.diagnostics[0].diagnostic.line, + e.diagnostics[0].diagnostic.column + ), + (2, 27) + ); + } + assert_eq!(b, before); + assert_eq!(desired, expected("Project")); + } +} +fn warning() -> SourceDiagnostic { + SourceDiagnostic { + path: "main.arcadia".into(), + related: Vec::new(), + diagnostic: language::Diagnostic { + severity: validation::Severity::Warning, + code: "unnamed".into(), + object_id: Some(id(1)), + field: Some("name".into()), + message: "Give the project a descriptive name.".into(), + span: Span { start: 11, end: 69 }, + line: 2, + column: 1, + }, + } +} +#[test] +fn actual_warning_mapping_boundary_and_final_transfer() { + for bytes in [161, 160] { + let mut b = single(""); + b.files.insert( + "main.arcadia".into(), + "arcadia 8;\nproject p \"\" id \"00000000-0000-0000-0000-000000000001\" {\n\n}\n" + .into(), + ); + assert_eq!(b.files["main.arcadia"].len(), 71); + let limits = profile(18, bytes); + let source = limited(&b, limits, ProjectLimits::default()); + assert_eq!( + source.warnings()[0].diagnostic.span, + Span { start: 11, end: 66 } + ); + let desired = expected(""); + let pending = source + .plan_rename(&desired) + .unwrap() + .emit_changed_unverified(&b) + .unwrap(); + let result = pending.verify(); + if bytes == 161 { + let accepted = result.unwrap(); + assert_eq!(accepted.bundle().files["main.arcadia"], UNNAMED); + assert_eq!(accepted.project(), &expected("")); + assert_eq!(accepted.warnings(), [warning()]); + assert_eq!( + accepted.retained(), + Slots { + entries: 2, + occurrences: 0, + text: 157 + } + ); + assert_eq!( + accepted.verification_work(), + ( + Usage { + bytes: 74, + tokens: 19, + entries: 1, + references: 0, + deepest_frame: 0 + }, + 0 + ) + ); + same_policy(&accepted, limits, ProjectLimits::default()); + } else { + let e = failure(result); + diagnostic(&e, "Source8 index/output byte limit exceeded.", 11, 69); + assert_eq!( + ( + e.diagnostics[0].diagnostic.line, + e.diagnostics[0].diagnostic.column + ), + (2, 1) + ); + } + } +} +#[test] +fn owning_result_survives_all_inputs_and_preserves_every_policy_field() { + let limits = Limits { + bytes: 2048, + tokens: 101, + entries: 99, + references: 31, + frames: 17, + numeric_units: 8192, + }; + let policy = ProjectLimits { + max_files: 7, + max_include_depth: 3, + max_include_edges: 5, + model: ParseLimits { + max_source_bytes: 3000, + max_graph_objects: 67, + max_records: 123, + max_step_references: 37, + }, + }; + let accepted = { + let b = single(""); + let desired = expected("Project"); + let pending = limited(&b, limits, policy) + .plan_rename(&desired) + .unwrap() + .emit_changed_unverified(&b) + .unwrap(); + // A short pending borrow carries a longer independently owned target borrow. + { + let syntax = + carried::parse_entry_producers(carried::CarriedInput::from_pending(&pending)) + .unwrap(); + assert_eq!(syntax.bundle, &pending.bundle); + drop(syntax); + } + pending.verify().unwrap() + }; + assert_eq!(accepted.bundle().files["main.arcadia"], NEXT); + assert_eq!(accepted.project(), &expected("Project")); + assert!(accepted.warnings().is_empty()); + same_policy(&accepted, limits, policy); +} +#[test] +fn equal_byte_wrong_models_malformed_and_admission_failure_are_distinct() { + let b = single(""); + let desired = expected("Project"); + for (start, end, replacement) in [(19, 23, "nope"), (25, 32, "Changed"), (8, 9, "?")] { + let mut pending = authored(&b) + .plan_rename(&desired) + .unwrap() + .emit_changed_unverified(&b) + .unwrap(); + pending + .bundle + .files + .get_mut("main.arcadia") + .unwrap() + .replace_range(start..end, replacement); + assert_eq!(pending.bundle.files["main.arcadia"].len(), 81); + if start != 8 { + let actual = project::resolve( + carried::parse_entry_producers(carried::CarriedInput::from_pending(&pending)) + .unwrap(), + ) + .unwrap(); + assert_ne!(actual.project(), &desired); // Wrong output really is admissible. + drop(actual); + diagnostic(&failure(pending.verify()), MISMATCH, 0, 0); + } else { + let e = failure(pending.verify()); + assert_eq!(e.diagnostics[0].diagnostic.message, "Invalid source token."); + } + } + let b = single(&format!( + "oa_activity a \"A\" id \"{}\" {{ parent @b; }}\noa_activity b \"B\" id \"{}\" {{}}", + id(2), + id(3) + )); + let source = authored(&b); + let mut desired = source.project().clone(); + desired.alias = "next".into(); + let mut pending = source + .plan_rename(&desired) + .unwrap() + .emit_changed_unverified(&b) + .unwrap(); + let text = pending.bundle.files.get_mut("main.arcadia").unwrap(); + let at = text.find("parent @b").unwrap() + 8; + text.replace_range(at..at + 1, "a"); + let syntax = + carried::parse_entry_producers(carried::CarriedInput::from_pending(&pending)).unwrap(); + drop(syntax); // This corruption is grammar-valid but model-invalid. + let e = failure(pending.verify()); + assert!( + e.diagnostics + .iter() + .any(|d| d.diagnostic.code == "containment_cycle") + ); +} +#[test] +fn actual_expression_arena_controls_equality_reservation() { + let b = single(&format!( + "calculation c \"C\" id \"{}\" {{ result Number; body 10000; }}", + id(2) + )); + let source = limited(&b, profile(4096, 4096), ProjectLimits::default()); + let mut desired = source.project().clone(); + desired.alias = "next".into(); + let mut pending = source + .plan_rename(&desired) + .unwrap() + .emit_changed_unverified(&b) + .unwrap(); + assert_eq!(pending.expression_nodes, 1); + assert_eq!( + pending.retained, + Slots { + entries: 1, + occurrences: 0, + text: 195 + } + ); + pending + .bundle + .files + .get_mut("main.arcadia") + .unwrap() + .replace_range(160..165, "1 + 2"); + let mut actual = project::resolve( + carried::parse_entry_producers(carried::CarriedInput::from_pending(&pending)).unwrap(), + ) + .unwrap(); + assert_eq!(actual.expressions.len(), 3); + let before = actual.storage.live; + assert!(!equal_target(&mut actual, &desired).unwrap()); + assert_eq!(actual.storage.live, before); + // Labeled ledger fault: two free slots would fit old E1, but not actual E3. + // This is not an end-to-end late cap or a policy mutation. + actual.storage.live.entries = 4094; + let e = equal_target(&mut actual, &desired).unwrap_err(); + diagnostic(&e, "Source8 lowering entry limit exceeded.", 0, 0); + assert_eq!(actual.storage.live.entries, 4094); + drop(actual); + diagnostic(&failure(pending.verify()), MISMATCH, 0, 0); +} +#[test] +fn internal_warning_shape_and_destination_inventory_faults() { + assert_eq!( + warning_inventory(&[warning()]).unwrap(), + Slots { + entries: 1, + occurrences: 0, + text: 59 + } + ); + let mut bad = warning(); + bad.related.push(crate::source_project::SourceLocation { + path: "extra".into(), + span: Span::default(), + }); + assert_eq!( + warning_inventory(&[bad]).unwrap_err().message, + "Unexpected accepted warning shape." + ); + let mut bad = warning(); + bad.diagnostic.severity = validation::Severity::Error; + assert!(warning_inventory(&[bad]).is_err()); + let budget = Budget::new(profile(100, 4096)).unwrap(); + let live = Slots { + entries: 3, + occurrences: 2, + text: 200, + }; + for retained in [ + Slots { entries: 4, ..live }, + Slots { + occurrences: 3, + ..live + }, + Slots { text: 201, ..live }, + ] { + assert_eq!( + check_destination(&budget, live, retained) + .unwrap_err() + .message, + "Accepted inventory exceeds live verification storage." + ); + } + assert!( + Slots::text(usize::MAX) + .add(Slots::text(1), Span::default()) + .is_err() + ); + assert!( + Slots::entries(usize::MAX) + .add(Slots::entries(1), Span::default()) + .is_err() + ); +} +fn rich_desired(layer: Layer) -> Project { + let mut p = rich_model(layer); + p.alias = "w".into(); + p.elements.get_mut(&id(2)).unwrap().alias = "p".into(); + p.elements.get_mut(&id(3)).unwrap().alias = "n".into(); + let d = p.diagrams.get_mut(&id(5)).unwrap(); + d.alias = "z".into(); + d.nodes.get_mut(&id(4)).unwrap().alias = "v".into(); + for (n, name) in [(10, "g"), (11, "f")] { + let def = p.definitions.get_mut(&id(n)).unwrap(); + def.alias = name.into(); + let crate::analysis::DefinitionBody::Calculation(c) = &mut def.body else { + panic!("calculation") + }; + c.parameters[0].name = "y".into(); + c.parameters[1].name = "x".into(); + } + if layer == Layer::Sa { + p.elements.get_mut(&id(6)).unwrap().alias = "u".into(); + } + p +} +// Independently authored entire expected files: no edit iterator or output text +// contributes to these strings. The LF/CRLF transform applies to whole literals. +const RICH_A: &str = "calculation g \"F\" id \"00000000-0000-0000-0000-000000000010\" {\ninput y id \"00000000-0000-0000-0000-000000000001\" : Number;\ninput x id \"00000000-0000-0000-0000-000000000012\" : Number;\nresult Number; body $y + $x;\n}\n"; +const RICH_B: &str = "calculation f \"G\" id \"00000000-0000-0000-0000-000000000011\" {\ninput y id \"00000000-0000-0000-0000-000000000001\" : Number;\ninput x id \"00000000-0000-0000-0000-000000000012\" : Number;\nresult Number; body $y - $x;\n}\n"; +fn rich_expected(layer: Layer) -> SourceBundle { + let (property, work, diagram, extra) = if layer == Layer::Oa { + ("oa_property", "oa_activity", "", "") + } else { + ( + "sa_property", + "sa_function", + "layer Sa;", + "sa_system u \"System\" id \"00000000-0000-0000-0000-000000000006\" {}", + ) + }; + SourceBundle { + entry: "main.arcadia".into(), + files: BTreeMap::from([ + ("a.arcadia".into(), RICH_A.into()), + ("b.arcadia".into(), RICH_B.into()), + ( + "main.arcadia".into(), + format!( + "arcadia 8;\nproject w \"Project\" id \"00000000-0000-0000-0000-000000000001\" {{\ninclude \"b.arcadia\"; include \"a.arcadia\";\nnode v id \"00000000-0000-0000-0000-000000000004\" {{ diagram @z; element @n; }}\n{property} n \"Value\" id \"00000000-0000-0000-0000-000000000003\" {{ subject @p; type Number; base @g(x: 2, y: @f(y: 3, x: 4)) + @g(y: 5, x: 6); }}\n{work} p \"Work\" id \"00000000-0000-0000-0000-000000000002\" {{}}\ndiagram z \"Diagram\" id \"00000000-0000-0000-0000-000000000005\" {{ {diagram} }}\n{extra}\n}}\n" + ), + ), + ]), + } +} +#[test] +fn rich_oa_sa_swaps_unicode_crlf_complete_actual_bytes_and_models() { + for (layer, bytes, unicode_bytes) in [(Layer::Oa, 900, 943), (Layer::Sa, 974, 1017)] { + for unicode in [false, true] { + let mut b = rich_bundle(layer); + let mut expected = rich_expected(layer); + if unicode { + for text in b.files.values_mut() { + *text = format!("// 雨\r\n{}", text.replace('\n', "\r\n")); + } + for text in expected.files.values_mut() { + *text = format!("// 雨\r\n{}", text.replace('\n', "\r\n")); + } + } + let before = b.clone(); + let desired = rich_desired(layer); + let accepted = authored(&b) + .plan_rename(&desired) + .unwrap() + .emit_changed_unverified(&b) + .unwrap() + .verify() + .unwrap(); + assert_eq!(accepted.bundle(), &expected); + assert_eq!(accepted.project(), &rich_desired(layer)); + assert!(accepted.warnings().is_empty()); + assert_eq!( + accepted.retained(), + Slots { + entries: 3, + occurrences: 0, + text: if unicode { + unicode_bytes + 42 + } else { + bytes + 42 + } + } + ); + assert_eq!(b, before); + assert_eq!(desired, rich_desired(layer)); + } + } +} +fn calculation(p: &mut Project) -> &mut a::CalculationDefinition { + let a::DefinitionBody::Calculation(c) = &mut p.definitions.get_mut(&id(2)).unwrap().body else { + panic!("calculation") + }; + c +} +#[test] +fn scoped_small_stack_actual_deep_wide_verification_and_drop() { + for (expression, result, nodes) in [ + ( + format!("{}1{}", "abs(".repeat(127), ")".repeat(127)), + "Number", + 128, + ), + ( + format!("[{}]", vec!["1"; 128].join(", ")), + "List(Number)", + 129, + ), + ] { + let body = format!( + "calculation c \"C\" id \"{}\" {{ result {result}; body {expression}; }}", + id(2) + ); + let b = single(&body); + let before = b.clone(); + let expected_bytes = format!( + "arcadia 8;\nproject next \"Project\" id \"00000000-0000-0000-0000-000000000001\" {{\n{body}\n}}\n" + ); + for mismatch in [false, true] { + let source = authored(&b); + let mut desired = source.project().clone(); + desired.alias = "next".into(); + let mut target = desired.clone(); + if result == "List(Number)" { + let a::Binding::Expression(a::Expr::List(items)) = + &mut calculation(&mut target).body + else { + panic!("list") + }; + items.pop(); // Actual left remains wide; target-only shape differs. + } else { + target.name = "Different".into(); + } + let mut pending = source + .plan_rename(&desired) + .unwrap() + .emit_changed_unverified(&b) + .unwrap(); + assert_eq!(pending.expression_nodes, nodes); + if mismatch { + pending.desired = ⌖ + } // Labeled target-only fault after valid emission. + std::thread::scope(|scope| { + let expected = &expected_bytes; + let desired = &desired; + std::thread::Builder::new() + .stack_size(128 * 1024) + .spawn_scoped(scope, move || { + let result = pending.verify(); + if mismatch { + diagnostic(&failure(result), MISMATCH, 0, 0); + } else { + let accepted = result.unwrap(); + assert_eq!(&accepted.bundle().files["main.arcadia"], expected); + assert_eq!(accepted.project(), desired); + drop(accepted); + } + }) + .unwrap() + .join() + .unwrap(); + }); + assert_eq!(b, before); + } + } + // Genuine inherited151 profile reaches a late parser refusal inside worker. + let b = single(""); + let desired = expected("Project"); + let pending = limited(&b, profile(17, 151), ProjectLimits::default()) + .plan_rename(&desired) + .unwrap() + .emit_changed_unverified(&b) + .unwrap(); + std::thread::scope(|scope| { + std::thread::Builder::new() + .stack_size(128 * 1024) + .spawn_scoped(scope, move || { + diagnostic( + &failure(pending.verify()), + "Source8 index/output byte limit exceeded.", + 37, + 38, + ); + }) + .unwrap() + .join() + .unwrap(); + }); +} +#[test] +fn zero_actual_expression_rejects_large_target_only_variants() { + let b = single(&format!( + "calculation c \"C\" id \"{}\" {{ result Number; body unbound; }}", + id(2) + )); + let mut deep = a::Expr::Boolean(true); + for _ in 0..2048 { + deep = a::Expr::Unary { + op: a::UnaryOp::Not, + operand: Box::new(deep), + }; + } + for expression in [deep, a::Expr::List(vec![a::Expr::Boolean(true); 4096])] { + let source = authored(&b); + let mut desired = source.project().clone(); + desired.alias = "next".into(); + let mut damaged = desired.clone(); + calculation(&mut damaged).body = a::Binding::Expression(expression); + let mut pending = source + .plan_rename(&desired) + .unwrap() + .emit_changed_unverified(&b) + .unwrap(); + assert_eq!(pending.expression_nodes, 0); + pending.desired = &damaged; + diagnostic(&failure(pending.verify()), MISMATCH, 0, 0); + } +} +#[test] +fn signed_zero_geometry_scalar_and_historical_equality_moves_actual_model() { + let b = rich_bundle(Layer::Oa); + let mut desired = rich_model(Layer::Oa); + desired.alias = "next".into(); + desired + .diagrams + .get_mut(&id(5)) + .unwrap() + .nodes + .get_mut(&id(4)) + .unwrap() + .position + .x = -0.0; + let accepted = authored(&b) + .plan_rename(&desired) + .unwrap() + .emit_changed_unverified(&b) + .unwrap() + .verify() + .unwrap(); + assert_eq!(accepted.project(), &desired); + assert_eq!( + accepted.project().diagrams[&id(5)].nodes[&id(4)] + .position + .x + .to_bits(), + 0.0f32.to_bits() + ); + let b = single(&format!( + r#" + oa_data_type a "A" id "{}" {{ definition {{ Real {{ minimum 0.0; maximum 1.0; }}; }}; default {{ Real 0.0; }}; }} + sa_data_type b "B" id "{}" {{ definition {{ Real {{ minimum 0.0; maximum 1.0; }}; }}; default {{ Real 0.0; }}; }} + trace t "T" id "{}" {{ oa @a; sa @b; kind Item; baseline {{ + oa {{ name "A"; description ""; }}; sa {{ name "B"; description ""; }}; + semantic {{ facets [{{ Data {{ RealMinimum {{ oa {{ Value 0.0; }}; sa {{ Value 0.0; }}; }}; }}; }}]; }}; + }}; }} + "#, + id(2), + id(3), + id(4) + )); + let source = authored(&b); + let mut desired = source.project().clone(); + desired.alias = "next".into(); + for e in desired.elements.values_mut() { + let body = match &mut e.body { + crate::ElementBody::Oa(crate::OaBody::Shared(b)) + | crate::ElementBody::Sa(crate::SaBody::Shared(b)) => b, + _ => panic!("data"), + }; + let crate::SharedBody::DataType { + definition, + default, + .. + } = body + else { + panic!("data") + }; + let crate::DataTypeDefinition::Real { minimum, .. } = definition else { + panic!("real") + }; + *minimum = Some(-0.0); + *default = Some(crate::DataValue::Real(-0.0)); + } + let facet = &mut desired + .traces + .get_mut(&id(4)) + .unwrap() + .baseline + .as_mut() + .unwrap() + .semantic + .as_mut() + .unwrap() + .facets[0]; + let crate::semantic::FacetReview::Data(crate::semantic::DataReview::RealMinimum(review)) = + facet + else { + panic!("historical") + }; + review.oa = crate::semantic::Observed::Value(Some(-0.0)); + review.sa = crate::semantic::Observed::Value(Some(-0.0)); + let accepted = source + .plan_rename(&desired) + .unwrap() + .emit_changed_unverified(&b) + .unwrap() + .verify() + .unwrap(); + assert_eq!(accepted.project(), &desired); + for e in accepted.project().elements.values() { + let body = match &e.body { + crate::ElementBody::Oa(crate::OaBody::Shared(b)) + | crate::ElementBody::Sa(crate::SaBody::Shared(b)) => b, + _ => panic!("data"), + }; + let crate::SharedBody::DataType { + definition: + crate::DataTypeDefinition::Real { + minimum: Some(minimum), + .. + }, + default: Some(crate::DataValue::Real(value)), + .. + } = body + else { + panic!("real") + }; + assert_eq!(minimum.to_bits(), 0.0f64.to_bits()); + assert_eq!(value.to_bits(), 0.0f64.to_bits()); + } +} +#[test] +fn four_file_cross_family_swap_preserves_entire_untouched_file_and_comments() { + let bundle=SourceBundle {entry:"main.arcadia".into(),files:BTreeMap::from([ + ("a.arcadia".into(),"calculation f \"F\" id \"00000000-0000-0000-0000-000000000010\" { result Number; body 1; }\r\n".into()), + ("b.arcadia".into(),"// f and w are comments\r\noa_activity w \"Work\" id \"00000000-0000-0000-0000-000000000002\" {}\r\n".into()), + ("main.arcadia".into(),"// 雨\r\narcadia 8;\r\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\r\ninclude \"a.arcadia\"; include \"b.arcadia\"; include \"untouched.arcadia\";\r\noa_property v \"Value\" id \"00000000-0000-0000-0000-000000000003\" { subject @w; type Number; base @f(); }\r\n}\r\n".into()), + ("untouched.arcadia".into(),"// 雪 untouched f w\r\n".into()), + ])}; + let expected=SourceBundle {entry:"main.arcadia".into(),files:BTreeMap::from([ + ("a.arcadia".into(),"calculation w \"F\" id \"00000000-0000-0000-0000-000000000010\" { result Number; body 1; }\r\n".into()), + ("b.arcadia".into(),"// f and w are comments\r\noa_activity f \"Work\" id \"00000000-0000-0000-0000-000000000002\" {}\r\n".into()), + ("main.arcadia".into(),"// 雨\r\narcadia 8;\r\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\r\ninclude \"a.arcadia\"; include \"b.arcadia\"; include \"untouched.arcadia\";\r\noa_property v \"Value\" id \"00000000-0000-0000-0000-000000000003\" { subject @f; type Number; base @w(); }\r\n}\r\n".into()), + ("untouched.arcadia".into(),"// 雪 untouched f w\r\n".into()), + ])}; + let source = authored(&bundle); + let mut desired = source.project().clone(); + desired.definitions.get_mut(&id(10)).unwrap().alias = "w".into(); + desired.elements.get_mut(&id(2)).unwrap().alias = "f".into(); + let pending = source + .plan_rename(&desired) + .unwrap() + .emit_changed_unverified(&bundle) + .unwrap(); + let pending = pending.verify().unwrap(); + assert_eq!(pending.project(), &desired); + assert!(pending.warnings().is_empty()); + assert_eq!(pending.bundle, expected); + assert_eq!( + pending.retained, + Slots { + entries: 4, + occurrences: 0, + text: 526 + } + ); +} diff --git a/harness/analysis-mixed-acceptance-implementation-20260925.md b/harness/analysis-mixed-acceptance-implementation-20260925.md new file mode 100644 index 00000000..7fc573be --- /dev/null +++ b/harness/analysis-mixed-acceptance-implementation-20260925.md @@ -0,0 +1,62 @@ +# Private actual-output rename acceptance + +Base `29417130c71f70c4eaefe8ed4c879efba78eb72c`. Implements the parent-selected +`harness/analysis-mixed-acceptance-contract-20260925.md` (root b86c8dbc) within the +private experimental mixed source path. No public source8, save, history, +installation or repeated carried-source emission is enabled. + +A consumed PendingRename now verifies its actual complete bundle through the +combined text/entry producer parser and existing complete resolver/admission. +CarriedInput and its slim guard borrow that entire pending owner; covariance +shortens the desired borrow to the verification scope without another parser +lifetime, policy copy in guards, arbitrary tuple constructor or heap wrapper. +Narrow immutable views supply the exact bundle, policy and retained O together. +The existing fixture constructors and old parser modes remain available unchanged. + +The actual resolved expression arena supplies E for equality reservation against +its full live storage. Complete actual Project PartialEq compares every field +with desired; old name-exempt equality and pending's original E are not used. +A static entry/default-span diagnostic reports mismatch. Warning inventory uses +an exhaustive borrowed diagnostic-field match, checks empty related/non-Error +shapes and sums checked entries/text. O+W is checked as a componentwise live +subset and against inherited ceilings without resetting or double-charging the +full ledger. Actual model/warnings move after equality; expression/occurrence +arenas, syntax and complete pending guard drop before the bundle moves into +AcceptedRename. Its private fields have read-only accessors, exact policies, +final retained slots and separate verification Usage/numeric work; no input or +desired borrow remains. + +Eleven new tests use the acceptance module filter. Independent minimal literal +and whole model verify 152/151 through one unchanged profile; warning literals +and full diagnostics verify 161/160 and final O+W=(2,0,157). Exact policy fields +and a lexical scope dropping all inputs exercise returned ownership. Previously +authored OA/SA/scoped-global swap and UTF-8/CRLF/all-file literals are reused, +including an untouched fourth file. Equal-byte wrong alias/name, malformed +source and parseable containment-cycle corruption separate equality, syntax and +model-admission refusal. Actual E1->3 corruption plus a labeled internal ledger +fault distinguishes reserving3 from trusting old1, without a fabricated late +end-to-end equality cap. Other internal controls cover warning shapes, final +subset and checked inventory overflow. + +Scoped 128 KiB workers perform actual verification/assertions/drop for deep128 +and wide129 expression nodes (success and target-only mismatch), plus the genuine +151-byte late refusal. All original prerequisites are prepared outside those +workers. Separate E0 target-only deep/wide and signed-zero geometry/scalar/history +cases exercise complete equality. Positive-zero bits in actual returned model +help distinguish moving the parsed model from substituting desired intent. + +Before execution, source review prompted correcting the carried module summary; +parent readback caught an unnamed-source test slice, replaced with the exact +independent 71-byte original literal. Local type inspection corrected the f32 +geometry bit assertion. These are pre-execution authoring corrections, not +runtime failures. No prior tests or stack sizes are edited. Only touched Rust +files were directly formatted with skip_children; compiler, full prior outcome +comparison, Clippy, format and measured prologue/stack gates belong to the parent. +No runtime result is claimed in this implementation note. + +The selected retained/decode/equality domain remains logical, not universal heap +or before-every-allocation accounting. Existing traversal/path-copy, delimiter, +ordinary-open, Pratt and duplicate-name scratch keep their independent controls +and stated exclusions; model/validator/numeric/decoder internals and native stack +remain separate domains. File counts do not tightly bound traversal path bytes. +Actual lifetime coercion and stack headroom still require the parent gates.