From 042e25efc25e0b73e4a39e1e794b48f88d4765bf Mon Sep 17 00:00:00 2001 From: "sorokin.oleg.edu" Date: Thu, 24 Sep 2026 21:20:11 -0700 Subject: [PATCH] feat(source): expose borrowed read-only mixed source eight validation --- crates/oa-core/src/language.rs | 35 +----- .../oa-core/src/language/analysis_source.rs | 8 +- .../src/language/analysis_source/budget.rs | 1 + .../src/language/analysis_source/cursor.rs | 1 + .../src/language/analysis_source/lower.rs | 7 +- .../src/language/analysis_source/mixed.rs | 20 +++- .../language/analysis_source/mixed/carried.rs | 40 ++++++- .../mixed/carried/public_read.rs | 65 +++++++++++ .../language/analysis_source/mixed/project.rs | 101 +++++++++++++++++- docs/language.html | 17 +++ docs/language.rst | 20 ++++ ...sis-public-read-implementation-20260925.md | 43 ++++++++ 12 files changed, 314 insertions(+), 44 deletions(-) create mode 100644 crates/oa-core/src/language/analysis_source/mixed/carried/public_read.rs create mode 100644 harness/analysis-public-read-implementation-20260925.md diff --git a/crates/oa-core/src/language.rs b/crates/oa-core/src/language.rs index 53ab722e..e2a2ca04 100644 --- a/crates/oa-core/src/language.rs +++ b/crates/oa-core/src/language.rs @@ -3,10 +3,9 @@ mod raw_tokens; #[path = "language/semantic_source.rs"] mod semantic_source; mod source3_bridge; -// Staged typed analysis only. Public mixed source8 admission waits for complete -// declaration lowering, source updates and the root-owned resource gate. -#[cfg(test)] +// Mixed source8 is exposed only through the separate borrowed read-only checker. mod analysis_source; +pub use analysis_source::{CheckedMixed, check_mixed_bundle}; #[path = "migration_v2/source.rs"] mod v2_source; use crate::*; @@ -480,7 +479,6 @@ fn parse_document(source: &str, limits: ParseLimits) -> Result { cursor: usize, value_depth: usize, limits: ParseLimits, - #[cfg(test)] mixed: Option>, } impl Parser<'_> { fn token(&self, index: usize) -> Option { - #[cfg(test)] if let Some(mixed) = &self.mixed { return mixed.token(index); } @@ -1236,7 +1231,6 @@ impl Parser<'_> { }) } fn syntax_entry(&mut self) -> Result<(), Error> { - #[cfg(test)] { let span = self.current().span; if let Some(mixed) = &mut self.mixed { @@ -1266,9 +1260,7 @@ impl Parser<'_> { if self.current().class != TokenClass::String { return Err(self.error("Expected a double-quoted string.")); } - #[cfg(test)] let span = self.current().span; - #[cfg(test)] if let Some(mixed) = &mut self.mixed { let raw = &self.source[span.start..span.end]; let value = mixed.quoted_text(raw, span)?; @@ -1290,9 +1282,8 @@ impl Parser<'_> { } fn optional_id(&mut self) -> Result { if self.text() != "id" { - #[cfg(test)] if self.mixed.is_some() { - return Err(self.error("Private mixed syntax requires explicit IDs.")); + return Err(self.error("Mixed source8 requires explicit UUIDs.")); } return Ok(Id::new_v4()); } @@ -1321,21 +1312,15 @@ impl Parser<'_> { Ok(id) } fn own_text(&mut self, bytes: usize, span: Span) -> Result<(), Error> { - #[cfg(test)] if let Some(mixed) = &mut self.mixed { mixed.own_text(bytes, span)?; } - #[cfg(not(test))] - let _ = (bytes, span); Ok(()) } fn release_text(&mut self, bytes: usize) { - #[cfg(test)] if let Some(mixed) = &mut self.mixed { mixed.release_text(bytes); } - #[cfg(not(test))] - let _ = bytes; } fn fields(&mut self) -> Result, Error> { self.take("{")?; @@ -1383,7 +1368,6 @@ impl Parser<'_> { .span .end; self.take(";")?; - #[cfg(test)] if let Some(mixed) = &mut self.mixed { mixed.projection(1, Span { start, end })?; } @@ -1412,7 +1396,6 @@ impl Parser<'_> { self.value_depth += 1; let result = self.value_inner(); self.value_depth -= 1; - #[cfg(test)] if let Ok(ast) = &result { let end = self .token(self.cursor - 1) @@ -1433,7 +1416,6 @@ impl Parser<'_> { fn value_inner(&mut self) -> Result { let token = self.current(); let offset = token.span.start; - #[cfg(test)] if let Some(mixed) = &self.mixed && (token.class == TokenClass::Number || matches!(self.text(), "-" | "+")) { @@ -1541,17 +1523,14 @@ impl Parser<'_> { fn unit(&mut self, entry: bool, includes_allowed: bool) -> Result { let mut header = if entry { self.take("arcadia")?; - #[cfg(test)] let mixed_mode = self.mixed.is_some(); - #[cfg(not(test))] - let mixed_mode = false; if if mixed_mode { self.text() != "8" } else { !matches!(self.text(), "3" | "4" | "5" | "6" | "7" | "8") } { return Err(self.error(if mixed_mode { - "Private mixed syntax requires arcadia 8." + "Mixed source requires arcadia 8." } else { "Unsupported schema version: expected arcadia 3, 4, 5, 6, 7 or 8." })); @@ -1561,7 +1540,6 @@ impl Parser<'_> { self.take(";")?; let start = self.current().span.start; self.take("project")?; - #[cfg(test)] if let Some(mixed) = &mut self.mixed { mixed.record(false, Span { start, end: start })?; } @@ -1606,7 +1584,6 @@ impl Parser<'_> { let mut open: Vec = Vec::new(); loop { let offset = self.current().span.start; - #[cfg(test)] if let Some(mixed) = &mut self.mixed && mixed.typed_header(self.cursor, open.is_empty()) { @@ -1658,7 +1635,6 @@ impl Parser<'_> { if !includes_allowed { return Err(self.error("An include requires a source bundle; single-string parsing never reads files.")); } - #[cfg(test)] if let Some(mixed) = &mut self.mixed { mixed.include(Span { start: offset, @@ -1705,7 +1681,6 @@ impl Parser<'_> { header = Some(entry_header); continue; } - #[cfg(test)] if self.mixed.is_some() && analysis_source::mixed::long_analysis_word(self.text()) { return Err(self.error("Use the short source8 analysis declaration word.")); } @@ -1713,7 +1688,6 @@ impl Parser<'_> { let word = self.current().text(self.source); let kind = declaration_kind(word) .ok_or_else(|| self.error(format!("Unknown declaration '{word}'.")))?; - #[cfg(test)] if let Some(mixed) = &mut self.mixed { mixed.record( matches!(kind, DeclKind::Element(..) | DeclKind::Relation(..)), @@ -1750,7 +1724,6 @@ impl Parser<'_> { let body_start = self.current().span.end; self.take("{")?; let lexical_owner = open.last().map(|&owner| declarations[owner].id); - #[cfg(test)] if let Some(mixed) = &mut self.mixed { let span = Span { start: offset, diff --git a/crates/oa-core/src/language/analysis_source.rs b/crates/oa-core/src/language/analysis_source.rs index 8f035a5c..1109b5a8 100644 --- a/crates/oa-core/src/language/analysis_source.rs +++ b/crates/oa-core/src/language/analysis_source.rs @@ -1,14 +1,16 @@ -//! Unresolved typed source8 syntax, staged for the complete source adapter. -//! This module does not admit a Project or enable public source8. +//! Private typed syntax and complete read-only mixed source8 resolution. mod budget; mod cursor; mod declarations; +#[cfg(test)] mod index; mod lex; mod lower; pub(super) mod mixed; mod syntax; +#[cfg(test)] mod update; +#[cfg(test)] mod write; #[cfg(test)] @@ -18,6 +20,7 @@ use super::Span; use crate::analysis::{BinaryOp, UnaryOp}; use crate::analysis_numeric::Exact; use budget::Budget; +pub use mixed::project::{CheckedMixed, check_mixed_bundle}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Error { @@ -104,6 +107,7 @@ pub enum Binding { Expression(Expression), } +#[cfg(test)] pub fn parse(source: &str, budget: &mut Budget) -> Result { let tokens = lex::tokens(source, budget)?; let mut cursor = cursor::Cursor::new(source, &tokens, budget); diff --git a/crates/oa-core/src/language/analysis_source/budget.rs b/crates/oa-core/src/language/analysis_source/budget.rs index 97b5b46f..9e109223 100644 --- a/crates/oa-core/src/language/analysis_source/budget.rs +++ b/crates/oa-core/src/language/analysis_source/budget.rs @@ -294,6 +294,7 @@ impl Budget { } } } + #[cfg(test)] pub fn numeric_units(&self) -> u64 { self.numeric.usage().numeric_units } diff --git a/crates/oa-core/src/language/analysis_source/cursor.rs b/crates/oa-core/src/language/analysis_source/cursor.rs index 82e5d4d4..6f9780db 100644 --- a/crates/oa-core/src/language/analysis_source/cursor.rs +++ b/crates/oa-core/src/language/analysis_source/cursor.rs @@ -42,6 +42,7 @@ impl Cursor<'_, '_> { self.cursor += 1; Ok(span) } + #[cfg(test)] pub(super) fn end(&self) -> Result<(), Error> { if self.cursor != self.tokens.len() { Err(error( diff --git a/crates/oa-core/src/language/analysis_source/lower.rs b/crates/oa-core/src/language/analysis_source/lower.rs index 14b5be02..4f90a42b 100644 --- a/crates/oa-core/src/language/analysis_source/lower.rs +++ b/crates/oa-core/src/language/analysis_source/lower.rs @@ -2,12 +2,15 @@ //! single complete-Project semantic admission after merging these additions. use super::declarations::{self as d, Declaration, DeclarationKind}; use super::*; +#[cfg(test)] +use crate::Project; use crate::analysis as a; use crate::analysis_numeric::Dimension; -use crate::{Element, ElementBody, Id, Kind, Layer, Project, SharedBody}; +use crate::{Element, ElementBody, Id, Kind, Layer, SharedBody}; use std::collections::{BTreeMap, BTreeSet}; /// Caller-assigned file key; spans are original offsets within that file. +#[cfg(test)] pub(super) struct Unit { pub file: usize, pub declarations: Vec, @@ -180,6 +183,7 @@ impl Tables { Ok(()) } + #[cfg(test)] fn ordinary(&mut self, project: &Project, budget: &Budget) -> Result<(), Error> { let at = Span::default(); self.insert(project.id, &project.alias, Category::Project, at, budget)?; @@ -270,6 +274,7 @@ impl Tables { /// Inputs must come from the same live Budget and stabilization pass. No scan, /// exact conversion or reference charge occurs here. The ordinary skeleton is /// not semantically admitted: its typed forward references may need additions. +#[cfg(test)] pub(super) fn lower( ordinary: &Project, units: &[Unit], diff --git a/crates/oa-core/src/language/analysis_source/mixed.rs b/crates/oa-core/src/language/analysis_source/mixed.rs index a8c18bd4..6d558ee7 100644 --- a/crates/oa-core/src/language/analysis_source/mixed.rs +++ b/crates/oa-core/src/language/analysis_source/mixed.rs @@ -13,7 +13,7 @@ //! value-end entries, projected value/field/header/declaration positions, lexical //! owner slots and the file table. Checks do not recharge syntax or latch errors; //! every failure must return immediately. Tokens themselves use the token bound. -//! This deliberately bounded staging policy is not calibrated public source8. +//! The public read checker selects both producers; editing remains separately staged. use super::{ budget::{Budget, Limits}, @@ -53,11 +53,15 @@ pub(super) struct ParsedMixedSyntax<'s> { struct FileSyntax<'s> { path: &'s str, source: &'s str, + // Retained original indexing metadata; the read result preserves its accounted ownership. + #[cfg_attr(not(test), allow(dead_code))] tokens: Vec, ordinary: SourceUnit, typed: Vec, positions: Vec<(Id, EditDeclaration)>, // A lexical owner is an occurrence index, not only a possibly repeated UUID. + // Retained original indexing metadata; the read result preserves its accounted ownership. + #[cfg_attr(not(test), allow(dead_code))] lexical_owners: Vec>, } @@ -256,7 +260,7 @@ impl State<'_> { return Err(Error { code: "invalid_source", offset: declaration.span.start, - message: "Private mixed syntax requires explicit IDs.".into(), + message: "Mixed source8 requires explicit UUIDs.".into(), }); } self.typed.push(declaration); @@ -276,17 +280,22 @@ pub(in crate::language) fn long_analysis_word(word: &str) -> bool { ) } +#[cfg(test)] pub(super) fn parse( bundle: &SourceBundle, limits: ProjectLimits, ) -> Result, Vec> { - let profile = Limits::source8(ParseLimits { + let profile = source_profile(limits); + parse_with_profile(bundle, limits, profile) +} + +fn source_profile(limits: ProjectLimits) -> Limits { + Limits::source8(ParseLimits { // Storage is not the caller's final model record/ordered-step accounting. max_source_bytes: limits.model.max_source_bytes, max_step_references: limits.model.max_step_references, ..ParseLimits::UNLIMITED - }); - parse_with_profile(bundle, limits, profile) + }) } fn located(path: &str, source: &str, error: Error) -> Vec { @@ -297,6 +306,7 @@ fn located(path: &str, source: &str, error: Error) -> Vec { }] } +#[cfg(test)] fn parse_with_profile( bundle: &SourceBundle, limits: ProjectLimits, 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 c57de87c..87582ecf 100644 --- a/crates/oa-core/src/language/analysis_source/mixed/carried.rs +++ b/crates/oa-core/src/language/analysis_source/mixed/carried.rs @@ -2,6 +2,7 @@ //! Checks existing logical phase totals; does not meter every parser allocation. use super::*; use project::accounting::{Ledger, Slots}; +#[cfg(test)] use project::ordinary_index::rename::emission::PendingRename; struct SnapshotPolicy { @@ -10,17 +11,20 @@ struct SnapshotPolicy { } /// The allocation-owning bundle guard. No production constructor exists yet. +#[cfg(test)] struct RetainedSnapshot { bundle: SourceBundle, policy: SnapshotPolicy, retained: Slots, } enum SnapshotBacking<'s> { + #[cfg(test)] Pending(&'s PendingRename<'s>), Borrowed { bundle: &'s SourceBundle, policy: SnapshotPolicy, }, + #[cfg(test)] Retained(&'s RetainedSnapshot), } @@ -29,8 +33,12 @@ pub(super) struct CarriedInput<'s> { backing: SnapshotBacking<'s>, } enum GuardBacking<'s> { + #[cfg(test)] Pending(&'s PendingRename<'s>), - Borrowed, + Borrowed { + _bundle: &'s SourceBundle, + }, + #[cfg(test)] Retained(&'s RetainedSnapshot), } @@ -40,6 +48,18 @@ pub(super) struct CarriedGuard<'s> { backing: GuardBacking<'s>, } impl<'s> CarriedInput<'s> { + fn fresh(bundle: &'s SourceBundle, limits: ProjectLimits) -> Self { + Self { + backing: SnapshotBacking::Borrowed { + bundle, + policy: SnapshotPolicy { + limits: source_profile(limits), + project_limits: limits, + }, + }, + } + } + #[cfg(test)] pub(super) fn from_pending<'t: 's>(pending: &'s PendingRename<'t>) -> Self { Self { backing: SnapshotBacking::Pending(pending), @@ -47,6 +67,7 @@ impl<'s> CarriedInput<'s> { } fn into_parts(self) -> (&'s SourceBundle, SnapshotPolicy, CarriedGuard<'s>) { match self.backing { + #[cfg(test)] SnapshotBacking::Pending(pending) => { let (limits, project_limits) = pending.carried_policy(); ( @@ -64,9 +85,10 @@ impl<'s> CarriedInput<'s> { bundle, policy, CarriedGuard { - backing: GuardBacking::Borrowed, + backing: GuardBacking::Borrowed { _bundle: bundle }, }, ), + #[cfg(test)] SnapshotBacking::Retained(owner) => ( &owner.bundle, SnapshotPolicy { @@ -83,8 +105,10 @@ impl<'s> CarriedInput<'s> { impl CarriedGuard<'_> { pub(super) fn slots(&self) -> Slots { match &self.backing { + #[cfg(test)] GuardBacking::Pending(pending) => pending.carried_slots(), - GuardBacking::Borrowed => Slots::default(), + GuardBacking::Borrowed { .. } => Slots::default(), + #[cfg(test)] GuardBacking::Retained(owner) => owner.retained, } } @@ -105,6 +129,7 @@ impl CarriedGuard<'_> { } /// Input already binds the actual bundle; callers cannot supply a replacement. +#[cfg(test)] fn parse_carried(input: CarriedInput<'_>) -> Result, Vec> { let (bundle, policy, guard) = input.into_parts(); parse_impl( @@ -205,3 +230,12 @@ mod text_producer; #[cfg(test)] mod entry_producer; + +pub(super) fn parse_fresh( + bundle: &SourceBundle, + limits: ProjectLimits, +) -> Result, Vec> { + parse_entry_producers(CarriedInput::fresh(bundle, limits)) +} +#[cfg(test)] +mod public_read; diff --git a/crates/oa-core/src/language/analysis_source/mixed/carried/public_read.rs b/crates/oa-core/src/language/analysis_source/mixed/carried/public_read.rs new file mode 100644 index 00000000..581e3435 --- /dev/null +++ b/crates/oa-core/src/language/analysis_source/mixed/carried/public_read.rs @@ -0,0 +1,65 @@ +//! Real public-constructor wiring; arbitrary fixture profiles are not public policy. +use super::*; + +#[test] +fn public_read_fresh_constructor_keeps_zero_carry_both_modes_and_exact_policy() { + let bundle = SourceBundle { + entry: "main.arcadia".into(), + files: [("main.arcadia".into(), "arcadia 8;\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\n\n}\n".into())].into(), + }; + let original = bundle.clone(); + for (source_bytes, references, expected_bytes, expected_tokens, expected_references) in [ + (4096, 17, 4096, 4096, 17), + (usize::MAX, usize::MAX, 64 * 1024 * 1024, 1_048_576, 500_000), + ] { + let limits = ProjectLimits { + max_files: 7, + max_include_depth: 9, + max_include_edges: 11, + model: ParseLimits { + max_source_bytes: source_bytes, + max_graph_objects: 3, + max_records: 1, + max_step_references: references, + }, + }; + // This is the same production helper the exported checker calls. + let syntax = parse_fresh(&bundle, limits).unwrap(); + let guard = syntax.carried.as_ref().expect("explicit carried mode"); + assert!(matches!(guard.backing, GuardBacking::Borrowed { .. })); + assert_eq!(guard.slots(), Slots::default()); + assert_eq!(syntax.budget.producer_text(), Some((8, 0))); + assert_eq!(syntax.budget.producer_entries(), Some(11)); + assert_eq!(syntax.metadata, 11); + let profile = syntax.budget.limits(); + assert_eq!(profile.bytes, expected_bytes); + assert_eq!(profile.tokens, expected_tokens); + assert_eq!(profile.entries, 262_144); + assert_eq!(profile.references, expected_references); + assert_eq!(profile.frames, 256); + assert_eq!(profile.numeric_units, 50_000_000); + assert_eq!(syntax.limits.max_files, 7); + assert_eq!(syntax.limits.max_include_depth, 9); + assert_eq!(syntax.limits.max_include_edges, 11); + assert_eq!(syntax.limits.model.max_source_bytes, source_bytes); + assert_eq!(syntax.limits.model.max_graph_objects, 3); + assert_eq!(syntax.limits.model.max_records, 1); + assert_eq!(syntax.limits.model.max_step_references, references); + let prior = parse(&bundle, limits).unwrap(); + assert_eq!(syntax.budget.usage, prior.budget.usage); + assert_eq!(syntax.budget.numeric_units(), prior.budget.numeric_units()); + let resolved = project::resolve(syntax).unwrap(); + let prior = project::resolve(prior).unwrap(); + let checked = project::check_mixed_bundle(&bundle, limits).unwrap(); + let mut expected = crate::Project::empty(); + expected.id = Id::from_u128(1); + expected.alias = "p".into(); + expected.name = "Project".into(); + assert_eq!(resolved.project(), &expected); + assert_eq!(prior.project(), &expected); + assert_eq!(checked.project(), &expected); + assert!(checked.warnings().is_empty()); + assert_eq!(resolved.warnings(), prior.warnings()); + } + assert_eq!(bundle, original); +} diff --git a/crates/oa-core/src/language/analysis_source/mixed/project.rs b/crates/oa-core/src/language/analysis_source/mixed/project.rs index ba23e829..20e7e5a5 100644 --- a/crates/oa-core/src/language/analysis_source/mixed/project.rs +++ b/crates/oa-core/src/language/analysis_source/mixed/project.rs @@ -1,5 +1,5 @@ -//! Private complete-Project admission from the actual complete syntax witness. -//! Consumed read-only typed index; no update authority, public source8 dispatch or replacement policy. +//! Complete-Project admission and opaque public read-only mixed source8 witness. +//! The separate typed index/edit chain remains test-only; no replacement policy. use super::super::{ Binding, Budget, Error as TypedError, Expression, ExpressionKind, Reference, Span, declarations as d, error, lower, @@ -22,17 +22,108 @@ pub(super) fn text_producer_inventory(syntax: &ParsedMixedSyntax<'_>) -> Result< accounting::syntax(syntax, &mut ledger)?; Ok(ledger.live) } +#[cfg(test)] pub(in crate::language::analysis_source) mod ordinary_index; +/// An admitted mixed source8 model borrowing its complete original source bundle. +/// +/// This non-Clone result grants immutable validation queries only. It is not an +/// editable source index, stabilized output, or permission to format/save a typed +/// model through the ordinary source APIs. All input UUIDs must be explicit. +/// +/// The source cannot be mutated while a later query uses the result: +/// +/// ```compile_fail +/// use oa_core::{language::check_mixed_bundle, source_project::{SourceBundle, ProjectLimits}}; +/// let mut bundle = SourceBundle { +/// entry: "main.arcadia".into(), +/// files: [("main.arcadia".into(), +/// "arcadia 8; project p \"P\" id \"00000000-0000-0000-0000-000000000001\" {}".into())].into(), +/// }; +/// let checked = check_mixed_bundle(&bundle, ProjectLimits::default()).unwrap(); +/// bundle.files.clear(); +/// assert_eq!(checked.project().alias, "p"); +/// ``` +/// +/// Nor can the result outlive that source: +/// +/// ```compile_fail +/// use oa_core::{language::check_mixed_bundle, source_project::{SourceBundle, ProjectLimits}}; +/// let checked = { +/// let bundle = SourceBundle { +/// entry: "main.arcadia".into(), +/// files: [("main.arcadia".into(), +/// "arcadia 8; project p \"P\" id \"00000000-0000-0000-0000-000000000001\" {}".into())].into(), +/// }; +/// check_mixed_bundle(&bundle, ProjectLimits::default()).unwrap() +/// }; +/// assert_eq!(checked.project().alias, "p"); +/// ``` +pub struct CheckedMixed<'s> { + resolved: ResolvedMixedProject<'s>, +} +impl CheckedMixed<'_> { + /// The complete admitted model, without mutation or ownership extraction. + pub fn project(&self) -> &Project { + self.resolved.project() + } + /// Actual located non-error validation diagnostics in validation order. + pub fn warnings(&self) -> &[SourceDiagnostic] { + self.resolved.warnings() + } +} + +/// Validate a complete mixed ordinary/typed source8 bundle without changing bytes. +/// +/// Every project, ordinary/typed declaration and typed parameter requires an +/// explicit UUID. Supplied files must be canonical and reachable from the entry. +/// Errors return no partial admitted model. There is no migration, ID generation, +/// filesystem discovery, formatting or editable-index construction. +/// +/// Caller policy is applied across the entire bundle with the existing bounded +/// source8 syntax/storage/numeric policy; this is not an all-heap memory bound. +/// Ordinary parse/compile/edit/save APIs and CLI discovery remain separate. +/// +/// ``` +/// use oa_core::{language::check_mixed_bundle, source_project::{SourceBundle, ProjectLimits}}; +/// let bundle = SourceBundle { +/// entry: "main.arcadia".into(), +/// files: [("main.arcadia".into(), +/// "arcadia 8; project p \"P\" id \"00000000-0000-0000-0000-000000000001\" {}".into())].into(), +/// }; +/// let checked = check_mixed_bundle(&bundle, ProjectLimits::default()).unwrap(); +/// assert_eq!(checked.project().alias, "p"); +/// assert!(checked.warnings().is_empty()); +/// ``` +pub fn check_mixed_bundle( + bundle: &crate::source_project::SourceBundle, + limits: crate::source_project::ProjectLimits, +) -> Result, Vec> { + let syntax = super::carried::parse_fresh(bundle, limits)?; + let resolved = resolve(syntax).map_err(|failure| failure.diagnostics)?; + Ok(CheckedMixed { resolved }) +} /// Read-only authority: every component originates in the consumed syntax. pub(in crate::language::analysis_source) struct ResolvedMixedProject<'s> { + // Retained ownership/admission evidence; consumed by the private test-only edit chain. + #[cfg_attr(not(test), allow(dead_code))] syntax: ParsedMixedSyntax<'s>, project: Project, warnings: Vec, + // Retained ownership/admission evidence; consumed by the private test-only edit chain. + #[cfg_attr(not(test), allow(dead_code))] static_limits: analysis::AdmissionLimits, + // Retained ownership/admission evidence; consumed by the private test-only edit chain. + #[cfg_attr(not(test), allow(dead_code))] static_usage: analysis::AdmissionUsage, + // Retained ownership/admission evidence; consumed by the private test-only edit chain. + #[cfg_attr(not(test), allow(dead_code))] expressions: Vec, + // Retained ownership/admission evidence; consumed by the private test-only edit chain. + #[cfg_attr(not(test), allow(dead_code))] occurrences: Vec, + // Retained ownership/admission evidence; consumed by the private test-only edit chain. + #[cfg_attr(not(test), allow(dead_code))] storage: Ledger, } impl ResolvedMixedProject<'_> { @@ -51,10 +142,12 @@ impl ResolvedMixedProject<'_> { /// Complete original bundle authority with a typed-only occurrence view. /// Ordinary globals are catalogued, but their authored occurrences are not indexed. /// This owner cannot be converted into the legacy typed-only edit authority. +#[cfg(test)] pub(in crate::language::analysis_source) struct IndexedMixedProject<'s> { resolved: ResolvedMixedProject<'s>, data: super::super::index::IndexData, } +#[cfg(test)] impl<'s> ResolvedMixedProject<'s> { pub fn index(mut self) -> Result, Failure> { let input = IndexInput { @@ -72,6 +165,7 @@ impl<'s> ResolvedMixedProject<'s> { }) } } +#[cfg(test)] impl IndexedMixedProject<'_> { pub fn project(&self) -> &Project { self.resolved.project() @@ -108,12 +202,14 @@ impl IndexedMixedProject<'_> { /// Constructor-only borrowed adapter. Its private file table prevents fabrication; /// no API returns it and no alternative source/token table can be installed. +#[cfg(test)] pub(in crate::language::analysis_source) struct IndexInput<'a> { files: &'a [FileSyntax<'a>], pub(in crate::language::analysis_source) project: &'a Project, pub(in crate::language::analysis_source) expressions: &'a [lower::ExpressionSite], pub(in crate::language::analysis_source) occurrences: &'a [lower::Occurrence], } +#[cfg(test)] impl IndexInput<'_> { pub(in crate::language::analysis_source) fn declarations( &self, @@ -140,6 +236,7 @@ pub(in crate::language::analysis_source) struct Failure { } impl Failure { /// Canonical attribution when justified; otherwise a safe explicit start fallback. + #[cfg(test)] fn index(syntax: &ParsedMixedSyntax<'_>, mut failure: lower::Failure) -> Self { failure.file = failure.file.filter(|&f| f < syntax.files.len()); if failure.file.is_none_or(|file| { diff --git a/docs/language.html b/docs/language.html index 4b7f5d7c..730659e4 100644 --- a/docs/language.html +++ b/docs/language.html @@ -633,6 +633,23 @@ a { text-decoration: none; }

