Compare commits

..
Author SHA1 Message Date
sorokin.oleg.edu fad1f5f33b Sync agent instructions from main 2026-09-13 11:38:37 +03:00
sorokin.oleg.edu 9d7e311320 Sync agent instructions from main 2026-09-12 13:43:14 +03:00
sorokin.oleg.edu 9cc8194d7b Sync agent instructions from main 2026-09-12 13:39:57 +03:00
sorokin.oleg.edu 3ee5aa6809 Sync agent instructions from main 2026-09-12 13:15:27 +03:00
sorokin.oleg.edu ca19ad277a Merge commit '113fb8b7a98c6a63bf67e77ebe5974555d033c62' into codex/behavior-canvas-20260911 2026-09-11 11:14:23 -07:00
sorokin.oleg.edu fb1da912c9 Implement reviewed shared behavior canvas controls and focus 2026-09-11 10:34:29 -07:00
sorokin.oleg.edu 113fb8b7a9 Sync agent instructions from main 2026-09-11 19:51:38 +03:00
Codex e511c4153f WIP checkpoint shared behavior canvas before user reboot 2026-09-11 06:44:13 -07:00
sorokin.oleg.edu c0e66f94e3 Stage explicit prerequisite sources inside behavior creation forms 2026-09-11 06:26:16 -07:00
Codex 7f081ad457 Integrate scoped reference chooser and scenario form focus and reveal 2026-09-11 06:24:20 -07:00
Codex c7ed894900 Merge shared instruction checkpoint preserving exact authoritative goal 2026-09-11 06:14:11 -07:00
Codex 98427b9620 Adopt shared Gitea and harness maintenance instructions 2026-09-11 06:14:05 -07:00
sorokin.oleg.edu cab01c3d7f Sync agent instructions from main 2026-09-11 15:55:34 +03:00
Codex 7d14bd0dd1 Add exact analysis numbers units and bounded request work 2026-09-11 05:50:45 -07:00
Codex dc0f2e12f3 Import corrected creation oracle and record exact SA action history 2026-09-11 05:41:06 -07:00
sorokin.oleg.edu bc311bc076 Add source-first CLI with guarded writes and reviewed evidence 2026-09-11 05:27:16 -07:00
Codex a34558d3be Record OA native history and ratified analysis library decisions 2026-09-11 05:21:37 -07:00
Codex 2cc4d115d0 Specify baseline analysis and verified library contract compatibility 2026-09-11 04:48:44 -07:00
sorokin.oleg.edu 1516949567 Add named behavior creation and verified textual authoring examples 2026-09-11 04:26:56 -07:00
Codex c4e1580bb0 Exercise bounded reference chooser with mounted input tests 2026-09-11 02:29:15 -07:00
1751 changed files with 15984 additions and 148557 deletions
+16 -48
View File
@@ -22,65 +22,33 @@ harness/history/2026-09-06-ledgers-before-consolidation/** -text
harness/history/2026-09-10-ledgers-before-continuation-refresh/** -text
harness/history/2026-09-10-ledgers-before-second-consolidation/** -text whitespace=blank-at-eol,blank-at-eof,space-before-tab,cr-at-eol
harness/experiments/textual-authoring-20260911-local/** -text whitespace=blank-at-eol,blank-at-eof,space-before-tab,cr-at-eol
harness/history/2026-09-11-ytr83r-integration/** -text whitespace=cr-at-eol
crates/oa-desktop/src/recovery/fixtures/*.json -text
crates/oa-core/src/analysis_numeric/fixtures/** -text whitespace=cr-at-eol
harness/fixtures/source7-native/** -text
harness/fixtures/basic-editing/** -text
harness/fixtures/scenario-authoring/base/** -text whitespace=cr-at-eol
harness/experiments/textual-authoring-20260911/** -text whitespace=cr-at-eol
harness/experiments/textual-authoring-20260911/observed/split-entry.arcadia whitespace=-blank-at-eol,cr-at-eol
harness/experiments/analysis-context-20260911/** -text whitespace=cr-at-eol
harness/experiments/constructor-defaults-20260911/** -text whitespace=blank-at-eol,blank-at-eof,space-before-tab,cr-at-eol
harness/experiments/constructor-defaults-20260911/results/*.arcadia whitespace=-blank-at-eol,cr-at-eol
harness/experiments/behavior-creation-native-20260911/** -text whitespace=blank-at-eol,blank-at-eof,space-before-tab,cr-at-eol
harness/experiments/analysis-baseline-20260911/** -text whitespace=cr-at-eol
harness/experiments/library-compatibility-20260911/** -text whitespace=cr-at-eol
harness/experiments/refined-contracts-review-20260911/** -text whitespace=cr-at-eol
harness/behavior-creation-contract.rs -text whitespace=cr-at-eol
harness/experiments/source-cli-independent-20260911/** -text whitespace=cr-at-eol
harness/fixtures/scenario-lifecycle-native/**/*.arcadia -text whitespace=cr-at-eol
harness/experiments/lifecycle-native-ytr83r-20260911/** -text whitespace=cr-at-eol
harness/experiments/lifecycle-native-ytr83r-20260911/*.log -text -whitespace
harness/experiments/lifecycle-native-ui-oa-20260911/** -text -whitespace
harness/experiments/lifecycle-native-ui-sa-20260911/** -text -whitespace
harness/experiments/wsl-postboot-ytr83r-20260911/** -text -whitespace
harness/experiments/authoring-prerelease-20260911/** -text whitespace=cr-at-eol
harness/experiments/cli-packaging-20260911/** -text whitespace=cr-at-eol
harness/experiments/cli-prerelease-20260911/** -text whitespace=cr-at-eol
harness/experiments/contextual-source-20260911/** -text whitespace=cr-at-eol
harness/experiments/contextual-source-20260911/**/*.log whitespace=-blank-at-eof,cr-at-eol
harness/fixtures/staged-source-authoring/** -text whitespace=cr-at-eol
harness/staged-source-authoring-contract.rs -text whitespace=cr-at-eol
# Preserve checksum-bound font notices verbatim through Git round trips.
scripts/windows-notices/GNOME-Cantarell-61362600e104-COPYING -text
scripts/windows-notices/Fira-OFL.txt -text
scripts/windows-notices/FiraSans-Regular-NOTICE.txt -text
scripts/windows-notices/llvm-6dfe1677ab8d-libunwind-LICENSE.TXT -text
harness/experiments/lifecycle-header-overflow-before-20260911/** -text -whitespace
harness/history/2026-09-11-ytr83r-record-migration/STATE-before.md -text -whitespace
harness/fixtures/staged-source-authoring/** -text whitespace=cr-at-eol
harness/staged-source-authoring-contract.rs -text whitespace=cr-at-eol
harness/experiments/contextual-lifecycle-integration-ytr83r-20260911/** -text -whitespace
harness/experiments/windows-runtime-321897f-20260911/** -text -whitespace
harness/child-work-native-contract.rs -text whitespace=cr-at-eol
harness/fixtures/child-work-native/** -text whitespace=cr-at-eol
harness/experiments/windows-gates-865483b-20260911/** -text -whitespace
harness/experiments/native-child-oa-865483b-20260911/** -text -whitespace
harness/experiments/packaged-server-865483b-20260911/** -text -whitespace
harness/experiments/canvas-union-a57c334-20260911/** -text -whitespace
harness/experiments/windows-gates-a57c334-20260911/** -text -whitespace
harness/experiments/native-child-continuation-865483b-20260911/** -text -whitespace
harness/experiments/lifecycle-header-correction-native-865483b-20260911/** -text -whitespace
harness/release-native-child-continuation-review-20260911.md -text -whitespace
harness/experiments/packaged-gui-close-865483b-20260911/** -text -whitespace
harness/release-grapheme-shortening-review-20260911.md -text -whitespace
harness/experiments/staged-source-native-a57c334-20260911/** -text -whitespace
harness/experiments/grapheme-windows-cb91525-20260911/** -text -whitespace
harness/experiments/canvas-native-close-a57c334-20260911/** -text -whitespace
harness/release-staged-native-a57-independent-review-20260911.md -text -whitespace
harness/release-saved-destination-independent-review-20260912.md -text -whitespace
harness/experiments/repair-union-windows-33e30c1-20260912/** -text -whitespace
harness/experiments/canvas-camera-a57c334-20260912/** -text -whitespace
harness/release-camera-a57-independent-review-20260912.md -text -whitespace
harness/experiments/canvas-repair-package-33e30c1-20260912/** -text -whitespace
harness/experiments/canvas-repair-native-33e30c1-20260912/** -text -whitespace
harness/experiments/canvas-repair-server-33e30c1-20260912/** -text -whitespace
harness/experiments/canvas-repair-publication-20260912/** -text -whitespace
harness/experiments/behavior-creation-native-20260911/** -text whitespace=cr-at-eol
harness/experiments/constructor-defaults-20260911/** -text whitespace=cr-at-eol
harness/experiments/constructor-defaults-20260911/results/*.arcadia whitespace=-blank-at-eol,cr-at-eol
harness/release-scenario-native-9e8r6t-20260911.md -text whitespace=cr-at-eol
harness/experiments/analysis-numeric-gate-20260911/** -text
Generated
+22 -1
View File
@@ -2107,6 +2107,25 @@ dependencies = [
"jni-sys 0.3.1",
]
[[package]]
name = "num-bigint"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
dependencies = [
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -2153,6 +2172,9 @@ name = "oa-core"
version = "0.2.0"
dependencies = [
"libc",
"num-bigint",
"num-integer",
"num-traits",
"same-file",
"serde",
"serde_json",
@@ -2176,7 +2198,6 @@ dependencies = [
"serde_json",
"tempfile",
"tokio",
"unicode-segmentation",
]
[[package]]
+3
View File
@@ -12,6 +12,9 @@ uuid.workspace = true
thiserror.workspace = true
tempfile = "3"
same-file = "1"
num-bigint = "0.5.1"
num-integer = "0.1.47"
num-traits = "0.2.19"
[target.'cfg(target_os = "linux")'.dependencies]
libc = "0.2"
+103
View File
@@ -0,0 +1,103 @@
//! Exact numeric foundation for analysis; no model, expression, or package resolver.
//!
//! Values use reduced rational coordinates. All nontrivial arithmetic takes one
//! request-local [WorkMeter]. The future evaluator must also charge *expanded*
//! expression/call/property/list/target visits and retain the same logical walk on
//! cache hits. Stored AST size is not a dynamic execution limit.
//!
//! Approximate engineering functions/irrational unit scales require a subsequent
//! explicit numeric profile; rationals do not implement degree/radian conversion
//! or transcendental mathematics.
mod dimension;
mod exact;
mod meter;
mod quantity;
pub use dimension::{Dimension, DimensionAxis};
pub use exact::{Exact, ExactWire};
pub use meter::{
EvaluationStep, NumericOperation, WorkExhaustion, WorkLimits, WorkMeter, WorkResource,
WorkSite, WorkUsage,
};
pub use quantity::{NumericType, NumericValue, ResolvedKind, Unit};
use serde::{Deserialize, Serialize};
use thiserror::Error;
/// Profile identity includes the accounting rules, not crate-internal timing.
pub const WORK_PROFILE: &str = "Q1-logical-work-1";
pub const MAX_COMPONENT_BITS: u64 = 4096;
/// Cross products require 8192 bits; their sum can require one additional bit.
pub const MAX_INTERMEDIATE_BITS: u64 = 8193;
pub const MAX_COMPONENT_DIGITS: usize = 1234;
pub const MAX_DECIMAL_EXPONENT: i32 = 128;
pub const MAX_INTEGER_POWER: i8 = 16;
/// Includes ratio punctuation/whitespace without permitting unbounded scanning.
pub const MAX_LITERAL_BYTES: usize = 2 * MAX_COMPONENT_DIGITS + 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum NumberComponent {
Numerator,
Denominator,
}
#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)]
pub enum NumericLimit {
#[error("request work exhausted: {0}")]
Work(WorkExhaustion),
#[error("{component:?} uses {bits} bits, above the {limit}-bit {stage} limit")]
Bits {
component: NumberComponent,
bits: u64,
limit: u64,
stage: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)]
pub enum NumericError {
#[error("numeric limit: {0}")]
Limit(NumericLimit),
#[error("invalid numeric literal")]
InvalidLiteral,
#[error("numeric literal exceeds its digit or byte limit")]
LiteralTooLong,
#[error("decimal exponent is outside -128 through 128")]
DecimalExponent,
#[error("wire rational is not in canonical reduced form")]
NonCanonicalWire,
#[error("rational denominator is zero")]
ZeroDenominator,
#[error("division by zero")]
DivisionByZero,
#[error("zero to the power zero is undefined in this profile")]
ZeroPowerZero,
#[error("integer power is outside -16 through 16")]
IntegerPower,
#[error("dimension exponent at {axis:?} is outside -32 through 32: {value}")]
DimensionExponent { axis: DimensionAxis, value: i32 },
#[error("unit scale must be strictly positive")]
UnitScale,
#[error("a linear unit must have zero offset")]
LinearOffset,
#[error("an affine point must have a distinct linear difference kind")]
DifferenceKind,
#[error("numeric kinds or dimensions are incompatible")]
IncompatibleTypes,
#[error("operation is not defined for affine points")]
AffineOperation,
#[error("AsKind requires an anonymous matching dimension and a linear target")]
KindAssertion,
#[error("request work limits exceed the provisional reference ceilings")]
InvalidWorkLimits,
}
impl NumericError {
pub fn is_terminal_work_exhaustion(&self) -> bool {
matches!(self, Self::Limit(NumericLimit::Work(_)))
}
}
#[cfg(test)]
mod tests;
@@ -0,0 +1,107 @@
use super::NumericError;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DimensionAxis {
Length,
Mass,
Time,
ElectricCurrent,
Temperature,
Amount,
LuminousIntensity,
}
impl DimensionAxis {
pub const ALL: [Self; 7] = [
Self::Length,
Self::Mass,
Self::Time,
Self::ElectricCurrent,
Self::Temperature,
Self::Amount,
Self::LuminousIntensity,
];
}
/// SI base-dimension vector in DimensionAxis::ALL order, each entry -32..=32.
/// This is a dimension, never a nominal quantity-kind compatibility proof.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "[i16; 7]", into = "[i16; 7]")]
pub struct Dimension([i16; 7]);
impl Dimension {
pub const ZERO: Self = Self([0; 7]);
pub fn new(exponents: [i16; 7]) -> Result<Self, NumericError> {
for (index, value) in exponents.iter().copied().enumerate() {
if !(-32..=32).contains(&value) {
return Err(NumericError::DimensionExponent {
axis: DimensionAxis::ALL[index],
value: i32::from(value),
});
}
}
Ok(Self(exponents))
}
pub fn exponents(self) -> [i16; 7] {
self.0
}
pub fn is_dimensionless(self) -> bool {
self == Self::ZERO
}
pub fn checked_product(self, rhs: Self) -> Result<Self, NumericError> {
self.combine(rhs, 1)
}
pub fn checked_quotient(self, rhs: Self) -> Result<Self, NumericError> {
self.combine(rhs, -1)
}
pub fn checked_power(self, exponent: i8) -> Result<Self, NumericError> {
if !(-super::MAX_INTEGER_POWER..=super::MAX_INTEGER_POWER).contains(&exponent) {
return Err(NumericError::IntegerPower);
}
let mut output = [0; 7];
for (index, value) in self.0.iter().enumerate() {
output[index] = Self::checked_exponent(index, i32::from(*value) * i32::from(exponent))?;
}
Ok(Self(output))
}
fn combine(self, rhs: Self, sign: i32) -> Result<Self, NumericError> {
let mut output = [0; 7];
for (index, value) in self.0.iter().enumerate() {
output[index] =
Self::checked_exponent(index, i32::from(*value) + sign * i32::from(rhs.0[index]))?;
}
Ok(Self(output))
}
fn checked_exponent(index: usize, value: i32) -> Result<i16, NumericError> {
if !(-32..=32).contains(&value) {
return Err(NumericError::DimensionExponent {
axis: DimensionAxis::ALL[index],
value,
});
}
Ok(value as i16)
}
}
impl TryFrom<[i16; 7]> for Dimension {
type Error = NumericError;
fn try_from(value: [i16; 7]) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl From<Dimension> for [i16; 7] {
fn from(value: Dimension) -> Self {
value.0
}
}
@@ -0,0 +1,469 @@
use super::meter::{normalization_cost, parse_cost, product_cost, sum_cost, words};
use super::{
MAX_COMPONENT_BITS, MAX_COMPONENT_DIGITS, MAX_DECIMAL_EXPONENT, MAX_INTEGER_POWER,
MAX_INTERMEDIATE_BITS, MAX_LITERAL_BYTES, NumberComponent, NumericError, NumericLimit,
NumericOperation, WorkLimits, WorkMeter,
};
use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{One, Signed, Zero};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
/// The interchange representation deliberately does not use num-bigint's serde
/// format or JSON numeric values. Duplicate/missing/unknown fields reject.
/// Its derived decoder allocates String fields before numeric validation; a
/// transport/document adapter must bound the encoded envelope and decoded text.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExactWire {
pub numerator: String,
pub denominator: String,
}
/// Reduced rational with positive denominator and <=4096-bit components.
///
/// There are no unmetered arithmetic/ordering traits. Eq is structural equality
/// of already normalized bounded values, not an expression comparison API.
/// Standalone serde admission bounds the subsequent BigInt validation, not the
/// preceding String decoding. Document adapters must bound encoded/decoded input
/// and aggregate admission work. Evaluation uses one explicit WorkMeter.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "ExactWire", into = "ExactWire")]
pub struct Exact {
numerator: BigInt,
denominator: BigInt,
}
impl Exact {
pub fn from_integer(value: i64) -> Self {
Self {
numerator: BigInt::from(value),
denominator: BigInt::one(),
}
}
pub fn is_zero(&self) -> bool {
self.numerator.is_zero()
}
pub fn is_positive(&self) -> bool {
self.numerator.is_positive()
}
pub fn component_bits(&self) -> (u64, u64) {
(self.numerator.bits(), self.denominator.bits())
}
pub fn to_wire(&self) -> ExactWire {
ExactWire {
numerator: self.numerator.to_string(),
denominator: self.denominator.to_string(),
}
}
/// Canonical *literal* spelling, not a generic expression formatter.
/// Negative literal nodes use ratio(-n,d), even when d=1. A leading minus
/// belongs to the future expression parser's UnaryNegate node instead.
pub fn canonical_literal(&self) -> String {
if self.denominator.is_one() && !self.numerator.is_negative() {
self.numerator.to_string()
} else {
format!("ratio({}, {})", self.numerator, self.denominator)
}
}
pub fn from_wire(wire: &ExactWire, meter: &mut WorkMeter) -> Result<Self, NumericError> {
meter.ensure_active()?;
let n_digits = wire_digits(&wire.numerator, true)?;
let d_digits = wire_digits(&wire.denominator, false)?;
let n_bound = decimal_bits(n_digits.len());
let d_bound = decimal_bits(d_digits.len());
meter.numeric(
NumericOperation::ParseWire,
parse_cost(n_digits.len(), n_bound)
+ parse_cost(d_digits.len(), d_bound)
+ normalization_cost(n_bound, d_bound),
)?;
let numerator = parse_integer(&wire.numerator)?;
let denominator = parse_integer(&wire.denominator)?;
Self::check_bits(&numerator, &denominator, MAX_COMPONENT_BITS, "wire")?;
if denominator.is_zero() {
return Err(NumericError::ZeroDenominator);
}
if !numerator.gcd(&denominator).is_one() {
return Err(NumericError::NonCanonicalWire);
}
Ok(Self {
numerator,
denominator,
})
}
/// Reads one unsigned decimal/integer atom or a ratio(n,d) literal only.
/// It does not parse expressions, unary operators, units, calls or names.
/// Source ratios/decimals may reduce; wire values must already be canonical.
pub fn parse_literal(source: &str, meter: &mut WorkMeter) -> Result<Self, NumericError> {
meter.ensure_active()?;
if source.len() > MAX_LITERAL_BYTES {
return Err(NumericError::LiteralTooLong);
}
let source = trim_ascii(source);
if let Some(body) = source.strip_prefix("ratio(") {
let body = body.strip_suffix(')').ok_or(NumericError::InvalidLiteral)?;
let (n, d) = body.split_once(',').ok_or(NumericError::InvalidLiteral)?;
return Self::parse_ratio(trim_ascii(n), trim_ascii(d), meter);
}
Self::parse_decimal(source, meter)
}
fn parse_ratio(n: &str, d: &str, meter: &mut WorkMeter) -> Result<Self, NumericError> {
let n_digits = source_integer_digits(n, true)?;
let d_digits = source_integer_digits(d, false)?;
let n_bound = decimal_bits(n_digits.len());
let d_bound = decimal_bits(d_digits.len());
Self::check_estimates(n_bound, d_bound)?;
meter.numeric(
NumericOperation::ParseLiteral,
parse_cost(n_digits.len(), n_bound)
+ parse_cost(d_digits.len(), d_bound)
+ normalization_cost(n_bound, d_bound),
)?;
Self::normalize_precharged(parse_integer(n)?, parse_integer(d)?)
}
fn parse_decimal(source: &str, meter: &mut WorkMeter) -> Result<Self, NumericError> {
let mut parts = source.split(['e', 'E']);
let mantissa = parts.next().ok_or(NumericError::InvalidLiteral)?;
let exponent = match parts.next() {
Some(text) => parse_exponent(text)?,
None => 0,
};
if parts.next().is_some() {
return Err(NumericError::InvalidLiteral);
}
let (whole, fraction) = match mantissa.split_once('.') {
Some((whole, fraction)) if !fraction.is_empty() => (whole, fraction),
Some(_) => return Err(NumericError::InvalidLiteral),
None => (mantissa, ""),
};
source_integer_digits(whole, false)?;
if !fraction.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(NumericError::InvalidLiteral);
}
let digit_count = whole.len() + fraction.len();
if digit_count > MAX_COMPONENT_DIGITS {
return Err(NumericError::LiteralTooLong);
}
let adjustment = exponent - fraction.len() as i32;
let power = adjustment.unsigned_abs();
let mantissa_bound = decimal_bits(digit_count);
let scale_bound = decimal_bits(power as usize + 1);
let (n_bound, d_bound) = if adjustment >= 0 {
(mantissa_bound + scale_bound, 1)
} else {
(mantissa_bound, scale_bound)
};
Self::check_estimates(n_bound, d_bound)?;
// Fixed logical allowance for constructing 10^power; independent of
// num-bigint's selected multiplication/exponentiation implementation.
let power_steps = 1 + 2 * u64::from(u32::BITS - power.leading_zeros());
meter.numeric(
NumericOperation::ParseLiteral,
parse_cost(digit_count, mantissa_bound)
+ power_steps * product_cost(scale_bound, scale_bound)
+ product_cost(mantissa_bound, scale_bound)
+ normalization_cost(n_bound, d_bound),
)?;
let digits = format!("{whole}{fraction}");
let mut numerator = parse_integer(&digits)?;
let scale = BigInt::from(10_u8).pow(power);
let denominator = if adjustment >= 0 {
numerator *= scale;
BigInt::one()
} else {
scale
};
Self::normalize_precharged(numerator, denominator)
}
pub fn add(&self, rhs: &Self, meter: &mut WorkMeter) -> Result<Self, NumericError> {
self.add_or_subtract(rhs, false, meter)
}
pub fn subtract(&self, rhs: &Self, meter: &mut WorkMeter) -> Result<Self, NumericError> {
self.add_or_subtract(rhs, true, meter)
}
fn add_or_subtract(
&self,
rhs: &Self,
subtract: bool,
meter: &mut WorkMeter,
) -> Result<Self, NumericError> {
let (a, b) = self.component_bits();
let (c, d) = rhs.component_bits();
let left_bound = a + d;
let right_bound = c + b;
let n_bound = left_bound.max(right_bound) + 1;
let d_bound = b + d;
Self::check_estimates(n_bound, d_bound)?;
meter.numeric(
if subtract {
NumericOperation::Subtract
} else {
NumericOperation::Add
},
product_cost(a, d)
+ product_cost(c, b)
+ sum_cost(left_bound, right_bound)
+ product_cost(b, d)
+ normalization_cost(n_bound, d_bound),
)?;
let left = &self.numerator * &rhs.denominator;
let right = &rhs.numerator * &self.denominator;
let numerator = if subtract { left - right } else { left + right };
let denominator = &self.denominator * &rhs.denominator;
Self::normalize_precharged(numerator, denominator)
}
pub fn multiply(&self, rhs: &Self, meter: &mut WorkMeter) -> Result<Self, NumericError> {
let (a, b) = self.component_bits();
let (c, d) = rhs.component_bits();
Self::check_estimates(a + c, b + d)?;
meter.numeric(
NumericOperation::Multiply,
product_cost(a, c) + product_cost(b, d) + normalization_cost(a + c, b + d),
)?;
Self::normalize_precharged(
&self.numerator * &rhs.numerator,
&self.denominator * &rhs.denominator,
)
}
pub fn divide(&self, rhs: &Self, meter: &mut WorkMeter) -> Result<Self, NumericError> {
let (a, b) = self.component_bits();
let (c, d) = rhs.component_bits();
Self::check_estimates(a + d, b + c)?;
meter.numeric(
NumericOperation::Divide,
product_cost(a, d) + product_cost(b, c) + normalization_cost(a + d, b + c),
)?;
if rhs.is_zero() {
return Err(NumericError::DivisionByZero);
}
Self::normalize_precharged(
&self.numerator * &rhs.denominator,
&self.denominator * &rhs.numerator,
)
}
pub fn compare(&self, rhs: &Self, meter: &mut WorkMeter) -> Result<Ordering, NumericError> {
let (a, b) = self.component_bits();
let (c, d) = rhs.component_bits();
Self::check_estimates((a + d).max(c + b), 1)?;
meter.numeric(
NumericOperation::Compare,
product_cost(a, d) + product_cost(c, b) + sum_cost(a + d, c + b),
)?;
Ok((&self.numerator * &rhs.denominator).cmp(&(&rhs.numerator * &self.denominator)))
}
pub fn negated(&self, meter: &mut WorkMeter) -> Result<Self, NumericError> {
meter.numeric(NumericOperation::Negate, self.copy_cost())?;
Ok(Self {
numerator: -&self.numerator,
denominator: self.denominator.clone(),
})
}
pub fn absolute(&self, meter: &mut WorkMeter) -> Result<Self, NumericError> {
meter.numeric(NumericOperation::Absolute, self.copy_cost())?;
Ok(Self {
numerator: self.numerator.abs(),
denominator: self.denominator.clone(),
})
}
pub fn reciprocal(&self, meter: &mut WorkMeter) -> Result<Self, NumericError> {
meter.numeric(NumericOperation::Reciprocal, self.copy_cost())?;
if self.is_zero() {
return Err(NumericError::DivisionByZero);
}
Ok(Self {
numerator: if self.numerator.is_negative() {
-&self.denominator
} else {
self.denominator.clone()
},
denominator: self.numerator.abs(),
})
}
/// Fixed left-to-right repeated multiplication, at most 16 multiplications.
/// Each reduced stage obeys the same component bound; no large power is
/// allocated and subsequently discarded after discovering it is oversized.
pub fn power(&self, exponent: i8, meter: &mut WorkMeter) -> Result<Self, NumericError> {
meter.numeric(NumericOperation::Power, self.copy_cost())?;
if !(-MAX_INTEGER_POWER..=MAX_INTEGER_POWER).contains(&exponent) {
return Err(NumericError::IntegerPower);
}
if self.is_zero() && exponent == 0 {
return Err(NumericError::ZeroPowerZero);
}
let factor = if exponent < 0 {
self.reciprocal(meter)?
} else {
self.clone()
};
let mut value = Self::from_integer(1);
for _ in 0..exponent.unsigned_abs() {
value = value.multiply(&factor, meter)?;
}
Ok(value)
}
pub(super) fn copy_cost(&self) -> u64 {
let (n, d) = self.component_bits();
1 + words(n) + words(d)
}
fn normalize_precharged(
mut numerator: BigInt,
mut denominator: BigInt,
) -> Result<Self, NumericError> {
Self::check_bits(
&numerator,
&denominator,
MAX_INTERMEDIATE_BITS,
"intermediate",
)?;
if denominator.is_zero() {
return Err(NumericError::ZeroDenominator);
}
if denominator.is_negative() {
numerator = -numerator;
denominator = -denominator;
}
let gcd = numerator.gcd(&denominator);
numerator /= &gcd;
denominator /= gcd;
Self::check_bits(&numerator, &denominator, MAX_COMPONENT_BITS, "reduced")?;
Ok(Self {
numerator,
denominator,
})
}
fn check_estimates(n: u64, d: u64) -> Result<(), NumericError> {
Self::check_bit_count(
n,
NumberComponent::Numerator,
MAX_INTERMEDIATE_BITS,
"predicted",
)?;
Self::check_bit_count(
d,
NumberComponent::Denominator,
MAX_INTERMEDIATE_BITS,
"predicted",
)
}
fn check_bits(n: &BigInt, d: &BigInt, limit: u64, stage: &str) -> Result<(), NumericError> {
Self::check_bit_count(n.bits(), NumberComponent::Numerator, limit, stage)?;
Self::check_bit_count(d.bits(), NumberComponent::Denominator, limit, stage)
}
fn check_bit_count(
bits: u64,
component: NumberComponent,
limit: u64,
stage: &str,
) -> Result<(), NumericError> {
if bits > limit {
return Err(NumericError::Limit(NumericLimit::Bits {
component,
bits,
limit,
stage: stage.to_owned(),
}));
}
Ok(())
}
}
impl TryFrom<ExactWire> for Exact {
type Error = NumericError;
fn try_from(value: ExactWire) -> Result<Self, Self::Error> {
// BigInt validation is bounded after ExactWire strings have decoded.
// The adapter owns envelope/text/aggregate admission bounds and must not
// reset evaluation meters.
Self::from_wire(&value, &mut WorkMeter::new(WorkLimits::REFERENCE))
}
}
impl From<Exact> for ExactWire {
fn from(value: Exact) -> Self {
value.to_wire()
}
}
fn trim_ascii(text: &str) -> &str {
text.trim_matches(|ch: char| ch.is_ascii_whitespace())
}
fn source_integer_digits(text: &str, signed: bool) -> Result<&str, NumericError> {
let digits = if signed {
text.strip_prefix('-').unwrap_or(text)
} else {
text
};
if digits.len() > MAX_COMPONENT_DIGITS {
return Err(NumericError::LiteralTooLong);
}
if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(NumericError::InvalidLiteral);
}
Ok(digits)
}
fn wire_digits(text: &str, signed: bool) -> Result<&str, NumericError> {
let digits = source_integer_digits(text, signed).map_err(|error| match error {
NumericError::LiteralTooLong => error,
_ => NumericError::NonCanonicalWire,
})?;
if (digits.len() > 1 && digits.starts_with('0')) || (text.starts_with('-') && digits == "0") {
return Err(NumericError::NonCanonicalWire);
}
Ok(digits)
}
fn parse_integer(text: &str) -> Result<BigInt, NumericError> {
BigInt::parse_bytes(text.as_bytes(), 10).ok_or(NumericError::InvalidLiteral)
}
fn parse_exponent(text: &str) -> Result<i32, NumericError> {
let (negative, digits) = if let Some(digits) = text.strip_prefix('-') {
(true, digits)
} else {
(false, text.strip_prefix('+').unwrap_or(text))
};
if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(NumericError::InvalidLiteral);
}
let mut value = 0_i32;
for digit in digits.bytes() {
value = value * 10 + i32::from(digit - b'0');
if value > MAX_DECIMAL_EXPONENT {
return Err(NumericError::DecimalExponent);
}
}
Ok(if negative { -value } else { value })
}
fn decimal_bits(digits: usize) -> u64 {
// 3.322 exceeds log2(10); dimensions are bounded before this conversion.
(digits as u64 * 3322).div_ceil(1000).max(1)
}
@@ -0,0 +1,32 @@
[
{
"canonical_source": "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"expression": {
"Number": {
"denominator": "1",
"numerator": "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}
},
"name": "ten_to_300"
},
{
"canonical_source": "ratio(1, 522194440706576253345876355358312191289982124523691890192116741641976953985778728424413405967498779170445053357219631418993786719092896803631618043925682638972978488271854999170180795067191859157214035005927973113188159419698856372836167342172293308748403954352901852035642024370059304557233988891799014503343469488440893892973452815095130470299789726716411734651513348221529512507986199933857107770846917779942645743159118957217248367043905936319748237550094520674504208530837546834166925275516486044134775384991808184705966507606898412918594045916828375610659246423184062775112999150206172392431297837246097308511903252956622805412865917690043804311051417135098849101156584508839003337597742539960818209685142687562392007453579567729991395256699805775897135553415567045292136442139895777424891477161767258532611634530697452993846501061481697843891439474220308003706472837459911525285821188577408160690315522951458068463354171428220365223949985950890732881736611925133626529949897998045399734600887312408859224933727829625089164535236559716582775403784110923285873186648442456409760158728501220463308455437074192539205964902261490928669488824051563042951500651206733594863336608245755565801460390869016718045121902354170201577095168)",
"expression": {
"Number": {
"denominator": "522194440706576253345876355358312191289982124523691890192116741641976953985778728424413405967498779170445053357219631418993786719092896803631618043925682638972978488271854999170180795067191859157214035005927973113188159419698856372836167342172293308748403954352901852035642024370059304557233988891799014503343469488440893892973452815095130470299789726716411734651513348221529512507986199933857107770846917779942645743159118957217248367043905936319748237550094520674504208530837546834166925275516486044134775384991808184705966507606898412918594045916828375610659246423184062775112999150206172392431297837246097308511903252956622805412865917690043804311051417135098849101156584508839003337597742539960818209685142687562392007453579567729991395256699805775897135553415567045292136442139895777424891477161767258532611634530697452993846501061481697843891439474220308003706472837459911525285821188577408160690315522951458068463354171428220365223949985950890732881736611925133626529949897998045399734600887312408859224933727829625089164535236559716582775403784110923285873186648442456409760158728501220463308455437074192539205964902261490928669488824051563042951500651206733594863336608245755565801460390869016718045121902354170201577095168",
"numerator": "1"
}
},
"name": "one_over_two_to_4095"
},
{
"canonical_source": "ratio(-1044388881413152506691752710716624382579964249047383780384233483283953907971557456848826811934997558340890106714439262837987573438185793607263236087851365277945956976543709998340361590134383718314428070011855946226376318839397712745672334684344586617496807908705803704071284048740118609114467977783598029006686938976881787785946905630190260940599579453432823469303026696443059025015972399867714215541693835559885291486318237914434496734087811872639496475100189041349008417061675093668333850551032972088269550769983616369411933015213796825837188091833656751221318492846368125550225998300412344784862595674492194617023806505913245610825731835380087608622102834270197698202313169017678006675195485079921636419370285375124784014907159135459982790513399611551794271106831134090584272884279791554849782954323534517065223269061394905987693002122963395687782878948440616007412945674919823050571642377154816321380631045902916136926708342856440730447899971901781465763473223850267253059899795996090799469201774624817718449867455659250178329070473119433165550807568221846571746373296884912819520317457002440926616910874148385078411929804522981857338977648103126085903001302413467189726673216491511131602920781738033436090243804708340403154190335, 1)",
"expression": {
"Number": {
"denominator": "1",
"numerator": "-1044388881413152506691752710716624382579964249047383780384233483283953907971557456848826811934997558340890106714439262837987573438185793607263236087851365277945956976543709998340361590134383718314428070011855946226376318839397712745672334684344586617496807908705803704071284048740118609114467977783598029006686938976881787785946905630190260940599579453432823469303026696443059025015972399867714215541693835559885291486318237914434496734087811872639496475100189041349008417061675093668333850551032972088269550769983616369411933015213796825837188091833656751221318492846368125550225998300412344784862595674492194617023806505913245610825731835380087608622102834270197698202313169017678006675195485079921636419370285375124784014907159135459982790513399611551794271106831134090584272884279791554849782954323534517065223269061394905987693002122963395687782878948440616007412945674919823050571642377154816321380631045902916136926708342856440730447899971901781465763473223850267253059899795996090799469201774624817718449867455659250178329070473119433165550807568221846571746373296884912819520317457002440926616910874148385078411929804522981857338977648103126085903001302413467189726673216491511131602920781738033436090243804708340403154190335"
}
},
"name": "negative_4096_bit_bound"
}
]
@@ -0,0 +1,240 @@
use super::{NumericError, NumericLimit};
use serde::{Deserialize, Serialize};
use thiserror::Error;
/// Every event is charged once per dynamic occurrence, before entering it.
///
/// A Call expression therefore charges Expression and CalculationEntry; a
/// Property read charges Expression and PropertyBinding. List element visits
/// are additional to evaluation of their expressions. Only executed branches
/// are charged. Targets are sorted by category (properties, then checks), then
/// stable UUID bytes, by the integrating evaluator.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EvaluationStep {
Target,
Expression,
CalculationEntry,
PropertyBinding,
ListElement,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum NumericOperation {
ParseWire,
ParseLiteral,
Add,
Subtract,
Multiply,
Divide,
Compare,
Negate,
Absolute,
Reciprocal,
Power,
KindAssertion,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum WorkResource {
DynamicEvents,
NumericUnits,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum WorkSite {
Evaluation(EvaluationStep),
Numeric(NumericOperation),
}
/// Failed reservations consume no partial charge. The first failure is latched.
#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)]
#[error("{resource:?}: used {used}, requested {requested}, limit {limit}, at {site:?}")]
pub struct WorkExhaustion {
pub resource: WorkResource,
pub used: u64,
pub requested: u64,
pub limit: u64,
pub site: WorkSite,
}
/// Provisional calibration ceilings, not measured execution-time guarantees.
///
/// Effective limits, profile identity, capture, targets and context must all be
/// recorded in request/result/frozen identity. A caller may lower these ceilings.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "WorkLimitsWire", into = "WorkLimitsWire")]
pub struct WorkLimits {
dynamic_events: u64,
numeric_units: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct WorkLimitsWire {
dynamic_events: u64,
numeric_units: u64,
}
impl WorkLimits {
pub const REFERENCE: Self = Self {
dynamic_events: 1_000_000,
numeric_units: 50_000_000,
};
pub fn new(dynamic_events: u64, numeric_units: u64) -> Result<Self, NumericError> {
if dynamic_events > Self::REFERENCE.dynamic_events
|| numeric_units > Self::REFERENCE.numeric_units
{
return Err(NumericError::InvalidWorkLimits);
}
Ok(Self {
dynamic_events,
numeric_units,
})
}
pub fn dynamic_events(self) -> u64 {
self.dynamic_events
}
pub fn numeric_units(self) -> u64 {
self.numeric_units
}
}
impl TryFrom<WorkLimitsWire> for WorkLimits {
type Error = NumericError;
fn try_from(value: WorkLimitsWire) -> Result<Self, Self::Error> {
Self::new(value.dynamic_events, value.numeric_units)
}
}
impl From<WorkLimits> for WorkLimitsWire {
fn from(value: WorkLimits) -> Self {
Self {
dynamic_events: value.dynamic_events,
numeric_units: value.numeric_units,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WorkUsage {
pub dynamic_events: u64,
pub numeric_units: u64,
}
/// Intentionally neither Clone nor Default: pass the same mutable meter through
/// every target, numeric operation and expanded call in one evaluation request.
///
/// Exhaustion is terminal. Integrators must stop traversing siblings/targets and
/// mark already collected causes as incomplete. Replaying an aggregate cache
/// cost is insufficient if it changes the exact exhaustion location; initially
/// retain logical traversal even when numerical values come from a cache.
#[derive(Debug)]
pub struct WorkMeter {
limits: WorkLimits,
usage: WorkUsage,
exhausted: Option<WorkExhaustion>,
}
impl WorkMeter {
pub fn new(limits: WorkLimits) -> Self {
Self {
limits,
usage: WorkUsage::default(),
exhausted: None,
}
}
pub fn limits(&self) -> WorkLimits {
self.limits
}
pub fn usage(&self) -> WorkUsage {
self.usage
}
pub fn exhaustion(&self) -> Option<&WorkExhaustion> {
self.exhausted.as_ref()
}
pub fn ensure_active(&self) -> Result<(), NumericError> {
match &self.exhausted {
Some(failure) => Err(NumericError::Limit(NumericLimit::Work(failure.clone()))),
None => Ok(()),
}
}
pub fn visit(&mut self, step: EvaluationStep) -> Result<(), NumericError> {
self.reserve(WorkResource::DynamicEvents, 1, WorkSite::Evaluation(step))
}
pub(super) fn numeric(
&mut self,
operation: NumericOperation,
units: u64,
) -> Result<(), NumericError> {
self.reserve(
WorkResource::NumericUnits,
units,
WorkSite::Numeric(operation),
)
}
fn reserve(
&mut self,
resource: WorkResource,
requested: u64,
site: WorkSite,
) -> Result<(), NumericError> {
self.ensure_active()?;
let (used, limit) = match resource {
WorkResource::DynamicEvents => (self.usage.dynamic_events, self.limits.dynamic_events),
WorkResource::NumericUnits => (self.usage.numeric_units, self.limits.numeric_units),
};
// Subtraction is safe: only successful reservations update usage.
if requested > limit - used {
let failure = WorkExhaustion {
resource,
used,
requested,
limit,
site,
};
self.exhausted = Some(failure.clone());
return Err(NumericError::Limit(NumericLimit::Work(failure)));
}
match resource {
WorkResource::DynamicEvents => self.usage.dynamic_events += requested,
WorkResource::NumericUnits => self.usage.numeric_units += requested,
}
Ok(())
}
}
// Logical 64-bit words are platform independent. These are deterministic
// accounting weights, not a claim about num-bigint's internal algorithm/timing.
pub(super) fn words(bits: u64) -> u64 {
bits.max(1).div_ceil(64)
}
pub(super) fn product_cost(a_bits: u64, b_bits: u64) -> u64 {
1 + words(a_bits) * words(b_bits)
}
pub(super) fn sum_cost(a_bits: u64, b_bits: u64) -> u64 {
1 + words(a_bits.max(b_bits))
}
pub(super) fn normalization_cost(n_bits: u64, d_bits: u64) -> u64 {
let width = words(n_bits.max(d_bits));
// 64*w² for GCD plus 2*w² for the two quotient operations.
1 + 66 * width * width
}
pub(super) fn parse_cost(digits: usize, estimated_bits: u64) -> u64 {
1 + digits as u64 * words(estimated_bits)
}
@@ -0,0 +1,378 @@
use super::{Dimension, Exact, MAX_INTEGER_POWER, NumericError, NumericOperation, WorkMeter};
use std::cmp::Ordering;
/// K is supplied by the common resolver's verified nominal compatibility
/// relation. It is not inferred from dimension/name/symbol/archive bytes here.
/// Coordinate basis/algebra/difference-kind closure must be part of that proof.
/// Exact definition references and pins remain the caller's provenance.
/// K must be a bounded, cheap-to-clone/compare verified handle. Constructing or
/// comparing an unbounded rooted contract graph is resolver admission work and
/// must not be hidden inside K's Clone/Eq implementations. Rust trait bounds do
/// not enforce this resource contract.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedKind<K> {
key: K,
dimension: Dimension,
difference: Option<K>,
}
impl<K> ResolvedKind<K> {
pub fn linear(key: K, dimension: Dimension) -> Self {
Self {
key,
dimension,
difference: None,
}
}
pub fn key(&self) -> &K {
&self.key
}
pub fn dimension(&self) -> Dimension {
self.dimension
}
pub fn is_affine(&self) -> bool {
self.difference.is_some()
}
}
impl<K: Clone + Eq> ResolvedKind<K> {
pub fn affine(key: K, difference: &Self) -> Result<Self, NumericError> {
if difference.is_affine() || key == difference.key {
return Err(NumericError::DifferenceKind);
}
Ok(Self {
key,
dimension: difference.dimension,
difference: Some(difference.key.clone()),
})
}
pub fn difference_kind(&self) -> Option<Self> {
self.difference
.as_ref()
.map(|key| Self::linear(key.clone(), self.dimension))
}
}
/// Canonical coordinate = magnitude * scale + offset.
/// Unit symbols and display preferences are outside this numeric representation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Unit<K> {
kind: ResolvedKind<K>,
scale: Exact,
offset: Exact,
}
impl<K: Clone + Eq> Unit<K> {
pub fn new(kind: ResolvedKind<K>, scale: Exact, offset: Exact) -> Result<Self, NumericError> {
if !scale.is_positive() {
return Err(NumericError::UnitScale);
}
if !kind.is_affine() && !offset.is_zero() {
return Err(NumericError::LinearOffset);
}
Ok(Self {
kind,
scale,
offset,
})
}
pub fn kind(&self) -> &ResolvedKind<K> {
&self.kind
}
pub fn scale(&self) -> &Exact {
&self.scale
}
pub fn offset(&self) -> &Exact {
&self.offset
}
/// Fixed multiply-then-add path. Each reduced arithmetic stage is bounded.
/// A mathematically cancelling wider intermediate still reports NumericLimit;
/// replacing this path with a fused operation requires a new work profile.
pub fn to_canonical(
&self,
magnitude: &Exact,
meter: &mut WorkMeter,
) -> Result<Exact, NumericError> {
magnitude
.multiply(&self.scale, meter)?
.add(&self.offset, meter)
}
/// Fixed subtract-then-divide path; never rounds a coordinate.
pub fn from_canonical(
&self,
coordinate: &Exact,
meter: &mut WorkMeter,
) -> Result<Exact, NumericError> {
coordinate
.subtract(&self.offset, meter)?
.divide(&self.scale, meter)
}
pub fn convert(
&self,
magnitude: &Exact,
target: &Self,
meter: &mut WorkMeter,
) -> Result<Exact, NumericError> {
meter.ensure_active()?;
if self.kind != target.kind {
return Err(NumericError::IncompatibleTypes);
}
target.from_canonical(&self.to_canonical(magnitude, meter)?, meter)
}
}
/// Internal numerical types. Boolean/list/check semantics belong to the common
/// expression evaluator. Anonymous zero dimension normalizes to Number.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NumericType<K> {
Number,
Named(ResolvedKind<K>),
Anonymous(Dimension),
}
impl<K> NumericType<K> {
pub fn dimension(&self) -> Dimension {
match self {
Self::Number => Dimension::ZERO,
Self::Named(kind) => kind.dimension,
Self::Anonymous(dimension) => *dimension,
}
}
pub fn is_affine(&self) -> bool {
matches!(self, Self::Named(kind) if kind.is_affine())
}
fn anonymous(dimension: Dimension) -> Self {
if dimension.is_dimensionless() {
Self::Number
} else {
Self::Anonymous(dimension)
}
}
fn normalized(self) -> Self {
match self {
Self::Anonymous(dimension) => Self::anonymous(dimension),
other => other,
}
}
}
/// A canonical rational coordinate with an internal numerical type.
///
/// Derived Eq compares stored representation. Formal equality/ordering must call
/// compare(), which rejects mismatched nominal kinds instead of treating them
/// as a false predicate. Public property output must still check its declared
/// type and anchor the result to that declaration's exact definition reference.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NumericValue<K> {
value_type: NumericType<K>,
coordinate: Exact,
}
impl<K: Clone + Eq> NumericValue<K> {
pub fn new(value_type: NumericType<K>, coordinate: Exact) -> Self {
Self {
value_type: value_type.normalized(),
coordinate,
}
}
pub fn number(coordinate: Exact) -> Self {
Self::new(NumericType::Number, coordinate)
}
pub fn from_unit(
magnitude: &Exact,
unit: &Unit<K>,
meter: &mut WorkMeter,
) -> Result<Self, NumericError> {
meter.ensure_active()?;
Ok(Self::new(
NumericType::Named(unit.kind.clone()),
unit.to_canonical(magnitude, meter)?,
))
}
pub fn value_type(&self) -> &NumericType<K> {
&self.value_type
}
pub fn coordinate(&self) -> &Exact {
&self.coordinate
}
pub fn display_in(&self, unit: &Unit<K>, meter: &mut WorkMeter) -> Result<Exact, NumericError> {
meter.ensure_active()?;
if !matches!(&self.value_type, NumericType::Named(kind) if kind == &unit.kind) {
return Err(NumericError::IncompatibleTypes);
}
unit.from_canonical(&self.coordinate, meter)
}
pub fn add(&self, rhs: &Self, meter: &mut WorkMeter) -> Result<Self, NumericError> {
meter.ensure_active()?;
let output = self.additive_type(rhs, false)?;
Ok(Self::new(
output,
self.coordinate.add(&rhs.coordinate, meter)?,
))
}
pub fn subtract(&self, rhs: &Self, meter: &mut WorkMeter) -> Result<Self, NumericError> {
meter.ensure_active()?;
let output = self.additive_type(rhs, true)?;
Ok(Self::new(
output,
self.coordinate.subtract(&rhs.coordinate, meter)?,
))
}
fn additive_type(&self, rhs: &Self, subtract: bool) -> Result<NumericType<K>, NumericError> {
use NumericType::{Anonymous, Named, Number};
match (&self.value_type, &rhs.value_type) {
(Number, Number) => Ok(Number),
(Anonymous(left), Anonymous(right)) if left == right => Ok(Anonymous(*left)),
(Named(left), Named(right)) => match (&left.difference, &right.difference) {
(None, None) if left == right => Ok(Named(left.clone())),
(Some(difference), Some(_)) if subtract && left == right => Ok(Named(
ResolvedKind::linear(difference.clone(), left.dimension),
)),
(Some(difference), None)
if difference == &right.key && left.dimension == right.dimension =>
{
Ok(Named(left.clone()))
}
(None, Some(difference))
if !subtract
&& difference == &left.key
&& left.dimension == right.dimension =>
{
Ok(Named(right.clone()))
}
_ => Err(NumericError::IncompatibleTypes),
},
_ => Err(NumericError::IncompatibleTypes),
}
}
pub fn multiply(&self, rhs: &Self, meter: &mut WorkMeter) -> Result<Self, NumericError> {
meter.ensure_active()?;
let output = self.multiplicative_type(rhs, false)?;
Ok(Self::new(
output,
self.coordinate.multiply(&rhs.coordinate, meter)?,
))
}
pub fn divide(&self, rhs: &Self, meter: &mut WorkMeter) -> Result<Self, NumericError> {
meter.ensure_active()?;
let output = self.multiplicative_type(rhs, true)?;
Ok(Self::new(
output,
self.coordinate.divide(&rhs.coordinate, meter)?,
))
}
fn multiplicative_type(
&self,
rhs: &Self,
divide: bool,
) -> Result<NumericType<K>, NumericError> {
use NumericType::Number;
if self.value_type.is_affine() || rhs.value_type.is_affine() {
return Err(NumericError::AffineOperation);
}
match (&self.value_type, &rhs.value_type) {
(Number, Number) => Ok(Number),
(_, Number) => Ok(self.value_type.clone()),
(Number, _) if !divide => Ok(rhs.value_type.clone()),
_ => {
let left = self.value_type.dimension();
let right = rhs.value_type.dimension();
let dimension = if divide {
left.checked_quotient(right)?
} else {
left.checked_product(right)?
};
Ok(NumericType::anonymous(dimension))
}
}
}
pub fn negated(&self, meter: &mut WorkMeter) -> Result<Self, NumericError> {
meter.ensure_active()?;
if self.value_type.is_affine() {
return Err(NumericError::AffineOperation);
}
Ok(Self::new(
self.value_type.clone(),
self.coordinate.negated(meter)?,
))
}
pub fn absolute(&self, meter: &mut WorkMeter) -> Result<Self, NumericError> {
meter.ensure_active()?;
if self.value_type.is_affine() {
return Err(NumericError::AffineOperation);
}
Ok(Self::new(
self.value_type.clone(),
self.coordinate.absolute(meter)?,
))
}
pub fn power(&self, exponent: i8, meter: &mut WorkMeter) -> Result<Self, NumericError> {
meter.ensure_active()?;
if !(-MAX_INTEGER_POWER..=MAX_INTEGER_POWER).contains(&exponent) {
return Err(NumericError::IntegerPower);
}
if self.value_type.is_affine() {
return Err(NumericError::AffineOperation);
}
let output = match exponent {
0 => NumericType::Number,
1 => self.value_type.clone(),
_ => NumericType::anonymous(self.value_type.dimension().checked_power(exponent)?),
};
Ok(Self::new(output, self.coordinate.power(exponent, meter)?))
}
pub fn compare(&self, rhs: &Self, meter: &mut WorkMeter) -> Result<Ordering, NumericError> {
meter.ensure_active()?;
if self.value_type != rhs.value_type {
return Err(NumericError::IncompatibleTypes);
}
self.coordinate.compare(&rhs.coordinate, meter)
}
pub fn as_kind(
&self,
target: &ResolvedKind<K>,
meter: &mut WorkMeter,
) -> Result<Self, NumericError> {
meter.ensure_active()?;
if target.is_affine()
|| matches!(self.value_type, NumericType::Named(_))
|| self.value_type.dimension() != target.dimension
{
return Err(NumericError::KindAssertion);
}
meter.numeric(NumericOperation::KindAssertion, self.coordinate.copy_cost())?;
Ok(Self::new(
NumericType::Named(target.clone()),
self.coordinate.clone(),
))
}
}
@@ -0,0 +1,804 @@
use super::*;
use num_bigint::BigInt;
use num_traits::One;
use std::cmp::Ordering;
fn meter() -> WorkMeter {
WorkMeter::new(WorkLimits::REFERENCE)
}
fn exact(n: impl ToString, d: impl ToString) -> Exact {
Exact::from_wire(
&ExactWire {
numerator: n.to_string(),
denominator: d.to_string(),
},
&mut meter(),
)
.unwrap()
}
fn source(text: &str) -> Exact {
Exact::parse_literal(text, &mut meter()).unwrap()
}
fn assert_fraction(value: &Exact, n: impl ToString, d: impl ToString) {
assert_eq!(
value.to_wire(),
ExactWire {
numerator: n.to_string(),
denominator: d.to_string(),
}
);
}
// Independent bounded i128 oracle. It does not call the production Exact/GCD
// implementation when deriving expected arithmetic or ordering.
fn small_fraction(mut n: i128, mut d: i128) -> (i128, i128) {
assert_ne!(d, 0);
if d < 0 {
n = -n;
d = -d;
}
let (mut a, mut b) = (n.abs(), d);
while b != 0 {
(a, b) = (b, a % b);
}
(n / a, d / a)
}
#[test]
fn arithmetic_matches_independent_small_integer_oracle() {
for a in [-5_i128, -2, 0, 1, 4] {
for b in [1_i128, 2, 3, 7] {
for c in [-5_i128, -2, 0, 1, 4] {
for d in [1_i128, 2, 3, 7] {
let (an, ad) = small_fraction(a, b);
let (bn, bd) = small_fraction(c, d);
let left = exact(an, ad);
let right = exact(bn, bd);
let mut work = meter();
let (n, den) = small_fraction(a * d + c * b, b * d);
assert_fraction(&left.add(&right, &mut work).unwrap(), n, den);
let (n, den) = small_fraction(a * d - c * b, b * d);
assert_fraction(&left.subtract(&right, &mut work).unwrap(), n, den);
let (n, den) = small_fraction(a * c, b * d);
assert_fraction(&left.multiply(&right, &mut work).unwrap(), n, den);
if c != 0 {
let (n, den) = small_fraction(a * d, b * c);
assert_fraction(&left.divide(&right, &mut work).unwrap(), n, den);
} else {
assert_eq!(
left.divide(&right, &mut work),
Err(NumericError::DivisionByZero)
);
}
assert_eq!(
left.compare(&right, &mut work).unwrap(),
(a * d).cmp(&(c * b))
);
}
}
}
}
}
#[test]
fn decimals_are_exact_and_source_ratios_reduce() {
assert_fraction(
&source("0.1").add(&source("0.2"), &mut meter()).unwrap(),
3,
10,
);
assert_fraction(&source("0.50"), 1, 2);
assert_fraction(&source("12.50e-2"), 1, 8);
assert_fraction(&source("1e+3"), 1000, 1);
assert_fraction(&source("0001.2500"), 5, 4);
assert_fraction(&source(" ratio( -6, 8 ) "), -3, 4);
assert_fraction(&source("ratio(-0, 27)"), 0, 1);
assert_eq!(source("0.5").canonical_literal(), "ratio(1, 2)");
assert_eq!(source("42").canonical_literal(), "42");
}
#[test]
fn powers_and_signs_have_explicit_domains() {
let x = exact(-2, 3);
assert_fraction(&x.power(3, &mut meter()).unwrap(), -8, 27);
assert_fraction(&x.power(-3, &mut meter()).unwrap(), -27, 8);
assert_fraction(&x.power(0, &mut meter()).unwrap(), 1, 1);
assert_fraction(&x.absolute(&mut meter()).unwrap(), 2, 3);
assert_fraction(&x.negated(&mut meter()).unwrap(), 2, 3);
assert_fraction(&x.reciprocal(&mut meter()).unwrap(), -3, 2);
let zero = Exact::from_integer(0);
assert_eq!(
zero.power(0, &mut meter()),
Err(NumericError::ZeroPowerZero)
);
assert_eq!(
zero.power(-1, &mut meter()),
Err(NumericError::DivisionByZero)
);
assert_eq!(x.power(17, &mut meter()), Err(NumericError::IntegerPower));
assert_eq!(
x.power(i8::MIN, &mut meter()),
Err(NumericError::IntegerPower)
);
}
#[test]
fn canonical_wire_rejects_both_component_noncanonical_forms() {
for (n, d) in [
("+1", "1"),
("01", "1"),
("-0", "1"),
("1", "+1"),
("1", "-1"),
("1", "01"),
("2", "4"),
("0", "3"),
("", "1"),
("1", ""),
("1.0", "1"),
("1", "1.0"),
("1e1", "1"),
("1", "1e1"),
(" 1", "1"),
("1", "1 "),
] {
assert!(
Exact::from_wire(
&ExactWire {
numerator: n.into(),
denominator: d.into()
},
&mut meter(),
)
.is_err(),
"accepted {n}/{d}"
);
}
assert_eq!(
Exact::from_wire(
&ExactWire {
numerator: "1".into(),
denominator: "0".into()
},
&mut meter()
),
Err(NumericError::ZeroDenominator)
);
assert_fraction(&exact(0, 1), 0, 1);
assert_fraction(&exact(-1, 2), -1, 2);
}
#[test]
fn serde_uses_closed_canonical_string_wire() {
let value = exact(-3, 7);
let encoded = serde_json::to_value(&value).unwrap();
assert_eq!(
encoded,
serde_json::json!({"numerator":"-3","denominator":"7"})
);
assert_eq!(serde_json::from_value::<Exact>(encoded).unwrap(), value);
for invalid in [
r#"{"numerator":1,"denominator":"2"}"#,
r#"{"numerator":"1","denominator":2}"#,
r#"{"numerator":"1","denominator":"2","other":0}"#,
r#"{"numerator":"1","numerator":"2","denominator":"3"}"#,
r#"{"numerator":"1"}"#,
r#"{"numerator":"2","denominator":"4"}"#,
] {
assert!(serde_json::from_str::<Exact>(invalid).is_err(), "{invalid}");
}
}
#[test]
fn literal_reader_does_not_fold_expression_unary_minus() {
assert_eq!(
Exact::parse_literal("-2", &mut meter()),
Err(NumericError::InvalidLiteral)
);
let negative_literal = source("ratio(-2, 1)");
assert_eq!(negative_literal.canonical_literal(), "ratio(-2, 1)");
assert_eq!(source("2").negated(&mut meter()).unwrap(), negative_literal);
// Equality of coordinates does not claim AST equality. The expression
// parser must preserve UnaryNegate independently of its operand literal.
assert_eq!(source("2").canonical_literal(), "2");
}
#[test]
fn literal_syntax_and_decimal_exponent_are_bounded() {
for invalid in [
"",
"-",
"+1",
".5",
"1.",
"1.2.3",
"1e",
"1ee2",
"1e+-2",
"1_000",
"NaN",
"Infinity",
"1/2",
"ratio(1)",
"ratio(1,2,3)",
"ratio(1,-2)",
"ratio(+1,2)",
"ratio(1,2) + 1",
"12",
] {
assert!(
Exact::parse_literal(invalid, &mut meter()).is_err(),
"{invalid}"
);
}
for invalid in ["1e129", "1e-129", "1e9999999999999999999999"] {
assert_eq!(
Exact::parse_literal(invalid, &mut meter()),
Err(NumericError::DecimalExponent)
);
}
assert!(Exact::parse_literal("1e128", &mut meter()).is_ok());
assert!(Exact::parse_literal("1e-128", &mut meter()).is_ok());
assert_eq!(
Exact::parse_literal(&"1".repeat(MAX_COMPONENT_DIGITS + 1), &mut meter()),
Err(NumericError::LiteralTooLong)
);
assert_eq!(
Exact::parse_literal(&" ".repeat(MAX_LITERAL_BYTES + 1), &mut meter()),
Err(NumericError::LiteralTooLong)
);
}
#[test]
fn full_independent_r7_packets_round_trip_as_literals_and_wire() {
let packets: serde_json::Value =
serde_json::from_str(include_str!("fixtures/r7-literal-boundaries.json")).unwrap();
for packet in packets.as_array().unwrap() {
let wire: ExactWire =
serde_json::from_value(packet["expression"]["Number"].clone()).unwrap();
let expected = Exact::from_wire(&wire, &mut meter()).unwrap();
let spelling = packet["canonical_source"].as_str().unwrap();
assert_eq!(expected.canonical_literal(), spelling);
assert_eq!(
Exact::parse_literal(spelling, &mut meter()).unwrap(),
expected
);
let serialized = serde_json::to_string(&expected).unwrap();
assert_eq!(
serde_json::from_str::<Exact>(&serialized).unwrap(),
expected
);
assert_eq!(expected.to_wire(), wire);
}
}
#[test]
fn full_8193_bit_sum_intermediate_can_reduce_to_admitted_value() {
// N=(2^4096)-1, D=N-1. gcd(N,D)=1 and D is even.
// N/D + N/D = N/(D/2), although its un-reduced numerator needs 8193 bits.
let n = (BigInt::one() << 4096_usize) - 1_u8;
let d = &n - 1_u8;
let value = exact(&n, &d);
let result = value.add(&value, &mut meter()).unwrap();
assert_fraction(&result, &n, &d / 2_u8);
}
#[test]
fn cross_products_cancel_before_reduced_component_limit() {
let n = BigInt::one() << 4095_usize;
let d = &n - 1_u8;
let left = exact(&n, &d);
let right = exact(&d, &n);
assert_fraction(&left.multiply(&right, &mut meter()).unwrap(), 1, 1);
assert_fraction(
&left
.add(&left.negated(&mut meter()).unwrap(), &mut meter())
.unwrap(),
0,
1,
);
}
#[test]
fn both_reduced_component_overflows_are_reported() {
let n = BigInt::one() << 4095_usize;
let mut work = meter();
let failure = exact(&n, 1)
.multiply(&Exact::from_integer(2), &mut work)
.unwrap_err();
assert!(matches!(
failure,
NumericError::Limit(NumericLimit::Bits {
component: NumberComponent::Numerator,
bits: 4097,
..
})
));
assert!(work.exhaustion().is_none());
let failure = exact(1, &n)
.divide(&Exact::from_integer(2), &mut work)
.unwrap_err();
assert!(matches!(
failure,
NumericError::Limit(NumericLimit::Bits {
component: NumberComponent::Denominator,
bits: 4097,
..
})
));
// Value overflow is a nonterminal supported-operation failure; fuel
// exhaustion, in contrast, terminates the entire request.
assert_fraction(&source("1").add(&source("1"), &mut work).unwrap(), 2, 1);
}
#[test]
fn wire_rejects_4097_bits_before_gcd_can_reduce_them() {
let oversized = (BigInt::one() << 4096_usize).to_string();
for wire in [
ExactWire {
numerator: oversized.clone(),
denominator: "1".into(),
},
ExactWire {
numerator: "1".into(),
denominator: oversized.clone(),
},
ExactWire {
numerator: oversized.clone(),
denominator: oversized,
},
] {
assert!(matches!(
Exact::from_wire(&wire, &mut meter()),
Err(NumericError::Limit(NumericLimit::Bits { stage, .. })) if stage == "wire"
));
}
}
#[test]
fn numeric_precharge_precedes_big_integer_result_allocation() {
let n = BigInt::one() << 4095_usize;
let left = exact(&n, 1);
let mut work = WorkMeter::new(WorkLimits::new(10, 0).unwrap());
let failure = left
.multiply(&Exact::from_integer(2), &mut work)
.unwrap_err();
assert!(failure.is_terminal_work_exhaustion());
assert_eq!(work.usage().numeric_units, 0);
assert!(matches!(
work.exhaustion().unwrap().site,
WorkSite::Numeric(NumericOperation::Multiply)
));
// With available fuel the same operation reaches the 4097-bit result check.
assert!(matches!(
left.multiply(&Exact::from_integer(2), &mut meter()),
Err(NumericError::Limit(NumericLimit::Bits { .. }))
));
let too_wide_wire = ExactWire {
numerator: (BigInt::one() << 4096_usize).to_string(),
denominator: "1".into(),
};
let mut parse_work = WorkMeter::new(WorkLimits::new(0, 0).unwrap());
assert!(
Exact::from_wire(&too_wide_wire, &mut parse_work)
.unwrap_err()
.is_terminal_work_exhaustion()
);
}
#[test]
fn provisional_limits_and_effective_limits_round_trip() {
let limits = WorkLimits::new(500, 2000).unwrap();
let encoded = serde_json::to_value(limits).unwrap();
assert_eq!(
encoded,
serde_json::json!({"dynamic_events":500,"numeric_units":2000})
);
assert_eq!(
serde_json::from_value::<WorkLimits>(encoded).unwrap(),
limits
);
assert_eq!(
WorkLimits::new(1_000_001, 0),
Err(NumericError::InvalidWorkLimits)
);
assert_eq!(
WorkLimits::new(0, 50_000_001),
Err(NumericError::InvalidWorkLimits)
);
assert!(
serde_json::from_str::<WorkLimits>(r#"{"dynamic_events":1000001,"numeric_units":0}"#)
.is_err()
);
}
#[test]
fn exact_accounting_boundary_and_terminal_failure_are_stable() {
// One-word addition: three products at 2, one sum at 2,
// normalization at 67 => 75 logical numeric units.
let mut work = WorkMeter::new(WorkLimits::new(4, 75).unwrap());
assert_fraction(&exact(1, 2).add(&exact(1, 3), &mut work).unwrap(), 5, 6);
assert_eq!(work.usage().numeric_units, 75);
assert!(work.exhaustion().is_none());
let first = Exact::from_integer(1).negated(&mut work).unwrap_err();
let at_failure = work.usage();
assert_eq!(
work.exhaustion().unwrap(),
&WorkExhaustion {
resource: WorkResource::NumericUnits,
used: 75,
requested: 3,
limit: 75,
site: WorkSite::Numeric(NumericOperation::Negate),
}
);
assert_eq!(work.visit(EvaluationStep::Target).unwrap_err(), first);
assert_eq!(
Exact::from_integer(1)
.divide(&Exact::from_integer(0), &mut work)
.unwrap_err(),
first
);
assert_eq!(
work.numeric(NumericOperation::Compare, 0).unwrap_err(),
first
);
assert_eq!(work.usage(), at_failure);
}
#[test]
fn zero_cost_and_maximum_reservation_do_not_underflow_or_reactivate() {
let mut work = WorkMeter::new(WorkLimits::new(0, 0).unwrap());
work.numeric(NumericOperation::Compare, 0).unwrap();
let failure = work
.numeric(NumericOperation::Multiply, u64::MAX)
.unwrap_err();
assert!(failure.is_terminal_work_exhaustion());
assert_eq!(work.usage(), WorkUsage::default());
assert_eq!(
work.numeric(NumericOperation::Compare, 0).unwrap_err(),
failure
);
let mut dynamic = WorkMeter::new(WorkLimits::new(0, 5).unwrap());
assert!(dynamic.visit(EvaluationStep::Target).is_err());
assert_eq!(dynamic.usage(), WorkUsage::default());
}
#[test]
fn multiple_targets_share_one_aggregate_meter() {
let mut work = WorkMeter::new(WorkLimits::new(3, 50).unwrap());
work.visit(EvaluationStep::Target).unwrap();
work.visit(EvaluationStep::Expression).unwrap();
work.visit(EvaluationStep::Target).unwrap();
let failure = work.visit(EvaluationStep::Expression).unwrap_err();
assert!(failure.is_terminal_work_exhaustion());
assert_eq!(work.usage().dynamic_events, 3);
}
// A direct bounded call-tree workload, not an expression interpreter/dialect.
fn expanded_calls(
depth: u8,
x: u64,
work: &mut WorkMeter,
entered: &mut u64,
) -> Result<u64, NumericError> {
work.visit(EvaluationStep::CalculationEntry)?;
*entered += 1;
if depth == 0 {
return Ok(x);
}
let left = expanded_calls(depth - 1, 2 * x, work, entered)?;
let right = expanded_calls(depth - 1, 2 * x + 1, work, entered)?;
Ok(left + right)
}
#[test]
fn c1_expanded_call_witness_terminates_at_dynamic_limit() {
let mut work = WorkMeter::new(WorkLimits::new(10_000, 0).unwrap());
let mut entered = 0;
let failure = expanded_calls(30, 0, &mut work, &mut entered).unwrap_err();
assert!(failure.is_terminal_work_exhaustion());
assert_eq!(entered, 10_000);
assert_eq!(work.usage().dynamic_events, 10_000);
assert_eq!(
work.exhaustion().unwrap().site,
WorkSite::Evaluation(EvaluationStep::CalculationEntry)
);
// Nearby finite control: 63 invocations, leaves 0..31, sum 496.
let mut bounded = WorkMeter::new(WorkLimits::new(63, 0).unwrap());
let mut entered = 0;
assert_eq!(
expanded_calls(5, 0, &mut bounded, &mut entered).unwrap(),
496
);
assert_eq!(entered, 63);
assert!(bounded.exhaustion().is_none());
}
fn dim(value: [i16; 7]) -> Dimension {
Dimension::new(value).unwrap()
}
fn linear(key: &str, dimension: Dimension) -> ResolvedKind<String> {
ResolvedKind::linear(key.to_owned(), dimension)
}
fn named(n: i64, kind: &ResolvedKind<String>) -> NumericValue<String> {
NumericValue::new(NumericType::Named(kind.clone()), Exact::from_integer(n))
}
#[test]
fn dimensions_use_checked_seven_axis_integer_algebra() {
let length = dim([1, 0, 0, 0, 0, 0, 0]);
let mass = dim([0, 1, 0, 0, 0, 0, 0]);
let time = dim([0, 0, 1, 0, 0, 0, 0]);
assert_eq!(
mass.checked_product(length)
.unwrap()
.checked_quotient(time.checked_power(2).unwrap())
.unwrap(),
dim([1, 1, -2, 0, 0, 0, 0])
);
assert!(time.checked_quotient(time).unwrap().is_dimensionless());
assert!(matches!(
dim([32, 0, 0, 0, 0, 0, 0]).checked_product(length),
Err(NumericError::DimensionExponent {
axis: DimensionAxis::Length,
value: 33
})
));
assert!(dim([0, 0, -32, 0, 0, 0, 0]).checked_quotient(time).is_err());
assert!(length.checked_power(i8::MIN).is_err());
assert!(Dimension::new([33, 0, 0, 0, 0, 0, 0]).is_err());
assert_eq!(
serde_json::to_value(time).unwrap(),
serde_json::json!([0, 0, 1, 0, 0, 0, 0])
);
for invalid in ["[0,0,33,0,0,0,0]", "[0,0,1]", "[0,0,1,0,0,0,0,0]"] {
assert!(serde_json::from_str::<Dimension>(invalid).is_err());
}
}
#[test]
fn duration_unit_coordinates_are_exact_and_nominal() {
let duration = linear("duration", dim([0, 0, 1, 0, 0, 0, 0]));
let s = Unit::new(duration.clone(), source("1"), source("0")).unwrap();
let ms = Unit::new(duration.clone(), source("0.001"), source("0")).unwrap();
assert_fraction(&ms.convert(&source("750"), &s, &mut meter()).unwrap(), 3, 4);
let receive = NumericValue::from_unit(&source("750"), &ms, &mut meter()).unwrap();
let dispatch = NumericValue::from_unit(&source("1"), &s, &mut meter()).unwrap();
let total = receive.add(&dispatch, &mut meter()).unwrap();
assert_fraction(total.coordinate(), 7, 4);
assert_fraction(&total.display_in(&ms, &mut meter()).unwrap(), 1750, 1);
let different = Unit::new(
linear("different-kind", duration.dimension()),
source("1"),
source("0"),
)
.unwrap();
assert_eq!(
ms.convert(&source("750"), &different, &mut meter()),
Err(NumericError::IncompatibleTypes)
);
let inconsistent = Unit::new(
linear("duration", dim([1, 0, 0, 0, 0, 0, 0])),
source("1"),
source("0"),
)
.unwrap();
assert!(
ms.convert(&source("1"), &inconsistent, &mut meter())
.is_err()
);
assert!(Unit::new(duration.clone(), source("0"), source("0")).is_err());
assert!(Unit::new(duration.clone(), source("ratio(-1,1)"), source("0")).is_err());
assert_eq!(
Unit::new(duration, source("1"), source("1")).unwrap_err(),
NumericError::LinearOffset
);
}
#[test]
fn affine_point_and_difference_arithmetic_has_distinct_kinds() {
let interval = linear("temperature-difference", dim([0, 0, 0, 0, 1, 0, 0]));
let point = ResolvedKind::affine("temperature-point".to_owned(), &interval).unwrap();
let celsius = Unit::new(point.clone(), source("1"), source("273.15")).unwrap();
let fahrenheit = Unit::new(
point.clone(),
source("ratio(5,9)"),
source("ratio(45967,180)"),
)
.unwrap();
let kelvin_interval = Unit::new(interval.clone(), source("1"), source("0")).unwrap();
assert_fraction(
&celsius
.convert(&source("0"), &fahrenheit, &mut meter())
.unwrap(),
32,
1,
);
assert_fraction(
&fahrenheit
.convert(&source("70"), &celsius, &mut meter())
.unwrap(),
190,
9,
);
let p20 = NumericValue::from_unit(&source("20"), &celsius, &mut meter()).unwrap();
let p10 = NumericValue::from_unit(&source("10"), &celsius, &mut meter()).unwrap();
let difference = p20.subtract(&p10, &mut meter()).unwrap();
assert_eq!(
difference.value_type(),
&NumericType::Named(interval.clone())
);
assert_fraction(
&difference
.display_in(&kelvin_interval, &mut meter())
.unwrap(),
10,
1,
);
let delta5 = named(5, &interval);
let p25 = p20.add(&delta5, &mut meter()).unwrap();
assert_fraction(&p25.display_in(&celsius, &mut meter()).unwrap(), 25, 1);
assert_eq!(delta5.add(&p20, &mut meter()).unwrap(), p25);
assert!(p20.add(&p10, &mut meter()).is_err());
assert!(delta5.subtract(&p20, &mut meter()).is_err());
assert_eq!(
p20.power(1, &mut meter()),
Err(NumericError::AffineOperation)
);
assert_eq!(
p20.negated(&mut meter()),
Err(NumericError::AffineOperation)
);
assert_eq!(p20.compare(&p10, &mut meter()).unwrap(), Ordering::Greater);
assert!(p20.display_in(&kelvin_interval, &mut meter()).is_err());
assert!(ResolvedKind::affine(interval.key().clone(), &interval).is_err());
assert!(ResolvedKind::affine("other".to_owned(), &point).is_err());
}
#[test]
fn affine_conversion_rejects_a_wider_stage_even_when_the_final_value_would_fit() {
// A fused coordinate calculation 2*m-m equals m, where m=2^4095 fits.
// The fixed multiply stage produces 2^4096, so it must fail before offset
// cancellation. The inverse path likewise rejects (m-(-m))/2 at subtraction.
let large = BigInt::one() << 4095_usize;
let magnitude = exact(&large, 1);
let interval = linear("temperature-interval", dim([0, 0, 0, 0, 1, 0, 0]));
let point = ResolvedKind::affine("temperature-point".to_owned(), &interval).unwrap();
let unit = Unit::new(point, Exact::from_integer(2), exact(-&large, 1)).unwrap();
let mut work = meter();
for outcome in [
unit.to_canonical(&magnitude, &mut work),
unit.from_canonical(&magnitude, &mut work),
] {
assert!(matches!(
outcome,
Err(NumericError::Limit(NumericLimit::Bits {
component: NumberComponent::Numerator,
bits: 4097,
limit: MAX_COMPONENT_BITS,
..
}))
));
}
assert!(work.exhaustion().is_none());
let half = exact(&large >> 1_usize, 1);
assert_fraction(&unit.to_canonical(&half, &mut work).unwrap(), 0, 1);
assert_eq!(
unit.from_canonical(&Exact::from_integer(0), &mut work)
.unwrap(),
half
);
}
#[test]
fn r6_anonymous_compound_energy_needs_one_outer_assertion() {
let force = linear("force", dim([1, 1, -2, 0, 0, 0, 0]));
let length = linear("length", dim([1, 0, 0, 0, 0, 0, 0]));
let energy = linear("energy", dim([2, 1, -2, 0, 0, 0, 0]));
let a = named(3, &force)
.multiply(&named(4, &length), &mut meter())
.unwrap();
let b = named(2, &force)
.multiply(&named(7, &length), &mut meter())
.unwrap();
assert_eq!(a.value_type(), &NumericType::Anonymous(energy.dimension()));
let sum = a.add(&b, &mut meter()).unwrap();
let result = sum.as_kind(&energy, &mut meter()).unwrap();
assert_fraction(result.coordinate(), 26, 1);
assert_eq!(result.value_type(), &NumericType::Named(energy.clone()));
assert_eq!(a.compare(&b, &mut meter()).unwrap(), Ordering::Less);
assert!(result.add(&sum, &mut meter()).is_err());
assert_eq!(
result.as_kind(&energy, &mut meter()),
Err(NumericError::KindAssertion)
);
}
#[test]
fn anonymous_product_then_quotient_closes_and_preserves_scaling() {
let mass = linear("mass", dim([0, 1, 0, 0, 0, 0, 0]));
let length = linear("length", dim([1, 0, 0, 0, 0, 0, 0]));
let time = linear("duration", dim([0, 0, 1, 0, 0, 0, 0]));
let momentum = linear("momentum", dim([1, 1, -1, 0, 0, 0, 0]));
let result = named(2, &mass)
.multiply(&named(3, &length), &mut meter())
.unwrap()
.divide(&named(2, &time), &mut meter())
.unwrap()
.as_kind(&momentum, &mut meter())
.unwrap();
assert_fraction(result.coordinate(), 3, 1);
let scalar = NumericValue::number(source("2"));
let scaled = scalar.multiply(&named(3, &time), &mut meter()).unwrap();
assert_eq!(scaled.value_type(), &NumericType::Named(time.clone()));
assert_fraction(scaled.coordinate(), 6, 1);
assert_eq!(
named(3, &time)
.divide(&scalar, &mut meter())
.unwrap()
.value_type(),
&NumericType::Named(time.clone())
);
assert_eq!(
scalar
.divide(&named(3, &time), &mut meter())
.unwrap()
.value_type(),
&NumericType::Anonymous(dim([0, 0, -1, 0, 0, 0, 0]))
);
}
#[test]
fn nominal_kinds_survive_comparison_but_explicit_quotients_can_erase_dimension() {
let energy = linear("energy", dim([2, 1, -2, 0, 0, 0, 0]));
let torque = linear("torque", energy.dimension());
let left = named(7, &energy);
let right = named(7, &torque);
assert!(left.add(&right, &mut meter()).is_err());
assert!(left.compare(&right, &mut meter()).is_err());
let ratio = left.divide(&right, &mut meter()).unwrap();
assert_eq!(ratio.value_type(), &NumericType::Number);
assert_fraction(ratio.coordinate(), 1, 1);
let named_ratio = linear("chosen-ratio", Dimension::ZERO);
let asserted = ratio.as_kind(&named_ratio, &mut meter()).unwrap();
assert_eq!(
asserted.value_type(),
&NumericType::Named(named_ratio.clone())
);
assert!(asserted.add(&ratio, &mut meter()).is_err());
assert_eq!(
NumericValue::<String>::new(NumericType::Anonymous(Dimension::ZERO), source("1")),
NumericValue::number(source("1"))
);
}
#[test]
fn numerical_power_types_and_dimension_overflow_are_checked() {
let length = linear("length", dim([1, 0, 0, 0, 0, 0, 0]));
let value = named(2, &length);
assert_eq!(
value.power(0, &mut meter()).unwrap().value_type(),
&NumericType::Number
);
assert_eq!(
value.power(1, &mut meter()).unwrap().value_type(),
value.value_type()
);
assert_eq!(
value.power(-2, &mut meter()).unwrap().value_type(),
&NumericType::Anonymous(dim([-2, 0, 0, 0, 0, 0, 0]))
);
let large_dimension = linear("large-dimension", dim([32, 0, 0, 0, 0, 0, 0]));
assert!(
named(1, &large_dimension)
.multiply(&value, &mut meter())
.is_err()
);
assert!(named(1, &large_dimension).power(2, &mut meter()).is_err());
}
+1
View File
@@ -1,4 +1,5 @@
//! Framework-independent OA/SA model, transactional commands and editing services.
pub mod analysis_numeric;
pub mod data;
pub mod language;
pub mod model;
-1
View File
@@ -18,7 +18,6 @@ serde_json.workspace = true
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
tokio = { version = "1", features = ["rt", "time"] }
tempfile = "3"
unicode-segmentation = "1.13.3"
[dev-dependencies]
iced_runtime = "0.14"
+61 -107
View File
@@ -58,7 +58,7 @@ mod direct_scenario_tests {
}
}
#[test]
fn direct_scenario_camera_details_and_real_resize_invalidate_only_pointer_context() {
fn direct_scenario_scroll_and_real_resize_invalidate_only_pointer_context() {
let (mut app, f) = setup();
let original = app.document.bundle().clone();
let event = begin(&app, &f);
@@ -74,26 +74,18 @@ mod direct_scenario_tests {
let _ = app.update(Message::ScenarioCanvas(event));
assert!(!app.behavior.has_pending());
let context = app.scenario_context().unwrap();
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Camera(
context,
app.camera_epoch,
behavior::canvas_workspace::Event::Pan(iced::Vector::new(30., 150.)),
)));
assert_ne!(app.scenario_context(), Some(context));
let context = app.scenario_context().unwrap();
for offset in [iced::Vector::new(0., 90.), iced::Vector::new(0., 150.)] {
for (area, offset) in [
(scenario::ScrollArea::Canvas, iced::Vector::new(30., 150.)),
(scenario::ScrollArea::Page, iced::Vector::new(0., 90.)),
] {
let before = app.scenario_context().unwrap();
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled(
context,
scenario::ScrollArea::Page,
offset,
context, area, offset,
)));
assert_ne!(app.scenario_context(), Some(before));
let current = app.scenario_context().unwrap();
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled(
context,
scenario::ScrollArea::Page,
offset,
context, area, offset,
)));
assert_eq!(
app.scenario_context(),
@@ -122,7 +114,7 @@ mod direct_scenario_tests {
}
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled(
stale,
scenario::ScrollArea::Page,
scenario::ScrollArea::Canvas,
iced::Vector::new(0., 900.),
)));
assert_eq!(app.scenario_context(), Some(current));
@@ -1046,12 +1038,9 @@ impl App {
crate::model_menu::popup(self),
)
.into();
crate::behavior::scenario::focus::root(
behavior::canvas_workspace::focus_root(
crate::hosted::view::overlay(self, self.reference_overlay(base)),
behavior::canvas_workspace::Scope::input_owner(self),
),
self.scenario_focus_owner(),
behavior::canvas_workspace::focus_root(
crate::hosted::view::overlay(self, self.reference_overlay(base)),
behavior::canvas_workspace::Scope::input_owner(self),
)
}
pub fn subscription(&self) -> Subscription<Message> {
@@ -1500,20 +1489,6 @@ impl App {
epoch: self.canvas_epoch,
})
}
pub(crate) fn scenario_focus_owner(&self) -> Option<crate::behavior::scenario::focus::Stamp> {
if self.busy
|| self.hosted.visible
|| self.workflow.is_some()
|| self.architecture.is_some()
|| self.model_menu.is_some()
|| self.file_prompt.is_some()
|| self.delete_pending
|| self.pending.is_some()
{
return None;
}
self.behavior.scenario.focus_stamp(self.scenario_context()?)
}
fn handle(&mut self, message: Message) -> Task<Message> {
match message {
Message::BehaviorControl(intent) => return self.behavior_control(intent),
@@ -1532,15 +1507,14 @@ impl App {
Message::ScenarioCanvas(event) => {
use crate::behavior::scenario::{self, CanvasEvent};
let context = event.context();
if let CanvasEvent::FormShown(_, mount) = event {
if let CanvasEvent::FormShown(_, session) = event {
if self.scenario_context().is_some_and(|current| {
current.document == context.document
&& current.scenario == context.scenario
&& current.revision == context.revision
}) && self.scenario_focus_owner().is_some()
&& self.behavior.scenario.take_form_focus(mount)
}) && self.behavior.scenario.take_form_focus(session)
{
return scenario::focus::task(self.scenario_focus_owner().unwrap());
return iced::widget::operation::focus(scenario::NAME_INPUT);
}
return Task::none();
}
@@ -1568,9 +1542,9 @@ impl App {
return Task::none();
}
if let CanvasEvent::Scrolled(_, area, offset) = event {
// Details may report more than once in a frame. Accept the
// current owner's passive clamps across an epoch bump while
// still rejecting another document, owner or revision.
// Canvas and outline notifications may come from the
// same frame. Accept both across the first epoch bump,
// while still rejecting another owner/document/revision.
if self.scenario_context().is_some_and(|current| {
current.document == context.document
&& current.scenario == context.scenario
@@ -1725,6 +1699,37 @@ impl App {
return Task::none();
}
match mutation {
CanvasEvent::First(_, tool) => {
let empty = match tool {
scenario::Tool::Lifeline => !self
.project()
.children(context.scenario)
.any(|e| e.kind() == Kind::Lifeline),
scenario::Tool::Message | scenario::Tool::Action => {
self.project()
.children(context.scenario)
.any(|e| e.kind() == Kind::Lifeline)
&& !self
.project()
.children(context.scenario)
.any(|e| e.kind() == Kind::Occurrence)
}
_ => false,
};
if !empty {
self.status="This scenario already has content. Use its current authoring tools.".into();
return Task::none();
}
let mut state = std::mem::take(&mut self.behavior.scenario);
let result = state.update(
self.project(),
scenario::Event::Start(context.scenario, tool),
);
self.behavior.scenario = state;
if let Err(error) = result {
self.status = error;
}
}
CanvasEvent::BeginOccurrence {
tool,
slot,
@@ -2283,7 +2288,7 @@ impl App {
if let crate::workflow::Workflow::Semantic(state) = &mut workflow {
match event {
crate::workflow::Event::Semantic(event) => {
state.update(self.project(), *event)
state.update(self.project(), event)
}
crate::workflow::Event::Apply => {
let focus = state.target(self.project());
@@ -3037,17 +3042,11 @@ impl App {
|| self.path.as_ref() == Some(&receipt.target)
{
self.path = Some(receipt.target);
self.document.bind_repair_baseline(disk.reachable_bundle());
self.disk_project = Some(disk);
}
self.status = "Interrupted disk save recovered. Current model and drafts are retained; Save all can be retried.".into();
}
Err(error) => {
if self.path.is_none()
|| self.path.as_ref() == Some(&receipt.target)
{
self.document.invalidate_saved_baseline();
}
self.status = format!(
"Disk recovery completed; files could not be reloaded: {error}"
)
@@ -3477,52 +3476,6 @@ impl App {
id,
));
}
// Refining a leaf moves its explicit responsibilities to the new leaf.
// Later children inherit only the existing unambiguous performer summary.
if self.create_kind.is_work()
&& let Some(owner) = parent
&& self
.project()
.elements
.get(&owner)
.is_some_and(|e| e.layer() == self.layer && e.kind() == self.create_kind)
{
let performer = oa_core::validation::allocation_summary(self.project(), owner);
let leaf = !self
.project()
.children(owner)
.any(|e| e.kind() == self.create_kind);
let responsibilities: Vec<_> = self
.project()
.relations
.values()
.filter(|r| {
r.target() == owner
&& matches!(
r.kind(),
RelationKind::Allocation | RelationKind::RoleAssignment
)
})
.collect();
if leaf && !responsibilities.is_empty() {
for original in responsibilities {
let mut responsibility = original.clone();
match &mut responsibility.body {
RelationBody::Allocation { target, .. }
| RelationBody::RoleAssignment { target, .. } => *target = id,
_ => unreachable!(),
}
commands.push(Command::EditRelation(original.id, responsibility));
}
} else if let Some(performer) = performer {
commands.push(Command::Connect(
RelationKind::Allocation,
"Performs".into(),
performer,
id,
));
}
}
if self.create_kind == Kind::Occurrence
&& let Some(owner) = parent.and_then(|id| self.project().elements.get(&id))
&& let Some(steps) = owner.steps()
@@ -3540,13 +3493,19 @@ impl App {
.get(&diagram)
.is_none_or(|d| d.projection.includes(self.create_kind))
{
let mut p = match execute_batch(self.project(), &commands) {
Ok(candidate) => candidate,
Err(error) => {
self.status = error.to_string();
return;
let mut p = self.project().clone();
for command in &commands {
if let Command::CreateDiagram(d) = command {
p.diagrams.insert(d.id, d.clone());
}
};
}
p.elements.insert(
id,
match &commands[0] {
Command::CreateElement(e) => e.clone(),
_ => unreachable!(),
},
);
let owner = if allocated {
Some(self.selected)
} else {
@@ -3864,7 +3823,6 @@ impl App {
self.canvas_document = Id::new_v4();
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
self.path = Some(path);
self.document.bind_repair_baseline(disk.reachable_bundle());
self.source_workspace = Workspace::new(&disk.bundle, false);
self.source_workspace.reachable = disk.reachable.clone();
self.show_source_file(false);
@@ -4532,10 +4490,6 @@ fn read_snapshot(path: PathBuf) -> Result<LoadedSnapshot, String> {
#[path = "app/save_as_tests.rs"]
mod save_as_tests;
#[cfg(test)]
#[path = "app/repair_save_state_tests.rs"]
mod repair_save_state_tests;
#[cfg(test)]
mod source_project_tests {
use super::*;
+20 -19
View File
@@ -569,30 +569,32 @@ mod tests {
assert!(!app.behavior.has_pending());
}
#[test]
fn direct_process_app_save_details_resize_and_passive_geometry_keep_drafts_and_camera() {
fn direct_process_app_save_scroll_resize_and_passive_geometry_keep_drafts_and_current_origin() {
let (mut app, f) = setup();
let context = app.topology_context().unwrap();
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Camera(
context,
app.camera_epoch,
behavior::canvas_workspace::Event::Pan(iced::Vector::new(25., 100.)),
)));
let context = app.topology_context().unwrap();
let original = app.document.snapshot();
app.busy = true;
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Scrolled(
context,
behavior::scenario::ScrollArea::Page,
iced::Vector::new(0., 40.),
)));
for (area, offset) in [
(
behavior::scenario::ScrollArea::Canvas,
iced::Vector::new(25., 100.),
),
(
behavior::scenario::ScrollArea::Page,
iced::Vector::new(0., 40.),
),
] {
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Scrolled(
context, area, offset,
)));
}
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Viewport(
context,
iced::Size::new(500., 300.),
)));
let camera = app.behavior.topology.camera_boundary();
assert_eq!(
app.behavior.topology.details_offset(),
iced::Vector::new(0., 40.)
app.behavior.topology.scroll_offset(),
iced::Vector::new(25., 140.)
);
assert_ne!(app.topology_context(), Some(context));
assert!(app.busy);
@@ -600,12 +602,12 @@ mod tests {
stale.document = Id::new_v4();
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Scrolled(
stale,
behavior::scenario::ScrollArea::Page,
behavior::scenario::ScrollArea::Canvas,
iced::Vector::new(0., 900.),
)));
assert_eq!(
app.behavior.topology.details_offset(),
iced::Vector::new(0., 40.)
app.behavior.topology.scroll_offset(),
iced::Vector::new(25., 140.)
);
let root = PathBuf::from("process-scroll-save-fixture");
let receipt = FileSnapshot {
@@ -625,7 +627,6 @@ mod tests {
assert!(!app.busy);
assert!(!app.document.can_undo());
assert_eq!(app.project(), &f.p);
assert!(app.behavior.topology.camera_boundary() == camera);
let context = app.topology_context().unwrap();
let size = app.window_size;
let _ = app.update(Message::WindowResized(size));
@@ -26,7 +26,7 @@ impl Host {
match self {
Self::Inspector => Message::FieldReferenceEvent(event),
Self::Review { address, facet } => {
Message::Workflow(workflow::Event::semantic(semantic_review::Event::Row {
Message::Workflow(workflow::Event::Semantic(semantic_review::Event::Row {
address: *address,
facet: Some(facet.clone()),
event: RowEvent::ReferenceEvent(event),
@@ -210,15 +210,11 @@ impl App {
Message::FieldReferenceEvent(event) => {
Some(self.reference_event(Host::Inspector, event.clone()))
}
Message::Workflow(workflow::Event::Semantic(review_event)) => {
let semantic_review::Event::Row {
address,
facet: Some(facet),
event,
} = review_event.as_ref()
else {
return None;
};
Message::Workflow(workflow::Event::Semantic(semantic_review::Event::Row {
address,
facet: Some(facet),
event,
})) => {
let host = Host::Review {
address: *address,
facet: facet.clone(),
@@ -74,7 +74,7 @@ fn open(app: &mut App, host: &Host, path: &str) {
let message = match host {
Host::Inspector => Message::FieldReferenceOpen(request),
Host::Review { address, facet } => {
Message::Workflow(workflow::Event::semantic(semantic_review::Event::Row {
Message::Workflow(workflow::Event::Semantic(semantic_review::Event::Row {
address: *address,
facet: Some(facet.clone()),
event: RowEvent::ReferenceOpen(request),
@@ -125,7 +125,7 @@ fn review(app: &App) -> &semantic_review::State {
}
fn send_review(app: &mut App, event: RowEvent) {
let addressed = review(app).address_event(event);
let _ = app.update(Message::Workflow(workflow::Event::semantic(addressed)));
let _ = app.update(Message::Workflow(workflow::Event::Semantic(addressed)));
}
fn review_host(app: &App) -> Host {
let semantic_review::Event::Row {
@@ -306,13 +306,8 @@ fn chooser_rejects_old_results_after_context_recovery_reload_or_modal_and_keeps_
)));
}
_ => {
app.source_workspace.edit(
"unfinished source".into(),
iced::widget::text_editor::Cursor {
position: iced::widget::text_editor::Position { line: 0, column: 0 },
selection: None,
},
);
app.source_workspace
.edit("unfinished source".into(), Default::default());
}
}
let expected = raw(&app);
@@ -425,25 +420,6 @@ fn review_reference_facets_groups_and_attachment_hosts_browse_without_replanning
};
let mut app = App::with_project(p.clone());
let _ = app.update(Message::StartSemanticReview(Id::from_u128(100)));
if mode == 2 {
// A paired attachment is a real catalog facet only after explicit
// correspondence; selecting an unstaged synthetic pair is rejected.
for (id, oa) in [(103, true), (104, false)] {
let choice = crate::app::Choice {
id: Id::from_u128(id),
label: format!("Record · @object_{id}"),
};
send_review(
&mut app,
if oa {
RowEvent::ReferenceOa(choice)
} else {
RowEvent::ReferenceSa(choice)
},
);
}
send_review(&mut app, RowEvent::AddReference);
}
send_review(&mut app, RowEvent::Select(facet.clone()));
send_review(
&mut app,
@@ -455,19 +431,7 @@ fn review_reference_facets_groups_and_attachment_hosts_browse_without_replanning
} else {
Id::from_u128(101)
};
assert_eq!(
app.reference_draft(&host, path)
.unwrap_or_else(|| {
panic!(
"Reference fixture mode={mode}, facet={facet:?}, selected={:?}, decisions={:?}, error={:?}",
review(&app).selected,
review(&app).decisions,
review(&app).error
)
})
.original,
record
);
assert_eq!(app.reference_draft(&host, path).unwrap().original, record);
let before = review(&app).recovery_snapshot().unwrap();
let plan = serde_json::to_value(&review(&app).plan).unwrap();
let proposed = review(&app).proposed.clone();
@@ -563,7 +527,7 @@ fn review_reference_cannot_reopen_or_write_a_banked_row_after_another_row_takes_
}),
semantic_review::Event::OpenTrace,
] {
let _ = app.update(Message::Workflow(workflow::Event::semantic(event)));
let _ = app.update(Message::Workflow(workflow::Event::Semantic(event)));
}
assert_eq!(review(&app).target(app.project()), Some(Id::from_u128(111)));
let expected = review(&app).recovery_snapshot().unwrap();
@@ -572,7 +536,7 @@ fn review_reference_cannot_reopen_or_write_a_banked_row_after_another_row_takes_
let Host::Review { address, facet } = host else {
unreachable!()
};
let _ = app.update(Message::Workflow(workflow::Event::semantic(
let _ = app.update(Message::Workflow(workflow::Event::Semantic(
semantic_review::Event::Row {
address,
facet: Some(facet),
@@ -3,7 +3,7 @@
use super::*;
use iced::futures::StreamExt;
use iced::{
Element, Event as Input, Point, Size, Theme, Vector,
Event as Input, Point, Size, Theme, Vector,
advanced::{
Layout, Shell, layout,
widget::{Id as WidgetId, Operation, Tree, operation},
@@ -11,7 +11,6 @@ use iced::{
keyboard, mouse,
widget::{Space, column, container, opaque, scrollable, stack, text_input},
};
use oa_core::transitions::TransitionDirection;
#[derive(Default)]
struct Clipboard;
@@ -109,7 +108,7 @@ fn view(
text_input("Required engineering rationale", raw)
.id("review-rationale")
.on_input(|raw| {
Message::Workflow(workflow::Event::semantic(
Message::Workflow(workflow::Event::Semantic(
state.address_event(RowEvent::OperationRationale(raw)),
))
}),
@@ -119,7 +118,7 @@ fn view(
adapter::field(draft, app.project(), "/parent").map(|message| {
match message {
Message::FieldReferenceOpen(request) => {
Message::Workflow(workflow::Event::semantic(
Message::Workflow(workflow::Event::Semantic(
state.address_event(RowEvent::ReferenceOpen(request)),
))
}
@@ -128,7 +127,7 @@ fn view(
}),
draft.reference_session(),
));
opaque(container(trigger::modal(content)).width(300).height(180))
opaque(container(trigger::modal(content)).width(300).height(180)).into()
}
None => Space::new().into(),
}
@@ -724,7 +723,7 @@ async fn mounted_review_pointer_open_and_outside_dismissal_keep_background_input
let messages = mount.click(&app, Point::new(359., 319.));
assert!(matches!(
messages.as_slice(),
[Message::Workflow(workflow::Event::Semantic(event))] if matches!(event.as_ref(),
[Message::Workflow(workflow::Event::Semantic(
semantic_review::Event::Row {
event: RowEvent::ReferenceEvent(chooser::Event {
action: Action::Close(Close::Outside),
@@ -732,7 +731,7 @@ async fn mounted_review_pointer_open_and_outside_dismissal_keep_background_input
}),
..
}
)
))]
));
for message in messages {
let task = app.update(message);
@@ -760,34 +759,22 @@ async fn mounted_reverse_review_required_rationale_remains_editable_beside_candi
let sa = p
.elements
.values()
.find(|element| element.alias == "operation_probe_43")
.find(|element| {
element.layer() == Layer::Sa
&& matches!(
element.shared(),
Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange {
operation: Some(_),
message_kind: MessageKind::Call | MessageKind::Asynchronous,
..
},
..
})
)
})
.unwrap()
.id;
let allocation = p
.relations
.values()
.find(|relation| relation.alias == "operation_probe_30")
.unwrap()
.id;
// The boundary fixture deliberately has no selected operation. Author one
// through the validated command path before testing reverse omission.
let mut message = p.elements[&sa].clone();
let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { operation, .. },
..
}) = message.shared_mut()
else {
panic!("The operation fixture must contain an Exchange call")
};
*operation = Some(OperationUse {
allocation: Some(allocation),
});
p = execute_batch(&p, &[Command::EditElement(sa, message)]).unwrap();
assert!(matches!(
oa_core::operations::resolve(&p, sa).unwrap(),
oa_core::operations::OperationResolution::Selected(value)
if value.allocation == allocation
));
let inspector_id = Id::new_v4();
element(
&mut p,
@@ -833,7 +820,7 @@ async fn mounted_reverse_review_required_rationale_remains_editable_beside_candi
let source = app.document.bundle().clone();
let inspector_raw = raw(&app);
let _ = app.update(Message::StartSemanticReview(trace_id));
let _ = app.update(Message::Workflow(workflow::Event::semantic(
let _ = app.update(Message::Workflow(workflow::Event::Semantic(
semantic_review::Event::Direction(TransitionDirection::SaToOa),
)));
let facet = FacetSelector::Header(HeaderFacet::Parent);
@@ -848,14 +835,7 @@ async fn mounted_reverse_review_required_rationale_remains_editable_beside_candi
let candidate = serde_json::to_value(
review(&app)
.reference_form(app.project())
.unwrap_or_else(|| {
panic!(
"Reverse review candidate: selected={:?}, decisions={:?}, error={:?}",
review(&app).selected,
review(&app).decisions,
review(&app).error
)
})
.unwrap()
.recovery_snapshot(),
)
.unwrap();
@@ -877,10 +857,10 @@ async fn mounted_reverse_review_required_rationale_remains_editable_beside_candi
);
assert!(messages.iter().all(|message| matches!(
message,
Message::Workflow(workflow::Event::Semantic(event)) if matches!(event.as_ref(), semantic_review::Event::Row {
Message::Workflow(workflow::Event::Semantic(semantic_review::Event::Row {
event: RowEvent::OperationRationale(_),
..
})
}))
)));
mount.dispatch(&mut app, messages).await;
assert_eq!(
@@ -1,263 +0,0 @@
use super::*;
const VALID: &str = concat!(
"arcadia 7;\r\n",
"project repair \"Repair 救援\" id \"9e8f0000-0000-4000-8000-000000000001\" {\r\n",
" oa_activity assess \"Assess\" id \"9e8f0000-0000-4000-8000-000000000002\" {}\r\n",
"}\r\n",
);
fn open(app: &mut App, path: &std::path::Path) {
let _ = app.update(Message::Loaded(Ok(Some(
read_snapshot(path.to_owned()).unwrap(),
))));
}
fn edit(app: &mut App, text: &str) {
let _ = app.update(Message::SourceEdit(text_editor::Action::SelectAll));
let _ = app.update(Message::SourceEdit(text_editor::Action::Edit(
text_editor::Edit::Paste(Arc::new(text.into())),
)));
}
fn save(app: &mut App, path: &std::path::Path) {
let receipt = write_snapshot(
app.canvas_document,
app.document.revision(),
app.document.snapshot(),
path.to_owned(),
app.disk_project.clone(),
)
.unwrap();
let _ = app.update(Message::Saved(Ok(Some(receipt))));
}
#[test]
fn repair_to_previous_saved_bytes_requires_save_at_new_destination_and_guards_close() {
for extra in [
" include \"missing.arcadia\";\r\n",
" oa_activity assess \"Duplicate\" id \"9e8f0000-0000-4000-8000-000000000003\" {}\r\n",
] {
let first = tempfile::tempdir().unwrap();
let second = tempfile::tempdir().unwrap();
let first_path = first.path().canonicalize().unwrap().join("project.arcadia");
let second_path = second
.path()
.canonicalize()
.unwrap()
.join("project.arcadia");
let invalid = VALID.strip_suffix("}\r\n").unwrap().to_owned() + extra + "}\r\n";
std::fs::write(&first_path, VALID).unwrap();
std::fs::write(&second_path, &invalid).unwrap();
let mut app = App::with_project(Project::empty());
open(&mut app, &first_path);
assert!(!app.dirty());
let before = app.document.snapshot();
let revision = app.document.revision();
open(&mut app, &second_path);
assert!(!app.source_workspace.applied);
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
edit(&mut app, VALID);
assert!(app.apply_source(), "{}", app.status);
assert!(app.source_workspace.applied);
assert!(!app.source_dirty());
assert_eq!(app.document.bundle(), &before.bundle);
assert_eq!(app.project(), &before.decoded.project);
assert_eq!(app.document.revision(), revision);
assert!(
!app.document.can_undo(),
"Equal content creates no history step"
);
assert!(app.document.dirty());
assert_eq!(app.hosted_label(), "Unsaved changes");
assert_eq!(std::fs::read(&second_path).unwrap(), invalid.as_bytes());
assert_eq!(std::fs::read(&first_path).unwrap(), VALID.as_bytes());
let close = window::Id::unique();
let task = app.update(Message::Request(Pending::Close(close)));
assert!(iced_runtime::task::into_stream(task).is_none());
assert!(matches!(app.pending, Some(Pending::Close(id)) if id == close));
let _ = app.update(Message::Cancel);
save(&mut app, &second_path);
assert!(!app.dirty());
assert_eq!(app.hosted_label(), "All changes saved");
assert_eq!(std::fs::read(&second_path).unwrap(), VALID.as_bytes());
assert_eq!(std::fs::read(&first_path).unwrap(), VALID.as_bytes());
}
}
#[test]
fn repair_history_compares_to_new_disk_baseline_and_then_successful_save() {
let directory = tempfile::tempdir().unwrap();
let path = directory
.path()
.canonicalize()
.unwrap()
.join("project.arcadia");
std::fs::write(&path, VALID).unwrap();
let mut app = App::with_project(Project::empty());
open(&mut app, &path);
let before = app.document.snapshot();
let invalid = format!("{VALID} unfinished {{");
std::fs::write(&path, &invalid).unwrap();
open(&mut app, &path);
let repaired = format!("{VALID}// Repaired rationale\r\n");
edit(&mut app, &repaired);
assert!(app.apply_source());
assert!(app.dirty());
let _ = app.update(Message::Undo);
assert_eq!(app.document.bundle(), &before.bundle);
assert!(
app.dirty(),
"Old saved bytes are not saved at the repair destination"
);
let _ = app.update(Message::Redo);
assert!(app.dirty());
assert_eq!(std::fs::read(&path).unwrap(), invalid.as_bytes());
std::fs::write(&path, "external edit").unwrap();
let failed = write_snapshot(
app.canvas_document,
app.document.revision(),
app.document.snapshot(),
path.clone(),
app.disk_project.clone(),
)
.unwrap_err();
let _ = app.update(Message::Saved(Err(failed)));
assert!(app.dirty());
assert_eq!(std::fs::read(&path).unwrap(), b"external edit");
std::fs::write(&path, &invalid).unwrap();
save(&mut app, &path);
assert!(!app.dirty());
let _ = app.update(Message::Undo);
assert!(app.dirty());
assert_eq!(app.document.bundle(), &before.bundle);
let _ = app.update(Message::Redo);
assert!(!app.dirty(), "Redo to the new saved snapshot is clean");
assert_eq!(std::fs::read(&path).unwrap(), repaired.as_bytes());
}
#[test]
fn valid_equal_content_at_distinct_destinations_stays_clean() {
let first = tempfile::tempdir().unwrap();
let second = tempfile::tempdir().unwrap();
let first_path = first.path().canonicalize().unwrap().join("project.arcadia");
let second_path = second
.path()
.canonicalize()
.unwrap()
.join("project.arcadia");
std::fs::write(&first_path, VALID).unwrap();
std::fs::write(&second_path, VALID).unwrap();
let mut app = App::with_project(Project::empty());
open(&mut app, &first_path);
let before = app.document.bundle().clone();
open(&mut app, &second_path);
assert_eq!(app.document.bundle(), &before);
assert_eq!(app.path.as_ref(), Some(&second_path));
assert!(!app.dirty());
assert_eq!(app.hosted_label(), "All changes saved");
}
#[test]
fn recovering_applied_repair_rebinds_rolled_back_disk_bytes_without_changing_model() {
let first = tempfile::tempdir().unwrap();
let second = tempfile::tempdir().unwrap();
let first_path = first.path().canonicalize().unwrap().join("project.arcadia");
let root = second.path().canonicalize().unwrap();
let path = root.join("project.arcadia");
let before = VALID.replace("Repair 救援", "Before 救援");
std::fs::write(&first_path, VALID).unwrap();
std::fs::write(&path, VALID).unwrap();
let journal = root.join(".arcadia-save-project.arcadia.journal");
std::fs::create_dir(&journal).unwrap();
std::fs::write(
journal.join("journal.json"),
br#"{"version":1,"entry":"project.arcadia","files":[{"path":"project.arcadia","before":true,"write":true}]}"#,
)
.unwrap();
std::fs::write(journal.join("0.before"), &before).unwrap();
std::fs::write(journal.join("0.after"), VALID).unwrap();
let mut app = App::with_project(Project::empty());
open(&mut app, &first_path);
let accepted = app.document.snapshot();
open(&mut app, &path);
assert_eq!(app.pending_recovery.as_ref(), Some(&path));
assert!(!app.source_workspace.applied);
assert!(app.apply_source());
assert!(
!app.dirty(),
"Current bytes still match disk before rollback"
);
let _ = app.update(Message::SourceRecovered(recover_snapshot(path.clone())));
assert!(app.pending_recovery.is_none());
assert!(!journal.exists());
assert_eq!(std::fs::read(&path).unwrap(), before.as_bytes());
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
assert!(!app.source_dirty());
assert!(app.dirty(), "Recovery changed disk, not the accepted model");
assert_eq!(app.hosted_label(), "Unsaved changes");
save(&mut app, &path);
assert!(!app.dirty());
assert_eq!(std::fs::read(&path).unwrap(), VALID.as_bytes());
}
#[test]
fn recovery_missing_entry_or_failed_reload_preserves_current_destination_save_authority() {
for (current_destination, failed_reload) in
[(true, false), (false, false), (true, true), (false, true)]
{
let directory = tempfile::tempdir().unwrap();
let root = directory.path().canonicalize().unwrap();
let path = root.join("project.arcadia");
std::fs::write(&path, VALID).unwrap();
let mut app = App::with_project(Project::empty());
open(&mut app, &path);
let accepted = app.document.snapshot();
let journal = root.join(".arcadia-save-project.arcadia.journal");
std::fs::create_dir(&journal).unwrap();
std::fs::write(
journal.join("journal.json"),
br#"{"version":1,"entry":"project.arcadia","files":[{"path":"project.arcadia","before":false,"write":true}]}"#,
)
.unwrap();
std::fs::write(journal.join("0.after"), VALID).unwrap();
open(&mut app, &path);
assert!(app.apply_source());
assert!(!app.dirty());
let destination = tempfile::tempdir().unwrap();
let saved_elsewhere = destination
.path()
.canonicalize()
.unwrap()
.join("project.arcadia");
if !current_destination {
save(&mut app, &saved_elsewhere);
assert_eq!(app.path.as_ref(), Some(&saved_elsewhere));
assert_eq!(app.pending_recovery.as_ref(), Some(&path));
}
let mut receipt = recover_snapshot(path.clone()).unwrap();
assert!(
receipt.disk.as_ref().unwrap().bundle.files.is_empty(),
"Missing entry reloads as an empty repair workspace"
);
if failed_reload {
// Separately exercise the adapter's successful-recovery/failed-read
// boundary; first-save rollback itself does not cause a read error.
receipt.disk = Err("Injected post-recovery reload failure".into());
}
let _ = app.update(Message::SourceRecovered(Ok(receipt)));
assert!(!path.exists());
assert!(!journal.exists());
assert!(app.pending_recovery.is_none());
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
assert_eq!(app.dirty(), current_destination);
if current_destination {
let _ = app.update(Message::Request(Pending::New));
assert!(matches!(app.pending, Some(Pending::New)));
} else {
assert_eq!(app.path.as_ref(), Some(&saved_elsewhere));
assert_eq!(std::fs::read(&saved_elsewhere).unwrap(), VALID.as_bytes());
}
}
}
@@ -16,7 +16,7 @@ fn state(app: &App) -> &State {
}
fn row_event(app: &mut App, event: RowEvent) {
let event = state(app).address_event(event);
send(app, Message::Workflow(Event::semantic(event)));
send(app, Message::Workflow(Event::Semantic(event)));
}
fn fixture(reverse: bool, destination_view: bool) -> (App, Id, Id, Id, Option<Id>) {
let mut p = Project::empty();
+251 -380
View File
@@ -20,6 +20,8 @@ mod contextual_source_tests;
pub(crate) mod creation;
mod form_session;
pub(crate) mod scenario;
// Projection acceptance precedes the coordinated scenario renderer integration.
#[cfg(test)]
mod scenario_lifecycle;
pub(crate) mod source_draft;
pub(crate) mod state_canvas;
@@ -391,96 +393,97 @@ mod scenario_scroll {
std::time::Instant::now(),
))
};
let area = ScrollArea::Page;
for returned_height in [600., 250.] {
let (project, [a, _, _], [use_id, _, _]) = navigation_fixture();
let mut app = App::with_project(project.clone());
let _ = app.update(Message::Select(a));
let _ = app.update(Message::Page(crate::app::Page::Behavior));
let source = app.document.bundle().clone();
let mut view = observer(600., app.scenario_context().unwrap());
view.area = area;
let mut tree = widget::Tree::new(&view as &dyn Widget<Message, Theme, ()>);
for message in update(&mut view, &mut tree, redraw()).0 {
let _ = app.update(message);
for area in [ScrollArea::Page] {
for returned_height in [600., 250.] {
let (project, [a, _, _], [use_id, _, _]) = navigation_fixture();
let mut app = App::with_project(project.clone());
let _ = app.update(Message::Select(a));
let _ = app.update(Message::Page(crate::app::Page::Behavior));
let source = app.document.bundle().clone();
let mut view = observer(600., app.scenario_context().unwrap());
view.area = area;
let mut tree = widget::Tree::new(&view as &dyn Widget<Message, Theme, ()>);
for message in update(&mut view, &mut tree, redraw()).0 {
let _ = app.update(message);
}
for message in update(
&mut view,
&mut tree,
iced::Event::Mouse(mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Pixels { x: 0., y: -180. },
}),
)
.0
{
let _ = app.update(message);
}
assert_eq!(actual_offset(&mut view, &mut tree), Vector::new(0., 180.));
navigation_event(&mut app, |ctx| CanvasEvent::OpenUse(ctx, use_id));
view = observer(600., app.scenario_context().unwrap());
view.area = area;
view.diff(&mut tree);
for message in update(&mut view, &mut tree, redraw()).0 {
let _ = app.update(message);
}
assert_eq!(actual_offset(&mut view, &mut tree), Vector::ZERO);
navigation_event(&mut app, CanvasEvent::Back);
assert_eq!(app.selected, use_id);
let context = app.scenario_context().unwrap();
let request = app.behavior.scenario.scroll_restore(context, area).unwrap();
view = observer(returned_height, context);
view.area = area;
view.restore = Some(request);
view.diff(&mut tree);
let expected = Vector::new(0., (returned_height - 200.).min(180.));
let messages = update(&mut view, &mut tree, redraw()).0;
assert!(
matches!(messages.as_slice(), [Message::ScenarioCanvas(CanvasEvent::Restored(_, _, _, actual))] if *actual == expected)
);
assert_eq!(actual_offset(&mut view, &mut tree), expected);
for message in messages {
let _ = app.update(message);
}
assert!(
app.behavior
.scenario
.scroll_restore(app.scenario_context().unwrap(), area)
.is_none()
);
// A delayed receipt leaves the same request in the view; later input must not snap back.
for message in update(
&mut view,
&mut tree,
iced::Event::Mouse(mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Pixels { x: 0., y: 20. },
}),
)
.0
{
let _ = app.update(message);
}
let moved = expected - Vector::new(0., 20.);
assert_eq!(actual_offset(&mut view, &mut tree), moved);
view.context = app.scenario_context().unwrap();
view.diff(&mut tree);
assert!(update(&mut view, &mut tree, redraw()).0.is_empty());
assert_eq!(actual_offset(&mut view, &mut tree), moved);
navigation_event(&mut app, |ctx| {
CanvasEvent::Restored(ctx, request.token, area, Vector::new(0., 999.))
});
let late_context = app.scenario_context().unwrap();
let _ = app.update(Message::Page(crate::app::Page::Diagram));
let epoch = app.canvas_epoch;
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Restored(
late_context,
request.token,
area,
Vector::ZERO,
)));
assert_eq!(app.canvas_epoch, epoch);
assert_eq!(app.project(), &project);
assert_eq!(app.document.bundle(), &source);
assert!(!app.document.can_undo());
}
for message in update(
&mut view,
&mut tree,
iced::Event::Mouse(mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Pixels { x: 0., y: -180. },
}),
)
.0
{
let _ = app.update(message);
}
assert_eq!(actual_offset(&mut view, &mut tree), Vector::new(0., 180.));
navigation_event(&mut app, |ctx| CanvasEvent::OpenUse(ctx, use_id));
view = observer(600., app.scenario_context().unwrap());
view.area = area;
view.diff(&mut tree);
for message in update(&mut view, &mut tree, redraw()).0 {
let _ = app.update(message);
}
assert_eq!(actual_offset(&mut view, &mut tree), Vector::ZERO);
navigation_event(&mut app, CanvasEvent::Back);
assert_eq!(app.selected, use_id);
let context = app.scenario_context().unwrap();
let request = app.behavior.scenario.scroll_restore(context, area).unwrap();
view = observer(returned_height, context);
view.area = area;
view.restore = Some(request);
view.diff(&mut tree);
let expected = Vector::new(0., (returned_height - 200.).min(180.));
let messages = update(&mut view, &mut tree, redraw()).0;
assert!(
matches!(messages.as_slice(), [Message::ScenarioCanvas(CanvasEvent::Restored(_, _, _, actual))] if *actual == expected)
);
assert_eq!(actual_offset(&mut view, &mut tree), expected);
for message in messages {
let _ = app.update(message);
}
assert!(
app.behavior
.scenario
.scroll_restore(app.scenario_context().unwrap(), area)
.is_none()
);
// A delayed receipt leaves the same request in the view; later input must not snap back.
for message in update(
&mut view,
&mut tree,
iced::Event::Mouse(mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Pixels { x: 0., y: 20. },
}),
)
.0
{
let _ = app.update(message);
}
let moved = expected - Vector::new(0., 20.);
assert_eq!(actual_offset(&mut view, &mut tree), moved);
view.context = app.scenario_context().unwrap();
view.diff(&mut tree);
assert!(update(&mut view, &mut tree, redraw()).0.is_empty());
assert_eq!(actual_offset(&mut view, &mut tree), moved);
navigation_event(&mut app, |ctx| {
CanvasEvent::Restored(ctx, request.token, area, Vector::new(0., 999.))
});
let late_context = app.scenario_context().unwrap();
let _ = app.update(Message::Page(crate::app::Page::Diagram));
let epoch = app.canvas_epoch;
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Restored(
late_context,
request.token,
area,
Vector::ZERO,
)));
assert_eq!(app.canvas_epoch, epoch);
assert_eq!(app.project(), &project);
assert_eq!(app.document.bundle(), &source);
assert!(!app.document.can_undo());
}
}
#[test]
@@ -562,9 +565,9 @@ mod scenario_scroll {
other.scenario = Id::new_v4();
let mut state = topology::State::default();
state.enter(context.document, context.scenario);
state.scrolled(scenario::ScrollArea::Page, Vector::new(0., 180.));
state.scrolled(scenario::ScrollArea::Canvas, Vector::new(0., 180.));
state.enter(other.document, other.scenario);
assert_eq!(state.details_offset(), Vector::ZERO);
assert_eq!(state.scroll_offset(), Vector::ZERO);
view = observer(600., other);
view.process = true;
view.diff(&mut tree);
@@ -581,91 +584,92 @@ mod scenario_scroll {
{
use widget::operation::scrollable::{AbsoluteOffset, scroll_to};
for process in [false, true] {
let area = scenario::ScrollArea::Page;
for change in 0..4 {
let context = scenario::direct_tests::context(Id::new_v4());
let mut view = observer(600., context);
view.process = process;
view.area = area;
let mut tree = widget::Tree::new(&view as &dyn Widget<Message, Theme, ()>);
let node = view.layout(
&mut tree,
&(),
&layout::Limits::new(Size::ZERO, Size::new(800., 700.)),
);
// Model a programmatic reveal before the observer has reported.
// Mounted identity must not depend on its last scroll notification.
let mut operation = scroll_to::<()>(
widget::Id::new("observer-test"),
AbsoluteOffset {
x: None,
y: Some(180.),
},
);
view.operate(&mut tree, Layout::new(&node), &(), &mut operation);
let mut next = context;
match change {
0 => next.scenario = Id::new_v4(),
1 => next.document = Id::new_v4(),
2 => {
next.revision += 1;
next.epoch += 5;
for area in [scenario::ScrollArea::Canvas, scenario::ScrollArea::Page] {
for change in 0..4 {
let context = scenario::direct_tests::context(Id::new_v4());
let mut view = observer(600., context);
view.process = process;
view.area = area;
let mut tree = widget::Tree::new(&view as &dyn Widget<Message, Theme, ()>);
let node = view.layout(
&mut tree,
&(),
&layout::Limits::new(Size::ZERO, Size::new(800., 700.)),
);
// Model a programmatic reveal before the observer has reported.
// Mounted identity must not depend on its last scroll notification.
let mut operation = scroll_to::<()>(
widget::Id::new("observer-test"),
AbsoluteOffset {
x: None,
y: Some(180.),
},
);
view.operate(&mut tree, Layout::new(&node), &(), &mut operation);
let mut next = context;
match change {
0 => next.scenario = Id::new_v4(),
1 => next.document = Id::new_v4(),
2 => {
next.revision += 1;
next.epoch += 5;
}
_ => {}
}
_ => {}
view = observer(if change == 3 { 800. } else { 600. }, next);
view.process = process;
view.area = area;
view.diff(&mut tree);
let redraw = || {
iced::Event::Window(iced::window::Event::RedrawRequested(
std::time::Instant::now(),
))
};
let (messages, _) = update(&mut view, &mut tree, redraw());
let actual = messages
.into_iter()
.filter_map(|message| match message {
Message::TopologyCanvas(topology::CanvasEvent::Scrolled(
c,
reported_area,
offset,
)) => {
assert_eq!(
(c.document, c.owner, c.revision, c.epoch),
(next.document, next.scenario, next.revision, next.epoch)
);
assert_eq!(
std::mem::discriminant(&reported_area),
std::mem::discriminant(&area)
);
Some(offset)
}
Message::ScenarioCanvas(scenario::CanvasEvent::Scrolled(
c,
reported_area,
offset,
)) => {
assert_eq!(c, next);
assert_eq!(
std::mem::discriminant(&reported_area),
std::mem::discriminant(&area)
);
Some(offset)
}
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(
actual,
vec![if change < 2 {
Vector::ZERO
} else {
Vector::new(0., 180.)
}],
"process={process} area={area:?} change={change}"
);
assert!(update(&mut view, &mut tree, redraw()).0.is_empty());
}
view = observer(if change == 3 { 800. } else { 600. }, next);
view.process = process;
view.area = area;
view.diff(&mut tree);
let redraw = || {
iced::Event::Window(iced::window::Event::RedrawRequested(
std::time::Instant::now(),
))
};
let (messages, _) = update(&mut view, &mut tree, redraw());
let actual = messages
.into_iter()
.filter_map(|message| match message {
Message::TopologyCanvas(topology::CanvasEvent::Scrolled(
c,
reported_area,
offset,
)) => {
assert_eq!(
(c.document, c.owner, c.revision, c.epoch),
(next.document, next.scenario, next.revision, next.epoch)
);
assert_eq!(
std::mem::discriminant(&reported_area),
std::mem::discriminant(&area)
);
Some(offset)
}
Message::ScenarioCanvas(scenario::CanvasEvent::Scrolled(
c,
reported_area,
offset,
)) => {
assert_eq!(c, next);
assert_eq!(
std::mem::discriminant(&reported_area),
std::mem::discriminant(&area)
);
Some(offset)
}
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(
actual,
vec![if change < 2 {
Vector::ZERO
} else {
Vector::new(0., 180.)
}],
"process={process} area={area:?} change={change}"
);
assert!(update(&mut view, &mut tree, redraw()).0.is_empty());
}
}
}
@@ -1124,31 +1128,6 @@ struct BoxItem {
detail: String,
marker: Option<Kind>,
}
fn box_text_clip(
pan: iced::Vector,
rect: Rectangle,
scale: f32,
viewport: Size,
) -> Option<Rectangle> {
if !pan.x.is_finite()
|| !pan.y.is_finite()
|| !scale.is_finite()
|| scale <= 0.
|| rect.width <= 20.
|| rect.height <= 16.
{
return None;
}
Rectangle::new(
Point::new(
pan.x + (rect.x + 10.) * scale,
pan.y + (rect.y + 8.) * scale,
),
Size::new((rect.width - 20.) * scale, (rect.height - 16.) * scale),
)
.intersection(&Rectangle::new(Point::ORIGIN, viewport))
}
struct Line {
id: Id,
points: Vec<Point>,
@@ -1162,6 +1141,7 @@ struct Scene {
size: Size,
selected: Id,
scale: f32,
scroll_offset: iced::Vector,
camera_pan: iced::Vector,
interaction: Option<(scenario::Context, scenario::CanvasTool, bool)>,
pending_placement: Option<(scenario::Slot, Option<Id>)>,
@@ -1170,7 +1150,6 @@ struct Scene {
rows: BTreeMap<Id, (Id, f32)>,
open_calls: Vec<Point>,
sequence_bounds: BTreeMap<Id, (f32, f32)>,
lifecycle: scenario_lifecycle::render::Rendering,
}
struct SequenceSlot {
slot: scenario::Slot,
@@ -1217,6 +1196,7 @@ impl Scene {
fn sequence(p: &Project, owner: Id, selected: Id) -> Self {
let mut scene = Self {
scale: 1.,
scroll_offset: iced::Vector::ZERO,
camera_pan: iced::Vector::ZERO,
boxes: vec![],
lines: vec![],
@@ -1229,7 +1209,6 @@ impl Scene {
rows: BTreeMap::new(),
open_calls: vec![],
sequence_bounds: BTreeMap::new(),
lifecycle: scenario_lifecycle::render::Rendering::default(),
};
let mut lanes = p
.children(owner)
@@ -1316,17 +1295,7 @@ impl Scene {
}
let mut y = if scenario { 148. } else { 120. };
scene.occurrences(p, owner, scenario, &lane_x, &mut y, 0);
scene.size.height = (y + 45.).max(210.);
if scenario {
scene.lifecycle = scenario_lifecycle::render::Rendering::new(
p,
owner,
&lane_x,
&scene.rows,
&scene.sequence_bounds,
scene.size.height,
);
scene.lifecycle.align_messages(&mut scene.lines);
let calls: Vec<_> = scene
.rows
.keys()
@@ -1351,28 +1320,23 @@ impl Scene {
scene.activation_sequence(p, p.elements[&call].parent.unwrap(), call, x, None);
}
}
scene.size.height = (y + 45.).max(210.);
for (id, name, detail) in lanes {
let x = lane_x[&id];
if !scenario {
scene.lines.insert(
0,
Line {
id,
points: vec![Point::new(x, 85.), Point::new(x, scene.size.height - 20.)],
label: String::new(),
label_at: Point::ORIGIN,
dashed: false,
},
);
}
scene.lines.insert(
0,
Line {
id,
points: vec![Point::new(x, 85.), Point::new(x, scene.size.height - 20.)],
label: String::new(),
label_at: Point::ORIGIN,
dashed: false,
},
);
scene.boxes.push(BoxItem {
id,
rect: Rectangle::new(Point::new(x - 102., 15.), Size::new(204., 70.)),
title: if scenario {
format!("Lane · {name}")
} else {
name
},
title: name,
detail,
marker: None,
});
@@ -1414,16 +1378,6 @@ impl Scene {
let Some(SharedBody::Occurrence { body, .. }) = e.shared() else {
continue;
};
if scenario
&& matches!(body, OccurrenceBody::Exchange {
sender: Some(sender), receiver: Some(receiver),
message_kind: MessageKind::Create | MessageKind::Delete, ..
} if sender == receiver)
{
// Self lifecycle elbows and their caption need clearance from
// the preceding action/state/reference box, before recording rows.
*y += 16.;
}
if ordered {
let slot = scenario::Slot {
parent: owner,
@@ -1643,27 +1597,17 @@ impl Scene {
});
}
}
fn activation_segment(
&mut self,
call: Id,
scope: Id,
receiver: Id,
x: f32,
top: f32,
bottom: f32,
) {
for span in self.lifecycle.alive_spans(receiver, scope, top, bottom) {
self.boxes.push(BoxItem {
id: call,
rect: Rectangle::new(
Point::new(x - 4., span.start),
Size::new(8., span.end - span.start),
),
title: String::new(),
detail: String::new(),
marker: None,
});
fn activation_segment(&mut self, call: Id, x: f32, top: f32, bottom: f32) {
if bottom <= top {
return;
}
self.boxes.push(BoxItem {
id: call,
rect: Rectangle::new(Point::new(x - 4., top), Size::new(8., bottom - top)),
title: String::new(),
detail: String::new(),
marker: None,
});
}
/// Follow a single correlation through the ordered tree. Branches are drawn
/// as independent segments, never as one chronological stretch through ALT
@@ -1676,18 +1620,6 @@ impl Scene {
x: f32,
mut start: Option<f32>,
) -> bool {
let Some(SharedBody::Occurrence {
body:
OccurrenceBody::Exchange {
receiver: Some(receiver),
..
},
..
}) = p.elements[&call].shared()
else {
return false;
};
let receiver = *receiver;
let entered_open = start.is_some();
let mut split = false;
for id in p.elements[&owner].steps().into_iter().flatten() {
@@ -1701,21 +1633,6 @@ impl Scene {
continue;
};
match p.elements[id].shared() {
Some(SharedBody::Occurrence {
body:
OccurrenceBody::Exchange {
message_kind: MessageKind::Delete,
receiver: Some(deleted),
..
},
..
}) if *deleted == receiver => {
// This path cannot show activation beyond its local death.
// Keep the unresolved-call cue; a Delete is not a Reply.
self.activation_segment(call, owner, receiver, x, top, y);
self.open_calls.push(Point::new(x + 12., y - 16.));
start = None;
}
Some(SharedBody::Occurrence {
body:
OccurrenceBody::Exchange {
@@ -1725,19 +1642,14 @@ impl Scene {
},
..
}) if *correlation == call => {
self.activation_segment(call, owner, receiver, x, top, y);
for span in self.lifecycle.alive_spans(receiver, owner, y - 1., y + 1.) {
self.boxes.push(BoxItem {
id: call,
rect: Rectangle::new(
Point::new(x - 7., span.start),
Size::new(14., span.end - span.start),
),
title: String::new(),
detail: String::new(),
marker: None,
});
}
self.activation_segment(call, x, top, y);
self.boxes.push(BoxItem {
id: call,
rect: Rectangle::new(Point::new(x - 7., y - 1.), Size::new(14., 2.)),
title: String::new(),
detail: String::new(),
marker: None,
});
start = None;
}
Some(SharedBody::Occurrence {
@@ -1745,7 +1657,7 @@ impl Scene {
..
}) => {
split = true;
self.activation_segment(call, owner, receiver, x, top, y);
self.activation_segment(call, x, top, y);
let bottom = self
.boxes
.iter()
@@ -1780,7 +1692,7 @@ impl Scene {
} else {
top + 26.
};
self.activation_segment(call, owner, receiver, x, top, bottom);
self.activation_segment(call, x, top, bottom);
self.open_calls.push(Point::new(x + 8., bottom - 7.));
}
start.is_some()
@@ -1813,12 +1725,6 @@ impl Scene {
))
}
fn hit(&self, point: Point) -> Option<Id> {
if !point.x.is_finite() || !point.y.is_finite() {
return None;
}
if let Some(id) = self.lifecycle.marker_at(point) {
return Some(id);
}
self.boxes
.iter()
.rev()
@@ -1833,7 +1739,7 @@ impl Scene {
.iter()
.find(|line| {
!line.label.is_empty()
&& Rectangle::new(
&& (Rectangle::new(
line.label_at,
Size::new(
(line.label.chars().count() as f32 * 6.8 + 8.).min(225.),
@@ -1841,10 +1747,13 @@ impl Scene {
),
)
.contains(point)
|| line.points.windows(2).any(|p| {
crate::diagram::distance_to_segment(point, p[0], p[1])
< 8. / self.scale
}))
})
.map(|line| line.id)
})
.or_else(|| self.near_sequence_item(point))
.or_else(|| {
self.boxes
.iter()
@@ -1852,7 +1761,6 @@ impl Scene {
.find(|b| b.rect.contains(point) && b.title.is_empty())
.map(|b| b.id)
})
.or_else(|| self.lifecycle.lane_at(point, self.scale))
.or_else(|| self.lane_at(point))
.or_else(|| {
self.lines
@@ -1867,34 +1775,6 @@ impl Scene {
.map(|line| line.id)
})
}
fn near_sequence_item(&self, point: Point) -> Option<Id> {
if !self.scale.is_finite() || self.scale <= 0. {
return None;
}
let messages = self
.lines
.iter()
.filter(|line| !line.label.is_empty())
.map(|line| {
let distance = line
.points
.windows(2)
.map(|p| crate::diagram::distance_to_segment(point, p[0], p[1]))
.fold(f32::INFINITY, f32::min);
(line.id, distance)
});
// One distance comparison for both kinds of screen-sized hit tolerance.
// Dense minimum-Fit rows must not give the first message or last marker
// a larger hit priority than the visibly nearer item. UUID breaks ties.
self.lifecycle
.nearest_marker(point)
.into_iter()
.chain(messages)
.filter(|(_, distance)| *distance <= 8. / self.scale)
.min_by(|a, b| a.1.total_cmp(&b.1).then_with(|| a.0.cmp(&b.0)))
.map(|(id, _)| id)
}
}
impl canvas::Program<Message> for Scene {
type State = SequenceInput;
@@ -1928,7 +1808,8 @@ impl canvas::Program<Message> for Scene {
if !editable {
state.cancel();
}
// The viewport masks input outside its visible canvas bounds.
// Scrollable marks a pointer outside its visible viewport unavailable,
// even when it lies inside the full (scrolled) canvas rectangle.
let visible = cursor.position().is_some();
if !visible
&& matches!(
@@ -1951,7 +1832,7 @@ impl canvas::Program<Message> for Scene {
SequencePress::Occurrence { .. } => {}
}
}
mouse::Cursor::Available(*position)
mouse::Cursor::Available(*position + self.scroll_offset)
}
canvas::Event::Mouse(mouse::Event::WheelScrolled { .. })
| canvas::Event::Window(iced::window::Event::Resized(_))
@@ -1963,9 +1844,11 @@ impl canvas::Program<Message> for Scene {
}
canvas::Event::Mouse(
mouse::Event::ButtonPressed(_) | mouse::Event::ButtonReleased(_),
) => state.pointer.map_or(cursor, mouse::Cursor::Available),
) => state
.pointer
.map_or(cursor, |p| mouse::Cursor::Available(p + self.scroll_offset)),
canvas::Event::Window(iced::window::Event::RedrawRequested(_)) => {
state.pointer = cursor.position();
state.pointer = cursor.position().map(|p| p - self.scroll_offset);
cursor
}
canvas::Event::Keyboard(iced::keyboard::Event::KeyPressed {
@@ -2002,7 +1885,7 @@ impl canvas::Program<Message> for Scene {
state.press = Some(SequencePress::Message {
source,
slot,
start: cursor.position()?,
start: cursor.position()? - self.scroll_offset,
first,
moved: false,
});
@@ -2031,7 +1914,7 @@ impl canvas::Program<Message> for Scene {
state.press = Some(SequencePress::Reorder {
occurrence,
parent: self.rows[&occurrence].0,
start: cursor.position()?,
start: cursor.position()? - self.scroll_offset,
moved: false,
});
return Some(Action::request_redraw().and_capture());
@@ -2156,7 +2039,6 @@ impl canvas::Program<Message> for Scene {
f.fill_rectangle(Point::ORIGIN, bounds.size(), Color::WHITE);
f.translate(self.camera_pan);
f.scale(self.scale);
self.lifecycle.draw_lanes(&mut f, self.selected);
for b in &self.boxes {
let outline = matches!(
b.marker,
@@ -2235,32 +2117,24 @@ impl canvas::Program<Message> for Scene {
}
}
for b in &self.boxes {
if let Some(clip) = box_text_clip(self.camera_pan, b.rect, self.scale, f.size()) {
// Clipped child frames start with an identity transform. Match
// the card's pan and scale, as for lifecycle Create labels.
f.with_clip(clip, |label| {
label.translate(self.camera_pan);
label.scale(self.scale);
label.fill_text(Text {
content: short(&b.title, ((b.rect.width - 25.) / 7.) as usize),
position: Point::new(b.rect.x + 12., b.rect.y + 12.),
size: 13.into(),
color: if b.id == self.selected {
style::TEAL
} else {
style::INK
},
..Default::default()
});
if !b.detail.is_empty() {
label.fill_text(Text {
content: short(&b.detail, ((b.rect.width - 25.) / 6.) as usize),
position: Point::new(b.rect.x + 12., b.rect.y + 39.),
size: 11.into(),
color: style::MUTED,
..Default::default()
});
}
f.fill_text(Text {
content: short(&b.title, ((b.rect.width - 25.) / 7.) as usize),
position: Point::new(b.rect.x + 12., b.rect.y + 12.),
size: 13.into(),
color: if b.id == self.selected {
style::TEAL
} else {
style::INK
},
..Default::default()
});
if !b.detail.is_empty() {
f.fill_text(Text {
content: short(&b.detail, ((b.rect.width - 25.) / 6.) as usize),
position: Point::new(b.rect.x + 12., b.rect.y + 39.),
size: 11.into(),
color: style::MUTED,
..Default::default()
});
}
}
@@ -2273,8 +2147,6 @@ impl canvas::Program<Message> for Scene {
..Default::default()
});
}
self.lifecycle
.draw_markers(&mut f, self.selected, self.scale, self.camera_pan);
if let Some((context, tool, editable)) = self.interaction {
let active = editable && state.context == Some(context) && state.tool == tool;
let point = cursor.position_in(bounds).map(|p| {
@@ -2544,26 +2416,25 @@ mod direct_canvas_tests {
}
}
#[test]
fn direct_panned_canvas_maps_ordered_pointer_events_to_the_visible_slot() {
fn direct_scrolled_canvas_maps_raw_pointer_events_to_the_visible_slot() {
let (f, mut scene) = scene();
scene.camera_pan = iced::Vector::new(-35., -140.);
scene.scroll_offset = iced::Vector::new(35., 140.);
let bounds = Rectangle::new(Point::new(20., 230.), scene.size);
let slot = scene.slots.last().unwrap().slot;
let y = scene.slots.last().unwrap().y;
let a = Point::new(bounds.x + scene.lanes[&f.sender], bounds.y + y) + scene.camera_pan;
let b = Point::new(bounds.x + scene.lanes[&f.receiver], bounds.y + y) + scene.camera_pan;
assert!(bounds.contains(a) && bounds.contains(b));
let a = Point::new(bounds.x + scene.lanes[&f.sender], bounds.y + y) - scene.scroll_offset;
let b = Point::new(bounds.x + scene.lanes[&f.receiver], bounds.y + y) - scene.scroll_offset;
let mut state = SequenceInput::default();
// Batched pointer events use their own positions with the final cursor.
// Iced Scrollable passes raw event coordinates and a translated cursor.
for event in [
mouse::Event::CursorMoved { position: a },
mouse::Event::ButtonPressed(mouse::Button::Left),
mouse::Event::CursorMoved { position: b },
] {
send(&scene, &mut state, event, b, bounds);
send(&scene, &mut state, event, b + scene.scroll_offset, bounds);
}
assert!(
matches!(send(&scene, &mut state, mouse::Event::ButtonReleased(mouse::Button::Left), b, bounds), Some(CanvasEvent::Begin { sender, receiver, slot: actual, .. }) if sender == f.sender && receiver == f.receiver && actual == slot)
matches!(send(&scene, &mut state, mouse::Event::ButtonReleased(mouse::Button::Left), b + scene.scroll_offset, bounds), Some(CanvasEvent::Begin { sender, receiver, slot: actual, .. }) if sender == f.sender && receiver == f.receiver && actual == slot)
);
}
#[test]
@@ -63,7 +63,7 @@ pub struct Scope {
pub epoch: u64,
}
impl Scope {
pub(crate) fn current(app: &App) -> Option<Self> {
pub fn current(app: &App) -> Option<Self> {
if let Some(context) = app.scenario_context() {
Some(Self {
document: context.document,
@@ -81,7 +81,7 @@ impl Scope {
})
}
}
pub(crate) fn input_owner(app: &App) -> Option<Self> {
pub fn input_owner(app: &App) -> Option<Self> {
if keyboard_enabled(app)
&& !app.behavior.has_pending()
&& !app.draft_dirty()
@@ -158,12 +158,14 @@ impl<Message: Clone, R: renderer::Renderer> Widget<Message, Theme, R> for Contro
if matches!(
event,
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
) && !layout
.bounds()
.intersection(viewport)
.is_some_and(|bounds| cursor.is_over(bounds))
{
state.focused = false;
) {
if !layout
.bounds()
.intersection(viewport)
.is_some_and(|bounds| cursor.is_over(bounds))
{
state.focused = false;
}
}
}
if matches!(event, Event::Window(iced::window::Event::Unfocused)) {
@@ -93,19 +93,16 @@ pub(crate) struct Mounted {
pub operations: usize,
}
impl Mounted {
pub async fn unshown() -> Self {
pub async fn new(app: &mut App) -> Self {
use iced::advanced::renderer::Headless;
let renderer = <iced::Renderer as Headless>::new(iced::Font::DEFAULT, 16.into(), None)
.await
.expect("production mounted tests require the supported WGPU headless renderer");
Self {
let mut mounted = Self {
renderer,
cache: Cache::new(),
operations: 0,
}
}
pub async fn new(app: &mut App) -> Self {
let mut mounted = Self::unshown().await;
};
mounted.settle(app).await;
mounted
}
@@ -265,8 +262,7 @@ impl Mounted {
mouse::Cursor::Unavailable,
);
self.cache = ui.into_cache();
Headless::screenshot(
&mut self.renderer,
self.renderer.screenshot(
Size::new(app.window_size.width as u32, app.window_size.height as u32),
1.,
crate::style::BG,
@@ -886,7 +886,7 @@ fn contextual_source_incomplete_recovered_kind_reports_error_without_panicking()
}
#[test]
fn contextual_source_empty_palette_lifeline_cannot_open_a_previous_owner_or_layer() {
fn contextual_source_old_empty_action_cannot_open_a_previous_owner_or_layer() {
use crate::app::{App, Message, Page};
let (mut p, a) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
let b = p
@@ -898,32 +898,30 @@ fn contextual_source_empty_palette_lifeline_cannot_open_a_previous_owner_or_laye
app.behavior.active_owner = Some(a);
app.selected = a;
let context = app.scenario_context().unwrap();
let source = app.document.bundle().clone();
let begin = |context| scenario::CanvasEvent::BeginOccurrence {
context,
tool: scenario::Tool::Lifeline,
slot: scenario::Slot {
parent: a,
before: None,
},
lifeline: None,
};
app.behavior.active_owner = Some(b);
app.selected = b;
let _ = app.update(Message::ScenarioCanvas(begin(context)));
let _ = app.update(Message::ScenarioCanvas(scenario::CanvasEvent::First(
context,
scenario::Tool::Lifeline,
)));
assert!(!app.behavior.has_pending());
assert_eq!(app.project(), &p);
app.behavior.active_owner = Some(a);
app.selected = a;
app.layer = Layer::Sa;
let _ = app.update(Message::ScenarioCanvas(begin(context)));
let _ = app.update(Message::ScenarioCanvas(scenario::CanvasEvent::First(
context,
scenario::Tool::Lifeline,
)));
assert!(!app.behavior.has_pending());
app.layer = Layer::Oa;
let current = app.scenario_context().unwrap();
let _ = app.update(Message::ScenarioCanvas(begin(current)));
let _ = app.update(Message::ScenarioCanvas(scenario::CanvasEvent::First(
current,
scenario::Tool::Lifeline,
)));
assert_eq!(app.behavior.pending_owner(), Some(a));
assert_eq!(app.project(), &p);
assert_eq!(app.document.bundle(), &source);
assert!(!app.document.can_undo());
}
+1 -6
View File
@@ -694,9 +694,7 @@ fn picker_view<'a, R: iced::advanced::text::Renderer + 'a>(
Picker::Owner => d.owner,
Picker::Context => d.context,
};
let staged_owner = picker == Picker::Owner && d.source.label(selected).is_some();
let title = match picker {
Picker::Owner if staged_owner => "Or choose an existing participant",
Picker::Owner => "Owned by participant",
Picker::Context => "Scenario context · optional",
};
@@ -707,16 +705,13 @@ fn picker_view<'a, R: iced::advanced::text::Renderer + 'a>(
.padding(6)
]
.spacing(5);
if let Some(id) = selected
&& !staged_owner
{
if let Some(id) = selected {
out = out.push(text(format!("Selected: {}", reference_label(p, id))).size(12));
} else if picker == Picker::Context {
out = out.push(text("No context selected").size(12));
}
if options.is_empty() {
out = out.push(text(match picker {
Picker::Owner if staged_owner => "No existing participants match this search. The new owner above remains selected.",
Picker::Owner => "No matching participant. Refine the search or choose Create new owner.",
Picker::Context => "No matching context. Refine the search, or leave this optional reference empty.",
}).size(12));
+91 -119
View File
@@ -4,7 +4,6 @@ use super::topology::{occurrence, unique_batch};
use super::*;
use iced::widget::{Column, column, text_input};
use std::{cell::RefCell, collections::BTreeSet};
pub(crate) mod focus;
mod workspace;
pub(crate) use workspace::NAME_INPUT;
pub use workspace::{Shelf, view};
@@ -96,6 +95,7 @@ impl std::fmt::Display for CanvasTool {
}
#[derive(Debug, Clone)]
pub enum CanvasEvent {
First(Context, Tool),
Scrolled(Context, ScrollArea, iced::Vector),
Restored(Context, Id, ScrollArea, iced::Vector),
Viewport(Context, Size),
@@ -129,13 +129,15 @@ pub enum CanvasEvent {
}
#[derive(Debug, Clone, Copy)]
pub enum ScrollArea {
// The independent Details/outline scroller; viewport motion uses Camera.
Canvas,
// Retained navigation channel, now the independent Selection/outline shelf.
Page,
}
impl CanvasEvent {
pub fn context(&self) -> Context {
match self {
Self::Scrolled(context, _, _)
Self::First(context, _)
| Self::Scrolled(context, _, _)
| Self::Restored(context, ..)
| Self::Viewport(context, _)
| Self::FormShown(context, _)
@@ -204,13 +206,12 @@ const VALUE_PAGE_SIZE: usize = 24;
pub struct State {
draft: Option<Draft>,
form_session: Id,
form_mount: Id,
form_focus_pending: bool,
canvas_tool: Option<(Id, CanvasTool)>,
call_choices: RefCell<Option<(CallChoiceContext, Vec<Choice>)>>,
details_offset: iced::Vector,
scroll_offsets: [iced::Vector; 2],
return_to: Vec<Caller>,
restore: Option<DetailsRestore>,
restore: Option<ViewportRestore>,
presentation: workspace::Presentation,
}
const CALLER_LIMIT: usize = 32;
@@ -220,14 +221,15 @@ pub(crate) struct Caller {
pub occurrence: Id,
target: Id,
tool: CanvasTool,
details_offset: iced::Vector,
offsets: [iced::Vector; 2],
presentation: workspace::Presentation,
}
struct DetailsRestore {
struct ViewportRestore {
document: Id,
owner: Id,
token: Id,
offset: iced::Vector,
offsets: [iced::Vector; 2],
pending: [bool; 2],
}
#[derive(Clone, Copy)]
pub(super) struct ScrollRestore {
@@ -318,11 +320,7 @@ impl State {
self.draft.as_ref().is_some_and(|d| d.source.is_open())
}
pub fn source_back(&mut self) -> bool {
let returned = self.draft.as_mut().is_some_and(|d| d.source.back());
if returned {
self.remount_form();
}
returned
self.draft.as_mut().is_some_and(|d| d.source.back())
}
pub(crate) fn enter(&mut self, document: Id, owner: Id) {
@@ -332,7 +330,7 @@ impl State {
focused: self.presentation.focused,
..Default::default()
};
self.details_offset = iced::Vector::ZERO;
self.scroll_offsets = [iced::Vector::ZERO; 2];
self.restore = None;
}
}
@@ -392,11 +390,11 @@ impl State {
occurrence,
target,
tool: self.canvas_tool(context.scenario),
details_offset: self.details_offset,
offsets: self.scroll_offsets,
presentation: self.presentation.clone(),
});
self.canvas_tool = Some((target, CanvasTool::Select));
self.details_offset = iced::Vector::ZERO;
self.scroll_offsets = [iced::Vector::ZERO; 2];
self.restore = None;
Ok(target)
}
@@ -423,30 +421,34 @@ impl State {
return;
}
self.canvas_tool = Some((frame.owner, frame.tool));
self.details_offset = frame.details_offset;
self.scroll_offsets = frame.offsets;
self.presentation = frame.presentation;
self.presentation.identity = Some((context.document, context.scenario));
self.restore = Some(DetailsRestore {
self.restore = Some(ViewportRestore {
document: context.document,
owner: frame.owner,
token: Id::new_v4(),
offset: frame.details_offset,
offsets: frame.offsets,
pending: [false, true],
});
}
pub(super) fn scroll_restore(
&self,
context: Context,
ScrollArea::Page: ScrollArea,
area: ScrollArea,
) -> Option<ScrollRestore> {
let restore = self
.restore
.as_ref()
.filter(|r| r.document == context.document && r.owner == context.scenario)?;
let index = match area {
ScrollArea::Canvas => 0,
ScrollArea::Page => 1,
};
let restore = self.restore.as_ref().filter(|r| {
r.document == context.document && r.owner == context.scenario && r.pending[index]
})?;
Some(ScrollRestore {
document: restore.document,
owner: restore.owner,
token: restore.token,
offset: restore.offset,
offset: restore.offsets[index],
})
}
pub(crate) fn restored(
@@ -456,12 +458,22 @@ impl State {
area: ScrollArea,
offset: iced::Vector,
) -> bool {
if !self.restore.as_ref().is_some_and(|r| {
r.document == context.document && r.owner == context.scenario && r.token == token
}) {
let index = match area {
ScrollArea::Canvas => 0,
ScrollArea::Page => 1,
};
let Some(restore) = self.restore.as_mut().filter(|r| {
r.document == context.document
&& r.owner == context.scenario
&& r.token == token
&& r.pending[index]
}) else {
return false;
};
restore.pending[index] = false;
if !restore.pending.contains(&true) {
self.restore = None;
}
self.restore = None;
self.scrolled(area, offset)
}
pub(super) fn recovery_snapshot(&self) -> Result<Option<serde_json::Value>, String> {
@@ -474,11 +486,9 @@ impl State {
pub(super) fn from_recovery(value: serde_json::Value) -> Result<Self, String> {
let draft = serde_json::from_value(value).map_err(|e| e.to_string())?;
let session = Id::new_v4();
Ok(Self {
draft: Some(draft),
form_session: session,
form_mount: session,
form_session: Id::new_v4(),
form_focus_pending: true,
..Default::default()
})
@@ -488,28 +498,8 @@ impl State {
self.draft.as_ref().map(|_| self.form_session)
}
pub(crate) fn form_mount(&self) -> Option<Id> {
self.draft
.as_ref()
.filter(|d| !d.source.is_open())
.map(|_| self.form_mount)
}
pub(crate) fn focus_stamp(&self, context: Context) -> Option<focus::Stamp> {
(self.draft.as_ref()?.owner == context.scenario).then_some(focus::Stamp::new(
context,
self.form_session,
self.form_mount()?,
))
}
fn remount_form(&mut self) {
self.form_mount = Id::new_v4();
self.form_focus_pending = true;
}
pub(crate) fn take_form_focus(&mut self, mount: Id) -> bool {
if self.form_mount() == Some(mount) && self.form_focus_pending {
pub(crate) fn take_form_focus(&mut self, session: Id) -> bool {
if self.form_session() == Some(session) && self.form_focus_pending {
self.form_focus_pending = false;
true
} else {
@@ -517,14 +507,23 @@ impl State {
}
}
pub(crate) fn scrolled(&mut self, ScrollArea::Page: ScrollArea, offset: iced::Vector) -> bool {
pub(crate) fn scrolled(&mut self, area: ScrollArea, offset: iced::Vector) -> bool {
if !offset.x.is_finite() || !offset.y.is_finite() || offset.x < 0. || offset.y < 0. {
return false;
}
let changed = self.details_offset != offset;
self.details_offset = offset;
let index = match area {
ScrollArea::Canvas => 0,
ScrollArea::Page => 1,
};
let changed = self.scroll_offsets[index] != offset;
self.scroll_offsets[index] = offset;
changed
}
#[cfg(test)]
fn scroll_offset(&self) -> iced::Vector {
// The former page scroll now belongs to a sibling outline shelf.
self.scroll_offsets[0]
}
fn call_choices(&self, p: &Project, context: Context, draft: &Draft) -> Vec<Choice> {
let key = CallChoiceContext {
document: context.document,
@@ -674,7 +673,6 @@ impl State {
.source
.dismiss(&event)?
{
self.remount_form();
return Ok(None);
}
let d = self
@@ -715,11 +713,7 @@ impl State {
next.target = Some(id);
next.command(p)?;
}
let returned = d.source.is_open() && !next.source.is_open();
self.draft = Some(next);
if returned {
self.remount_form();
}
return Ok(None);
}
if self.source_open() && !matches!(&event, Event::Cancel) {
@@ -735,7 +729,6 @@ impl State {
return Err("Choose a scenario.".into());
}
self.form_session = Id::new_v4();
self.form_mount = self.form_session;
self.form_focus_pending = true;
self.draft = Some(Draft {
source: Default::default(),
@@ -2052,31 +2045,20 @@ pub(crate) mod direct_tests {
navigation_event(&mut app, |ctx| {
CanvasEvent::Choose(ctx, CanvasTool::Add(Tool::Reuse))
});
navigation_event(&mut app, |ctx| CanvasEvent::Zoom(ctx, None));
let camera_epoch = app.camera_epoch;
navigation_event(&mut app, |ctx| {
CanvasEvent::Camera(
ctx,
camera_epoch,
super::super::canvas_workspace::Event::Pan(iced::Vector::new(25., 170.)),
)
CanvasEvent::Scrolled(ctx, ScrollArea::Canvas, iced::Vector::new(25., 170.))
});
navigation_event(&mut app, |ctx| {
CanvasEvent::Scrolled(ctx, ScrollArea::Page, iced::Vector::new(0., 80.))
});
navigation_event(&mut app, |ctx| CanvasEvent::OpenUse(ctx, caller));
assert_eq!(app.selected, b);
assert_eq!(app.behavior.scenario.presentation.pan, iced::Vector::ZERO);
assert_eq!(app.behavior.scenario.scroll_offset(), iced::Vector::ZERO);
navigation_event(&mut app, |ctx| {
CanvasEvent::Choose(ctx, CanvasTool::Message(MessageStyle::Timer))
});
let camera_epoch = app.camera_epoch;
navigation_event(&mut app, |ctx| {
CanvasEvent::Camera(
ctx,
camera_epoch,
super::super::canvas_workspace::Event::Pan(iced::Vector::new(0., 115.)),
)
CanvasEvent::Scrolled(ctx, ScrollArea::Canvas, iced::Vector::new(0., 115.))
});
navigation_event(&mut app, |ctx| CanvasEvent::OpenUse(ctx, nested));
assert_eq!(app.selected, c);
@@ -2087,7 +2069,7 @@ pub(crate) mod direct_tests {
CanvasTool::Message(MessageStyle::Timer)
);
assert_eq!(
app.behavior.scenario.presentation.pan,
app.behavior.scenario.scroll_offset(),
iced::Vector::new(0., 115.)
);
navigation_event(&mut app, CanvasEvent::Back);
@@ -2098,11 +2080,11 @@ pub(crate) mod direct_tests {
CanvasTool::Add(Tool::Reuse)
);
assert_eq!(
app.behavior.scenario.presentation.pan,
app.behavior.scenario.scroll_offset(),
iced::Vector::new(25., 170.)
);
assert_eq!(
app.behavior.scenario.details_offset,
app.behavior.scenario.scroll_offsets[1],
iced::Vector::new(0., 80.),
"The caller's independent outline scroll is restored separately"
);
@@ -2280,24 +2262,22 @@ pub(crate) mod direct_tests {
let before = app.document.bundle().clone();
let mut scene = Scene::sequence(app.project(), f.owner, f.call);
scene.interaction = Some((context, CanvasTool::Add(tool), true));
scene.camera_pan = iced::Vector::new(23., 80.);
let bounds = Rectangle::new(
Point::new(20., 40.),
Size::new(scene.size.width + 46., scene.size.height + 160.),
);
scene.scroll_offset = iced::Vector::new(23., 80.);
let bounds = Rectangle::new(Point::new(20., 40.), scene.size);
let y = scene.slots.iter().find(|s| s.slot == slot).unwrap().y;
let x = if on_lifeline {
scene.lanes[&f.sender]
} else {
scene.size.width - 35.
};
let point = Point::new(bounds.x + x, bounds.y + y) + scene.camera_pan;
assert!(bounds.contains(point));
let point = Point::new(bounds.x + x, bounds.y + y);
let mut input = SequenceInput::default();
canvas_mouse(
&scene,
&mut input,
mouse::Event::CursorMoved { position: point },
mouse::Event::CursorMoved {
position: point - scene.scroll_offset,
},
point,
bounds,
);
@@ -2473,10 +2453,10 @@ pub(crate) mod direct_tests {
assert!(!app.behavior.has_pending());
}
let old_context = app.scenario_context().unwrap();
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Camera(
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled(
old_context,
app.camera_epoch,
super::super::canvas_workspace::Event::Pan(iced::Vector::new(0., 100.)),
ScrollArea::Canvas,
iced::Vector::new(0., 100.),
)));
let _ = app.update(Message::ScenarioCanvas(event(old_context)));
assert!(!app.behavior.has_pending());
@@ -2667,29 +2647,26 @@ pub(crate) mod direct_tests {
);
}
#[test]
fn direct_busy_save_keeps_details_current_and_camera_for_the_next_pointer_gesture() {
fn direct_busy_save_keeps_observed_scroll_current_for_the_next_pointer_gesture() {
let f = fixture();
let mut app = App::with_project(f.project.clone());
let _ = app.update(Message::Select(f.owner));
let _ = app.update(Message::Page(crate::app::Page::Behavior));
let context = app.scenario_context().unwrap();
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Camera(
context,
app.camera_epoch,
super::super::canvas_workspace::Event::Pan(iced::Vector::new(-25., -100.)),
)));
let context = app.scenario_context().unwrap();
let original = app.document.snapshot();
app.busy = true;
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled(
context,
ScrollArea::Page,
iced::Vector::new(0., 40.),
)));
for (area, offset) in [
(ScrollArea::Canvas, iced::Vector::new(25., 100.)),
(ScrollArea::Page, iced::Vector::new(0., 40.)),
] {
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled(
context, area, offset,
)));
}
assert!(app.busy);
assert_eq!(
app.behavior.scenario.details_offset,
iced::Vector::new(0., 40.)
app.behavior.scenario.scroll_offset(),
iced::Vector::new(25., 100.)
);
assert_ne!(
app.scenario_context(),
@@ -2700,14 +2677,14 @@ pub(crate) mod direct_tests {
stale.document = Id::new_v4();
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled(
stale,
ScrollArea::Page,
ScrollArea::Canvas,
iced::Vector::new(0., 900.),
)));
assert_eq!(
app.behavior.scenario.details_offset,
iced::Vector::new(0., 40.)
app.behavior.scenario.scroll_offset(),
iced::Vector::new(25., 100.)
);
let root = std::path::PathBuf::from("scenario-details-save-fixture");
let root = std::path::PathBuf::from("scenario-scroll-save-fixture");
let receipt = crate::app::FileSnapshot {
document: context.document,
revision: context.revision,
@@ -2728,17 +2705,12 @@ pub(crate) mod direct_tests {
let context = app.scenario_context().unwrap();
let mut scene = Scene::sequence(app.project(), f.owner, f.call);
scene.interaction = Some((context, CanvasTool::Message(MessageStyle::CallReply), true));
assert_eq!(
app.behavior.scenario.presentation.pan,
iced::Vector::new(-25., -100.)
);
scene.camera_pan = app.behavior.scenario.presentation.pan;
scene.scroll_offset = app.behavior.scenario.scroll_offset();
let slot = scene.slots.last().unwrap().slot;
let bounds = Rectangle::new(Point::new(20., 250.), scene.size);
let y = bounds.y + scene.slots.last().unwrap().y;
let a = Point::new(bounds.x + scene.lanes[&f.sender], y) + scene.camera_pan;
let b = Point::new(bounds.x + scene.lanes[&f.receiver], y) + scene.camera_pan;
assert!(bounds.contains(a) && bounds.contains(b));
let a = Point::new(bounds.x + scene.lanes[&f.sender], y) - scene.scroll_offset;
let b = Point::new(bounds.x + scene.lanes[&f.receiver], y) - scene.scroll_offset;
let mut input = super::super::SequenceInput::default();
let mut published = vec![];
for event in [
@@ -2752,7 +2724,7 @@ pub(crate) mod direct_tests {
&mut input,
&canvas::Event::Mouse(event),
bounds,
mouse::Cursor::Available(b),
mouse::Cursor::Available(b + scene.scroll_offset),
)
.and_then(|action| action.into_inner().0)
{
@@ -1,334 +0,0 @@
//! A delayed focus request must still own the live parent tree when executed.
//! Collect first: a missing target must not unfocus unrelated fields.
use super::Context;
use iced::advanced::widget::{Id, Operation, Tree, operation};
use iced::advanced::{Clipboard, Layout, Shell, Widget, layout, overlay, renderer};
use iced::{Element, Event, Length, Rectangle, Size, Task, Theme, Vector, mouse};
use oa_core::Id as ModelId;
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Stamp {
document: ModelId,
owner: ModelId,
revision: u64,
session: ModelId,
mount: ModelId,
}
impl Stamp {
pub(crate) fn new(context: Context, session: ModelId, mount: ModelId) -> Self {
Self {
document: context.document,
owner: context.scenario,
revision: context.revision,
session,
mount,
}
}
}
pub(crate) fn name_id(stamp: Stamp) -> Id {
Id::from(format!(
"{}:{}:{}:{}:{}:{}",
super::NAME_INPUT,
stamp.document,
stamp.owner,
stamp.revision,
stamp.session,
stamp.mount
))
}
struct Owner(Option<Stamp>);
struct Root<'a, Message, R: renderer::Renderer> {
content: Element<'a, Message, Theme, R>,
owner: Option<Stamp>,
}
pub(crate) fn root<'a, Message: 'a, R: renderer::Renderer + 'a>(
content: impl Into<Element<'a, Message, Theme, R>>,
owner: Option<Stamp>,
) -> Element<'a, Message, Theme, R> {
Element::new(Root {
content: content.into(),
owner,
})
}
impl<Message, R: renderer::Renderer> Widget<Message, Theme, R> for Root<'_, Message, R> {
fn children(&self) -> Vec<Tree> {
vec![Tree::new(self.content.as_widget())]
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children(std::slice::from_ref(&self.content));
}
fn size(&self) -> Size<Length> {
self.content.as_widget().size()
}
fn layout(&mut self, tree: &mut Tree, renderer: &R, limits: &layout::Limits) -> layout::Node {
self.content
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &R,
op: &mut dyn Operation,
) {
// This is computed from the current App view, never retained widget state.
op.custom(None, layout.bounds(), &mut Owner(self.owner));
self.content
.as_widget_mut()
.operate(&mut tree.children[0], layout, renderer, op);
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &R,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
fn draw(
&self,
tree: &Tree,
renderer: &mut R,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout,
cursor,
viewport,
);
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &R,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor,
viewport,
renderer,
)
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &R,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, R>> {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
viewport,
translation,
)
}
}
#[derive(Clone)]
struct Scroll {
index: usize,
bounds: Rectangle,
capacity: Vector,
offset: Vector,
}
struct Collect {
stamp: Stamp,
valid: bool,
translation: Vector,
scrolls: Vec<Scroll>,
pending: Option<Scroll>,
index: usize,
target: Option<(Rectangle, Vec<Scroll>)>,
}
pub(crate) fn task<Message: Send + 'static>(stamp: Stamp) -> Task<Message> {
iced::advanced::widget::operate(Collect {
stamp,
valid: false,
translation: Vector::ZERO,
scrolls: vec![],
pending: None,
index: 0,
target: None,
})
.discard()
}
impl Operation for Collect {
fn custom(&mut self, _: Option<&Id>, _: Rectangle, state: &mut dyn std::any::Any) {
if let Some(owner) = state.downcast_ref::<Owner>() {
self.valid = owner.0 == Some(self.stamp);
}
}
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation)) {
let previous = (self.translation, self.scrolls.len());
if let Some(scroll) = self.pending.take() {
self.translation += scroll.offset;
self.scrolls.push(scroll);
}
visit(self);
self.translation = previous.0;
self.scrolls.truncate(previous.1);
}
fn scrollable(
&mut self,
_: Option<&Id>,
bounds: Rectangle,
content: Rectangle,
offset: Vector,
_: &mut dyn operation::Scrollable,
) {
self.pending = Some(Scroll {
index: self.index,
bounds: translated(bounds, self.translation),
capacity: Vector::new(
(content.width - bounds.width).max(0.),
(content.height - bounds.height).max(0.),
),
offset,
});
self.index += 1;
}
fn focusable(&mut self, id: Option<&Id>, bounds: Rectangle, _: &mut dyn operation::Focusable) {
if id == Some(&name_id(self.stamp)) {
self.target = Some((translated(bounds, self.translation), self.scrolls.clone()));
}
}
fn finish(&self) -> operation::Outcome<()> {
if !self.valid {
return operation::Outcome::None;
}
let Some((mut bounds, ancestors)) = self.target.clone() else {
return operation::Outcome::None;
};
let mut shifts = BTreeMap::new();
for ancestor in ancestors.iter().rev() {
let dx = reveal(
bounds.x,
bounds.width,
ancestor.bounds.x,
ancestor.bounds.width,
);
let dy = reveal(
bounds.y,
bounds.height,
ancestor.bounds.y,
ancestor.bounds.height,
);
// Drawing offsets are rounded; an already-visible axis must not
// acquire a scroll delta from a fractional capacity alone.
let delta = Vector::new(
if dx == 0. {
0.
} else {
(ancestor.offset.x + dx).clamp(0., ancestor.capacity.x) - ancestor.offset.x
},
if dy == 0. {
0.
} else {
(ancestor.offset.y + dy).clamp(0., ancestor.capacity.y) - ancestor.offset.y
},
);
if delta != Vector::ZERO {
shifts.insert(ancestor.index, delta);
}
bounds = translated(bounds, delta);
}
operation::Outcome::Chain(Box::new(Apply {
stamp: self.stamp,
valid: false,
shifts,
index: 0,
}))
}
}
fn translated(bounds: Rectangle, delta: Vector) -> Rectangle {
Rectangle {
x: bounds.x - delta.x,
y: bounds.y - delta.y,
..bounds
}
}
fn reveal(start: f32, length: f32, viewport: f32, size: f32) -> f32 {
if start < viewport || length > size {
start - viewport
} else {
(start + length - viewport - size).max(0.)
}
}
struct Apply {
stamp: Stamp,
valid: bool,
shifts: BTreeMap<usize, Vector>,
index: usize,
}
impl Operation for Apply {
fn custom(&mut self, _: Option<&Id>, _: Rectangle, state: &mut dyn std::any::Any) {
if let Some(owner) = state.downcast_ref::<Owner>() {
self.valid = owner.0 == Some(self.stamp);
}
}
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation)) {
visit(self);
}
fn focusable(&mut self, id: Option<&Id>, _: Rectangle, state: &mut dyn operation::Focusable) {
if self.valid {
if id == Some(&name_id(self.stamp)) {
state.focus();
} else {
state.unfocus();
}
}
}
fn scrollable(
&mut self,
_: Option<&Id>,
bounds: Rectangle,
content: Rectangle,
_: Vector,
state: &mut dyn operation::Scrollable,
) {
if self.valid
&& let Some(delta) = self.shifts.get(&self.index)
{
state.scroll_by(
operation::scrollable::AbsoluteOffset {
x: delta.x,
y: delta.y,
},
bounds,
content,
);
}
self.index += 1;
}
}
@@ -1,5 +1,5 @@
//! Scenario presentation. Layout/camera controls never construct model commands.
use super::super::{form_session, scenario_scroll};
use super::super::scenario_scroll;
use super::*;
use iced::widget::{Space, column, sensor};
@@ -7,6 +7,7 @@ pub(crate) const NAME_INPUT: &str = "scenario-form-name";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Shelf {
Tools,
Selection,
}
@@ -279,9 +280,7 @@ pub fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Element<'a
let mut form = column![
text(d.tool.label()).size(16),
text_input("Name (defaults to selected source)", &d.name)
.id(focus::name_id(
state.focus_stamp(context).expect("visible parent form")
))
.id(NAME_INPUT)
.on_input(|s| event(Event::Name(s)))
.padding(9)
.size(12)
@@ -560,10 +559,9 @@ pub fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Element<'a
]
.spacing(7);
let session = state.form_session().expect("visible scenario draft");
let mount = state.form_mount().expect("visible parent form");
form_panel = sensor(editor_panel(session, footer.into(), form.into()))
.key(mount)
.on_show(move |_| Message::ScenarioCanvas(CanvasEvent::FormShown(context, mount)))
.key(session)
.on_show(move |_| Message::ScenarioCanvas(CanvasEvent::FormShown(context, session)))
.into();
}
}
@@ -594,7 +592,6 @@ fn details_pane<'a, R: iced::advanced::text::Renderer + 'a>(
scenario_scroll::observe(
context,
ScrollArea::Page,
false,
restore,
scrollable(details)
.id("scenario-details")
@@ -611,7 +608,7 @@ fn editor_panel<'a, R: iced::advanced::text::Renderer + 'a>(
container(
column![
container(actions).id("scenario-actions").width(Fill),
form_session::reset_on_change(
super::super::form_session::reset_on_change(
session,
scrollable(fields)
.id("scenario-form")
File diff suppressed because it is too large Load Diff
@@ -2,8 +2,6 @@
use oa_core::{FragmentOperator, Id, Kind, MessageKind, OccurrenceBody, Project, SharedBody};
use std::collections::{BTreeMap, BTreeSet};
pub(super) mod render;
/// Maps already recorded by the sequence renderer. Rows carry their owning sequence.
pub(crate) struct Geometry<'a> {
pub rows: &'a BTreeMap<Id, (Id, f32)>,
@@ -1,392 +0,0 @@
//! Scene bridge for local lifetime geometry; no authoring or model mutations.
use super::{Geometry, LaneLifetime, Life, Marker, MarkerKind, Span, project_lifetimes};
use crate::{behavior::Line, style, views::short};
use iced::{
Color, Point, Rectangle, Size,
widget::canvas::{self, Frame, Path, Stroke, Text},
};
use oa_core::{Id, OccurrenceBody, Project, SharedBody};
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq)]
struct EventMarker {
event: Marker,
lifeline: Id,
center: Point,
capsule: Rectangle,
name: String,
sender_x: Option<f32>,
}
#[derive(Debug, Default, Clone, PartialEq)]
pub(in crate::behavior) struct Rendering {
// Complete rendering positions, including warning-admitted unassigned lanes.
// Scene.lanes independently retains the existing authoring eligibility rule.
lanes: BTreeMap<Id, f32>,
span: Option<Span>,
lifetimes: Vec<LaneLifetime>,
markers: Vec<EventMarker>,
}
impl Rendering {
pub(in crate::behavior) fn new(
project: &Project,
owner: Id,
lanes: &BTreeMap<Id, f32>,
rows: &BTreeMap<Id, (Id, f32)>,
insertion_scopes: &BTreeMap<Id, (f32, f32)>,
height: f32,
) -> Self {
let span = Span {
start: 85.,
end: height - 20.,
};
if !span.valid() {
return Self::default();
}
// Insertion scopes deliberately start later and end earlier. Empty and
// Create-only scenarios still have the full visible lifetime extent.
let mut scopes = insertion_scopes.clone();
for (scope, (start, end)) in &mut scopes {
if *scope != owner && *start == *end && start.is_finite() {
// An empty operand has 42 px between its insertion axis and
// frame end. Match the existing 26 px open-activation allowance.
*end = *start + 26.;
}
}
scopes.insert(owner, (span.start, span.end));
let lifetimes = project_lifetimes(
project,
owner,
&Geometry {
rows,
scopes: &scopes,
},
);
let lanes: BTreeMap<_, _> = lanes
.iter()
.filter(|(_, x)| x.is_finite())
.map(|(id, x)| (*id, *x))
.collect();
let mut markers = Vec::new();
for lane in &lifetimes {
let Some(&x) = lanes.get(&lane.lifeline) else {
continue;
};
for event in &lane.markers {
let sender_x = project
.elements
.get(&event.occurrence)
.and_then(|e| e.shared())
.and_then(|body| match body {
SharedBody::Occurrence {
body:
OccurrenceBody::Exchange {
sender: Some(sender),
..
},
..
} => lanes.get(sender).copied(),
_ => None,
});
markers.push(EventMarker {
event: *event,
lifeline: lane.lifeline,
center: Point::new(x, event.y),
capsule: Rectangle::new(
Point::new(x - 70., event.y - 14.),
Size::new(140., 28.),
),
name: project.named(lane.lifeline).to_owned(),
sender_x,
});
}
}
Self {
lanes,
span: Some(span),
lifetimes,
markers,
}
}
pub(in crate::behavior) fn align_messages(&self, lines: &mut [Line]) {
for marker in &self.markers {
let Some(sender) = marker.sender_x else {
continue;
};
let Some(line) = lines
.iter_mut()
.find(|line| line.id == marker.event.occurrence)
else {
continue;
};
let x = marker.center.x;
let y = marker.center.y;
let self_message = sender == x;
let end_x = match marker.event.kind {
MarkerKind::Birth if sender < x => marker.capsule.x,
MarkerKind::Birth => marker.capsule.x + marker.capsule.width,
MarkerKind::Death => x,
};
line.points = if self_message {
// The receiver is on the recorded occurrence row, including self
// lifecycle events. The elbow stays outside the birth capsule.
let elbow = x + 112.;
line.label_at = Point::new(x + 12., y - 52.);
vec![
Point::new(x, y - 24.),
Point::new(elbow, y - 24.),
Point::new(elbow, y),
Point::new(end_x, y),
]
} else {
if marker.event.kind == MarkerKind::Birth {
// Keep the label above the capsule, including right-to-left
// messages whose existing label starts beside the receiver.
line.label_at.y = y - 40.;
}
vec![Point::new(sender, y), Point::new(end_x, y)]
};
}
}
pub(in crate::behavior) fn alive_spans(
&self,
lifeline: Id,
scope: Id,
top: f32,
bottom: f32,
) -> Vec<Span> {
if !top.is_finite() || !bottom.is_finite() || bottom <= top {
return vec![];
}
self.lifetimes
.iter()
.find(|lane| lane.lifeline == lifeline)
.into_iter()
.flat_map(|lane| &lane.intervals)
.filter(|interval| interval.scope == scope && interval.life == Life::Alive)
.filter_map(|interval| {
let start = top.max(interval.span.start);
let end = bottom.min(interval.span.end);
(start < end).then_some(Span { start, end })
})
.collect()
}
pub(in crate::behavior) fn marker_at(&self, point: Point) -> Option<Id> {
self.markers
.iter()
.rev()
.find(|marker| match marker.event.kind {
MarkerKind::Birth => marker.capsule.contains(point),
MarkerKind::Death => marker_distance(marker, point) <= 1.25,
})
.map(|marker| marker.event.occurrence)
}
pub(in crate::behavior) fn nearest_marker(&self, point: Point) -> Option<(Id, f32)> {
if !point.x.is_finite() || !point.y.is_finite() {
return None;
}
// Exact visible shapes are tested first by Scene. A low-zoom tolerance
// must not outrank another marker's actual capsule, a header, or a label.
self.markers
.iter()
.map(|marker| (marker.event.occurrence, marker_distance(marker, point)))
.min_by(|a, b| a.1.total_cmp(&b.1).then_with(|| a.0.cmp(&b.0)))
}
pub(in crate::behavior) fn lane_at(&self, point: Point, scale: f32) -> Option<Id> {
let span = self.span?;
if !scale.is_finite() || scale <= 0. || point.y < span.start || point.y > span.end {
return None;
}
self.lanes
.iter()
.filter(|(_, x)| (**x - point.x).abs() < 12. / scale)
.min_by(|a, b| (a.1 - point.x).abs().total_cmp(&(b.1 - point.x).abs()))
.map(|(id, _)| *id)
}
pub(in crate::behavior) fn draw_lanes(&self, frame: &mut Frame, selected: Id) {
let Some(span) = self.span else {
return;
};
for (&id, &x) in &self.lanes {
frame.stroke(
&Path::line(Point::new(x, span.start), Point::new(x, span.end)),
Stroke {
line_dash: canvas::LineDash {
segments: &[2., 5.],
offset: 0,
},
..Stroke::default()
.with_width(1.)
.with_color(if id == selected {
Color::from_rgb8(148, 190, 187)
} else {
Color::from_rgb8(214, 222, 222)
})
},
);
}
for lifetime in &self.lifetimes {
let Some(&x) = self.lanes.get(&lifetime.lifeline) else {
continue;
};
for interval in &lifetime.intervals {
match interval.life {
Life::Alive => frame.stroke(
&Path::line(
Point::new(x, interval.span.start),
Point::new(x, interval.span.end),
),
Stroke::default().with_width(2.).with_color(
if lifetime.lifeline == selected {
style::TEAL
} else {
Color::from_rgb8(91, 113, 113)
},
),
),
Life::Unresolved if interval.span.end - interval.span.start >= 16. => {
frame.fill_text(Text {
content: "?".into(),
position: Point::new(x + 9., interval.span.start + 2.),
size: 11.into(),
color: style::MUTED,
..Default::default()
});
}
_ => {}
}
}
}
if !self.lanes.is_empty() {
frame.fill_text(Text {
content:
"Dotted: editing guide · Solid: local lifetime · ?: lifetime depends on path"
.into(),
position: Point::new(28., span.end + 4.),
size: 10.into(),
color: style::MUTED,
..Default::default()
});
}
}
pub(in crate::behavior) fn draw_markers(
&self,
frame: &mut Frame,
selected: Id,
scale: f32,
pan: iced::Vector,
) {
for marker in &self.markers {
let color = if selected == marker.event.occurrence || selected == marker.lifeline {
style::TEAL
} else {
style::INK
};
match marker.event.kind {
MarkerKind::Birth => {
let path = Path::rounded_rectangle(
marker.capsule.position(),
marker.capsule.size(),
6.into(),
);
frame.fill(&path, Color::from_rgb8(243, 248, 248));
frame.stroke(
&path,
Stroke::default().with_color(color).with_width(
if selected == marker.event.occurrence {
2.5
} else {
1.5
},
),
);
if let Some(clip) = birth_label_clip(pan, marker.capsule, scale, frame.size()) {
// WGPU clipped frames start with an identity transform;
// neither the parent's pan/scale nor its clip is inherited.
frame.with_clip(clip, |label| {
label.translate(pan);
label.scale(scale);
label.fill_text(Text {
content: short(&marker.name, 18),
position: marker.capsule.position() + iced::Vector::new(8., 6.),
size: 12.into(),
color,
..Default::default()
});
});
}
}
MarkerKind::Death => {
for (a, b) in death_strokes(marker.center) {
frame.stroke(
&Path::line(a, b),
Stroke::default().with_color(color).with_width(2.5),
);
}
}
}
}
}
}
fn birth_label_clip(
pan: iced::Vector,
capsule: Rectangle,
scale: f32,
viewport: Size,
) -> Option<Rectangle> {
if !pan.x.is_finite() || !pan.y.is_finite() || !scale.is_finite() || scale <= 0. {
return None;
}
Rectangle::new(
Point::new(
pan.x + (capsule.x + 8.) * scale,
pan.y + (capsule.y + 4.) * scale,
),
Size::new((capsule.width - 16.) * scale, (capsule.height - 8.) * scale),
)
.intersection(&Rectangle::new(Point::ORIGIN, viewport))
}
fn marker_distance(marker: &EventMarker, point: Point) -> f32 {
match marker.event.kind {
MarkerKind::Birth => {
let dx = (marker.capsule.x - point.x)
.max(point.x - marker.capsule.x - marker.capsule.width)
.max(0.);
let dy = (marker.capsule.y - point.y)
.max(point.y - marker.capsule.y - marker.capsule.height)
.max(0.);
dx.hypot(dy)
}
MarkerKind::Death => death_strokes(marker.center)
.iter()
.map(|(a, b)| crate::diagram::distance_to_segment(point, *a, *b))
.fold(f32::INFINITY, f32::min),
}
}
fn death_strokes(center: Point) -> [(Point, Point); 2] {
[
(
center + iced::Vector::new(-7., -7.),
center + iced::Vector::new(7., 7.),
),
(
center + iced::Vector::new(-7., 7.),
center + iced::Vector::new(7., -7.),
),
]
}
#[cfg(test)]
#[path = "render_tests.rs"]
mod tests;
@@ -1,973 +0,0 @@
use super::*;
use crate::{behavior::Scene, document::Document};
use iced::Size;
use oa_core::*;
struct Fixture {
project: Project,
owner: Id,
sender: Id,
receiver: Id,
work: Id,
exchange: Id,
}
impl Fixture {
fn new(layer: Layer) -> Self {
let mut project = Project::empty();
let kind = if layer == Layer::Oa {
Kind::Activity
} else {
Kind::Function
};
let a = project.add_element(kind, layer, "Request", None).unwrap();
let work = project.add_element(kind, layer, "Provide", None).unwrap();
let exchange = project.relate(
if layer == Layer::Oa {
RelationKind::Interaction
} else {
RelationKind::FunctionalExchange
},
"Service",
a,
work,
);
let owner = project
.add_element(Kind::Scenario, layer, "Lifecycle", None)
.unwrap();
let mut lane = |name, represented| {
let id = project
.add_element(Kind::Lifeline, layer, name, Some(owner))
.unwrap();
*project.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
represented: Some(represented),
};
id
};
let sender = lane("Requester", a);
let receiver = lane("Provider", work);
Self {
project,
owner,
sender,
receiver,
work,
exchange,
}
}
fn occurrence(&mut self, parent: Id, name: &str, body: OccurrenceBody) -> Id {
let mut element = self
.project
.make_element(
Kind::Occurrence,
self.project.elements[&parent].layer(),
name,
Some(parent),
)
.unwrap();
let id = element.id;
*element.shared_mut().unwrap() = SharedBody::Occurrence {
body,
precondition: String::new(),
postcondition: String::new(),
};
self.project.elements.insert(id, element);
let parent = self.project.elements.get_mut(&parent).unwrap();
if let Some(steps) = parent.steps_mut() {
steps.push(id);
} else if let Some(SharedBody::Occurrence {
body: OccurrenceBody::Fragment { branches, .. },
..
}) = parent.shared_mut()
{
branches.push(id);
} else {
panic!("fixture parent must own ordered occurrences or operands");
}
id
}
fn message(&mut self, parent: Id, kind: MessageKind) -> Id {
self.occurrence(
parent,
"Message",
OccurrenceBody::Exchange {
exchange: Some(self.exchange),
sender: Some(self.sender),
receiver: Some(self.receiver),
message_kind: kind,
operation: None,
reply_to: None,
values: vec![],
},
)
}
fn fragment(&mut self, parent: Id, operator: FragmentOperator) -> Id {
self.occurrence(
parent,
"Paths",
OccurrenceBody::Fragment {
operator,
branches: vec![],
},
)
}
fn branch(&mut self, fragment: Id) -> Id {
self.occurrence(
fragment,
"Operand",
OccurrenceBody::Branch {
guard: "condition".into(),
occurrences: vec![],
},
)
}
fn document(&self) -> Document {
validate_candidate(&self.project).expect("ordinary core admission must succeed");
let mut bundle = source_project::format_bundle(&self.project);
for text in bundle.files.values_mut() {
*text = format!(
"// Retain exact authored CRLF and Unicode Ω\r\n{}",
text.replace('\n', "\r\n")
);
}
let document =
Document::from_bundle(bundle.clone(), source_project::ProjectLimits::default())
.unwrap();
assert_eq!(document.project(), &self.project);
assert_eq!(document.bundle(), &bundle);
document
}
fn scene(&self) -> Scene {
let document = self.document();
let before = document.snapshot();
let scene = Scene::sequence(document.project(), self.owner, self.owner);
assert_eq!(document.project(), &before.decoded.project);
assert_eq!(document.bundle(), &before.bundle);
assert_eq!(document.revision(), 0);
assert!(!document.can_undo() && !document.can_redo() && !document.dirty());
assert_eq!(
scene.lifecycle,
Scene::sequence(document.project(), self.owner, self.owner).lifecycle
);
assert!(
scene
.lines
.iter()
.flat_map(|line| &line.points)
.all(|p| p.x.is_finite() && p.y.is_finite())
);
scene
}
}
fn intervals(scene: &Scene, lifeline: Id, scope: Id) -> Vec<(f32, f32, Life)> {
scene
.lifecycle
.lifetimes
.iter()
.find(|lane| lane.lifeline == lifeline)
.unwrap()
.intervals
.iter()
.filter(|i| i.scope == scope)
.map(|i| (i.span.start, i.span.end, i.life))
.collect()
}
#[test]
fn real_scene_long_header_title_and_detail_keep_names_and_have_bounded_clips() {
for layer in [Layer::Oa, Layer::Sa] {
for name in [
"W".repeat(18),
"界".repeat(24),
"e\u{301}".repeat(24),
"👩\u{200d}🔧".repeat(12),
] {
let mut f = Fixture::new(layer);
f.project.elements.get_mut(&f.receiver).unwrap().name = name.clone();
f.project.elements.get_mut(&f.work).unwrap().name = name.clone();
let scene = f.scene();
let header = scene.boxes.iter().find(|b| b.id == f.receiver).unwrap();
assert_eq!(header.title, format!("Lane · {name}"));
assert_eq!(header.detail, name);
assert_eq!(header.rect.size(), Size::new(204., 70.));
assert_eq!(scene.hit(header.rect.center()), Some(f.receiver));
// Scroll offset is handled by the existing outer canvas/scrollable;
// these are the local drawing frame's coordinates, not screen input.
for scale in [0.05, 0.5, 0.512, 0.64, 0.8, 1., 1.953125, 2.] {
let clip = crate::behavior::box_text_clip(
iced::Vector::ZERO,
header.rect,
scale,
Size::new(2000., 2000.),
)
.unwrap();
let world = Rectangle::new(
Point::new(clip.x / scale, clip.y / scale),
Size::new(clip.width / scale, clip.height / scale),
);
assert!((world.x - header.rect.x - 10.).abs() < 0.001);
assert!((world.y - header.rect.y - 8.).abs() < 0.001);
assert!((world.width - 184.).abs() < 0.001);
assert!((world.height - 54.).abs() < 0.001);
assert!(header.rect.contains(world.position()));
assert!(
header
.rect
.contains(Point::new(world.x + world.width, world.y + world.height,))
);
}
}
}
}
#[test]
fn box_text_clip_intersects_parent_viewport_and_rejects_empty_interiors() {
use crate::behavior::box_text_clip;
let card = Rectangle::new(Point::new(100., 200.), Size::new(204., 70.));
assert_eq!(
box_text_clip(iced::Vector::ZERO, card, 2., Size::new(300., 500.)),
Some(Rectangle::new(Point::new(220., 416.), Size::new(80., 84.)))
);
assert_eq!(
box_text_clip(
iced::Vector::ZERO,
Rectangle::new(Point::new(-20., -10.), card.size()),
2.,
Size::new(300., 500.),
),
Some(Rectangle::new(Point::ORIGIN, Size::new(300., 104.)))
);
for viewport in [Size::new(200., 500.), Size::new(300., 400.), Size::ZERO] {
assert_eq!(box_text_clip(iced::Vector::ZERO, card, 2., viewport), None);
}
for scale in [0., -1., f32::NAN, f32::INFINITY] {
assert_eq!(
box_text_clip(iced::Vector::ZERO, card, scale, Size::new(1000., 1000.)),
None
);
}
for size in [Size::new(20., 70.), Size::new(204., 16.), Size::ZERO] {
assert_eq!(
box_text_clip(
iced::Vector::ZERO,
Rectangle::new(Point::ORIGIN, size),
1.,
card.size()
),
None
);
}
}
#[test]
fn real_scene_wide_latin_header_exceeds_old_character_budget() {
use iced::advanced::text::{self, Paragraph as _};
let mut f = Fixture::new(Layer::Oa);
f.project.elements.get_mut(&f.receiver).unwrap().name = "W".repeat(18);
f.project.elements.get_mut(&f.work).unwrap().name = "W".repeat(24);
let scene = f.scene();
let header = scene.boxes.iter().find(|b| b.id == f.receiver).unwrap();
for (content, size, divisor) in [(&header.title, 13., 7.), (&header.detail, 11., 6.)] {
let label = iced::widget::canvas::Text {
content: crate::views::short(content, ((header.rect.width - 25.) / divisor) as usize),
size: size.into(),
..Default::default()
};
// Use the same real paragraph/font/shaping defaults as canvas Text.
// The native defect survives short(): glyph width, not scalar count,
// is what exceeds the card. This is a font witness, not a pixel gate.
let paragraph = <iced::Renderer as text::Renderer>::Paragraph::with_text(text::Text {
content: label.content.as_str(),
bounds: Size::INFINITE,
size: label.size,
line_height: label.line_height,
font: label.font,
align_x: label.align_x,
align_y: label.align_y,
shaping: label.shaping,
wrapping: text::Wrapping::default(),
});
let old_available = header.rect.width - 12.;
assert!(
paragraph.min_width() > old_available,
"wide glyph fixture must overflow the old unbounded card: {} <= {old_available}",
paragraph.min_width()
);
}
}
#[test]
fn birth_label_clip_keeps_padded_world_bounds_at_each_scene_scale() {
let capsule = Rectangle::new(Point::new(100., 200.), Size::new(140., 28.));
for (scale, expected) in [
(
0.05,
Rectangle::new(Point::new(5.4, 10.2), Size::new(6.2, 1.)),
),
(
0.5,
Rectangle::new(Point::new(54., 102.), Size::new(62., 10.)),
),
(
1.,
Rectangle::new(Point::new(108., 204.), Size::new(124., 20.)),
),
(
2.,
Rectangle::new(Point::new(216., 408.), Size::new(248., 40.)),
),
] {
let clip =
birth_label_clip(iced::Vector::ZERO, capsule, scale, Size::new(1000., 1000.)).unwrap();
for (actual, expected) in [
(clip.x, expected.x),
(clip.y, expected.y),
(clip.width, expected.width),
(clip.height, expected.height),
] {
assert!((actual - expected).abs() < 0.0001);
}
assert!(capsule.contains(Point::new(clip.x / scale, clip.y / scale)));
assert!(capsule.contains(Point::new(
(clip.x + clip.width) / scale,
(clip.y + clip.height) / scale,
)));
}
}
#[test]
fn birth_label_clip_preserves_parent_viewport_bounds() {
let capsule = Rectangle::new(Point::new(100., 200.), Size::new(140., 28.));
assert_eq!(
birth_label_clip(iced::Vector::ZERO, capsule, 2., Size::new(300., 420.)),
Some(Rectangle::new(Point::new(216., 408.), Size::new(84., 12.)))
);
let crossing_origin = Rectangle::new(Point::new(-20., -10.), Size::new(140., 28.));
assert_eq!(
birth_label_clip(
iced::Vector::ZERO,
crossing_origin,
2.,
Size::new(300., 420.)
),
Some(Rectangle::new(Point::ORIGIN, Size::new(224., 28.)))
);
for viewport in [Size::new(200., 420.), Size::new(300., 400.), Size::ZERO] {
assert_eq!(
birth_label_clip(iced::Vector::ZERO, capsule, 2., viewport),
None
);
}
for scale in [0., -1., f32::NAN, f32::INFINITY] {
assert_eq!(
birth_label_clip(iced::Vector::ZERO, capsule, scale, Size::new(1000., 1000.)),
None
);
}
}
#[test]
fn real_scene_empty_and_create_only_use_visible_root_extents_not_insertion_bounds() {
for layer in [Layer::Oa, Layer::Sa] {
let mut f = Fixture::new(layer);
let empty = f.scene();
assert_eq!(empty.sequence_bounds[&f.owner], (106., 106.));
assert_eq!(
intervals(&empty, f.receiver, f.owner),
vec![(85., empty.size.height - 20., Life::Alive)]
);
assert!(empty.lines.is_empty(), "lane guides are not message lines");
let create = f.message(f.owner, MessageKind::Create);
let scene = f.scene();
let birth = scene.rows[&create].1;
assert_eq!(birth, 148.);
assert_eq!(scene.sequence_bounds[&f.owner], (106., 198.));
assert_eq!(
intervals(&scene, f.receiver, f.owner),
vec![
(85., birth, Life::Absent),
(birth, scene.size.height - 20., Life::Alive),
]
);
assert_eq!(
intervals(&scene, f.sender, f.owner),
vec![(85., scene.size.height - 20., Life::Alive)]
);
assert_eq!(scene.slots[0].y, 106.);
assert_eq!(
scene.lane_at(Point::new(scene.lanes[&f.receiver], 90.)),
Some(f.receiver)
);
}
}
#[test]
fn real_scene_birth_death_markers_align_endpoints_and_win_selection_without_mutating_source() {
for layer in [Layer::Oa, Layer::Sa] {
let mut f = Fixture::new(layer);
let create = f.message(f.owner, MessageKind::Create);
f.occurrence(
f.owner,
"Work",
OccurrenceBody::Action {
action: Some(f.work),
lifeline: Some(f.receiver),
},
);
let delete = f.message(f.owner, MessageKind::Delete);
let mut scene = f.scene();
let birth = scene.rows[&create].1;
let death = scene.rows[&delete].1;
assert_eq!(
intervals(&scene, f.receiver, f.owner),
vec![
(85., birth, Life::Absent),
(birth, death, Life::Alive),
(death, scene.size.height - 20., Life::Absent),
]
);
let capsule = scene
.lifecycle
.markers
.iter()
.find(|m| m.event.occurrence == create)
.unwrap()
.capsule;
let line = scene.lines.iter().find(|l| l.id == create).unwrap();
let expected_x = if scene.lanes[&f.sender] < scene.lanes[&f.receiver] {
capsule.x
} else {
capsule.x + capsule.width
};
assert_eq!(line.points.last(), Some(&Point::new(expected_x, birth)));
assert!(
line.label_at.y + 22. < capsule.y,
"message caption clears birth capsule"
);
let line = scene.lines.iter().find(|l| l.id == delete).unwrap();
assert_eq!(
line.points.last(),
Some(&Point::new(scene.lanes[&f.receiver], death))
);
for scale in [0.5, 1., 2.] {
scene.scale = scale;
assert_eq!(
scene.hit(Point::new(scene.lanes[&f.receiver], birth)),
Some(create)
);
assert_eq!(
scene.hit(Point::new(scene.lanes[&f.receiver], death)),
Some(delete)
);
assert_eq!(
scene.hit(Point::new(
scene.lanes[&f.receiver],
scene.size.height - 25.
)),
Some(f.receiver)
);
assert_eq!(
scene.hit(Point::new(scene.lanes[&f.receiver], 40.)),
Some(f.receiver)
);
}
}
}
#[test]
fn real_scene_branch_delete_clips_only_its_activation_and_keeps_unanswered_call_visible() {
for layer in [Layer::Oa, Layer::Sa] {
let mut f = Fixture::new(layer);
let call = f.message(f.owner, MessageKind::Call);
let fragment = f.fragment(f.owner, FragmentOperator::Alternative);
let left = f.branch(fragment);
let delete = f.message(left, MessageKind::Delete);
let right = f.branch(fragment);
let scene = f.scene();
let death = scene.rows[&delete].1;
let (left_start, left_end) = scene.sequence_bounds[&left];
let (right_start, _) = scene.sequence_bounds[&right];
let bars: Vec<_> = scene
.boxes
.iter()
.filter(|b| b.id == call && b.rect.width == 8.)
.map(|b| b.rect)
.collect();
assert!(
bars.iter()
.any(|r| r.y == left_start && r.y + r.height == death)
);
assert!(
!bars
.iter()
.any(|r| r.y < left_end && r.y + r.height > death && r.y >= left_start)
);
assert!(
bars.iter().any(|r| r.y == right_start && r.height == 26.),
"empty exclusive sibling retains its own open activation"
);
assert_eq!(
intervals(&scene, f.receiver, right),
vec![(right_start, right_start + 26., Life::Alive)]
);
let frame_end = scene.boxes.iter().find(|b| b.id == fragment).unwrap().rect;
assert!(
!bars.iter().any(|r| r.y >= frame_end.y + frame_end.height),
"uncertain post-fragment lifetime cannot carry a definite activation bar"
);
assert!(
scene
.open_calls
.iter()
.any(|p| p.y < death && p.y > left_start)
);
assert!(scene.open_calls.iter().any(|p| p.y > right_start));
assert_eq!(
intervals(&scene, f.receiver, left),
vec![
(left_start, death, Life::Alive),
(death, left_end, Life::Absent)
]
);
assert_eq!(
intervals(&scene, f.receiver, f.owner).last().unwrap().2,
Life::Unresolved
);
assert!(!f.project.elements.values().any(|e| matches!(
e.shared(),
Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange {
message_kind: MessageKind::Reply,
..
},
..
})
)));
}
}
#[test]
fn real_scene_unassigned_lanes_and_explicit_incomplete_receiver_keep_real_marker_identity() {
for layer in [Layer::Oa, Layer::Sa] {
let mut f = Fixture::new(layer);
*f.project
.elements
.get_mut(&f.receiver)
.unwrap()
.shared_mut()
.unwrap() = SharedBody::Lifeline { represented: None };
let create = f.message(f.owner, MessageKind::Create);
if let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { exchange, .. },
..
}) = f.project.elements.get_mut(&create).unwrap().shared_mut()
{
*exchange = None;
}
let scene = f.scene();
assert!(
!scene.lanes.contains_key(&f.receiver),
"authoring eligibility remains separate"
);
let x = scene.lifecycle.lanes[&f.receiver];
assert_eq!(scene.lifecycle.markers[0].lifeline, f.receiver);
assert_eq!(
scene.hit(Point::new(x, scene.rows[&create].1)),
Some(create)
);
assert_eq!(scene.hit(Point::new(x, 40.)), Some(f.receiver));
assert_eq!(
scene.hit(Point::new(x, scene.size.height - 25.)),
Some(f.receiver)
);
assert_eq!(scene.lane_at(Point::new(x, 90.)), None);
if let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange {
receiver, exchange, ..
},
..
}) = f.project.elements.get_mut(&create).unwrap().shared_mut()
{
*receiver = None;
*exchange = Some(f.exchange);
}
let missing = f.scene();
assert!(
missing.lifecycle.markers.is_empty(),
"do not infer receiver from global exchange"
);
}
}
#[test]
fn real_scene_minimum_zoom_marker_halos_do_not_steal_adjacent_capsule_or_header_hits() {
let mut f = Fixture::new(Layer::Sa);
*f.project
.elements
.get_mut(&f.receiver)
.unwrap()
.shared_mut()
.unwrap() = SharedBody::Lifeline { represented: None };
let create = f.message(f.owner, MessageKind::Create);
let delete = f.message(f.owner, MessageKind::Delete);
for event in [create, delete] {
if let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { exchange, .. },
..
}) = f.project.elements.get_mut(&event).unwrap().shared_mut()
{
*exchange = None;
}
}
let mut scene = f.scene();
let x = scene.lifecycle.lanes[&f.receiver];
assert_eq!(scene.rows[&delete].1 - scene.rows[&create].1, 92.);
for scale in [0.05, 0.1, 0.5, 1., 2.] {
scene.scale = scale;
assert_eq!(
scene.hit(Point::new(x, scene.rows[&create].1)),
Some(create)
);
assert_eq!(
scene.hit(Point::new(x, scene.rows[&delete].1)),
Some(delete)
);
assert_eq!(scene.hit(Point::new(x, 40.)), Some(f.receiver));
if scale <= 0.1 {
for offset in [-25., -15., 15., 25.] {
assert_eq!(
scene.hit(Point::new(x, scene.rows[&delete].1 + offset)),
Some(delete),
"a nearby death halo must beat an earlier message's wider tolerance"
);
}
}
}
}
#[test]
fn real_scene_minimum_zoom_near_ordinary_message_wins_over_lifecycle_halos() {
let mut f = Fixture::new(Layer::Sa);
f.message(f.owner, MessageKind::Create);
let ordinary = f.message(f.owner, MessageKind::Asynchronous);
f.message(f.owner, MessageKind::Delete);
let mut scene = f.scene();
let x = (scene.lanes[&f.sender] + scene.lanes[&f.receiver]) / 2.;
for scale in [0.05, 0.1] {
scene.scale = scale;
assert_eq!(
scene.hit(Point::new(x, scene.rows[&ordinary].1 + 2.)),
Some(ordinary)
);
}
}
#[test]
fn real_scene_self_lifecycle_elbows_end_on_recorded_marker_rows() {
for layer in [Layer::Oa, Layer::Sa] {
for kind in [MessageKind::Create, MessageKind::Delete] {
let mut f = Fixture::new(layer);
let event = f.message(f.owner, kind);
if let Some(SharedBody::Occurrence {
body:
OccurrenceBody::Exchange {
exchange, sender, ..
},
..
}) = f.project.elements.get_mut(&event).unwrap().shared_mut()
{
*exchange = None;
*sender = Some(f.receiver);
}
let scene = f.scene();
let marker = &scene.lifecycle.markers[0];
let line = scene.lines.iter().find(|l| l.id == event).unwrap();
assert_eq!(line.points.len(), 4);
assert_eq!(line.points[0].y, marker.center.y - 24.);
assert_eq!(line.points[3].y, marker.center.y);
assert_eq!(
line.points[3].x,
if marker.event.kind == MarkerKind::Birth {
marker.capsule.x + marker.capsule.width
} else {
marker.center.x
}
);
assert!(line.points[1].x > marker.capsule.x + marker.capsule.width);
assert_eq!(scene.hit(marker.center), Some(event));
}
}
}
#[test]
fn real_scene_self_delete_caption_clears_the_preceding_action_box() {
let mut f = Fixture::new(Layer::Sa);
let action = f.occurrence(
f.owner,
"Finish work",
OccurrenceBody::Action {
action: Some(f.work),
lifeline: Some(f.receiver),
},
);
let delete = f.message(f.owner, MessageKind::Delete);
if let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange {
exchange, sender, ..
},
..
}) = f.project.elements.get_mut(&delete).unwrap().shared_mut()
{
*exchange = None;
*sender = Some(f.receiver);
}
let scene = f.scene();
let action = scene.boxes.iter().find(|b| b.id == action).unwrap();
let line = scene.lines.iter().find(|l| l.id == delete).unwrap();
assert!(line.label_at.y > action.rect.y + action.rect.height);
assert!(line.label_at.y + 22. < line.points[0].y);
assert_eq!(line.points[3].y, scene.rows[&delete].1);
assert_eq!(
scene
.slots
.iter()
.find(|s| s.slot.before == Some(delete))
.unwrap()
.y,
scene.rows[&delete].1 - 42.
);
}
#[test]
fn real_scene_conditional_repeated_and_parallel_scopes_do_not_claim_definite_continuation() {
for operator in [
FragmentOperator::Alternative,
FragmentOperator::Optional,
FragmentOperator::Loop,
FragmentOperator::Parallel,
] {
let mut f = Fixture::new(Layer::Sa);
let fragment = f.fragment(f.owner, operator);
let first = f.branch(fragment);
let create = f.message(first, MessageKind::Create);
let scene = f.scene();
assert_eq!(scene.lifecycle.markers[0].event.occurrence, create);
assert_eq!(
intervals(&scene, f.receiver, f.owner).first().unwrap().2,
Life::Unresolved
);
assert_eq!(
intervals(&scene, f.receiver, f.owner).last().unwrap().2,
Life::Unresolved
);
assert!(
scene
.lifecycle
.lifetimes
.iter()
.find(|l| l.lifeline == f.sender)
.unwrap()
.intervals
.iter()
.all(|i| i.life == Life::Alive)
);
}
}
#[test]
fn real_scene_lifecycle_edit_undo_redo_restores_exact_source_and_changed_geometry() {
for layer in [Layer::Oa, Layer::Sa] {
let mut f = Fixture::new(layer);
let create = f.message(f.owner, MessageKind::Create);
let mut document = f.document();
let before = document.snapshot();
let geometry = Scene::sequence(document.project(), f.owner, create).lifecycle;
let mut edited = document.project().elements[&create].clone();
if let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { message_kind, .. },
..
}) = edited.shared_mut()
{
*message_kind = MessageKind::Asynchronous;
}
document
.execute(Command::EditElement(create, edited))
.unwrap();
let after = document.snapshot();
let changed = Scene::sequence(document.project(), f.owner, create).lifecycle;
assert_ne!(geometry, changed);
assert!(changed.markers.is_empty());
assert_ne!(before.bundle, after.bundle);
assert!(document.undo());
assert_eq!(document.project(), &before.decoded.project);
assert_eq!(document.bundle(), &before.bundle);
assert_eq!(
Scene::sequence(document.project(), f.owner, create).lifecycle,
geometry
);
assert!(!document.can_undo() && document.can_redo());
assert!(document.redo());
assert_eq!(document.project(), &after.decoded.project);
assert_eq!(document.bundle(), &after.bundle);
assert_eq!(
Scene::sequence(document.project(), f.owner, create).lifecycle,
changed
);
assert!(!document.can_redo());
}
}
#[test]
fn panned_header_and_create_clips_use_screen_translation_and_actual_viewport() {
use crate::behavior::box_text_clip;
use iced::Vector;
let header = Rectangle::new(Point::new(100., 200.), Size::new(204., 70.));
let capsule = Rectangle::new(header.position(), Size::new(140., 28.));
let rect = |x, y, w, h| Rectangle::new(Point::new(x, y), Size::new(w, h));
for (pan, scale, viewport, header_expected, birth_expected) in [
(
Vector::new(37., -41.),
0.5,
Size::new(300., 400.),
rect(92., 63., 92., 27.),
rect(91., 61., 62., 10.),
),
(
Vector::new(37., -41.),
1.,
Size::new(300., 400.),
rect(147., 167., 153., 54.),
rect(145., 163., 124., 20.),
),
(
Vector::new(37., -41.),
2.,
Size::new(300., 400.),
rect(257., 375., 43., 25.),
rect(253., 367., 47., 33.),
),
(
Vector::new(-200., -400.),
2.,
Size::new(300., 100.),
rect(20., 16., 280., 84.),
rect(16., 8., 248., 40.),
),
(
Vector::new(-237., -411.),
2.,
Size::new(300., 100.),
rect(0., 5., 300., 95.),
rect(0., 0., 227., 37.),
),
] {
assert_eq!(
box_text_clip(pan, header, scale, viewport),
Some(header_expected)
);
assert_eq!(
birth_label_clip(pan, capsule, scale, viewport),
Some(birth_expected)
);
}
for pan in [
Vector::new(1000., 1000.),
Vector::new(-1000., -1000.),
Vector::new(f32::NAN, 0.),
Vector::new(0., f32::INFINITY),
] {
assert_eq!(box_text_clip(pan, header, 2., Size::new(300., 100.)), None);
assert_eq!(
birth_label_clip(pan, capsule, 2., Size::new(300., 100.)),
None
);
}
}
#[test]
fn real_scene_panned_zoomed_program_keeps_header_birth_and_death_hit_identity() {
use crate::{
app::Message,
behavior::{
SequenceInput,
scenario::{CanvasEvent, CanvasTool},
},
};
use iced::{Vector, mouse, widget::canvas};
for layer in [Layer::Oa, Layer::Sa] {
let mut f = Fixture::new(layer);
f.project.elements.get_mut(&f.receiver).unwrap().name = "界".repeat(24);
let create = f.message(f.owner, MessageKind::Create);
let delete = f.message(f.owner, MessageKind::Delete);
let document = f.document();
let before = document.snapshot();
for scale in [0.5, 1., 2.] {
for pan in [Vector::new(35., 40.), Vector::new(-35., -5.)] {
let mut scene = f.scene();
scene.scale = scale;
scene.camera_pan = pan;
let context = crate::behavior::scenario::direct_tests::context(f.owner);
scene.interaction = Some((context, CanvasTool::Select, true));
let header = scene.boxes.iter().find(|b| b.id == f.sender).unwrap();
let targets = [
(f.sender, header.rect.center()),
(
create,
scene
.lifecycle
.markers
.iter()
.find(|m| m.event.occurrence == create)
.unwrap()
.center,
),
(
delete,
scene
.lifecycle
.markers
.iter()
.find(|m| m.event.occurrence == delete)
.unwrap()
.center,
),
];
// Absolute widget origin models the outer layout/scroll translation;
// camera pan remains local screen units and must be subtracted once.
let bounds = Rectangle::new(Point::new(73., 119.), Size::new(2000., 1200.));
for (expected, world) in targets {
let screen =
bounds.position() + pan + Vector::new(world.x * scale, world.y * scale);
assert!(bounds.contains(screen));
let mut state = SequenceInput::default();
let mut published = Vec::new();
for event in [
mouse::Event::CursorMoved { position: screen },
mouse::Event::ButtonPressed(mouse::Button::Left),
mouse::Event::ButtonReleased(mouse::Button::Left),
] {
if let Some(message) = canvas::Program::update(
&scene,
&mut state,
&canvas::Event::Mouse(event),
bounds,
mouse::Cursor::Available(screen),
)
.and_then(|a| a.into_inner().0)
{
published.push(message);
}
}
assert!(
matches!(published.as_slice(), [Message::ScenarioCanvas(CanvasEvent::Select(actual_context, actual))] if *actual_context == context && *actual == expected),
"Pan/zoom must retain the typed hit identity"
);
}
assert_eq!(scene.lifecycle, f.scene().lifecycle);
}
}
assert_eq!(document.project(), &before.decoded.project);
assert_eq!(document.bundle(), &before.bundle);
assert!(!document.can_undo() && !document.dirty());
}
}
+37 -583
View File
@@ -325,73 +325,50 @@ impl State {
) -> Option<UiElement<'a, Event, iced::Theme, R>> {
let d = self.editing.as_ref()?;
let event = move |edit| Event::Edit(d.token, edit);
let kinds = d.scope.kinds(d.layer);
let determined = kinds.len() == 1 && d.kind == kinds.first().copied();
let mut fields = column![
text(if determined {
format!("New {} · {}", kinds[0], d.layer)
} else {
format!("New source · {}", d.layer)
text(format!("New source · {}", d.layer)).size(18),
text(match d.scope {
Scope::Lifeline { owner, viewpoint } =>
format!("For a lifeline in {} · {:?}", p.named(owner), viewpoint),
Scope::Work { owner } => format!("For a contribution in {}", p.named(owner)),
Scope::Machine { .. } => "Owner for the pending state machine".into(),
})
.size(14),
.size(12),
text("The parent form is retained. This source is created only with its consumer.")
.size(12)
]
.spacing(8);
for kind in d.scope.kinds(d.layer) {
fields = fields.push(
button(text(kind.to_string()))
.on_press(event(Edit::Kind(kind)))
.style(if d.kind == Some(kind) {
button::primary
} else {
button::secondary
}),
);
}
fields = fields.push(
text_input("Source name (independent of the consumer name)", &d.name)
.id(NAME_INPUT)
.on_input(move |v| event(Edit::Name(v)))
.size(14)
.padding(6),
]
.spacing(4);
if !determined {
let mut choices = row![].spacing(6);
for kind in kinds {
choices = choices.push(
container(
button(text(kind.to_string()).size(14))
.padding(4)
.on_press(event(Edit::Kind(kind)))
.style(if d.kind == Some(kind) {
button::primary
} else {
button::secondary
}),
)
.id(format!("behavior-source-kind-{kind:?}")),
);
}
fields = fields.push(choices.wrap());
}
.padding(8),
);
fields = fields.push(
container(
text(format!(
"Semantic parent: {}",
d.parent
.as_ref()
.map_or(format!("{} layer root", d.layer), |e| format!(
"{} · {} · @{}",
e.name,
e.kind(),
e.alias
))
))
.size(12),
)
.id("behavior-source-parent"),
text(format!(
"Semantic parent: {}",
d.parent
.as_ref()
.map_or(format!("{} layer root", d.layer), |e| format!(
"{} · {} · @{}",
e.name,
e.kind(),
e.alias
))
))
.size(12),
);
fields = fields
.push(
text(match d.scope {
Scope::Lifeline { owner, viewpoint } => {
format!("For a lifeline in {} · {:?}", p.named(owner), viewpoint)
}
Scope::Work { owner } => format!("For a contribution in {}", p.named(owner)),
Scope::Machine { .. } => "Owner for the pending state machine".into(),
})
.size(12),
)
.push(
text("The parent form is retained. This source is created only with its consumer.")
.size(12),
);
if let Some(kind) = d.kind {
fields = fields.push(button("Use layer root").on_press_maybe(
validation::can_parent(p, kind, d.layer, None).then(|| event(Edit::Parent(None))),
@@ -509,526 +486,3 @@ impl Draft {
Ok(command)
}
}
#[cfg(all(test, debug_assertions))]
mod layout_tests {
use super::*;
use iced::advanced::{Layout, Shell, graphics, layout, renderer, text as shaped, widget};
use iced::{Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, Vector};
// Use Iced's real paragraph shaping/measurement without a window or GPU.
// The unit renderer's zero-size paragraphs cannot detect this regression.
struct FontLayout;
impl renderer::Renderer for FontLayout {
fn start_layer(&mut self, _: Rectangle) {}
fn end_layer(&mut self) {}
fn start_transformation(&mut self, _: Transformation) {}
fn end_transformation(&mut self) {}
fn reset(&mut self, _: Rectangle) {}
fn fill_quad(&mut self, _: renderer::Quad, _: impl Into<Background>) {}
fn allocate_image(
&mut self,
handle: &iced::advanced::image::Handle,
callback: impl FnOnce(
Result<iced::advanced::image::Allocation, iced::advanced::image::Error>,
) + Send
+ 'static,
) {
renderer::Renderer::allocate_image(&mut (), handle, callback);
}
}
impl shaped::Renderer for FontLayout {
type Font = Font;
type Paragraph = graphics::text::Paragraph;
type Editor = ();
const ICON_FONT: Font = Font::DEFAULT;
const CHECKMARK_ICON: char = '0';
const ARROW_DOWN_ICON: char = '0';
const SCROLL_UP_ICON: char = '0';
const SCROLL_DOWN_ICON: char = '0';
const SCROLL_LEFT_ICON: char = '0';
const SCROLL_RIGHT_ICON: char = '0';
const ICED_LOGO: char = '0';
fn default_font(&self) -> Font {
Font::DEFAULT
}
fn default_size(&self) -> Pixels {
Pixels(16.)
}
fn fill_paragraph(&mut self, _: &Self::Paragraph, _: Point, _: Color, _: Rectangle) {}
fn fill_editor(&mut self, _: &Self::Editor, _: Point, _: Color, _: Rectangle) {}
fn fill_text(&mut self, _: shaped::Text, _: Point, _: Color, _: Rectangle) {}
}
#[derive(Default)]
struct Geometry {
fields: Option<Rectangle>,
content: Option<Rectangle>,
name: Option<Rectangle>,
parent: Option<Rectangle>,
actions: Option<Rectangle>,
kinds: BTreeMap<String, Rectangle>,
text: Vec<(String, Rectangle)>,
focus_name: bool,
}
impl widget::Operation for Geometry {
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) {
visit(self);
}
fn container(&mut self, id: Option<&widget::Id>, bounds: Rectangle) {
if id == Some(&widget::Id::new("behavior-source-actions")) {
self.actions = Some(bounds);
}
if id == Some(&widget::Id::new("behavior-source-parent")) {
self.parent = Some(bounds);
}
for kind in [
Kind::Entity,
Kind::Actor,
Kind::Role,
Kind::System,
Kind::SystemActor,
Kind::Activity,
Kind::Function,
] {
if id == Some(&widget::Id::from(format!("behavior-source-kind-{kind:?}"))) {
self.kinds.insert(kind.to_string(), bounds);
}
}
}
fn scrollable(
&mut self,
id: Option<&widget::Id>,
bounds: Rectangle,
content: Rectangle,
offset: Vector,
_: &mut dyn widget::operation::Scrollable,
) {
if id == Some(&widget::Id::new("behavior-source-fields")) {
assert_eq!(
offset,
Vector::ZERO,
"Initial fields must be visible without scrolling"
);
self.fields = Some(bounds);
self.content = Some(content);
}
}
fn text_input(
&mut self,
id: Option<&widget::Id>,
bounds: Rectangle,
_: &mut dyn widget::operation::TextInput,
) {
if id == Some(&widget::Id::new(NAME_INPUT)) {
self.name = Some(bounds);
}
}
fn focusable(
&mut self,
id: Option<&widget::Id>,
_: Rectangle,
state: &mut dyn widget::operation::Focusable,
) {
if self.focus_name && id == Some(&widget::Id::new(NAME_INPUT)) {
state.focus();
}
}
fn text(&mut self, _: Option<&widget::Id>, bounds: Rectangle, text: &str) {
self.text.push((text.into(), bounds));
}
}
fn contains(outer: Rectangle, inner: Rectangle) -> bool {
inner.width > 0.
&& inner.height > 0.
&& inner.x >= outer.x - 0.1
&& inner.y >= outer.y - 0.1
&& inner.x + inner.width <= outer.x + outer.width + 0.1
&& inner.y + inner.height <= outer.y + outer.height + 0.1
}
fn host<'a>(
state: &'a State,
p: &'a Project,
caption: &str,
) -> UiElement<'a, Event, iced::Theme, FontLayout> {
iced::widget::column![text(caption.to_owned()).size(12), state.view(p).unwrap()]
.spacing(6)
.height(Fill)
.width(Fill)
.into()
}
fn geometry(
view: &mut UiElement<'_, Event, iced::Theme, FontLayout>,
tree: &mut widget::Tree,
size: Size,
) -> (layout::Node, Geometry) {
let node =
view.as_widget_mut()
.layout(tree, &FontLayout, &layout::Limits::new(Size::ZERO, size));
let mut geometry = Geometry::default();
view.as_widget_mut()
.operate(tree, Layout::new(&node), &FontLayout, &mut geometry);
(node, geometry)
}
fn fixture(layer: Layer, viewpoint: Option<ScenarioViewpoint>, work: bool) -> (Project, State) {
let mut p = Project::empty();
let scope = if work {
let owner = p
.add_element(
if layer == Layer::Oa {
Kind::Process
} else {
Kind::FunctionalChain
},
layer,
"Consumer",
None,
)
.unwrap();
Scope::Work { owner }
} else if let Some(viewpoint) = viewpoint {
let owner = p
.add_element(
Kind::Scenario,
layer,
"Incident coordination with a long contextual heading 雨 Ω",
None,
)
.unwrap();
if let Some(SharedBody::Scenario { viewpoint: v, .. }) =
p.elements.get_mut(&owner).unwrap().shared_mut()
{
*v = viewpoint;
}
Scope::Lifeline { owner, viewpoint }
} else {
Scope::Machine { layer }
};
let mut state = State::default();
let invocation = Id::new_v4();
state
.update(&p, scope, invocation, Event::Open(invocation))
.unwrap();
(p, state)
}
#[test]
fn contextual_source_layout_initial_essentials_fit_short_composed_host_with_real_paragraphs() {
for (layer, viewpoint, work) in [
(
Layer::Oa,
Some(ScenarioViewpoint::OperationalActivity),
false,
),
(Layer::Sa, Some(ScenarioViewpoint::Functional), false),
(
Layer::Oa,
Some(ScenarioViewpoint::ParticipantInteraction),
false,
),
(Layer::Sa, Some(ScenarioViewpoint::BoundaryInterface), false),
(Layer::Oa, None, false),
(Layer::Sa, None, false),
(Layer::Oa, None, true),
(Layer::Sa, None, true),
] {
let (p, state) = fixture(layer, viewpoint, work);
let raw = serde_json::to_value(&state).unwrap();
for width in [340., 500.] {
// The retained caption + 6px gap leaves approximately the observed 194px card.
let size = Size::new(width, 215.);
let mut view = host(&state, &p, "Consumer name: Assessment lifeline");
let mut tree = widget::Tree::new(view.as_widget());
let (_, g) = geometry(&mut view, &mut tree, size);
let fields = g.fields.unwrap();
assert!(
(125. ..=132.).contains(&fields.height),
"Measured field viewport: {fields:?}"
);
assert!(
contains(fields, g.name.unwrap()),
"Name clipped: {layer:?}/{viewpoint:?}/{work}/{width}"
);
assert!(
contains(fields, g.parent.unwrap()),
"Parent clipped: {layer:?}/{viewpoint:?}/{work}/{width}"
);
assert!(contains(
Rectangle::new(Point::ORIGIN, size),
g.actions.unwrap()
));
assert!(fields.y + fields.height <= g.actions.unwrap().y);
let expected = state.editing.as_ref().unwrap().scope.kinds(layer);
assert_eq!(
g.kinds.len(),
if expected.len() == 1 {
0
} else {
expected.len()
}
);
for bounds in g.kinds.values() {
assert!(
contains(fields, *bounds),
"Kind clipped: {bounds:?} in {fields:?}"
);
}
let parent_text = g
.text
.iter()
.find(|(s, _)| s.starts_with("Semantic parent:"))
.unwrap();
assert_eq!(
parent_text.0,
format!("Semantic parent: {layer} layer root")
);
assert!(
parent_text.1.height > 0.,
"Paragraph measurement must not be null"
);
let explanation = g
.text
.iter()
.find(|(s, _)| s.starts_with("The parent form"))
.unwrap();
assert!(explanation.1.y >= g.parent.unwrap().y + g.parent.unwrap().height);
}
assert_eq!(
serde_json::to_value(&state).unwrap(),
raw,
"Layout must not choose a kind or mutate the draft"
);
if state.editing.as_ref().unwrap().scope.kinds(layer).len() == 1 {
let mut incomplete = state.clone();
incomplete.editing.as_mut().unwrap().kind = None;
let mut view = host(&incomplete, &p, "Consumer name: Assessment lifeline");
let mut tree = widget::Tree::new(view.as_widget());
let (_, g) = geometry(&mut view, &mut tree, Size::new(340., 215.));
assert_eq!(
g.kinds.len(),
1,
"An incomplete recovery still needs an explicit repair choice"
);
assert!(contains(
g.fields.unwrap(),
*g.kinds.values().next().unwrap()
));
assert!(contains(g.fields.unwrap(), g.name.unwrap()));
assert_eq!(incomplete.editing.as_ref().unwrap().kind, None);
}
}
}
#[test]
fn contextual_source_layout_long_parent_wraps_after_visible_name_without_losing_raw_data() {
let (mut p, mut state) = fixture(Layer::Oa, None, true);
let parent = p
.add_element(
Kind::Activity,
Layer::Oa,
"Northern operations coordination — exact parent 雨",
None,
)
.unwrap();
let d = state.editing.as_ref().unwrap();
let token = d.token;
let scope = d.scope;
let invocation = Id::new_v4();
state
.update(
&p,
scope,
invocation,
Event::Edit(token, Edit::Parent(Some(parent))),
)
.unwrap();
state
.update(
&p,
scope,
invocation,
Event::Edit(token, Edit::Name(" Distinct source 雨 ".into())),
)
.unwrap();
let raw = serde_json::to_value(&state).unwrap();
for width in [340., 500.] {
let size = Size::new(width, 235.);
let mut view = host(
&state,
&p,
"Consumer name: Coordination with a long independent label 雨 Ω",
);
let mut tree = widget::Tree::new(view.as_widget());
let (_, g) = geometry(&mut view, &mut tree, size);
assert!(contains(g.fields.unwrap(), g.name.unwrap()));
assert!(contains(g.fields.unwrap(), g.parent.unwrap()));
assert!(contains(
Rectangle::new(Point::ORIGIN, size),
g.actions.unwrap()
));
let summary = g
.text
.iter()
.find(|(s, _)| s.starts_with("Semantic parent:"))
.unwrap();
assert!(summary.0.contains(&p.elements[&parent].name));
assert!(summary.0.contains(&p.elements[&parent].alias));
assert!(
summary.1.height > 15.,
"Long parent should exercise real wrapping"
);
}
assert_eq!(serde_json::to_value(&state).unwrap(), raw);
assert_eq!(
state.editing.as_ref().unwrap().name,
" Distinct source 雨 "
);
let (mut p, mut state) = fixture(
Layer::Oa,
Some(ScenarioViewpoint::ParticipantInteraction),
false,
);
let parent = p
.add_element(
Kind::Entity,
Layer::Oa,
// Eight repetitions need not exceed a whole viewport with the
// platform's measured font. Exercise substantial real overflow.
&"Northern operational responsibility 雨 ".repeat(32),
None,
)
.unwrap();
let d = state.editing.as_ref().unwrap();
let token = d.token;
let scope = d.scope;
state
.update(
&p,
scope,
Id::new_v4(),
Event::Edit(token, Edit::Kind(Kind::Actor)),
)
.unwrap();
state
.update(
&p,
scope,
Id::new_v4(),
Event::Edit(token, Edit::Parent(Some(parent))),
)
.unwrap();
let raw = serde_json::to_value(&state).unwrap();
let recovered: State = serde_json::from_value(raw.clone()).unwrap();
let mut view = host(&recovered, &p, "Consumer name: Assessment lifeline");
let mut tree = widget::Tree::new(view.as_widget());
let (_, g) = geometry(&mut view, &mut tree, Size::new(340., 215.));
let fields = g.fields.unwrap();
assert!(contains(fields, g.name.unwrap()));
assert_eq!(g.kinds.len(), 3);
for bounds in g.kinds.values() {
assert!(
contains(fields, *bounds),
"Long parent must not hide a semantic choice"
);
assert!(bounds.y + bounds.height <= g.parent.unwrap().y);
}
// Arbitrary authored detail may scroll, but its label and first line are
// visible after the required kind controls, never in front of them.
let parent_bounds = g.parent.unwrap();
assert!(parent_bounds.y + 16. <= fields.y + fields.height);
assert!(
parent_bounds.height > fields.height,
"Exercise genuinely overflowing parent text: {parent_bounds:?} in {fields:?}"
);
let content = g.content.unwrap();
assert!(
content.height > fields.height && contains(content, parent_bounds),
"Full parent must remain in scrollable content: {parent_bounds:?}, {content:?}, {fields:?}"
);
let summary = g
.text
.iter()
.find(|(s, _)| s.starts_with("Semantic parent:"))
.unwrap();
assert!(summary.0.contains(&p.elements[&parent].name));
assert!(summary.0.contains(&p.elements[&parent].alias));
assert!(contains(
Rectangle::new(Point::ORIGIN, Size::new(340., 215.)),
g.actions.unwrap()
));
assert_eq!(serde_json::to_value(&recovered).unwrap(), raw);
}
#[test]
fn contextual_source_layout_visible_kind_click_keeps_choice_explicit_and_name_input_works() {
let (p, mut state) = fixture(
Layer::Oa,
Some(ScenarioViewpoint::ParticipantInteraction),
false,
);
let original = p.clone();
let token = state.editing.as_ref().unwrap().token;
let scope = state.editing.as_ref().unwrap().scope;
assert_eq!(state.editing.as_ref().unwrap().kind, None);
let size = Size::new(340., 215.);
let mut view = host(&state, &p, "Consumer name: Assessment lifeline");
let mut tree = widget::Tree::new(view.as_widget());
let (node, g) = geometry(&mut view, &mut tree, size);
let mut messages = Vec::new();
for kind in [Kind::Entity, Kind::Actor, Kind::Role] {
let bounds = g.kinds[&kind.to_string()];
assert!(contains(g.fields.unwrap(), bounds));
for action in [
iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left),
iced::mouse::Event::ButtonReleased(iced::mouse::Button::Left),
] {
view.as_widget_mut().update(
&mut tree,
&iced::Event::Mouse(action),
Layout::new(&node),
iced::mouse::Cursor::Available(bounds.center()),
&FontLayout,
&mut iced::advanced::clipboard::Null,
&mut Shell::new(&mut messages),
&Rectangle::new(Point::ORIGIN, size),
);
}
assert!(
matches!(messages.pop(), Some(Event::Edit(id, Edit::Kind(k))) if id == token && k == kind)
);
}
let mut focus = Geometry {
focus_name: true,
..Geometry::default()
};
view.as_widget_mut()
.operate(&mut tree, Layout::new(&node), &FontLayout, &mut focus);
let event = iced::Event::Keyboard(iced::keyboard::Event::KeyPressed {
key: iced::keyboard::Key::Character("Ω".into()),
modified_key: iced::keyboard::Key::Character("Ω".into()),
physical_key: iced::keyboard::key::Physical::Code(iced::keyboard::key::Code::KeyA),
location: iced::keyboard::Location::Standard,
modifiers: iced::keyboard::Modifiers::empty(),
text: Some("Ω".into()),
repeat: false,
});
view.as_widget_mut().update(
&mut tree,
&event,
Layout::new(&node),
iced::mouse::Cursor::Unavailable,
&FontLayout,
&mut iced::advanced::clipboard::Null,
&mut Shell::new(&mut messages),
&Rectangle::new(Point::ORIGIN, size),
);
let edit = messages.pop().unwrap();
assert!(matches!(&edit, Event::Edit(id, Edit::Name(name)) if *id == token && name == "Ω"));
drop(view);
state.update(&p, scope, Id::new_v4(), edit).unwrap();
assert_eq!(
state.editing.as_ref().unwrap().kind,
None,
"Rendering/click event production must not silently adopt a kind"
);
assert_eq!(state.editing.as_ref().unwrap().name, "Ω");
assert_eq!(p, original);
}
}
+17 -11
View File
@@ -59,6 +59,7 @@ pub enum Event {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Shelf {
Tools,
Selection,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -79,7 +80,7 @@ pub struct State {
identity: Option<(Id, Id)>,
tool: CanvasTool,
expanded: BTreeSet<Id>,
details_offset: iced::Vector,
scroll_offsets: [iced::Vector; 2],
return_to: Vec<direct::ReturnFrame>,
draft: Option<Draft>,
viewport: Option<Size>,
@@ -350,8 +351,8 @@ impl State {
}
fn fit_area(&self, fallback_width: f32) -> Size {
// The sensor measures the viewport, independent of graph content.
// Leave a small content gutter; before first layout use a compact
// The sensor measures the inner scroller, independent of graph content.
// Leave a small gutter for scrollbars; before first layout use a compact
// provisional viewport and recompute when the sensor reports actual size.
let viewport = self.viewport.unwrap_or(Size::new(fallback_width, 180.));
Size::new(
@@ -360,6 +361,11 @@ impl State {
)
}
#[cfg(test)]
fn canvas_offset(&self) -> iced::Vector {
self.scroll_offsets[0]
}
#[cfg(test)]
fn fit_limited(&self, graph: Size, fallback_width: f32) -> bool {
let available = self.fit_area(fallback_width);
@@ -978,6 +984,7 @@ mod contextual_routing;
struct Graph<'a> {
project: &'a Project,
interaction: Option<(Context, CanvasTool, bool)>,
scroll_offset: iced::Vector,
camera_pan: iced::Vector,
pending_insertion: Option<Insertion>,
nodes: Vec<GraphNode>,
@@ -1311,6 +1318,7 @@ impl<'a> Graph<'a> {
Self {
project: p,
interaction: None,
scroll_offset: iced::Vector::ZERO,
camera_pan: iced::Vector::ZERO,
pending_insertion: None,
nodes,
@@ -2164,6 +2172,7 @@ mod tests {
let accepted = app.document.snapshot();
assert!(app.behavior.prepare_pending(app.project()).is_err());
for (shelf, expected) in [
(Shelf::Tools, Some(Shelf::Tools)),
(Shelf::Selection, Some(Shelf::Selection)),
(Shelf::Selection, None),
] {
@@ -2180,12 +2189,10 @@ mod tests {
}
#[test]
fn process_camera_selection_ignores_independent_details_scroll() {
fn process_fit_canvas_selection_ignores_retained_ordered_page_scroll() {
let (p, owner, _, _, _, _) = fixture();
let mut state = State::default();
state.camera(crate::behavior::canvas_workspace::Event::Pan(
iced::Vector::new(11., 19.),
));
state.scrolled(scenario::ScrollArea::Canvas, iced::Vector::new(11., 19.));
state.scrolled(scenario::ScrollArea::Page, iced::Vector::new(0., 370.));
let context = Context {
document: p.id,
@@ -2196,17 +2203,16 @@ mod tests {
for scale in [0.45, 1.] {
let mut graph = Graph::new(&p, owner, owner);
graph.scale = scale;
graph.camera_pan = state.pan;
graph.scroll_offset = state.canvas_offset();
graph.interaction = Some((context, CanvasTool::Select, true));
let node = &graph.nodes[0];
let id = node.id;
let point = node.rect.center();
let bounds = Rectangle::new(Point::new(40., 100.), Size::new(800., 300.));
let screen = Point::new(
bounds.x + point.x * scale + 11.,
bounds.y + point.y * scale + 19.,
bounds.x + point.x * scale - 11.,
bounds.y + point.y * scale - 19.,
);
assert!(bounds.contains(screen));
let message = graph
.input(
&mut Input::default(),
@@ -158,23 +158,20 @@ impl State {
self.fit = false;
self.viewport = None;
self.expanded.clear();
self.details_offset = iced::Vector::ZERO;
self.scroll_offsets = [iced::Vector::ZERO; 2];
}
}
#[cfg(test)]
pub(crate) fn details_offset(&self) -> iced::Vector {
self.details_offset
pub(crate) fn scroll_offset(&self) -> iced::Vector {
self.scroll_offsets[0] + self.scroll_offsets[1]
}
pub(crate) fn scrolled(
&mut self,
scenario::ScrollArea::Page: scenario::ScrollArea,
offset: iced::Vector,
) -> bool {
if !offset.x.is_finite() || !offset.y.is_finite() || offset.x < 0. || offset.y < 0. {
return false;
}
let changed = self.details_offset != offset;
self.details_offset = offset;
pub(crate) fn scrolled(&mut self, area: scenario::ScrollArea, offset: iced::Vector) -> bool {
let index = match area {
scenario::ScrollArea::Canvas => 0,
scenario::ScrollArea::Page => 1,
};
let changed = self.scroll_offsets[index] != offset;
self.scroll_offsets[index] = offset;
changed
}
pub(crate) fn choose(&mut self, tool: CanvasTool) {
@@ -625,7 +622,7 @@ impl Graph<'_> {
{
*moved |= position.distance(*start) >= 3.;
}
mouse::Cursor::Available(*position)
mouse::Cursor::Available(*position + self.scroll_offset)
}
canvas::Event::Mouse(mouse::Event::WheelScrolled { .. } | mouse::Event::CursorLeft)
| canvas::Event::Window(
@@ -644,9 +641,11 @@ impl Graph<'_> {
}
canvas::Event::Mouse(
mouse::Event::ButtonPressed(_) | mouse::Event::ButtonReleased(_),
) => state.pointer.map_or(cursor, mouse::Cursor::Available),
) => state
.pointer
.map_or(cursor, |p| mouse::Cursor::Available(p + self.scroll_offset)),
canvas::Event::Window(iced::window::Event::RedrawRequested(_)) => {
state.pointer = cursor.position();
state.pointer = cursor.position().map(|p| p - self.scroll_offset);
cursor
}
_ => cursor,
@@ -670,7 +669,7 @@ impl Graph<'_> {
if !editable {
return None;
}
let start = cursor.position()?;
let start = cursor.position()? - self.scroll_offset;
match tool {
CanvasTool::Add(..) => {
state.press = Some(Press::Add {
@@ -830,6 +829,7 @@ impl Graph<'_> {
}
let point = state
.pointer
.map(|p| p + self.scroll_offset)
.or_else(|| cursor.position())
.filter(|p| bounds.contains(*p))
.map(|p| {
@@ -540,7 +540,7 @@ fn send(
input,
&canvas::Event::Mouse(event),
bounds,
mouse::Cursor::Available(point),
mouse::Cursor::Available(point + graph.scroll_offset),
)
.and_then(|a| a.into_inner().0)
}
@@ -554,20 +554,16 @@ fn position(graph: &Graph<'_>, id: Id) -> Point {
.center()
}
#[test]
fn direct_process_production_click_click_drag_insert_and_camera_coordinates() {
fn direct_process_production_click_click_drag_insert_and_scroll_coordinates() {
let f = fixture(Layer::Oa);
for drag in [false, true] {
let mut graph = graph(&f, CanvasTool::Connect);
graph.camera_pan = iced::Vector::new(35., 90.);
let bounds = Rectangle::new(
Point::new(10., 20.),
Size::new(graph.size.width + 70., graph.size.height + 180.),
);
graph.scroll_offset = iced::Vector::new(35., 90.);
let bounds = Rectangle::new(Point::new(10., 20.), graph.size);
let screen =
|id| position(&graph, id) + iced::Vector::new(bounds.x, bounds.y) + graph.camera_pan;
|id| position(&graph, id) + iced::Vector::new(bounds.x, bounds.y) - graph.scroll_offset;
let a = screen(f.nodes[0]);
let b = screen(f.nodes[2]);
assert!(bounds.contains(a) && bounds.contains(b));
let mut input = Input::default();
send(
&graph,
@@ -1335,9 +1331,7 @@ fn direct_process_palette_preserves_fit_and_manual_camera() {
let document = Id::new_v4();
state.enter(document, f.owner);
state.viewport(Size::new(500., 300.));
state.camera(super::super::canvas_workspace::Event::Pan(
iced::Vector::new(35., 90.),
));
state.scrolled(scenario::ScrollArea::Canvas, iced::Vector::new(35., 90.));
state.scrolled(scenario::ScrollArea::Page, iced::Vector::new(0., 120.));
for camera in [None, Some(0.75), Some(1.5)] {
state.zoom(camera);
@@ -1359,16 +1353,14 @@ fn direct_process_palette_preserves_fit_and_manual_camera() {
"{layer:?} {camera:?} {tool:?}"
);
assert_eq!(state.fit, camera.is_none());
assert_eq!(state.pan, iced::Vector::new(35., 90.));
assert_eq!(state.details_offset(), iced::Vector::new(0., 120.));
assert_eq!(state.scroll_offset(), iced::Vector::new(35., 210.));
state.enter(document, f.owner);
assert_eq!(state.scale(size, 500.), before);
}
}
state.enter(Id::new_v4(), f.owner);
assert_eq!(state.scale(Size::new(690., 250.), 500.), 1.);
assert_eq!(state.pan, iced::Vector::ZERO);
assert_eq!(state.details_offset(), iced::Vector::ZERO);
assert_eq!(state.scroll_offset(), iced::Vector::ZERO);
}
}
@@ -1379,18 +1371,15 @@ fn direct_process_scaled_control_and_link_gestures_use_visible_positions() {
for scale in [0.15, 0.6898551, 1.5] {
let mut graph = graph(&f, CanvasTool::Add(Tool::Control, ControlKind::Choice));
graph.scale = scale;
graph.camera_pan = iced::Vector::new(11., 19.);
graph.scroll_offset = iced::Vector::new(11., 19.);
let bounds = Rectangle::new(
Point::new(40., 70.),
Size::new(
graph.size.width * scale + 22.,
graph.size.height * scale + 38.,
),
Size::new(graph.size.width * scale, graph.size.height * scale),
);
let screen = |world: Point| {
Point::new(
world.x * scale + bounds.x + 11.,
world.y * scale + bounds.y + 19.,
world.x * scale + bounds.x - 11.,
world.y * scale + bounds.y - 19.,
)
};
let edge = &graph.edges[0];
@@ -1398,7 +1387,6 @@ fn direct_process_scaled_control_and_link_gestures_use_visible_positions() {
(edge.points[0].x + edge.points[1].x) / 2.,
edge.points[0].y,
));
assert!(bounds.contains(point));
let mut input = Input::default();
send(
&graph,
@@ -1429,7 +1417,6 @@ fn direct_process_scaled_control_and_link_gestures_use_visible_positions() {
let mut input = Input::default();
let a = screen(position(&graph, f.nodes[0]));
let b = screen(position(&graph, f.nodes[2]));
assert!(bounds.contains(a) && bounds.contains(b));
send(
&graph,
&mut input,
@@ -86,7 +86,7 @@ async fn production_process_canvas_scaled_path_creation_preserves_source_and_edi
assert_eq!(app.behavior.topology.pan, pan);
mounted.scroll(&app, "process-form", 500.);
assert_eq!(mounted.geometry(&app).bounds("process-actions"), actions);
for shelf in [Shelf::Selection, Shelf::Selection] {
for shelf in [Shelf::Tools, Shelf::Selection, Shelf::Selection] {
let _ = app.update(event(Event::Shelf(shelf)));
assert_eq!(mounted.geometry(&app).bounds("process-viewport"), bounds);
assert_eq!(
@@ -723,7 +723,7 @@ async fn production_canvas_keyboard_and_delayed_tasks_respect_real_chooser_and_r
panic!("actual Review dialog opens");
};
let message = state.address_event(row);
let task = app.update(Message::Workflow(crate::workflow::Event::semantic(message)));
let task = app.update(Message::Workflow(crate::workflow::Event::Semantic(message)));
mounted.perform(&mut app, task).await;
}
let Some(crate::workflow::Workflow::Semantic(state)) = app.workflow.as_ref() else {
+4 -18
View File
@@ -95,7 +95,7 @@ pub struct Document {
past: Vec<HistoryEntry>,
future: Vec<HistoryEntry>,
current_bytes: usize,
saved: Option<Arc<SourceBundle>>,
saved: Arc<SourceBundle>,
revision: u64,
view_index: std::cell::OnceCell<oa_core::view_options::ViewIndex>,
limits: ProjectLimits,
@@ -125,7 +125,7 @@ impl Document {
current_bytes,
past: Vec::new(),
future: Vec::new(),
saved: Some(Arc::new(bundle)),
saved: Arc::new(bundle),
revision: 0,
view_index: Default::default(),
limits,
@@ -156,7 +156,7 @@ impl Document {
}
pub fn dirty(&self) -> bool {
self.recovered_unsaved || self.saved.as_deref() != Some(self.bundle())
self.recovered_unsaved || self.bundle() != self.saved.as_ref()
}
/// Recovered work is a detached copy until explicitly saved by its user.
@@ -181,24 +181,10 @@ impl Document {
/// Records the bytes actually saved, even when later edits remain current.
/// The I/O adapter owns document identity and save completion ordering.
pub fn mark_saved(&mut self, snapshot: Arc<CompiledSourceProject>) {
self.saved = Some(Arc::new(snapshot.bundle.clone()));
self.saved = Arc::new(snapshot.bundle.clone());
self.recovered_unsaved = false;
}
/// Bind an installed or recovered repair destination to source read there.
/// These bytes may be invalid or differ from the retained accepted model,
/// but must replace the previous disk baseline. Accepted history is retained;
/// applying or undoing to old saved content does not save this destination.
pub(crate) fn bind_repair_baseline(&mut self, source: SourceBundle) {
self.saved = Some(Arc::new(source));
}
/// Recovery changed the current destination, but its source could not be
/// reloaded. No accepted or historical snapshot is known to be saved there.
pub(crate) fn invalidate_saved_baseline(&mut self) {
self.saved = None;
}
/// Check a source Save as before writing any files. A retained included file
/// must never be displaced by the new entry, including on a later Undo.
pub fn check_save_entry(&self, entry: &str) -> Result<(), DocumentError> {
+1 -1
View File
@@ -670,7 +670,7 @@ mod tests {
}
fn semantic_send(app: &mut crate::app::App, event: crate::semantic_review::RowEvent) {
let event = semantic_state(app).address_event(event);
let _ = app.update(Message::Workflow(crate::workflow::Event::semantic(event)));
let _ = app.update(Message::Workflow(crate::workflow::Event::Semantic(event)));
}
fn semantic_decide(app: &mut crate::app::App, facet: FieldFacet, decision: SemanticDecision) {
use crate::semantic_review::RowEvent as Event;
@@ -35,10 +35,10 @@ pub fn modal<'a, Message: 'a, R: renderer::Renderer + 'a>(
}
pub fn field_id(generation: ModelId, path: &str) -> Id {
Id::from(format!("reference:{generation}:{path}"))
Id::new(format!("reference:{generation}:{path}"))
}
pub fn scope_id(session: ModelId) -> Id {
Id::from(format!("reference-form:{session}"))
Id::new(format!("reference-form:{session}"))
}
#[derive(Default)]
@@ -226,7 +226,7 @@ impl<Message, R: renderer::Renderer> Widget<Message, Theme, R> for Field<'_, Mes
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
if let Some(on_open) = &self.on_open {
if self.on_open.is_some() {
let state = tree.state.downcast_mut::<Interaction>();
let visible = layout.bounds().intersection(viewport);
let hovered = visible.is_some_and(|bounds| cursor.is_over(bounds));
@@ -260,7 +260,7 @@ impl<Message, R: renderer::Renderer> Widget<Message, Theme, R> for Field<'_, Mes
_ => false,
};
if activate && let Some(bounds) = state.anchor {
shell.publish(on_open(bounds));
shell.publish(self.on_open.as_ref().unwrap()(bounds));
shell.capture_event();
return;
}
@@ -437,7 +437,7 @@ pub fn focus_operation(
) -> impl Operation + 'static {
focus::exact(session, generation, path, step)
}
pub fn focus<Message: Send + 'static>(
pub fn focus<Message>(
session: ModelId,
generation: ModelId,
path: &str,
@@ -183,19 +183,9 @@ impl Operation for Collect {
ancestor.bounds.y,
ancestor.bounds.height,
);
// Iced reports rounded drawing offsets, while capacity can be
// fractional. Clamping an already-visible axis can move it a pixel.
let delta = Vector::new(
if dx == 0. {
0.
} else {
(ancestor.offset.x + dx).clamp(0., ancestor.capacity.x) - ancestor.offset.x
},
if dy == 0. {
0.
} else {
(ancestor.offset.y + dy).clamp(0., ancestor.capacity.y) - ancestor.offset.y
},
(ancestor.offset.x + dx).clamp(0., ancestor.capacity.x) - ancestor.offset.x,
(ancestor.offset.y + dy).clamp(0., ancestor.capacity.y) - ancestor.offset.y,
);
if delta != Vector::ZERO {
shifts.insert(ancestor.index, delta);
+16 -32
View File
@@ -445,32 +445,28 @@ fn article(topic: Topic) -> Article {
walkthrough(
"Describe a path through exchanges",
&[
"On an empty path, choose Work (W) from the right palette and click the canvas. You can use existing work or stage a new source in that form; exchanges still need explicit compatible source and target work.",
"A blank path offers Add first activity/function contribution. You can use existing work or stage a new source in that form; exchanges still need explicit compatible source and target work.",
"Open Behavior & scenarios → New. Choose Operational process in OA or Functional chain in SA, enter its exact name, then Create and open. The new behavior belongs at the layer root and opens immediately; Undo removes the creation as one edit.",
"Choose Work (W) or Exchange (E) from the permanent palette on the right, then a highlighted position. Hover a palette button to read its tool name. In Work, Create new source keeps the contribution name and position while you name its Activity/Function and choose its semantic parent. Use this new source stages it; Create commits source and contribution in one Undo. An adjacent exchange may require a particular existing work identity.",
"Choose Sequence link (->) and click or drag from the preceding contribution to the following contribution. Choose Select contributions or paths (S) to drag a selected path endpoint and reconnect it. A contribution's ↕ handle reorders contributions within the same column; links determine the columns. These are local contributions and sequence links, not new architecture exchanges.",
"Select a contribution, then add another after it. Details → Two paths... creates a split, two paths and their merge in one undoable edit. Enter explicit alternative conditions.",
"Open Tools and choose Work or Exchange, then a highlighted position. In Work, Create new source keeps the contribution name and position while you name its Activity/Function and choose its semantic parent. Use this new source stages it; Create commits source and contribution in one Undo. An adjacent exchange may require a particular existing work identity.",
"In Tools, choose Sequence link and click or drag from the preceding contribution to the following contribution. Choose Tools → Select / reorder to drag a selected path endpoint and reconnect it; a contribution's ↕ handle changes its position within a column. These are local contributions and sequence links, not new architecture exchanges.",
"Select a contribution, then add another after it. Tools → Two paths… creates a split, two paths and their merge in one undoable edit. Enter explicit alternative conditions. Fit overview uses the visible canvas area; 100% keeps working scale with scrollbars. If the minimum zoom is reached, scroll to inspect the remainder.",
"Use Chain involvement from the Process/Functional chain to the capability it supports. Read Model checks for unfinished or disconnected paths.",
],
),
explain(
"Navigate the canvas",
"The header and palette stay available while Details and creation forms overlay the canvas. Fit frames the contents; −/+ changes zoom and 100% restores working scale. Focus the canvas to zoom with the wheel or pan with middle-button drag or Space + left drag. F6 enters or leaves canvas controls; Tab moves to the next control, Enter/Space activates it, and Escape leaves canvas controls or cancels a gesture.",
),
explain(
"Choose the right behavioral description",
"A chain concentrates on a route through existing exchanges. A scenario can also carry actions, state observations, lifelines and nested fragments. The architecture diagram remains a view of the possible relationships rather than an execution trace.",
),
explain(
"Review the path as an engineer",
"Check that exchanges express meaningful handoffs and that the links and branch guards support the selected outcome. Use Reuse process / chain (ref) in the palette to involve an existing process or chain as a nested reference. For an imported ordered exchange path, Details → Append exchange... continues that path; Edit path links and guards exposes its implied links for topology editing. Conversion preserves the original order in one undoable change.",
"Check that exchanges express meaningful handoffs and that the links and branch guards support the selected outcome. Use Tools → Reuse to involve an existing process or chain as a nested reference. For an imported ordered exchange path, Append exchange… continues that path; Convert ordered path to explicit links exposes its implied links for topology editing. Conversion preserves the original order in one undoable change.",
),
walkthrough(
"Connect a particular use of a procedure",
&[
"Choose Focus canvas for more working space; Restore panels brings the explorer and properties back. Select a contribution inside a reusable preview, then open Details to read its complete caller and reference identity.",
"In Details, use Expand nested reference or Fold nested reference to inspect that instance. Open source or Open contribution definition visits the shared definition; Back to caller returns to the original caller context.",
"Open Details → Qualified link / assembly... and choose its source and target. Use Enter reference to open an instance's contributions, Up one reference for its caller scope, or Root for the link owner. Search and page through all contributions in that scope, then select the exact endpoint. The first and second uses of a procedure remain different choices even when their work has the same name.",
"Choose Focus canvas for more working space; Restore panels brings the explorer and properties back. Select a contribution inside a reusable preview, then open Selection details to read its complete caller and reference identity.",
"In Selection details, use Expand nested reference or Fold nested reference to inspect that instance. Open source or Open contribution definition visits the shared definition; Back to caller returns to the original caller context.",
"Open Tools → Qualified link / assembly… and choose its source and target. Use Enter reference to open an instance's contributions, Up one reference for its caller scope, or Root for the link owner. Search and page through all contributions in that scope, then select the exact endpoint. The first and second uses of a procedure remain different choices even when their work has the same name.",
"Choose Precedence to describe order and its guard, Exchange assembly to identify a work exchange between the contributions, or Common work incidence to state that both use the same work. Select the engineering identity when required, then Apply link.",
],
),
@@ -488,40 +484,28 @@ fn article(topic: Topic) -> Article {
const {
Article {
summary: "Use a scenario to tell a concrete behavioral story, including repeated messages, actions, state observations and guarded alternatives.",
keywords: "scenario sequence lifeline lifetime lifecycle birth death capsule sender receiver fragment branch alternative loop guard parallel strict optional occurrence exchange action state interface operation allocation call reply unresolved",
keywords: "scenario sequence lifeline sender receiver fragment branch alternative loop guard parallel strict optional occurrence exchange action state interface operation allocation call reply unresolved",
sections: &[
walkthrough(
"Create a scenario",
&[
"Open Behavior & scenarios → New → Scenario and enter its name. Explicitly choose Activities or Participants in OA, or Functions or System boundary in SA. Add optional context only when needed: this is a reference, while the scenario itself belongs at the layer root. Create and open starts the empty scenario.",
"Choose Lifeline (L) in the permanent palette on the right, then click the canvas. Choose existing work/participant or Create new source. The explicit viewpoint limits its kinds; choose participant kinds deliberately. Source name and lifeline name are separate, and the source semantic parent is independent of the scenario. No allocation or capability involvement is inferred.",
"Choose a message style from the palette; hover a button to read its tool name. Start at a highlighted sequence position on a lifeline, then click or drag to the receiving lifeline. Complete the exchange and details in the form, which retains that sequence position. Escape cancels the gesture.",
"Call and correlated reply (C/R) creates both occurrences in one edit. Reply to existing call offers valid unanswered calls. A local timer has an expression and a lifeline, with no global exchange. Create and delete messages describe participant lifetime; Model checks also flag risks inside repeated loops.",
"Choose Action (A), State observation (St), Reuse scenario (ref) or Fragment (alt) and click the intended sequence position. Clicking an action or observation on a lifeline also selects that lifeline. Complete any remaining choices before Create.",
"Choose Select / reorder occurrences (S) and drag an occurrence itself to another position within the same sequence. A drag into another fragment operand is refused. Open Details for the ordered list's up/down controls or Properties for the selected occurrence, including its message values.",
"Use Add first lifeline on an empty scenario, or + Lifeline. Choose existing work/participant or Create new source. The explicit viewpoint limits its kinds; choose participant kinds deliberately. Source name and lifeline name are separate, and the source semantic parent is independent of the scenario. No allocation or capability involvement is inferred.",
"Choose a message style in the tool selector. Start at a highlighted sequence position on a lifeline, then click or drag to the receiving lifeline. Complete the exchange and details in the form, which retains that sequence position. Escape cancels the gesture.",
"Call + correlated reply creates both occurrences in one edit. Reply to existing call offers valid unanswered calls. A local timer has an expression and a lifeline, with no global exchange. Create and delete messages describe participant lifetime; Model checks also flag risks inside repeated loops.",
"Choose Add action, Observe state, Reuse scenario or Add fragment and click the intended sequence position. Clicking an action or observation on a lifeline also selects that lifeline. Complete any remaining choices before Create.",
"In Select / reorder, click an occurrence and drag its ↕ handle to another position within the same sequence. The ordered list's up/down controls provide another way to reorder. Select an occurrence to inspect its particular use of the exchange, including its message values.",
],
),
explain(
"Navigate the canvas",
"The header and palette stay available while Details and creation forms overlay the canvas. Fit frames the contents; −/+ changes zoom and 100% restores working scale. Focus the canvas to zoom with the wheel or pan with middle-button drag or Space + left drag. F6 enters or leaves canvas controls; Tab moves to the next control, Enter/Space activates it, and Escape leaves canvas controls or cancels a gesture.",
),
walkthrough(
"Describe alternatives",
&[
"Choose Fragment (alt), click its sequence position and select ALT, PAR, LOOP or OPT. Enter the branch conditions; the tool creates the fragment and its operands together.",
"Choose Add fragment, click its sequence position and select ALT, PAR, LOOP or OPT. Enter the branch conditions; the tool creates the fragment and its operands together.",
"Click the highlighted position inside an operand to place the next message, action or reference there. The form shows its destination and insertion position. Append to this sequence instead explicitly moves the pending insertion to that sequence's end.",
"Choose Reuse scenario and click its position, then bind every referenced lifeline to a compatible local lifeline explicitly. Select the resulting use and choose Details → Open definition. Back returns to that exact use and its viewport after you apply or cancel pending edits.",
"Choose Reuse scenario and click its position, then bind every referenced lifeline to a compatible local lifeline explicitly. Select the resulting use and choose Open definition. Back to caller returns to that exact use and its viewport after you apply or cancel pending edits.",
"Inspect the scenario projection and Model checks. State exactly what each guard means, including units and assumptions where relevant.",
],
),
explain(
"Read lifeline creation and deletion",
"The Lane header and dotted guide identify a lifeline; the guide alone does not establish its lifetime. The stronger solid line shows where the lifeline is known to exist on this scenario path. Create starts that line at a named capsule; Delete ends it at an X. Select the capsule or X to edit that message occurrence. Select the header or guide to inspect the lifeline.",
),
explain(
"Distinguish uncertain lifetime from an unanswered call",
"A ? means lifetime depends on an alternative, optional, repeated or parallel path. Operands have independent lifetimes: Delete in one alternative does not delete a lifeline in another. Beyond such a fragment, the guide can continue without a definite solid line. This uncertainty is not by itself a model error. The … beside an activation marks a Call that remains unanswered on that path. A reused scenario's internal Create/Delete events appear when you choose Open definition; they do not create or delete the caller's lifelines.",
),
walkthrough(
"Invoke an interface operation",
&[
@@ -694,7 +694,7 @@ impl State {
Fill,
widget::{button, checkbox, column, pick_list, row, text},
};
let message = |event| Message::Workflow(crate::workflow::Event::semantic(event));
let message = |event| Message::Workflow(crate::workflow::Event::Semantic(event));
let mut available: Vec<_> = self
.base
.traces
@@ -821,12 +821,8 @@ impl State {
.active
.view(&self.base, header.into(), self.can_apply())
.map(move |message| match message {
Message::Workflow(crate::workflow::Event::Semantic(event)) => {
let event = match *event {
Event::Editor(event) => self.address_event(event),
other => other,
};
Message::Workflow(crate::workflow::Event::semantic(event))
Message::Workflow(crate::workflow::Event::Semantic(Event::Editor(event))) => {
Message::Workflow(crate::workflow::Event::Semantic(self.address_event(event)))
}
other => other,
});
@@ -522,10 +522,10 @@ fn batch_apply_is_one_exact_source_and_history_undo_redo_roundtrip() {
&mut app,
RowEvent::Input(path(f.direction, "minimum"), "120".into()),
);
let _ = app.update(Message::Workflow(crate::workflow::Event::semantic(
let _ = app.update(Message::Workflow(crate::workflow::Event::Semantic(
Event::ChooseTrace(view::choice(&f.project, f.traces[1])),
)));
let _ = app.update(Message::Workflow(crate::workflow::Event::semantic(
let _ = app.update(Message::Workflow(crate::workflow::Event::Semantic(
Event::OpenTrace,
)));
tests::decide(
+1 -1
View File
@@ -926,5 +926,5 @@ pub fn form_event(message: Message) -> Message {
Message::FieldReferenceEvent(event) => RowEvent::ReferenceEvent(event),
_ => RowEvent::Preview,
};
Message::Workflow(crate::workflow::Event::semantic(Event::Editor(event)))
Message::Workflow(crate::workflow::Event::Semantic(Event::Editor(event)))
}
@@ -3,7 +3,7 @@ use crate::{app::App, document::Document, workflow::Workflow};
use serde_json::json;
pub(super) fn send(app: &mut App, event: RowEvent) {
let _ = app.update(Message::Workflow(crate::workflow::Event::semantic(
let _ = app.update(Message::Workflow(crate::workflow::Event::Semantic(
state(app).address_event(event),
)));
}
@@ -13,7 +13,7 @@ use iced::{
mod reserved_artifact_tests;
fn event(event: RowEvent) -> Message {
Message::Workflow(crate::workflow::Event::semantic(Event::Editor(event)))
Message::Workflow(crate::workflow::Event::Semantic(Event::Editor(event)))
}
fn action<'a>(
label: impl Into<String>,
-385
View File
@@ -1663,388 +1663,3 @@ fn adding_bend_between_close_ports_stays_outside_owner_clearance() {
}]
);
}
#[test]
fn child_work_preserves_allocated_context_and_atomic_history() {
use oa_core::projection::project_view;
for (layer, performer_kind, work_kind) in [
(Layer::Oa, Kind::Entity, Kind::Activity),
(Layer::Sa, Kind::System, Kind::Function),
] {
for folded in [false, true] {
let mut p = Project::empty();
let performer = p
.add_element(performer_kind, layer, "Performer", None)
.unwrap();
let work = p
.add_element(work_kind, layer, "Parent work", None)
.unwrap();
let allocation = p.relate(
RelationKind::Allocation,
"Engineering assignment",
performer,
work,
);
p.relations.get_mut(&allocation).unwrap().description =
"Retain assignment metadata".into();
let original = p.relations[&allocation].clone();
let diagram = p.ensure_diagram(layer);
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation;
let performer_node = p.place(diagram, performer, Position { x: 60., y: 60. });
let work_node = p.place(diagram, work, Position { x: 84., y: 124. });
p = execute_batch(&p, &[Command::Layout(diagram)]).unwrap();
for node in [performer_node, work_node] {
p.diagrams
.get_mut(&diagram)
.unwrap()
.nodes
.get_mut(&node)
.unwrap()
.collapsed = folded;
}
let mut app = App::with_project(p);
send(&mut app, Message::Layer(layer));
send(&mut app, Message::Page(Page::Diagram));
send(
&mut app,
Message::Diagram(crate::app::Choice {
id: diagram,
label: String::new(),
}),
);
let mut first = None;
for step in 0..3 {
let parent = if step == 2 { first.unwrap() } else { work };
send(&mut app, Message::Select(parent));
let before = app.project().clone();
let before_source = app.document.bundle().clone();
send(&mut app, Message::CreateContextElement(work_kind, true));
let created = app.selected;
assert!(
!before.elements.contains_key(&created),
"{layer:?} step {step}: {}",
app.status
);
assert_eq!(app.project().elements.len(), before.elements.len() + 1);
assert_eq!(app.project().elements[&created].parent, Some(parent));
for (id, element) in &before.elements {
assert_eq!(&app.project().elements[id], element);
}
assert!(
!oa_core::validation::validate(app.project())
.iter()
.any(|i| i.severity == oa_core::validation::Severity::Error)
);
assert_eq!(
oa_core::validation::allocation_summary(app.project(), work),
Some(performer)
);
assert_eq!(
oa_core::validation::effective_performer(app.project(), created),
Some(performer)
);
assert!(
!app.project()
.relations
.values()
.any(|r| r.kind() == RelationKind::Allocation && r.target() == parent)
);
if step != 1 {
let mut expected = original.clone();
if let RelationBody::Allocation { target, .. } = &mut expected.body {
*target = created;
}
assert_eq!(app.project().relations[&allocation], expected);
} else {
assert_eq!(
app.project().relations[&allocation],
before.relations[&allocation]
);
}
assert_eq!(
app.project().relations.len(),
before.relations.len() + usize::from(step == 1)
);
let view = project_view(app.project(), diagram);
let parent_node = view.nodes.iter().find(|n| n.element == parent).unwrap();
let child_node = view.nodes.iter().find(|n| n.element == created).unwrap();
let root_node = view.nodes.iter().find(|n| n.element == work).unwrap();
assert_eq!(root_node.visual_parent, Some(performer_node));
assert_eq!(child_node.visual_parent, Some(parent_node.id));
assert!(!app.project().diagrams[&diagram].nodes[&performer_node].collapsed);
assert!(!app.project().diagrams[&diagram].nodes[&parent_node.id].collapsed);
assert!(child_node.position.x >= parent_node.position.x);
assert!(child_node.position.y >= parent_node.position.y + 64.);
assert!(
child_node.position.x + child_node.size.width
<= parent_node.position.x + parent_node.size.width
);
assert!(
child_node.position.y + child_node.size.height
<= parent_node.position.y + parent_node.size.height
);
for sibling in view
.nodes
.iter()
.filter(|n| n.visual_parent == Some(parent_node.id) && n.id != child_node.id)
{
assert!(sibling.position.y + sibling.size.height <= child_node.position.y);
}
let after = app.project().clone();
let after_source = app.document.bundle().clone();
send(&mut app, Message::Undo);
assert_eq!(
app.project(),
&before,
"creation, allocation and reveal/reflow undo together"
);
assert_eq!(app.document.bundle(), &before_source);
send(&mut app, Message::Redo);
assert_eq!(app.project(), &after);
assert_eq!(app.document.bundle(), &after_source);
if step == 0 {
first = Some(created);
}
}
}
}
}
#[test]
fn child_work_does_not_invent_unallocated_or_mixed_performers() {
for (layer, performer_kind, other_kind, work_kind) in [
(Layer::Oa, Kind::Entity, Kind::Entity, Kind::Activity),
(Layer::Sa, Kind::System, Kind::SystemActor, Kind::Function),
] {
// Root leaf, partly unallocated composite, and mixed-performer composite.
for context in 0..3 {
let mut p = Project::empty();
let performer = p
.add_element(performer_kind, layer, "Performer", None)
.unwrap();
let other = p
.add_element(other_kind, layer, "Other performer", None)
.unwrap();
let work = p
.add_element(work_kind, layer, "Parent work", None)
.unwrap();
let mut placed = vec![performer, other, work];
if context != 0 {
let first = p
.add_element(work_kind, layer, "First leaf", Some(work))
.unwrap();
let second = p
.add_element(work_kind, layer, "Second leaf", Some(work))
.unwrap();
p.relate(
RelationKind::Allocation,
"First assignment",
performer,
first,
);
if context == 2 {
p.relate(RelationKind::Allocation, "Other assignment", other, second);
}
placed.extend([first, second]);
}
assert_eq!(oa_core::validation::allocation_summary(&p, work), None);
let diagram = p.ensure_diagram(layer);
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation;
for id in placed {
p.place(diagram, id, Position { x: 60., y: 60. });
}
p = execute_batch(&p, &[Command::Layout(diagram)]).unwrap();
let mut app = App::with_project(p);
send(&mut app, Message::Layer(layer));
send(&mut app, Message::Page(Page::Diagram));
send(
&mut app,
Message::Diagram(crate::app::Choice {
id: diagram,
label: String::new(),
}),
);
send(&mut app, Message::Select(work));
let before = app.project().clone();
let before_source = app.document.bundle().clone();
send(&mut app, Message::CreateContextElement(work_kind, true));
let child = app.selected;
assert!(!before.elements.contains_key(&child), "{}", app.status);
assert_eq!(app.project().elements[&child].parent, Some(work));
assert_eq!(app.project().relations, before.relations);
assert_eq!(
oa_core::validation::effective_performer(app.project(), child),
None
);
assert_eq!(
oa_core::validation::allocation_summary(app.project(), work),
None
);
let after = app.project().clone();
let after_source = app.document.bundle().clone();
send(&mut app, Message::Undo);
assert_eq!(app.project(), &before);
assert_eq!(app.document.bundle(), &before_source);
send(&mut app, Message::Redo);
assert_eq!(app.project(), &after);
assert_eq!(app.document.bundle(), &after_source);
}
}
}
#[test]
fn child_work_transfers_leaf_roles_without_fabricating_responsibilities() {
// Direct allocation + matching roles, uniquely realized roles only,
// unresolved roles, and roles realized by different performers.
for context in 0..4 {
let mut p = Project::empty();
let performer = p
.add_element(Kind::Entity, Layer::Oa, "Performer", None)
.unwrap();
let other = p
.add_element(Kind::Entity, Layer::Oa, "Other performer", None)
.unwrap();
let work = p
.add_element(Kind::Activity, Layer::Oa, "Responsible work", None)
.unwrap();
let mut responsibilities = Vec::new();
if context == 0 {
responsibilities.push(p.relate(
RelationKind::Allocation,
"Direct responsibility",
performer,
work,
));
}
for index in 0..2 {
let role = p
.add_element(Kind::Role, Layer::Oa, &format!("Role {index}"), None)
.unwrap();
responsibilities.push(p.relate(
RelationKind::RoleAssignment,
&format!("Assigned role {index}"),
role,
work,
));
if context != 2 {
let source = if context == 3 && index == 1 {
other
} else {
performer
};
p.relate(RelationKind::RoleRealization, "Realizes role", source, role);
}
}
for (index, id) in responsibilities.iter().enumerate() {
let relation = p.relations.get_mut(id).unwrap();
relation.description =
format!("Responsibility {index}: preserve engineering rationale");
let link = Id::new_v4();
p.artifact_links.insert(
link,
ArtifactLink {
id: link,
alias: format!("responsibility_evidence_{index}"),
name: format!("Responsibility evidence {index}"),
description: "Preserve the reference to this exact relation".into(),
owner: *id,
relation: ArtifactRelation::References,
uri: format!("https://example.invalid/responsibility/{index}"),
revision: Some("review-7".into()),
fragment: Some("assignment".into()),
},
);
}
let original_relations = p.relations.clone();
let diagram = p.ensure_diagram(Layer::Oa);
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation;
p.place(diagram, performer, Position { x: 60., y: 60. });
p.place(diagram, work, Position { x: 84., y: 124. });
p = execute_batch(&p, &[Command::Layout(diagram)]).unwrap();
let expected_performer = (context < 2).then_some(performer);
assert_eq!(
oa_core::validation::allocation_summary(&p, work),
expected_performer
);
let mut app = App::with_project(p);
send(&mut app, Message::Page(Page::Diagram));
send(
&mut app,
Message::Diagram(crate::app::Choice {
id: diagram,
label: String::new(),
}),
);
let mut first = None;
for step in 0..2 {
send(&mut app, Message::Select(work));
let before = app.project().clone();
let before_source = app.document.bundle().clone();
send(
&mut app,
Message::CreateContextElement(Kind::Activity, true),
);
let child = app.selected;
assert!(
!before.elements.contains_key(&child),
"context {context}, step {step}: {}",
app.status
);
assert_eq!(app.project().elements[&child].parent, Some(work));
if step == 0 {
first = Some(child);
}
for (id, original) in &original_relations {
let mut expected = original.clone();
if responsibilities.contains(id) {
match &mut expected.body {
RelationBody::Allocation { target, .. }
| RelationBody::RoleAssignment { target, .. } => *target = first.unwrap(),
_ => unreachable!(),
}
}
assert_eq!(app.project().relations[id], expected);
}
assert_eq!(app.project().artifact_links, before.artifact_links);
assert!(!app.project().artifact_links.is_empty());
let added = usize::from(step == 1 && expected_performer.is_some());
assert_eq!(
app.project().relations.len(),
before.relations.len() + added
);
assert_eq!(
oa_core::validation::effective_performer(app.project(), child),
expected_performer
);
assert_eq!(
app.project()
.relations
.values()
.filter(|r| r.kind() == RelationKind::RoleAssignment && r.target() == child)
.count(),
if step == 0 { 2 } else { 0 }
);
if context != 0 && step == 0 {
assert!(
!app.project()
.relations
.values()
.any(|r| r.kind() == RelationKind::Allocation)
);
}
assert!(
!oa_core::validation::validate(app.project())
.iter()
.any(|i| i.severity == oa_core::validation::Severity::Error)
);
let after = app.project().clone();
let after_source = app.document.bundle().clone();
send(&mut app, Message::Undo);
assert_eq!(app.project(), &before);
assert_eq!(app.document.bundle(), &before_source);
send(&mut app, Message::Redo);
assert_eq!(app.project(), &after);
assert_eq!(app.document.bundle(), &after_source);
}
}
}
+2 -59
View File
@@ -12,7 +12,6 @@ use iced::{
},
};
use oa_core::*;
use unicode_segmentation::UnicodeSegmentation;
pub fn label<'a>(s: impl Into<String>) -> widget::Text<'a> {
text(s.into()).size(11).color(style::MUTED)
@@ -25,12 +24,10 @@ pub fn action<'a>(s: &str, m: Message) -> widget::Button<'a, Message> {
}
pub fn short(s: &str, limit: usize) -> String {
let s = s.split_whitespace().collect::<Vec<_>>().join(" ");
if s.graphemes(true).count() > limit {
if s.chars().count() > limit {
format!(
"{}…",
s.graphemes(true)
.take(limit.saturating_sub(1))
.collect::<String>()
s.chars().take(limit.saturating_sub(1)).collect::<String>()
)
} else {
s
@@ -1293,57 +1290,3 @@ fn checks(app: &App) -> View<'_, Message> {
}
scrollable(out.padding(24)).into()
}
#[cfg(test)]
mod tests {
use super::short;
#[test]
fn short_preserves_native_lifeline_clusters_at_cutoff() {
let oa = "abcdefghijklmnope\u{0301}尾";
assert_eq!(short(oa, 17), "abcdefghijklmnop…");
assert_eq!(short(oa, 18), oa);
assert_eq!(short(oa, 19), oa);
let sa = "abcdefghijklmnop👩\u{200d}🔬 tail";
assert_eq!(short(sa, 17), "abcdefghijklmnop…");
assert_eq!(short(sa, 18), "abcdefghijklmnop👩\u{200d}🔬…");
assert_eq!(short(sa, 19), "abcdefghijklmnop👩\u{200d}🔬 …");
assert_eq!(short(sa, 22), sa);
}
#[test]
fn short_never_splits_retained_extended_graphemes() {
for cluster in ["e\u{0301}", "👩\u{200d}🔬", "👍🏽", "🇫🇷"] {
let value = format!("A{cluster}BC");
assert_eq!(short(&value, 2), "A…");
assert_eq!(short(&value, 3), format!("A{cluster}…"));
assert_eq!(short(&value, 4), value);
}
}
#[test]
fn short_preserves_ascii_cjk_and_whitespace_policy() {
assert_eq!(short("abcd", 3), "ab…");
assert_eq!(short("abcd", 4), "abcd");
assert_eq!(short("abcd", 5), "abcd");
assert_eq!(short("甲乙丙丁", 3), "甲乙…");
assert_eq!(short("甲乙丙丁", 4), "甲乙丙丁");
assert_eq!(short(" \tA\r\n e\u{0301}\u{2003}B ", 5), "A e\u{0301} B");
assert_eq!(short(" \tA\r\n e\u{0301}\u{2003}B ", 4), "A e\u{0301}…");
}
#[test]
fn short_preserves_empty_and_small_limit_policy() {
for limit in [0, 1, 2] {
assert_eq!(short("", limit), "");
assert_eq!(short(" \t\r\n", limit), "");
}
assert_eq!(short("A", 0), "…");
assert_eq!(short("A", 1), "A");
assert_eq!(short("AB", 1), "…");
assert_eq!(short("e\u{0301}", 0), "…");
assert_eq!(short("e\u{0301}", 1), "e\u{0301}");
assert_eq!(short("e\u{0301}B", 1), "…");
}
}
+1 -6
View File
@@ -19,7 +19,7 @@ mod transition_tests;
#[derive(Debug, Clone)]
pub enum Event {
Semantic(Box<crate::semantic_review::Event>),
Semantic(crate::semantic_review::Event),
Port(crate::port_workflow::Event),
ExchangeItem(Id, bool),
Endpoint(bool, EndpointChoice),
@@ -41,11 +41,6 @@ pub enum Event {
Resolve(Id, TransitionField, FieldDecision),
Preview,
}
impl Event {
pub fn semantic(event: crate::semantic_review::Event) -> Self {
Self::Semantic(Box::new(event))
}
}
pub enum Workflow {
Semantic(Box<crate::semantic_review::State>),
Ports(Box<crate::port_workflow::State>),
+5 -49
View File
@@ -1319,25 +1319,16 @@ references a Call occurrence in the same scenario. The call MUST precede the
reply on a guaranteed common sequence/enclosing branch path. Reply lifelines
reverse the call's sender/receiver; it may reuse the call's exchange in reverse
or use an explicit reverse-direction exchange. A second reply to the same call
that can follow another reply on a shared execution path, including conditional
or parallel operands, is an error. Missing reply correlation is a draft
on that guaranteed path is an error. Missing reply correlation is a draft
warning; non-Reply messages MUST NOT have <span class="docutils literal">reply_to</span>.</p>
<p>Create identifies the receiver lifeline's creation. Another use that can precede
its creation on a shared execution path is an error. Delete identifies its
destruction; another use that can follow it on a shared execution path is an
error. These checks include conditional and parallel operands but exclude
mutually exclusive alternatives. Ordering checks respect branch ownership; they
MUST NOT flatten alternative branches into a false unconditional order. A Timer is
<p>Create identifies the receiver lifeline's creation. An earlier guaranteed use
of that lifeline is an error. Delete identifies its destruction; a later
guaranteed use is an error. Ordering checks respect branch ownership; they MUST
NOT flatten alternative branches into a false unconditional order. A Timer is
local to its sender lifeline, has no global exchange or values, and has either
no receiver or the same receiver as sender. Its nonempty expression describes
the delay/deadline; an empty expression warns. Conditions and expressions are
stored engineering annotations, not an executable simulation language.</p>
<p>These ordering checks compare distinct stored occurrences along structural
paths; they do not simulate repeated loop iterations. Repetition risks use
<span class="docutils literal">reply_repetition</span> and <span class="docutils literal">lifeline_repetition</span> warnings separately. For example,
a loop containing one Reply to an outer Call can warn about repetition without
containing two distinct replies; a same-operand Create/use/Delete lifetime can
repeat without its previous iteration being treated as an earlier stored use.</p>
<p>Each MessageValue is <span class="docutils literal">{ path [&#64;parameter]; field &#64;data_field; expression &quot;value expression&quot;; }</span>.
Its identity is the pair (path, field), so two uses of one Record can have
different values for the same terminal field. Path contains enclosing DataField
@@ -2649,41 +2640,6 @@ leaving dangling references. Deleting a sequence does not delete the semantic
exchanges it references. Cross-layer counterparts are not owned descendants.
Text edits describe a complete candidate and must remove or repair dependencies
explicitly before Apply succeeds.</p>
<section id="scenario-lifetime-presentation">
<h3>8.1. Scenario lifetime presentation</h3>
<p>Scenario lifetime geometry is derived from the stored occurrences and their
sequence/operand ownership. It adds no model records, source annotations or
history entries. A lifeline's identity header and neutral editing guide remain
available before creation, after deletion and when its represented element is
unfinished. The guide MUST be visually distinguishable from a live interval;
it is an editing axis, not a statement that the participant exists.</p>
<p>A directly ordered Create starts the receiver's live interval at its occurrence;
Delete ends that receiver's interval at its occurrence. Create uses a named birth
marker and Delete an X, each selectable through the original occurrence identity.
The message's receiving endpoint MUST align with its marker, including self
messages. A pre-existing lifeline without a creation has a live interval from
below its header; without deletion it extends to the visible scenario bottom,
including in an empty scenario. A missing lifecycle receiver MUST NOT be inferred from
the exchange. An explicit local receiver remains meaningful when its exchange
reference is unfinished.</p>
<p>Operands MUST be projected independently from their enclosing entry context.
A lifetime change in an alternative, optional, loop or parallel operand MUST
NOT imply the same change in its displayed sibling or a definite lifetime
after the fragment. When local structure does not establish one lifetime across
the represented paths, the continuation is visually unresolved. A subsequent
explicit Create or Delete in the enclosing sequence can establish a definite interval.
Neither guard text, screen order nor a guessed loop count proves execution.
ScenarioUse remains opaque: opening its definition may show its lifecycle,
but a callee's geometry MUST NOT create or destroy a caller lifeline.</p>
<p>Call activation is distinct from lifetime. A branch-local Delete clips that
branch's visible activation at the death marker without inventing a Reply,
removing an unresolved-call warning or terminating sibling paths.
Solid activation bars are restricted to definite live intervals; an unresolved
continuation retains its uncertainty/call diagnostic rather than depicting
definite survival after a possible deletion. Selection,
insertion slots, source edits and Undo/Redo retain the same semantic identities
and validation boundary regardless of the rendered intervals.</p>
</section>
</section>
<section id="persistence-and-explicit-migration">
<h2>9. Persistence and explicit migration</h2>
+5 -54
View File
@@ -680,28 +680,18 @@ references a Call occurrence in the same scenario. The call MUST precede the
reply on a guaranteed common sequence/enclosing branch path. Reply lifelines
reverse the call's sender/receiver; it may reuse the call's exchange in reverse
or use an explicit reverse-direction exchange. A second reply to the same call
that can follow another reply on a shared execution path, including conditional
or parallel operands, is an error. Missing reply correlation is a draft
on that guaranteed path is an error. Missing reply correlation is a draft
warning; non-Reply messages MUST NOT have ``reply_to``.
Create identifies the receiver lifeline's creation. Another use that can precede
its creation on a shared execution path is an error. Delete identifies its
destruction; another use that can follow it on a shared execution path is an
error. These checks include conditional and parallel operands but exclude
mutually exclusive alternatives. Ordering checks respect branch ownership; they
MUST NOT flatten alternative branches into a false unconditional order. A Timer is
Create identifies the receiver lifeline's creation. An earlier guaranteed use
of that lifeline is an error. Delete identifies its destruction; a later
guaranteed use is an error. Ordering checks respect branch ownership; they MUST
NOT flatten alternative branches into a false unconditional order. A Timer is
local to its sender lifeline, has no global exchange or values, and has either
no receiver or the same receiver as sender. Its nonempty expression describes
the delay/deadline; an empty expression warns. Conditions and expressions are
stored engineering annotations, not an executable simulation language.
These ordering checks compare distinct stored occurrences along structural
paths; they do not simulate repeated loop iterations. Repetition risks use
``reply_repetition`` and ``lifeline_repetition`` warnings separately. For example,
a loop containing one Reply to an outer Call can warn about repetition without
containing two distinct replies; a same-operand Create/use/Delete lifetime can
repeat without its previous iteration being treated as an earlier stored use.
Each MessageValue is ``{ path [@parameter]; field @data_field; expression "value expression"; }``.
Its identity is the pair (path, field), so two uses of one Record can have
different values for the same terminal field. Path contains enclosing DataField
@@ -2072,45 +2062,6 @@ exchanges it references. Cross-layer counterparts are not owned descendants.
Text edits describe a complete candidate and must remove or repair dependencies
explicitly before Apply succeeds.
8.1. Scenario lifetime presentation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Scenario lifetime geometry is derived from the stored occurrences and their
sequence/operand ownership. It adds no model records, source annotations or
history entries. A lifeline's identity header and neutral editing guide remain
available before creation, after deletion and when its represented element is
unfinished. The guide MUST be visually distinguishable from a live interval;
it is an editing axis, not a statement that the participant exists.
A directly ordered Create starts the receiver's live interval at its occurrence;
Delete ends that receiver's interval at its occurrence. Create uses a named birth
marker and Delete an X, each selectable through the original occurrence identity.
The message's receiving endpoint MUST align with its marker, including self
messages. A pre-existing lifeline without a creation has a live interval from
below its header; without deletion it extends to the visible scenario bottom,
including in an empty scenario. A missing lifecycle receiver MUST NOT be inferred from
the exchange. An explicit local receiver remains meaningful when its exchange
reference is unfinished.
Operands MUST be projected independently from their enclosing entry context.
A lifetime change in an alternative, optional, loop or parallel operand MUST
NOT imply the same change in its displayed sibling or a definite lifetime
after the fragment. When local structure does not establish one lifetime across
the represented paths, the continuation is visually unresolved. A subsequent
explicit Create or Delete in the enclosing sequence can establish a definite interval.
Neither guard text, screen order nor a guessed loop count proves execution.
ScenarioUse remains opaque: opening its definition may show its lifecycle,
but a callee's geometry MUST NOT create or destroy a caller lifeline.
Call activation is distinct from lifetime. A branch-local Delete clips that
branch's visible activation at the death marker without inventing a Reply,
removing an unresolved-call warning or terminating sibling paths.
Solid activation bars are restricted to definite live intervals; an unresolved
continuation retains its uncertainty/call diagnostic rather than depicting
definite survival after a possible deletion. Selection,
insertion slots, source edits and Undo/Redo retain the same semantic identities
and validation boundary regardless of the rendered intervals.
9. Persistence and explicit migration
-------------------------------------
+8 -10
View File
@@ -1,6 +1,6 @@
# Linux packaging and deployment inputs
The packaging command stages **already-built** desktop, server and source CLI executables for `x86_64-unknown-linux-gnu`. It runs on Windows PowerShell 5.1 or PowerShell 7 (including Linux) with Python 3.11+ standard library, Git, and the matching Rust/Cargo dependency cache. It does not build, download, install runtime libraries or launch the application. Python creates and reads back the tar archive; end users do not need Python, PowerShell or Rust.
The packaging command stages **already-built** desktop/server executables for `x86_64-unknown-linux-gnu`. It runs on Windows PowerShell 5.1 or PowerShell 7 (including Linux) with Python 3.11+ standard library, Git, and the matching Rust/Cargo dependency cache. It does not build, download, install runtime libraries or launch the application. Python creates and reads back the tar archive; end users do not need Python, PowerShell or Rust.
Build and validate first in a source checkout. The current runtime validation baseline is Ubuntu 24.04 x86-64. A glibc symbol inventory is not a claim that every distribution with that glibc version supports the renderer and dialogs.
@@ -9,25 +9,23 @@ Build and validate first in a source checkout. The current runtime validation ba
-Version 0.2.0-rc1 -BinaryDirectory ./target/release -Python python3
```
For an explicit-target build, use `target/x86_64-unknown-linux-gnu/release`. A cross-build may supply another directory containing all three executables: `arcadia-studio`, `arcadia-server` and `arcadia`. Select the Rust compiler used for the intended build before packaging; its Linux target libraries, copyright reports and locked source/archive cache must be installed. Read-only `cargo metadata --locked --offline --filter-platform x86_64-unknown-linux-gnu` and Rust version/sysroot queries are the only Rust commands run by packaging. When packaging from Windows, `-Python C:/path/to/python.exe` selects Python explicitly.
For an explicit-target build, use `target/x86_64-unknown-linux-gnu/release`. A cross-build may supply another directory containing both `arcadia-studio` and `arcadia-server`. Select the Rust compiler used for the intended build before packaging; its Linux target libraries, copyright reports and locked source/archive cache must be installed. Read-only `cargo metadata --locked --offline --filter-platform x86_64-unknown-linux-gnu` and Rust version/sysroot queries are the only Rust commands run by packaging. When packaging from Windows, `-Python C:/path/to/python.exe` selects Python explicitly.
The script refuses existing output/claim names. It creates `dist/ArcadiaStudio-<version>-x86_64-unknown-linux-gnu/`, a `.tar.gz`, and an adjacent `.sha256`. A failed attempt leaves its claim and any partial outputs for diagnosis; choose a fresh version or output root rather than overwriting them. Archives use sorted paths, zero timestamps and uid/gid, 0755 for the three Linux executables and 0644 for other files. Every entry is read back and compared against staging, including permissions. The same unchanged inputs on the same Python/zlib implementation produce the same bytes; this does not promise reproducible compiler output.
The script refuses existing output/claim names. It creates `dist/ArcadiaStudio-<version>-x86_64-unknown-linux-gnu/`, a `.tar.gz`, and an adjacent `.sha256`. A failed attempt leaves its claim and any partial outputs for diagnosis; choose a fresh version or output root rather than overwriting them. Archives use sorted paths, zero timestamps and uid/gid, 0755 for the two Linux executables and 0644 for other files. Every entry is read back and compared against staging, including permissions. The same unchanged inputs on the same Python/zlib implementation produce the same bytes; this does not promise reproducible compiler output.
## Contents and provenance
All three executables, the MIT license, a package-specific README, complete tracked examples (including all water-pressure teaching/evidence files), deployment references and the reachable documentation closure are included. Relative Markdown/reST/HTML file links are checked before archiving; upstream license texts are preserved verbatim and their internal source-tree hyperlinks are outside that documentation check. Untracked private files, caches and unrelated harness history are not included merely because they exist on disk.
Both executables, the MIT license, a package-specific README, complete tracked examples (including all water-pressure teaching/evidence files), deployment references and the reachable documentation closure are included. Relative Markdown/reST/HTML file links are checked before archiving; upstream license texts are preserved verbatim and their internal source-tree hyperlinks are outside that documentation check. Untracked private files, caches and unrelated harness history are not included merely because they exist on disk.
`package.json` records input hashes and static ELF dependencies. `SHA256SUMS.txt` inventories every file except itself; the archive checksum covers that inventory too. `dependency-notices.json`, `THIRD-PARTY-NOTICES.txt` and `licenses/` use the same strict collector as Windows: exact target-filtered normal/build dependency graph rooted at `oa-desktop`, `oa-server` and `oa-cli`, locked crate archives, actual source hashes, checksum-bound supplements and embedded fonts, Rust distribution notices and target-library hashes. Unknown sources, absent notices or changed supplements stop packaging. No Linux system libraries are bundled by this pipeline; their runtime distribution supplies them and its notices.
`package.json` records input hashes and static ELF dependencies. `SHA256SUMS.txt` inventories every file except itself; the archive checksum covers that inventory too. `dependency-notices.json`, `THIRD-PARTY-NOTICES.txt` and `licenses/` use the same strict collector as Windows: exact target-filtered normal/build dependency graph, locked crate archives, actual source hashes, checksum-bound supplements and embedded fonts, Rust distribution notices and target-library hashes. Unknown sources, absent notices or changed supplements stop packaging. No Linux system libraries are bundled by this pipeline; their runtime distribution supplies them and its notices.
ELF preflight requires little-endian x86-64 executables/PIE, the glibc dynamic loader, no RPATH/RUNPATH and a reviewed set of direct libc/compiler-runtime imports. Unknown libraries require an explicit packaging design instead of being silently copied from a developer machine. Graphics/window/portal libraries loaded at runtime are outside that static import list.
Optionally pass `-BuildEvidence <json-file>`, containing nonempty `source_identity`, `profile`, `compiler`, exact `target`, and `binary_sha256` keyed by `arcadia-studio`, `arcadia-server` and `arcadia`. Hash strings must be lowercase SHA256. Target and binary hashes are checked; other fields remain explicit caller assertions. Omission records build provenance as unknown. Never label a development binary an optimized release because of its directory or package version.
Optionally pass `-BuildEvidence <json-file>`, containing nonempty `source_identity`, `profile`, `compiler`, exact `target`, and `binary_sha256` keyed by `arcadia-studio` and `arcadia-server`. Hash strings must be lowercase SHA256. Target and binary hashes are checked; other fields remain explicit caller assertions. Omission records build provenance as unknown. Never label a development binary an optimized release because of its directory or package version.
## Native extracted-bundle checks
The source authoring CLI is `./arcadia`. Run `./arcadia --help` for `check`, `lint`, `stabilize` and `rename-alias`; each takes an explicit `.arcadia` entry file. Mutation commands preview by default and require `--write` to apply their candidate; `--json` produces a machine-readable report. Test writes on a separate project copy. CLI use does not require a display, a server or Rust. Include CLI check, preview, write and unchanged-repeat checks in extracted-bundle acceptance, alongside desktop and server checks.
Extract into a fresh user-writable Linux directory and run from an unrelated working directory. Verify SHA256SUMS, file permissions, `file`/`ldd` and actual desktop/runtime libraries. A typical Ubuntu runtime needs X11/XKB and Wayland clients and Vulkan loader/Mesa Vulkan drivers. Linux Open/Save uses the application's path-entry dialog and does not require an xdg-desktop-portal backend. A successful `ldd` alone is insufficient because graphics/window libraries can be loaded dynamically.
Extract into a fresh user-writable Linux directory and run from an unrelated working directory. Verify SHA256SUMS, file permissions, `file`/`ldd` and actual desktop/runtime libraries. A typical Ubuntu runtime needs X11/XKB and Wayland clients, Vulkan loader/Mesa Vulkan drivers, and working session DBus plus an xdg-desktop-portal backend for file dialogs. A successful `ldd` alone is insufficient because these libraries can be loaded dynamically.
For a software Vulkan trial, first locate the installed lavapipe ICD; only if it exists, use:
@@ -35,7 +33,7 @@ For a software Vulkan trial, first locate the installed lavapipe ICD; only if it
WGPU_BACKEND=vulkan VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json ./arcadia-studio
```
A compatible native GPU may run without these variables. WSLg uses real Linux user space with a particular display environment; record the actual X11/Wayland backend and do not infer support for all Linux sessions from one trial. Exercise Home walkthrough, Help/search, Linux Open/Save path entry, existing-file handling, SVG export overwrite confirmation, a multi-file project, diagram/source edits, Undo/Redo and reopening at normal/minimum window sizes. Compare complete saved model/source and inspect stderr, not merely whether a window appeared.
A compatible native GPU may run without these variables. WSLg uses real Linux user space with a particular display environment; record the actual X11/Wayland backend and do not infer support for all Linux sessions from one trial. Exercise Home walkthrough, Help/search, native Open/Save dialogs, a multi-file project, diagram/source edits, Undo/Redo and reopening at normal/minimum window sizes. Compare complete saved model/source and inspect stderr, not merely whether a window appeared.
Run the server against a separate local data directory and unused loopback port. Follow [server configuration](server.md) for health, authentication, ETags, restart and backup/restore. Test create/import, a changed conditional update, unchanged no-op, stale/invalid rejection, and persistence after a clean restart. Record exact binary/archive/source/compiler hashes and platform. Packaging verification is separate from current-tree release acceptance.
+91 -23
View File
@@ -320,29 +320,97 @@ wrong-context edits or inability to preserve reproducible independent studies.
The comparison establishes semantic/text maintenance evidence, not human task
timings or full quantity/library integration acceptance.
## Scenario lifetime rendering integration — 2026-09-11
## Analysis and library contract convergence — 2026-09-11
Codex-ytr83r /root. Adopt the [scoped lifecycle proposal](release-scenario-lifecycle-design-20260910.md)
as derived presentation over existing source-7 semantics. Always retain a neutral
identity header/editing guide; depict definite live intervals separately, with
original-occurrence Create capsules and Delete crosses. Alternative, optional,
loop and parallel operands inherit their enclosing entry independently and leave
an unresolved continuation after a local lifetime change. Reused scenarios remain
opaque. This does not execute guards or add model/source/history records.
Root Codex-956atf read the complete [baseline refinement](release-analysis-baseline-contract-20260911.md),
[library refinement](release-library-compatibility-contract-20260911.md), their
complete fixtures and [independent review](release-refined-contracts-review-20260911.md).
Select the following architecture for production implementation:
Full-height ordinary spines fail to communicate creation/deletion; one global
earliest-birth/latest-death range would falsely order exclusive operands. Both
alternatives lose to the scoped projection. The [independent review](release-scenario-lifecycle-independent-review-20260911.md)
requires true visible root bounds, a complete rendering lane map and local
activation clipping at Delete; insertion bounds/eligible lanes must keep their
separate roles. Receiver endpoints and selectable markers must agree, including
self messages. Those findings determine the integration contract in language
section 8.1 and the real-Scene/native acceptance fixtures.
- Required property-owned Binding values, explicit ModelBaseline/Case selection,
one level of complete-case override maps and shared root checks. Clear removes
an override; Unbound masks the base. Inherited formulas evaluate dependencies
in the requested context. Pure calculations see only explicit parameters.
- Same-layer property dependencies and case targets, with explicit analysis
selection for transitions and exports. Check/case subjects may be absent;
architectural containment must not infer their selection. Validate every
effective dependency graph, including statically referenced unused branches.
- The refined R6 closed algebra, exact rational R7 literal/AST preservation,
affine point/difference distinction and declared-output kind anchoring.
Strict expression operands gather actual missing/failure causes; Boolean
control flow evaluates only the selected branch. Supported domain failures
are distinct from violated predicates and missing values.
- Whole accepted PortableDocument freeze with exact SourceBundle, local
definitions, all imported package contents/pins and validated provenance.
Replay resolves only that immutable private capture. A result report, shallow
case copy or current cache is not a reproducible freeze.
- Exact archive/reference/history identity alongside verified same-origin
typed-contract equivalence. Different nominal origins remain distinct.
Compatibility supplies a scoped expected/actual field-path and operation
correspondence; it never advances a pin or rewrites outside record identities.
Keep qualified neutral definitions, immutable mapped layered definitions and
ordinary editable pattern instances for the first coherent implementation.
- Explicit source preparation/acceptance with complete version-controlled
package/base/map/lineage state, recomputed admission and stale-input rejection.
Exact three-way facets retain independent local edits. Mounted OA/SA transfer
requires a verified exact target-layer export or an explicitly independent
editable local derivative; no relabeling or silent mutation of a mount.
Core/source semantics are unchanged. In section 4.3, replacing the old
guaranteed-only lifetime/duplicate-reply wording with possible shared-path wording
corrects documentation to `validation.rs` and the existing conditional/parallel
regressions; it does not broaden admission or add a new rule. Compilation and
native acceptance of the renderer are still pending at this decision. Revisit the
visual encoding if actual minimum-size engineering workflows make marker identity,
uncertain continuation or the distinction between guide and existence unclear.
These are architecture decisions, not claims that the proposed schema8 examples
are admitted by current source7. Before real consumer integration, enumerate and
implement the class-specific canonical contract over all interpretation-bearing
fields and use sites, exact transitive archive closure and full source/history
boundaries. Retain the frozen120-assertion prototype with its documented C2
threshold-unit coverage gap; production typing must check every operand.
Close independent C1 before evaluator expansion: a request-local, profile-versioned
logical work meter must bound dynamic expression visits, calls, list traversal
and exact-number work. Static acyclicity/depth/node limits do not bound expanded
execution. Cache warmth must not change outcomes; terminal exhaustion stops
further traversal and marks collected causes as incomplete. Numeric ceilings and
the precise charging table require measured foundation implementation before
publishing the profile. The333-node/31-level counterexample and a nearby successful
case are mandatory tests across direct/API/frozen replay.
The exact numeric foundation is the next bounded core implementation. Ordinary
engineering approximations, a complete low-noise text workflow, two-project
library maintenance and native/API/source parity remain required release work.
An exact-only slice cannot complete the user's first release.
## Analysis production staging — 2026-09-11
Root accepts the concrete [production integration sequence](release-analysis-production-integration-plan-20260911.md).
Allocate schema8 to the next coherent production analysis integration. Start with
the actual Project's neutral definition catalog, three analysis element kinds,
typed expressions and a closed Local definition-reference environment. Imported
references will be admitted only with the real package environment; no placeholder
resolver, guessed pin, implicit type compatibility or competing package format.
This stages implementation and does not defer the required maintained libraries.
The first useful gate is the complete persisted/source-authored response budget,
shared two-parameter calculation and check: baseline7/4 seconds versus degraded9/4,
with explicit request context and the same core result through CLI and REST.
Required property fields cannot come from generic factory guesses. Existing
descriptive expressions/unit labels retain their exact old meaning and bytes.
Freeze older top-level wire DTOs before making definitions required in schema8;
reject new fields/constructors in older versions instead of silently dropping them.
Global, scoped parameter, imported and historical identities remain distinct.
Implement pure static numeric type helpers once and reuse them for admission and
runtime. Validate every effective case graph and unused branch before accepting a
candidate; deleting dependencies requires explicit same-batch repair. Numeric
domain failure is not a static type failure. Full evaluator traversal and actual
C1 input, provenance, terminal incomplete causes and immutable replay are required
before claiming evaluated engineering behavior. Until explicit analysis review/
transition facets exist, those entry points must reject unsupported analysis with
a clear capability error, including manual-trace paths that could otherwise appear
to reconcile headers alone.
Logical expression depth128 must be reconciled with bounded actual JSON/source
decoding; do not simply disable recursion limits. Raw byte, decoded string and
aggregate literal/graph/case-admission limits are separate from evaluation fuel.
Concrete decoder/admission ceilings remain subject to bounded boundary and timing
experiments; the implementer must surface those choices before locking the profile.
The current local numeric gate runs against frozen root source. New core contract
work proceeds in an isolated checkout, preserving root's Cargo/native leases and
the other hosts' CLI, staged-source and B4 ownership.
+73 -66
View File
@@ -1,77 +1,84 @@
> Superseded on 2026-09-11. This document is a preserved historical checkpoint.
> Live plans, priorities, ownership and handoffs now reside in [Gitea issue 1](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/1#issuecomment-410) and [issue 2](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/2).
> Do not resume synchronizing this ledger. See [STATE.md](STATE.md) for the integrated checkpoint.
# First release execution plan
Updated 2026-09-11 for the published Codex-ytr83r B4 source checkpoint.
[PROJECT_GOAL.md](../PROJECT_GOAL.md) defines the full outcome;
[STATE.md](STATE.md) scopes evidence/ownership and
[TASK_LEDGER.md](TASK_LEDGER.md) separates facts from decisions.
Updated 2026-09-11. [PROJECT_GOAL.md](../PROJECT_GOAL.md) is the authoritative
current objective. [STATE.md](STATE.md) records exact execution/evidence and leases;
[TASK_LEDGER.md](TASK_LEDGER.md) separates facts, assumptions and decisions.
The complete previous plan is [preserved byte-for-byte](history/2026-09-10-ledgers-before-continuation-refresh/PLAN.md).
Its earlier goal paths, active-process claims and Escape pauses are historical.
## Immediate integration and verification
## Outcome and working principles
1. Preserve completed integration c81e285/e207b04/b853a1e and reviewed B4 source
52f6e17 on codex/release1-ytr83r. Do not replay equivalent3978/d114 changes.
CLI-only01b76a1 was reviewed and imported as023e3a8 without core/dependency
changes. Preserve the new AGENTS maintenance instruction and exact goal bytes.
2. Core433, independent lifecycle-oracle admission, focused lifecycle22 and full
desktop675 passed on this host. Production freeze2908405 is unchanged by
evidence checkpoint3ade397. GUI build, both-layer history/reopen and corrected
workspace strict Clippy passed. Finish the affected test and Unicode/native
workflows below. Do not repeat the passed suites without a
source change or new finding. Root serializes writers and records exact scope.
3. Use the imported evidence2ec creation-oracle v2 for native Undo comparisons.
Reject unknown/changed detached files while allowing exact files
retained from an explicit pre-Undo origin. Preserve full Project and reachable
SourceBundle equality and original role identities through Redo/reopen.
4. Exercise current Windows UI at minimum/wide sizes: creation name/owner/viewpoint,
one-step history, stale/raw-draft rejection, first Escape/modal priority,
recovery, clicked root/ALT action/observation/reuse, exact caller return,
same-size Fit and natural subsequent scrolling. Use independent semantic
predictions, not only screenshots or serialization of observed outputs.
5. Review current external Gitea handoffs before importing staged prerequisites,
empty-state actions, contextual chooser or source CLI. Validate final combined
workflows and preserve explicit ownership/qualification semantics.
6. After actual B4 test/native acceptance, prepare a distinct runnable Windows
prerelease with current-toolchain notices and exact executable/source hashes.
Existing MCP credentials have now completed a scoped branch push and remote
readback; release attachment upload still needs concrete-artifact verification.
Codex-9e8r6t retains the earlier named-authoring prerelease ownership.
7. Linux setup/downloads/builds/checking are paused by direct user instruction.
Wait for the user to resolve WSL2 networking and explicitly instruct resumption.
Preserve installed Ubuntu, exact VirtioProxy configuration and complete/partial
offline inputs. Networking remains unresolved; preparation is not Linux
acceptance. Follow the current WSL/offline records rather than old reboot plans.
8. Incorporate the owner clarification in issue2: process/scenario interaction
should use consistent direct canvas workflows while preserving domain
constraints. Codex-956atf owns the shared canvas design; B4 rendering and
prerequisite forms support it but do not close this requirement.
Deliver usable, complete OA/SA engineering workflows on Windows and Linux, with
one precise reST contract and equivalent graphical, textual and REST editing.
Constraints/calculations, quantities/units and reusable libraries are explicit
requirements alongside behavior/state modeling, traceability and navigable help.
Small and large models must be practical to author entirely in uncluttered text;
evaluate complete authored and stabilized models, not abbreviated grammar sketches.
Later Arcadia layers remain future scope. Compilation, metadata fields and polished
screens alone do not satisfy this outcome.
Retain the shared typed core, stable identity, semantic ownership/allocation distinct
from geometry, validated atomic commands and exact source/history. Persistent
extensions need coherent references/deletion, transition, source/migration,
API/schema and reachable graphical authoring. Define observable acceptance before
broad downstream changes. Do not silently promote a design proposal to a decision.
## Capability milestones
| Outcome | Work and acceptance |
| --- | --- |
| Usable direct behavior/state engineering | Continuous ordered/explicit process authoring, clicked root/nested scenario insertion, exact reuse bindings/caller return, state naming/creation, recovery and source-preserving history. Complete B4 lifetime rendering with actual lifetime bounds, branch-local Delete activation clipping, incomplete lanes and self-arrow endpoints. |
| Precise engineering analysis | Refine property-owned baseline values/formulas plus explicit ModelBaseline/Case one-level overrides. Close quantity kind/unit/value, exact/approximate numerical policy, dependencies/cycles, false/missing/error, complete freeze and definition provenance. Implement coherent validated core/source/REST/GUI/history contracts. An exact-duration slice alone cannot satisfy the full goal. |
| Maintained reusable libraries | Resolve source-only normalization, pinned offline definitions/closure, provenance versus checked same-origin compatibility, source→local instance maps, explicit three-way updates/local divergence, subjectless analysis closure and OA/SA mounted-data transition policy. Demonstrate two projects/two instances, cosmetic and semantic upgrades, conflicts/detach and rollback. |
| Practical complete textual and graphical tasks | Measure small/large text-only creation, cross-file reference navigation, safe alias rename and lossless patch preview. Integrate source CLI and bounded contextual choices; actual keyboard/IME, repeated names, stale events, recovery and minimum geometry must pass. Keep offline guide/examples and normative source documentation aligned. |
| Complete engineering stories | Close OA↔SA transition/reconciliation, typed data/interfaces/state, artifacts, saved views, hosted workflows and tutorial tasks against the full current goal. Confirm parity and precise specification rather than inheriting historical exclusions. |
| Reproducible Windows/Linux release | Current tests/format/strict Clippy, reST/OpenAPI/migration/example gates; real GUI and REST transport/restart/ETag conflict acceptance; matching optimized desktop/server packages, complete notices/archive hashes and clean-runtime launch. Fresh independent requirements/domain/parity/architecture/UX/runtime review resolves material findings before release. |
| Milestone | Work and acceptance | Dependency/current limit |
| --- | --- | --- |
| 1. Reliable direct authoring | Exercise actual minimum/wide native creation, allocation, editing, rejection, cancellation and history without implicit draft application or placement drift. Integrate compact named behavior creation after its coherent external gate. | Named creation4bb4aaf is imported; combined desktop676/676, format and strict Clippy pass at2cc4d11. Native80cf197 passes all four OA creations/history/reopen; new workspace fixes and production chooser await compilation. Workspace/B3/palette/recovery/packages are imported. Root owns this host's Cargo/native lease. |
| 2. Coherent process/scenario work | Interactive ordered/explicit process editing; clicked root/nested scenario insertion; exact reuse bindings and caller return; measured canvas/zoom/scroll; visible scoped Create/Delete lifetimes. Validate model/source/identity and no unintended edit during navigation, including stale/deleted/retargeted references and recovery. | B1/B2/B3 regressions pass in the full suite. Local B4 remains cfg(test); Codex-ytr83r owns production Scene integration and its independent native gate. Use the independently self-checked oracle for native checkpoints. |
| 3. Precise engineering analysis | Compare owned properties, analysis contexts and existing-data/external alternatives; settle quantity kind/unit/value, numerical policy, calculation/binding and Boolean/error/unknown semantics through falsifying examples. Implement mixed-unit budgets and reusable calculation/constraint definitions with full UI/source/REST parity and exact history/recovery. | B/shared-check/full-capture and R6/R7 architecture ratified after complete fixtures and criticism. Exact numeric/work-meter foundation is undeclared and under independent review. Approximate profile and full evaluator parity remain required. |
| 4. Maintained reusable libraries | Compare package/definition/instance alternatives. Establish pinned identity/version/dependency closure, local edits, import bindings, detach and explicit three-way updates. Demonstrate two projects, two independent instances, offline reopen, conflicting updates and stable unaffected identities. | Exact pins plus verified same-origin typed compatibility, explicit source preparation/acceptance and three-way editable updates are ratified. Finite120-assertion experiment is not production contract/archive/consumer implementation. |
| 5. Understandable complete engineering tasks | Close teaching, OA↔SA transition/reconciliation, typed state/data/interface, artifact, saved-view and hosted workflows. Establish complete text-only creation/refactoring/modular maintenance with stable identity. Replace concrete long homogeneous selectors with contextual/searchable bounded choices; verify large models, keyboard access, minimum sizes and failure/recovery. Keep offline guide/examples aligned. | A complete warning-free text-only model probe passes source/identity/rename/reparent/file-move checks; measure usability separately. Anchored chooser passes13 isolated mounted tests under cfg(test); production inspector/review integration is active; its compile/native gates remain open. Separate-host CLI is coherent at01b76a1 and awaits import here. Reassess current goal rather than copying historical scope exclusions. |
| 6. Release confidence and delivery | Fresh current Windows/Linux suites, strict Clippy/format, normative reST/OpenAPI/migration/examples; actual GUI/service transport, optimized builds, runtime/notices/archive/provenance/checksums and clean-environment launch. Fresh independent requirements/domain/parity/architecture/UX/runtime review; resolve material findings, then final handoff. | Integrates all preceding capabilities. Linux runtime remains open; use an approved supported environment, preserving historical failed/denied experiments. Historical previews are not final distributions. |
Milestones run adaptively where independent work helps. Analysis and libraries
share definition/identity assumptions and must converge before broad migrations.
Other-host slices remain coordinated through issue #1 and Wiki. Native/Cargo
leases are host-local; source ownership and exact transfer heads are shared.
Milestones are capability gates, not a rigid serial schedule. Root can continue
build/native acceptance while independent foundational investigations compare
alternatives. Libraries and analysis share definition-resolution assumptions and
must converge coherently before broad model/schema edits. User workflows and
cross-representation evaluators guide implementation boundaries.
## Evaluator boundaries and replanning
## Immediate execution sequence
Use authoritative source semantics, exact UUID/source preservation and actual
user workflows. Source-only experiments are not accepted new grammar; mounted
widget tests are not native readability; synthetic packaging checks are not
Linux runtime. Preserve old environmental failures as evidence without inventing
current blockers. Continue independent work when platform or transfer work waits.
1. Complete the coherent workspace/production chooser build and focused gates,
then stage a new GUI for real engineering tasks. Full676/format/Clippy passed
for2cc4d11; working changes require their own verification.
2. Implement the user-required common canvas interactions for Process and Scenario
in issue2 after the bounded shared-viewport/domain-constraint design decision.
Treat this as direct-manipulation acceptance, not a tool-shelf restyle.
3. Exchange exact verified commits with the separate host, preserving its workspace
ownership. Direct authenticated publication works. CLI01b and exact evidence2ec
are imported; next contextual-source and B4 handoffs retain their owners.
4. Use the self-checked [scenario oracle](scenario-authoring-contract.rs), follow
its [native procedure](fixtures/scenario-authoring/README.md), and compare exact
whole snapshots for Undo/Redo/reopen at minimum/wide sizes.
5. Exact numeric/unit/work-meter foundation26/26 unit tests pass at7d14bd0; complete
its strict gate while actual schema8 core work proceeds in an isolated worktree.
Extend actual model/source/API/GUI under the ratified
B/R6/R7/full-capture and exact-pin/typed-compatibility contracts. Preserve explicit
approximate numeric, complete consumer/archive and two-project release gates.
6. Update current ledgers and durable decisions when evidence changes the plan.
Reassess unresolved assumptions before expanding downstream implementation.
[Archived prior plans](history/2026-09-11-ytr83r-integration/README.md) preserve
all previous milestone detail. Update this plan when a counterexample changes
the right next action; do not lower completion criteria after substantial effort.
## Coordination and evidence rules
Use Gitea issue #1 and Wiki for current ownership/handoffs; read before resuming.
Codex-956atf, Codex-9e8r6t and Codex-ytr83r are distinct hosts. Cargo/native leases
are host-local; shared source ownership is coordinated.9e8r6t owns the next runnable
Windows named-authoring prerelease and contextual-source/CLI; ytr83r owns B4 Scene
and its platform gates. Root owns local chooser/numeric/workspace integration.
Updated AGENTS requires runnable milestone prereleases. User#178 authorizes existing
MCP credential reuse for Git/REST; secrets must never be printed or committed.
Direct branch publication replaces bundle work when available; old exact bundles
remain valid evidence and transfer branches must not be merged.
Record exact source/binary, command exit, test scope and native model/source oracle.
Do not add focused reruns to full-suite totals or transfer old binary acceptance.
Older denied Linux workaround instructions remain applicable to that action; do
not bypass them. Older stopped native sessions do not prevent currently authorized
native work. The [archived plan](history/2026-09-10-ledgers-before-continuation-refresh/PLAN.md)
and [archived progress](history/2026-09-10-ledgers-before-continuation-refresh/STATE.md)
preserve all previous strategy and scoped evidence; archive report links resolve
from their original `harness/` directory.
-41
View File
@@ -1,41 +0,0 @@
# Current engineering work and evidence
Start with the [user goal](../PROJECT_GOAL.md), [current status](STATE.md),
[execution plan](PLAN.md), [task ledger](TASK_LEDGER.md) and
[decisions](DECISIONS.md). Follow [AGENTS.md](../AGENTS.md) and current
[Gitea coordination](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/1)
before changing shared work. Dated reports describe their own source, host and
artifact; they are not current instructions or acceptance of later changes.
## Runnable milestones
- [Windows Studio and REST server authoring preview](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/releases/tag/v0.2.0-preview.authoring.20260911)
- [Standalone Windows source CLI preview](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/releases/tag/cli-v0.2.0-preview.authoring.20260911)
These are unfinished development milestones. Their release notes and validation
attachments define exactly what was shipped and tested. Full OA/SA engineering,
analysis, maintained libraries and current Linux acceptance remain unfinished.
## Current local evidence
| Question | Authoritative record |
| --- | --- |
| Which source was integrated? | [Integration audit](release-integration-ytr83r-20260911.md), [CLI import review](release-cli-integration-ytr83r-20260911.md) |
| What does lifetime rendering mean? | [Implementation and corrected review findings](release-lifecycle-render-ytr83r-20260911.md), [language specification](../docs/language.rst) |
| What will native behavior be compared against? | [Independent fixture and procedure](fixtures/scenario-lifecycle-native/README.md), [exact pre-native evidence](experiments/lifecycle-native-ytr83r-20260911/) |
| Which checks ran on this host? | [Host build record](release-host-bootstrap-ytr83r-20260911.md), [current status](STATE.md) |
| What remains for Windows packaging? | [Current runtime/notice preflight](release-b4-package-preflight-ytr83r-20260911.md), [publication path](release-publication-path-ytr83r-20260911.md) |
| Is Linux available? | [WSL outcomes and current user pause](release-wsl-postboot-ytr83r-20260911.md), [preserved offline inputs](release-linux-offline-preparation-ytr83r-20260911.md); setup/build/checking waits for the user's networking-resolution signal |
| What changed for Linux delivery? | [Three-binary CLI packaging and checks](release-linux-cli-packaging-ytr83r-20260911.md), [portable REST restart probe](release-rest-probe-portability-ytr83r-20260911.md); native execution remains pending |
| Which bounded follow-ups are proposals? | [Explicit Action allocation](release-qualified-action-design-ytr83r-20260911.md), [inheritance scaling diagnosis](release-inheritance-scaling-diagnosis-ytr83r-20260911.md) |
External staged-source, chooser and analysis/library owners maintain their current
handoffs in Gitea and the [shared Wiki](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/wiki/Home).
Import exact coherent source checkpoints before transferring conclusions to this checkout.
## History
[Archived integration ledgers](history/2026-09-11-ytr83r-integration/README.md)
preserve source-specific facts and earlier ownership. Existing dated evidence and
Git history remain available. Preserve unique findings and update links before
moving or removing material; active issue #1 remains open for the unfinished goal.
-64
View File
@@ -1,64 +0,0 @@
# Resume after the user-requested reboot — 2026-09-11
Codex-ytr83r /root. The complete first-release goal remains active and unfinished.
Read AGENTS.md, PROJECT_GOAL.md, current Gitea issues1/2 and Wiki before resuming.
This is a pause for the user's reboot, not a completed milestone or failed test.
## Saved source and evidence
Branch: codex/release1-ytr83r. Production source is B4 lifetime rendering52f6e17
plus CLI01b imported as023e3a8. b96286e records exact core/oracle evidence and
current harness index. d041212 preserves owner instruction-sync history and the
byte-exact supplied PROJECT_GOAL. a9a5768 records the Linux trial and contextual
source integration review. Later checkpoint commits add this resumption note.
Core all-targets passed433 tests, one intentionally ignored probe; the optional
probe separately passed. The independent lifecycle oracle and all six frozen
baseline/rename files passed before any native GUI launch. See the harness index.
No current desktop compile/test/native acceptance has been established.
## Interrupted Windows build
Cargo session93786 was interrupted at the user's request while compiling Iced
dependencies (last log entry cc1.4.5). It exited1 from interruption; no final gate
status JSON was emitted. A readback found no cargo.exe/rustc.exe process. Exact
partial log and explicit interruption record are in
experiments/lifecycle-native-ytr83r-20260911/. Preserve cached target outputs.
Use the existing ignored task-local toolchain, one Cargo job, after checking no
other writer is active. From the repository root in PowerShell:
```powershell
& '.local\run-gate.ps1' -GateName 'ytr83r-desktop-lifecycle-resume' -CargoArgs @('test','--locked','--offline','-p','oa-desktop','scenario_lifecycle','--','--test-threads=1')
```
The wrapper loads .local/toolchain/env.ps1. Then run appropriate direct-canvas,
full/strict desktop, server/CLI and GUI build gates serially. Review the new
typography report: wide Unicode Create labels can overflow the fixed capsule.
The proposed clipping correction is not implemented; preserve existing fixtures
and define separate expected Unicode cases before native output. Actual Windows
wide/minimum/Fit/Save/Undo/Redo/reopen acceptance and B4 packaging remain pending.
## Linux after reboot
Recheck wsl --status and wsl --list --verbose. Before reboot, no distribution was
installed; WSL reported virtualization unavailable and Windows had a pending
reboot. Hardware virtualization must be available (firmware or nested VM host).
No host setting was changed and no reboot was performed by the agent.
Prefer a working WSL2 Ubuntu for real Linux build/runtime validation. The verified
Ubuntu image and QEMU cache remain under .local/linux-host-ytr83r. Its bounded
UEFI/TCG pilot ended without guest-ready evidence; all owned pilot processes
stopped. Do not repeat failed delivery or infer Linux acceptance from the image.
## Coordination and next integration
Codex-9e8r6t owns contextual prerequisites; df6f5956 is available locally and has
a reviewed minimal import path, with two B4-preserving merges. Later short-host
layout and Save-procedure corrections are separate; inspect the current remote
handoff before import. Codex-956atf owns chooser, shared constrained canvas design
for issue2, and analysis/library contracts. Local audit agents have stopped their
assigned work. They must recheck coordination before taking new tasks.
The two published Windows authoring/CLI prereleases remain available. No B4
release should be published until actual current tests/native acceptance pass.
+181 -36
View File
@@ -1,57 +1,202 @@
# Integrated checkpoint — 2026-09-12
# Progress Ledger — first release
Codex-ytr83r, isolated branch `codex/canvas-union-ytr83r-20260911`. Current source `33e30c1` combines the a57 canvas, cb91525 grapheme and narrow saved-destination repair corrections. Its 530 frozen inputs match HEAD; independent repair review found no actionable finding. Windows desktop tests, strict Clippy, GUI build and bounded matching native/package acceptance passed. The [canvas-repair Windows prerelease](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/releases/tag/v0.2.0-preview.canvas-repair.20260912) is published; ZIP, checksum and validation attachments were downloaded and hash-verified. [Publication record](release-canvas-repair-publication-20260912.md) preserves the exact identities. This is not a full [user-goal](../PROJECT_GOAL.md) completion claim. Live [canvas integration](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/2), [native/release acceptance](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/5), [saved-destination repair](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/6) and [project overview](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/1) retain ownership and plans. Earlier production `865483b` and its package remain preserved.
Updated 2026-09-11, coordination through Gitea #245 and issue2. This ledger describes the
Codex-956atf checkout; external-host evidence is labeled separately. The entire
previous ledger is preserved [byte-for-byte](history/2026-09-10-ledgers-before-continuation-refresh/STATE.md).
Historical dates, binaries, process IDs and stopped-session instructions in that
archive are evidence history, not current status or authorization.
## Verified code and capabilities
## Current objective and source
Source `33e30c1` binds repair dirty state to the actual saved destination, preserving accepted-model history even when Apply is an identical-content no-op. It selectively incorporates peer `33c6d83` in three files. Windows focused repair **5**, full desktop **761**, strict desktop all-targets Clippy and GUI build passed; the five are included in 761. All **530 files / 9,727,025 bytes** remained unchanged between gates and matched HEAD after the build. The preserved GUI `50486253…` passed matching repair/history/recovery and Unicode selection/passive-save checks, packaged walkthrough/help smoke and normal exit0 with empty logs. All559 package files stayed unchanged; fresh extracted server25-request editing/persistence checks passed. [Current validation and artifact hashes](release-canvas-repair-validation-20260912.md) links the exact raw archives and independent reviews. [Repair union and limits](release-repair-union-ytr83r-20260912.md), [closed gates and exact inputs](experiments/repair-union-windows-33e30c1-20260912/README.md), [verbatim independent review](release-saved-destination-independent-review-20260912.md).
Deliver the complete, usable OA/SA first release in [PROJECT_GOAL.md](../PROJECT_GOAL.md),
including graphical/source/REST parity, constraints/calculations, quantities/units,
reusable model libraries, traceability, offline guidance and Windows/Linux release
acceptance. Direct behavior/scenario authoring and reduced cognitive load remain
immediate priorities. Complete small and large models must be practical to develop
through uncluttered text alone. The release is incomplete.
Source `cb91525` preserves extended grapheme clusters in the shared display helper. Its four focused tests, full desktop 756 and strict desktop Clippy passed; the four overlap the 756. Its GUI build was intentionally superseded by the repair union; matching combined33e native Unicode evidence is recorded above, without reattributing that result to cb91525. [Correction and limits](release-grapheme-shortening-ytr83r-20260911.md), [closed Windows evidence](experiments/grapheme-windows-cb91525-20260911/README.md).
Local branch: `codex/behavior-authoring-20260910`. Last compiled source checkpoint
is `2cc4d115d0ff28423c7ed87fd5f4065efb011f2d`, including external named creation
`4bb4aaf` as `1516949`, the isolated chooser and final baseline/library contracts.
Full desktop676/676, formatting and strict Clippy pass at that source. The active
native GUI remains immutable80cf197. New workspace and production chooser changes
are outside those artifacts and await a coherent build. Current committed source
adds exact numeric foundation7d14bd0, imported CLIbc311bc and evidence/native
dc0f2e1. Numeric26/26 unit tests and strict core all-target Clippy pass. Shared
instruction ancestrycab01c3 is merged asc7ed894, retaining the exact uploaded goal.
The authoritative goal was copied byte-identically from attachment
`a49d1780-25d5-4778-ad68-6140c60e4207/goal-objective.md`; SHA256
`DD58F75C93D55FE16ED141A23D834898FB6EBC625AE7E7AE379DA552A8312435`.
See [Task Ledger](TASK_LEDGER.md) for knowledge/decisions and [Plan](PLAN.md)
for capability acceptance. Older goal paths are superseded.
The isolated `a57c334` union selectively combines peer `a31b40d` canvas/chooser/form prerequisites with B4 lifetime rendering and contextual source/child-work corrections. Windows lifecycle **27**, full desktop **752**, strict desktop Clippy and Studio build passed on 529 unchanged frozen inputs. GUI `69DBB90F…` passed bounded staged source/consumer creation, Cancel, Save, Undo/Redo and reopen checks: 13 oracles and 45 screenshots at the default wide window only. [Canvas checkpoint](release-canvas-union-ytr83r-20260911.md), [staged archive](experiments/staged-source-native-a57c334-20260911/README.md), [independent review](release-staged-native-a57-independent-review-20260911.md). A fresh a57 camera session subsequently passed bounded OA 100%/125%/Fit and minimum 1100×700 client Fit66 checks, OA/SA pan and moved Create/Delete hit selection, exact passive Save equality and normal exit 0. The 18 images support bounded clipping without toolbar bleed; labels are small at 66%. [Camera archive and limits](experiments/canvas-camera-a57c334-20260912/README.md), [verbatim independent review](release-camera-a57-independent-review-20260912.md). The [earlier picker failure](experiments/canvas-native-close-a57c334-20260911/README.md) remains historical evidence. These pre-grapheme a57 results do not establish current `33e30c1` native or complete usability acceptance.
## Completed work and evidence
Earlier published checkpoint `3ade397008cffe826d5f5a70e7c0233159e3bd42` contains production `29084050a7c020e0fb488be37c6e1ab235769cc0`; push and exact remote readback passed. It combines prior named behavior creation/history integration (`c81e285`, `e207b04`), corrected creation-oracle evidence (`b853a1e`), CLI-only integration (`023e3a8`) and B4 lifecycle rendering.
The creation bundle was fetched anonymously and verified at105022 bytes,
SHA256 `0DEFAE91A56430076C432758F923BC0675911DA9FA7588C9961FF15F47EAEBF6`.
Its coherent commit adds named Process/Chain/Scenario/Machine drafts, bounded
owner search, recovery/selection guards, offline guide and measured textual
examples. The duplicate Sensor correction was not reapplied. Both hosts' report
additions and exact experiment bytes are retained; this host's goal and ledgers
remain authoritative locally. External653-test/native evidence is attributed in
[its integrated report](release-integrated-gates-20260910.md), not claimed as a
local run. The separate isolated chooser at `c4e1580` passed13/13 mounted tests
under cfg(test); it is not a production selector yet.
B4 renders local lifetimes, branch-local activation termination, neutral incomplete lanes, Create capsules and Delete markers with original occurrence identity. Real Windows OA/SA six-case matrices and separate name-only Save/Undo/Redo/reopen workflows passed complete independent Project, reachable-source and physical-file comparisons. These are bounded fixture checks, not complete product usability acceptance.
| Scope | Current evidence and limits |
| --- | --- |
| Reproducible local toolchain | Ignored `.local/toolchain/env.ps1` selects Rust/Cargo 1.98.1 GNU/LLVM and verified LLVM-MinGW 20260908 (clang 23.1.1), with one Cargo job. First cold desktop test build completed in 58m03s; one unused import warning was corrected separately. |
| B1 process authoring | `01bb52a` includes interactive empty/ordered/explicit topology and presentation-only inferred paths; first conversion preserves ordered adjacency atomically. All topology regressions pass within the current full desktop suite. Native acceptance is pending. |
| B2 scenario authoring | Clicked root/operand/slot and optional lifeline feed Action/State/Reuse/Fragment adapters. Pointer-to-App, cancellation, stale-event, recovery and history tests pass within the current full desktop suite. |
| B3 caller navigation and workspace | External `3978bbe` imported as `865cd55` fixes visible-owner restoration, pending-draft guards and remaining-height workspace. Root `80cf197` keys the viewport Sensor by document/scenario; a real mounted regression failed before the fix and now passes. The full suite is green; actual OA caller return and source-neutral navigation pass. Remaining native scope is explicit below. |
| Architecture palette | External palette commit `7d1df4f` was cherry-picked as `aed40d7`, retaining local invariant tests. Captured Escape, stale Submit/raw retention and bounded card fixes are source-integrated. External native results do not transfer to this checkout. |
| Typed stored-history F1 | `b6b76ef` fixes nullable constructor cache discrimination; `4ecad78` fixes NotApplicable seeding by checking actual enum alternatives. Thirteen focused tests passed and are included in the current full suite; core validation remains authoritative. |
| Lifeline lifecycle B4 | `eb5161b` adds pure local/scope-aware lifecycle projection with ten tests, declared under `cfg(test)` only. It is not production Scene wiring or native acceptance. [Design](release-scenario-lifecycle-design-20260910.md). |
| Native scenario oracle | [Independent checker](scenario-authoring-contract.rs) compiled/self-checked and verified the generated62-element/two-source baseline. Native OA Action/State/Reuse/ALT-fragment, each exact Save/Undo/Redo, and final native reopen pass at minimum size; caller identity/scroll return and source-neutral navigation pass. Remaining prescribed checkpoints are open. [Native scope](release-scenario-native-20260911.md), [oracle inputs](fixtures/scenario-authoring/README.md). |
| Normative documentation | Strict Docutils 0.22.2 language HTML regeneration passed. `semantic_schema.py --check --validate` passed 72 source7 fixtures, 204 schemas, 14 families/90 scalar leaves. These are documentation/schema gates, not a Rust build or runtime pass. |
| Comparable-product study | Actual isolated Capella 7.1 scenario/activity/lifeline/LOOP creation was saved and XML inspected, then closed normally. [Report](release-capella-scenario-benchmark-20260910.md). Current Studio native evidence is recorded separately. |
| New requirement audit | [Independent delta report](release-requirements-delta-20260910.md) establishes that current expressions/unit labels are descriptive and source includes are not maintained cross-project libraries. Refined B/R6/R7 and exact-pin/same-origin compatibility contracts are ratified; current source7 does not implement them. |
| Packaging integration | External `b98f135` imported as `6d2b961`; all 25 checked-in exact notices match the manifest. `scripts/test-package-artifacts.py` passes 11/11 and `scripts/test-windows-notices.ps1` passes synthetic package/runtime/source/font checks. Actual optimized distributions and clean-environment launches remain open. |
- [Native lifecycle observations and limits](release-lifecycle-native-ytr83r-20260911.md).
- [OA evidence archive](lifecycle-native-ui-oa-archive-20260911.md) and [SA evidence archive](lifecycle-native-ui-sa-archive-20260911.md).
- [Lifecycle implementation and independent findings](release-lifecycle-render-ytr83r-20260911.md).
- [Creation and source integration evidence](release-staged-source-integration-ytr83r-20260911.md).
## Active work, ownership and unresolved findings
Source `321897fd55af9e07afa2c6ebf1e50be49e86f7d9` integrates the published contextual source-creation/minimum-card corrections from b2c3e540 with B4 and the header clipping correction. Its own Windows desktop all-targets gate passed 704 tests. [Exact selective integration, independent union review and Windows results](release-contextual-integration-update-ytr83r-20260911.md).
Root Codex-956atf owns this host's integration, serialized Cargo and native UI.
The source2cc4d11 gate completed with exit0: **676 passed, zero failed/ignored**,
tests611.24 seconds after warning-free compilation10m47s. Command:
`cargo test --locked -p oa-desktop --bin arcadia-studio -- --test-threads=1`.
Log `.local/logs/desktop-creation-chooser-20260911.log`. Immutable executable
`.local/creation-chooser-20260911/tests-2cc4d11/arcadia-studio-tests.exe`, SHA256
`8D7FB35071F155B3BD07CE1885A1136B2D2EF71C4FF8C704665202499C86A721`.
Its manifest pins256 tracked input hashes. Format passed; strict all-target desktop
Clippy passed7m18s after the isolated manual-clamp correction. Final logs are copied
beside the immutable binary. These gates supersede the historical642 total, not the
current uncompiled working changes. Focused counts are included, never additive.
Published production `865483b4c66361a01fe0d9e731859220bafb4136` adds the reviewed child-work correction: first refinement transfers existing Allocation and RoleAssignment identities, metadata and artifact references to the child; later siblings inherit only a unique performer. OA/SA regressions cover nested children, roles, folded grouping, unallocated/mixed controls and exact history. The old extracted Studio `BF9C2E61…` passed OA first-child, sibling and grandchild Save/Undo/Redo checks, then confirmed OA reopen/Save and the SA first/sibling/grandchild, Undo/Redo and reopen/Save continuation. Original identity carriers and complete source/physical snapshots matched. These are native results for **865483b/BF9**, not a57/69DB. [Original OA evidence](experiments/native-child-oa-865483b-20260911/README.md), [closed continuation](experiments/native-child-continuation-865483b-20260911/README.md), [independent continuation review](release-native-child-continuation-review-20260911.md), [fixture/oracle](fixtures/child-work-native/README.md) and [issue4](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/4) preserve the exact scope.
Actual native Studio remains `.local/native-qa/studio-80cf197/`, GUI SHA256
`A5ED6DF334B1BFB620425AFDF3E863F0B522990E6FB47C6C401A71CF4443954A`.
That GUI build passed3m28s. At1100x700, all four OA creation checkpoints and final
reopen now pass complete Project/two-source prediction and exact UUID restoration.
Preserved OA snapshots end in `oa-reopened/`; separate SA Action Save/Undo/Redo
also passes complete oracle/history comparisons, ending in `sa-action-redo/`,
63objects. The GUI is saved at SA A caller, no active form. SA State/Reuse/Fragment
and final reopen remain. No new-fix acceptance is inferred.
[Native report](release-scenario-native-20260911.md) records details and two real
workspace findings: new forms retain downward scroll, and opening a form can hide
its clicked marker below the reduced canvas. Root has written a fresh form-session
key, guarded Name focus, one-time marker reveal and three meaningful mounted/App
regressions. They are not yet compiled. The isolated chooser's corrected realistic
typing/paste/IME gate passes13/13; production inspector/review integration is now
source is now frozen, with exact row/facet/group/attachment guards. Independent
review exposed and closed three focus defects at source level, including the
legitimate Review rationale input. Fourteen new App/mounted tests await execution;
the earlier isolated13 is separately scoped. [Review](release-reference-chooser-production-review-20260911.md).
## Verification scope
User [issue2](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/2)
requires Process and Scenario to behave as actual canvases like architecture/state
diagrams. Current scrollable diagrams/tool shelves do not close that requirement.
behavior_audit owns a bounded shared-canvas design; root re-observed the saved
native scenario and its restricted viewport, without new model input.
On this Windows host, core all-targets passed433 tests with one intentionally ignored scaling probe; that probe separately passed with unoptimized timings. Production29084050 passed focused lifecycle22 and full desktop675 tests (the22 are included in675), and the ordinary GUI build/launch. An equivalent test-helper let-chain correction subsequently passed strict workspace/all-targets Clippy and its focused regression1/1. These results predate the newly identified header-rendering correction.
The [baseline contract](release-analysis-baseline-contract-20260911.md) supplies a
complete17-UUID proposed model,11 exact fixtures, explicit ModelBaseline/Case,
property bases and shared checks. Its transport fixtures are not parser/evaluator
acceptance. The [library contract](release-library-compatibility-contract-20260911.md)
has120 executed finite-prototype assertions, separate-process reopen and11 complete
expected/reopened file comparisons. Final81-file manifest was independently
verified; preliminary output is retained and labeled superseded. All166 experiment
files retain exact Git bytes. Root ratified B/R6/R7/full private capture, nominal
typed compatibility alongside exact pins, explicit source normalization and OA/SA
dispositions in [DECISIONS](DECISIONS.md).
- [Exact current-host logs and inputs](experiments/lifecycle-native-ytr83r-20260911/).
- [Test-only Clippy correction](lifecycle-clippy-correction-ytr83r-20260911.md).
- [Current-host toolchain setup](release-host-bootstrap-ytr83r-20260911.md).
- [Independent Windows gap review](release-windows-gap-review-ytr83r-20260911.md).
The [independent refined review](release-refined-contracts-review-20260911.md)
passes292 fixture/identity checks but finds C1:333 stored AST nodes can expand to
over2billion calls. Request-local dynamic/numeric work accounting is mandatory;
static depth/node limits alone fail. C2 is a finite-prototype threshold-unit gap,
not a passing production type-checker. Exact arithmetic, unit/dimension operations
and terminal work meter are declared/committed at7d14bd0; their26/26 unit gate has
[exact source/artifact evidence](release-analysis-numeric-gate-20260911.md).
requirements_delta now owns actual schema8 definitions, expressions, commands,
references, migration and static admission in the isolated
`.local/analysis-core-worktree-20260911/`, based at7d14bd0. It remains uncompiled.
semantics_critic reviews the bounded decoder strategy separately; existing default
recursion rejection stays until a full allocation/lifecycle gate is justified.
Root owns compilation. Numeric/runtime and static-admission ceilings remain
explicitly provisional pending measured gates. Approximate engineering calculations,
full evaluator/model/source/REST/GUI parity and actual library maintenance are open.
The closed 321897f Windows gate passed 704 desktop tests with zero failed/ignored/filtered; all 261 recorded build inputs remained unchanged. Matching server `ABA87A5F…` passed semantic HTTP checks (204 schemas, 19 values, 9 rejected invalid shapes) and the corrected REST probe's 25 requests. [Closed runtime evidence](experiments/windows-runtime-321897f-20260911/README.md) preserves both earlier source-version failures. [Earlier integration checkpoint evidence](experiments/contextual-lifecycle-integration-ytr83r-20260911/README.md) preserves server/CLI 68 across 7 targets, schema 72/204 after the missing-jsonschema correction, and the old GUI's wide-name passive Save comparison.
Codex-9e8r6t and Codex-ytr83r are separate hosts/teams. The former owns contextual
prerequisite creation and source CLI. CLI01b76a1 is imported asbc311bc; exact
corrected evidence2ec72d4 is imported atdc0f2e1, preserving root's separate native
report. External675 staged-source and CLI12/183-assertion results remain externally
attributed. Its runnable Studio/server preview release1 and standalone CLI preview
release2 are published; see their release notes for exact source/runtime limits.
Strict language/API HTML regeneration and the72-fixture source7 schema gate pass
locally after that evidence import. Codex-ytr83r owns B4
production Scene wiring, independent native oracle and local Linux route. Its
rendering source has independent criticism but no passing build/native gate yet.
Do not duplicate these changes. Preserve root's distinct scenario-scroll/focus hook
when integrating other hosts' small App/behavior bridges.
The later 865483b Windows desktop gate passed **707 tests**, including the three focused child-work regressions, with zero failed/ignored/filtered. All 261 recorded build inputs stayed unchanged. The matching debug GUI build and strict **desktop-only** all-targets Clippy passed; child-work and staged-source checker compilation/admission also passed. [Closed gates and exact inputs](experiments/windows-gates-865483b-20260911/README.md). These checks do not establish the remaining native rendering or packaged acceptance.
Updated AGENTS4b8465c8 adds concise shared maintenance to milestone releases. Gitea
user#178 explicitly authorizes programmatic reuse of existing MCP credentials for
Git/REST and relevant commits/tags/prereleases, without printing or committing them.
Direct authenticated push/readback passed on this host, most recently publishing
dc0f2e1. The subsequent owner's instruction commitcab01c3 has been fetched and
merged locally; no force push or additional credentials are required.
Historical verified bundle exchange remains reproducible: c6d296a bundle on
`codex/behavior-transfer-20260910`,334830 decoded bytes, SHA256
`526A80E2D32ED0E57A7B2372B5F02D4652E8751DFEBA81953C4F045225D6AACB`,
prerequisite1797516. Both other hosts received it. Never merge transfer branches.
Older bundles/logs retain historical scope in Git and the archived ledger.
The extracted package's unchanged server `ABA87A5F…` passed the actual HTTP schema checks (204/19/9) and 25-request REST editing/persistence/restart probe with a restricted process PATH and unrelated working directory. Seven raw response pairs and exact source snapshots matched; all five test-owned server processes stopped. The first REST run's PowerShell-added PATH directory is preserved as historical evidence; the corrected restricted-PATH rerun is authoritative. [Packaged server evidence](experiments/packaged-server-865483b-20260911/README.md). This completed the earlier bounded server runtime acceptance. Fresh33e package/server/GUI results are linked above; the old receipts retain their own source attribution.
## Remaining gates and highest-value next actions
## Material limitations
1. Freeze and compile the new workspace/chooser integration, resolve focused
failures, then build an immutable GUI and verify both native workspace fixes.
The676-test gate is complete for2cc4d11; new changes require their own evidence.
2. Publish coherent checkpoints directly; integrate9e8r6t's next staged-source
work, then ytr83r B4 only after its coherent handoff. CLI/evidence import is done.
Preserve ownership and distinguish every external versus local gate.
3. Use the self-checked independent oracle to exercise current Studio at
minimum/wide sizes: named creation, direct process/scenario edits, nested reuse,
caller return, lifeline lifecycle, invalid/cancel/recovery, Save/Undo/Redo/reopen.
Compare complete model/source and exact identities, not screenshots alone.
4. Compile/review/measure the numeric foundation, then implement the ratified
analysis/library contracts with shared-core/source/API/graphical parity. Use real
budgets, unit/kind failures, complete text-only and two-project update workflows.
Integrate the bounded chooser and run its actual keyboard/minimum-size gates.
5. Complete current teaching, transitions, semantic review, state/data, saved-view,
artifact, hosted transport and migration acceptance. Historical scoped passes
remain useful references, never blanket current acceptance.
6. Complete Windows/Linux build/test/native/runtime and optimized package gates,
notices, provenance and clean-environment launch. WSL has no installed distro
here; Linux acceptance remains open. Historical denied guest workarounds must
not be retried or split to bypass review. No host restart is authorized.
7. Run fresh independent cross-disciplinary end-state review, resolve material
findings and reconcile specification/help/coverage before final handoff.
Earlier native wide Latin/CJK checks found neutral lifeline header text overflowing its card while Create labels stayed contained. The [header correction](lifecycle-header-overflow-correction-ytr83r-20260911.md) is included in 865483b and retained in a57. Actual corrected wide-W/CJK observations in BF9 show bounded headers and Create labels, including selected zoom, scroll and minimum-window cases. Both complete wide and cluster sources remained unchanged after passive Save. Cluster checks exposed a separate scalar-shortening defect: an accent or joined emoji can be split at the label cutoff. The later cb91525 correction and current33e accent/ZWJ selection/passive-save checks resolve the observed string-splitting case. Long wide labels can still be hard-clipped and arbitrary font/DPI presentation is unverified; see the historical [native typography record](experiments/lifecycle-header-correction-native-865483b-20260911/README.md).
## Reassessment
The earlier capture failures and pending Open dialog are historical: later confirmed OA and SA reopen/Save actions are preserved in the [continuation archive](experiments/native-child-continuation-865483b-20260911/README.md). Its first OA `reopened-1948` receipts remain data-only evidence. Bounded a57 staged and camera workflows above are complete; full focus/keyboard, every clipping edge/zoom and accessibility remain unverified. Bounded combined `33e30c1` native/package acceptance is now linked above. Small 66% labels are a recorded limitation. The BF9 Home page, scenario guide and bundled water-pressure example opened; one normal Alt+F4 then completed, with the process absent and empty stdout/stderr. The [old Home/Guide/close archive](experiments/packaged-gui-close-865483b-20260911/README.md) preserves those old865/BF9 observations and package readback. No exit code was captured for that older BF9 process; the separate a57 exit 0 does not change its evidence.
Meaningful progress continues: concrete interaction defects have been found and
corrected,676 desktop tests pass and all four OA native creation/history/reopen
checks pass. Contracts have converged through independent falsification; numeric
implementation now addresses the exposed dynamic-work risk. The revised semantic
requirements are no longer hidden behind metadata.
Passing older suites or accumulating prose would not close the user problem.
Current highest information gain comes from actual build/native results and
independently criticized foundational examples, while preserving existing identity,
source and atomicity contracts. Replan when those results falsify a proposal.
The selected chooser/canvas union now has a57 source and Windows gate evidence; broader persistent graphical manipulation and separate text-navigation contributions retain their own integration/acceptance boundaries. Calculations, quantities/units, maintained libraries, practical large-model textual workflows, complete OA↔SA engineering parity, documentation and fresh cross-disciplinary review remain full-goal requirements. See the [analysis proposal](release-analysis-semantics-design-20260910.md), [library proposal](release-libraries-semantics-design-20260910.md), [text-authoring design](release-textual-authoring-design-20260911.md) and [decisions](DECISIONS.md) for attributed design evidence, not completed implementation claims.
## Historical evidence access
## Platform and published deliverables
**Linux setup, downloads, builds and checks are paused by explicit user instruction until the user reports networking resolved and instructs resumption.** Ubuntu26.04/WSL2 shell execution was observed; guest networking remained unavailable. Offline preparation was a workaround, not a network fix or Linux acceptance. Host VPN settings were not inspected or changed. [WSL evidence](release-wsl-postboot-ytr83r-20260911.md) and [preserved offline inputs](release-linux-offline-preparation-ytr83r-20260911.md) retain exact outcomes.
Peer Codex-9e8r6t published the separately verified [Windows contextual prerelease](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/releases/tag/v0.2.0-preview.contextual.20260911), source `b2c3e540`; [its publication handoff](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/2#issuecomment-406) binds its artifacts and original-host checks. It is not a B4 release from this branch.
## Record navigation
The [one-time record migration](history/2026-09-11-ytr83r-record-migration/README.md) preserves the exact prior STATE ledger and marks PLAN/TASK_LEDGER superseded. [Earlier integration history](history/2026-09-11-ytr83r-integration/README.md) preserves source-attributed older results. Historical leases, failed approaches and next-step wording do not override current Gitea coordination or the Linux pause.
The [complete prior STATE](history/2026-09-10-ledgers-before-continuation-refresh/STATE.md)
retains exact historical binary hashes, counts, logs, process closure, bundle hashes
and all report links. Links inside the byte-preserved archive retain their original
`harness/` context; resolve those relative report paths from `harness/`.
Useful direct entries: [source7 native](release-source7-native-acceptance-20260907.md),
[teaching](release-teaching-native-20260906.md),
[artifact native](release-artifacts-native-20260906.md),
[coverage baseline](release-current-coverage-20260906.md),
[Linux gate](release-source7-linux-gate-20260907.md),
[Linux cross-build](release-source7-linux-gui-cross-build-20260907.md),
[denied boot experiment](release-source7-linux-server-corrected-gate-20260907.md).
+203 -125
View File
@@ -1,145 +1,223 @@
> Superseded on 2026-09-11. This document is a preserved historical checkpoint.
> Live plans, priorities, ownership and handoffs now reside in [Gitea issue 1](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/1#issuecomment-410) and [issue 2](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/2).
> Do not resume synchronizing this ledger. See [STATE.md](STATE.md) for the integrated checkpoint.
# Task Ledger — first release
Updated 2026-09-11 for Codex-ytr83r. The full
[user goal](../PROJECT_GOAL.md) and [current progress](STATE.md) remain authoritative.
[Exact host ledger archives](history/2026-09-11-ytr83r-integration/README.md)
preserve prior facts, dates and reasoning without transferring their acceptance.
Updated 2026-09-11. [STATE.md](STATE.md) owns current execution/evidence;
[PLAN.md](PLAN.md) owns capability milestones. The entire former Task Ledger is
[preserved byte-for-byte](history/2026-09-10-ledgers-before-continuation-refresh/TASK_LEDGER.md).
Its old goal references, aggregate test claims and stopped-session directives retain
historical scope. New semantic decisions are recorded explicitly in DECISIONS.md;
prototype results retain their separate scope.
## Goal and given requirements
Current compiled checkpoint2cc4d11 includes external creation4bb4aaf as1516949,
isolated chooser13, and final baseline/library contracts. Local full676/format/
strict Clippy pass; native80cf197 now passes four OA creations, exact history and
reopen. Separate native SA Action/history also passes. Exact numeric foundation
is declared at7d14bd0 and passes26/26 unit tests plus strict core all-target Clippy.
Working workspace/production chooser changes remain outside compiled artifacts;
independent chooser source review closed three focus findings,14 new host tests
are prepared. Schema8 core implementation is isolated and uncompiled. External
results retain their host/source attribution.
Deliver a complete, usable OA/SA release on Windows and Linux, with one explicit
reST semantic contract and equivalent graphical, textual and REST access. Include
constraints/calculations, quantities/units, maintained libraries, behavior/state,
traceability and navigable offline help. Reduce cognitive load and visual clutter;
small and large models must be practical using text alone. Later Arcadia layers
remain future scope. Compilation or a pleasant happy path is not completion.
Issue2 further requires consistent direct process/scenario canvas interaction;
the owner's clarification allows domain constraints rather than arbitrary movement.
## Goal and success criteria
At15:28UTC on2026-09-11, the user explicitly deferred Linux builds and checking
until they resolve WSL2 networking and instruct resumption. Linux setup/downloads
are paused with inputs preserved. Continue independent Windows work; do not treat
offline preparation as a networking fix or Linux acceptance.
Deliver the usable, feature-complete OA/SA first release specified in
[PROJECT_GOAL.md](../PROJECT_GOAL.md). It is the exact copy of the user's revised
`a49d1780-25d5-4778-ad68-6140c60e4207/goal-objective.md`, SHA256
`DD58F75C93D55FE16ED141A23D834898FB6EBC625AE7E7AE379DA552A8312435`.
Success requires coherent explicit semantics, practical engineering workflows,
equivalent graphical/textual/REST behavior, real Windows/Linux runtime evidence,
recoverable authoring, relevant automated evaluators, precise documentation,
reproducible packages and fresh independent cross-disciplinary end-state review.
No known blocker or high-impact finding may remain unresolved. Later Arcadia
layers remain future scope. Code, a passing slice or an attractive screen alone
cannot establish completion.
## Given facts
- Rust/Iced desktop and a deployable REST service serve system engineers. OA/SA
transitions in both directions must be understandable, explicit and useful.
- The revised goal explicitly requires constraints/calculations, quantities/units,
reusable model libraries, state/behavior modeling and strong standardized
specification/API, as well as textual/graphical notation and artifact traceability.
- Human/AI/Git-friendly source and a concrete unambiguous reST specification must
match graphical editing. The source editor needs highlighting/linting; help must
be offline, concise and navigable.
- The newest explicit goal requires uncluttered notation that reduces cognitive
load and supports development of complete small and large models using text alone.
- Direct behavior/scenario diagram authoring and reduced cognitive load are current
priorities. Long homogeneous selectors and bloated persistent creation controls
are rejected. Study actual Capella's useful domain workflows and nesting without
mechanically copying its interaction problems.
- The user authorizes autonomous bounded engineering, native Capella/Studio
evaluation and explicit independent delegation, exclusively with GPT-6 Astra
at effort appropriate to the question. Root owns synthesis/integration.
- Project instructions require Gitea coordination, ownership preservation and
durable handoffs. Current root reserves one Cargo/native operator per host.
Updated AGENTS4b8465c8 requires coordinated runnable milestone prereleases and
concise current shared maintenance with unique historical evidence preserved;
user Gitea#178 authorizes programmatic existing-MCP-credential reuse for relevant
Git/REST publication without printing/committing secrets. Restart and usage reset
remain unauthorized.
- Gitea issue2 explicitly requires actual Process/Scenario canvases with the same
interaction approach as architecture/state diagrams. Existing pointer edits
inside scrollable projections do not by themselves satisfy that requirement.
## Verified facts
- Local start was cleanff615d0. Root decoded/hash-verified/imported published3978,
c6 and4bb bundles; merged c6 as c81e285 and integrated creation4bb at e207b04. There were
no production cherry-pick conflicts; docs/attributes require evidence union.
The current goal is copied from the user's latest file.
- Read-only integration comparison finds equivalent workspace/Sensor code in c6;
replaying3978 or d114 is unnecessary. Core source is unchanged betweenff andc6.
B4 now has a production Scene bridge and independently reviewed source, with
focused lifecycle22/22 and full desktop675/675 passed on source2908405. The
ordinary GUI build and real OA/SA lifecycle matrices now ran; OA full name-only
Save/Undo/Redo/reopen and the corresponding SA workflow passed exact independent
comparisons. Corrected workspace/all-targets strict Clippy also passed0;
Unicode appearance remains open. See the
[native record](release-lifecycle-native-ytr83r-20260911.md) for exact scope.
- Imported source implements named behavior creation, exact prepared-batch
correlation, success-only selection, raw-draft guards, recoverable original
layer and modal-priority Escape. These are source facts; combined local runtime
acceptance is pending.
- Earlier-host desktop642 and653 suites, scoped native scenarios/creation,
packaging25 notices and source experiments retain exact attribution in
[STATE.md](STATE.md). They are not new local passes.
- The original transferred creation oracle incorrectly equates physical disk files
with reachable sources during Undo. Later external v2 corrects the evaluator
with explicit origin/retained-byte rules; exact v2/evidence2ec was imported at
b853a1e after complete path/hash preflight. New host execution is still pending.
- Isolated Rust1.98.1/LLVM23.1.1 and locked dependencies are available. Packaging
11/11 and synthetic Windows notice checks pass. Current core all-targets passed
433 tests; its optional inheritance probe separately passed with slow debug
timings that require controlled optimized interpretation. Focused lifecycle
passed22/22 after two installed-Iced API corrections; full desktop passed675/675.
Strict reST/HTML generation passes with Docutils0.22.2.
- B4 independent criticism found and resolved self-caption overlap and two dense
minimum-Fit hit-selection counterexamples. Shared nearest marker/message-stroke
arbitration preserves exact marker/header/label selection. Ten real-Scene tests
and two clip geometry tests passed in the22-test focused gate. The independent twelve-scenario native oracle passed compile,
admission, self-check and exact baseline/name-only predictions before GUI output.
- The separate baseline-context experiment passed218 assertions; explicit
one-level overrides reduce repeated stable assumptions while complete frozen
definition environments remain necessary. Proposed syntax is not source7.
- After the user's virtualization/reboot change, supported Ubuntu26.04 install
completed exit0 and a real x86_64 WSL2 kernel shell completed. Later VM startups
timed out before apt. Owner328's exact VirtioProxy configuration now supports
a persistent root shell, but requested IP/DNS/HTTPS checks failed. Signed offline
dependency preparation advances without host VPN inspection or changes. This
establishes guest execution, not Linux builds or GUI acceptance; exact outcomes are
in the [WSL record](release-wsl-postboot-ytr83r-20260911.md).
- The independent scenario checker compiled with warnings denied and passed its
self-check, deterministic fixture generation and complete baseline check on this
host. Its eight OA/SA checkpoints and exact UUID/source restoration comparisons
are evaluator evidence, not native Studio acceptance. Exact inputs/hashes are in
[the fixture instructions](fixtures/scenario-authoring/README.md).
## Assumptions and unresolved questions
- The historical native implementation is `80cf197` on `codex/behavior-authoring-20260910`, following
baseline `1797516`. Local commits cover B1/B2/B3 authoring, nullable history cache
(`b6b76ef`), external palette integration (`aed40d7`), cfg(test) lifecycle projection
(`eb5161b`), history seed correction (`4ecad78`) and exact-identity native oracle.
Later imports add focus recovery, exact packaging notices and scenario workspace/
owner restoration. Root80cf197 fixes document/scenario viewport Sensor identity
with a real mounted red-before/green-after regression.
- This fresh host has task-local Rust/Cargo 1.98.1 GNU/LLVM and verified LLVM-MinGW.
The integrated2cc4d11 desktop suite passes676/676, zero failed/ignored, with a
warning-free build; formatting and strict Clippy pass. History, pure lifecycle,
creation and isolated chooser are included. New working code still needs its
own gate. Native80cf197 evidence is separate. [Exact hashes/logs](STATE.md).
- Strict language HTML regeneration and the source7 OpenAPI checker passed locally:
72 fixtures, 204 schemas and 90 scalar leaves. Actual isolated Capella scenario,
activity/lifeline and LOOP creation was saved/inspected; this is reference study,
not Studio acceptance. [Native benchmark](release-capella-scenario-benchmark-20260910.md).
- Core/source7 already implements substantial direct behavior, typed data/ports,
contextual chain links, maintained message-operation identity, semantic reviews,
artifacts, hosted source envelopes and recovery. Historical absence claims must
be checked against code and newer scoped evidence, not copied forward.
- [Requirements audit](release-requirements-delta-20260910.md): Constraint and other
expression strings are descriptive, with no calculation/binding/result evaluator;
unit strings persist/inherit without dimensional compatibility or conversion;
named types/sequence/scenario reuse and includes exist, but there is no maintained
cross-project package/version/instance/update contract. These are explicit gaps.
- The same audit identifies concrete unbounded Constraint target choices and generic
expanded collection editors. Explorer already has hierarchy/search/paging; the
problem is targeted, not a claim that all selection is flat.
- Gitea #79's same-selected-ID/visible-owner defect is fixed in the imported
scenario checkpoint; owner restoration and pending-draft guards pass current
tests. B4 independent review #114 adds Scene integration requirements for actual
lifetime extents, branch-local activation clipping, incomplete lanes and self arrows.
- Codex-9e8r6t and Codex-ytr83r have independent host-local Cargo/native leases.
Named creation/workspace/palette/package integration is imported here.9e8r6t's
coherent CLI/evidence checkpoint01b76a1 awaits import; it owns staged-source and
the next runnable Windows prerelease. ytr83r owns production B4 and its native
oracle.9e8r6t successfully reused existing credentials for direct push; this
host is setting up the same authorized publication path.
- Current Linux acceptance is open; this host has no installed WSL distribution.
Historical Linux core/server/cross-build results have exact older snapshot scope.
The earlier denied guest workaround did not run and must not be bypassed.
- Combining already coherent desktop slices should preserve behavior; fresh build,
automated and native gates must test that assumption.
- The baseline/library designs can fit existing atomic Project/Command/source
boundaries; exact references, deletion, transition, imports, freezing and
numeric rules still need ratified contracts and falsification.
- Pinned package provenance must be distinguished from checked type compatibility;
unchanged quantity kinds cannot become incompatible merely through cosmetic
repackaging without a deliberate policy. Energy and Torque remain distinct.
- Ubuntu26.04/WSL2 is the current supported Linux route; reliable repeat startup,
guest toolchain/dependencies and GUI/service/package execution remain open.
Initial shell success does not erase later startup timeouts. Old QEMU work is
retained as history, not the next route to repeat.
- Large-model human/AI task usability, cross-file refactoring, actual chooser
latency/keyboard/minimum geometry and complete native lifecycle behavior remain
to be measured. Parser admission or a widget experiment is insufficient.
## Assumptions
## Candidate designs and selected directions
- Compact contextual tools and an adequately sized scenario workspace can reduce
friction; only actual engineering tasks at minimum/wide sizes can validate this.
- The ratified bounded pure analysis and pinned-library architecture can satisfy
real engineering work without full language/runtime adoption. This remains a
product adequacy assumption until actual budgets, approximate calculations and
two-project/source/API/native workflows pass; metadata is insufficient.
- Existing shared core/source/history boundaries remain suitable for additions;
foundational examples and independent criticism must test that assumption.
The imported [analysis proposal](release-analysis-semantics-design-20260910.md),
[library proposal](release-libraries-semantics-design-20260910.md),
[paired critique](release-semantic-proposals-review-20260910.md) and
[textual authoring design](release-textual-authoring-design-20260911.md) retain
alternatives and counterexamples. Metadata-only analysis and bare source includes
do not meet evaluated quantities or maintained libraries.
## Open questions
Codex-956atf root selected property-owned baseline values/formulas with explicit
ModelBaseline/Case evaluation and one-level case overrides for contract refinement.
No hidden default case or UI-dependent evaluation; inherited formulas use the
effective requested case, and complete freeze captures definitions/provenance.
Exact numeric, library normalization/type compatibility and transition details
remain open. This direction is recorded in [DECISIONS.md](DECISIONS.md), not silently
treated as completed production semantics.
- Do the ratified owned-value/shared-check/R6/R7 contracts meet real engineering
budgets once implemented, including approximate numeric requirements? What
measured ceilings keep work bounded without rejecting ordinary studies?
- Does actual package closure/canonical consumer typing/source preparation and
three-way update preserve every stable identity and local engineering edit?
- What migration and OA↔SA treatment follows from those contracts without guessing
executable meaning from old strings or silently changing old models?
- How can text-only creation establish library instance bases/maps, preserve
compatibility across cosmetic package revisions, export subjectless cases, and
transition mounted OA/SA data without hidden GUI normalization or silent forks?
- Will full evaluator and private-capture replay preserve effective-case and
deterministic work-limit behavior across direct, REST and cache-warm execution?
- Which source-preserving refactoring/navigation and grammar changes reduce actual
engineering effort while retaining stable identity and cross-file ownership?
- Do current real gestures, exact caller return, nested scopes and lifecycle markers
remain usable and correct under minimum sizing, scrolling, zoom and dirty drafts?
- Which uncovered teaching, traceability, transition, saved-view, state/data, hosted
and platform cases remain after current-binary acceptance? What supported Linux
environment can complete the remaining actual runtime gate?
- Does "standardized" require any external conformance beyond the project's precise
interoperable spec/API? Full OMG conformance is not inferred merely from comparison
to SysML v2; explicit claims require a mapped and verified contract.
The external staged prerequisite design reserves source/consumer identities,
keeps the parent context/rawtext/slot, stages locally and creates source+consumer
in one atomic final command. It must not implicitly allocate work, involve
capabilities, retarget an Exchange-fixed endpoint or accept a different editor.
Qualified Action allocation remains a distinct engineering choice.
## Candidate hypotheses / designs
## Retained implementation decisions
The [requirements delta](release-requirements-delta-20260910.md) compares metadata/
snippets, bounded pure engineering analysis with maintained pinned libraries,
external evaluators, and full standards adoption. Metadata alone fails the explicit
new semantics; external tools alone still need binding/result contracts. The report
recommends the bounded route for root synthesis, not as a ratified decision.
- Keep one authoritative typed Project, validated atomic Commands, stable
identities and explicit references. Semantic ownership/allocation/order and
diagram geometry remain distinct; forms are adapters, not a second metamodel.
- Preserve source ownership/comments/includes and exact model/source history.
Writeback succeeds before adoption. Invalid/rejected raw drafts survive and
browsing/tool selection does not silently accept another editor. Recovery
validates/replans rather than trusting serialized commands.
Independent maximum-reasoning tracks now compare owned engineering properties
versus analysis contexts; calculation definitions/applications; numeric/unit rules;
and copy, linked-definition, live-instance or pinned-definition/editable-instance
library architectures. Require concrete falsifiers, source/API migration effects,
identity/history/recovery behavior and independent critique before convergence.
These alternatives converged after concrete experiments and independent review:
B property bases/shared checks/full capture, R6/R7, exact pins with same-origin
typed compatibility and explicit portable normalization are now ratified. Complete
11-file proposed baseline and120-assertion finite library prototype remain distinct
from production admission. Independent292-check review exposed C1 dynamic-work
expansion and a C2 prototype threshold-unit gap; implementation must close both.
The text-only40-identity fixture and13 mounted chooser tests pass. Production
numeric foundation and inspector/review chooser are active, with gates pending.
The subsequent context experiment passed218 assertions: shared property baselines
and one-level overrides reduce repeated input maintenance while retaining explicit
independent freeze. Root selected that B direction for precise contract refinement;
quantity/library/source details remain open. [Decision](DECISIONS.md),
[experiment and limits](release-analysis-context-experiment-20260911.md).
## Decisions retained
- Keep explicit Rust crate responsibilities and one authoritative typed Project
with validated atomic Commands. A thin source CLI reuses the shared core. Separate semantic identity/ownership, allocation/reference/order and
diagram geometry. Forms are closed adapters, not a second metamodel. New persistent
semantics require core/source/UI/API/reference/transition parity. [Decisions](DECISIONS.md).
- Preserve authored source ownership/comments/includes, exact identity and accepted
model/source history. Source writeback must succeed before adoption. Raw invalid,
excluded or rejected drafts survive; navigation/tools must not implicitly accept
another editor. Recovery validates/replans rather than trusting serialized commands.
- Semantic reconciliation uses finite typed facets, frozen baselines, explicit
mappings and one atomic candidate. Names alone never establish identity.
- OA↔SA transitions are explicit engineering decisions. New persistent semantics
need validation/reference/deletion/transition/source/API/UI/history parity.
- Exact evidence bytes and host attribution survive integration. No focused
test total is added to a full-suite count; old binary acceptance does not
transfer to newly merged code.
mappings, actual destination slots and a single atomic candidate. Do not infer
identity by name or apply reviewed rows as sequential independent edits.
[Foundation](release-semantic-batch-foundation-design.md),
[artifact contract](release-semantic-artifacts.md).
- Layer transitions are explicit engineering decisions, not automatic propagation.
Behavior-root navigation must establish the requested owner while preserving
unrelated architecture presentation. Ports/allocation and contextual occurrences
retain their distinct domain meaning. [Source7 contract](release-source7-integration-decision-20260906.md).
- Hosted revision/source envelopes and ETag checks remain atomic; credentials stay
out of persisted models and repository. Linux journal publication retains the
selected no-replace invariant, with measured limits, not an overwrite fallback.
[Journal decision](release-linux-journal-publication.md).
- Repair/evaluate the compact palette informed by native Capella evidence; do not
return to the rejected horizontal toolbar. Lifecycle projection remains isolated
under cfg(test) until coordinated Scene integration and actual acceptance.
- Preserve historical scope decisions as history. In particular, architecture-only
SVG is implemented and behavior SVG was deferred; that is not proof of satisfying
the revised release objective. Reassess older exclusions against current scope.
## Current strategy and coordination
## Current strategy
Root owns local integration, serialized builds, native inputs and final synthesis.
The local audit resolves evidence and reviews source; independent server review
has its own report. Codex-9e8r6t owns staged sources/first actions and source CLI;
Codex-956atf owns chooser, semantic contracts and the shared canvas design. Read Gitea issues #1/#2 and Wiki before
resuming, obtain exact coherent transfer heads, and respect their source ownership.
Finish the new workspace/chooser compile and native loop; independently review
and measure the numeric foundation before expanding the ratified semantics.
Integrate coherent external CLI/staged-source/B4 checkpoints while preserving
ownership and source attribution. Use independent complete model/source oracles
and exact identity/history comparisons. Publish coordinated runnable milestone
prereleases; close actual analysis/library/source/API/native, cross-platform,
service/package and independent end-state gates before the final handoff.
First finish and verify the integrated current source using the imported corrected
evaluator and newly prepared independent lifetime oracle. Then close remaining modeling/textual,
analysis/library and platform gates with independent criticism and whole-task
acceptance. Update facts, assumptions and decisions as evidence changes them.
The [archived Task Ledger](history/2026-09-10-ledgers-before-continuation-refresh/TASK_LEDGER.md)
and [archived STATE](history/2026-09-10-ledgers-before-continuation-refresh/STATE.md)
preserve all older claims, decisions and evidence links exactly. Relative report
links inside those byte-preserved files resolve from their original `harness/`
context. Their historical test totals and process holds are not live status.
-156
View File
@@ -1,156 +0,0 @@
//! Native issue4 fixture/oracle. generate DIR | check oa|sa STAGE ENTRY
//! snapshot ENTRY DIR | compare ENTRY DIR | self-check
use oa_core::{source_project::*, *};
use std::{collections::{BTreeMap, BTreeSet}, fs, path::Path};
fn id(n: u128) -> Id { Id::from_u128(0xc1140000000040008000000000000000 + n) }
fn work(layer: Layer) -> Kind { if layer == Layer::Oa { Kind::Activity } else { Kind::Function } }
fn fixture(layer: Layer) -> Project {
let mut p = Project::empty();
p.id = id(1); p.alias = "child_work_native".into(); p.name = format!("Issue4 {layer:?} child work");
for (n, kind, name) in [(10, if layer == Layer::Oa { Kind::Entity } else { Kind::System }, "Performer"), (11, work(layer), "Parent work")] {
p.elements.insert(id(n), Element { id: id(n), alias: format!("seed_{n}"), name: name.into(), description: format!("Preserve original {name}"), parent: None, body: ElementBody::new(kind, layer).unwrap() });
}
p.relations.insert(id(20), Relation { id:id(20), alias:"original_allocation".into(), name:"Original responsibility".into(), description:"Preserve allocation engineering rationale 雨".into(), body:RelationBody::Allocation { source:id(10), target:id(11) } });
if layer == Layer::Oa {
for n in [12,13] {
p.elements.insert(id(n), Element { id:id(n), alias:format!("role_{n}"), name:format!("Responsibility role {n}"), description:"Preserve role".into(), parent:None, body:ElementBody::new(Kind::Role, layer).unwrap() });
for (r, kind, source, target) in [(n+10,RelationKind::RoleAssignment,id(n),id(11)),(n+20,RelationKind::RoleRealization,id(10),id(n))] {
p.relations.insert(id(r), Relation { id:id(r), alias:format!("responsibility_{r}"), name:format!("Role link {r}"), description:format!("Retain role link metadata {r}"), body:RelationBody::new(kind,source,target) });
}
}
}
for (index, owner) in [id(20),id(22),id(23)].into_iter().filter(|owner| p.relations.contains_key(owner)).enumerate() {
let key=id(50+index as u128);
p.artifact_links.insert(key, ArtifactLink { id:key, alias:format!("evidence_{index}"), name:format!("Responsibility evidence {index}"), description:"Keep owner identity and evidence metadata".into(), owner, relation:ArtifactRelation::References, uri:format!("https://example.invalid/evidence/{index}"), revision:Some("review-7".into()), fragment:Some("assignment".into()) });
}
p.diagrams.insert(id(2), Diagram { id:id(2), alias:"allocation_view".into(), name:format!("{layer:?} allocated work"), description:"Native child-work fixture".into(), layer, projection:ProjectionKind::Allocation, routes:vec![], view_options:Default::default(), nodes:BTreeMap::new() });
for (n,element,x,y,collapsed) in [(40,id(10),60.,60.,false),(41,id(11),84.,124.,true)] {
p.diagrams.get_mut(&id(2)).unwrap().nodes.insert(id(n), NodePlacement { id:id(n), alias:format!("seed_view_{n}"), element, position:Position{x,y}, size:Size{width:224.,height:94.}, collapsed, port_anchors:vec![] });
}
execute_batch(&p,&[]).unwrap()
}
fn bundle(p:&Project)->CompiledSourceProject {
let source=language::format(p).replacen("{\n","{\n include \"rationale.arcadia\";\n",1);
let raw=SourceBundle { entry:"project.arcadia".into(), files:BTreeMap::from([("project.arcadia".into(),source),("rationale.arcadia".into(),"// Preserve included note bytes: responsibility 雨.\r\n".into())]) };
let result=compile_bundle(&raw,ProjectLimits::default()).unwrap();
assert_eq!(result.bundle,raw); assert_eq!(&result.decoded.project,p); result
}
fn load(entry:&Path)->CompiledSourceProject {
let raw=source_files::load_project(entry,ProjectLimits::default()).unwrap().reachable_bundle();
let result=compile_bundle(&raw,ProjectLimits::default()).unwrap();
assert_eq!(result.bundle,raw,"No source normalization permitted");
execute_batch(&result.decoded.project,&[]).unwrap(); result
}
fn advance(base:&mut CompiledSourceProject,commands:Vec<Command>) {
let expected=execute_batch(&base.decoded.project,&commands).unwrap();
*base=update_bundle(base,&expected,ProjectLimits::default()).unwrap();
}
#[derive(Clone, Copy)]
struct Keys { children:[Id;3], nodes:[Id;3], allocation:Id }
fn expected(layer:Layer,stage:usize,k:Keys)->CompiledSourceProject {
assert!(stage<=3);
let mut base=bundle(&fixture(layer));
for step in 0..stage {
let p=&base.decoded.project;
let key=k.children[step];
let name=format!("New {}",work(layer).label().to_lowercase());
let element=Element { id:key, alias:p.fresh_alias(&name,"element",key), name, description:String::new(), parent:Some(if step==2 { k.children[0] } else { id(11) }), body:ElementBody::new(work(layer),layer).unwrap() };
let mut commands=vec![Command::CreateElement(element.clone())];
if step==1 {
commands.push(Command::CreateRelation(Relation { id:k.allocation, alias:p.fresh_alias("Performs","relation",k.allocation), name:"Performs".into(), description:String::new(), body:RelationBody::Allocation { source:id(10),target:key } }));
} else {
for original in p.relations.values().filter(|r| [id(20),id(22),id(23)].contains(&r.id)) {
let mut r=original.clone();
match &mut r.body { RelationBody::Allocation{target,..}|RelationBody::RoleAssignment{target,..}=>*target=key,_=>unreachable!() }
commands.push(Command::EditRelation(r.id,r));
}
}
if step==0 {
let mut node=p.diagrams[&id(2)].nodes[&id(41)].clone(); node.collapsed=false;
commands.push(Command::EditNode{diagram:id(2),original:node.id,node});
}
let mut with_element=p.clone(); with_element.elements.insert(key,element);
let (x,y)=[(108.,188.),(108.,298.),(132.,252.)][step];
commands.push(Command::CreateNode { diagram:id(2), node:NodePlacement { id:k.nodes[step], alias:with_element.fresh_alias(&format!("view_{}",with_element.named(key)),"node",k.nodes[step]), element:key,position:Position{x,y},size:Size{width:224.,height:94.},collapsed:false,port_anchors:vec![] } });
if step==2 {
// Grandchild expands first child to bottom370; sibling follows at394
// (24px reflow gap), independently predicted for this fixed fixture.
let mut sibling=p.diagrams[&id(2)].nodes[&k.nodes[1]].clone(); sibling.position.y=394.;
commands.push(Command::EditNode{diagram:id(2),original:sibling.id,node:sibling});
}
advance(&mut base,commands);
}
base
}
fn only(ids:Vec<Id>,role:&str)->Id { assert_eq!(ids.len(),1,"Exactly one {role}"); ids[0] }
fn admit(actual:&Project,layer:Layer,stage:usize)->Keys {
let mut k=Keys{children:[Id::nil();3],nodes:[Id::nil();3],allocation:Id::nil()};
if stage==0 { return k; }
let target=actual.relations[&id(20)].target();
k.children[0]=if stage==3 { actual.elements[&target].parent.unwrap() } else { target };
if stage>=2 {
k.children[1]=only(actual.elements.values().filter(|e|e.parent==Some(id(11))&&e.id!=k.children[0]&&e.kind()==work(layer)).map(|e|e.id).collect(),"second child");
k.allocation=only(actual.relations.values().filter(|r|r.kind()==RelationKind::Allocation&&r.source()==id(10)&&r.target()==k.children[1]).map(|r|r.id).collect(),"new sibling allocation");
}
if stage==3 { k.children[2]=target; }
for step in 0..stage {
let e=&actual.elements[&k.children[step]];
assert_eq!(e.kind(),work(layer)); assert_eq!(e.layer(),layer);
assert_eq!(e.parent,Some(if step==2{k.children[0]}else{id(11)}));
k.nodes[step]=only(actual.diagrams[&id(2)].nodes.values().filter(|n|n.element==e.id).map(|n|n.id).collect(),"child placement");
}
let seed=fixture(layer); let mut used=BTreeSet::new();
for key in k.children[..stage].iter().chain(&k.nodes[..stage]).copied().chain((stage>=2).then_some(k.allocation)) {
assert_eq!(key.get_version_num(),4,"Native fresh UUIDv4 required");
assert!(!seed.contains(key)&&used.insert(key),"Fresh distinct role identity required");
}
k
}
fn check(actual:&CompiledSourceProject,layer:Layer,stage:usize) {
let prediction=expected(layer,stage,admit(&actual.decoded.project,layer,stage));
assert_eq!(actual.decoded.project,prediction.decoded.project,"Complete typed model mismatch, including placements and references");
assert_eq!(actual.bundle,prediction.bundle,"Exact source mismatch, including included note bytes");
}
fn history(actual:&CompiledSourceProject,layer:Layer,stage:usize,first:&CompiledSourceProject,second:Option<&CompiledSourceProject>) {
check(first,layer,1);
let first_keys=admit(&first.decoded.project,layer,1);
let current_keys=admit(&actual.decoded.project,layer,stage);
assert_eq!(current_keys.children[0],first_keys.children[0],"First child identity must match original first-commit carrier");
assert_eq!(current_keys.nodes[0],first_keys.nodes[0],"First placement identity must match original first-commit carrier");
if stage==3 {
let second=second.expect("Original second-commit carrier required");
check(second,layer,2);
history(second,layer,2,first,None);
let second_keys=admit(&second.decoded.project,layer,2);
assert_eq!(current_keys.children[1],second_keys.children[1],"Sibling identity must persist");
assert_eq!(current_keys.nodes[1],second_keys.nodes[1],"Sibling placement identity must persist");
assert_eq!(current_keys.allocation,second_keys.allocation,"Sibling allocation identity must persist");
}
}
fn write_bundle(dir:&Path,raw:&SourceBundle) {
assert!(!dir.exists(),"Use a new evidence directory"); fs::create_dir_all(dir).unwrap();
for (name,bytes) in &raw.files { let path=dir.join(name); if let Some(parent)=path.parent(){fs::create_dir_all(parent).unwrap();} fs::write(path,bytes).unwrap(); }
}
fn main() {
assert_eq!(SCHEMA_VERSION,7);
let args:Vec<_>=std::env::args().collect();
match args.get(1).map(String::as_str) {
Some("generate")=>{assert_eq!(args.len(),3);let dir=Path::new(&args[2]);assert!(!dir.exists()); for (name,layer) in [("oa",Layer::Oa),("sa",Layer::Sa)] {write_bundle(&dir.join(name),&bundle(&fixture(layer)).bundle);} println!("PASS deterministic OA/SA native fixtures generated");}
Some("check")=>{
let layer=match args[2].as_str(){"oa"=>Layer::Oa,"sa"=>Layer::Sa,_=>panic!("oa|sa")};
let stage=args[3].parse::<usize>().unwrap();assert!(stage<=3);
assert_eq!(args.len(),5+stage.saturating_sub(1),"Stages2/3 require original stage1/stage2 checkpoint directories");
let actual=load(Path::new(&args[4]));check(&actual,layer,stage);
if stage>=2 {
let first=load(&Path::new(&args[5]).join("project.arcadia"));
let second=if stage==3 {Some(load(&Path::new(&args[6]).join("project.arcadia")))} else {None};
history(&actual,layer,stage,&first,second.as_ref());
}
println!("PASS complete model, exact source and pinned history stage {stage}");
}
Some("snapshot")=>{assert_eq!(args.len(),4);let actual=load(Path::new(&args[2]));write_bundle(Path::new(&args[3]),&actual.bundle);println!("PASS immutable source checkpoint");}
Some("compare")=>{assert_eq!(args.len(),4);let actual=load(Path::new(&args[2]));let checkpoint=load(&Path::new(&args[3]).join(&actual.bundle.entry));assert_eq!(actual.bundle,checkpoint.bundle,"Saved reachable source bytes changed");assert_eq!(actual.decoded.project,checkpoint.decoded.project);println!("PASS exact checkpoint bytes and model");}
Some("self-check")=>{for layer in [Layer::Oa,Layer::Sa]{let keys=Keys{children:std::array::from_fn(|_|Id::new_v4()),nodes:std::array::from_fn(|_|Id::new_v4()),allocation:Id::new_v4()};for stage in 0..=3 {let candidate=expected(layer,stage,keys);check(&candidate,layer,stage);if stage>=2 {history(&candidate,layer,stage,&expected(layer,1,keys),(stage==3).then(||expected(layer,2,keys)).as_ref());}let mut altered=candidate.decoded.project.clone();altered.elements.get_mut(&id(11)).unwrap().description="Unrequested mutation".into();assert_ne!(expected(layer,stage,admit(&altered,layer,stage)).decoded.project,altered);}}println!("PASS oracle fixed points and unrelated-mutation discriminator");}
_=>panic!("generate DIR | check oa|sa STAGE ENTRY [STAGE1DIR] [STAGE2DIR] | snapshot ENTRY DIR | compare ENTRY DIR | self-check"),
}
}
@@ -0,0 +1,15 @@
# Baseline contract fixtures
Companion to [the candidate contract](../../release-analysis-baseline-contract-20260911.md). This is proposed schema/source/JSON, not implemented Studio syntax.
- active.arcadia.proposed is a complete UUID-bearing local model with four base bindings, two cases and one shared check.
- evaluate-*.json are complete candidate REST request bodies.
- portable-document.json.proposed exposes the complete candidate Project, exact source, local definitions and explicitly empty library collections.
- freeze-degraded.json.proposed retains those exact document bytes, a SHA256 commitment, complete effective bindings/checks, and private definition inventory.
- expected-observations.json records hand-derived arithmetic/context expectations, not evaluator output.
- r7-literal-boundaries.json contains full exact JSON literal nodes and their expected canonical source, including 301-digit and 4096-bit boundaries.
- fixture-integrity.json records only transport/source/UUID checks and size counts.
Run build-fixtures.ps1 with PowerShell 7 to regenerate the JSON fixtures. It writes only within this directory. The script constructs the proposed typed data; it does not parse source, evaluate expressions, run Rust, or establish Project/source semantic equality. Changing the fixture file's exact line endings changes the captured document and digest; preserve these snapshot bytes when publishing.
The full local capture has no imports, so empty pins/package content are complete for this example. Nonempty pinned-package capture and offline replay remain a required paired library/evaluator acceptance test, not something this local transport fixture has proved.
@@ -0,0 +1,55 @@
arcadia 8;
project response_budget "Response budget" id "4210b9f2-7046-47f0-aa3b-153f626a579d" {
description "A complete proposed analysis model. Schema 8 and the new declarations are not implemented source7 syntax.";
quantity_kind duration "Duration" id "188b911c-5f8b-431c-9eea-2d41f3953767" {
dimension { time: 1; }
algebra Linear;
}
unit second "Second" id "534f4672-1ba5-4157-8b0a-b9cf41d0a3ce" {
kind @duration; scale 1; offset 0; symbol "s";
}
unit millisecond "Millisecond" id "39053f37-fec9-4708-9f0a-3959d223f891" {
kind @duration; scale ratio(1, 1000); offset 0; symbol "ms";
}
calculation combine "Combine durations" id "fd7ce7ab-f789-40bc-b699-0c37f1b6902f" {
input a id "28ff40b0-1b8f-4735-8536-5a6ab21b56cb" : Quantity(@duration);
input b id "7a47cc1e-8ae5-4551-b2d8-8a68108e0976" : Quantity(@duration);
result Quantity(@duration);
body $a + $b;
}
oa_activity respond "Respond" id "cabec6a1-7d5d-48f3-8d2d-a7fc8cf78f59" {
oa_activity receive "Receive" id "4ab9a8a7-7552-488d-9862-e14b89e7b353" {}
oa_activity dispatch "Dispatch" id "6df991ba-4c5e-4535-99e2-524ae274ee8b" {}
}
oa_property receive_time "Receive time" id "63a931c2-675a-4fce-89e5-04361cb99450" {
subject @receive; type Quantity(@duration); display_unit @millisecond;
base 750 [@millisecond];
}
oa_property dispatch_time "Dispatch time" id "c33fc772-2a60-4044-9af9-73c61c563ace" {
subject @dispatch; type Quantity(@duration); display_unit @second;
base 1 [@second];
}
oa_property total_time "Total time" id "bd5b68e8-65fc-4e35-9071-a73061316a4b" {
subject @respond; type Quantity(@duration); display_unit @second;
// The formula is shared. Every read uses the requested evaluation context.
base @combine(a: @receive_time, b: @dispatch_time);
}
oa_property limit "Allowed duration" id "437bedbb-2cf4-44ee-a139-6563cc5086ec" {
subject @respond; type Quantity(@duration); display_unit @second;
base 2 [@second];
}
oa_check budget "Response budget" id "bac0de99-9d2f-4e53-9158-d424ecac61bf" {
subject none; documented_constraint none;
predicate @total_time <= @limit;
}
oa_analysis degraded "Degraded radio" id "38036c65-16d4-449c-837b-91eb141c1d93" {
subject none; base ModelBaseline;
overrides { @receive_time = 1250 [@millisecond]; }
}
oa_analysis unmeasured "Unmeasured radio" id "99d3e09a-0ec3-4194-9dd4-9f3de6625cde" {
subject none; base ModelBaseline;
overrides { @receive_time = unbound; }
}
}
@@ -0,0 +1,216 @@
# Fixture construction only. This does not parse/evaluate the proposed language.
$ErrorActionPreference = 'Stop'
$taskDir = $PSScriptRoot
$ids = @{
project = '4210b9f2-7046-47f0-aa3b-153f626a579d'
duration = '188b911c-5f8b-431c-9eea-2d41f3953767'
second = '534f4672-1ba5-4157-8b0a-b9cf41d0a3ce'
millisecond = '39053f37-fec9-4708-9f0a-3959d223f891'
combine = 'fd7ce7ab-f789-40bc-b699-0c37f1b6902f'
input_a = '28ff40b0-1b8f-4735-8536-5a6ab21b56cb'
input_b = '7a47cc1e-8ae5-4551-b2d8-8a68108e0976'
respond = 'cabec6a1-7d5d-48f3-8d2d-a7fc8cf78f59'
receive = '4ab9a8a7-7552-488d-9862-e14b89e7b353'
dispatch = '6df991ba-4c5e-4535-99e2-524ae274ee8b'
receive_time = '63a931c2-675a-4fce-89e5-04361cb99450'
dispatch_time = 'c33fc772-2a60-4044-9af9-73c61c563ace'
total_time = 'bd5b68e8-65fc-4e35-9071-a73061316a4b'
limit = '437bedbb-2cf4-44ee-a139-6563cc5086ec'
degraded = '38036c65-16d4-449c-837b-91eb141c1d93'
unmeasured = '99d3e09a-0ec3-4194-9dd4-9f3de6625cde'
budget = 'bac0de99-9d2f-4e53-9158-d424ecac61bf'
}
function Exact([string]$n, [string]$d = '1') {
return @{ numerator = $n; denominator = $d }
}
function Local([string]$name) { return @{ Local = $ids[$name] } }
function ReadProperty([string]$name) { return @{ Property = $ids[$name] } }
function Literal([string]$n, [string]$unit) {
return @{ Quantity = @{ magnitude = (Exact $n); unit = (Local $unit) } }
}
function Bound($expr) { return @{ Expression = $expr } }
function Record([string]$key, [string]$name, $body) {
return @{
id = $ids[$key]; alias = $key; name = $name; description = ''; body = $body
}
}
function Element([string]$key, [string]$name, $body, $parent = $null) {
$r = Record $key $name $body
$r.parent = $parent
return $r
}
function Property([string]$key, [string]$name, [string]$subject, [string]$unit, $binding) {
return Element $key $name @{ Oa = @{ Shared = @{ EngineeringProperty = @{
subject = @{ Element = $ids[$subject] }
value_type = $quantityType
display_unit = (Local $unit)
base = $binding
} } } }
}
function OrderedCopy($value) {
if ($null -eq $value) { return $null }
if ($value -is [Collections.IDictionary]) {
$copy = [ordered]@{}
[string[]]$keys = @($value.Keys)
[Array]::Sort($keys, [StringComparer]::Ordinal)
foreach ($key in $keys) { $copy[$key] = OrderedCopy $value[$key] }
return $copy
}
if ($value -is [Collections.IEnumerable] -and $value -isnot [string]) {
$copy = @()
foreach ($item in $value) { $copy += ,(OrderedCopy $item) }
return ,$copy
}
return $value
}
function JsonBytes($value) {
$json = (OrderedCopy $value) | ConvertTo-Json -Depth 100
$json = $json.Replace([string][char]13 + [string][char]10, [string][char]10)
return [Text.UTF8Encoding]::new($false).GetBytes($json + [string][char]10)
}
function SaveBytes([string]$name, [byte[]]$bytes) {
[IO.File]::WriteAllBytes((Join-Path $taskDir $name), $bytes)
}
$quantityType = @{ Scalar = @{ Quantity = (Local 'duration') } }
$definitions = @{}
$definitions[$ids.duration] = Record 'duration' 'Duration' @{ QuantityKind = @{
dimension = @(0, 0, 1, 0, 0, 0, 0); algebra = 'Linear'
} }
$definitions[$ids.second] = Record 'second' 'Second' @{ Unit = @{
kind = (Local 'duration'); scale = (Exact '1'); offset = (Exact '0'); symbol = 's'
} }
$definitions[$ids.millisecond] = Record 'millisecond' 'Millisecond' @{ Unit = @{
kind = (Local 'duration'); scale = (Exact '1' '1000'); offset = (Exact '0'); symbol = 'ms'
} }
$definitions[$ids.combine] = Record 'combine' 'Combine durations' @{ Calculation = @{
parameters = @(
@{ id = $ids.input_a; name = 'a'; value_type = $quantityType },
@{ id = $ids.input_b; name = 'b'; value_type = $quantityType }
)
result = $quantityType
body = (Bound @{ Binary = @{
op = 'Add'; left = @{ Parameter = $ids.input_a }; right = @{ Parameter = $ids.input_b }
} })
} }
$receiveBase = Bound (Literal '750' 'millisecond')
$dispatchBase = Bound (Literal '1' 'second')
$totalBase = Bound @{ Call = @{
definition = (Local 'combine')
arguments = @{
$ids.input_a = (ReadProperty 'receive_time')
$ids.input_b = (ReadProperty 'dispatch_time')
}
} }
$limitBase = Bound (Literal '2' 'second')
$degradedBinding = Bound (Literal '1250' 'millisecond')
$predicate = Bound @{ Binary = @{
op = 'LessOrEqual'; left = (ReadProperty 'total_time'); right = (ReadProperty 'limit')
} }
$elements = @{}
$elements[$ids.respond] = Element 'respond' 'Respond' @{ Oa = 'Activity' }
$elements[$ids.receive] = Element 'receive' 'Receive' @{ Oa = 'Activity' } $ids.respond
$elements[$ids.dispatch] = Element 'dispatch' 'Dispatch' @{ Oa = 'Activity' } $ids.respond
$elements[$ids.receive_time] = Property 'receive_time' 'Receive time' 'receive' 'millisecond' $receiveBase
$elements[$ids.dispatch_time] = Property 'dispatch_time' 'Dispatch time' 'dispatch' 'second' $dispatchBase
$elements[$ids.total_time] = Property 'total_time' 'Total time' 'respond' 'second' $totalBase
$elements[$ids.limit] = Property 'limit' 'Allowed duration' 'respond' 'second' $limitBase
$elements[$ids.budget] = Element 'budget' 'Response budget' @{ Oa = @{ Shared = @{ AnalysisCheck = @{
subject = $null; documented_constraint = $null; predicate = $predicate
} } } }
$elements[$ids.degraded] = Element 'degraded' 'Degraded radio' @{ Oa = @{ Shared = @{ AnalysisCase = @{
subject = $null; base = 'ModelBaseline'; overrides = @{ $ids.receive_time = $degradedBinding }
} } } }
$elements[$ids.unmeasured] = Element 'unmeasured' 'Unmeasured radio' @{ Oa = @{ Shared = @{ AnalysisCase = @{
subject = $null; base = 'ModelBaseline'; overrides = @{ $ids.receive_time = 'Unbound' }
} } } }
$sourceBytes = [IO.File]::ReadAllBytes((Join-Path $taskDir 'active.arcadia.proposed'))
$source = [Text.UTF8Encoding]::new($false, $true).GetString($sourceBytes)
$project = @{
schema_version = 8; id = $ids.project; alias = 'response_budget'; name = 'Response budget'
description = 'A complete proposed analysis model. Schema 8 and the new declarations are not implemented source7 syntax.'
elements = $elements; relations = @{}; diagrams = @{}; artifact_links = @{}; traces = @{}
definitions = $definitions; imports = @{}; definition_mounts = @{}; library_instances = @{}
}
$document = @{
model = $project
source = @{ entry = 'active.arcadia'; files = @{ 'active.arcadia' = $source } }
packages = @{}
}
[byte[]]$documentBytes = JsonBytes $document
SaveBytes 'portable-document.json.proposed' $documentBytes
$documentDigest = [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData($documentBytes)).ToLowerInvariant()
$manifest = @(
@{ property = $ids.receive_time; binding = $degradedBinding; origin = @{ CaseOverride = $ids.degraded } },
@{ property = $ids.dispatch_time; binding = $dispatchBase; origin = 'ModelBaseline' },
@{ property = $ids.total_time; binding = $totalBase; origin = 'ModelBaseline' },
@{ property = $ids.limit; binding = $limitBase; origin = 'ModelBaseline' }
) | Sort-Object { $_.property }
$freeze = @{
format = 'arcadia-analysis-freeze-1-proposal'
captured_document = @{
media_type = 'application/vnd.arcadia.portable-document+json'
sha256 = $documentDigest
bytes_base64 = [Convert]::ToBase64String($documentBytes)
}
origin = @{ Local = @{
document_instance = 'd911a3c0-8adb-4203-850c-b16e9b4e4d0a'; accepted_revision = 1
} }
model_id = $ids.project
context = @{ Case = $ids.degraded }
targets = @{ properties = @($ids.total_time); checks = @($ids.budget) }
numeric_profile = 'Q1-exact-1'
effective_bindings = @($manifest)
check_predicates = @(@{ check = $ids.budget; predicate = $predicate })
definition_capture = @{
document_sha256 = $documentDigest
local_definitions = @($ids.duration, $ids.second, $ids.millisecond, $ids.combine | Sort-Object)
imports = @()
}
}
SaveBytes 'freeze-degraded.json.proposed' (JsonBytes $freeze)
$expected = @{
status = 'Hand-derived expectations, not evaluator output'
baseline = @{ total_canonical_seconds = (Exact '7' '4'); check = 'Satisfied' }
degraded = @{ total_canonical_seconds = (Exact '9' '4'); check = 'Violated' }
unmeasured = @{ total = 'Missing'; input = $ids.receive_time; check = 'Unresolved' }
after_clear_unmeasured_override = @{ total_canonical_seconds = (Exact '7' '4'); check = 'Satisfied' }
after_baseline_dispatch_changes_to_two_seconds = @{
active_degraded_total = (Exact '13' '4'); frozen_degraded_total = (Exact '9' '4')
}
}
SaveBytes 'expected-observations.json' (JsonBytes $expected)
$largeInteger = [Numerics.BigInteger]::Pow([Numerics.BigInteger]10, 300).ToString()
$smallDenominator = [Numerics.BigInteger]::Pow([Numerics.BigInteger]2, 4095).ToString()
$boundInteger = ([Numerics.BigInteger]::Pow([Numerics.BigInteger]2, 4096) - 1).ToString()
$literalFixtures = @(
@{ name = 'ten_to_300'; expression = @{ Number = (Exact $largeInteger) }; canonical_source = $largeInteger },
@{ name = 'one_over_two_to_4095'; expression = @{ Number = (Exact '1' $smallDenominator) }; canonical_source = ('ratio(1, ' + $smallDenominator + ')') },
@{ name = 'negative_4096_bit_bound'; expression = @{ Number = (Exact ('-' + $boundInteger)) }; canonical_source = ('ratio(-' + $boundInteger + ', 1)') }
)
SaveBytes 'r7-literal-boundaries.json' (JsonBytes $literalFixtures)
# Verify only JSON transport, UUID completeness, raw source retention and hashes.
$parsed = Get-Content -LiteralPath (Join-Path $taskDir 'freeze-degraded.json.proposed') -Raw | ConvertFrom-Json
$decoded = [Convert]::FromBase64String($parsed.captured_document.bytes_base64)
if ([Convert]::ToBase64String($decoded) -cne [Convert]::ToBase64String($documentBytes)) { throw 'Document capsule differs' }
$parsedDoc = [Text.Encoding]::UTF8.GetString($decoded) | ConvertFrom-Json
if ($parsedDoc.source.files.'active.arcadia' -cne $source) { throw 'Source changed' }
if ($ids.Count -ne (@($ids.Values | Sort-Object -Unique)).Count) { throw 'Duplicate fixture UUID' }
foreach ($value in $ids.Values) {
[void][guid]::ParseExact($value, 'D')
if (-not $source.Contains('id "' + $value + '"')) { throw "Missing source identity $value" }
}
$stats = @{
source_lines = ($source -split [string][char]10).Count - 1
source_utf8_bytes = $sourceBytes.Length
explicit_source_uuids = $ids.Count
project_elements = $elements.Count
local_definitions = $definitions.Count
scoped_parameters = 2
base_bindings = 4
override_bindings = 2
shared_checks = 1
portable_document_sha256 = $documentDigest
verified = 'JSON transport, 17 syntactically valid distinct UUIDs, exact source bytes, capsule equality. No proposed parser/evaluator execution.'
}
SaveBytes 'fixture-integrity.json' (JsonBytes $stats)
$stats | ConvertTo-Json
@@ -0,0 +1,8 @@
{
"context": "ModelBaseline",
"targets": {
"properties": ["bd5b68e8-65fc-4e35-9071-a73061316a4b"],
"checks": ["bac0de99-9d2f-4e53-9158-d424ecac61bf"]
},
"numeric_profile": "Q1-exact-1"
}
@@ -0,0 +1,8 @@
{
"context": {"Case": "38036c65-16d4-449c-837b-91eb141c1d93"},
"targets": {
"properties": ["bd5b68e8-65fc-4e35-9071-a73061316a4b"],
"checks": ["bac0de99-9d2f-4e53-9158-d424ecac61bf"]
},
"numeric_profile": "Q1-exact-1"
}
@@ -0,0 +1,8 @@
{
"context": {"Case": "99d3e09a-0ec3-4194-9dd4-9f3de6625cde"},
"targets": {
"properties": ["bd5b68e8-65fc-4e35-9071-a73061316a4b"],
"checks": ["bac0de99-9d2f-4e53-9158-d424ecac61bf"]
},
"numeric_profile": "Q1-exact-1"
}
@@ -0,0 +1,39 @@
{
"after_baseline_dispatch_changes_to_two_seconds": {
"active_degraded_total": {
"denominator": "4",
"numerator": "13"
},
"frozen_degraded_total": {
"denominator": "4",
"numerator": "9"
}
},
"after_clear_unmeasured_override": {
"check": "Satisfied",
"total_canonical_seconds": {
"denominator": "4",
"numerator": "7"
}
},
"baseline": {
"check": "Satisfied",
"total_canonical_seconds": {
"denominator": "4",
"numerator": "7"
}
},
"degraded": {
"check": "Violated",
"total_canonical_seconds": {
"denominator": "4",
"numerator": "9"
}
},
"status": "Hand-derived expectations, not evaluator output",
"unmeasured": {
"check": "Unresolved",
"input": "63a931c2-675a-4fce-89e5-04361cb99450",
"total": "Missing"
}
}
@@ -0,0 +1,13 @@
{
"base_bindings": 4,
"explicit_source_uuids": 17,
"local_definitions": 4,
"override_bindings": 2,
"portable_document_sha256": "6667773d90f83626f760b2faa2bf7d57638947d95574e3fa3e80b64416f01497",
"project_elements": 10,
"scoped_parameters": 2,
"shared_checks": 1,
"source_lines": 55,
"source_utf8_bytes": 2678,
"verified": "JSON transport, 17 syntactically valid distinct UUIDs, exact source bytes, capsule equality. No proposed parser/evaluator execution."
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,425 @@
{
"model": {
"alias": "response_budget",
"artifact_links": {},
"definition_mounts": {},
"definitions": {
"188b911c-5f8b-431c-9eea-2d41f3953767": {
"alias": "duration",
"body": {
"QuantityKind": {
"algebra": "Linear",
"dimension": [
0,
0,
1,
0,
0,
0,
0
]
}
},
"description": "",
"id": "188b911c-5f8b-431c-9eea-2d41f3953767",
"name": "Duration"
},
"39053f37-fec9-4708-9f0a-3959d223f891": {
"alias": "millisecond",
"body": {
"Unit": {
"kind": {
"Local": "188b911c-5f8b-431c-9eea-2d41f3953767"
},
"offset": {
"denominator": "1",
"numerator": "0"
},
"scale": {
"denominator": "1000",
"numerator": "1"
},
"symbol": "ms"
}
},
"description": "",
"id": "39053f37-fec9-4708-9f0a-3959d223f891",
"name": "Millisecond"
},
"534f4672-1ba5-4157-8b0a-b9cf41d0a3ce": {
"alias": "second",
"body": {
"Unit": {
"kind": {
"Local": "188b911c-5f8b-431c-9eea-2d41f3953767"
},
"offset": {
"denominator": "1",
"numerator": "0"
},
"scale": {
"denominator": "1",
"numerator": "1"
},
"symbol": "s"
}
},
"description": "",
"id": "534f4672-1ba5-4157-8b0a-b9cf41d0a3ce",
"name": "Second"
},
"fd7ce7ab-f789-40bc-b699-0c37f1b6902f": {
"alias": "combine",
"body": {
"Calculation": {
"body": {
"Expression": {
"Binary": {
"left": {
"Parameter": "28ff40b0-1b8f-4735-8536-5a6ab21b56cb"
},
"op": "Add",
"right": {
"Parameter": "7a47cc1e-8ae5-4551-b2d8-8a68108e0976"
}
}
}
},
"parameters": [
{
"id": "28ff40b0-1b8f-4735-8536-5a6ab21b56cb",
"name": "a",
"value_type": {
"Scalar": {
"Quantity": {
"Local": "188b911c-5f8b-431c-9eea-2d41f3953767"
}
}
}
},
{
"id": "7a47cc1e-8ae5-4551-b2d8-8a68108e0976",
"name": "b",
"value_type": {
"Scalar": {
"Quantity": {
"Local": "188b911c-5f8b-431c-9eea-2d41f3953767"
}
}
}
}
],
"result": {
"Scalar": {
"Quantity": {
"Local": "188b911c-5f8b-431c-9eea-2d41f3953767"
}
}
}
}
},
"description": "",
"id": "fd7ce7ab-f789-40bc-b699-0c37f1b6902f",
"name": "Combine durations"
}
},
"description": "A complete proposed analysis model. Schema 8 and the new declarations are not implemented source7 syntax.",
"diagrams": {},
"elements": {
"38036c65-16d4-449c-837b-91eb141c1d93": {
"alias": "degraded",
"body": {
"Oa": {
"Shared": {
"AnalysisCase": {
"base": "ModelBaseline",
"overrides": {
"63a931c2-675a-4fce-89e5-04361cb99450": {
"Expression": {
"Quantity": {
"magnitude": {
"denominator": "1",
"numerator": "1250"
},
"unit": {
"Local": "39053f37-fec9-4708-9f0a-3959d223f891"
}
}
}
}
},
"subject": null
}
}
}
},
"description": "",
"id": "38036c65-16d4-449c-837b-91eb141c1d93",
"name": "Degraded radio",
"parent": null
},
"437bedbb-2cf4-44ee-a139-6563cc5086ec": {
"alias": "limit",
"body": {
"Oa": {
"Shared": {
"EngineeringProperty": {
"base": {
"Expression": {
"Quantity": {
"magnitude": {
"denominator": "1",
"numerator": "2"
},
"unit": {
"Local": "534f4672-1ba5-4157-8b0a-b9cf41d0a3ce"
}
}
}
},
"display_unit": {
"Local": "534f4672-1ba5-4157-8b0a-b9cf41d0a3ce"
},
"subject": {
"Element": "cabec6a1-7d5d-48f3-8d2d-a7fc8cf78f59"
},
"value_type": {
"Scalar": {
"Quantity": {
"Local": "188b911c-5f8b-431c-9eea-2d41f3953767"
}
}
}
}
}
}
},
"description": "",
"id": "437bedbb-2cf4-44ee-a139-6563cc5086ec",
"name": "Allowed duration",
"parent": null
},
"4ab9a8a7-7552-488d-9862-e14b89e7b353": {
"alias": "receive",
"body": {
"Oa": "Activity"
},
"description": "",
"id": "4ab9a8a7-7552-488d-9862-e14b89e7b353",
"name": "Receive",
"parent": "cabec6a1-7d5d-48f3-8d2d-a7fc8cf78f59"
},
"63a931c2-675a-4fce-89e5-04361cb99450": {
"alias": "receive_time",
"body": {
"Oa": {
"Shared": {
"EngineeringProperty": {
"base": {
"Expression": {
"Quantity": {
"magnitude": {
"denominator": "1",
"numerator": "750"
},
"unit": {
"Local": "39053f37-fec9-4708-9f0a-3959d223f891"
}
}
}
},
"display_unit": {
"Local": "39053f37-fec9-4708-9f0a-3959d223f891"
},
"subject": {
"Element": "4ab9a8a7-7552-488d-9862-e14b89e7b353"
},
"value_type": {
"Scalar": {
"Quantity": {
"Local": "188b911c-5f8b-431c-9eea-2d41f3953767"
}
}
}
}
}
}
},
"description": "",
"id": "63a931c2-675a-4fce-89e5-04361cb99450",
"name": "Receive time",
"parent": null
},
"6df991ba-4c5e-4535-99e2-524ae274ee8b": {
"alias": "dispatch",
"body": {
"Oa": "Activity"
},
"description": "",
"id": "6df991ba-4c5e-4535-99e2-524ae274ee8b",
"name": "Dispatch",
"parent": "cabec6a1-7d5d-48f3-8d2d-a7fc8cf78f59"
},
"99d3e09a-0ec3-4194-9dd4-9f3de6625cde": {
"alias": "unmeasured",
"body": {
"Oa": {
"Shared": {
"AnalysisCase": {
"base": "ModelBaseline",
"overrides": {
"63a931c2-675a-4fce-89e5-04361cb99450": "Unbound"
},
"subject": null
}
}
}
},
"description": "",
"id": "99d3e09a-0ec3-4194-9dd4-9f3de6625cde",
"name": "Unmeasured radio",
"parent": null
},
"bac0de99-9d2f-4e53-9158-d424ecac61bf": {
"alias": "budget",
"body": {
"Oa": {
"Shared": {
"AnalysisCheck": {
"documented_constraint": null,
"predicate": {
"Expression": {
"Binary": {
"left": {
"Property": "bd5b68e8-65fc-4e35-9071-a73061316a4b"
},
"op": "LessOrEqual",
"right": {
"Property": "437bedbb-2cf4-44ee-a139-6563cc5086ec"
}
}
}
},
"subject": null
}
}
}
},
"description": "",
"id": "bac0de99-9d2f-4e53-9158-d424ecac61bf",
"name": "Response budget",
"parent": null
},
"bd5b68e8-65fc-4e35-9071-a73061316a4b": {
"alias": "total_time",
"body": {
"Oa": {
"Shared": {
"EngineeringProperty": {
"base": {
"Expression": {
"Call": {
"arguments": {
"28ff40b0-1b8f-4735-8536-5a6ab21b56cb": {
"Property": "63a931c2-675a-4fce-89e5-04361cb99450"
},
"7a47cc1e-8ae5-4551-b2d8-8a68108e0976": {
"Property": "c33fc772-2a60-4044-9af9-73c61c563ace"
}
},
"definition": {
"Local": "fd7ce7ab-f789-40bc-b699-0c37f1b6902f"
}
}
}
},
"display_unit": {
"Local": "534f4672-1ba5-4157-8b0a-b9cf41d0a3ce"
},
"subject": {
"Element": "cabec6a1-7d5d-48f3-8d2d-a7fc8cf78f59"
},
"value_type": {
"Scalar": {
"Quantity": {
"Local": "188b911c-5f8b-431c-9eea-2d41f3953767"
}
}
}
}
}
}
},
"description": "",
"id": "bd5b68e8-65fc-4e35-9071-a73061316a4b",
"name": "Total time",
"parent": null
},
"c33fc772-2a60-4044-9af9-73c61c563ace": {
"alias": "dispatch_time",
"body": {
"Oa": {
"Shared": {
"EngineeringProperty": {
"base": {
"Expression": {
"Quantity": {
"magnitude": {
"denominator": "1",
"numerator": "1"
},
"unit": {
"Local": "534f4672-1ba5-4157-8b0a-b9cf41d0a3ce"
}
}
}
},
"display_unit": {
"Local": "534f4672-1ba5-4157-8b0a-b9cf41d0a3ce"
},
"subject": {
"Element": "6df991ba-4c5e-4535-99e2-524ae274ee8b"
},
"value_type": {
"Scalar": {
"Quantity": {
"Local": "188b911c-5f8b-431c-9eea-2d41f3953767"
}
}
}
}
}
}
},
"description": "",
"id": "c33fc772-2a60-4044-9af9-73c61c563ace",
"name": "Dispatch time",
"parent": null
},
"cabec6a1-7d5d-48f3-8d2d-a7fc8cf78f59": {
"alias": "respond",
"body": {
"Oa": "Activity"
},
"description": "",
"id": "cabec6a1-7d5d-48f3-8d2d-a7fc8cf78f59",
"name": "Respond",
"parent": null
}
},
"id": "4210b9f2-7046-47f0-aa3b-153f626a579d",
"imports": {},
"library_instances": {},
"name": "Response budget",
"relations": {},
"schema_version": 8,
"traces": {}
},
"packages": {},
"source": {
"entry": "active.arcadia",
"files": {
"active.arcadia": "arcadia 8;\nproject response_budget \"Response budget\" id \"4210b9f2-7046-47f0-aa3b-153f626a579d\" {\n description \"A complete proposed analysis model. Schema 8 and the new declarations are not implemented source7 syntax.\";\n\n quantity_kind duration \"Duration\" id \"188b911c-5f8b-431c-9eea-2d41f3953767\" {\n dimension { time: 1; }\n algebra Linear;\n }\n unit second \"Second\" id \"534f4672-1ba5-4157-8b0a-b9cf41d0a3ce\" {\n kind @duration; scale 1; offset 0; symbol \"s\";\n }\n unit millisecond \"Millisecond\" id \"39053f37-fec9-4708-9f0a-3959d223f891\" {\n kind @duration; scale ratio(1, 1000); offset 0; symbol \"ms\";\n }\n calculation combine \"Combine durations\" id \"fd7ce7ab-f789-40bc-b699-0c37f1b6902f\" {\n input a id \"28ff40b0-1b8f-4735-8536-5a6ab21b56cb\" : Quantity(@duration);\n input b id \"7a47cc1e-8ae5-4551-b2d8-8a68108e0976\" : Quantity(@duration);\n result Quantity(@duration);\n body $a + $b;\n }\n\n oa_activity respond \"Respond\" id \"cabec6a1-7d5d-48f3-8d2d-a7fc8cf78f59\" {\n oa_activity receive \"Receive\" id \"4ab9a8a7-7552-488d-9862-e14b89e7b353\" {}\n oa_activity dispatch \"Dispatch\" id \"6df991ba-4c5e-4535-99e2-524ae274ee8b\" {}\n }\n oa_property receive_time \"Receive time\" id \"63a931c2-675a-4fce-89e5-04361cb99450\" {\n subject @receive; type Quantity(@duration); display_unit @millisecond;\n base 750 [@millisecond];\n }\n oa_property dispatch_time \"Dispatch time\" id \"c33fc772-2a60-4044-9af9-73c61c563ace\" {\n subject @dispatch; type Quantity(@duration); display_unit @second;\n base 1 [@second];\n }\n oa_property total_time \"Total time\" id \"bd5b68e8-65fc-4e35-9071-a73061316a4b\" {\n subject @respond; type Quantity(@duration); display_unit @second;\n // The formula is shared. Every read uses the requested evaluation context.\n base @combine(a: @receive_time, b: @dispatch_time);\n }\n oa_property limit \"Allowed duration\" id \"437bedbb-2cf4-44ee-a139-6563cc5086ec\" {\n subject @respond; type Quantity(@duration); display_unit @second;\n base 2 [@second];\n }\n oa_check budget \"Response budget\" id \"bac0de99-9d2f-4e53-9158-d424ecac61bf\" {\n subject none; documented_constraint none;\n predicate @total_time <= @limit;\n }\n oa_analysis degraded \"Degraded radio\" id \"38036c65-16d4-449c-837b-91eb141c1d93\" {\n subject none; base ModelBaseline;\n overrides { @receive_time = 1250 [@millisecond]; }\n }\n oa_analysis unmeasured \"Unmeasured radio\" id \"99d3e09a-0ec3-4194-9dd4-9f3de6625cde\" {\n subject none; base ModelBaseline;\n overrides { @receive_time = unbound; }\n }\n}\n"
}
}
}
@@ -0,0 +1,32 @@
[
{
"canonical_source": "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"expression": {
"Number": {
"denominator": "1",
"numerator": "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}
},
"name": "ten_to_300"
},
{
"canonical_source": "ratio(1, 522194440706576253345876355358312191289982124523691890192116741641976953985778728424413405967498779170445053357219631418993786719092896803631618043925682638972978488271854999170180795067191859157214035005927973113188159419698856372836167342172293308748403954352901852035642024370059304557233988891799014503343469488440893892973452815095130470299789726716411734651513348221529512507986199933857107770846917779942645743159118957217248367043905936319748237550094520674504208530837546834166925275516486044134775384991808184705966507606898412918594045916828375610659246423184062775112999150206172392431297837246097308511903252956622805412865917690043804311051417135098849101156584508839003337597742539960818209685142687562392007453579567729991395256699805775897135553415567045292136442139895777424891477161767258532611634530697452993846501061481697843891439474220308003706472837459911525285821188577408160690315522951458068463354171428220365223949985950890732881736611925133626529949897998045399734600887312408859224933727829625089164535236559716582775403784110923285873186648442456409760158728501220463308455437074192539205964902261490928669488824051563042951500651206733594863336608245755565801460390869016718045121902354170201577095168)",
"expression": {
"Number": {
"denominator": "522194440706576253345876355358312191289982124523691890192116741641976953985778728424413405967498779170445053357219631418993786719092896803631618043925682638972978488271854999170180795067191859157214035005927973113188159419698856372836167342172293308748403954352901852035642024370059304557233988891799014503343469488440893892973452815095130470299789726716411734651513348221529512507986199933857107770846917779942645743159118957217248367043905936319748237550094520674504208530837546834166925275516486044134775384991808184705966507606898412918594045916828375610659246423184062775112999150206172392431297837246097308511903252956622805412865917690043804311051417135098849101156584508839003337597742539960818209685142687562392007453579567729991395256699805775897135553415567045292136442139895777424891477161767258532611634530697452993846501061481697843891439474220308003706472837459911525285821188577408160690315522951458068463354171428220365223949985950890732881736611925133626529949897998045399734600887312408859224933727829625089164535236559716582775403784110923285873186648442456409760158728501220463308455437074192539205964902261490928669488824051563042951500651206733594863336608245755565801460390869016718045121902354170201577095168",
"numerator": "1"
}
},
"name": "one_over_two_to_4095"
},
{
"canonical_source": "ratio(-1044388881413152506691752710716624382579964249047383780384233483283953907971557456848826811934997558340890106714439262837987573438185793607263236087851365277945956976543709998340361590134383718314428070011855946226376318839397712745672334684344586617496807908705803704071284048740118609114467977783598029006686938976881787785946905630190260940599579453432823469303026696443059025015972399867714215541693835559885291486318237914434496734087811872639496475100189041349008417061675093668333850551032972088269550769983616369411933015213796825837188091833656751221318492846368125550225998300412344784862595674492194617023806505913245610825731835380087608622102834270197698202313169017678006675195485079921636419370285375124784014907159135459982790513399611551794271106831134090584272884279791554849782954323534517065223269061394905987693002122963395687782878948440616007412945674919823050571642377154816321380631045902916136926708342856440730447899971901781465763473223850267253059899795996090799469201774624817718449867455659250178329070473119433165550807568221846571746373296884912819520317457002440926616910874148385078411929804522981857338977648103126085903001302413467189726673216491511131602920781738033436090243804708340403154190335, 1)",
"expression": {
"Number": {
"denominator": "1",
"numerator": "-1044388881413152506691752710716624382579964249047383780384233483283953907971557456848826811934997558340890106714439262837987573438185793607263236087851365277945956976543709998340361590134383718314428070011855946226376318839397712745672334684344586617496807908705803704071284048740118609114467977783598029006686938976881787785946905630190260940599579453432823469303026696443059025015972399867714215541693835559885291486318237914434496734087811872639496475100189041349008417061675093668333850551032972088269550769983616369411933015213796825837188091833656751221318492846368125550225998300412344784862595674492194617023806505913245610825731835380087608622102834270197698202313169017678006675195485079921636419370285375124784014907159135459982790513399611551794271106831134090584272884279791554849782954323534517065223269061394905987693002122963395687782878948440616007412945674919823050571642377154816321380631045902916136926708342856440730447899971901781465763473223850267253059899795996090799469201774624817718449867455659250178329070473119433165550807568221846571746373296884912819520317457002440926616910874148385078411929804522981857338977648103126085903001302413467189726673216491511131602920781738033436090243804708340403154190335"
}
},
"name": "negative_4096_bit_bound"
}
]
@@ -0,0 +1,840 @@
{
"source_commit": "7d14bd0dd1cf4a2293c00c185573fd4a7fc364ae",
"command": "cargo test --locked -p oa-core --lib analysis_numeric::tests -- --test-threads=1",
"exit_code": 0,
"passed": 26,
"failed": 0,
"ignored": 0,
"filtered_out": 42,
"test_seconds": 0.29,
"build_seconds": 385,
"scope": "Exact numeric foundation unit tests only; no schema8 model/evaluator or desktop acceptance.",
"toolchain": "Rust/Cargo1.98.1 x86_64-pc-windows-gnullvm; LLVM-MinGW20260908; jobs1",
"prior_attempt": "The broader command without --lib was intentionally stopped before tests while compiling unrelated integration executables. It supplies no pass result.",
"artifacts": {
"oa-core-tests.exe": {
"bytes": 24529920,
"sha256": "F92A4C4CF01DB0AD4810527F0D8AC2212196AEB3A4907E8EEA12FD5EDEADDB0D"
},
"libunwind.dll": {
"bytes": 190464,
"sha256": "AA282FA47BB2C4E210CEA5E470CDBDCA58568F0BE0BC73E404D86AB9CD95EF6A"
},
"numeric-unit.log": {
"bytes": 2754,
"sha256": "DF7F71D037CF2B3D38234C834F9F399F3BB30CC4757F453D72833DE9A9C3C063"
}
},
"input_files": [
{
"path": "Cargo.lock",
"bytes": 119973,
"sha256": "065C23C94908FE499D32ED91D55A70946ED5B08ECFDA3B483EC1E635572257BF"
},
{
"path": "Cargo.toml",
"bytes": 400,
"sha256": "369F5B4B2B60705A94623C8C0B68F8E0B048F263280CE07AE0629AD353797D62"
},
{
"path": "crates/oa-core/Cargo.toml",
"bytes": 408,
"sha256": "6B5C062474EBB62D5082E07DABED99EE85E4A833EDF26690FB4F887028D3416F"
},
{
"path": "crates/oa-core/build.rs",
"bytes": 45,
"sha256": "AB1BE6AE1FD8BF3EBAEEF6348347A9D5E666BDE693126F199E041659E0B094A9"
},
{
"path": "crates/oa-core/examples/generate_demo.rs",
"bytes": 795,
"sha256": "E26F432C95331AECA50985DBB3AD2C558AFFD43023B585811F0F7BFAA59F6661"
},
{
"path": "crates/oa-core/examples/generate_release1.rs",
"bytes": 1798,
"sha256": "1D5F11165D74893E4BC18DBF3A2F2111F4026BD89FD06EE7F0BAE69E45C4D4B5"
},
{
"path": "crates/oa-core/examples/release_dense_views.rs",
"bytes": 14664,
"sha256": "A83158488EF9B7394F136C52B1496719BE935C4E5BDFE457CAB5C9894F5F9412"
},
{
"path": "crates/oa-core/examples/routing_workshop.rs",
"bytes": 17248,
"sha256": "571307B18B6D4FA04700C5A4D4B0C2154C419E93A21292B2028E7B6F46A2FAA7"
},
{
"path": "crates/oa-core/src/analysis_numeric.rs",
"bytes": 3735,
"sha256": "6C88EEF3119162AEB6C07B9E75812C3AF9FBFE75554B0503AD00F103CB478408"
},
{
"path": "crates/oa-core/src/analysis_numeric/dimension.rs",
"bytes": 3045,
"sha256": "D4AD8E5307A1620E31791D724A0E31FDBA860D00DC83E3E1F2209D9C5E22A340"
},
{
"path": "crates/oa-core/src/analysis_numeric/exact.rs",
"bytes": 17338,
"sha256": "4EB8CB0F66B10E5DA204899AF82EE26ADFEC74BAB47B1A6C9842B5488EE04C4D"
},
{
"path": "crates/oa-core/src/analysis_numeric/fixtures/r7-literal-boundaries.json",
"bytes": 6085,
"sha256": "3B959DEC5E100A6F03817F5314BEF497347BA579EBD235476516D4BD83399181"
},
{
"path": "crates/oa-core/src/analysis_numeric/meter.rs",
"bytes": 7129,
"sha256": "1F7174BEB992935B4C399ACDEF91114AF73FAF041070FEAB968455AC827E8EE4"
},
{
"path": "crates/oa-core/src/analysis_numeric/quantity.rs",
"bytes": 12257,
"sha256": "EAD5142D6EEEA8F99399D57E02D8F7D819980923DE76387CD454694D6A747AEE"
},
{
"path": "crates/oa-core/src/analysis_numeric/tests.rs",
"bytes": 27486,
"sha256": "323CE82FC057D36F44463569FD8EA324C7D700FDDFBF32BCE15E04BD2A66970B"
},
{
"path": "crates/oa-core/src/data.rs",
"bytes": 42696,
"sha256": "7F9C7C7DD8353CB5A38597B92CDE6D3E890203D392473187EB387D41FF37414A"
},
{
"path": "crates/oa-core/src/demo.rs",
"bytes": 20657,
"sha256": "AD62123F96C20FF023CAFC1A9849B0FA06BC62EDD1FB9701799E4599B458600C"
},
{
"path": "crates/oa-core/src/language.rs",
"bytes": 99938,
"sha256": "638F08A42E9CF4996B5D07F69B4D6F517A518369F3604D25BE1006E40E080765"
},
{
"path": "crates/oa-core/src/language/semantic_source.rs",
"bytes": 7326,
"sha256": "584B5BB59D87A7B8D44D5DD2D124C3A1323B21492B138A17BC9526C0087C9E29"
},
{
"path": "crates/oa-core/src/language_nesting_tests.rs",
"bytes": 20639,
"sha256": "BD061611E8CB6F28AB203C73E544AC98BF275EB464CD314AD5FBD25F773DF33B"
},
{
"path": "crates/oa-core/src/lib.rs",
"bytes": 549,
"sha256": "EA1D9BF5ACBA8381BA204DB12FF5B5FE1F320417D17AF517E46B90FD6BCB241C"
},
{
"path": "crates/oa-core/src/migration_v2/README.md",
"bytes": 1515,
"sha256": "FB61D3FAA33D7BDFBA73415415A0882C0FBD3E0B45DF42F972A19AEC6DA927B9"
},
{
"path": "crates/oa-core/src/migration_v2/adapter.rs",
"bytes": 9490,
"sha256": "66741B4C8354A23B8A932ECC5BF1BC3BB6E74279558757B8C35BC2157A7E51B5"
},
{
"path": "crates/oa-core/src/migration_v2/mod.rs",
"bytes": 1158,
"sha256": "857D8BB13B6814DABA6780CA770C16352943C01CB130245D055F11B3B957B3E9"
},
{
"path": "crates/oa-core/src/migration_v2/model.rs",
"bytes": 52059,
"sha256": "8AC63EB59B2E57BDECFF2477EE0634096090924D3DD1851E1C454880A889DACB"
},
{
"path": "crates/oa-core/src/migration_v2/references.rs",
"bytes": 13369,
"sha256": "33A6819ABC83139FDAEBEEBD1B553238A328637106CCE7DD5A535F1791D43E20"
},
{
"path": "crates/oa-core/src/migration_v2/source.rs",
"bytes": 21946,
"sha256": "74FEE9B24E7C912182C998AB9D9AEEC125E2009C3214959229E844C67AFE2BF6"
},
{
"path": "crates/oa-core/src/migration_v2/state_validation.rs",
"bytes": 15769,
"sha256": "E29EF071BAB0A68460DDF55C46131C6F66C022D52C3C602C37C013C3649DA1B0"
},
{
"path": "crates/oa-core/src/migration_v2/transitions.rs",
"bytes": 704,
"sha256": "F74174C532BA56B57C80B719A11FB4ADEA0A78CCC2828FFF72FE2B9EDA5A10B1"
},
{
"path": "crates/oa-core/src/migration_v2/validation.rs",
"bytes": 98736,
"sha256": "6D5E4A4E2805AE5045BF789A0485E14A61C63346587BACE2B4A91173A688F780"
},
{
"path": "crates/oa-core/src/migration_v3/README.md",
"bytes": 2128,
"sha256": "3755A63F840D7F0EADF68AFA7A9D587877A9468FF982D3610539E8A0BB067A82"
},
{
"path": "crates/oa-core/src/migration_v3/mod.rs",
"bytes": 23023,
"sha256": "F4EDB6A0B50ECFE8670973776D703047B064420CB1F05913129C00FC656B3722"
},
{
"path": "crates/oa-core/src/migration_v4.rs",
"bytes": 4020,
"sha256": "50C652964F0F309E7F3F54B6EF12C40D42FCECC0490F005995C713DF1179AF70"
},
{
"path": "crates/oa-core/src/model.rs",
"bytes": 66352,
"sha256": "4585C895C64C4A183941C92CF047A9E52FF0CBE41971E16E78D229EA9DC101FB"
},
{
"path": "crates/oa-core/src/operations.rs",
"bytes": 11405,
"sha256": "A5FFBBF54CD440316A106D1188BFB699B57DAE59F466EDCB33618C333CD19F41"
},
{
"path": "crates/oa-core/src/persistence.rs",
"bytes": 31774,
"sha256": "1C01474F906A63E552C0D590B272C95821C00A100B3E79E58E67DBBEAC341A14"
},
{
"path": "crates/oa-core/src/ports.rs",
"bytes": 45413,
"sha256": "5CAC12EEA70A54EBEBBD6BE77B63CE92E3195325C4BC7D27705C518686CBAC80"
},
{
"path": "crates/oa-core/src/projection.rs",
"bytes": 21374,
"sha256": "69ED12BACE2CC5024E151752A174AB14034AC2D05EB741FCF13E9A27521C059B"
},
{
"path": "crates/oa-core/src/projection/port_layout.rs",
"bytes": 13474,
"sha256": "A08D17D10A58BB8B206DD001B19B91D5AA3A9F743B298A4DA57D503223C20079"
},
{
"path": "crates/oa-core/src/projection/state.rs",
"bytes": 5233,
"sha256": "73A607BAC713EC7FF49A509FEFB1A53C7686DCA93EE241FFC835564D743AFB28"
},
{
"path": "crates/oa-core/src/references.rs",
"bytes": 21957,
"sha256": "93553C234F32DA35F1F41A7B83AD81C471C9803EFA78CA222CF7DE3C2F86C0BB"
},
{
"path": "crates/oa-core/src/routing.rs",
"bytes": 50309,
"sha256": "C5EBB350BA76F731F5C48D968BFBBA4B6EF68EC046EB5893D116C4637EF15080"
},
{
"path": "crates/oa-core/src/semantic.rs",
"bytes": 540,
"sha256": "F9895DD5B8FB27D10D4CFBB41C3542EF2018365A589B8D5778E5674AE5A1F239"
},
{
"path": "crates/oa-core/src/semantic/catalog.rs",
"bytes": 49407,
"sha256": "1FF74FCCA18084D178681D7FF38EE1CCAC632C27C4DE0595BA1CEAB8067AB3A4"
},
{
"path": "crates/oa-core/src/semantic/historical.rs",
"bytes": 15271,
"sha256": "EAF3284BF234312698514CDB636BC8348E1B5D746CB16B18DF777E77D312ABFB"
},
{
"path": "crates/oa-core/src/semantic/linked.rs",
"bytes": 6035,
"sha256": "6F335B14E809AECFC35DEFCAF77CB821B080D1506DBC46297912B25AF98C07A2"
},
{
"path": "crates/oa-core/src/semantic/mutate.rs",
"bytes": 22683,
"sha256": "DC2B4C4A6EB156FE468EAA8C85FD37A505C0CD8EA1F226CB69CD01689AFD3D9D"
},
{
"path": "crates/oa-core/src/semantic/review.rs",
"bytes": 51116,
"sha256": "0C157D2C327E72E89CCA3B423E71EE73798105C5BB7271439C1A101642C91E3E"
},
{
"path": "crates/oa-core/src/semantic/types.rs",
"bytes": 10405,
"sha256": "6922FE35089F12FAD43AB0CE1C6886D01A3855F90B8E2543E99802AC5E6F1BBB"
},
{
"path": "crates/oa-core/src/session.rs",
"bytes": 35237,
"sha256": "1EB77B329B2986FC6F5DC0171E77FD860375CDFD8C769A1367B445D6C49EBE00"
},
{
"path": "crates/oa-core/src/source_files.rs",
"bytes": 74803,
"sha256": "669491DBDDB8E8E6556161426DFFA1A49AC1C826D31020BFF0956D81A4D84683"
},
{
"path": "crates/oa-core/src/source_project.rs",
"bytes": 61843,
"sha256": "1394370BCC5908873B830C2C11312E6E1611AF41F46C4F43CA0A6BCB713BB466"
},
{
"path": "crates/oa-core/src/source_project/source_migration.rs",
"bytes": 4700,
"sha256": "ADD463AE85C420F853D324B00A221F0F874164CC3F3ED6BBB07557A9ED7A8C95"
},
{
"path": "crates/oa-core/src/source_project/source_structure.rs",
"bytes": 20225,
"sha256": "9F1F83315856A7025FE662C0FBB13452F31613224A03663150D6225D1DDC162F"
},
{
"path": "crates/oa-core/src/state_validation.rs",
"bytes": 15665,
"sha256": "E28D7F74385E2A78763716B34491BD73A00BFECCC085970CEB130627E3FAEE18"
},
{
"path": "crates/oa-core/src/topology_context.rs",
"bytes": 13734,
"sha256": "16DE69B297DE95D93C73808621607833B713AF811DD082F0169186AE1E5C3E2D"
},
{
"path": "crates/oa-core/src/topology_context/analysis.rs",
"bytes": 20036,
"sha256": "DEAD6194F0AA60CC46045C0E5EE10DBDBB689158C38634DFBBBC45D2BA88AC65"
},
{
"path": "crates/oa-core/src/topology_context/analysis_tests.rs",
"bytes": 5258,
"sha256": "17839263410815E96FE027D4EBA3AAE0A9AB3B82B16947E2B29EBE0D09782B56"
},
{
"path": "crates/oa-core/src/transitions.rs",
"bytes": 51998,
"sha256": "FADD1B07B89CAE4117B91ABB4F1AC9EA81FF818EF20F2175E76EE8CF21FB708D"
},
{
"path": "crates/oa-core/src/transitions/communication.rs",
"bytes": 8634,
"sha256": "2B476630A88EDD9021B9156CA12A5402E01604095F0D4002AFB452671903A028"
},
{
"path": "crates/oa-core/src/validation.rs",
"bytes": 112792,
"sha256": "7C1B4AE5E4EBBA13E99AF9158CCDE138DFC5E9A7AFB580E76FBA72D0B64BA74E"
},
{
"path": "crates/oa-core/src/validation/coverage.rs",
"bytes": 6478,
"sha256": "B08E4B344ECAD055714168C83BEFF7FE29BA4EDC53EDBFFA2EAE04E5580A4D5F"
},
{
"path": "crates/oa-core/src/view_options.rs",
"bytes": 24481,
"sha256": "421EDE0A04C593CA1DF0E6E0D06FABCFCC8113CDBCFB0ADC7F5C8EF4D1724208"
},
{
"path": "crates/oa-core/src/view_options_tests.rs",
"bytes": 21433,
"sha256": "64AAC44DE2DF42A1012239A3DD75C473DAF0C8990B3170B7BF7E78A556080208"
},
{
"path": "crates/oa-core/tests/communication_boundary_scenarios.rs",
"bytes": 8816,
"sha256": "E1100B8F363910A487D90C73ACC7DF543DC6D532158C3C63AD11CD3D40D8E12B"
},
{
"path": "crates/oa-core/tests/communication_means.rs",
"bytes": 18102,
"sha256": "A25F2749180BC6F4BEB1445BFAD3351EEDF0669131A7439B633AB804439E95A4"
},
{
"path": "crates/oa-core/tests/coverage_diagnostics.rs",
"bytes": 20527,
"sha256": "E2E9C3996DAAB47F2BD23F5D8C48B4E372AF33331152F5FC85B76F06D04FCDF4"
},
{
"path": "crates/oa-core/tests/fixtures/v2-coastal-rescue.arcadia",
"bytes": 29220,
"sha256": "F0DF129C1914BFC01D0B047B0CD4B520D8CF0C249EA9CED085DE400C0BE87FF3"
},
{
"path": "crates/oa-core/tests/fixtures/v2-coastal-rescue.json",
"bytes": 43990,
"sha256": "3ED53508EB544BBD788832F7BEBEA2EC31B3D66191F24F45098CC9750EC4C005"
},
{
"path": "crates/oa-core/tests/fixtures/v3-data-contract.arcadia",
"bytes": 2450,
"sha256": "305F19D1B79F99F8D4CF627BA2E9CA6836BB7959CC0070227815937A699EAFEA"
},
{
"path": "crates/oa-core/tests/fixtures/v3-data-contract.json",
"bytes": 8240,
"sha256": "1B41299C53204E6232397D25A58D5BCCDBBC16E770C75E8EDB55CE36CCFE58A2"
},
{
"path": "crates/oa-core/tests/fixtures/v4-reviewed-prose.arcadia",
"bytes": 1222,
"sha256": "5FA53A26A5AD010C8E59EB56D9B2B52003569E66C0377FFB58840590168AD0C1"
},
{
"path": "crates/oa-core/tests/fixtures/v4-reviewed-prose.json",
"bytes": 1734,
"sha256": "2808088AFCC165F4BD6DA5129151143411C997041C3DE61D600AB76E05D24A47"
},
{
"path": "crates/oa-core/tests/language.rs",
"bytes": 56482,
"sha256": "77F6CB2A69976BFB5F60B243B71C07FB0EDC7505002C1E9E73FCD6DDF6D9496E"
},
{
"path": "crates/oa-core/tests/port_anchors.rs",
"bytes": 26893,
"sha256": "EFBC0FA18733B1AAAC501F9785A4C392FC49C05C45A5778D26E9B5DD951CA6FA"
},
{
"path": "crates/oa-core/tests/port_migration.rs",
"bytes": 15620,
"sha256": "1801C376A5435AC7EFB2A8342348B2C7FFEF930BBA65DECA4ACB66B06E0483D7"
},
{
"path": "crates/oa-core/tests/port_source.rs",
"bytes": 4858,
"sha256": "8D2DDACA4045D330F6EDA2591561497B858AAD53769A823249D0BEA4F5916E12"
},
{
"path": "crates/oa-core/tests/prepared_commands.rs",
"bytes": 9555,
"sha256": "52CAB1C668064777412DF0A6A3C20D74D579B33794B4B00525B9F151C06EAC9B"
},
{
"path": "crates/oa-core/tests/process_diagnostics.rs",
"bytes": 1104,
"sha256": "1DBBC590239EF7542BE4A2BDC9543AAA64D108417B03DD312A0894E364745906"
},
{
"path": "crates/oa-core/tests/r3_constructor_ack.rs",
"bytes": 24163,
"sha256": "E5C44676EA0AAB97F2E091DDAC2F4BC4A4A90F5877F7BBF08B61229600944DD0"
},
{
"path": "crates/oa-core/tests/r3_migration.rs",
"bytes": 6590,
"sha256": "EBD04120F5215B3956FDC79EE78F6E38734C7D13392F1FC28AABA957912265BD"
},
{
"path": "crates/oa-core/tests/r3_semantic.rs",
"bytes": 77473,
"sha256": "49C3763EAF2131070A7C46CC23A84F917D292D41610B6404510E5607F8890C32"
},
{
"path": "crates/oa-core/tests/r3_source.rs",
"bytes": 15311,
"sha256": "F31F60659DF3C7EF601650D1CFF140589D8729F01A3F65A4A6A6C2272F15C408"
},
{
"path": "crates/oa-core/tests/r6_data.rs",
"bytes": 41437,
"sha256": "5F7FFB3BC73C3C85423623FE56F8680646DC97040327BDBEAD9D47A37E80F155"
},
{
"path": "crates/oa-core/tests/r6_migration.rs",
"bytes": 9174,
"sha256": "81519BC1512F5E069432E9A1CDAD04C10216D492DF01B4B75B02102480EBD6A1"
},
{
"path": "crates/oa-core/tests/r6_source.rs",
"bytes": 9519,
"sha256": "88A0AA087A7678E2898FF6057538072CA92072190712F05FFF08372CC56EBED6"
},
{
"path": "crates/oa-core/tests/release1_teaching.rs",
"bytes": 10010,
"sha256": "BE179290D871125DC97CFD2A3067C7AC89C8A538FDB0AFAFC5B825BCDFB2BEF2"
},
{
"path": "crates/oa-core/tests/release_capabilities.rs",
"bytes": 19422,
"sha256": "B92CBA2A562E78287DDD7DC3E94557848D17FAA0C8A102601B50286B9AF48A30"
},
{
"path": "crates/oa-core/tests/release_diagrams.rs",
"bytes": 7689,
"sha256": "CEFCC0582D91266D5DE6EE5DAEFCFE41B606AB971AB235B59034A8E1DBC24B7D"
},
{
"path": "crates/oa-core/tests/release_language_limits.rs",
"bytes": 5204,
"sha256": "8578DA4F76BBD2D0E1BFE295AB1259E2162A3FE3D0D2BA8901915D7A40942806"
},
{
"path": "crates/oa-core/tests/release_ports.rs",
"bytes": 32740,
"sha256": "506E62BEAB0C7EC0CECE83E9E0C1A810FA1F99285C9E1CAB0C77F06DE55A40E1"
},
{
"path": "crates/oa-core/tests/release_routing.rs",
"bytes": 23165,
"sha256": "AE168FAABA23769B7CA1FD65F217B9DB671726E67276EB5A04E5E72B1447A462"
},
{
"path": "crates/oa-core/tests/release_scenarios.rs",
"bytes": 41067,
"sha256": "0F5D876BADEEC653FC7B12610A77D867E9A53582E605253DEFF8D379DA21572F"
},
{
"path": "crates/oa-core/tests/release_states.rs",
"bytes": 26873,
"sha256": "B1A26ACEE95EABA7F9920F9DA738A1E33606552EB078E6D298895DAFF7E3D811"
},
{
"path": "crates/oa-core/tests/release_topology.rs",
"bytes": 17643,
"sha256": "C51DBE5AA5A038F9ED830DAA02AA67714472179EB7FD6DA5CE36DC6931C8B3A9"
},
{
"path": "crates/oa-core/tests/review_regressions.rs",
"bytes": 21823,
"sha256": "F154E5C88D35E31512E2B07733A1327EA3E2FCFD1EF8EB23066A4E9573F387C1"
},
{
"path": "crates/oa-core/tests/route_source.rs",
"bytes": 7270,
"sha256": "1A73FDBAC0D0AE0E84D8D67B6AD398E11223958F5DCA007F73757C91827DECFB"
},
{
"path": "crates/oa-core/tests/routing_geometry.rs",
"bytes": 31890,
"sha256": "2D8EEA4A2BBA535ED0EA6DAABCCDAF87B375828C404BAA10518C7BD7FD6253D3"
},
{
"path": "crates/oa-core/tests/routing_labels.rs",
"bytes": 5530,
"sha256": "DFF884D799C3C5C9257210248C55767E294E3CB317C605CBD0DDC9600C216EB6"
},
{
"path": "crates/oa-core/tests/semantic_artifact_absence_oracle.rs",
"bytes": 4640,
"sha256": "D93EAF9667F4221232168BEF0FE9F069E5BE1B1F23410BDA5DD67010B96E5C8A"
},
{
"path": "crates/oa-core/tests/semantic_attachments_gui_oracle.rs",
"bytes": 14957,
"sha256": "7F526D363BA05E7AC89FAF0FEAD0796E419C34EE204EDF44A1B46C78308B0970"
},
{
"path": "crates/oa-core/tests/source6_view_contract.rs",
"bytes": 14307,
"sha256": "FA566DE53946D40BBBD9033EDFEED177180C4DC44AFA2F2F4667FFDC622EB3BB"
},
{
"path": "crates/oa-core/tests/source7_contract.rs",
"bytes": 43262,
"sha256": "CAD4F1A135DDBD947BDF3C71D1B44F1E438725095EA8C4D893464BC721C2363F"
},
{
"path": "crates/oa-core/tests/source_files.rs",
"bytes": 13168,
"sha256": "F0429A0B89A443458EA9AF7083C1E4E7411417A7E6B47E618BFBB9266DC164F9"
},
{
"path": "crates/oa-core/tests/source_identity_updates.rs",
"bytes": 5139,
"sha256": "F52AA38B4D317E532C641AD7463472E4B40E802E60052CF0017556FFC63DA06C"
},
{
"path": "crates/oa-core/tests/source_nested_updates.rs",
"bytes": 16892,
"sha256": "9AC3390936C0D250651EA94A1B8FAF0ADFB680F061ECECC56F2A52909B5BA855"
},
{
"path": "crates/oa-core/tests/source_project.rs",
"bytes": 45910,
"sha256": "CEF58ADB16AA4D2527DDC0628AEC3E26F0B42B4BE70014A7E360FAFDAC13A942"
},
{
"path": "crates/oa-core/tests/state_diagrams.rs",
"bytes": 23973,
"sha256": "6B02B387A3AE559CB3AD72DE24292A5ABCB76AAAC0ADBAB5D2AA5211B22CD2E5"
},
{
"path": "crates/oa-core/tests/workflows.rs",
"bytes": 33426,
"sha256": "BD843DFDDBD5724EC08F657571F887496203056FE2FA5864EF50A168A613B89B"
},
{
"path": "docs/language.rst",
"bytes": 130753,
"sha256": "AC6B1D982C55F246DE2F3C844B24B874599A3C20CFBD7ED22EFE4561AC622B83"
},
{
"path": "examples/coastal-rescue.arcadia",
"bytes": 33107,
"sha256": "7E183EF9FD8154D3074303F1956CD0D9871C19E3FFC630D333B0BFFBD6E30E17"
},
{
"path": "examples/coastal-rescue.json",
"bytes": 49715,
"sha256": "242F0B031229A87AF5C4EBE5D651DEFE59C191A6B7A69DC0A77F7E203411F195"
},
{
"path": "examples/legacy-v1-coastal-rescue.arcadia",
"bytes": 3449,
"sha256": "4DBBBDF8BC7E6CB8E0A81DD678069DAE585A9E5187C59BC3665E1FDAB3F1A86A"
},
{
"path": "examples/legacy-v1-coastal-rescue.oa.json",
"bytes": 5976,
"sha256": "FD0E9DEDFB7ADB922DC0575CC2B820D40344D639F1D8052BD70363AA59DE9C24"
},
{
"path": "examples/release1/README.md",
"bytes": 11137,
"sha256": "113CFB1CF38A98B51BFF60442D86A35D636A3D80EF206BECE7D53D0AAE67F2D5"
},
{
"path": "examples/release1/evidence/repair-check.md",
"bytes": 1419,
"sha256": "F4DB17BDCA4ECBB1BF0CAEF01B2DA5F1F7F798AB3152B648CE6DD8696A6A68E5"
},
{
"path": "examples/release1/expected-diagnostics.json",
"bytes": 288,
"sha256": "3700393D76B940CFC076BF52861BD7B1F43370F8EABCE3594531BD0BF1FC851E"
},
{
"path": "examples/release1/model.json",
"bytes": 134520,
"sha256": "EF0EF4006C3E09D5DB6E1B621256B3F7D6642F3A7B09CC34607CFBCCA830C727"
},
{
"path": "examples/release1/model.rs",
"bytes": 28762,
"sha256": "15F30F6BDE655808FE754665C26D560E36F2A9BAD340F346B09137D0D05045F5"
},
{
"path": "examples/release1/oa/activities/activity_oa_restore_work.arcadia",
"bytes": 1307,
"sha256": "83E73D0A013D5C3E1A4AF0D9139AB0CFA362D25A846671FC2DAC59181061A9D4"
},
{
"path": "examples/release1/oa/behavior/process_oa_assessment_procedure.arcadia",
"bytes": 697,
"sha256": "172C32C044EF4B5BC58A1F4635CC81F48DABB4014479B47DCE7B25FEDF2DBD0D"
},
{
"path": "examples/release1/oa/behavior/process_oa_field_procedure.arcadia",
"bytes": 1781,
"sha256": "D2552DC5383E2E91CB302A790364E197F663C2EDC013A2AB1978789825C9C324"
},
{
"path": "examples/release1/oa/behavior/process_oa_response.arcadia",
"bytes": 3225,
"sha256": "CF86F46FA5C5D70592EF43F1BC1CB4D968EFD8ADD384BA88036BEF6B88AA4ABC"
},
{
"path": "examples/release1/oa/behavior/scenario_oa_conversation.arcadia",
"bytes": 1795,
"sha256": "B25BE524E98A35EBAC5E4481B7609C027B32BAF9605E8097F807560F94A77CA6"
},
{
"path": "examples/release1/oa/behavior/scenario_oa_full_scenario.arcadia",
"bytes": 1261,
"sha256": "00A1EDF2C9707A0EB62AC070C2D02467874F2C9C1F273CC6DDB53D6D65999A1C"
},
{
"path": "examples/release1/oa/behavior/state_machine_oa_monitor.arcadia",
"bytes": 2022,
"sha256": "447C9AD1359F4A31ECF975AD07C0B08D8E7E03191FB0858E2FC56F377B223CD5"
},
{
"path": "examples/release1/oa/capabilities.arcadia",
"bytes": 781,
"sha256": "FC8012E5554350B12E1CB80080F8FEAB80D933C9B33C0F2A7BB500CB7D609936"
},
{
"path": "examples/release1/oa/model.arcadia",
"bytes": 661,
"sha256": "F7CF914766EA73EC53B85E32D117EA9A9DBA52D3E038246863541AE7069B1A15"
},
{
"path": "examples/release1/oa/participants.arcadia",
"bytes": 1834,
"sha256": "951764C14179F27C10E315ACF2EA37D1FFF54C77B8441E23ADDD239E8C715CE3"
},
{
"path": "examples/release1/oa/types.arcadia",
"bytes": 788,
"sha256": "9D0396FB82DD5733EDD8D1CF52F968CA84906927C704F9CF0944A7FAE215CF49"
},
{
"path": "examples/release1/project.arcadia",
"bytes": 1958,
"sha256": "B2674E6BF77CFE0F6F0BA79C81D2E96D92F39C02CA2C6266D80E91723BFB9FDA"
},
{
"path": "examples/release1/sa/behavior/process_sa_assessment_procedure.arcadia",
"bytes": 706,
"sha256": "73928CED61C06CBC08728D7956676A50FD4C05AA5B7F1FF4F29CE27A0B0F2088"
},
{
"path": "examples/release1/sa/behavior/process_sa_field_procedure.arcadia",
"bytes": 1790,
"sha256": "886D462AF83110CBFA55DD232A667EA1446761234471357CBB878794E98E8768"
},
{
"path": "examples/release1/sa/behavior/process_sa_response.arcadia",
"bytes": 3234,
"sha256": "041785FC22842423822431BD5E6E8D16094EBD0716965E7C765FB8C1D7549E8A"
},
{
"path": "examples/release1/sa/behavior/scenario_sa_conversation.arcadia",
"bytes": 2127,
"sha256": "97276F5757EE9AA12A0DBD9D3CCD58174B3091162BB8BCCE357CEC20A45F5D85"
},
{
"path": "examples/release1/sa/behavior/scenario_sa_full_scenario.arcadia",
"bytes": 1256,
"sha256": "E12FC406B99D60A2F7768AAA0B25BA560E8605F0B8E37F3A7CDD3B8443562B74"
},
{
"path": "examples/release1/sa/behavior/state_machine_sa_monitor.arcadia",
"bytes": 2022,
"sha256": "05CE29AF99424C491C47FE2450DC6F5F8CE995511C01EC7E17BB5855BE3C9736"
},
{
"path": "examples/release1/sa/capabilities.arcadia",
"bytes": 781,
"sha256": "B6912FB3ECE439912345FA71724ACD462E716A1E5A88203A1058A5C3B2C20A4F"
},
{
"path": "examples/release1/sa/functions/function_sa_audit_thresholds.arcadia",
"bytes": 297,
"sha256": "E5A04325D52183956DCA814DD90F4E56C96C6B7DB0F4F202F203ECB49C3E0F4F"
},
{
"path": "examples/release1/sa/functions/function_sa_restore_work.arcadia",
"bytes": 2576,
"sha256": "FDDEB67308051325041FEE641FE758DDF43345B7C47B66D433616AD07FDCCDD6"
},
{
"path": "examples/release1/sa/model.arcadia",
"bytes": 661,
"sha256": "D431509251ACB859FBB98733B6D18ADD952495E10A78703FBE8F31E40BC083C4"
},
{
"path": "examples/release1/sa/participants.arcadia",
"bytes": 4012,
"sha256": "780D2EB08B217B67B55FEBB3D34846F727B8EE9018DF6C5741748647E36B2FF8"
},
{
"path": "examples/release1/sa/types.arcadia",
"bytes": 1227,
"sha256": "101FC0FBD99F1101514AA7B6DA4F8E535AAE0BB94C9E2B9E7E6C151135B28660"
},
{
"path": "examples/release1/traceability.arcadia",
"bytes": 29506,
"sha256": "48A96F543456C9B5A5798FC795594E4119147FE45F11A6F109F141B7F514560F"
},
{
"path": "examples/release1/transition-before.json",
"bytes": 35801,
"sha256": "2253E6434F741687E22327C37D520ED15C043BC36025498650BC2366BB6A874D"
},
{
"path": "examples/release1/transition-request.json",
"bytes": 4276,
"sha256": "43B9A22E5F95AD3BC0EBD2478EAD0E14406854ABC989AE94DED9367B013D812A"
},
{
"path": "examples/release1/views/diagram_oa_allocation_view.arcadia",
"bytes": 2176,
"sha256": "DFC0EB61E0E2AA5BDA45524571FBFA3494F44CE19696F3E15E07438074A07BC7"
},
{
"path": "examples/release1/views/diagram_sa_allocation_view.arcadia",
"bytes": 2173,
"sha256": "B78B43A26A82EFB8FBD5374B09C329FA341F57B29F635D397B9EF8AEDE71D389"
},
{
"path": "examples/release1/views/diagram_sa_capability_view.arcadia",
"bytes": 1620,
"sha256": "75E8DC9FB30F25D6F8FD3C6B595593AB4F98958A25798F961E336E0FAC557649"
},
{
"path": "examples/release1/views/diagram_sa_service_view.arcadia",
"bytes": 2209,
"sha256": "B08C84F3EA633E7C78DFDAD3272F75415F6256CF3614B6D99BFD4F0269076C9B"
},
{
"path": "examples/release1/views/diagram_sa_state_view.arcadia",
"bytes": 1931,
"sha256": "5F42F4C605E57B817D8400ED21F2B14BFC4E1592AEBE941FF113767504C2B869"
},
{
"path": "examples/routing-workshop.arcadia",
"bytes": 66431,
"sha256": "DC6B75DA72884D399132ED9E300FA5A26B73D802BB526C9AF986AE9EEB0A646A"
},
{
"path": "examples/routing-workshop.json",
"bytes": 100281,
"sha256": "8592714F75B271133F21CE017D87896B6CF91EA0E4837F1C2ED453E21BE26B14"
},
{
"path": "examples/source-project/oa.arcadia",
"bytes": 2142,
"sha256": "5F60642F211B2D352A951092FAEEB7503D4FB7F6F7FCA99E810347231101D31C"
},
{
"path": "examples/source-project/project.arcadia",
"bytes": 606,
"sha256": "787F6B67B179FFD221F7E5FAF1A4EDD74CFD116AA1E3B5E9ED9F22FBFF612D8D"
},
{
"path": "examples/source-project/sa/assurance.arcadia",
"bytes": 682,
"sha256": "AB76044836E4DCD42A82D316846EC87DA2108939AA2037640C5BCA1D2ECDEE0F"
},
{
"path": "examples/source-project/sa/participants.arcadia",
"bytes": 1231,
"sha256": "1ABAD579812E77877B45A3D8E3C3E9DA734BE2D04A0254CC730A5257EBD2DC88"
},
{
"path": "examples/source-project/sa/reporting.arcadia",
"bytes": 2839,
"sha256": "F0B7443F728912CA163DE39B6055876B533A77FEAA3219B2D40A2DD65712E058"
},
{
"path": "examples/source-project/views.arcadia",
"bytes": 3508,
"sha256": "E29AD3F406A6A50D2F0AE2CA2A53EE3B0C18222CD4098704A46D4F48B64EA17F"
},
{
"path": "examples/text-first/README.md",
"bytes": 1367,
"sha256": "4EC8250D5ED556F1D496E8C85137E973E802ECB11DF1A36488411E8CDC5A9F9E"
},
{
"path": "examples/text-first/project.arcadia",
"bytes": 480,
"sha256": "F441535C623E0CF20E1875EB2CD33B587ED00E078C37AF39CB36D89BB5EDC19A"
},
{
"path": "rust-toolchain.toml",
"bytes": 66,
"sha256": "A6A0BBD29FFAA8182DC22D1D9149709F1091E47DF40ED96EB8A78A711C66A4CE"
}
]
}
@@ -1,15 +0,0 @@
# Bounded a57 camera and minimum-window native checks
Root exercised source `a57c334b302eb13352b4bb65df1c75d94d002348`, unoptimized GUI SHA256 `69DBB90FA299B3B3D90140C7934891F79166025893AF9B35B2B8D754A37A0D40`, in a fresh isolated session on 2026-09-12. Launch and close bind PID 5004 and unchanged GUI/runtime hashes. This is the pre-grapheme a57 executable: no clusters, native combined-source, all-zoom, package or full-product acceptance follows.
The archiver independently viewed all 18 original JPEGs and checked the closed receipts; no new UI, oracle, compiler or application action was performed. Image 01 shows the wide source project opened. Images 02–06 show OA wide-W lifecycle at 100%, Create selected as `life_oa_110`, zoom 125%, Delete selected as `life_oa_112`, and Fit restoring 100%. Header text and Create text remain within the visible card/capsule or viewport; at 125% the headers are partly above the viewport, with no visible spill into the toolbar.
Images 07–08 document native resizing from 1442×932 to 1102×932 and then 1102×732 captured frame; root reports the final client as 1100×700. At 100% the narrow viewport cuts the right side of the receiver card/capsule at its boundary. Images 09–12 show Fit 66%, OA Create selection, content shifted 60 pixels left after root's Right pan, and Delete selected at its new position with alias `life_oa_112`. The left header is then partly outside the viewport and clipped there. Images 13–17 show the SA CJK receiver: switching layer restores 100%; Fit gives 66%; Create selects `life_sa_110`; content moves 60 pixels up after root's Down pan, leaving headers above the viewport; selecting the moved Delete shows `life_sa_112`. No visible header bleed into the toolbar is present. Exact keyboard inputs and camera values (-60,0)/(0,-60) are root's action account, while the images independently support the resulting displacement, zoom labels and selection identities.
At 66% labels are visibly small. These screenshots do not establish general readability, accessibility, focus/keyboard completeness, every clipping edge or every zoom. The visible 19 model checks are not zero diagnostics; the fixture intentionally contains sparse lifecycle cases. No model editing is inferred from selection or camera navigation.
Image 18 records **All source files saved**. The closed lifecycle `same` invocation compares the original `baseline-wide/project.arcadia` with `checkpoints/wide-passive/project.arcadia`, exiting 0 at 2026-09-12T11:02:28.993557Z with `PASS same: all identities, fields, reachable source and physical bytes`. A fresh archival all-file comparison also confirms both directories contain exactly the same two files, 20,214 bytes total. The checker source, historical compilation/self-check/admission and binary/input provenance are included; no fresh checker execution or compilation is claimed by this archive.
Root reports two temporary 1×16 captures during Windows sizing, recovered by fresh window selection and full capture before coordinate input. Those tiny captures are not among the 18 saved JPEGs and are not independently reconstructed here. Root then reports one Alt+F4 and fresh inventory without Studio. The waiting logger independently records exit 0 at 2026-09-12T11:02:56.0410831Z; closed stdout/stderr are empty. Launch's pending acceptance text is historical. This fresh successful bounded run is separate from the [earlier camera picker failure and close](../canvas-native-close-a57c334-20260911/README.md).
`manifest.json` binds exact original/copy bytes. `readback.json` independently rereads original payloads, archived payloads and final metadata inventory. Preparation and provenance may mention clusters, other oracles, ZIPs, working files or prior gates; these are historical references, not acceptance of those cases. Executables, DLLs, ZIPs, profiles and live working directories are excluded. Source-only archival/preparation/launch scripts are records, not instructions to rerun. Root owns integration and publication.
@@ -1,29 +0,0 @@
$ErrorActionPreference = 'Stop'
$originRoot = 'C:\Users\User\Documents\Git\ARCADIA-Studio\.local\native-a57c334-camera'
$destinationRoot = $PSScriptRoot
$paths = @('launch-script-syntax.json','launch.json','normal-close.json','preparation-manifest.json','prepare-inputs.py','second-readback.json','wide-passive-status.json')
foreach ($directory in @('baseline-wide','checkpoints/wide-passive','logs','provenance','screenshots')) {
$paths += Get-ChildItem -LiteralPath (Join-Path $originRoot $directory) -Recurse -File | ForEach-Object { $_.FullName.Substring($originRoot.Length + 1).Replace('\','/') }
}
$entries = @()
foreach ($relative in ($paths | Sort-Object)) {
$original = Join-Path $originRoot $relative
$copy = Join-Path $destinationRoot $relative
if (Test-Path -LiteralPath $copy) { throw "Refusing to overwrite $copy" }
$before = (Get-FileHash -LiteralPath $original -Algorithm SHA256).Hash
New-Item -ItemType Directory -Path (Split-Path $copy) -Force | Out-Null
Copy-Item -LiteralPath $original -Destination $copy
$after = (Get-FileHash -LiteralPath $copy -Algorithm SHA256).Hash
if ($before -ne $after) { throw "Copy mismatch $relative" }
$entries += [ordered]@{path=$relative;original=$original;bytes=(Get-Item -LiteralPath $copy).Length;original_sha256=$before;copy_sha256=$after}
}
$original = 'C:\Users\User\Documents\Git\ARCADIA-Studio\.local\launch-native-a57c334-camera.ps1'
$copy = Join-Path $destinationRoot 'launch-native-a57c334-camera.ps1'
if (Test-Path -LiteralPath $copy) { throw "Refusing to overwrite $copy" }
$before = (Get-FileHash -LiteralPath $original).Hash
Copy-Item -LiteralPath $original -Destination $copy
$after = (Get-FileHash -LiteralPath $copy).Hash
if ($before -ne $after) { throw 'Launch script mismatch' }
$entries += [ordered]@{path='launch-native-a57c334-camera.ps1';original=$original;bytes=(Get-Item -LiteralPath $copy).Length;original_sha256=$before;copy_sha256=$after}
$manifest = [ordered]@{created_utc=[DateTime]::UtcNow.ToString('o');scope='Closed a57 camera evidence only; copying does not execute gates or UI';payload_count=$entries.Count;payload_bytes=($entries|ForEach-Object { $_.bytes }|Measure-Object -Sum).Sum;files=$entries;excluded=@('bin','oracles executables/DLL','baseline-clusters','working-wide','working-clusters','profile','unrelated-cwd','ZIP archives')}
[IO.File]::WriteAllText((Join-Path $destinationRoot 'manifest.json'),($manifest|ConvertTo-Json -Depth 6)+"`n",[Text.UTF8Encoding]::new($false))
@@ -1,692 +0,0 @@
arcadia 7;
project lifecycle_native "Lifecycle native contract" id "5ceab400-0000-4000-8000-000000000001" {
include "rationale.arcadia";
description "Fixed semantic inputs; lifecycle geometry is derived and never authored.";
oa_activity life_oa_10 "OA Controller work" id "5ceab400-0000-4000-8000-0000000003f2" {
description "";
parent none;
}
oa_activity life_oa_11 "OA Receiver work" id "5ceab400-0000-4000-8000-0000000003f3" {
description "";
parent none;
}
oa_scenario life_oa_100 "OA 01 Root create action delete" id "5ceab400-0000-4000-8000-00000000044c" {
context none;
description "";
parent none;
postcondition "";
precondition "";
steps [@life_oa_110, @life_oa_111, @life_oa_112];
viewpoint OperationalActivity;
}
oa_lifeline life_oa_101 "OA Controller lane" id "5ceab400-0000-4000-8000-00000000044d" {
description "";
parent @life_oa_100;
represented @life_oa_10;
}
oa_lifeline life_oa_102 "WWWWWWWWWWWWWWWWWW" id "5ceab400-0000-4000-8000-00000000044e" {
description "";
parent @life_oa_100;
represented @life_oa_11;
}
oa_occurrence life_oa_110 "OA Create receiver" id "5ceab400-0000-4000-8000-000000000456" {
body {
Exchange {
exchange @life_oa_service;
message_kind Create;
receiver @life_oa_102;
reply_to none;
sender @life_oa_101;
values [];
};
};
description "";
parent @life_oa_100;
postcondition "";
precondition "";
}
oa_occurrence life_oa_111 "OA Receiver action" id "5ceab400-0000-4000-8000-000000000457" {
body {
Action {
action @life_oa_11;
lifeline @life_oa_102;
};
};
description "";
parent @life_oa_100;
postcondition "";
precondition "";
}
oa_occurrence life_oa_112 "OA Delete receiver" id "5ceab400-0000-4000-8000-000000000458" {
body {
Exchange {
exchange @life_oa_service;
message_kind Delete;
receiver @life_oa_102;
reply_to none;
sender @life_oa_101;
values [];
};
};
description "";
parent @life_oa_100;
postcondition "";
precondition "";
}
oa_scenario life_oa_200 "OA 02 Preexisting empty" id "5ceab400-0000-4000-8000-0000000004b0" {
context none;
description "";
parent none;
postcondition "";
precondition "";
steps [];
viewpoint OperationalActivity;
}
oa_lifeline life_oa_201 "OA Controller lane" id "5ceab400-0000-4000-8000-0000000004b1" {
description "";
parent @life_oa_200;
represented @life_oa_10;
}
oa_lifeline life_oa_202 "OA Receiver lane" id "5ceab400-0000-4000-8000-0000000004b2" {
description "";
parent @life_oa_200;
represented @life_oa_11;
}
oa_scenario life_oa_300 "OA 03 Create only" id "5ceab400-0000-4000-8000-000000000514" {
context none;
description "";
parent none;
postcondition "";
precondition "";
steps [@life_oa_310];
viewpoint OperationalActivity;
}
oa_lifeline life_oa_301 "OA Controller lane" id "5ceab400-0000-4000-8000-000000000515" {
description "";
parent @life_oa_300;
represented @life_oa_10;
}
oa_lifeline life_oa_302 "OA Receiver lane" id "5ceab400-0000-4000-8000-000000000516" {
description "";
parent @life_oa_300;
represented @life_oa_11;
}
oa_occurrence life_oa_310 "OA Create only receiver" id "5ceab400-0000-4000-8000-00000000051e" {
body {
Exchange {
exchange @life_oa_service;
message_kind Create;
receiver @life_oa_302;
reply_to none;
sender @life_oa_301;
values [];
};
};
description "";
parent @life_oa_300;
postcondition "";
precondition "";
}
oa_scenario life_oa_400 "OA 04 Conditional delete" id "5ceab400-0000-4000-8000-000000000578" {
context none;
description "";
parent none;
postcondition "";
precondition "";
steps [@life_oa_410, @life_oa_411];
viewpoint OperationalActivity;
}
oa_lifeline life_oa_401 "OA Controller lane" id "5ceab400-0000-4000-8000-000000000579" {
description "";
parent @life_oa_400;
represented @life_oa_10;
}
oa_lifeline life_oa_402 "OA Receiver lane" id "5ceab400-0000-4000-8000-00000000057a" {
description "";
parent @life_oa_400;
represented @life_oa_11;
}
oa_occurrence life_oa_410 "OA Open call" id "5ceab400-0000-4000-8000-000000000582" {
body {
Exchange {
exchange @life_oa_service;
message_kind Call;
receiver @life_oa_402;
reply_to none;
sender @life_oa_401;
values [];
};
};
description "";
parent @life_oa_400;
postcondition "";
precondition "";
}
oa_occurrence life_oa_411 "OA ALT delete or keep" id "5ceab400-0000-4000-8000-000000000583" {
body {
Fragment {
branches [@life_oa_412, @life_oa_413];
operator Alternative;
};
};
description "";
parent @life_oa_400;
postcondition "";
precondition "";
}
oa_occurrence life_oa_412 "OA Left delete operand" id "5ceab400-0000-4000-8000-000000000584" {
body {
Branch {
guard "stop_requested";
occurrences [@life_oa_414];
};
};
description "";
parent @life_oa_411;
postcondition "";
precondition "";
}
oa_occurrence life_oa_413 "OA Right empty operand" id "5ceab400-0000-4000-8000-000000000585" {
body {
Branch {
guard "else";
occurrences [];
};
};
description "";
parent @life_oa_411;
postcondition "";
precondition "";
}
oa_occurrence life_oa_414 "OA Conditional delete receiver" id "5ceab400-0000-4000-8000-000000000586" {
body {
Exchange {
exchange @life_oa_service;
message_kind Delete;
receiver @life_oa_402;
reply_to none;
sender @life_oa_401;
values [];
};
};
description "";
parent @life_oa_412;
postcondition "";
precondition "";
}
oa_scenario life_oa_500 "OA 05 Unassigned receiver" id "5ceab400-0000-4000-8000-0000000005dc" {
context none;
description "";
parent none;
postcondition "";
precondition "";
steps [@life_oa_510, @life_oa_511];
viewpoint OperationalActivity;
}
oa_lifeline life_oa_501 "OA Controller lane" id "5ceab400-0000-4000-8000-0000000005dd" {
description "";
parent @life_oa_500;
represented @life_oa_10;
}
oa_lifeline life_oa_502 "OA Unassigned receiver lane" id "5ceab400-0000-4000-8000-0000000005de" {
description "";
parent @life_oa_500;
represented none;
}
oa_occurrence life_oa_510 "OA Create unassigned receiver" id "5ceab400-0000-4000-8000-0000000005e6" {
body {
Exchange {
exchange none;
message_kind Create;
receiver @life_oa_502;
reply_to none;
sender @life_oa_501;
values [];
};
};
description "";
parent @life_oa_500;
postcondition "";
precondition "";
}
oa_occurrence life_oa_511 "OA Delete unassigned receiver" id "5ceab400-0000-4000-8000-0000000005e7" {
body {
Exchange {
exchange none;
message_kind Delete;
receiver @life_oa_502;
reply_to none;
sender @life_oa_501;
values [];
};
};
description "";
parent @life_oa_500;
postcondition "";
precondition "";
}
oa_scenario life_oa_600 "OA 06 Self delete" id "5ceab400-0000-4000-8000-000000000640" {
context none;
description "";
parent none;
postcondition "";
precondition "";
steps [@life_oa_610];
viewpoint OperationalActivity;
}
oa_lifeline life_oa_601 "OA Controller lane" id "5ceab400-0000-4000-8000-000000000641" {
description "";
parent @life_oa_600;
represented @life_oa_10;
}
oa_occurrence life_oa_610 "OA Self delete controller" id "5ceab400-0000-4000-8000-00000000064a" {
body {
Exchange {
exchange none;
message_kind Delete;
receiver @life_oa_601;
reply_to none;
sender @life_oa_601;
values [];
};
};
description "";
parent @life_oa_600;
postcondition "";
precondition "";
}
sa_function life_sa_10 "SA Controller work" id "5ceab400-0000-4000-8000-0000000007da" {
description "";
parent none;
}
sa_function life_sa_11 "SA Receiver work" id "5ceab400-0000-4000-8000-0000000007db" {
description "";
parent none;
}
sa_scenario life_sa_100 "SA 01 Root create action delete" id "5ceab400-0000-4000-8000-000000000834" {
context none;
description "";
parent none;
postcondition "";
precondition "";
steps [@life_sa_110, @life_sa_111, @life_sa_112];
viewpoint Functional;
}
sa_lifeline life_sa_101 "SA Controller lane" id "5ceab400-0000-4000-8000-000000000835" {
description "";
parent @life_sa_100;
represented @life_sa_10;
}
sa_lifeline life_sa_102 "界界界界界界界界界界界界界界界界界界" id "5ceab400-0000-4000-8000-000000000836" {
description "";
parent @life_sa_100;
represented @life_sa_11;
}
sa_occurrence life_sa_110 "SA Create receiver" id "5ceab400-0000-4000-8000-00000000083e" {
body {
Exchange {
exchange @life_sa_service;
message_kind Create;
receiver @life_sa_102;
reply_to none;
sender @life_sa_101;
values [];
};
};
description "";
parent @life_sa_100;
postcondition "";
precondition "";
}
sa_occurrence life_sa_111 "SA Receiver action" id "5ceab400-0000-4000-8000-00000000083f" {
body {
Action {
action @life_sa_11;
lifeline @life_sa_102;
};
};
description "";
parent @life_sa_100;
postcondition "";
precondition "";
}
sa_occurrence life_sa_112 "SA Delete receiver" id "5ceab400-0000-4000-8000-000000000840" {
body {
Exchange {
exchange @life_sa_service;
message_kind Delete;
receiver @life_sa_102;
reply_to none;
sender @life_sa_101;
values [];
};
};
description "";
parent @life_sa_100;
postcondition "";
precondition "";
}
sa_scenario life_sa_200 "SA 02 Preexisting empty" id "5ceab400-0000-4000-8000-000000000898" {
context none;
description "";
parent none;
postcondition "";
precondition "";
steps [];
viewpoint Functional;
}
sa_lifeline life_sa_201 "SA Controller lane" id "5ceab400-0000-4000-8000-000000000899" {
description "";
parent @life_sa_200;
represented @life_sa_10;
}
sa_lifeline life_sa_202 "SA Receiver lane" id "5ceab400-0000-4000-8000-00000000089a" {
description "";
parent @life_sa_200;
represented @life_sa_11;
}
sa_scenario life_sa_300 "SA 03 Create only" id "5ceab400-0000-4000-8000-0000000008fc" {
context none;
description "";
parent none;
postcondition "";
precondition "";
steps [@life_sa_310];
viewpoint Functional;
}
sa_lifeline life_sa_301 "SA Controller lane" id "5ceab400-0000-4000-8000-0000000008fd" {
description "";
parent @life_sa_300;
represented @life_sa_10;
}
sa_lifeline life_sa_302 "SA Receiver lane" id "5ceab400-0000-4000-8000-0000000008fe" {
description "";
parent @life_sa_300;
represented @life_sa_11;
}
sa_occurrence life_sa_310 "SA Create only receiver" id "5ceab400-0000-4000-8000-000000000906" {
body {
Exchange {
exchange @life_sa_service;
message_kind Create;
receiver @life_sa_302;
reply_to none;
sender @life_sa_301;
values [];
};
};
description "";
parent @life_sa_300;
postcondition "";
precondition "";
}
sa_scenario life_sa_400 "SA 04 Conditional delete" id "5ceab400-0000-4000-8000-000000000960" {
context none;
description "";
parent none;
postcondition "";
precondition "";
steps [@life_sa_410, @life_sa_411];
viewpoint Functional;
}
sa_lifeline life_sa_401 "SA Controller lane" id "5ceab400-0000-4000-8000-000000000961" {
description "";
parent @life_sa_400;
represented @life_sa_10;
}
sa_lifeline life_sa_402 "SA Receiver lane" id "5ceab400-0000-4000-8000-000000000962" {
description "";
parent @life_sa_400;
represented @life_sa_11;
}
sa_occurrence life_sa_410 "SA Open call" id "5ceab400-0000-4000-8000-00000000096a" {
body {
Exchange {
exchange @life_sa_service;
message_kind Call;
receiver @life_sa_402;
reply_to none;
sender @life_sa_401;
values [];
};
};
description "";
parent @life_sa_400;
postcondition "";
precondition "";
}
sa_occurrence life_sa_411 "SA ALT delete or keep" id "5ceab400-0000-4000-8000-00000000096b" {
body {
Fragment {
branches [@life_sa_412, @life_sa_413];
operator Alternative;
};
};
description "";
parent @life_sa_400;
postcondition "";
precondition "";
}
sa_occurrence life_sa_412 "SA Left delete operand" id "5ceab400-0000-4000-8000-00000000096c" {
body {
Branch {
guard "stop_requested";
occurrences [@life_sa_414];
};
};
description "";
parent @life_sa_411;
postcondition "";
precondition "";
}
sa_occurrence life_sa_413 "SA Right empty operand" id "5ceab400-0000-4000-8000-00000000096d" {
body {
Branch {
guard "else";
occurrences [];
};
};
description "";
parent @life_sa_411;
postcondition "";
precondition "";
}
sa_occurrence life_sa_414 "SA Conditional delete receiver" id "5ceab400-0000-4000-8000-00000000096e" {
body {
Exchange {
exchange @life_sa_service;
message_kind Delete;
receiver @life_sa_402;
reply_to none;
sender @life_sa_401;
values [];
};
};
description "";
parent @life_sa_412;
postcondition "";
precondition "";
}
sa_scenario life_sa_500 "SA 05 Unassigned receiver" id "5ceab400-0000-4000-8000-0000000009c4" {
context none;
description "";
parent none;
postcondition "";
precondition "";
steps [@life_sa_510, @life_sa_511];
viewpoint Functional;
}
sa_lifeline life_sa_501 "SA Controller lane" id "5ceab400-0000-4000-8000-0000000009c5" {
description "";
parent @life_sa_500;
represented @life_sa_10;
}
sa_lifeline life_sa_502 "SA Unassigned receiver lane" id "5ceab400-0000-4000-8000-0000000009c6" {
description "";
parent @life_sa_500;
represented none;
}
sa_occurrence life_sa_510 "SA Create unassigned receiver" id "5ceab400-0000-4000-8000-0000000009ce" {
body {
Exchange {
exchange none;
message_kind Create;
receiver @life_sa_502;
reply_to none;
sender @life_sa_501;
values [];
};
};
description "";
parent @life_sa_500;
postcondition "";
precondition "";
}
sa_occurrence life_sa_511 "SA Delete unassigned receiver" id "5ceab400-0000-4000-8000-0000000009cf" {
body {
Exchange {
exchange none;
message_kind Delete;
receiver @life_sa_502;
reply_to none;
sender @life_sa_501;
values [];
};
};
description "";
parent @life_sa_500;
postcondition "";
precondition "";
}
sa_scenario life_sa_600 "SA 06 Self delete" id "5ceab400-0000-4000-8000-000000000a28" {
context none;
description "";
parent none;
postcondition "";
precondition "";
steps [@life_sa_610];
viewpoint Functional;
}
sa_lifeline life_sa_601 "SA Controller lane" id "5ceab400-0000-4000-8000-000000000a29" {
description "";
parent @life_sa_600;
represented @life_sa_10;
}
sa_occurrence life_sa_610 "SA Self delete controller" id "5ceab400-0000-4000-8000-000000000a32" {
body {
Exchange {
exchange none;
message_kind Delete;
receiver @life_sa_601;
reply_to none;
sender @life_sa_601;
values [];
};
};
description "";
parent @life_sa_600;
postcondition "";
precondition "";
}
sa_port sa_lifecycle_service_input "SA Lifecycle service input" id "a4a6ecc0-3e12-51a5-b08b-199ad743525c" {
contract {
FunctionInput {
incoming_items [];
};
};
description "";
legacy none;
parent @life_sa_11;
}
sa_port sa_lifecycle_service_output "SA Lifecycle service output" id "b1aa66c4-0e8e-5091-beda-2b8d2005b688" {
contract {
FunctionOutput {
outgoing_items [];
};
};
description "";
legacy none;
parent @life_sa_10;
}
interaction life_oa_service "OA Lifecycle service" id "5ceab400-0000-4000-8000-0000000003f4" {
description "";
items [];
source @life_oa_10;
target @life_oa_11;
}
functional_exchange life_sa_service "SA Lifecycle service" id "5ceab400-0000-4000-8000-0000000007dc" {
description "";
items [];
source @life_sa_10;
source_port @sa_lifecycle_service_output;
target @life_sa_11;
target_port @sa_lifecycle_service_input;
}
}
@@ -1,2 +0,0 @@
// Preserve lifecycle note: 雨 · receiver.
// Geometry never changes these bytes.
@@ -1,692 +0,0 @@
arcadia 7;
project lifecycle_native "Lifecycle native contract" id "5ceab400-0000-4000-8000-000000000001" {
include "rationale.arcadia";
description "Fixed semantic inputs; lifecycle geometry is derived and never authored.";
oa_activity life_oa_10 "OA Controller work" id "5ceab400-0000-4000-8000-0000000003f2" {
description "";
parent none;
}
oa_activity life_oa_11 "OA Receiver work" id "5ceab400-0000-4000-8000-0000000003f3" {
description "";
parent none;
}
oa_scenario life_oa_100 "OA 01 Root create action delete" id "5ceab400-0000-4000-8000-00000000044c" {
context none;
description "";
parent none;
postcondition "";
precondition "";
steps [@life_oa_110, @life_oa_111, @life_oa_112];
viewpoint OperationalActivity;
}
oa_lifeline life_oa_101 "OA Controller lane" id "5ceab400-0000-4000-8000-00000000044d" {
description "";
parent @life_oa_100;
represented @life_oa_10;
}
oa_lifeline life_oa_102 "WWWWWWWWWWWWWWWWWW" id "5ceab400-0000-4000-8000-00000000044e" {
description "";
parent @life_oa_100;
represented @life_oa_11;
}
oa_occurrence life_oa_110 "OA Create receiver" id "5ceab400-0000-4000-8000-000000000456" {
body {
Exchange {
exchange @life_oa_service;
message_kind Create;
receiver @life_oa_102;
reply_to none;
sender @life_oa_101;
values [];
};
};
description "";
parent @life_oa_100;
postcondition "";
precondition "";
}
oa_occurrence life_oa_111 "OA Receiver action" id "5ceab400-0000-4000-8000-000000000457" {
body {
Action {
action @life_oa_11;
lifeline @life_oa_102;
};
};
description "";
parent @life_oa_100;
postcondition "";
precondition "";
}
oa_occurrence life_oa_112 "OA Delete receiver" id "5ceab400-0000-4000-8000-000000000458" {
body {
Exchange {
exchange @life_oa_service;
message_kind Delete;
receiver @life_oa_102;
reply_to none;
sender @life_oa_101;
values [];
};
};
description "";
parent @life_oa_100;
postcondition "";
precondition "";
}
oa_scenario life_oa_200 "OA 02 Preexisting empty" id "5ceab400-0000-4000-8000-0000000004b0" {
context none;
description "";
parent none;
postcondition "";
precondition "";
steps [];
viewpoint OperationalActivity;
}
oa_lifeline life_oa_201 "OA Controller lane" id "5ceab400-0000-4000-8000-0000000004b1" {
description "";
parent @life_oa_200;
represented @life_oa_10;
}
oa_lifeline life_oa_202 "OA Receiver lane" id "5ceab400-0000-4000-8000-0000000004b2" {
description "";
parent @life_oa_200;
represented @life_oa_11;
}
oa_scenario life_oa_300 "OA 03 Create only" id "5ceab400-0000-4000-8000-000000000514" {
context none;
description "";
parent none;
postcondition "";
precondition "";
steps [@life_oa_310];
viewpoint OperationalActivity;
}
oa_lifeline life_oa_301 "OA Controller lane" id "5ceab400-0000-4000-8000-000000000515" {
description "";
parent @life_oa_300;
represented @life_oa_10;
}
oa_lifeline life_oa_302 "OA Receiver lane" id "5ceab400-0000-4000-8000-000000000516" {
description "";
parent @life_oa_300;
represented @life_oa_11;
}
oa_occurrence life_oa_310 "OA Create only receiver" id "5ceab400-0000-4000-8000-00000000051e" {
body {
Exchange {
exchange @life_oa_service;
message_kind Create;
receiver @life_oa_302;
reply_to none;
sender @life_oa_301;
values [];
};
};
description "";
parent @life_oa_300;
postcondition "";
precondition "";
}
oa_scenario life_oa_400 "OA 04 Conditional delete" id "5ceab400-0000-4000-8000-000000000578" {
context none;
description "";
parent none;
postcondition "";
precondition "";
steps [@life_oa_410, @life_oa_411];
viewpoint OperationalActivity;
}
oa_lifeline life_oa_401 "OA Controller lane" id "5ceab400-0000-4000-8000-000000000579" {
description "";
parent @life_oa_400;
represented @life_oa_10;
}
oa_lifeline life_oa_402 "OA Receiver lane" id "5ceab400-0000-4000-8000-00000000057a" {
description "";
parent @life_oa_400;
represented @life_oa_11;
}
oa_occurrence life_oa_410 "OA Open call" id "5ceab400-0000-4000-8000-000000000582" {
body {
Exchange {
exchange @life_oa_service;
message_kind Call;
receiver @life_oa_402;
reply_to none;
sender @life_oa_401;
values [];
};
};
description "";
parent @life_oa_400;
postcondition "";
precondition "";
}
oa_occurrence life_oa_411 "OA ALT delete or keep" id "5ceab400-0000-4000-8000-000000000583" {
body {
Fragment {
branches [@life_oa_412, @life_oa_413];
operator Alternative;
};
};
description "";
parent @life_oa_400;
postcondition "";
precondition "";
}
oa_occurrence life_oa_412 "OA Left delete operand" id "5ceab400-0000-4000-8000-000000000584" {
body {
Branch {
guard "stop_requested";
occurrences [@life_oa_414];
};
};
description "";
parent @life_oa_411;
postcondition "";
precondition "";
}
oa_occurrence life_oa_413 "OA Right empty operand" id "5ceab400-0000-4000-8000-000000000585" {
body {
Branch {
guard "else";
occurrences [];
};
};
description "";
parent @life_oa_411;
postcondition "";
precondition "";
}
oa_occurrence life_oa_414 "OA Conditional delete receiver" id "5ceab400-0000-4000-8000-000000000586" {
body {
Exchange {
exchange @life_oa_service;
message_kind Delete;
receiver @life_oa_402;
reply_to none;
sender @life_oa_401;
values [];
};
};
description "";
parent @life_oa_412;
postcondition "";
precondition "";
}
oa_scenario life_oa_500 "OA 05 Unassigned receiver" id "5ceab400-0000-4000-8000-0000000005dc" {
context none;
description "";
parent none;
postcondition "";
precondition "";
steps [@life_oa_510, @life_oa_511];
viewpoint OperationalActivity;
}
oa_lifeline life_oa_501 "OA Controller lane" id "5ceab400-0000-4000-8000-0000000005dd" {
description "";
parent @life_oa_500;
represented @life_oa_10;
}
oa_lifeline life_oa_502 "OA Unassigned receiver lane" id "5ceab400-0000-4000-8000-0000000005de" {
description "";
parent @life_oa_500;
represented none;
}
oa_occurrence life_oa_510 "OA Create unassigned receiver" id "5ceab400-0000-4000-8000-0000000005e6" {
body {
Exchange {
exchange none;
message_kind Create;
receiver @life_oa_502;
reply_to none;
sender @life_oa_501;
values [];
};
};
description "";
parent @life_oa_500;
postcondition "";
precondition "";
}
oa_occurrence life_oa_511 "OA Delete unassigned receiver" id "5ceab400-0000-4000-8000-0000000005e7" {
body {
Exchange {
exchange none;
message_kind Delete;
receiver @life_oa_502;
reply_to none;
sender @life_oa_501;
values [];
};
};
description "";
parent @life_oa_500;
postcondition "";
precondition "";
}
oa_scenario life_oa_600 "OA 06 Self delete" id "5ceab400-0000-4000-8000-000000000640" {
context none;
description "";
parent none;
postcondition "";
precondition "";
steps [@life_oa_610];
viewpoint OperationalActivity;
}
oa_lifeline life_oa_601 "OA Controller lane" id "5ceab400-0000-4000-8000-000000000641" {
description "";
parent @life_oa_600;
represented @life_oa_10;
}
oa_occurrence life_oa_610 "OA Self delete controller" id "5ceab400-0000-4000-8000-00000000064a" {
body {
Exchange {
exchange none;
message_kind Delete;
receiver @life_oa_601;
reply_to none;
sender @life_oa_601;
values [];
};
};
description "";
parent @life_oa_600;
postcondition "";
precondition "";
}
sa_function life_sa_10 "SA Controller work" id "5ceab400-0000-4000-8000-0000000007da" {
description "";
parent none;
}
sa_function life_sa_11 "SA Receiver work" id "5ceab400-0000-4000-8000-0000000007db" {
description "";
parent none;
}
sa_scenario life_sa_100 "SA 01 Root create action delete" id "5ceab400-0000-4000-8000-000000000834" {
context none;
description "";
parent none;
postcondition "";
precondition "";
steps [@life_sa_110, @life_sa_111, @life_sa_112];
viewpoint Functional;
}
sa_lifeline life_sa_101 "SA Controller lane" id "5ceab400-0000-4000-8000-000000000835" {
description "";
parent @life_sa_100;
represented @life_sa_10;
}
sa_lifeline life_sa_102 "界界界界界界界界界界界界界界界界界界" id "5ceab400-0000-4000-8000-000000000836" {
description "";
parent @life_sa_100;
represented @life_sa_11;
}
sa_occurrence life_sa_110 "SA Create receiver" id "5ceab400-0000-4000-8000-00000000083e" {
body {
Exchange {
exchange @life_sa_service;
message_kind Create;
receiver @life_sa_102;
reply_to none;
sender @life_sa_101;
values [];
};
};
description "";
parent @life_sa_100;
postcondition "";
precondition "";
}
sa_occurrence life_sa_111 "SA Receiver action" id "5ceab400-0000-4000-8000-00000000083f" {
body {
Action {
action @life_sa_11;
lifeline @life_sa_102;
};
};
description "";
parent @life_sa_100;
postcondition "";
precondition "";
}
sa_occurrence life_sa_112 "SA Delete receiver" id "5ceab400-0000-4000-8000-000000000840" {
body {
Exchange {
exchange @life_sa_service;
message_kind Delete;
receiver @life_sa_102;
reply_to none;
sender @life_sa_101;
values [];
};
};
description "";
parent @life_sa_100;
postcondition "";
precondition "";
}
sa_scenario life_sa_200 "SA 02 Preexisting empty" id "5ceab400-0000-4000-8000-000000000898" {
context none;
description "";
parent none;
postcondition "";
precondition "";
steps [];
viewpoint Functional;
}
sa_lifeline life_sa_201 "SA Controller lane" id "5ceab400-0000-4000-8000-000000000899" {
description "";
parent @life_sa_200;
represented @life_sa_10;
}
sa_lifeline life_sa_202 "SA Receiver lane" id "5ceab400-0000-4000-8000-00000000089a" {
description "";
parent @life_sa_200;
represented @life_sa_11;
}
sa_scenario life_sa_300 "SA 03 Create only" id "5ceab400-0000-4000-8000-0000000008fc" {
context none;
description "";
parent none;
postcondition "";
precondition "";
steps [@life_sa_310];
viewpoint Functional;
}
sa_lifeline life_sa_301 "SA Controller lane" id "5ceab400-0000-4000-8000-0000000008fd" {
description "";
parent @life_sa_300;
represented @life_sa_10;
}
sa_lifeline life_sa_302 "SA Receiver lane" id "5ceab400-0000-4000-8000-0000000008fe" {
description "";
parent @life_sa_300;
represented @life_sa_11;
}
sa_occurrence life_sa_310 "SA Create only receiver" id "5ceab400-0000-4000-8000-000000000906" {
body {
Exchange {
exchange @life_sa_service;
message_kind Create;
receiver @life_sa_302;
reply_to none;
sender @life_sa_301;
values [];
};
};
description "";
parent @life_sa_300;
postcondition "";
precondition "";
}
sa_scenario life_sa_400 "SA 04 Conditional delete" id "5ceab400-0000-4000-8000-000000000960" {
context none;
description "";
parent none;
postcondition "";
precondition "";
steps [@life_sa_410, @life_sa_411];
viewpoint Functional;
}
sa_lifeline life_sa_401 "SA Controller lane" id "5ceab400-0000-4000-8000-000000000961" {
description "";
parent @life_sa_400;
represented @life_sa_10;
}
sa_lifeline life_sa_402 "SA Receiver lane" id "5ceab400-0000-4000-8000-000000000962" {
description "";
parent @life_sa_400;
represented @life_sa_11;
}
sa_occurrence life_sa_410 "SA Open call" id "5ceab400-0000-4000-8000-00000000096a" {
body {
Exchange {
exchange @life_sa_service;
message_kind Call;
receiver @life_sa_402;
reply_to none;
sender @life_sa_401;
values [];
};
};
description "";
parent @life_sa_400;
postcondition "";
precondition "";
}
sa_occurrence life_sa_411 "SA ALT delete or keep" id "5ceab400-0000-4000-8000-00000000096b" {
body {
Fragment {
branches [@life_sa_412, @life_sa_413];
operator Alternative;
};
};
description "";
parent @life_sa_400;
postcondition "";
precondition "";
}
sa_occurrence life_sa_412 "SA Left delete operand" id "5ceab400-0000-4000-8000-00000000096c" {
body {
Branch {
guard "stop_requested";
occurrences [@life_sa_414];
};
};
description "";
parent @life_sa_411;
postcondition "";
precondition "";
}
sa_occurrence life_sa_413 "SA Right empty operand" id "5ceab400-0000-4000-8000-00000000096d" {
body {
Branch {
guard "else";
occurrences [];
};
};
description "";
parent @life_sa_411;
postcondition "";
precondition "";
}
sa_occurrence life_sa_414 "SA Conditional delete receiver" id "5ceab400-0000-4000-8000-00000000096e" {
body {
Exchange {
exchange @life_sa_service;
message_kind Delete;
receiver @life_sa_402;
reply_to none;
sender @life_sa_401;
values [];
};
};
description "";
parent @life_sa_412;
postcondition "";
precondition "";
}
sa_scenario life_sa_500 "SA 05 Unassigned receiver" id "5ceab400-0000-4000-8000-0000000009c4" {
context none;
description "";
parent none;
postcondition "";
precondition "";
steps [@life_sa_510, @life_sa_511];
viewpoint Functional;
}
sa_lifeline life_sa_501 "SA Controller lane" id "5ceab400-0000-4000-8000-0000000009c5" {
description "";
parent @life_sa_500;
represented @life_sa_10;
}
sa_lifeline life_sa_502 "SA Unassigned receiver lane" id "5ceab400-0000-4000-8000-0000000009c6" {
description "";
parent @life_sa_500;
represented none;
}
sa_occurrence life_sa_510 "SA Create unassigned receiver" id "5ceab400-0000-4000-8000-0000000009ce" {
body {
Exchange {
exchange none;
message_kind Create;
receiver @life_sa_502;
reply_to none;
sender @life_sa_501;
values [];
};
};
description "";
parent @life_sa_500;
postcondition "";
precondition "";
}
sa_occurrence life_sa_511 "SA Delete unassigned receiver" id "5ceab400-0000-4000-8000-0000000009cf" {
body {
Exchange {
exchange none;
message_kind Delete;
receiver @life_sa_502;
reply_to none;
sender @life_sa_501;
values [];
};
};
description "";
parent @life_sa_500;
postcondition "";
precondition "";
}
sa_scenario life_sa_600 "SA 06 Self delete" id "5ceab400-0000-4000-8000-000000000a28" {
context none;
description "";
parent none;
postcondition "";
precondition "";
steps [@life_sa_610];
viewpoint Functional;
}
sa_lifeline life_sa_601 "SA Controller lane" id "5ceab400-0000-4000-8000-000000000a29" {
description "";
parent @life_sa_600;
represented @life_sa_10;
}
sa_occurrence life_sa_610 "SA Self delete controller" id "5ceab400-0000-4000-8000-000000000a32" {
body {
Exchange {
exchange none;
message_kind Delete;
receiver @life_sa_601;
reply_to none;
sender @life_sa_601;
values [];
};
};
description "";
parent @life_sa_600;
postcondition "";
precondition "";
}
sa_port sa_lifecycle_service_input "SA Lifecycle service input" id "a4a6ecc0-3e12-51a5-b08b-199ad743525c" {
contract {
FunctionInput {
incoming_items [];
};
};
description "";
legacy none;
parent @life_sa_11;
}
sa_port sa_lifecycle_service_output "SA Lifecycle service output" id "b1aa66c4-0e8e-5091-beda-2b8d2005b688" {
contract {
FunctionOutput {
outgoing_items [];
};
};
description "";
legacy none;
parent @life_sa_10;
}
interaction life_oa_service "OA Lifecycle service" id "5ceab400-0000-4000-8000-0000000003f4" {
description "";
items [];
source @life_oa_10;
target @life_oa_11;
}
functional_exchange life_sa_service "SA Lifecycle service" id "5ceab400-0000-4000-8000-0000000007dc" {
description "";
items [];
source @life_sa_10;
source_port @sa_lifecycle_service_output;
target @life_sa_11;
target_port @sa_lifecycle_service_input;
}
}
@@ -1,2 +0,0 @@
// Preserve lifecycle note: 雨 · receiver.
// Geometry never changes these bytes.
@@ -1,183 +0,0 @@
#Requires -Version 7.0
# PREPARED ONLY. Root decides when to invoke. Keep this logger alive until normal GUI close.
[CmdletBinding()]
param()
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$nativeRoot = Join-Path $PSScriptRoot 'native-a57c334-camera'
$nativeFreeze = 'a57c334b302eb13352b4bb65df1c75d94d002348'
$nativeManifestHash = '293892C2F430D6EADB58C6678F3C1B346742B3AA4D2FE5CE3729F896EAD2C2DB'
$nativeInputHash = '47FEC9CADC0E7FAE99D789376C111C02EEAAB7FAC3740DE48174A06F61FE16B3'
$nativeGuiHash = '69DBB90FA299B3B3D90140C7934891F79166025893AF9B35B2B8D754A37A0D40'
$nativeDllHash = 'AA282FA47BB2C4E210CEA5E470CDBDCA58568F0BE0BC73E404D86AB9CD95EF6A'
$nativeBin = Join-Path $nativeRoot 'bin'
$nativeProfile = Join-Path $nativeRoot 'profile'
$nativeCwd = Join-Path $nativeRoot 'unrelated-cwd'
$nativePath = 'C:\Windows\System32;C:\Windows;' + $nativeBin
$nativeReceipt = Join-Path $nativeRoot 'launch.json'
$nativeTerminal = Join-Path $nativeRoot 'normal-close.json'
$nativeStdout = Join-Path $nativeRoot 'logs/studio.stdout.log'
$nativeStderr = Join-Path $nativeRoot 'logs/studio.stderr.log'
$nativeUtf8 = [Text.UTF8Encoding]::new($false)
function Read-NativeItem([string]$Path) {
$item = Get-Item -LiteralPath $Path -Force
$ancestor = $item
while ($null -ne $ancestor) {
if (($ancestor.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
throw "Reparse path requires separate review: $($ancestor.FullName)"
}
$ancestor = if ($ancestor -is [IO.FileInfo]) { $ancestor.Directory } else { $ancestor.Parent }
}
return $item
}
function Read-NativeFile([string]$Path) {
$item = Read-NativeItem $Path
if ($item.PSIsContainer) { throw "Expected file: $Path" }
return [ordered]@{ path = $item.FullName; bytes = $item.Length; sha256 = (Get-FileHash -LiteralPath $item.FullName -Algorithm SHA256).Hash }
}
function Write-NativeRecord($Record, [IO.FileStream]$Stream) {
$bytes = $nativeUtf8.GetBytes(($Record | ConvertTo-Json -Depth 12) + "`n")
$Stream.Position = 0
$Stream.SetLength(0)
$Stream.Write($bytes, 0, $bytes.Length)
$Stream.Flush($true)
}
function Open-NativeOutput([string]$Path) {
return [IO.File]::Open($Path, [IO.FileMode]::CreateNew, [IO.FileAccess]::ReadWrite, [IO.FileShare]::Read)
}
# Source provenance is the closed a57 gate, never current dirty sources or target/debug.
$nativeManifestFile = Read-NativeFile (Join-Path $nativeRoot 'preparation-manifest.json')
if ($nativeManifestFile.sha256 -ne $nativeManifestHash) { throw 'Preparation manifest changed.' }
$nativeManifest = Get-Content -LiteralPath $nativeManifestFile.path -Raw | ConvertFrom-Json
if ($nativeManifest.source_commit -ne $nativeFreeze -or $nativeManifest.input_manifest_sha256 -ne $nativeInputHash -or $nativeManifest.frozen_input_count -ne 529) { throw 'Unexpected frozen input identity.' }
foreach ($file in $nativeManifest.files) {
$path = [IO.Path]::GetFullPath((Join-Path $nativeRoot $file.target))
if (-not $path.StartsWith($nativeRoot + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { throw 'Manifest path escapes private root.' }
$actual = Read-NativeFile $path
if ($actual.sha256 -ne $file.sha256 -or $actual.bytes -ne $file.bytes) { throw "Prepared input changed: $($file.target)" }
}
$nativeInputs = Read-NativeFile (Join-Path $nativeRoot 'provenance/inputs.json')
if ($nativeInputs.sha256 -ne $nativeInputHash) { throw 'Frozen input manifest changed.' }
$nativeBuildFile = Read-NativeFile (Join-Path $nativeRoot 'provenance/studio-build-status.json')
$nativeBuild = Get-Content -LiteralPath $nativeBuildFile.path -Raw | ConvertFrom-Json
if ($nativeBuild.exit -ne 0 -or $nativeBuild.name -ne 'canvas-union-windows-20260911/studio-build' -or [string]::IsNullOrWhiteSpace($nativeBuild.completed_utc)) { throw 'Matching completed build required.' }
$nativeGui = Read-NativeFile (Join-Path $nativeBin 'arcadia-studio.exe')
$nativeDll = Read-NativeFile (Join-Path $nativeBin 'libunwind.dll')
if ($nativeGui.sha256 -ne $nativeGuiHash -or $nativeGui.bytes -ne 113028608 -or $nativeDll.sha256 -ne $nativeDllHash -or $nativeDll.bytes -ne 190464) { throw 'Immutable GUI/runtime mismatch.' }
if (-not [string]::Equals($env:SystemRoot, 'C:\Windows', [StringComparison]::OrdinalIgnoreCase)) { throw 'This recorded launch requires SystemRoot C:\Windows.' }
foreach ($directory in @($nativeProfile, $nativeCwd, (Join-Path $nativeRoot 'logs'), 'C:\Windows\System32')) {
if (-not (Read-NativeItem $directory).PSIsContainer) { throw "Required directory missing: $directory" }
}
foreach ($empty in @($nativeProfile, $nativeCwd)) {
if (@(Get-ChildItem -LiteralPath $empty -Force).Count -ne 0) { throw "Fresh empty directory required: $empty" }
}
foreach ($output in @($nativeReceipt, $nativeTerminal, $nativeStdout, $nativeStderr)) {
if (Test-Path -LiteralPath $output) { throw "Never overwrite prior launch evidence: $output" }
}
$nativeState = [ordered]@{
source_commit = $nativeFreeze
scope = 'Independent pre-grapheme a57 camera/clipping/zoom/hit session; no package or new combined Unicode acceptance implied.'
preparation_manifest = $nativeManifestFile
build_inputs_manifest = $nativeInputs
build_status_file = $nativeBuildFile
build_status = $nativeBuild
studio = $nativeGui
runtime = $nativeDll
launch_script = (Read-NativeFile $PSCommandPath)
process_path = $nativePath
isolated_localappdata = $nativeProfile
working_directory = $nativeCwd
arguments = @()
environment_scope = 'Child ProcessStartInfo only; other normal inherited environment retained.'
window_style = 'Hidden'
use_shell_execute = $false
create_no_window = $true
stdout = $nativeStdout
stderr = $nativeStderr
pid = $null
launch_requested_utc = $null
process_started_utc = $null
state = 'preflight_passed'
acceptance = 'Pending root observation and independent source/model checks.'
}
$nativeReceiptStream = $null
$nativeOutStream = $null
$nativeErrStream = $null
$nativeProcess = $null
$nativeStarted = $false
try {
# Exclusive receipt reservation also serializes accidental concurrent invocations.
$nativeReceiptStream = Open-NativeOutput $nativeReceipt
Write-NativeRecord $nativeState $nativeReceiptStream
$nativeOutStream = Open-NativeOutput $nativeStdout
$nativeErrStream = Open-NativeOutput $nativeStderr
$nativeInfo = [Diagnostics.ProcessStartInfo]::new()
$nativeInfo.FileName = $nativeGui.path
$nativeInfo.WorkingDirectory = $nativeCwd
$nativeInfo.UseShellExecute = $false
$nativeInfo.CreateNoWindow = $true
$nativeInfo.WindowStyle = [Diagnostics.ProcessWindowStyle]::Hidden
$nativeInfo.RedirectStandardOutput = $true
$nativeInfo.RedirectStandardError = $true
$nativeInfo.Environment['PATH'] = $nativePath
$nativeInfo.Environment['LOCALAPPDATA'] = $nativeProfile
foreach ($file in @($nativeGui, $nativeDll, $nativeManifestFile, $nativeInputs, $nativeBuildFile)) {
if ((Read-NativeFile $file.path).sha256 -ne $file.sha256) { throw "Launch-critical input changed: $($file.path)" }
}
$nativeProcess = [Diagnostics.Process]::new()
$nativeProcess.StartInfo = $nativeInfo
$nativeState.launch_requested_utc = [DateTime]::UtcNow.ToString('o')
if (-not $nativeProcess.Start()) { throw 'Process.Start returned false.' }
$nativeStarted = $true
$nativeState.pid = $nativeProcess.Id
# Pump both raw streams concurrently; no line decoding/newline normalization.
$nativeOutCopy = $nativeProcess.StandardOutput.BaseStream.CopyToAsync($nativeOutStream)
$nativeErrCopy = $nativeProcess.StandardError.BaseStream.CopyToAsync($nativeErrStream)
$nativeState.process_started_utc = $nativeProcess.StartTime.ToUniversalTime().ToString('o')
$nativeState.state = 'launched'
Write-NativeRecord $nativeState $nativeReceiptStream
$nativeReceiptStream.Dispose()
$nativeReceiptStream = $null
$nativeState | ConvertTo-Json -Depth 12 | Write-Output
# Deliberately stays attached as a logger. Root uses a running shell session and closes GUI normally.
$nativeProcess.WaitForExit()
[Threading.Tasks.Task]::WaitAll([Threading.Tasks.Task[]]@($nativeOutCopy, $nativeErrCopy))
$nativeOutStream.Flush($true)
$nativeErrStream.Flush($true)
$nativeOutStream.Dispose()
$nativeOutStream = $null
$nativeErrStream.Dispose()
$nativeErrStream = $null
$nativeClosed = [ordered]@{
source_commit = $nativeFreeze
pid = $nativeState.pid
process_started_utc = $nativeState.process_started_utc
observed_exit_utc = [DateTime]::UtcNow.ToString('o')
exit_code = $nativeProcess.ExitCode
scope = 'Process termination and closed raw logs only; UI acceptance and reason for close require root observation.'
stdout = (Read-NativeFile $nativeStdout)
stderr = (Read-NativeFile $nativeStderr)
studio_readback = (Read-NativeFile $nativeGui.path)
runtime_readback = (Read-NativeFile $nativeDll.path)
}
$nativeCloseStream = Open-NativeOutput $nativeTerminal
try { Write-NativeRecord $nativeClosed $nativeCloseStream } finally { $nativeCloseStream.Dispose() }
$nativeClosed | ConvertTo-Json -Depth 12 | Write-Output
} catch {
if ($null -ne $nativeReceiptStream) {
$nativeState.state = if ($nativeStarted) { 'launched_record_error' } else { 'launch_failed' }
$nativeState['error'] = $_.Exception.Message
Write-NativeRecord $nativeState $nativeReceiptStream
}
throw
} finally {
if ($null -ne $nativeReceiptStream) { $nativeReceiptStream.Dispose() }
if ($null -ne $nativeOutStream) { $nativeOutStream.Dispose() }
if ($null -ne $nativeErrStream) { $nativeErrStream.Dispose() }
if ($null -ne $nativeProcess) { $nativeProcess.Dispose() }
# Never kill an application here. Root owns every native interaction and any recovery.
}
@@ -1,5 +0,0 @@
{
"scope": "Parser only; no launch",
"errors": [],
"pass": true
}
@@ -1,67 +0,0 @@
{
"source_commit": "a57c334b302eb13352b4bb65df1c75d94d002348",
"scope": "Independent pre-grapheme a57 camera/clipping/zoom/hit session; no package or new combined Unicode acceptance implied.",
"preparation_manifest": {
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\preparation-manifest.json",
"bytes": 9149,
"sha256": "293892C2F430D6EADB58C6678F3C1B346742B3AA4D2FE5CE3729F896EAD2C2DB"
},
"build_inputs_manifest": {
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\provenance\\inputs.json",
"bytes": 101859,
"sha256": "47FEC9CADC0E7FAE99D789376C111C02EEAAB7FAC3740DE48174A06F61FE16B3"
},
"build_status_file": {
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\provenance\\studio-build-status.json",
"bytes": 474,
"sha256": "AB1E04807F32F8DADA0A572D69F632BD6FD242150DF211C18E35D82EA8BE91EC"
},
"build_status": {
"seconds": 385.1131455,
"log": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\studio-build.log",
"started_utc": "2026-09-11T20:14:23.6348444Z",
"exit": 0,
"command": [
"cargo",
"build",
"-p",
"oa-desktop",
"--bin",
"arcadia-studio",
"--offline",
"--locked"
],
"name": "canvas-union-windows-20260911/studio-build",
"completed_utc": "2026-09-11T20:20:48.7677661Z"
},
"studio": {
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\bin\\arcadia-studio.exe",
"bytes": 113028608,
"sha256": "69DBB90FA299B3B3D90140C7934891F79166025893AF9B35B2B8D754A37A0D40"
},
"runtime": {
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\bin\\libunwind.dll",
"bytes": 190464,
"sha256": "AA282FA47BB2C4E210CEA5E470CDBDCA58568F0BE0BC73E404D86AB9CD95EF6A"
},
"launch_script": {
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\launch-native-a57c334-camera.ps1",
"bytes": 10001,
"sha256": "BE80A4B5B407BCEA3DBECC547DA5E5888F3C6DC06F5F7AF87F546C388963B4DE"
},
"process_path": "C:\\Windows\\System32;C:\\Windows;C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\bin",
"isolated_localappdata": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\profile",
"working_directory": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\unrelated-cwd",
"arguments": [],
"environment_scope": "Child ProcessStartInfo only; other normal inherited environment retained.",
"window_style": "Hidden",
"use_shell_execute": false,
"create_no_window": true,
"stdout": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\logs\\studio.stdout.log",
"stderr": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\logs\\studio.stderr.log",
"pid": 5004,
"launch_requested_utc": "2026-09-12T10:52:56.9766659Z",
"process_started_utc": "2026-09-12T10:52:57.0962185Z",
"state": "launched",
"acceptance": "Pending root observation and independent source/model checks."
}
@@ -1 +0,0 @@
PASS same: all identities, fields, reachable source and physical bytes
@@ -1,347 +0,0 @@
{
"created_utc": "2026-09-12T11:07:20.1744786Z",
"scope": "Closed a57 camera evidence only; copying does not execute gates or UI",
"payload_count": 47,
"payload_bytes": 2184242.0,
"files": [
{
"path": "baseline-wide/project.arcadia",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\baseline-wide\\project.arcadia",
"bytes": 20128,
"original_sha256": "6A18EE1F246321BDF8AB0E7E8E5D4EBA134CE486ED5224C4FFD61C7342C3A7D3",
"copy_sha256": "6A18EE1F246321BDF8AB0E7E8E5D4EBA134CE486ED5224C4FFD61C7342C3A7D3"
},
{
"path": "baseline-wide/rationale.arcadia",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\baseline-wide\\rationale.arcadia",
"bytes": 86,
"original_sha256": "8509F8D8A136D808CF98A1BB0FE50348B8A24409F30DEE4EE65B62BF5CB9E6BF",
"copy_sha256": "8509F8D8A136D808CF98A1BB0FE50348B8A24409F30DEE4EE65B62BF5CB9E6BF"
},
{
"path": "checkpoints/wide-passive/project.arcadia",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\checkpoints\\wide-passive\\project.arcadia",
"bytes": 20128,
"original_sha256": "6A18EE1F246321BDF8AB0E7E8E5D4EBA134CE486ED5224C4FFD61C7342C3A7D3",
"copy_sha256": "6A18EE1F246321BDF8AB0E7E8E5D4EBA134CE486ED5224C4FFD61C7342C3A7D3"
},
{
"path": "checkpoints/wide-passive/rationale.arcadia",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\checkpoints\\wide-passive\\rationale.arcadia",
"bytes": 86,
"original_sha256": "8509F8D8A136D808CF98A1BB0FE50348B8A24409F30DEE4EE65B62BF5CB9E6BF",
"copy_sha256": "8509F8D8A136D808CF98A1BB0FE50348B8A24409F30DEE4EE65B62BF5CB9E6BF"
},
{
"path": "launch-script-syntax.json",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\launch-script-syntax.json",
"bytes": 77,
"original_sha256": "9FE1369737B8A02EE9DDB83318CE800F200AFABF5486C9DD6F52DBEC9F87502C",
"copy_sha256": "9FE1369737B8A02EE9DDB83318CE800F200AFABF5486C9DD6F52DBEC9F87502C"
},
{
"path": "launch.json",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\launch.json",
"bytes": 3308,
"original_sha256": "7BCF09DFB689DB4E686BBACD0F1805B013597FB4159D5477D5DE56F52D79D147",
"copy_sha256": "7BCF09DFB689DB4E686BBACD0F1805B013597FB4159D5477D5DE56F52D79D147"
},
{
"path": "logs/studio.stderr.log",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\logs\\studio.stderr.log",
"bytes": 0,
"original_sha256": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
"copy_sha256": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855"
},
{
"path": "logs/studio.stdout.log",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\logs\\studio.stdout.log",
"bytes": 0,
"original_sha256": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
"copy_sha256": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855"
},
{
"path": "logs/wide-passive.stderr.log",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\logs\\wide-passive.stderr.log",
"bytes": 0,
"original_sha256": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
"copy_sha256": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855"
},
{
"path": "logs/wide-passive.stdout.log",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\logs\\wide-passive.stdout.log",
"bytes": 71,
"original_sha256": "72F1D2214D32F2C77CEB6666FFA91EB54795BC94E77DAF63589B00ABFEAAC477",
"copy_sha256": "72F1D2214D32F2C77CEB6666FFA91EB54795BC94E77DAF63589B00ABFEAAC477"
},
{
"path": "normal-close.json",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\normal-close.json",
"bytes": 1326,
"original_sha256": "21CE20643ED69CA1247B261A44A71A562850D0BCF8339761E60F11B7A19A7AF1",
"copy_sha256": "21CE20643ED69CA1247B261A44A71A562850D0BCF8339761E60F11B7A19A7AF1"
},
{
"path": "preparation-manifest.json",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\preparation-manifest.json",
"bytes": 9149,
"original_sha256": "293892C2F430D6EADB58C6678F3C1B346742B3AA4D2FE5CE3729F896EAD2C2DB",
"copy_sha256": "293892C2F430D6EADB58C6678F3C1B346742B3AA4D2FE5CE3729F896EAD2C2DB"
},
{
"path": "prepare-inputs.py",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\prepare-inputs.py",
"bytes": 5563,
"original_sha256": "661D6686AD0B59FBD28CB4FE42CFD4955DB5922B9CE3B5EF3F378A4CF1982884",
"copy_sha256": "661D6686AD0B59FBD28CB4FE42CFD4955DB5922B9CE3B5EF3F378A4CF1982884"
},
{
"path": "provenance/after-build-verify-head.json",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\provenance\\after-build-verify-head.json",
"bytes": 199,
"original_sha256": "FF0F533653B601F1456C726106A97D94346C3C18288912C620748112119AFF08",
"copy_sha256": "FF0F533653B601F1456C726106A97D94346C3C18288912C620748112119AFF08"
},
{
"path": "provenance/binary-manifest.json",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\provenance\\binary-manifest.json",
"bytes": 2828,
"original_sha256": "F8F8A97B2BA516685A0690E98DF90269A7C83F371A5A3257E80B865E15D3EDFA",
"copy_sha256": "F8F8A97B2BA516685A0690E98DF90269A7C83F371A5A3257E80B865E15D3EDFA"
},
{
"path": "provenance/desktop-all-targets-status.json",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\provenance\\desktop-all-targets-status.json",
"bytes": 472,
"original_sha256": "2C23196BEA849E77C49F2CF64FD31311A01998520460E92F91B935C5A7880BC0",
"copy_sha256": "2C23196BEA849E77C49F2CF64FD31311A01998520460E92F91B935C5A7880BC0"
},
{
"path": "provenance/desktop-clippy-status.json",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\provenance\\desktop-clippy-status.json",
"bytes": 503,
"original_sha256": "BA5F6D17354D194C1102EA75EE4E7B893EF04EEDD21518D591726A912E3E6A4C",
"copy_sha256": "BA5F6D17354D194C1102EA75EE4E7B893EF04EEDD21518D591726A912E3E6A4C"
},
{
"path": "provenance/focused-lifecycle-status.json",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\provenance\\focused-lifecycle-status.json",
"bytes": 509,
"original_sha256": "FA6618F15C4B41BDD691A613CF0992C83CE984A1AA0FAFFCD2DC4FB96CDEBCE3",
"copy_sha256": "FA6618F15C4B41BDD691A613CF0992C83CE984A1AA0FAFFCD2DC4FB96CDEBCE3"
},
{
"path": "provenance/inputs.json",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\provenance\\inputs.json",
"bytes": 101859,
"original_sha256": "47FEC9CADC0E7FAE99D789376C111C02EEAAB7FAC3740DE48174A06F61FE16B3",
"copy_sha256": "47FEC9CADC0E7FAE99D789376C111C02EEAAB7FAC3740DE48174A06F61FE16B3"
},
{
"path": "provenance/lifecycle-fixture-README.md",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\provenance\\lifecycle-fixture-README.md",
"bytes": 12199,
"original_sha256": "B441C562459B2AE000055D73529FB608C9CC015F64293206B9FB7D8C7CBC8B78",
"copy_sha256": "B441C562459B2AE000055D73529FB608C9CC015F64293206B9FB7D8C7CBC8B78"
},
{
"path": "provenance/lifecycle-native-contract.rs",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\provenance\\lifecycle-native-contract.rs",
"bytes": 19135,
"original_sha256": "FC5E71F9870D7B98E0F52EAF20C831EBFFBE9F7A9B8960269DC9BE3D9BF0D9E4",
"copy_sha256": "FC5E71F9870D7B98E0F52EAF20C831EBFFBE9F7A9B8960269DC9BE3D9BF0D9E4"
},
{
"path": "provenance/lifecycle-oracle-status.json",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\provenance\\lifecycle-oracle-status.json",
"bytes": 1272,
"original_sha256": "68452154CF96EC4FD245D89A11D1FC1C08CB471419A5F8D94039ECCBF2EFCF7C",
"copy_sha256": "68452154CF96EC4FD245D89A11D1FC1C08CB471419A5F8D94039ECCBF2EFCF7C"
},
{
"path": "provenance/oracles-865-copy-manifest.json",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\provenance\\oracles-865-copy-manifest.json",
"bytes": 1015,
"original_sha256": "5DC8D7A3C721E03A0A249E5B8D30BC41539B39D39EC7157D9DBF5C99044B1C4C",
"copy_sha256": "5DC8D7A3C721E03A0A249E5B8D30BC41539B39D39EC7157D9DBF5C99044B1C4C"
},
{
"path": "provenance/studio-build-status.json",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\provenance\\studio-build-status.json",
"bytes": 474,
"original_sha256": "AB1E04807F32F8DADA0A572D69F632BD6FD242150DF211C18E35D82EA8BE91EC",
"copy_sha256": "AB1E04807F32F8DADA0A572D69F632BD6FD242150DF211C18E35D82EA8BE91EC"
},
{
"path": "provenance/typography-admission.json",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\provenance\\typography-admission.json",
"bytes": 553,
"original_sha256": "B06FD83F554982B407E1B28A79DFADCB61717B4F076EB3062EC314C742A2CCD8",
"copy_sha256": "B06FD83F554982B407E1B28A79DFADCB61717B4F076EB3062EC314C742A2CCD8"
},
{
"path": "provenance/typography-predictions.json",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\provenance\\typography-predictions.json",
"bytes": 2490,
"original_sha256": "6951FF7C96C0A7A14D72781B416FC64CAB9F4AF5861F5C0EF4C327D843483243",
"copy_sha256": "6951FF7C96C0A7A14D72781B416FC64CAB9F4AF5861F5C0EF4C327D843483243"
},
{
"path": "screenshots/01-wide-opened.jpg",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\screenshots\\01-wide-opened.jpg",
"bytes": 102087,
"original_sha256": "76B5ABF72AAA2B40DB91FCF182E3B464BEE29CC9ABDCCC4005EC9567554C7E18",
"copy_sha256": "76B5ABF72AAA2B40DB91FCF182E3B464BEE29CC9ABDCCC4005EC9567554C7E18"
},
{
"path": "screenshots/02-behavior-open.jpg",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\screenshots\\02-behavior-open.jpg",
"bytes": 118609,
"original_sha256": "92D75BCF44D78221D99C393C039E6ED395AF70973477643EEF437CD0B5EFF1A2",
"copy_sha256": "92D75BCF44D78221D99C393C039E6ED395AF70973477643EEF437CD0B5EFF1A2"
},
{
"path": "screenshots/03-oa-create-selected-100.jpg",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\screenshots\\03-oa-create-selected-100.jpg",
"bytes": 134137,
"original_sha256": "0028B918C56C5686890C18D730D3394CC07A8D0714DBD5175B06A336C2BD937C",
"copy_sha256": "0028B918C56C5686890C18D730D3394CC07A8D0714DBD5175B06A336C2BD937C"
},
{
"path": "screenshots/04-oa-create-zoom125.jpg",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\screenshots\\04-oa-create-zoom125.jpg",
"bytes": 131774,
"original_sha256": "9B6627543E8A79E8D217029861407A89C884F36A3024BE44F5B8205AD2B557ED",
"copy_sha256": "9B6627543E8A79E8D217029861407A89C884F36A3024BE44F5B8205AD2B557ED"
},
{
"path": "screenshots/05-oa-delete-selected-125.jpg",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\screenshots\\05-oa-delete-selected-125.jpg",
"bytes": 132774,
"original_sha256": "1BC820418F12B18EBC97F52D2B2B85B8B9BDD57F9485BE040946FC9CC2F37FAA",
"copy_sha256": "1BC820418F12B18EBC97F52D2B2B85B8B9BDD57F9485BE040946FC9CC2F37FAA"
},
{
"path": "screenshots/06-oa-fit.jpg",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\screenshots\\06-oa-fit.jpg",
"bytes": 133100,
"original_sha256": "585063787D8E0299A7C2173BB8EC0264CDF51D5EDD8CBC3FCC953EF1917E1116",
"copy_sha256": "585063787D8E0299A7C2173BB8EC0264CDF51D5EDD8CBC3FCC953EF1917E1116"
},
{
"path": "screenshots/07-minimum-width.jpg",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\screenshots\\07-minimum-width.jpg",
"bytes": 123533,
"original_sha256": "9D798D0B087C7F05FAFDC23237BD934B069B69E1A08D8C33EAF74D4E85264582",
"copy_sha256": "9D798D0B087C7F05FAFDC23237BD934B069B69E1A08D8C33EAF74D4E85264582"
},
{
"path": "screenshots/08-minimum-window.jpg",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\screenshots\\08-minimum-window.jpg",
"bytes": 105379,
"original_sha256": "DFBAFFA375445BCD36565BDC8873DBF457E0A528A827E882F069B95D90529944",
"copy_sha256": "DFBAFFA375445BCD36565BDC8873DBF457E0A528A827E882F069B95D90529944"
},
{
"path": "screenshots/09-oa-minimum-fit.jpg",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\screenshots\\09-oa-minimum-fit.jpg",
"bytes": 99033,
"original_sha256": "FD2FEE1D602488243876B6FAEAFCBBCFE7065DDCFA2C28AD743A0FD19673C4B9",
"copy_sha256": "FD2FEE1D602488243876B6FAEAFCBBCFE7065DDCFA2C28AD743A0FD19673C4B9"
},
{
"path": "screenshots/10-oa-minimum-create-hit.jpg",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\screenshots\\10-oa-minimum-create-hit.jpg",
"bytes": 99898,
"original_sha256": "7975C21C9E41D32A8684E3420DEF616D7AC1E43F5EF291E94380FA63DA37241F",
"copy_sha256": "7975C21C9E41D32A8684E3420DEF616D7AC1E43F5EF291E94380FA63DA37241F"
},
{
"path": "screenshots/11-oa-minimum-pan-right.jpg",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\screenshots\\11-oa-minimum-pan-right.jpg",
"bytes": 99456,
"original_sha256": "61BD5C917B738E1C3B2267D623A41193A8C5F84F5A1F7B51DAFA1B147DB13A51",
"copy_sha256": "61BD5C917B738E1C3B2267D623A41193A8C5F84F5A1F7B51DAFA1B147DB13A51"
},
{
"path": "screenshots/12-oa-panned-delete-hit.jpg",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\screenshots\\12-oa-panned-delete-hit.jpg",
"bytes": 98952,
"original_sha256": "1B9C0E408BC9445AE0E6BDC2F86EA341AA385EB26D42693C76309963D9D4064C",
"copy_sha256": "1B9C0E408BC9445AE0E6BDC2F86EA341AA385EB26D42693C76309963D9D4064C"
},
{
"path": "screenshots/13-system-layer.jpg",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\screenshots\\13-system-layer.jpg",
"bytes": 101904,
"original_sha256": "02992B658775E55F0CBAF974050BBFEA0C1F608799C5D9BE9A05AA6789C9F1F4",
"copy_sha256": "02992B658775E55F0CBAF974050BBFEA0C1F608799C5D9BE9A05AA6789C9F1F4"
},
{
"path": "screenshots/14-sa-minimum-fit.jpg",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\screenshots\\14-sa-minimum-fit.jpg",
"bytes": 95469,
"original_sha256": "E4A274CA8DA7ECD2255EF519C8FC81D8310DB55C9DB9009FC8731751A25202CB",
"copy_sha256": "E4A274CA8DA7ECD2255EF519C8FC81D8310DB55C9DB9009FC8731751A25202CB"
},
{
"path": "screenshots/15-sa-minimum-create-hit.jpg",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\screenshots\\15-sa-minimum-create-hit.jpg",
"bytes": 99837,
"original_sha256": "4ED02839A417EE567CD8F23750DFFB535AF8C32063B535F8C0B4BE8ACB082542",
"copy_sha256": "4ED02839A417EE567CD8F23750DFFB535AF8C32063B535F8C0B4BE8ACB082542"
},
{
"path": "screenshots/16-sa-pan-up.jpg",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\screenshots\\16-sa-pan-up.jpg",
"bytes": 96836,
"original_sha256": "26048071F3B2A09ED054D18834C128093CC1FB9B1C4A7518B09E0F9E9EC8291A",
"copy_sha256": "26048071F3B2A09ED054D18834C128093CC1FB9B1C4A7518B09E0F9E9EC8291A"
},
{
"path": "screenshots/17-sa-panned-delete-hit.jpg",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\screenshots\\17-sa-panned-delete-hit.jpg",
"bytes": 96499,
"original_sha256": "31EA2A24959B4A5014C9FCD0E43BD918847C6FB58EB0B8B1577C56074E266BC0",
"copy_sha256": "31EA2A24959B4A5014C9FCD0E43BD918847C6FB58EB0B8B1577C56074E266BC0"
},
{
"path": "screenshots/18-passive-navigation-saved.jpg",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\screenshots\\18-passive-navigation-saved.jpg",
"bytes": 94345,
"original_sha256": "280942F9B9806884D4EE34A5FB740428A420D7603EDDDF21D6E4979BEE041B2D",
"copy_sha256": "280942F9B9806884D4EE34A5FB740428A420D7603EDDDF21D6E4979BEE041B2D"
},
{
"path": "second-readback.json",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\second-readback.json",
"bytes": 6332,
"original_sha256": "BB8063AAC3FFF3A7B8FC19BA81F9EBBC13068871F6924651A14B08D58CE525BB",
"copy_sha256": "BB8063AAC3FFF3A7B8FC19BA81F9EBBC13068871F6924651A14B08D58CE525BB"
},
{
"path": "wide-passive-status.json",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\wide-passive-status.json",
"bytes": 857,
"original_sha256": "A358FA3C26A11DEACFC9457EC96F27F43EBD9020575329E701F0F026759850BF",
"copy_sha256": "A358FA3C26A11DEACFC9457EC96F27F43EBD9020575329E701F0F026759850BF"
},
{
"path": "launch-native-a57c334-camera.ps1",
"original": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\launch-native-a57c334-camera.ps1",
"bytes": 10001,
"original_sha256": "BE80A4B5B407BCEA3DBECC547DA5E5888F3C6DC06F5F7AF87F546C388963B4DE",
"copy_sha256": "BE80A4B5B407BCEA3DBECC547DA5E5888F3C6DC06F5F7AF87F546C388963B4DE"
}
],
"excluded": [
"bin",
"oracles executables/DLL",
"baseline-clusters",
"working-wide",
"working-clusters",
"profile",
"unrelated-cwd",
"ZIP archives"
]
}
@@ -1,28 +0,0 @@
{
"source_commit": "a57c334b302eb13352b4bb65df1c75d94d002348",
"pid": 5004,
"process_started_utc": "2026-09-12T10:52:57.0962185Z",
"observed_exit_utc": "2026-09-12T11:02:56.0410831Z",
"exit_code": 0,
"scope": "Process termination and closed raw logs only; UI acceptance and reason for close require root observation.",
"stdout": {
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\logs\\studio.stdout.log",
"bytes": 0,
"sha256": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855"
},
"stderr": {
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\logs\\studio.stderr.log",
"bytes": 0,
"sha256": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855"
},
"studio_readback": {
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\bin\\arcadia-studio.exe",
"bytes": 113028608,
"sha256": "69DBB90FA299B3B3D90140C7934891F79166025893AF9B35B2B8D754A37A0D40"
},
"runtime_readback": {
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\bin\\libunwind.dll",
"bytes": 190464,
"sha256": "AA282FA47BB2C4E210CEA5E470CDBDCA58568F0BE0BC73E404D86AB9CD95EF6A"
}
}
@@ -1,166 +0,0 @@
{
"prepared_utc": "2026-09-11T21:32:45.779806+00:00",
"source_commit": "a57c334b302eb13352b4bb65df1c75d94d002348",
"scope": "Preparation only for independent OA/SA camera, clipping, minimum-size, zoom and hit checks with passive Save equality. Pre-grapheme a57; no package or new combined Unicode acceptance.",
"archive": {
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\exact-inputs.zip",
"sha256": "E2C8BB3CC8EB2BFD6ABB0A2C5769756148D594CE0CDC90449A894AB0CB026927"
},
"input_manifest_sha256": "47FEC9CADC0E7FAE99D789376C111C02EEAAB7FAC3740DE48174A06F61FE16B3",
"frozen_input_count": 529,
"files": [
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\exact-inputs.zip::harness/fixtures/scenario-lifecycle-native/typography-wide/rationale.arcadia",
"target": "baseline-wide/rationale.arcadia",
"bytes": 86,
"sha256": "8509F8D8A136D808CF98A1BB0FE50348B8A24409F30DEE4EE65B62BF5CB9E6BF"
},
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\exact-inputs.zip::harness/fixtures/scenario-lifecycle-native/typography-wide/rationale.arcadia",
"target": "working-wide/rationale.arcadia",
"bytes": 86,
"sha256": "8509F8D8A136D808CF98A1BB0FE50348B8A24409F30DEE4EE65B62BF5CB9E6BF"
},
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\exact-inputs.zip::harness/fixtures/scenario-lifecycle-native/typography-wide/project.arcadia",
"target": "baseline-wide/project.arcadia",
"bytes": 20128,
"sha256": "6A18EE1F246321BDF8AB0E7E8E5D4EBA134CE486ED5224C4FFD61C7342C3A7D3"
},
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\exact-inputs.zip::harness/fixtures/scenario-lifecycle-native/typography-wide/project.arcadia",
"target": "working-wide/project.arcadia",
"bytes": 20128,
"sha256": "6A18EE1F246321BDF8AB0E7E8E5D4EBA134CE486ED5224C4FFD61C7342C3A7D3"
},
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\exact-inputs.zip::harness/fixtures/scenario-lifecycle-native/typography-clusters/project.arcadia",
"target": "baseline-clusters/project.arcadia",
"bytes": 20110,
"sha256": "EECDDFC4A4514E995633EA58B89FE154DC6DC43509B1F53EE05B76F8E6B993C8"
},
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\exact-inputs.zip::harness/fixtures/scenario-lifecycle-native/typography-clusters/project.arcadia",
"target": "working-clusters/project.arcadia",
"bytes": 20110,
"sha256": "EECDDFC4A4514E995633EA58B89FE154DC6DC43509B1F53EE05B76F8E6B993C8"
},
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\exact-inputs.zip::harness/fixtures/scenario-lifecycle-native/typography-clusters/rationale.arcadia",
"target": "baseline-clusters/rationale.arcadia",
"bytes": 86,
"sha256": "8509F8D8A136D808CF98A1BB0FE50348B8A24409F30DEE4EE65B62BF5CB9E6BF"
},
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\exact-inputs.zip::harness/fixtures/scenario-lifecycle-native/typography-clusters/rationale.arcadia",
"target": "working-clusters/rationale.arcadia",
"bytes": 86,
"sha256": "8509F8D8A136D808CF98A1BB0FE50348B8A24409F30DEE4EE65B62BF5CB9E6BF"
},
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\exact-inputs.zip::harness/fixtures/scenario-lifecycle-native/README.md",
"target": "provenance/lifecycle-fixture-README.md",
"bytes": 12199,
"sha256": "B441C562459B2AE000055D73529FB608C9CC015F64293206B9FB7D8C7CBC8B78"
},
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\bin\\arcadia-studio.exe",
"target": "bin/arcadia-studio.exe",
"bytes": 113028608,
"sha256": "69DBB90FA299B3B3D90140C7934891F79166025893AF9B35B2B8D754A37A0D40"
},
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\bin\\libunwind.dll",
"target": "bin/libunwind.dll",
"bytes": 190464,
"sha256": "AA282FA47BB2C4E210CEA5E470CDBDCA58568F0BE0BC73E404D86AB9CD95EF6A"
},
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\inputs.json",
"target": "provenance/inputs.json",
"bytes": 101859,
"sha256": "47FEC9CADC0E7FAE99D789376C111C02EEAAB7FAC3740DE48174A06F61FE16B3"
},
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\binary-manifest.json",
"target": "provenance/binary-manifest.json",
"bytes": 2828,
"sha256": "F8F8A97B2BA516685A0690E98DF90269A7C83F371A5A3257E80B865E15D3EDFA"
},
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\after-build-verify-head.json",
"target": "provenance/after-build-verify-head.json",
"bytes": 199,
"sha256": "FF0F533653B601F1456C726106A97D94346C3C18288912C620748112119AFF08"
},
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\studio-build-status.json",
"target": "provenance/studio-build-status.json",
"bytes": 474,
"sha256": "AB1E04807F32F8DADA0A572D69F632BD6FD242150DF211C18E35D82EA8BE91EC"
},
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\desktop-clippy-status.json",
"target": "provenance/desktop-clippy-status.json",
"bytes": 503,
"sha256": "BA5F6D17354D194C1102EA75EE4E7B893EF04EEDD21518D591726A912E3E6A4C"
},
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\desktop-all-targets-status.json",
"target": "provenance/desktop-all-targets-status.json",
"bytes": 472,
"sha256": "2C23196BEA849E77C49F2CF64FD31311A01998520460E92F91B935C5A7880BC0"
},
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\focused-lifecycle-status.json",
"target": "provenance/focused-lifecycle-status.json",
"bytes": 509,
"sha256": "FA6618F15C4B41BDD691A613CF0992C83CE984A1AA0FAFFCD2DC4FB96CDEBCE3"
},
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-865483b\\oracles\\lifecycle.exe",
"target": "oracles/lifecycle.exe",
"bytes": 20700160,
"sha256": "798D7A1907B894C13549BAD35511B21D6BC7F3D67D63674042B7DF71184C7524"
},
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-865483b\\oracles\\libunwind.dll",
"target": "oracles/libunwind.dll",
"bytes": 190464,
"sha256": "AA282FA47BB2C4E210CEA5E470CDBDCA58568F0BE0BC73E404D86AB9CD95EF6A"
},
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\harness\\lifecycle-native-contract.rs",
"target": "provenance/lifecycle-native-contract.rs",
"bytes": 19135,
"sha256": "FC5E71F9870D7B98E0F52EAF20C831EBFFBE9F7A9B8960269DC9BE3D9BF0D9E4"
},
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-865483b\\oracles\\manifest.json",
"target": "provenance/oracles-865-copy-manifest.json",
"bytes": 1015,
"sha256": "5DC8D7A3C721E03A0A249E5B8D30BC41539B39D39EC7157D9DBF5C99044B1C4C"
},
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\lifecycle-native-ytr83r-20260911\\status.json",
"target": "provenance/lifecycle-oracle-status.json",
"bytes": 1272,
"sha256": "68452154CF96EC4FD245D89A11D1FC1C08CB471419A5F8D94039ECCBF2EFCF7C"
},
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\harness\\experiments\\lifecycle-native-ytr83r-20260911\\typography-admission.json",
"target": "provenance/typography-admission.json",
"bytes": 553,
"sha256": "B06FD83F554982B407E1B28A79DFADCB61717B4F076EB3062EC314C742A2CCD8"
},
{
"origin": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\harness\\experiments\\lifecycle-native-ytr83r-20260911\\typography-predictions.json",
"target": "provenance/typography-predictions.json",
"bytes": 2490,
"sha256": "6951FF7C96C0A7A14D72781B416FC64CAB9F4AF5861F5C0EF4C327D843483243"
}
],
"empty_profile": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\profile",
"unrelated_cwd": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\unrelated-cwd",
"restricted_path": "C:\\Windows\\System32;C:\\Windows;C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-a57c334-camera\\bin"
}
@@ -1,88 +0,0 @@
"""Preparation only: copy and hash frozen inputs; never execute model/oracle/GUI code."""
import hashlib
import json
import shutil
import zipfile
from datetime import datetime, timezone
from pathlib import Path
root = Path(__file__).resolve().parent
repo = root.parent.parent
gates = repo / '.local/canvas-union-windows-20260911'
freeze = 'a57c334b302eb13352b4bb65df1c75d94d002348'
def digest(data):
return hashlib.sha256(data).hexdigest().upper()
def write_new(path, data):
path.parent.mkdir(parents=True, exist_ok=True)
with path.open('xb') as out:
out.write(data)
records = []
def copy_exact(source, target, expected=None):
source = Path(source)
data = source.read_bytes()
sha = digest(data)
if expected:
assert sha == expected.upper(), (source, sha, expected)
destination = root / target
write_new(destination, data)
assert destination.read_bytes() == data == source.read_bytes()
records.append(dict(origin=str(source), target=target, bytes=len(data), sha256=sha))
assert not (root / 'preparation-manifest.json').exists(), 'Never overwrite a prior preparation'
manifest_bytes = (gates / 'inputs.json').read_bytes()
assert digest(manifest_bytes) == '47FEC9CADC0E7FAE99D789376C111C02EEAAB7FAC3740DE48174A06F61FE16B3'
inputs = json.loads(manifest_bytes)
indexed = {item['path']: item for item in inputs['files']}
assert len(indexed) == 529
archive = gates / 'exact-inputs.zip'
archive_sha = digest(archive.read_bytes())
with zipfile.ZipFile(archive) as frozen:
selected = {
'harness/fixtures/scenario-lifecycle-native/typography-wide/rationale.arcadia': ('baseline-wide/rationale.arcadia', 'working-wide/rationale.arcadia'),
'harness/fixtures/scenario-lifecycle-native/typography-wide/project.arcadia': ('baseline-wide/project.arcadia', 'working-wide/project.arcadia'),
'harness/fixtures/scenario-lifecycle-native/typography-clusters/project.arcadia': ('baseline-clusters/project.arcadia', 'working-clusters/project.arcadia'),
'harness/fixtures/scenario-lifecycle-native/typography-clusters/rationale.arcadia': ('baseline-clusters/rationale.arcadia', 'working-clusters/rationale.arcadia'),
'harness/fixtures/scenario-lifecycle-native/README.md': ('provenance/lifecycle-fixture-README.md',),
}
for name, destinations in selected.items():
data = frozen.read(name)
record = indexed[name]
assert len(data) == record['bytes'] and digest(data) == record['sha256'].upper(), name
for destination in destinations:
write_new(root / destination, data)
records.append(dict(origin=str(archive) + '::' + name, target=destination, bytes=len(data), sha256=digest(data)))
copy_exact(gates / 'bin/arcadia-studio.exe', 'bin/arcadia-studio.exe', '69DBB90FA299B3B3D90140C7934891F79166025893AF9B35B2B8D754A37A0D40')
copy_exact(gates / 'bin/libunwind.dll', 'bin/libunwind.dll', 'AA282FA47BB2C4E210CEA5E470CDBDCA58568F0BE0BC73E404D86AB9CD95EF6A')
for name in ('inputs.json', 'binary-manifest.json', 'after-build-verify-head.json', 'studio-build-status.json', 'desktop-clippy-status.json', 'desktop-all-targets-status.json', 'focused-lifecycle-status.json'):
copy_exact(gates / name, 'provenance/' + name)
for source, target, expected in (
('.local/native-865483b/oracles/lifecycle.exe', 'oracles/lifecycle.exe', '798D7A1907B894C13549BAD35511B21D6BC7F3D67D63674042B7DF71184C7524'),
('.local/native-865483b/oracles/libunwind.dll', 'oracles/libunwind.dll', 'AA282FA47BB2C4E210CEA5E470CDBDCA58568F0BE0BC73E404D86AB9CD95EF6A'),
('harness/lifecycle-native-contract.rs', 'provenance/lifecycle-native-contract.rs', 'FC5E71F9870D7B98E0F52EAF20C831EBFFBE9F7A9B8960269DC9BE3D9BF0D9E4'),
):
copy_exact(repo / source, target, expected)
for source, target in (
('.local/native-865483b/oracles/manifest.json', 'provenance/oracles-865-copy-manifest.json'),
('.local/lifecycle-native-ytr83r-20260911/status.json', 'provenance/lifecycle-oracle-status.json'),
('harness/experiments/lifecycle-native-ytr83r-20260911/typography-admission.json', 'provenance/typography-admission.json'),
('harness/experiments/lifecycle-native-ytr83r-20260911/typography-predictions.json', 'provenance/typography-predictions.json'),
):
copy_exact(repo / source, target)
for directory in ('profile', 'unrelated-cwd', 'logs', 'screenshots', 'checkpoints'):
(root / directory).mkdir(exist_ok=False)
for record in records:
if record['target'].startswith(('baseline-', 'bin/', 'oracles/', 'provenance/')):
(root / record['target']).chmod(0o444)
result = dict(prepared_utc=datetime.now(timezone.utc).isoformat(), source_commit=freeze,
scope='Preparation only for independent OA/SA camera, clipping, minimum-size, zoom and hit checks with passive Save equality. Pre-grapheme a57; no package or new combined Unicode acceptance.',
archive=dict(path=str(archive), sha256=archive_sha), input_manifest_sha256=digest(manifest_bytes),
frozen_input_count=529, files=records,
empty_profile=str(root / 'profile'), unrelated_cwd=str(root / 'unrelated-cwd'),
restricted_path='C:\\Windows\\System32;C:\\Windows;' + str(root / 'bin'))
write_new(root / 'preparation-manifest.json', (json.dumps(result, indent=2) + '\n').encode())
print(json.dumps(dict(files=len(records), copied_bytes=sum(r['bytes'] for r in records), archive_sha256=archive_sha), indent=2))
@@ -1 +0,0 @@
{"mode": "verify-head", "utc": "2026-09-11T20:20:58.775988+00:00", "files": 529, "bytes": 9712849, "manifest_sha256": "47fec9cadc0e7fae99d789376c111c02eeaab7fac3740de48174a06f61fe16b3", "pass": true}
@@ -1,67 +0,0 @@
{
"worktree": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-ytr83r-20260911",
"scope": "Windows source/build gates and immutable copies only; no native launch or package acceptance",
"source_commit": "a57c334 (root-reported; all 529 frozen inputs independently verified against worktree HEAD)",
"verified_inputs": 529,
"input_manifest_sha256": "47fec9cadc0e7fae99d789376c111c02eeaab7fac3740de48174a06f61fe16b3",
"binaries": [
{
"read_only": true,
"copy": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\bin\\arcadia-studio.exe",
"copy_sha256": "69DBB90FA299B3B3D90140C7934891F79166025893AF9B35B2B8D754A37A0D40",
"source_after_sha256": "69DBB90FA299B3B3D90140C7934891F79166025893AF9B35B2B8D754A37A0D40",
"source": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\target\\debug\\arcadia-studio.exe",
"bytes": 113028608,
"source_sha256": "69DBB90FA299B3B3D90140C7934891F79166025893AF9B35B2B8D754A37A0D40"
},
{
"read_only": true,
"copy": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\bin\\libunwind.dll",
"copy_sha256": "AA282FA47BB2C4E210CEA5E470CDBDCA58568F0BE0BC73E404D86AB9CD95EF6A",
"source_after_sha256": "AA282FA47BB2C4E210CEA5E470CDBDCA58568F0BE0BC73E404D86AB9CD95EF6A",
"source": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\llvm-mingw-20260908-ucrt-x86_64\\bin\\libunwind.dll",
"bytes": 190464,
"source_sha256": "AA282FA47BB2C4E210CEA5E470CDBDCA58568F0BE0BC73E404D86AB9CD95EF6A"
}
],
"clippy": {
"command": [
"cargo",
"clippy",
"-p",
"oa-desktop",
"--all-targets",
"--offline",
"--locked",
"--",
"-D",
"warnings"
],
"completed_utc": "2026-09-11T20:13:49.0390005Z",
"name": "canvas-union-windows-20260911/desktop-clippy",
"exit": 0,
"log": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\desktop-clippy.log",
"started_utc": "2026-09-11T20:08:57.0977732Z",
"seconds": 291.9567566
},
"build": {
"seconds": 385.1131455,
"log": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\studio-build.log",
"started_utc": "2026-09-11T20:14:23.6348444Z",
"exit": 0,
"command": [
"cargo",
"build",
"-p",
"oa-desktop",
"--bin",
"arcadia-studio",
"--offline",
"--locked"
],
"name": "canvas-union-windows-20260911/studio-build",
"completed_utc": "2026-09-11T20:20:48.7677661Z"
},
"verified_input_bytes": 9712849,
"preserved_utc": "2026-09-11T20:21:10.0070190Z"
}
@@ -1,17 +0,0 @@
{
"log": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\desktop-all-targets.log",
"completed_utc": "2026-09-11T20:06:06.1170595Z",
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"--all-targets",
"--offline",
"--locked"
],
"seconds": 565.0596711,
"name": "canvas-union-windows-20260911/desktop-all-targets",
"exit": 0,
"started_utc": "2026-09-11T19:56:40.8624918Z"
}
@@ -1,20 +0,0 @@
{
"command": [
"cargo",
"clippy",
"-p",
"oa-desktop",
"--all-targets",
"--offline",
"--locked",
"--",
"-D",
"warnings"
],
"completed_utc": "2026-09-11T20:13:49.0390005Z",
"name": "canvas-union-windows-20260911/desktop-clippy",
"exit": 0,
"log": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\desktop-clippy.log",
"started_utc": "2026-09-11T20:08:57.0977732Z",
"seconds": 291.9567566
}
@@ -1,19 +0,0 @@
{
"log": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\focused-lifecycle.log",
"completed_utc": "2026-09-11T19:55:46.4531913Z",
"started_utc": "2026-09-11T19:42:20.3858323Z",
"seconds": 806.0877483,
"name": "canvas-union-windows-20260911/focused-lifecycle",
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"--offline",
"--locked",
"scenario_lifecycle",
"--",
"--test-threads=1"
],
"exit": 0
}
File diff suppressed because it is too large Load Diff
@@ -1,163 +0,0 @@
# Independent native lifecycle fixture — 2026-09-11
Owner: Codex-ytr83r /root/integration_audit. **Core/source admission passed; native
execution remains pending.** Root compiled the checker, passed its self-check and
generated/checked all three immutable expectations before opening Studio. No native
or Scene acceptance is claimed here. The renderer implementation is independently
owned by /root/server_audit; this oracle does not import any desktop module.
## Build and source admission
Compile `harness/lifecycle-native-contract.rs` with the matching `oa_core` rlib and
dependency search directory, Rust2024 and warnings denied. No separate serde_json
dependency is needed. Root owns the writer/toolchain lease. Example shape:
```text
rustc --edition=2024 -D warnings harness/lifecycle-native-contract.rs --extern oa_core=ABSOLUTE_MATCHING_RLIB -L dependency=ABSOLUTE_DEPS -o ABSOLUTE_ISOLATED_EXE
lifecycle-native-contract.exe self-check
lifecycle-native-contract.exe inventory
lifecycle-native-contract.exe generate baseline FRESH_BASE_DIR
lifecycle-native-contract.exe check baseline FRESH_BASE_DIR/project.arcadia
lifecycle-native-contract.exe generate oa-rename FRESH_EXPECTED_OA_DIR
lifecycle-native-contract.exe generate sa-rename FRESH_EXPECTED_SA_DIR
```
Keep logs, screenshot files, hashes and model exports **outside** each checkpoint
directory. A checkpoint contains only its complete saved project files; the checker
compares every physical file, not just files with `.arcadia` extension. Generated
base is two sources: canonical project plus an intentionally CRLF Unicode note.
The two expected rename directories must be generated before native output is read.
All element/relation IDs are fixed; SA port UUIDv5 values derive from the fixed
functional-exchange ID through the authoritative port planner. No observed identity,
geometry, order, reference or name is borrowed into predictions.
The unchecked fixture builder is only preparation: final `execute_batch`, source
compile and load/recompile equality are mandatory. Self-Delete and unassigned
lifeline cases intentionally use explicit local sender/receiver with `exchange=None`.
If current core refuses either, record the exact refusal and revise or separate that
case under root review; do not bypass admission or silently remove validation.
Self-Create is deliberately absent because its admission was not established.
Expected unfinished-model warnings (unbound exchange/representation, unanswered
Call) are diagnostic evidence, not permission to bypass errors. Capture the actual
warning set after source admission.
## Fixed identities and navigation
All numeric offsets below are **decimal** added to UUID integer
`5ceab400-0000-4000-8000-000000000000`. Project is offset1. Add side base1000 for OA
or2000 for SA, then the listed local offset. Names begin OA/SA for explorer search.
Each scenario has at most two lanes and explicit OperationalActivity/Functional
viewpoint; no hidden legacy scenario mode is used.
| Scenario local offset/name | Lanes | Occurrences and declared order |
| --- | --- | --- |
|100 `01 Root create action delete`|101 controller,102 receiver|110 Create receiver →111 Receiver action →112 Delete receiver|
|200 `02 Preexisting empty`|201 controller,202 receiver|Empty root|
|300 `03 Create only`|301 controller,302 receiver|310 Create only receiver|
|400 `04 Conditional delete`|401 controller,402 receiver|410 Open call →411 ALT;412 left operand contains414 Conditional delete;413 right operand empty|
|500 `05 Unassigned receiver`|501 controller,502 represented=None|510 Create unassigned receiver →511 Delete unassigned receiver; both exchanges unresolved|
|600 `06 Self delete`|601 controller only|610 Self delete controller; sender=receiver601, exchange unresolved|
Controller/receiver work are local10/11; ordinary service relation is12. Root
Delete rename target is side+112. The exact new name is `OA Delete receiver · reviewed`
or `SA Delete receiver · reviewed`. Aliases, IDs and every other field stay unchanged.
For direct selection, OA root scenario is `5ceab400-0000-4000-8000-00000000044c`
and OA Delete is `5ceab400-0000-4000-8000-000000000458`; SA root scenario is
`5ceab400-0000-4000-8000-000000000834` and SA Delete is
`5ceab400-0000-4000-8000-000000000840`. `inventory` emits the complete admitted
scenario/lifeline/occurrence ID/name/parent table and actual validation diagnostics.
## Native procedure and independent observations
Use a copied working directory and isolated profile with an immutable matching GUI.
Record source commit, executable SHA256, actual client dimensions, input sequence,
screenshots and saved checkpoints outside the project directories. Open the actual
`project.arcadia` through Studio. Select Behavior & scenarios, the matching layer,
and the exact prefixed scenario. First run the OA matrix; repeat SA independently
from the same baseline. Neither a passing source checker nor pure Scene tests imply
the following visual/interaction results.
| Step | Native observation/evidence required | Semantic oracle |
| --- | --- | --- |
|Root lifecycle|Controller remains usable; receiver lifetime begins at Create and ends at Delete; Action lies between. Inspect birth/death hit targets and corresponding Properties identity. Clicking an intended lifecycle target must not select a different occurrence.|Save copied baseline; `check baseline`|
|Preexisting empty|Both lane headers/guides visible through the actual empty canvas; no invented birth/death. Neutral insertion guide remains usable even when no occurrence exists.|Save/navigation must remain `baseline`|
|Create-only|Receiver lifetime continues beyond the last occurrence to visible root extent; do not confuse insertion endpoint with lifetime endpoint.|`check baseline`|
|Conditional Delete|Left receiver activation/lifetime ends at its Delete. Right empty branch retains independent receiver state; Delete must not terminate sibling activation. Inspect unresolved-call indication without inventing Reply or silently rewriting the model.|`check baseline`|
|Unassigned receiver|Incomplete lane remains visible, with explicit receiver lifecycle targets/hit geometry. It must not disappear or crash because placement eligibility excludes unresolved representation. Record whatever completeness diagnostic is actually shown.|`check baseline`; only after admitted|
|Self Delete|Single lane's self-message end and death marker align and remain selectable. Inspect Properties and hit targets at ordinary100% then Fit; do not infer self-Create behavior.|`check baseline`; only after admitted|
|Narrow/overview|At actual1100×700 and a wide client, exercise Fit,100%, scroll, selection and Tools. Guides/markers remain correctly aligned and hit-testable; record font wrapping/overlap and clipped regions.|Save unchanged complete baseline|
|Real rename|Select root Delete occurrence by actual marker/message, verify identity, edit Name to exact side-specific reviewed name and Apply, or make precisely that source name edit and Apply. Save once; preserve complete checkpoint before Undo.|`check oa-rename` or `check sa-rename`; exact previously generated expectation via `same`|
|Undo|One Undo restores old name, exact IDs and complete source. Save and freeze checkpoint.|`check baseline UNDO_ENTRY RENAMED_ENTRY`, then `same BASE_ENTRY UNDO_ENTRY RENAMED_ENTRY`|
|Redo/reopen|One Redo and Save restores exact first rename checkpoint. Reopen through actual file picker and Save again. Observe lifecycle geometry/selection afresh.|`check SIDE-rename`, `same FIRST_RENAME REDO`, `same FIRST_RENAME REOPEN`|
Each rename mode starts from baseline and changes only its own layer. Undo OA before
starting SA or use a fresh baseline copy; `sa-rename` does not include an earlier OA
rename. Direct additional scenario insertion is outside these fixed rename modes:
define a separate prediction before using it as acceptance evidence.
## Exact physical-file and Undo policy
`check MODE ENTRY` independently constructs complete expected Project and reachable
SourceBundle, then requires the physical file map to equal those expected sources.
`check MODE ENTRY PRE_UNDO_ENTRY` additionally permits only the exact original files
present in the explicitly supplied pre-Undo checkpoint. Expected reachable bytes
overlay those origin files; missing, unknown or changed retained files fail. No
automatic disk-equals-reachable assertion is imposed on a valid Undo with retained
files. The explicit origin must be a separately frozen, previously verified accepted
checkpoint; do not supply a directory manufactured after seeing an unexpected file.
`same EXPECTED ACTUAL` pins all identities/model/source and the complete saved physical
map of a previously verified checkpoint. Its optional pre-Undo origin uses the same
exact retention rule. It never accepts source drift just because decoded models agree.
These rename cases normally create no detached files, but the checker explicitly
self-tests missing-origin, unknown/missing/changed retained-file discrimination.
## Status
Root execution completed on 2026-09-11 at12:41:29 UTC: direct Rust2024 compile with
warnings denied (23.439s), self-check, inventory, and generate/check of baseline,
OA rename and SA rename all passed. Total62.428s. The exact core tree is
`a9c71e2c3fc9260d06f191c0d65b1e63e41edbec`; its compiled rlib SHA256 is
`D282FEE7FF3B4FDD159EA85B12430262C8DA00E064BD290CCED380B6BA2335B5`.
The oracle executable is
`798D7A1907B894C13549BAD35511B21D6BC7F3D67D63674042B7DF71184C7524`.
Exact logs, execution metadata and all six pre-native fixture hashes are retained
under [lifecycle-native-ytr83r-20260911](../../experiments/lifecycle-native-ytr83r-20260911/).
An independent read-only check confirmed64 elements/2 relations per Project,
including12 scenarios/22 lifelines/24 occurrences. Both incomplete receiver and
self-Delete cases were admitted. There are19 warnings and no errors:
missing_system1, unallocated_leaf4, empty_sequence2, call_without_operation2,
incomplete_reference2, missing_exchange6 and empty_port_contract2. These describe
the intentionally partial engineering fixture; no validation was bypassed.
Each rename changes exactly one quoted name and adds12 UTF-8 bytes; every other
source byte and identity is unchanged. All three rationale files remain identical
86-byte Unicode text with exactly two CRLF lines and no bare LF. None of these
predictions used native outputs. Pending: matching GUI, visual/interaction checks
and every native Save/Undo/Redo/reopen step above.
## Separate Unicode display predictions — after reboot
`typography-wide` changes only the OA root receiver Lifeline name to eighteen
`W` characters and the SA root receiver name to eighteen `界` characters.
`typography-clusters` instead uses a combining accent and a joined emoji near
the existing display cutoff. Both derive from the original baseline by replacing
exactly the two named Lifeline declaration name tokens; reverse replacement
reproduced every original byte. All six original fixture hashes remain unchanged.
The expected full strings, IDs, byte sizes and SHA256 values are preserved in
`../../experiments/lifecycle-native-ytr83r-20260911/typography-predictions.json`.
The unchanged, hash-verified original oracle admitted both inputs through its
`same EXPECTED EXPECTED` path using ordinary core/source loading and validation.
This proves input admission only; it is not a native comparison. Exact logs and
timings are alongside the manifest in `typography-admission.json`.
Before opening either variant in Studio, copy it to a fresh native working
directory. Compare every passive open/zoom/select/Save result with the immutable
expected variant using `same EXPECTED ACTUAL`. Inspect Create label containment
at 0.5x/1x/2x and minimum Fit, while retaining the original Create selection IDs.
Select the neutral lane header/guide to inspect its full Lifeline name, then
cancel property editing without changing it. Missing font glyphs are observations
to record, not a reason to change the source names or weaken containment checks.
No native output or pixel clipping acceptance is claimed yet.
@@ -1,570 +0,0 @@
//! Independent B4 native source fixture/oracle. No desktop renderer is linked.
//! generate MODE DIR | check MODE ENTRY [PRE_UNDO_ENTRY] | same EXPECTED ACTUAL [PRE_UNDO_ENTRY]
//! self-check | inventory; modes: baseline, oa-rename, sa-rename. All fixture identities are fixed.
use oa_core::{source_project::*, *};
use std::{collections::BTreeMap, fs, path::Path};
type Disk = BTreeMap<String, Vec<u8>>;
fn id(n: u128) -> Id {
Id::from_u128(0x5ceab400000040008000000000000000 + n)
}
#[derive(Clone, Copy)]
struct Side {
layer: Layer,
word: &'static str,
base: u128,
}
const SIDES: [Side; 2] = [
Side {
layer: Layer::Oa,
word: "OA",
base: 1000,
},
Side {
layer: Layer::Sa,
word: "SA",
base: 2000,
},
];
impl Side {
fn id(self, n: u128) -> Id {
id(self.base + n)
}
fn name(self, text: &str) -> String {
format!("{} {text}", self.word)
}
}
fn element(p: &mut Project, side: Side, n: u128, kind: Kind, name: &str, parent: Option<u128>) {
let mut e = p
.make_element(
kind,
side.layer,
&side.name(name),
parent.map(|n| side.id(n)),
)
.unwrap();
e.id = side.id(n);
e.alias = format!("life_{}_{}", side.word.to_lowercase(), n);
p.elements.insert(e.id, e);
}
fn scenario(p: &mut Project, side: Side, n: u128, name: &str) {
element(p, side, n, Kind::Scenario, name, None);
let Some(SharedBody::Scenario { viewpoint, .. }) =
p.elements.get_mut(&side.id(n)).unwrap().shared_mut()
else {
unreachable!()
};
*viewpoint = if side.layer == Layer::Oa {
ScenarioViewpoint::OperationalActivity
} else {
ScenarioViewpoint::Functional
};
}
fn line(p: &mut Project, side: Side, n: u128, owner: u128, work: Option<u128>, name: &str) {
element(p, side, n, Kind::Lifeline, name, Some(owner));
*p.elements
.get_mut(&side.id(n))
.unwrap()
.shared_mut()
.unwrap() = SharedBody::Lifeline {
represented: work.map(|n| side.id(n)),
};
}
fn occurrence(
p: &mut Project,
side: Side,
n: u128,
parent: u128,
name: &str,
body: OccurrenceBody,
) {
element(p, side, n, Kind::Occurrence, name, Some(parent));
*p.elements
.get_mut(&side.id(n))
.unwrap()
.shared_mut()
.unwrap() = SharedBody::Occurrence {
body,
precondition: String::new(),
postcondition: String::new(),
};
}
fn steps(p: &mut Project, side: Side, owner: u128, ordered: &[u128]) {
*p.elements
.get_mut(&side.id(owner))
.unwrap()
.steps_mut()
.unwrap() = ordered.iter().map(|n| side.id(*n)).collect();
}
fn message(
side: Side,
sender: u128,
receiver: u128,
kind: MessageKind,
resolved: bool,
) -> OccurrenceBody {
OccurrenceBody::Exchange {
exchange: resolved.then(|| side.id(12)),
sender: Some(side.id(sender)),
receiver: Some(side.id(receiver)),
message_kind: kind,
operation: None,
reply_to: None,
values: vec![],
}
}
fn fixture() -> Project {
let mut p = Project::empty();
p.id = id(1);
p.alias = "lifecycle_native".into();
p.name = "Lifecycle native contract".into();
p.description =
"Fixed semantic inputs; lifecycle geometry is derived and never authored.".into();
for side in SIDES {
let work = if side.layer == Layer::Oa {
Kind::Activity
} else {
Kind::Function
};
element(&mut p, side, 10, work, "Controller work", None);
element(&mut p, side, 11, work, "Receiver work", None);
if side.layer == Layer::Oa {
let relation = Relation {
id: side.id(12),
alias: "life_oa_service".into(),
name: "OA Lifecycle service".into(),
description: String::new(),
body: RelationBody::new(RelationKind::Interaction, side.id(10), side.id(11)),
};
p.relations.insert(relation.id, relation);
} else {
// The port planner derives deterministic UUIDv5 endpoints from this fixed exchange ID.
let commands = ports::plan_functional_exchange(
&p,
&ports::FunctionalExchangeRequest {
id: side.id(12),
alias: "life_sa_service".into(),
name: "SA Lifecycle service".into(),
description: String::new(),
source: side.id(10),
target: side.id(11),
source_port: ports::PortChoice::Create,
target_port: ports::PortChoice::Create,
items: vec![],
},
)
.expect("typed fixed-ID functional exchange must be admitted");
p = execute_batch(&p, &commands).unwrap();
}
for (n, name) in [
(100, "01 Root create action delete"),
(200, "02 Preexisting empty"),
(300, "03 Create only"),
(400, "04 Conditional delete"),
(500, "05 Unassigned receiver"),
(600, "06 Self delete"),
] {
scenario(&mut p, side, n, name);
line(&mut p, side, n + 1, n, Some(10), "Controller lane");
if n != 600 {
line(
&mut p,
side,
n + 2,
n,
(n != 500).then_some(11),
if n == 500 {
"Unassigned receiver lane"
} else {
"Receiver lane"
},
);
}
}
occurrence(
&mut p,
side,
110,
100,
"Create receiver",
message(side, 101, 102, MessageKind::Create, true),
);
occurrence(
&mut p,
side,
111,
100,
"Receiver action",
OccurrenceBody::Action {
action: Some(side.id(11)),
lifeline: Some(side.id(102)),
},
);
occurrence(
&mut p,
side,
112,
100,
"Delete receiver",
message(side, 101, 102, MessageKind::Delete, true),
);
steps(&mut p, side, 100, &[110, 111, 112]);
occurrence(
&mut p,
side,
310,
300,
"Create only receiver",
message(side, 301, 302, MessageKind::Create, true),
);
steps(&mut p, side, 300, &[310]);
occurrence(
&mut p,
side,
410,
400,
"Open call",
message(side, 401, 402, MessageKind::Call, true),
);
occurrence(
&mut p,
side,
411,
400,
"ALT delete or keep",
OccurrenceBody::Fragment {
operator: FragmentOperator::Alternative,
branches: vec![side.id(412), side.id(413)],
},
);
occurrence(
&mut p,
side,
412,
411,
"Left delete operand",
OccurrenceBody::Branch {
guard: "stop_requested".into(),
occurrences: vec![side.id(414)],
},
);
occurrence(
&mut p,
side,
413,
411,
"Right empty operand",
OccurrenceBody::Branch {
guard: "else".into(),
occurrences: vec![],
},
);
occurrence(
&mut p,
side,
414,
412,
"Conditional delete receiver",
message(side, 401, 402, MessageKind::Delete, true),
);
steps(&mut p, side, 400, &[410, 411]);
occurrence(
&mut p,
side,
510,
500,
"Create unassigned receiver",
message(side, 501, 502, MessageKind::Create, false),
);
occurrence(
&mut p,
side,
511,
500,
"Delete unassigned receiver",
message(side, 501, 502, MessageKind::Delete, false),
);
steps(&mut p, side, 500, &[510, 511]);
occurrence(
&mut p,
side,
610,
600,
"Self delete controller",
message(side, 601, 601, MessageKind::Delete, false),
);
steps(&mut p, side, 600, &[610]);
}
execute_batch(&p, &[]).expect("complete source fixture must satisfy ordinary core admission; never bypass self/incomplete-lane rejection")
}
fn baseline() -> CompiledSourceProject {
let p = fixture();
let entry = language::format(&p).replacen("{\n", "{\n include \"rationale.arcadia\";\n", 1);
let raw = SourceBundle { entry: "project.arcadia".into(), files: BTreeMap::from([
("project.arcadia".into(), entry),
("rationale.arcadia".into(), "// Preserve lifecycle note: 雨 · receiver.\r\n// Geometry never changes these bytes.\r\n".into()),
]) };
let compiled = compile_bundle(&raw, ProjectLimits::default()).unwrap();
assert_eq!(compiled.decoded.project, p);
assert_eq!(
compiled.bundle, raw,
"no identity insertion, normalization or migration"
);
compiled
}
fn expected(mode: &str) -> CompiledSourceProject {
let base = baseline();
let side = match mode {
"baseline" => return base,
"oa-rename" => SIDES[0],
"sa-rename" => SIDES[1],
_ => panic!("mode: baseline|oa-rename|sa-rename"),
};
let next = execute_batch(
&base.decoded.project,
&[Command::Rename(
side.id(112),
side.name("Delete receiver · reviewed"),
)],
)
.unwrap();
update_bundle(&base, &next, ProjectLimits::default()).unwrap()
}
fn disk(root: &Path, here: &Path, out: &mut Disk) {
for entry in fs::read_dir(here).unwrap() {
let entry = entry.unwrap();
let kind = entry.file_type().unwrap();
assert!(!kind.is_symlink(), "checkpoint cannot contain symlinks");
if kind.is_dir() {
disk(root, &entry.path(), out);
} else {
assert!(kind.is_file(), "checkpoint files must be ordinary files");
let path = entry.path();
let name = path
.strip_prefix(root)
.unwrap()
.to_str()
.unwrap()
.replace('\\', "/");
out.insert(name, fs::read(path).unwrap());
}
}
}
struct Snapshot {
compiled: CompiledSourceProject,
disk: Disk,
}
fn load(entry: &Path) -> Snapshot {
assert_eq!(entry.file_name().unwrap(), "project.arcadia");
let raw = source_files::load_project(entry, ProjectLimits::default())
.unwrap()
.reachable_bundle();
let compiled = compile_bundle(&raw, ProjectLimits::default()).unwrap();
assert_eq!(compiled.bundle, raw, "no implicit source normalization");
execute_batch(&compiled.decoded.project, &[]).unwrap();
let mut files = Disk::new();
let root = entry.parent().unwrap();
disk(root, root, &mut files);
Snapshot {
compiled,
disk: files,
}
}
fn source_disk(bundle: &SourceBundle) -> Disk {
bundle
.files
.iter()
.map(|(name, text)| (name.clone(), text.as_bytes().to_vec()))
.collect()
}
fn expected_disk(bundle: &SourceBundle, origin: Option<&Disk>) -> Disk {
let mut files = origin.cloned().unwrap_or_default();
files.extend(source_disk(bundle));
files
}
fn equal(expected: &CompiledSourceProject, actual: &Snapshot, origin: Option<&Disk>) {
assert_eq!(
actual.compiled.decoded.project, expected.decoded.project,
"whole Project: every identity, field, order, reference and geometry"
);
assert_eq!(
actual.compiled.bundle, expected.bundle,
"whole reachable SourceBundle including retained note bytes"
);
assert_eq!(
actual.disk,
expected_disk(&expected.bundle, origin),
"whole physical file map: only exact pre-Undo retained files are permitted"
);
}
fn self_check() {
let base = baseline();
assert_eq!(
base.decoded.project,
fixture(),
"fixture deterministic, including typed ports"
);
assert_eq!(base.bundle, baseline().bundle);
for mode in ["baseline", "oa-rename", "sa-rename"] {
let prediction = expected(mode);
let actual = Snapshot {
disk: source_disk(&prediction.bundle),
compiled: expected(mode),
};
equal(&prediction, &actual, None);
if mode != "baseline" {
assert_ne!(prediction.decoded.project, base.decoded.project);
assert_ne!(prediction.bundle, base.bundle);
let side = if mode == "oa-rename" {
SIDES[0]
} else {
SIDES[1]
};
let mut independently_named = base.decoded.project.clone();
independently_named
.elements
.get_mut(&side.id(112))
.unwrap()
.name = side.name("Delete receiver · reviewed");
assert_eq!(
prediction.decoded.project, independently_named,
"rename changes precisely one name; alias and identity stay fixed"
);
}
let mut changed_project = prediction.decoded.project.clone();
changed_project.description.push_str("unrequested");
assert_ne!(changed_project, prediction.decoded.project);
let mut changed_source = prediction.bundle.clone();
changed_source
.files
.get_mut("rationale.arcadia")
.unwrap()
.push_str("// unrequested\n");
assert_ne!(changed_source, prediction.bundle);
}
let plain = source_disk(&base.bundle);
let mut origin = plain.clone();
origin.insert(
"detached.arcadia".into(),
b"// exact pre-Undo bytes\r\n".to_vec(),
);
let retained = expected_disk(&base.bundle, Some(&origin));
assert_eq!(retained, origin);
assert_ne!(
retained,
expected_disk(&base.bundle, None),
"origin is mandatory to authorize retained files"
);
let mut missing = retained.clone();
missing.remove("detached.arcadia");
assert_ne!(missing, retained);
let mut changed = retained.clone();
changed.get_mut("detached.arcadia").unwrap().push(b'!');
assert_ne!(changed, retained);
let mut unknown = retained.clone();
unknown.insert("unknown.arcadia".into(), vec![]);
assert_ne!(unknown, retained);
let mut wrong_reachable = origin.clone();
wrong_reachable.insert("rationale.arcadia".into(), b"wrong prior contents".to_vec());
assert_eq!(
expected_disk(&base.bundle, Some(&wrong_reachable)),
retained,
"expected reachable bytes override the origin"
);
println!(
"PASS self-check: fixed OA/SA fixture, exact name-only predictions, explicit retained-file origin policy"
);
}
fn main() {
assert_eq!(SCHEMA_VERSION, 7);
let args: Vec<_> = std::env::args().collect();
match args.get(1).map(String::as_str) {
Some("self-check") => {
assert_eq!(args.len(), 2);
self_check();
}
Some("inventory") => {
assert_eq!(args.len(), 2);
let base = baseline();
for e in
base.decoded.project.elements.values().filter(|e| {
matches!(e.kind(), Kind::Scenario | Kind::Lifeline | Kind::Occurrence)
})
{
println!(
"{} | {:?} | {} | parent={:?}",
e.id,
e.kind(),
e.name,
e.parent
);
}
for issue in validation::validate(&base.decoded.project) {
println!("DIAGNOSTIC {issue:?}");
}
}
Some("generate") => {
assert_eq!(args.len(), 4, "generate MODE FRESH_DIR");
let prediction = expected(&args[2]);
let dir = Path::new(&args[3]);
assert!(!dir.exists(), "preserve existing fixture/evidence");
fs::create_dir_all(dir).unwrap();
for (name, text) in &prediction.bundle.files {
let path = dir.join(name);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, text.as_bytes()).unwrap();
}
equal(&prediction, &load(&dir.join("project.arcadia")), None);
println!(
"PASS generated {}: {} fixed elements, {} relations, {} exact sources",
args[2],
prediction.decoded.project.elements.len(),
prediction.decoded.project.relations.len(),
prediction.bundle.files.len()
);
}
Some("check") => {
assert!(
(4..=5).contains(&args.len()),
"check MODE ENTRY [PRE_UNDO_ENTRY]"
);
let actual = load(Path::new(&args[3]));
let origin = args.get(4).map(|path| load(Path::new(path)));
equal(
&expected(&args[2]),
&actual,
origin.as_ref().map(|s| &s.disk),
);
println!(
"PASS {}: complete Project/reachable sources/physical map",
args[2]
);
}
Some("same") => {
assert!(
(4..=5).contains(&args.len()),
"same VERIFIED_EXPECTED_ENTRY ACTUAL_ENTRY [PRE_UNDO_ENTRY]"
);
let wanted = load(Path::new(&args[2]));
let actual = load(Path::new(&args[3]));
let origin = args.get(4).map(|path| load(Path::new(path)));
assert_eq!(
actual.compiled.decoded.project,
wanted.compiled.decoded.project
);
assert_eq!(actual.compiled.bundle, wanted.compiled.bundle);
let files = origin.as_ref().map_or_else(
|| wanted.disk.clone(),
|s| expected_disk(&wanted.compiled.bundle, Some(&s.disk)),
);
assert_eq!(
actual.disk, files,
"exact prior checkpoint or explicit retained-file origin"
);
println!("PASS same: all identities, fields, reachable source and physical bytes");
}
_ => panic!(
"self-check | inventory | generate MODE DIR | check MODE ENTRY [PRE_UNDO_ENTRY] | same EXPECTED ACTUAL [PRE_UNDO_ENTRY]"
),
}
}
@@ -1,53 +0,0 @@
{
"core_sha256": "D282FEE7FF3B4FDD159EA85B12430262C8DA00E064BD290CCED380B6BA2335B5",
"seconds": 62.4279746,
"completed_utc": "2026-09-11T12:41:29.1561610Z",
"results": [
{
"seconds": 23.4388482,
"name": "compile",
"exit": 0
},
{
"seconds": 8.1831391,
"name": "self-check",
"exit": 0
},
{
"seconds": 4.7308799,
"name": "inventory",
"exit": 0
},
{
"seconds": 5.0343211,
"name": "generate-baseline",
"exit": 0
},
{
"name": "check-baseline",
"exit": 0
},
{
"seconds": 3.5094392,
"name": "generate-oa-rename",
"exit": 0
},
{
"name": "check-oa-rename",
"exit": 0
},
{
"seconds": 3.6603173,
"name": "generate-sa-rename",
"exit": 0
},
{
"name": "check-sa-rename",
"exit": 0
}
],
"core_tree": "a9c71e2c3fc9260d06f191c0d65b1e63e41edbec",
"core_full_test_status": "Separate all-targets gate; no pass inferred from this oracle",
"source_sha256": "FC5E71F9870D7B98E0F52EAF20C831EBFFBE9F7A9B8960269DC9BE3D9BF0D9E4",
"exe_sha256": "798D7A1907B894C13549BAD35511B21D6BC7F3D67D63674042B7DF71184C7524"
}
@@ -1,29 +0,0 @@
{
"created_utc": "2026-09-11T18:15:30.508Z",
"files": [
{
"source": ".local/lifecycle-native-ytr83r-20260911/lifecycle-native-contract.exe",
"target": "lifecycle.exe",
"bytes": 20700160,
"sha256": "798d7a1907b894c13549bad35511b21d6bc7f3d67d63674042b7df71184c7524"
},
{
"source": ".local/staged-source-native-ytr83r-20260911/staged-source-authoring-contract.exe",
"target": "staged.exe",
"bytes": 20823552,
"sha256": "b1dea048e319fb7b9a41620a17a0db8b0831e09816accfdaec7cfbda0133c211"
},
{
"source": ".local/child-work-native/compile-01/contract.exe",
"target": "child.exe",
"bytes": 20577280,
"sha256": "c772bf4a6477bb540a838b20e60cf59c60dda51b6d2fd2cb3afee57f151a9c96"
},
{
"source": ".local/toolchain/llvm-mingw-20260908-ucrt-x86_64/bin/libunwind.dll",
"target": "libunwind.dll",
"bytes": 190464,
"sha256": "aa282fa47bb2c4e210cea5e470cdbdca58568f0be0bc73e404d86ab9cd95ef6a"
}
]
}
@@ -1,18 +0,0 @@
{
"seconds": 385.1131455,
"log": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\canvas-union-windows-20260911\\studio-build.log",
"started_utc": "2026-09-11T20:14:23.6348444Z",
"exit": 0,
"command": [
"cargo",
"build",
"-p",
"oa-desktop",
"--bin",
"arcadia-studio",
"--offline",
"--locked"
],
"name": "canvas-union-windows-20260911/studio-build",
"completed_utc": "2026-09-11T20:20:48.7677661Z"
}
@@ -1,18 +0,0 @@
{
"results": [
{
"seconds": 0.4776694,
"mode": "typography-wide",
"scope": "ordinary core/source admission of fixed expected input only; no native output",
"exit": 0
},
{
"seconds": 0.257928,
"mode": "typography-clusters",
"scope": "ordinary core/source admission of fixed expected input only; no native output",
"exit": 0
}
],
"completed_utc": "2026-09-11T13:50:08.2961401Z",
"oracle_sha256": "798D7A1907B894C13549BAD35511B21D6BC7F3D67D63674042B7DF71184C7524"
}

Some files were not shown because too many files have changed in this diff Show More