.arcadia

+
+

Staged source8 Rust read API

+

The core library separately offers language::check_mixed_bundle(&SourceBundle, ProjectLimits) for complete ordinary/typed source8 validation. Its opaque +CheckedMixed result borrows the unchanged input and exposes only project() +and warnings(). Explicit UUIDs are required for the project, every ordinary +and typed declaration, and typed parameters; no IDs are generated. All supplied +canonical files must be reachable from the entry. Errors return no admitted +model; success preserves actual located warnings and the complete model.

+

This bounded API selects both retained text and entry producer checks and carries +the caller's whole-project policy through admission. It does not promise an +all-heap memory bound. It performs no filesystem discovery, migration, formatting, +source stabilization, indexing, saving or history installation. Existing ordinary +parse/compile/update APIs, CLI commands, desktop and server flows remain separate; +a read result does not grant their typed editing or save authority. Generic and +typed depth policies and platform qualification limits remain unchanged. This +staged capability does not replace the broader conformance obligations below.

+

1. Scope and conformance

This specification defines the shared Operational Analysis (OA) and System diff --git a/docs/language.rst b/docs/language.rst index f195aceb..be14df2e 100644 --- a/docs/language.rst +++ b/docs/language.rst @@ -6,6 +6,26 @@ Arcadia Studio OA / SA Language and Editing Contract :Core implementation: ``crates/oa-core`` :Text suffix: ``.arcadia`` +Staged source8 Rust read API +---------------------------------------- + +The core library separately offers ``language::check_mixed_bundle(&SourceBundle, +ProjectLimits)`` for complete ordinary/typed source8 validation. Its opaque +``CheckedMixed`` result borrows the unchanged input and exposes only ``project()`` +and ``warnings()``. Explicit UUIDs are required for the project, every ordinary +and typed declaration, and typed parameters; no IDs are generated. All supplied +canonical files must be reachable from the entry. Errors return no admitted +model; success preserves actual located warnings and the complete model. + +This bounded API selects both retained text and entry producer checks and carries +the caller's whole-project policy through admission. It does not promise an +all-heap memory bound. It performs no filesystem discovery, migration, formatting, +source stabilization, indexing, saving or history installation. Existing ordinary +parse/compile/update APIs, CLI commands, desktop and server flows remain separate; +a read result does not grant their typed editing or save authority. Generic and +typed depth policies and platform qualification limits remain unchanged. This +staged capability does not replace the broader conformance obligations below. + 1. Scope and conformance ------------------------ diff --git a/harness/analysis-public-read-implementation-20260925.md b/harness/analysis-public-read-implementation-20260925.md new file mode 100644 index 00000000..83fb4b16 --- /dev/null +++ b/harness/analysis-public-read-implementation-20260925.md @@ -0,0 +1,43 @@ +# Public mixed read implementation + +Base7b06b8ea, selected root contract bcddaba3. Public +language::check_mixed_bundle returns opaque non-Clone CheckedMixed borrowing the +complete original SourceBundle, with only immutable project/warnings accessors. +Its private resolved field retains syntax, expression/occurrence vectors, ledger +and admission evidence; there is no index, output, save or extraction authority. + +The shared parser mixed hooks now compile in production. Ordinary callers still +select None. The fresh constructor binds the actual bundle and exact ProjectLimits, +derives existing internal policy once, selects Some(Borrowed) with zero carry and +both producers, then consumes syntax through complete resolution/admission. +The borrowed guard retains the actual source reference even when test-only +Pending/Retained variants are absent. No duplicate live policy/model is added. +Public schema/UUID requirement wording replaces private-staging wording only; +all limiter phase/category/span transport remains inherited. + +Private legacy index/update/write, ordinary-index/rename/emission/acceptance and +fixture/pending constructors remain test-only. Shared parse/lower code remains +private. Necessary retained fields have individual documented non-test dead-code +allowances; no module-wide suppression, fake read, or new public lint-only API. +Exact production compiler lint findings remain a parent gate, not inferred clean +from source inspection. Existing298 tests are not edited. + +One internal public_read test invokes the actual production parse helper and +observes borrowedC0, both producer inventories, every inherited/derived policy +field (including262144 entries independent of model records1), full empty-Project +and warning parity, Usage/numeric parity and unchanged input. Three rustdoc blocks +provide a compiling real usage example plus source-drop and source-mutation +compile-fail cases with later result use. Independent worker owns the non-test +mixed_source8_read integration target and its separate complete-model oracles. + +Established docs/language.rst gains a bounded staged-read note without changing +its broader conformance/version obligations. Existing docutils HTML5 generation +with halt/report level2 regenerated language.html successfully. An initial local +RST generation refused the new section formatting; the section separator and +blank line were corrected before successful regeneration. No Rust compiler, +Cargo test, CLI, debugger or publication was run by this worker. + +Parent owns pinned compilation, all old/new runtime tests, doctests, strict +Clippy, formatting disposition and production Parser stack regression checks. +DEPTH_128_VALIDATED=false, existing depth policies, logical-accounting exclusions, +full-pipeline limitation and deferred Linux qualification remain unchanged. \ No newline at end of file