Compare commits
38
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ef6868f00 | ||
|
|
819c360a6c | ||
|
|
1e848019d9 | ||
|
|
be00bc6f65 | ||
|
|
32334c6631 | ||
|
|
09da0d90aa | ||
|
|
865483b4c6 | ||
|
|
f796a27277 | ||
|
|
321897fd55 | ||
|
|
4da92ab481 | ||
|
|
7831abeed5 | ||
|
|
fe8364e7eb | ||
|
|
3ade397008 | ||
|
|
29084050a7 | ||
|
|
cb611a71df | ||
|
|
fb77b857d2 | ||
|
|
a9a5768144 | ||
|
|
d041212d17 | ||
|
|
b96286ea37 | ||
|
|
dd42d0ab01 | ||
|
|
023e3a8f09 | ||
|
|
52f6e176cd | ||
|
|
b853a1eca3 | ||
|
|
e207b04114 | ||
|
|
c81e285573 | ||
|
|
c6d296a7e2 | ||
|
|
80cf197be6 | ||
|
|
865cd55e21 | ||
|
|
6d2b9619e3 | ||
|
|
65a1fef17c | ||
|
|
ef1a4478c5 | ||
|
|
632c8feb36 | ||
|
|
aa40345281 | ||
|
|
8547b85825 | ||
|
|
4ecad78433 | ||
|
|
ff615d0ee0 | ||
|
|
eb5161b207 | ||
|
|
aed40d7464 |
+31
-51
@@ -15,69 +15,49 @@
|
||||
|
||||
# Exact source/recovery oracles and archived ledgers retain their measured bytes.
|
||||
# Keep ordinary text diffs; only disable checkout/check-in newline conversion.
|
||||
PROJECT_GOAL.md -text whitespace=cr-at-eol
|
||||
harness/fixtures/release1-native/** -text
|
||||
harness/fixtures/interface-operation-probe/** -text
|
||||
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/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-context-20260911/** -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/fixtures/scenario-authoring/base/** -text whitespace=cr-at-eol
|
||||
harness/history/2026-09-11-ytr83r-integration/** -text whitespace=cr-at-eol
|
||||
crates/oa-desktop/src/recovery/fixtures/*.json -text
|
||||
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/behavior-creation-contract.rs -text whitespace=cr-at-eol
|
||||
harness/experiments/source-cli-independent-20260911/** -text whitespace=cr-at-eol
|
||||
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-native-20260911/** -text whitespace=cr-at-eol
|
||||
harness/experiments/contextual-source-reopen-20260911/** -text whitespace=cr-at-eol
|
||||
harness/experiments/contextual-source-native-20260911/**/*.arcadia whitespace=-blank-at-eol,-blank-at-eof,cr-at-eol
|
||||
harness/experiments/contextual-source-layout-reboot-20260911/** -text whitespace=cr-at-eol
|
||||
harness/experiments/contextual-source-layout-reboot-20260911/run-gates2.ps1 whitespace=-blank-at-eof,cr-at-eol
|
||||
harness/experiments/contextual-source-20260911/**/*.log whitespace=-blank-at-eof,cr-at-eol
|
||||
harness/experiments/scenario-native-local-20260911/** -text whitespace=cr-at-eol
|
||||
harness/experiments/scenario-native-local-20260911/**/*.log whitespace=-blank-at-eof,cr-at-eol
|
||||
harness/experiments/scenario-native-local-20260911/**/*.arcadia whitespace=-blank-at-eol,cr-at-eol
|
||||
harness/fixtures/staged-source-authoring/** -text whitespace=cr-at-eol
|
||||
harness/staged-source-authoring-contract.rs -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
|
||||
|
||||
# Preserve checksum-bound font notices verbatim through Git round trips.
|
||||
harness/experiments/wsl-capacity-20260911/** -text whitespace=cr-at-eol
|
||||
harness/experiments/wsl-postboot-9e8r6t-20260911/** -text whitespace=-blank-at-eof,cr-at-eol
|
||||
harness/experiments/contextual-source-layout-postboot-20260911/** -text whitespace=-blank-at-eof,cr-at-eol
|
||||
harness/experiments/contextual-source-layout-verified-20260911/** -text whitespace=-blank-at-eof,cr-at-eol
|
||||
harness/experiments/contextual-source-layout-full-20260911/** -text whitespace=-blank-at-eof,cr-at-eol
|
||||
scripts/windows-notices/GNOME-Cantarell-61362600e104-COPYING -text
|
||||
scripts/windows-notices/Fira-OFL.txt -text
|
||||
scripts/windows-notices/FiraSans-Regular-NOTICE.txt -text
|
||||
harness/experiments/contextual-source-recovery-20260911/** -text whitespace=-blank-at-eof,cr-at-eol
|
||||
harness/experiments/contextual-source-machine-v2-20260911/** -text whitespace=-blank-at-eof,cr-at-eol
|
||||
harness/experiments/contextual-source-lifeline-v2-20260911/** -text whitespace=-blank-at-eof,cr-at-eol
|
||||
harness/experiments/contextual-source-lifeline-v2-20260911/**/*.arcadia whitespace=-blank-at-eol,-blank-at-eof,cr-at-eol
|
||||
harness/experiments/contextual-source-machine-v2-20260911/**/*.arcadia whitespace=-blank-at-eol,-blank-at-eof,cr-at-eol
|
||||
harness/experiments/contextual-source-reopen-20260911/launch-method.ps1 whitespace=-blank-at-eof,cr-at-eol
|
||||
# Raw compact.exe output includes CR-CR-LF and incidental trailing spaces.
|
||||
harness/experiments/wsl-capacity-20260911/*.log -whitespace
|
||||
harness/experiments/contextual-prerelease-20260911/** -text whitespace=cr-at-eol
|
||||
harness/experiments/contextual-prerelease-publication-20260911/** -text whitespace=cr-at-eol
|
||||
harness/history/2026-09-11-state-before-gitea-migration/** -text whitespace=cr-at-eol
|
||||
harness/experiments/contextual-prerelease-20260911/native/**/*.arcadia whitespace=-blank-at-eol,-blank-at-eof,cr-at-eol
|
||||
harness/experiments/contextual-prerelease-20260911/producer/package*.log whitespace=-blank-at-eof,cr-at-eol
|
||||
harness/experiments/contextual-prerelease-20260911/producer/source-push.log -whitespace
|
||||
harness/experiments/source-alias-rename-native-20260912/** -text whitespace=cr-at-eol
|
||||
harness/experiments/source-alias-rename-completed-20260912/** -text whitespace=cr-at-eol
|
||||
harness/experiments/source-alias-rename-refresh-20260912/** -text whitespace=cr-at-eol
|
||||
harness/experiments/source-alias-rename-native-closure-20260912/** -text whitespace=cr-at-eol
|
||||
harness/experiments/source-alias-rename-scroll-20260912/** -text whitespace=cr-at-eol
|
||||
harness/experiments/source-alias-rename-server-20260912/** -text whitespace=cr-at-eol
|
||||
harness/experiments/source-alias-rename-package-20260912/** -text whitespace=cr-at-eol
|
||||
harness/experiments/source-alias-rename-packaged-runtime-20260912/** -text whitespace=cr-at-eol
|
||||
harness/experiments/source-alias-rename-package-native-20260912/** -text whitespace=cr-at-eol
|
||||
harness/experiments/source-alias-rename-publication-20260912/** -text whitespace=cr-at-eol
|
||||
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
|
||||
|
||||
+27
-160
@@ -142,87 +142,12 @@ pub struct Analysis {
|
||||
pub declarations: Vec<DeclarationSpan>,
|
||||
}
|
||||
|
||||
/// Check the decoded spelling required by declaration-header UUID clauses.
|
||||
/// This is the shared 36-byte/hyphen shape rule, not a hexadecimal parser:
|
||||
/// callers must additionally parse the UUID. Uppercase hex and JSON escapes
|
||||
/// that decode to this spelling remain accepted. URN/braced/compact forms do not.
|
||||
pub fn has_uuid_header_shape(decoded: &str) -> bool {
|
||||
decoded.len() == 36
|
||||
&& [8, 13, 18, 23]
|
||||
.into_iter()
|
||||
.all(|index| decoded.as_bytes()[index] == b'-')
|
||||
}
|
||||
|
||||
/// Budgets for one original source buffer. Policy is caller-owned; these
|
||||
/// limits do not change the grammar or perform model admission.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct OutlineLimits {
|
||||
pub max_source_bytes: usize,
|
||||
pub max_tokens: usize,
|
||||
pub max_declarations: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum OutlineResource {
|
||||
SourceBytes,
|
||||
Tokens,
|
||||
Declarations,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct OutlineLimit {
|
||||
pub resource: OutlineResource,
|
||||
pub limit: usize,
|
||||
pub observed_at_least: usize,
|
||||
/// Original byte offset where admission stopped; never a rewritten offset.
|
||||
pub offset: usize,
|
||||
}
|
||||
|
||||
/// Roles discovered by the existing tolerant declaration walk. Keeping the
|
||||
/// wrapper preserves DeclarationSpan and Analysis as existing public APIs.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct OutlineDeclaration {
|
||||
pub declaration: DeclarationSpan,
|
||||
pub kind_span: Span,
|
||||
pub alias_span: Span,
|
||||
/// Whole quoted header ID token, even if it is not a valid UUID. In that
|
||||
/// case declaration.id is None. Missing/non-string ID values have no span.
|
||||
pub id_literal_span: Option<Span>,
|
||||
}
|
||||
|
||||
/// A full-file tolerant scan, not a successful parse or validated model. Invalid
|
||||
/// text remains available. Lexical errors and unmatched braces are facts only:
|
||||
/// their absence does not establish syntactic or semantic validity.
|
||||
///
|
||||
/// A budget failure returns Err and no partial Outline. Include reachability,
|
||||
/// project completeness and editor revision are intentionally not inferred here.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Outline<'a> {
|
||||
pub source: &'a str,
|
||||
pub tokens: Vec<TokenSpan>,
|
||||
pub declarations: Vec<OutlineDeclaration>,
|
||||
pub has_lexical_errors: bool,
|
||||
pub has_unmatched_braces: bool,
|
||||
}
|
||||
|
||||
/// Tolerant highlighting tokens cover the whole input, including incomplete
|
||||
/// strings and unexpected characters. No model is created or modified.
|
||||
pub fn tokenize(source: &str) -> Vec<TokenSpan> {
|
||||
tokenize_with_limit(source, usize::MAX).expect("unlimited token scan")
|
||||
}
|
||||
|
||||
fn tokenize_with_limit(source: &str, max_tokens: usize) -> Result<Vec<TokenSpan>, OutlineLimit> {
|
||||
let mut tokens = Vec::new();
|
||||
let mut cursor = 0;
|
||||
while cursor < source.len() {
|
||||
if tokens.len() == max_tokens {
|
||||
return Err(OutlineLimit {
|
||||
resource: OutlineResource::Tokens,
|
||||
limit: max_tokens,
|
||||
observed_at_least: tokens.len().saturating_add(1),
|
||||
offset: cursor,
|
||||
});
|
||||
}
|
||||
let start = cursor;
|
||||
let ch = source[cursor..].chars().next().expect("in bounds");
|
||||
let class = if matches!(ch, ' ' | '\t' | '\r' | '\n') {
|
||||
@@ -299,7 +224,7 @@ fn tokenize_with_limit(source: &str, max_tokens: usize) -> Result<Vec<TokenSpan>
|
||||
span: Span { start, end: cursor },
|
||||
});
|
||||
}
|
||||
Ok(tokens)
|
||||
tokens
|
||||
}
|
||||
|
||||
pub fn lint(source: &str) -> Vec<Diagnostic> {
|
||||
@@ -1305,7 +1230,11 @@ impl Parser<'_> {
|
||||
self.cursor += 1;
|
||||
let offset = self.current().span.start;
|
||||
let value = self.quoted()?;
|
||||
if !has_uuid_header_shape(&value) {
|
||||
if value.len() != 36
|
||||
|| ![8, 13, 18, 23]
|
||||
.into_iter()
|
||||
.all(|i| value.as_bytes()[i] == b'-')
|
||||
{
|
||||
return Err(Error {
|
||||
code: "invalid_source",
|
||||
offset,
|
||||
@@ -2033,49 +1962,8 @@ fn take_required_id(fields: &mut Map<String, Value>, key: &str) -> Result<Id, St
|
||||
|
||||
/// Analyze an incomplete source buffer without adopting a candidate. Declaration
|
||||
/// spans remain available even when a later declaration contains errors.
|
||||
/// Invalid compact/braced/URN header IDs retain no authored identity, matching
|
||||
/// the declaration parser rather than the UUID crate's broader accepted forms.
|
||||
pub fn analyze(source: &str) -> Analysis {
|
||||
let tokens = tokenize(source);
|
||||
let (declarations, _) =
|
||||
declaration_outline(source, &tokens, usize::MAX).expect("unlimited declaration walk");
|
||||
Analysis {
|
||||
tokens,
|
||||
diagnostics: lint(source),
|
||||
declarations: declarations.into_iter().map(|d| d.declaration).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read exact current text without lint, parsing a model, stabilization or I/O.
|
||||
/// Source bytes are admitted before token allocation; token/declaration limits
|
||||
/// are checked before lexing the next token or materializing the next record.
|
||||
pub fn outline(source: &str, limits: OutlineLimits) -> Result<Outline<'_>, OutlineLimit> {
|
||||
if source.len() > limits.max_source_bytes {
|
||||
return Err(OutlineLimit {
|
||||
resource: OutlineResource::SourceBytes,
|
||||
limit: limits.max_source_bytes,
|
||||
observed_at_least: source.len(),
|
||||
offset: 0,
|
||||
});
|
||||
}
|
||||
let tokens = tokenize_with_limit(source, limits.max_tokens)?;
|
||||
let has_lexical_errors = tokens.iter().any(|token| token.class == TokenClass::Error);
|
||||
let (declarations, has_unmatched_braces) =
|
||||
declaration_outline(source, &tokens, limits.max_declarations)?;
|
||||
Ok(Outline {
|
||||
source,
|
||||
tokens,
|
||||
declarations,
|
||||
has_lexical_errors,
|
||||
has_unmatched_braces,
|
||||
})
|
||||
}
|
||||
|
||||
fn declaration_outline(
|
||||
source: &str,
|
||||
tokens: &[TokenSpan],
|
||||
max_declarations: usize,
|
||||
) -> Result<(Vec<OutlineDeclaration>, bool), OutlineLimit> {
|
||||
let significant = tokens
|
||||
.iter()
|
||||
.filter(|t| !matches!(t.class, TokenClass::Whitespace | TokenClass::Comment))
|
||||
@@ -2084,21 +1972,17 @@ fn declaration_outline(
|
||||
// again would make tolerant navigation quadratic on deeply nested drafts.
|
||||
let mut body_ends = vec![None; significant.len()];
|
||||
let mut bodies = Vec::new();
|
||||
let mut unmatched_close = false;
|
||||
for (index, token) in significant.iter().enumerate() {
|
||||
match &source[token.span.start..token.span.end] {
|
||||
"{" => bodies.push(index),
|
||||
"}" => {
|
||||
if let Some(open) = bodies.pop() {
|
||||
body_ends[open] = Some(token.span.end);
|
||||
} else {
|
||||
unmatched_close = true;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let has_unmatched_braces = unmatched_close || !bodies.is_empty();
|
||||
let mut declarations = Vec::new();
|
||||
for (i, t) in significant.iter().enumerate() {
|
||||
let word = &source[t.span.start..t.span.end];
|
||||
@@ -2123,16 +2007,7 @@ fn declaration_outline(
|
||||
}
|
||||
j += 1;
|
||||
}
|
||||
if declarations.len() == max_declarations {
|
||||
return Err(OutlineLimit {
|
||||
resource: OutlineResource::Declarations,
|
||||
limit: max_declarations,
|
||||
observed_at_least: declarations.len().saturating_add(1),
|
||||
offset: t.span.start,
|
||||
});
|
||||
}
|
||||
let mut id = None;
|
||||
let mut id_literal_span = None;
|
||||
if significant
|
||||
.get(j)
|
||||
.is_some_and(|t| &source[t.span.start..t.span.end] == "id")
|
||||
@@ -2140,51 +2015,39 @@ fn declaration_outline(
|
||||
if let Some(value) = significant.get(j + 1)
|
||||
&& value.class == TokenClass::String
|
||||
{
|
||||
id_literal_span = Some(value.span);
|
||||
let text =
|
||||
serde_json::from_str::<String>(&source[value.span.start..value.span.end])
|
||||
.unwrap();
|
||||
if has_uuid_header_shape(&text) {
|
||||
id = Id::parse_str(&text).ok();
|
||||
}
|
||||
id = Id::parse_str(&text).ok();
|
||||
}
|
||||
j += 2;
|
||||
}
|
||||
// A recognized header ID at EOF must remain inside the whole span.
|
||||
// Keep the old next-token/body behavior; only the EOF fallback changes.
|
||||
let header_end = significant
|
||||
.get(j.saturating_sub(1))
|
||||
.or_else(|| significant.last())
|
||||
.map_or(alias_token.span.end, |token| token.span.end);
|
||||
let mut end = significant.get(j).map_or(header_end, |t| t.span.end);
|
||||
let mut end = significant
|
||||
.get(j)
|
||||
.map_or(alias_token.span.end, |t| t.span.end);
|
||||
if significant
|
||||
.get(j)
|
||||
.is_some_and(|t| &source[t.span.start..t.span.end] == "{")
|
||||
{
|
||||
end = body_ends[j].unwrap_or(source.len());
|
||||
}
|
||||
declarations.push(OutlineDeclaration {
|
||||
declaration: DeclarationSpan {
|
||||
alias: alias.into(),
|
||||
id,
|
||||
kind: word.into(),
|
||||
span: Span {
|
||||
start: t.span.start,
|
||||
end,
|
||||
},
|
||||
declarations.push(DeclarationSpan {
|
||||
alias: alias.into(),
|
||||
id,
|
||||
kind: word.into(),
|
||||
span: Span {
|
||||
start: t.span.start,
|
||||
end,
|
||||
},
|
||||
kind_span: t.span,
|
||||
alias_span: alias_token.span,
|
||||
id_literal_span,
|
||||
});
|
||||
}
|
||||
Ok((declarations, has_unmatched_braces))
|
||||
Analysis {
|
||||
tokens,
|
||||
diagnostics: lint(source),
|
||||
declarations,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "language_outline_tests.rs"]
|
||||
mod outline_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "language_nesting_tests.rs"]
|
||||
mod nesting_tests;
|
||||
@@ -2579,7 +2442,11 @@ pub(crate) mod legacy {
|
||||
let value = self.quoted()?;
|
||||
// Require the portable 8-4-4-4-12 spelling, not the UUID crate's URN,
|
||||
// braces, or compact alternatives. Uppercase hexadecimal is accepted.
|
||||
if !super::has_uuid_header_shape(&value) {
|
||||
if value.len() != 36
|
||||
|| ![8, 13, 18, 23]
|
||||
.into_iter()
|
||||
.all(|index| value.as_bytes()[index] == b'-')
|
||||
{
|
||||
return Err(Error {
|
||||
code: "invalid_source",
|
||||
offset,
|
||||
|
||||
@@ -1,386 +0,0 @@
|
||||
//! Prepared outline tests. Explicit compatibility tests additionally exercise
|
||||
//! the existing parsers/analyze; the outline entry itself never calls them.
|
||||
use super::*;
|
||||
|
||||
fn limits() -> OutlineLimits {
|
||||
OutlineLimits {
|
||||
max_source_bytes: 1024 * 1024,
|
||||
max_tokens: 100_000,
|
||||
max_declarations: 1_000,
|
||||
}
|
||||
}
|
||||
|
||||
fn declaration<'a>(result: &'a Outline<'_>, alias: &str) -> &'a OutlineDeclaration {
|
||||
result
|
||||
.declarations
|
||||
.iter()
|
||||
.find(|d| d.declaration.alias == alias)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn text(source: &str, span: Span) -> &str {
|
||||
&source[span.start..span.end]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outline_roles_preserve_unicode_crlf_and_exact_escaped_uuid_literal() {
|
||||
let source = "// 検査 🚒\r\noa_actor responder \"救助\" id \"\\u00300000000-0000-0000-0000-000000000001\" {}\r\nsource @ // 使用\r\n responder;";
|
||||
let before = source.as_bytes().to_vec();
|
||||
let result = outline(source, limits()).unwrap();
|
||||
assert_eq!(result.source.as_bytes(), before);
|
||||
assert_eq!(result.source.as_ptr(), source.as_ptr());
|
||||
let d = declaration(&result, "responder");
|
||||
assert_eq!(text(source, d.kind_span), "oa_actor");
|
||||
assert_eq!(text(source, d.alias_span), "responder");
|
||||
assert_eq!(
|
||||
text(source, d.id_literal_span.unwrap()),
|
||||
"\"\\u00300000000-0000-0000-0000-000000000001\""
|
||||
);
|
||||
assert_eq!(d.declaration.id, Some(Id::from_u128(1)));
|
||||
assert_eq!(d.declaration.span.start, source.find("oa_actor").unwrap());
|
||||
assert_eq!(d.declaration.span.end, source.find("{}\r\n").unwrap() + 2);
|
||||
let mut end = 0;
|
||||
for token in &result.tokens {
|
||||
assert_eq!(token.span.start, end);
|
||||
assert!(token.span.start < token.span.end);
|
||||
assert!(source.is_char_boundary(token.span.start));
|
||||
assert!(source.is_char_boundary(token.span.end));
|
||||
end = token.span.end;
|
||||
}
|
||||
assert_eq!(end, source.len());
|
||||
assert!(!result.has_lexical_errors);
|
||||
assert!(!result.has_unmatched_braces);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closed_id_at_eof_is_inside_the_declaration_extent() {
|
||||
for source in [
|
||||
"oa_actor a \"Actor\" id \"00000000-0000-0000-0000-000000000001\"",
|
||||
"node placed id \"00000000-0000-0000-0000-000000000001\"",
|
||||
] {
|
||||
let result = outline(source, limits()).unwrap();
|
||||
let d = &result.declarations[0];
|
||||
let literal = d.id_literal_span.unwrap();
|
||||
assert_eq!(d.declaration.id, Some(Id::from_u128(1)));
|
||||
assert_eq!(d.declaration.span.end, source.len());
|
||||
assert!(d.declaration.span.start <= literal.start);
|
||||
assert!(literal.end <= d.declaration.span.end);
|
||||
}
|
||||
for source in ["oa_actor a \"Actor\"", "oa_actor a \"Actor\" id"] {
|
||||
let result = outline(source, limits()).unwrap();
|
||||
let d = &result.declarations[0];
|
||||
assert_eq!(d.declaration.span.end, source.len());
|
||||
assert_eq!(d.declaration.id, None);
|
||||
assert_eq!(d.id_literal_span, None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_bad_and_historical_ids_never_allocate_authored_identity() {
|
||||
let source = "oa_actor missing \"No ID\" {}\noa_actor bad \"Bad ID\" id \"not-a-uuid\" {}\noa_actor historical \"00000000-0000-0000-0000-000000000001\" { history uuid \"00000000-0000-0000-0000-000000000001\"; }";
|
||||
let first = outline(source, limits()).unwrap();
|
||||
let second = outline(source, limits()).unwrap();
|
||||
assert_eq!(first, second);
|
||||
assert!(
|
||||
first
|
||||
.declarations
|
||||
.iter()
|
||||
.all(|d| d.declaration.id.is_none())
|
||||
);
|
||||
assert_eq!(declaration(&first, "missing").id_literal_span, None);
|
||||
assert_eq!(declaration(&first, "historical").id_literal_span, None);
|
||||
assert_eq!(
|
||||
text(source, declaration(&first, "bad").id_literal_span.unwrap()),
|
||||
"\"not-a-uuid\""
|
||||
);
|
||||
// Balanced tokens are not a successful syntax/semantic validation claim.
|
||||
assert!(!first.has_lexical_errors);
|
||||
assert!(!first.has_unmatched_braces);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_full_scan_keeps_closed_children_and_reports_lexical_body_facts() {
|
||||
let source = "oa_entity outer \"Outer\" {\n oa_actor ready \"Ready\" { description \"} stays text\"; }\n oa_actor pending \"Pending\" { description \"unfinished 🌊";
|
||||
let result = outline(source, limits()).unwrap();
|
||||
assert_eq!(result.source, source);
|
||||
assert_eq!(result.tokens.last().unwrap().span.end, source.len());
|
||||
assert!(result.has_lexical_errors);
|
||||
assert!(result.has_unmatched_braces);
|
||||
let ready = declaration(&result, "ready");
|
||||
assert_eq!(
|
||||
text(source, ready.declaration.span),
|
||||
"oa_actor ready \"Ready\" { description \"} stays text\"; }"
|
||||
);
|
||||
assert_eq!(
|
||||
declaration(&result, "outer").declaration.span.end,
|
||||
source.len()
|
||||
);
|
||||
assert_eq!(
|
||||
declaration(&result, "pending").declaration.span.end,
|
||||
source.len()
|
||||
);
|
||||
let unmatched_close = outline("}\n// brace in comment {\n", limits()).unwrap();
|
||||
assert!(unmatched_close.has_unmatched_braces);
|
||||
assert!(!unmatched_close.has_lexical_errors);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_body_syntax_is_unchecked_even_when_known_error_flags_are_clear() {
|
||||
let source = "oa_actor a \"A\" id \"00000000-0000-0000-0000-000000000001\"";
|
||||
let result = outline(source, limits()).unwrap();
|
||||
assert_eq!(result.declarations.len(), 1);
|
||||
assert!(!result.has_lexical_errors);
|
||||
assert!(!result.has_unmatched_braces);
|
||||
// There is deliberately no `valid` flag: the known-complete fact is scanning
|
||||
// these bytes, not the missing body becoming legal or the draft being applied.
|
||||
assert_eq!(result.source, source);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyword_alias_and_collisions_keep_every_source_ordered_header() {
|
||||
let source = "project root_model \"Model\" {\n oa_actor oa_activity \"First\" id \"00000000-0000-0000-0000-000000000001\" {}\n oa_actor oa_activity \"Second\" id \"00000000-0000-0000-0000-000000000001\" {}\n diagram view_one \"View\" {}\n node placement { diagram @view_one; }\n}";
|
||||
let result = outline(source, limits()).unwrap();
|
||||
assert_eq!(result.declarations.len(), 5);
|
||||
let duplicates: Vec<_> = result
|
||||
.declarations
|
||||
.iter()
|
||||
.filter(|d| d.declaration.alias == "oa_activity")
|
||||
.collect();
|
||||
assert_eq!(duplicates.len(), 2);
|
||||
assert!(duplicates[0].alias_span.start < duplicates[1].alias_span.start);
|
||||
assert_eq!(duplicates[0].declaration.id, duplicates[1].declaration.id);
|
||||
assert!(
|
||||
result
|
||||
.tokens
|
||||
.iter()
|
||||
.any(|t| t.span == duplicates[0].alias_span && t.class == TokenClass::Keyword)
|
||||
);
|
||||
assert_eq!(
|
||||
text(source, declaration(&result, "placement").kind_span),
|
||||
"node"
|
||||
);
|
||||
assert_eq!(declaration(&result, "placement").declaration.id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounds_refuse_before_returning_a_partial_outline() {
|
||||
let source = "oa_actor a \"A\" {}\noa_actor b \"B\" {}";
|
||||
let admitted = outline(source, limits()).unwrap();
|
||||
let exact = OutlineLimits {
|
||||
max_source_bytes: source.len(),
|
||||
max_tokens: admitted.tokens.len(),
|
||||
max_declarations: 2,
|
||||
};
|
||||
assert_eq!(outline(source, exact).unwrap(), admitted);
|
||||
for resource in [
|
||||
OutlineResource::SourceBytes,
|
||||
OutlineResource::Tokens,
|
||||
OutlineResource::Declarations,
|
||||
] {
|
||||
let mut lower = exact;
|
||||
match resource {
|
||||
OutlineResource::SourceBytes => lower.max_source_bytes -= 1,
|
||||
OutlineResource::Tokens => lower.max_tokens -= 1,
|
||||
OutlineResource::Declarations => lower.max_declarations -= 1,
|
||||
}
|
||||
let error = outline(source, lower).unwrap_err();
|
||||
assert_eq!(error.resource, resource);
|
||||
assert!(error.observed_at_least > error.limit);
|
||||
assert!(source.is_char_boundary(error.offset));
|
||||
if resource == OutlineResource::Declarations {
|
||||
assert_eq!(error.offset, source.find("oa_actor b").unwrap());
|
||||
}
|
||||
}
|
||||
let empty = outline(
|
||||
"",
|
||||
OutlineLimits {
|
||||
max_source_bytes: 0,
|
||||
max_tokens: 0,
|
||||
max_declarations: 0,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(empty.tokens.is_empty() && empty.declarations.is_empty());
|
||||
let unicode = "// 🌊\r\noa_actor a \"A\" {}";
|
||||
let error = outline(
|
||||
unicode,
|
||||
OutlineLimits {
|
||||
max_source_bytes: 4,
|
||||
..limits()
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(error.resource, OutlineResource::SourceBytes);
|
||||
assert_eq!(error.offset, 0);
|
||||
let error = outline(
|
||||
unicode,
|
||||
OutlineLimits {
|
||||
max_tokens: 1,
|
||||
..limits()
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(error.resource, OutlineResource::Tokens);
|
||||
assert_eq!(error.offset, unicode.find('\n').unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outline_ranges_follow_current_bytes_without_identity_stabilization() {
|
||||
let old = "oa_actor a \"A\" {}\nsource @a;";
|
||||
let current = format!("// 航海 🚒\r\n{old}");
|
||||
let old_outline = outline(old, limits()).unwrap();
|
||||
let current_outline = outline(¤t, limits()).unwrap();
|
||||
let shift = current.len() - old.len();
|
||||
let old_role = &old_outline.declarations[0];
|
||||
let current_role = ¤t_outline.declarations[0];
|
||||
assert_eq!(
|
||||
current_role.alias_span.start,
|
||||
old_role.alias_span.start + shift
|
||||
);
|
||||
assert_eq!(
|
||||
current_role.kind_span.start,
|
||||
old_role.kind_span.start + shift
|
||||
);
|
||||
assert_eq!(
|
||||
current_role.declaration.span.end,
|
||||
old_role.declaration.span.end + shift
|
||||
);
|
||||
assert_eq!(current_outline.source, current);
|
||||
assert_eq!(old_outline.source, old);
|
||||
assert_eq!(current_role.declaration.id, None);
|
||||
assert!(!current_outline.source.contains(" id "));
|
||||
// Workspace revision/stale-handle rejection belongs to the query/adapter,
|
||||
// not a claim that an old owned span can be applied to any other string.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deeply_nested_bounds_use_the_same_iterative_walk_and_declaration_budget() {
|
||||
let mut source = String::new();
|
||||
for index in 0..600 {
|
||||
source.push_str(&format!("oa_activity work_{index} \"Work\" {{\n"));
|
||||
}
|
||||
source.push_str(&"}\n".repeat(600));
|
||||
let result = outline(&source, limits()).unwrap();
|
||||
assert_eq!(result.declarations.len(), 600);
|
||||
assert!(!result.has_unmatched_braces);
|
||||
assert_eq!(
|
||||
result.declarations[0].declaration.span.end,
|
||||
source.len() - 1
|
||||
);
|
||||
let error = outline(
|
||||
&source,
|
||||
OutlineLimits {
|
||||
max_declarations: 599,
|
||||
..limits()
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(error.resource, OutlineResource::Declarations);
|
||||
assert_eq!(error.observed_at_least, 600);
|
||||
assert_eq!(error.offset, source.find("oa_activity work_599").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noncanonical_uuid_spellings_have_no_outline_or_analysis_identity() {
|
||||
for raw in [
|
||||
"00000000000000000000000000000001",
|
||||
"{00000000-0000-0000-0000-000000000001}",
|
||||
"urn:uuid:00000000-0000-0000-0000-000000000001",
|
||||
] {
|
||||
assert!(
|
||||
Id::parse_str(raw).is_ok(),
|
||||
"The UUID crate alone is too permissive"
|
||||
);
|
||||
assert!(!has_uuid_header_shape(raw));
|
||||
let literal = format!("\"{raw}\"");
|
||||
let modern = format!("arcadia 7; project model \"Model\" id {literal} {{}}");
|
||||
let before = modern.clone();
|
||||
let result = outline(&modern, limits()).unwrap();
|
||||
let d = declaration(&result, "model");
|
||||
assert_eq!(d.declaration.id, None);
|
||||
assert_eq!(text(&modern, d.id_literal_span.unwrap()), literal);
|
||||
assert_eq!(modern, before);
|
||||
let errors = parse(&modern).unwrap_err();
|
||||
assert!(
|
||||
errors
|
||||
.iter()
|
||||
.any(|error| error.message.contains("8-4-4-4-12")
|
||||
&& error.span.start == modern.find(&literal).unwrap())
|
||||
);
|
||||
// Intentional compatibility correction: old analyze yielded Some(Id(1))
|
||||
// here despite lint rejecting the header. It now shares outline's None.
|
||||
let analysis = analyze(&modern);
|
||||
assert_eq!(
|
||||
analysis
|
||||
.declarations
|
||||
.iter()
|
||||
.find(|d| d.alias == "model")
|
||||
.unwrap()
|
||||
.id,
|
||||
None
|
||||
);
|
||||
assert!(
|
||||
analysis
|
||||
.diagnostics
|
||||
.iter()
|
||||
.any(|error| error.message.contains("8-4-4-4-12"))
|
||||
);
|
||||
let legacy_source = format!("arcadia 1; project \"Legacy\" id {literal} {{}}");
|
||||
let legacy_errors = legacy::parse(&legacy_source).unwrap_err();
|
||||
assert!(
|
||||
legacy_errors
|
||||
.iter()
|
||||
.any(|error| error.message.contains("8-4-4-4-12"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uppercase_and_json_escaped_header_ids_survive_both_parsers_and_outline() {
|
||||
let expected = Id::from_u128(0xabcdef);
|
||||
let canonical = expected.to_string();
|
||||
let uppercase = canonical.to_uppercase();
|
||||
assert_ne!(uppercase, canonical);
|
||||
for raw in [uppercase.clone(), format!("\\u0030{}", &uppercase[1..])] {
|
||||
let literal = format!("\"{raw}\"");
|
||||
let decoded: String = serde_json::from_str(&literal).unwrap();
|
||||
assert!(has_uuid_header_shape(&decoded));
|
||||
let modern = format!("arcadia 7; project model \"Model\" id {literal} {{}}");
|
||||
let result = outline(&modern, limits()).unwrap();
|
||||
let d = declaration(&result, "model");
|
||||
assert_eq!(d.declaration.id, Some(expected));
|
||||
assert_eq!(text(&modern, d.id_literal_span.unwrap()), literal);
|
||||
assert_eq!(result.source.as_bytes(), modern.as_bytes());
|
||||
assert_eq!(parse(&modern).unwrap().id, expected);
|
||||
let analysis = analyze(&modern);
|
||||
assert_eq!(analysis.declarations[0].id, Some(expected));
|
||||
assert!(
|
||||
!analysis
|
||||
.diagnostics
|
||||
.iter()
|
||||
.any(|error| error.severity == crate::validation::Severity::Error)
|
||||
);
|
||||
let legacy_source = format!("arcadia 1; project \"Legacy\" id {literal} {{}}");
|
||||
assert_eq!(legacy::parse(&legacy_source).unwrap().id, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_shape_still_requires_valid_hexadecimal_uuid_data() {
|
||||
let raw = "00000000-0000-0000-0000-00000000000Z";
|
||||
assert!(has_uuid_header_shape(raw));
|
||||
assert!(Id::parse_str(raw).is_err());
|
||||
let literal = format!("\"{raw}\"");
|
||||
let source = format!("arcadia 7; project model \"Model\" id {literal} {{}}");
|
||||
let result = outline(&source, limits()).unwrap();
|
||||
let d = declaration(&result, "model");
|
||||
assert_eq!(d.declaration.id, None);
|
||||
assert_eq!(text(&source, d.id_literal_span.unwrap()), literal);
|
||||
assert!(
|
||||
parse(&source)
|
||||
.unwrap_err()
|
||||
.iter()
|
||||
.any(|error| error.message == "Invalid UUID identity.")
|
||||
);
|
||||
assert_eq!(analyze(&source).declarations[0].id, None);
|
||||
}
|
||||
+78
-192
@@ -7,8 +7,6 @@ mod diagram_selection_tests;
|
||||
mod hosted;
|
||||
#[cfg(test)]
|
||||
mod review_navigation_tests;
|
||||
pub(crate) mod source_rename;
|
||||
mod source_symbols;
|
||||
#[cfg(test)]
|
||||
mod transition_context_tests;
|
||||
#[cfg(test)]
|
||||
@@ -417,17 +415,9 @@ pub enum Message {
|
||||
Linted(u64, Result<Vec<SourceDiagnostic>, String>),
|
||||
SourceDiagnostic(usize),
|
||||
SourceRelated(SourceLocation),
|
||||
SourceViewport(Option<Id>),
|
||||
SourceSymbols(
|
||||
crate::source_navigation::SymbolContext,
|
||||
crate::source_navigation::SymbolEvent,
|
||||
),
|
||||
SourceSymbolsReady(crate::source_navigation::Completion),
|
||||
SourceRename(source_rename::Event),
|
||||
SourceRenameReady(source_rename::Completion),
|
||||
SourceViewport,
|
||||
SourceForSelection,
|
||||
SourceNavigation(crate::source_navigation::Event),
|
||||
SourceFindClose(crate::source_navigation::SymbolContext, Id),
|
||||
Save(bool),
|
||||
SavePath(Option<PathBuf>),
|
||||
Saved(Result<Option<FileSnapshot>, SaveFailure>),
|
||||
@@ -501,9 +491,6 @@ pub struct App {
|
||||
pub source_jump: Option<text_editor::Cursor>,
|
||||
source_select_line: bool,
|
||||
pub source_navigation: crate::source_navigation::State,
|
||||
source_find_instance: Id,
|
||||
pub source_symbols: crate::source_navigation::SymbolState,
|
||||
pub source_rename: source_rename::State,
|
||||
pub model_issues: Vec<validation::Issue>,
|
||||
pub migration: Option<persistence::MigrationReport>,
|
||||
source_version: u64,
|
||||
@@ -609,9 +596,6 @@ impl App {
|
||||
source_jump: None,
|
||||
source_select_line: false,
|
||||
source_navigation: crate::source_navigation::State::default(),
|
||||
source_find_instance: Id::new_v4(),
|
||||
source_symbols: crate::source_navigation::SymbolState::default(),
|
||||
source_rename: source_rename::State::default(),
|
||||
model_issues,
|
||||
migration: None,
|
||||
source_version: 0,
|
||||
@@ -658,37 +642,18 @@ impl App {
|
||||
|| self.behavior.has_pending()
|
||||
}
|
||||
fn sync_source(&mut self) {
|
||||
self.sync_source_with_alias_editor(None);
|
||||
}
|
||||
fn sync_source_with_alias_editor(&mut self, hint: Option<source_rename::EditorRefresh<'_>>) {
|
||||
self.source_workspace.accept(self.document.bundle());
|
||||
self.show_source_file_with_alias_editor(false, hint);
|
||||
self.show_source_file(false);
|
||||
self.source_diagnostics = self.document.compiled().diagnostics.clone();
|
||||
self.source_version += 1;
|
||||
self.lint_requested = None;
|
||||
}
|
||||
fn show_source_file(&mut self, restore_cursor: bool) {
|
||||
self.show_source_file_with_alias_editor(restore_cursor, None);
|
||||
}
|
||||
fn show_source_file_with_alias_editor(
|
||||
&mut self,
|
||||
restore_cursor: bool,
|
||||
hint: Option<source_rename::EditorRefresh<'_>>,
|
||||
) {
|
||||
self.source_symbols.reveal = None;
|
||||
self.source_tree.reveal(&self.source_workspace.active);
|
||||
self.source_select_line = false;
|
||||
let source = self.source_workspace.text().to_owned();
|
||||
self.source_navigation.refresh(&source);
|
||||
if !hint.is_some_and(|hint| {
|
||||
hint.apply(
|
||||
&mut self.source_editor,
|
||||
&self.source_workspace.active,
|
||||
&source,
|
||||
)
|
||||
}) {
|
||||
self.source_editor = text_editor::Content::with_text(&source);
|
||||
}
|
||||
self.source_editor = text_editor::Content::with_text(&source);
|
||||
self.source_snapshot = Arc::from(source);
|
||||
self.source_jump = restore_cursor
|
||||
.then(|| {
|
||||
@@ -698,9 +663,6 @@ impl App {
|
||||
.map(|b| b.cursor)
|
||||
})
|
||||
.flatten();
|
||||
if let Some(cursor) = self.source_jump {
|
||||
self.queue_source_reveal(cursor, false);
|
||||
}
|
||||
}
|
||||
fn selected_architecture_placement(&self) -> Option<Id> {
|
||||
self.active_diagram
|
||||
@@ -733,9 +695,6 @@ impl App {
|
||||
}
|
||||
}
|
||||
fn sync(&mut self) {
|
||||
self.sync_with_alias_editor(None);
|
||||
}
|
||||
fn sync_with_alias_editor(&mut self, hint: Option<source_rename::EditorRefresh<'_>>) {
|
||||
self.explorer_index = crate::explorer::Index::new(self.project(), self.layer);
|
||||
self.model_issues = validate(self.project());
|
||||
self.normalize_creation();
|
||||
@@ -765,7 +724,7 @@ impl App {
|
||||
.map(|d| d.id);
|
||||
}
|
||||
self.refresh_draft();
|
||||
self.sync_source_with_alias_editor(hint);
|
||||
self.sync_source();
|
||||
}
|
||||
fn normalize_creation(&mut self) {
|
||||
if !self.create_kind.supports(self.layer) {
|
||||
@@ -957,8 +916,6 @@ impl App {
|
||||
let canvas_size = self.canvas_size;
|
||||
let source_version = self.source_version + 1;
|
||||
let lint_running = self.lint_running;
|
||||
let navigation_in_flight = self.source_symbols.in_flight;
|
||||
let rename_in_flight = self.source_rename.in_flight;
|
||||
let guide = std::mem::take(&mut self.guide);
|
||||
let mut hosted = std::mem::take(&mut self.hosted);
|
||||
hosted.leave_document();
|
||||
@@ -970,8 +927,6 @@ impl App {
|
||||
self.canvas_size = canvas_size;
|
||||
self.source_version = source_version;
|
||||
self.lint_running = lint_running;
|
||||
self.source_symbols.in_flight = navigation_in_flight;
|
||||
self.source_rename.in_flight = rename_in_flight;
|
||||
self.guide = guide;
|
||||
self.path = path;
|
||||
self.status = "Project ready".into();
|
||||
@@ -1072,10 +1027,7 @@ impl App {
|
||||
crate::model_menu::popup(self),
|
||||
)
|
||||
.into();
|
||||
crate::source_navigation::interaction::observe(
|
||||
crate::hosted::view::overlay(self, base),
|
||||
self.source_symbols.raw_interaction.clone(),
|
||||
)
|
||||
crate::hosted::view::overlay(self, base)
|
||||
}
|
||||
pub fn subscription(&self) -> Subscription<Message> {
|
||||
Subscription::batch([
|
||||
@@ -1083,16 +1035,8 @@ impl App {
|
||||
window::close_requests().map(|id| Message::Request(Pending::Close(id))),
|
||||
window::resize_events().map(|(_, size)| Message::WindowResized(size)),
|
||||
architecture_authoring::subscription(self),
|
||||
// Use one captured-Escape bridge: a focused Find input consumes
|
||||
// Escape, while modal priority remains in the ordinary handler.
|
||||
behavior::creation::subscription(
|
||||
self.behavior.creation.has_pending()
|
||||
|| self.behavior.source_active()
|
||||
|| (self.page == Page::Text
|
||||
&& (self.source_symbols.open
|
||||
|| self.source_navigation.open
|
||||
|| self.source_rename.open())
|
||||
&& self.architecture.is_none()),
|
||||
self.behavior.creation.has_pending() || self.behavior.source_active(),
|
||||
)
|
||||
.map(|()| Message::Escape),
|
||||
iced::event::listen_with(|event, status, _| {
|
||||
@@ -1139,55 +1083,6 @@ impl App {
|
||||
])
|
||||
}
|
||||
pub fn update(&mut self, message: Message) -> Task<Message> {
|
||||
if let Message::SourceRename(event) = &message
|
||||
&& !self.source_rename_envelope(event)
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
let message = match message {
|
||||
Message::SourceFindClose(context, instance) => {
|
||||
if context != self.source_symbol_context()
|
||||
|| instance != self.source_find_instance
|
||||
|| !self.source_navigation.open
|
||||
|| !self.source_navigation_available()
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
// Only the checked envelope may enter the legacy Close handler.
|
||||
Message::SourceNavigation(crate::source_navigation::Event::Close)
|
||||
}
|
||||
Message::SourceNavigation(crate::source_navigation::Event::Close) => {
|
||||
return Task::none();
|
||||
}
|
||||
message => message,
|
||||
};
|
||||
if let Message::SourceSymbols(context, _) = &message
|
||||
&& (context.session != self.source_workspace.session()
|
||||
|| context.path != self.source_workspace.active)
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
// Internal results and geometry do not constitute a new user intent.
|
||||
// Process query completion even behind a modal to release its worker slot;
|
||||
// its handler independently refuses navigation outside the live editor.
|
||||
if let Message::SourceSymbolsReady(completion) = message {
|
||||
return self.source_symbols_ready(completion);
|
||||
}
|
||||
if let Message::SourceRenameReady(completion) = message {
|
||||
return self.source_rename_ready(completion);
|
||||
}
|
||||
if !matches!(
|
||||
&message,
|
||||
Message::CheckpointTick
|
||||
| Message::CheckpointWritten(..)
|
||||
| Message::Lint(_)
|
||||
| Message::Linted(..)
|
||||
| Message::SourceViewport(_)
|
||||
| Message::WindowResized(_)
|
||||
| Message::CanvasResized(_)
|
||||
) {
|
||||
self.source_interaction();
|
||||
}
|
||||
if matches!(&message, Message::Connection(_))
|
||||
&& self
|
||||
.architecture
|
||||
@@ -1252,7 +1147,7 @@ impl App {
|
||||
| Message::CanvasResized(_)
|
||||
| Message::Lint(_)
|
||||
| Message::Linted(_, _)
|
||||
| Message::SourceViewport(_)
|
||||
| Message::SourceViewport
|
||||
| Message::Request(Pending::Close(_))
|
||||
)
|
||||
{
|
||||
@@ -1298,7 +1193,7 @@ impl App {
|
||||
| Message::Exported(_)
|
||||
| Message::WindowResized(_)
|
||||
| Message::CanvasResized(_)
|
||||
| Message::SourceViewport(_)
|
||||
| Message::SourceViewport
|
||||
)
|
||||
{
|
||||
return Task::none();
|
||||
@@ -1336,7 +1231,7 @@ impl App {
|
||||
| Message::CheckpointLoaded(..)
|
||||
| Message::WindowResized(_)
|
||||
| Message::CanvasResized(_)
|
||||
| Message::SourceViewport(_)
|
||||
| Message::SourceViewport
|
||||
| Message::Lint(_)
|
||||
| Message::Linted(_, _)
|
||||
| Message::Saved(_)
|
||||
@@ -1366,18 +1261,6 @@ impl App {
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
if self.source_rename.open() {
|
||||
if matches!(
|
||||
message,
|
||||
Message::Escape | Message::Request(Pending::Close(_))
|
||||
) {
|
||||
self.close_source_rename();
|
||||
return Task::none();
|
||||
}
|
||||
if !modal_event && !matches!(message, Message::SourceRename(_)) {
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
if matches!(
|
||||
message,
|
||||
Message::Page(_) | Message::Layer(_) | Message::Diagram(_)
|
||||
@@ -1422,7 +1305,6 @@ impl App {
|
||||
self.last_workspace_page = self.page;
|
||||
}
|
||||
let mut task = self.handle(message);
|
||||
self.refresh_source_rename_context();
|
||||
self.route_state_diagram_workspace();
|
||||
if self.page != Page::Home {
|
||||
self.last_workspace_page = self.page;
|
||||
@@ -1909,8 +1791,6 @@ impl App {
|
||||
return self.update(Message::Add(child));
|
||||
}
|
||||
Message::Escape => {
|
||||
let restore_source_focus = self.source_navigation_available()
|
||||
&& (self.source_navigation.open || self.source_symbols.open);
|
||||
if self.behavior.source_back() {
|
||||
self.status =
|
||||
"Returned to the unchanged parent form; no source was created.".into();
|
||||
@@ -1927,12 +1807,6 @@ impl App {
|
||||
self.status = "Drawing cancelled · Select / move is active".into();
|
||||
}
|
||||
self.source_navigation.open = false;
|
||||
self.source_symbols.open = false;
|
||||
self.source_symbols.results = None;
|
||||
self.source_symbols.notice = None;
|
||||
if restore_source_focus {
|
||||
self.restore_source_editor_focus();
|
||||
}
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||
self.selected_bend = None;
|
||||
}
|
||||
@@ -2968,7 +2842,6 @@ impl App {
|
||||
if self.pending_recovery.as_ref() != Some(&receipt.target) {
|
||||
return Task::none();
|
||||
}
|
||||
self.invalidate_source_rename("Disk recovery changed this source context. Your replacement is retained; close and invoke Rename again.");
|
||||
self.pending_recovery = None;
|
||||
self.source_diagnostics
|
||||
.retain(|issue| issue.diagnostic.code != "pending_save");
|
||||
@@ -2982,21 +2855,12 @@ impl App {
|
||||
if self.path.is_none()
|
||||
|| self.path.as_ref() == Some(&receipt.target)
|
||||
{
|
||||
if self.path.as_ref() != Some(&receipt.target) {
|
||||
self.source_workspace.reset_navigation_session();
|
||||
}
|
||||
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}"
|
||||
)
|
||||
@@ -3053,13 +2917,14 @@ impl App {
|
||||
}
|
||||
}
|
||||
Message::SourceRelated(location) => self.jump_to_source(location),
|
||||
Message::SourceViewport(ticket) => return self.source_viewport(ticket),
|
||||
Message::SourceFindClose(..) => return Task::none(), // update checks and unwraps it
|
||||
Message::SourceSymbols(_, event) => return self.source_symbol_event(event),
|
||||
Message::SourceRename(event) => return self.source_rename_event(event),
|
||||
Message::SourceRenameReady(completion) => return self.source_rename_ready(completion),
|
||||
Message::SourceSymbolsReady(completion) => {
|
||||
return self.source_symbols_ready(completion);
|
||||
Message::SourceViewport => {
|
||||
if let Some(cursor) = self.source_jump.take() {
|
||||
self.source_editor.move_to(cursor);
|
||||
if self.source_select_line && cursor.selection.is_none() {
|
||||
self.source_editor.perform(text_editor::Action::SelectLine);
|
||||
}
|
||||
return iced::widget::operation::focus("source-editor");
|
||||
}
|
||||
}
|
||||
Message::ApplySource => {
|
||||
self.apply_source();
|
||||
@@ -3105,7 +2970,6 @@ impl App {
|
||||
return Task::none();
|
||||
}
|
||||
self.page = Page::Text;
|
||||
self.source_find_instance = Id::new_v4();
|
||||
self.source_navigation.open = true;
|
||||
self.source_navigation.refresh(&self.source_editor.text());
|
||||
return iced::widget::operation::focus(if go_to_line {
|
||||
@@ -3114,22 +2978,13 @@ impl App {
|
||||
"source-find"
|
||||
});
|
||||
}
|
||||
Event::Close => {
|
||||
let was_open = self.source_navigation.open;
|
||||
self.source_navigation.open = false;
|
||||
if was_open {
|
||||
self.restore_source_editor_focus();
|
||||
}
|
||||
}
|
||||
Event::Close => self.source_navigation.open = false,
|
||||
Event::Query(query) => {
|
||||
self.source_navigation.query = query;
|
||||
self.source_navigation.refresh(&self.source_editor.text());
|
||||
}
|
||||
Event::Next(backwards) if self.page == Page::Text => {
|
||||
self.source_jump = self.source_navigation.next(backwards);
|
||||
if let Some(cursor) = self.source_jump {
|
||||
self.queue_source_reveal(cursor, false);
|
||||
}
|
||||
if self.source_jump.is_none() {
|
||||
self.status = "No matching source text.".into();
|
||||
}
|
||||
@@ -3140,7 +2995,8 @@ impl App {
|
||||
.destination(self.source_editor.line_count())
|
||||
{
|
||||
Ok(cursor) => {
|
||||
self.queue_source_reveal(cursor, true);
|
||||
self.source_jump = Some(cursor);
|
||||
self.source_select_line = true;
|
||||
}
|
||||
Err(error) => self.status = error,
|
||||
},
|
||||
@@ -3223,9 +3079,6 @@ impl App {
|
||||
}
|
||||
self.sync_source();
|
||||
}
|
||||
if self.path.as_ref() != Some(&receipt.path) {
|
||||
self.source_workspace.reset_navigation_session();
|
||||
}
|
||||
self.disk_project = Some(disk);
|
||||
self.path = Some(receipt.path);
|
||||
self.hosted.session = None;
|
||||
@@ -3437,6 +3290,52 @@ 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()
|
||||
@@ -3454,19 +3353,13 @@ impl App {
|
||||
.get(&diagram)
|
||||
.is_none_or(|d| d.projection.includes(self.create_kind))
|
||||
{
|
||||
let mut p = self.project().clone();
|
||||
for command in &commands {
|
||||
if let Command::CreateDiagram(d) = command {
|
||||
p.diagrams.insert(d.id, d.clone());
|
||||
let mut p = match execute_batch(self.project(), &commands) {
|
||||
Ok(candidate) => candidate,
|
||||
Err(error) => {
|
||||
self.status = error.to_string();
|
||||
return;
|
||||
}
|
||||
}
|
||||
p.elements.insert(
|
||||
id,
|
||||
match &commands[0] {
|
||||
Command::CreateElement(e) => e.clone(),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
);
|
||||
};
|
||||
let owner = if allocated {
|
||||
Some(self.selected)
|
||||
} else {
|
||||
@@ -3784,9 +3677,8 @@ 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.set_reachable(disk.reachable.clone());
|
||||
self.source_workspace.reachable = disk.reachable.clone();
|
||||
self.show_source_file(false);
|
||||
self.source_diagnostics = error.diagnostics().to_vec();
|
||||
self.source_version += 1;
|
||||
@@ -3838,13 +3730,11 @@ impl App {
|
||||
let column = prefix
|
||||
.rfind('\n')
|
||||
.map_or(offset, |start| offset - start - 1);
|
||||
self.queue_source_reveal(
|
||||
text_editor::Cursor {
|
||||
position: text_editor::Position { line, column },
|
||||
selection: None,
|
||||
},
|
||||
true,
|
||||
);
|
||||
self.source_jump = Some(text_editor::Cursor {
|
||||
position: text_editor::Position { line, column },
|
||||
selection: None,
|
||||
});
|
||||
self.source_select_line = true;
|
||||
}
|
||||
|
||||
fn create_source_file(&mut self) -> Result<(), String> {
|
||||
@@ -4454,10 +4344,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::*;
|
||||
|
||||
@@ -213,14 +213,6 @@ impl App {
|
||||
self.pending = None;
|
||||
return Task::none();
|
||||
}
|
||||
if self.path.is_some()
|
||||
|| self.hosted.session.as_ref().is_none_or(|session| {
|
||||
session.resource_id != remote.resource_id
|
||||
|| session.connection.base() != request.connection.base()
|
||||
})
|
||||
{
|
||||
self.source_workspace.reset_navigation_session();
|
||||
}
|
||||
self.hosted.session = Some(Session {
|
||||
connection: request.connection.clone(),
|
||||
resource_id: remote.resource_id,
|
||||
@@ -353,14 +345,6 @@ impl App {
|
||||
match self.document.apply_compiled((*remote.compiled).clone()) {
|
||||
Ok(_) => {
|
||||
self.document.mark_saved(remote.compiled.clone());
|
||||
if self.path.is_some()
|
||||
|| self.hosted.session.as_ref().is_none_or(|session| {
|
||||
session.resource_id != remote.resource_id
|
||||
|| session.connection.base() != connection.base()
|
||||
})
|
||||
{
|
||||
self.source_workspace.reset_navigation_session();
|
||||
}
|
||||
self.hosted.session = Some(Session {
|
||||
connection,
|
||||
resource_id: remote.resource_id,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,219 +0,0 @@
|
||||
//! Keep Iced's single-line input from silently filtering pasted control bytes.
|
||||
//! Only the alias field uses this adapter. The child retains editing and focus.
|
||||
use iced::advanced::{
|
||||
Clipboard, Layout, Shell, Widget, clipboard, layout, overlay, renderer, text, widget,
|
||||
};
|
||||
use iced::{Element, Event, Length, Rectangle, Size, Vector, keyboard, mouse};
|
||||
|
||||
pub(super) fn guard<'a, Message: Clone + 'a, Theme: 'a, R: text::Renderer + 'a>(
|
||||
input: Element<'a, Message, Theme, R>,
|
||||
rejected: Option<Message>,
|
||||
) -> Element<'a, Message, Theme, R> {
|
||||
Element::new(AliasInput { input, rejected })
|
||||
}
|
||||
|
||||
struct AliasInput<'a, Message, Theme, R> {
|
||||
input: Element<'a, Message, Theme, R>,
|
||||
rejected: Option<Message>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
modifiers: keyboard::Modifiers,
|
||||
}
|
||||
|
||||
// Each fresh child clipboard read uses the checked snapshot. Iced may reuse its
|
||||
// previously admitted paste while V is held, until KeyReleased clears that cache.
|
||||
struct CapturedClipboard<'a> {
|
||||
content: Option<String>,
|
||||
original: &'a mut dyn Clipboard,
|
||||
}
|
||||
impl Clipboard for CapturedClipboard<'_> {
|
||||
fn read(&self, kind: clipboard::Kind) -> Option<String> {
|
||||
if kind == clipboard::Kind::Standard {
|
||||
self.content.clone()
|
||||
} else {
|
||||
self.original.read(kind)
|
||||
}
|
||||
}
|
||||
fn write(&mut self, kind: clipboard::Kind, contents: String) {
|
||||
self.original.write(kind, contents);
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message: Clone, Theme, R: text::Renderer> Widget<Message, Theme, R>
|
||||
for AliasInput<'_, Message, Theme, R>
|
||||
{
|
||||
fn size(&self) -> Size<Length> {
|
||||
self.input.as_widget().size()
|
||||
}
|
||||
fn size_hint(&self) -> Size<Length> {
|
||||
self.input.as_widget().size_hint()
|
||||
}
|
||||
fn tag(&self) -> widget::tree::Tag {
|
||||
widget::tree::Tag::of::<State>()
|
||||
}
|
||||
fn state(&self) -> widget::tree::State {
|
||||
widget::tree::State::new(State::default())
|
||||
}
|
||||
fn children(&self) -> Vec<widget::Tree> {
|
||||
vec![widget::Tree::new(self.input.as_widget())]
|
||||
}
|
||||
fn diff(&self, tree: &mut widget::Tree) {
|
||||
tree.diff_children(std::slice::from_ref(&self.input));
|
||||
}
|
||||
fn layout(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
renderer: &R,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
self.input
|
||||
.as_widget_mut()
|
||||
.layout(&mut tree.children[0], renderer, limits)
|
||||
}
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
renderer: &mut R,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.input.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
fn operate(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &R,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
self.input
|
||||
.as_widget_mut()
|
||||
.operate(&mut tree.children[0], layout, renderer, operation);
|
||||
}
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &R,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let focused = tree.children[0]
|
||||
.state
|
||||
.downcast_ref::<iced::widget::text_input::State<R::Paragraph>>()
|
||||
.is_focused();
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
// Iced 0.14.2 uses stored ModifiersChanged state, not KeyPressed.modifiers.
|
||||
// Mirror its Escape reset and its Latin/physical-key paste shortcut.
|
||||
if let Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) = event {
|
||||
state.modifiers = *modifiers;
|
||||
}
|
||||
let paste = matches!(event,
|
||||
Event::Keyboard(keyboard::Event::KeyPressed { key, physical_key, .. })
|
||||
if key.to_latin(*physical_key) == Some('v')
|
||||
&& state.modifiers.command() && !state.modifiers.alt());
|
||||
if focused
|
||||
&& paste
|
||||
&& let Some(rejected) = &self.rejected
|
||||
{
|
||||
let content = clipboard.read(clipboard::Kind::Standard);
|
||||
if content
|
||||
.as_ref()
|
||||
.is_some_and(|value| value.chars().any(char::is_control))
|
||||
{
|
||||
// Do not invoke the child: even an empty replacement would erase
|
||||
// its selection. Approval invalidation is an explicit App event.
|
||||
shell.publish(rejected.clone());
|
||||
shell.capture_event();
|
||||
return;
|
||||
}
|
||||
self.input.as_widget_mut().update(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
layout,
|
||||
cursor,
|
||||
renderer,
|
||||
&mut CapturedClipboard {
|
||||
content,
|
||||
original: clipboard,
|
||||
},
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
return;
|
||||
}
|
||||
self.input.as_widget_mut().update(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
layout,
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
// In Iced 0.14.2 the only KeyPressed branch that removes focus is its
|
||||
// Escape branch, which also resets stored modifiers. Observe that branch
|
||||
// after its text/shortcut early returns and effective-key conversion.
|
||||
if focused
|
||||
&& matches!(event, Event::Keyboard(keyboard::Event::KeyPressed { .. }))
|
||||
&& !tree.children[0]
|
||||
.state
|
||||
.downcast_ref::<iced::widget::text_input::State<R::Paragraph>>()
|
||||
.is_focused()
|
||||
{
|
||||
tree.state.downcast_mut::<State>().modifiers = keyboard::Modifiers::default();
|
||||
}
|
||||
}
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &R,
|
||||
) -> mouse::Interaction {
|
||||
self.input.as_widget().mouse_interaction(
|
||||
&tree.children[0],
|
||||
layout,
|
||||
cursor,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
fn overlay<'a>(
|
||||
&'a mut self,
|
||||
tree: &'a mut widget::Tree,
|
||||
layout: Layout<'a>,
|
||||
renderer: &R,
|
||||
viewport: &Rectangle,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'a, Message, Theme, R>> {
|
||||
self.input.as_widget_mut().overlay(
|
||||
&mut tree.children[0],
|
||||
layout,
|
||||
renderer,
|
||||
viewport,
|
||||
translation,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, debug_assertions))]
|
||||
mod tests;
|
||||
@@ -1,220 +0,0 @@
|
||||
use super::*;
|
||||
use crate::views::source_tests::FontLayout;
|
||||
use iced::keyboard::{
|
||||
Key, Modifiers,
|
||||
key::{Code, Named, Physical},
|
||||
};
|
||||
use std::cell::Cell;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum Signal {
|
||||
Input(String),
|
||||
Rejected,
|
||||
}
|
||||
|
||||
struct ClipboardText {
|
||||
text: String,
|
||||
reads: Cell<usize>,
|
||||
}
|
||||
impl Clipboard for ClipboardText {
|
||||
fn read(&self, _: clipboard::Kind) -> Option<String> {
|
||||
self.reads.set(self.reads.get() + 1);
|
||||
Some(self.text.clone())
|
||||
}
|
||||
fn write(&mut self, _: clipboard::Kind, _: String) {}
|
||||
}
|
||||
type InputState = iced::widget::text_input::State<<FontLayout as text::Renderer>::Paragraph>;
|
||||
|
||||
struct Mounted {
|
||||
input: Element<'static, Signal, iced::Theme, FontLayout>,
|
||||
tree: widget::Tree,
|
||||
node: layout::Node,
|
||||
clipboard: ClipboardText,
|
||||
messages: Vec<Signal>,
|
||||
}
|
||||
impl Mounted {
|
||||
fn new(focused: bool, enabled: bool, modifiers: Modifiers) -> Self {
|
||||
let mut child = iced::widget::text_input("Alias", "send_water");
|
||||
if enabled {
|
||||
child = child.on_input(Signal::Input);
|
||||
}
|
||||
let mut input = guard(child.into(), enabled.then_some(Signal::Rejected));
|
||||
let mut tree = widget::Tree::new(input.as_widget());
|
||||
let node = input.as_widget_mut().layout(
|
||||
&mut tree,
|
||||
&FontLayout,
|
||||
&layout::Limits::new(Size::ZERO, Size::new(500., 40.)),
|
||||
);
|
||||
if focused {
|
||||
let state = tree.children[0].state.downcast_mut::<InputState>();
|
||||
state.focus();
|
||||
state.select_range(0, 10);
|
||||
}
|
||||
let mut mounted = Self {
|
||||
input,
|
||||
tree,
|
||||
node,
|
||||
clipboard: ClipboardText {
|
||||
text: "foo\nbar".into(),
|
||||
reads: Cell::new(0),
|
||||
},
|
||||
messages: vec![],
|
||||
};
|
||||
mounted.event(Event::Keyboard(keyboard::Event::ModifiersChanged(
|
||||
modifiers,
|
||||
)));
|
||||
mounted
|
||||
}
|
||||
fn state(&self) -> &InputState {
|
||||
self.tree.children[0].state.downcast_ref::<InputState>()
|
||||
}
|
||||
fn event(&mut self, event: Event) -> bool {
|
||||
let mut shell = Shell::new(&mut self.messages);
|
||||
self.input.as_widget_mut().update(
|
||||
&mut self.tree,
|
||||
&event,
|
||||
Layout::new(&self.node),
|
||||
mouse::Cursor::Unavailable,
|
||||
&FontLayout,
|
||||
&mut self.clipboard,
|
||||
&mut shell,
|
||||
&Rectangle::new(iced::Point::ORIGIN, Size::new(500., 40.)),
|
||||
);
|
||||
shell.is_event_captured()
|
||||
}
|
||||
}
|
||||
fn key(key: Key, modified_key: Key, code: Code) -> Event {
|
||||
Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key,
|
||||
modified_key,
|
||||
physical_key: Physical::Code(code),
|
||||
location: keyboard::Location::Standard,
|
||||
modifiers: Modifiers::empty(),
|
||||
text: None,
|
||||
repeat: false,
|
||||
})
|
||||
}
|
||||
fn paste() -> Event {
|
||||
key(
|
||||
Key::Character("v".into()),
|
||||
Key::Character("v".into()),
|
||||
Code::KeyV,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paste_refusal_matches_child_focus_enabled_and_stored_modifier_protocol() {
|
||||
for focused in [false, true] {
|
||||
for enabled in [false, true] {
|
||||
for modifiers in [
|
||||
Modifiers::empty(),
|
||||
Modifiers::SHIFT,
|
||||
Modifiers::COMMAND,
|
||||
Modifiers::COMMAND | Modifiers::ALT,
|
||||
Modifiers::COMMAND | Modifiers::SHIFT,
|
||||
] {
|
||||
let mut mounted = Mounted::new(focused, enabled, modifiers);
|
||||
let before = mounted.state().cursor();
|
||||
let expected = focused && enabled && modifiers.command() && !modifiers.alt();
|
||||
let captured = mounted.event(paste());
|
||||
assert_eq!(
|
||||
mounted.messages,
|
||||
if expected {
|
||||
vec![Signal::Rejected]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
);
|
||||
assert_eq!(mounted.clipboard.reads.get(), usize::from(expected));
|
||||
assert_eq!(mounted.state().cursor(), before);
|
||||
if expected {
|
||||
assert!(captured);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_escape_reset_follows_child_after_early_returns() {
|
||||
// Raw Escape mapped to Home must not clear only the guard's Command state.
|
||||
let mut mounted = Mounted::new(true, true, Modifiers::COMMAND);
|
||||
mounted.event(key(
|
||||
Key::Named(Named::Escape),
|
||||
Key::Named(Named::Home),
|
||||
Code::Escape,
|
||||
));
|
||||
assert!(mounted.state().is_focused());
|
||||
assert!(mounted.event(paste()));
|
||||
assert_eq!(mounted.messages, vec![Signal::Rejected]);
|
||||
|
||||
// Effective Escape clears both states; a later focus operation does not
|
||||
// invent a new ModifiersChanged event or revive the previous Command state.
|
||||
let mut mounted = Mounted::new(true, true, Modifiers::COMMAND);
|
||||
mounted.event(key(
|
||||
Key::Named(Named::Home),
|
||||
Key::Named(Named::Escape),
|
||||
Code::Home,
|
||||
));
|
||||
assert!(!mounted.state().is_focused());
|
||||
mounted.tree.children[0]
|
||||
.state
|
||||
.downcast_mut::<InputState>()
|
||||
.focus();
|
||||
mounted.event(paste());
|
||||
assert!(mounted.messages.is_empty());
|
||||
assert_eq!(mounted.clipboard.reads.get(), 0);
|
||||
|
||||
// Printable text returns before effective Escape and leaves modifiers live.
|
||||
let mut mounted = Mounted::new(true, true, Modifiers::COMMAND);
|
||||
let mut event = key(
|
||||
Key::Named(Named::Home),
|
||||
Key::Named(Named::Escape),
|
||||
Code::Home,
|
||||
);
|
||||
if let Event::Keyboard(keyboard::Event::KeyPressed { text, .. }) = &mut event {
|
||||
*text = Some("x".into());
|
||||
}
|
||||
mounted.event(event);
|
||||
assert!(mounted.state().is_focused());
|
||||
assert_eq!(mounted.messages, vec![Signal::Input("x".into())]);
|
||||
mounted.messages.clear();
|
||||
mounted.event(paste());
|
||||
assert_eq!(mounted.messages, vec![Signal::Rejected]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn held_paste_cache_contains_only_previously_admitted_text() {
|
||||
let mut mounted = Mounted::new(true, true, Modifiers::COMMAND);
|
||||
mounted.clipboard.text = "water".into();
|
||||
mounted.event(paste());
|
||||
assert_eq!(mounted.messages, vec![Signal::Input("water".into())]);
|
||||
mounted.messages.clear();
|
||||
mounted.clipboard.text = "other".into();
|
||||
mounted.event(paste());
|
||||
assert_eq!(
|
||||
mounted.messages,
|
||||
vec![Signal::Input("waterwater".into())],
|
||||
"Iced reuses its first admitted paste while the key is held"
|
||||
);
|
||||
mounted.messages.clear();
|
||||
let before = mounted.state().cursor();
|
||||
mounted.clipboard.text = "bad\tvalue".into();
|
||||
mounted.event(paste());
|
||||
assert_eq!(mounted.messages, vec![Signal::Rejected]);
|
||||
assert_eq!(mounted.state().cursor(), before);
|
||||
mounted.messages.clear();
|
||||
mounted.event(Event::Keyboard(keyboard::Event::KeyReleased {
|
||||
key: Key::Character("v".into()),
|
||||
modified_key: Key::Character("v".into()),
|
||||
physical_key: Physical::Code(Code::KeyV),
|
||||
location: keyboard::Location::Standard,
|
||||
modifiers: Modifiers::empty(),
|
||||
}));
|
||||
mounted.clipboard.text = "later".into();
|
||||
mounted.event(paste());
|
||||
assert_eq!(
|
||||
mounted.messages,
|
||||
vec![Signal::Input("waterwaterlater".into())]
|
||||
);
|
||||
}
|
||||
@@ -1,303 +0,0 @@
|
||||
//! Display-only reuse after a guarded alias rename. This is not model authority.
|
||||
use iced::{advanced::text::Renderer, widget::text_editor};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Replace exact, unchanged-line-count source in an existing editor.
|
||||
///
|
||||
/// The caller must first authorize/commit the model change and must reconstruct
|
||||
/// from accepted source on false. Preflight failures do not mutate the editor;
|
||||
/// a failed final readback can leave it changed. On changing success the cursor
|
||||
/// is at 0:0 without selection; the caller restores its approved origin/reveal.
|
||||
/// Direct Content actions deliberately bypass App draft edits and Undo history.
|
||||
pub(super) fn replace<R: Renderer>(
|
||||
editor: &mut text_editor::Content<R>,
|
||||
before: &str,
|
||||
after: &str,
|
||||
) -> bool {
|
||||
if editor.text() != before {
|
||||
return false;
|
||||
}
|
||||
let Some(old_lines) = lines(before) else {
|
||||
return false;
|
||||
};
|
||||
let Some(new_lines) = lines(after) else {
|
||||
return false;
|
||||
};
|
||||
if old_lines.len() != new_lines.len() || old_lines.len() != editor.line_count() {
|
||||
return false;
|
||||
}
|
||||
let mut patches = Vec::new();
|
||||
for (line, ((old, old_ending), (new, new_ending))) in
|
||||
old_lines.into_iter().zip(new_lines).enumerate()
|
||||
{
|
||||
let Some(actual) = editor.line(line) else {
|
||||
return false;
|
||||
};
|
||||
// Cosmic also recognizes bare CR and LFCR. Comparing the actual lines
|
||||
// catches its greedy LFCR interpretation of e.g. "x\n\r\ny" too.
|
||||
if old_ending != new_ending || actual.text != old || actual.ending != old_ending {
|
||||
return false;
|
||||
}
|
||||
if old == new {
|
||||
continue;
|
||||
}
|
||||
let mut start = old
|
||||
.bytes()
|
||||
.zip(new.bytes())
|
||||
.take_while(|(a, b)| a == b)
|
||||
.count();
|
||||
while !old.is_char_boundary(start) || !new.is_char_boundary(start) {
|
||||
start -= 1;
|
||||
}
|
||||
let mut suffix = old[start..]
|
||||
.bytes()
|
||||
.rev()
|
||||
.zip(new[start..].bytes().rev())
|
||||
.take_while(|(a, b)| a == b)
|
||||
.count();
|
||||
while !old.is_char_boundary(old.len() - suffix) || !new.is_char_boundary(new.len() - suffix)
|
||||
{
|
||||
suffix -= 1;
|
||||
}
|
||||
// All positions and owned payloads are prepared before the first edit.
|
||||
patches.push((
|
||||
line,
|
||||
start,
|
||||
old.len() - suffix,
|
||||
Arc::new(new[start..new.len() - suffix].to_owned()),
|
||||
));
|
||||
}
|
||||
if patches.is_empty() {
|
||||
return true;
|
||||
}
|
||||
let empty = Arc::new(String::new());
|
||||
for (line, start, end, replacement) in patches.into_iter().rev() {
|
||||
select(editor, line, start, end);
|
||||
editor.perform(text_editor::Action::Edit(text_editor::Edit::Paste(
|
||||
replacement,
|
||||
)));
|
||||
}
|
||||
// Iced overwrites its earliest-change marker on each Edit. An explicit
|
||||
// zero-width selection + empty Paste changes no bytes, but marks line zero.
|
||||
// This also refreshes previously unhighlighted lines before the first patch.
|
||||
// It resets only line zero's text cache; other unchanged lines retain theirs.
|
||||
select(editor, 0, 0, 0);
|
||||
editor.perform(text_editor::Action::Edit(text_editor::Edit::Paste(empty)));
|
||||
editor.text() == after
|
||||
}
|
||||
|
||||
fn select<R: Renderer>(
|
||||
editor: &mut text_editor::Content<R>,
|
||||
line: usize,
|
||||
start: usize,
|
||||
end: usize,
|
||||
) {
|
||||
editor.move_to(text_editor::Cursor {
|
||||
position: text_editor::Position {
|
||||
line,
|
||||
column: start,
|
||||
},
|
||||
// move_to(None) does not clear a previous selection in this Iced version.
|
||||
selection: Some(text_editor::Position { line, column: end }),
|
||||
});
|
||||
}
|
||||
|
||||
fn lines(source: &str) -> Option<Vec<(&str, text_editor::LineEnding)>> {
|
||||
use text_editor::LineEnding;
|
||||
let mut result = Vec::new();
|
||||
let mut parts = source.split('\n').peekable();
|
||||
while let Some(part) = parts.next() {
|
||||
let (text, ending) = if parts.peek().is_none() {
|
||||
(part, LineEnding::None)
|
||||
} else if let Some(text) = part.strip_suffix('\r') {
|
||||
(text, LineEnding::CrLf)
|
||||
} else {
|
||||
(part, LineEnding::Lf)
|
||||
};
|
||||
if text.contains('\r')
|
||||
|| (text.is_empty()
|
||||
&& ending == LineEnding::CrLf
|
||||
&& result
|
||||
.last()
|
||||
.is_some_and(|(_, previous)| *previous == LineEnding::Lf))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
result.push((text, ending));
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
|
||||
#[cfg(all(test, debug_assertions))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::views::source_tests::FontLayout;
|
||||
use iced::advanced::{layout, text::Highlighter, widget::Tree};
|
||||
use iced::{Element, Font, Length, Size};
|
||||
use std::{cell::RefCell, ops::Range};
|
||||
|
||||
#[test]
|
||||
fn exact_text_matrix_and_reverse_preserve_endings() {
|
||||
for (before, after) in [
|
||||
("", ""),
|
||||
("", "alias"),
|
||||
("alias", ""),
|
||||
("same", "same"),
|
||||
("a\n", "alias\n"),
|
||||
("a\r\n", "alias\r\n"),
|
||||
("\r\na\r\n\r\n", "\r\nlong_alias\r\n\r\n"),
|
||||
("\na\n\n", "\nlong_alias\n\n"),
|
||||
("a\r\nb\nc", "long_a\r\nlong_b\nlong_c"),
|
||||
(
|
||||
"雨🙂 e\u{301} @a @a // a",
|
||||
"雨🙂 e\u{301} @a_long @a_long // a",
|
||||
),
|
||||
("@abc", "@abcdef"),
|
||||
("@abcdef", "@abc"),
|
||||
("@abc_suffix", "@def_suffix"),
|
||||
("@none", "@identifier"),
|
||||
// Shared prefix/suffix bytes inside distinct UTF-8 codepoints must
|
||||
// not be used as String split positions, even outside alias scope.
|
||||
("雨xé", "雪xê"),
|
||||
("é", "©"),
|
||||
(
|
||||
"unchanged\n@a\nunchanged\n@a\n",
|
||||
"unchanged\n@long\nunchanged\n@long\n",
|
||||
),
|
||||
] {
|
||||
let mut editor = text_editor::Content::<FontLayout>::with_text(before);
|
||||
assert!(
|
||||
replace(&mut editor, before, after),
|
||||
"{before:?} -> {after:?}"
|
||||
);
|
||||
assert_eq!(editor.text(), after);
|
||||
let expected = text_editor::Content::<FontLayout>::with_text(after);
|
||||
assert_eq!(editor.line_count(), expected.line_count());
|
||||
for (actual, expected) in editor.lines().zip(expected.lines()) {
|
||||
assert_eq!(actual.text, expected.text);
|
||||
assert_eq!(actual.ending, expected.ending);
|
||||
}
|
||||
if before != after {
|
||||
assert_eq!(
|
||||
editor.cursor().position,
|
||||
text_editor::Position { line: 0, column: 0 }
|
||||
);
|
||||
assert_eq!(editor.cursor().selection, None);
|
||||
}
|
||||
assert!(replace(&mut editor, after, before));
|
||||
assert_eq!(editor.text(), before);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_preflight_refusals_leave_text_cursor_and_selection_unchanged() {
|
||||
for (actual, before, after) in [
|
||||
("live", "stale", "changed"),
|
||||
("a\nb", "a\nb", "long\nb\nc"),
|
||||
("a\nb", "a\nb", "long\r\nb"),
|
||||
// Refusal on a later line must not apply the earlier planned edit.
|
||||
("a\nb\r\n", "a\nb\r\n", "long\nb\n"),
|
||||
("a\rb", "a\rb", "long\rb"),
|
||||
("a\n\rb", "a\n\rb", "long\n\rb"),
|
||||
("a\n\r\nb", "a\n\r\nb", "long\n\r\nb"),
|
||||
// The replacement must not introduce a greedy LFCR boundary either.
|
||||
("a\nx\r\n", "a\nx\r\n", "a\n\r\n"),
|
||||
("a", "a", "long\r"),
|
||||
] {
|
||||
let mut editor = text_editor::Content::<FontLayout>::with_text(actual);
|
||||
select(&mut editor, 0, 0, 1);
|
||||
let cursor = editor.cursor();
|
||||
let selection = editor.selection();
|
||||
assert!(
|
||||
!replace(&mut editor, before, after),
|
||||
"{before:?} -> {after:?}"
|
||||
);
|
||||
assert_eq!(editor.text(), actual);
|
||||
assert_eq!(editor.cursor(), cursor);
|
||||
assert_eq!(editor.selection(), selection);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unchanged_content_preserves_selection_and_next_direct_edit_works() {
|
||||
let mut editor = text_editor::Content::<FontLayout>::with_text("@a");
|
||||
select(&mut editor, 0, 1, 2);
|
||||
let cursor = editor.cursor();
|
||||
assert!(replace(&mut editor, "@a", "@a"));
|
||||
assert_eq!(editor.cursor(), cursor);
|
||||
assert!(replace(&mut editor, "@a", "@long"));
|
||||
editor.move_to(text_editor::Cursor {
|
||||
position: text_editor::Position { line: 0, column: 5 },
|
||||
selection: None,
|
||||
});
|
||||
editor.perform(text_editor::Action::Edit(text_editor::Edit::Insert('!')));
|
||||
assert_eq!(editor.text(), "@long!");
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static CHANGES: RefCell<Vec<usize>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
struct Tracked(crate::highlight::ArcadiaHighlighter);
|
||||
impl Highlighter for Tracked {
|
||||
type Settings = Arc<str>;
|
||||
type Highlight = oa_core::language::TokenClass;
|
||||
type Iterator<'a> = std::vec::IntoIter<(Range<usize>, Self::Highlight)>;
|
||||
fn new(settings: &Self::Settings) -> Self {
|
||||
Self(crate::highlight::ArcadiaHighlighter::new(settings))
|
||||
}
|
||||
fn update(&mut self, settings: &Self::Settings) {
|
||||
self.0.update(settings);
|
||||
}
|
||||
fn change_line(&mut self, line: usize) {
|
||||
CHANGES.with(|changes| changes.borrow_mut().push(line));
|
||||
self.0.change_line(line);
|
||||
}
|
||||
fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> {
|
||||
self.0.highlight_line(line)
|
||||
}
|
||||
fn current_line(&self) -> usize {
|
||||
self.0.current_line()
|
||||
}
|
||||
}
|
||||
|
||||
fn view(
|
||||
editor: &text_editor::Content<FontLayout>,
|
||||
source: Arc<str>,
|
||||
) -> Element<'_, (), iced::Theme, FontLayout> {
|
||||
text_editor(editor)
|
||||
.font(Font::MONOSPACE)
|
||||
.size(14)
|
||||
.height(Length::Fill)
|
||||
.highlight_with::<Tracked>(source, crate::highlight::format)
|
||||
.into()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn already_laid_out_editor_notifies_highlighter_from_zero_after_later_line_patch() {
|
||||
let before = "// earlier\n// unmodified\n@a";
|
||||
let after = "// earlier\n// unmodified\n@none";
|
||||
let mut editor = text_editor::Content::<FontLayout>::with_text(before);
|
||||
let limits = layout::Limits::new(Size::ZERO, Size::new(400., 60.));
|
||||
let mut tree = {
|
||||
let mut widget = view(&editor, Arc::from(before));
|
||||
let mut tree = Tree::new(widget.as_widget());
|
||||
let _ = widget
|
||||
.as_widget_mut()
|
||||
.layout(&mut tree, &FontLayout, &limits);
|
||||
tree
|
||||
};
|
||||
CHANGES.with(|changes| changes.borrow_mut().clear());
|
||||
assert!(replace(&mut editor, before, after));
|
||||
{
|
||||
let mut widget = view(&editor, Arc::from(after));
|
||||
let _ = widget
|
||||
.as_widget_mut()
|
||||
.layout(&mut tree, &FontLayout, &limits);
|
||||
}
|
||||
// Font/metrics/bounds are already stable. This notification must come
|
||||
// from the helper's empty edit, not a fresh renderer initialization.
|
||||
CHANGES.with(|changes| assert_eq!(*changes.borrow(), [0]));
|
||||
assert_eq!(editor.text(), after);
|
||||
}
|
||||
}
|
||||
@@ -1,449 +0,0 @@
|
||||
//! Real paragraph advances and clipping through the actual preview dialog.
|
||||
//! Native screenshots additionally check raster ink and the platform renderer.
|
||||
use super::*;
|
||||
use crate::views::source_tests::FontLayout;
|
||||
use iced::advanced::{Layout, layout, renderer, text as shaped, widget};
|
||||
use iced::{Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, Vector};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
struct Glyph {
|
||||
text: String,
|
||||
paragraph_y: u32,
|
||||
line: usize,
|
||||
bytes: std::ops::Range<usize>,
|
||||
bounds: Rectangle,
|
||||
clip: Rectangle,
|
||||
}
|
||||
|
||||
struct GlyphRecorder {
|
||||
wanted: BTreeSet<String>,
|
||||
transforms: Vec<Transformation>,
|
||||
clips: Vec<Rectangle>,
|
||||
glyphs: Vec<Glyph>,
|
||||
quads: Vec<Rectangle>,
|
||||
}
|
||||
impl GlyphRecorder {
|
||||
fn new(wanted: BTreeSet<String>, viewport: Rectangle) -> Self {
|
||||
Self {
|
||||
wanted,
|
||||
transforms: vec![Transformation::IDENTITY],
|
||||
clips: vec![viewport],
|
||||
glyphs: vec![],
|
||||
quads: vec![],
|
||||
}
|
||||
}
|
||||
fn transform(&self) -> Transformation {
|
||||
*self.transforms.last().unwrap()
|
||||
}
|
||||
fn clip(&self, bounds: Rectangle) -> Rectangle {
|
||||
self.clips
|
||||
.last()
|
||||
.unwrap()
|
||||
.intersection(&(bounds * self.transform()))
|
||||
.unwrap_or(Rectangle::with_size(Size::ZERO))
|
||||
}
|
||||
}
|
||||
impl renderer::Renderer for GlyphRecorder {
|
||||
fn start_layer(&mut self, bounds: Rectangle) {
|
||||
self.clips.push(self.clip(bounds));
|
||||
}
|
||||
fn end_layer(&mut self) {
|
||||
self.clips.pop().unwrap();
|
||||
}
|
||||
fn start_transformation(&mut self, transform: Transformation) {
|
||||
self.transforms.push(self.transform() * transform);
|
||||
}
|
||||
fn end_transformation(&mut self) {
|
||||
self.transforms.pop().unwrap();
|
||||
}
|
||||
fn reset(&mut self, bounds: Rectangle) {
|
||||
self.transforms = vec![Transformation::IDENTITY];
|
||||
self.clips = vec![bounds];
|
||||
self.glyphs.clear();
|
||||
self.quads.clear();
|
||||
}
|
||||
fn fill_quad(&mut self, quad: renderer::Quad, _: impl Into<Background>) {
|
||||
self.quads.push(quad.bounds * self.transform());
|
||||
}
|
||||
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 FontLayout, handle, callback);
|
||||
}
|
||||
}
|
||||
impl shaped::Renderer for GlyphRecorder {
|
||||
type Font = Font;
|
||||
type Paragraph = <FontLayout as shaped::Renderer>::Paragraph;
|
||||
type Editor = <FontLayout as shaped::Renderer>::Editor;
|
||||
const ICON_FONT: Font = <FontLayout as shaped::Renderer>::ICON_FONT;
|
||||
const CHECKMARK_ICON: char = <FontLayout as shaped::Renderer>::CHECKMARK_ICON;
|
||||
const ARROW_DOWN_ICON: char = <FontLayout as shaped::Renderer>::ARROW_DOWN_ICON;
|
||||
const SCROLL_UP_ICON: char = <FontLayout as shaped::Renderer>::SCROLL_UP_ICON;
|
||||
const SCROLL_DOWN_ICON: char = <FontLayout as shaped::Renderer>::SCROLL_DOWN_ICON;
|
||||
const SCROLL_LEFT_ICON: char = <FontLayout as shaped::Renderer>::SCROLL_LEFT_ICON;
|
||||
const SCROLL_RIGHT_ICON: char = <FontLayout as shaped::Renderer>::SCROLL_RIGHT_ICON;
|
||||
const ICED_LOGO: char = <FontLayout as shaped::Renderer>::ICED_LOGO;
|
||||
fn default_font(&self) -> Font {
|
||||
shaped::Renderer::default_font(&FontLayout)
|
||||
}
|
||||
fn default_size(&self) -> Pixels {
|
||||
shaped::Renderer::default_size(&FontLayout)
|
||||
}
|
||||
fn fill_paragraph(
|
||||
&mut self,
|
||||
paragraph: &Self::Paragraph,
|
||||
position: Point,
|
||||
_: Color,
|
||||
clip: Rectangle,
|
||||
) {
|
||||
let buffer = paragraph.buffer();
|
||||
let text = buffer
|
||||
.lines
|
||||
.iter()
|
||||
.map(|line| line.text())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
if !self.wanted.contains(&text) {
|
||||
return;
|
||||
}
|
||||
let clip = self.clip(clip);
|
||||
for run in buffer.layout_runs() {
|
||||
for glyph in run.glyphs {
|
||||
if run.text[glyph.start..glyph.end]
|
||||
.chars()
|
||||
.all(char::is_whitespace)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let bounds = Rectangle {
|
||||
x: position.x + glyph.x + glyph.x_offset * glyph.font_size,
|
||||
y: position.y + run.line_top,
|
||||
width: glyph.w,
|
||||
height: run.line_height,
|
||||
} * self.transform();
|
||||
self.glyphs.push(Glyph {
|
||||
text: text.clone(),
|
||||
paragraph_y: position.y.to_bits(),
|
||||
line: run.line_i,
|
||||
bytes: glyph.start..glyph.end,
|
||||
bounds,
|
||||
clip,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
fn fill_editor(
|
||||
&mut self,
|
||||
editor: &Self::Editor,
|
||||
position: Point,
|
||||
color: Color,
|
||||
clip: Rectangle,
|
||||
) {
|
||||
shaped::Renderer::fill_editor(&mut FontLayout, editor, position, color, clip);
|
||||
}
|
||||
fn fill_text(&mut self, text: shaped::Text, position: Point, color: Color, clip: Rectangle) {
|
||||
shaped::Renderer::fill_text(&mut FontLayout, text, position, color, clip);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ScrollGeometry {
|
||||
id: Option<widget::Id>,
|
||||
bounds: Option<Rectangle>,
|
||||
content: Option<Rectangle>,
|
||||
offset: Vector,
|
||||
}
|
||||
impl widget::Operation for ScrollGeometry {
|
||||
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) {
|
||||
visit(self);
|
||||
}
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
id: Option<&widget::Id>,
|
||||
bounds: Rectangle,
|
||||
content: Rectangle,
|
||||
offset: Vector,
|
||||
_: &mut dyn widget::operation::Scrollable,
|
||||
) {
|
||||
if id == self.id.as_ref() {
|
||||
self.bounds = Some(bounds);
|
||||
self.content = Some(content);
|
||||
self.offset = offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
fn normalized(text: &str) -> String {
|
||||
text.replace("\r\n", "\n").replace('\r', "\n")
|
||||
}
|
||||
fn required_chars(text: &str) -> BTreeSet<(usize, usize)> {
|
||||
text.split('\n')
|
||||
.enumerate()
|
||||
.flat_map(|(line, text)| {
|
||||
text.char_indices()
|
||||
.filter(|(_, ch)| !ch.is_whitespace())
|
||||
.map(move |(byte, _)| (line, byte))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn declarations(long: bool) -> App {
|
||||
let name = if long {
|
||||
format!("Assess {} request", "界🚀".repeat(90))
|
||||
} else {
|
||||
"Assess 救援 request".into()
|
||||
};
|
||||
let source = SourceBundle { entry: "project.arcadia".into(), files: BTreeMap::from([
|
||||
("project.arcadia".into(), "arcadia 7;\r\nproject navigation_trial \"Navigation — 救援\" id \"9e8f0000-0000-4000-8000-000000000001\" {\r\n include \"救援/assess.arcadia\";\r\n}\r\n".into()),
|
||||
("救援/assess.arcadia".into(), format!("// Comment @assess must not be a lexical use.\r\noa_activity assess \"{name}\" id \"9e8f0000-0000-4000-8000-000000000002\" {{\r\n description \"Keep @assess as prose, not a use.\";\r\n}}\r\n")),
|
||||
]) };
|
||||
let mut app = fixture();
|
||||
app.document = Document::from_bundle(source, LIMITS).unwrap();
|
||||
app.source_workspace = Workspace::new(app.document.bundle(), true);
|
||||
app.source_workspace.active = "救援/assess.arcadia".into();
|
||||
app.selected = app.project().id;
|
||||
app.refresh_draft();
|
||||
app.show_source_file(false);
|
||||
let start = app
|
||||
.source_workspace
|
||||
.text()
|
||||
.find("oa_activity assess")
|
||||
.unwrap()
|
||||
+ "oa_activity ".len();
|
||||
app.source_editor.move_to(cursor(
|
||||
app.source_workspace.text(),
|
||||
start,
|
||||
start + "assess".len(),
|
||||
));
|
||||
begin(&mut app);
|
||||
let replacement = if long {
|
||||
"a".repeat(255)
|
||||
} else {
|
||||
"assess_damage".into()
|
||||
};
|
||||
input(&mut app, &replacement);
|
||||
let _ = preview(&mut app);
|
||||
app
|
||||
}
|
||||
|
||||
fn assert_page_glyphs_visible(app: &App, host: Size) {
|
||||
let form = app.source_rename.form.as_ref().unwrap();
|
||||
let ready = form.approval.as_ref().unwrap();
|
||||
let file = &ready.preview.files()[form.file];
|
||||
let mut expected = BTreeMap::<String, usize>::new();
|
||||
for line in file.hunks()[form.hunk]
|
||||
.lines()
|
||||
.iter()
|
||||
.skip(form.line)
|
||||
.take(LINES_PER_PAGE)
|
||||
{
|
||||
for source in [
|
||||
line.before_text(&ready.prepared.before().bundle.files[file.path()])
|
||||
.unwrap(),
|
||||
line.after_text(&ready.prepared.after().bundle.files[file.path()])
|
||||
.unwrap(),
|
||||
] {
|
||||
if let Some(fragment) = preview::text_segment(source, form.segment) {
|
||||
let text = normalized(fragment);
|
||||
if !required_chars(&text).is_empty() {
|
||||
*expected.entry(text).or_default() += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(!expected.is_empty());
|
||||
let viewport = Rectangle::new(Point::ORIGIN, host);
|
||||
let mut renderer = GlyphRecorder::new(expected.keys().cloned().collect(), viewport);
|
||||
let editor = text_editor::Content::<GlyphRecorder>::new();
|
||||
let mut view = dialog_content(app, &editor);
|
||||
let mut tree = widget::Tree::new(view.as_widget());
|
||||
let node =
|
||||
view.as_widget_mut()
|
||||
.layout(&mut tree, &renderer, &layout::Limits::new(Size::ZERO, host));
|
||||
let mut geometry = ScrollGeometry {
|
||||
id: Some(preview_scroll_id(form, ready.id)),
|
||||
..ScrollGeometry::default()
|
||||
};
|
||||
view.as_widget_mut()
|
||||
.operate(&mut tree, Layout::new(&node), &renderer, &mut geometry);
|
||||
let bounds = geometry.bounds.unwrap();
|
||||
let content = geometry.content.unwrap();
|
||||
assert!(bounds.height >= 40.);
|
||||
let maximum = (content.height - bounds.height).max(0.);
|
||||
let steps = (maximum / (bounds.height * 0.5)).ceil() as usize;
|
||||
let mut visible = BTreeMap::<(String, u32), BTreeSet<(usize, usize)>>::new();
|
||||
let mut reserved_bar_gap = true;
|
||||
for step in 0..=steps {
|
||||
let offset = (step as f32 * bounds.height * 0.5).min(maximum);
|
||||
view.as_widget_mut().operate(
|
||||
&mut tree,
|
||||
Layout::new(&node),
|
||||
&renderer,
|
||||
&mut widget::operation::scrollable::scroll_to(
|
||||
preview_scroll_id(form, ready.id),
|
||||
widget::operation::scrollable::AbsoluteOffset {
|
||||
x: None,
|
||||
y: Some(offset),
|
||||
},
|
||||
),
|
||||
);
|
||||
view.as_widget_mut()
|
||||
.operate(&mut tree, Layout::new(&node), &renderer, &mut geometry);
|
||||
assert!(
|
||||
(geometry.offset.y - offset).abs() <= 0.5,
|
||||
"actual scroll operation reaches the requested source offset"
|
||||
);
|
||||
renderer::Renderer::reset(&mut renderer, viewport);
|
||||
view.as_widget().draw(
|
||||
&tree,
|
||||
&mut renderer,
|
||||
&iced::Theme::Light,
|
||||
&renderer::Style::default(),
|
||||
Layout::new(&node),
|
||||
iced::mouse::Cursor::Unavailable,
|
||||
&viewport,
|
||||
);
|
||||
// The vertical scrollbar's actual painted thumb fixes its excluded x
|
||||
// region; the outer scroll clip alone cannot detect overpainting.
|
||||
let scrollbar_left = renderer
|
||||
.quads
|
||||
.iter()
|
||||
.filter(|quad| {
|
||||
quad.width > 0.
|
||||
&& quad.width <= 16.
|
||||
&& quad.height > 0.
|
||||
&& quad.x >= bounds.x + bounds.width - 18.
|
||||
&& quad.x + quad.width <= bounds.x + bounds.width + 0.5
|
||||
&& quad.y >= bounds.y - 0.5
|
||||
&& quad.y + quad.height <= bounds.y + bounds.height + 0.5
|
||||
})
|
||||
.map(|quad| quad.x)
|
||||
.min_by(f32::total_cmp);
|
||||
if maximum > 0. {
|
||||
let left = scrollbar_left.expect("overflowing actual preview must paint its scrollbar");
|
||||
reserved_bar_gap &= content.x + content.width <= left - 7.5;
|
||||
}
|
||||
for glyph in &renderer.glyphs {
|
||||
let right = glyph.bounds.x + glyph.bounds.width;
|
||||
if let Some(left) = scrollbar_left {
|
||||
assert!(
|
||||
right <= left - 0.5,
|
||||
"source glyph under scrollbar: text={:?} cluster={:?} glyph={:?} bar x={left}",
|
||||
glyph.text,
|
||||
glyph.bytes,
|
||||
glyph.bounds
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
glyph.bounds.x >= content.x - 0.5 && right <= content.x + content.width + 0.5,
|
||||
"source glyph escaped reserved width: {:?} {:?}",
|
||||
glyph.text,
|
||||
glyph.bounds
|
||||
);
|
||||
assert!(
|
||||
glyph.bounds.x >= glyph.clip.x - 0.5
|
||||
&& right <= glyph.clip.x + glyph.clip.width + 0.5,
|
||||
"source glyph horizontally clipped: {:?} {:?} {:?}",
|
||||
glyph.text,
|
||||
glyph.bounds,
|
||||
glyph.clip
|
||||
);
|
||||
if glyph.bounds.y >= glyph.clip.y - 0.5
|
||||
&& glyph.bounds.y + glyph.bounds.height <= glyph.clip.y + glyph.clip.height + 0.5
|
||||
{
|
||||
let logical_line = glyph.text.split('\n').nth(glyph.line).unwrap();
|
||||
let chars = logical_line
|
||||
.char_indices()
|
||||
.filter(|(byte, ch)| glyph.bytes.contains(byte) && !ch.is_whitespace())
|
||||
.map(|(byte, _)| (glyph.line, byte));
|
||||
visible
|
||||
.entry((glyph.text.clone(), glyph.paragraph_y))
|
||||
.or_default()
|
||||
.extend(chars);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
reserved_bar_gap,
|
||||
"source content must reserve actual scrollbar width plus gap"
|
||||
);
|
||||
assert!(
|
||||
bounds.width >= host.width - 30.,
|
||||
"preview must use available dialog width: {bounds:?} / {host:?}"
|
||||
);
|
||||
for (text, count) in expected {
|
||||
let required = required_chars(&text);
|
||||
let instances: Vec<_> = visible
|
||||
.iter()
|
||||
.filter(|((value, _), _)| value == &text)
|
||||
.collect();
|
||||
assert_eq!(
|
||||
instances.len(),
|
||||
count,
|
||||
"all before/after occurrences must be seen: {text:?}"
|
||||
);
|
||||
for (_, seen) in instances {
|
||||
assert_eq!(
|
||||
seen, &required,
|
||||
"every non-whitespace source cluster must be fully visible at some vertical offset: {text:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn actual_preview_source_glyphs_clear_scrollbar_and_all_long_fragments_are_reachable() {
|
||||
for long in [false, true] {
|
||||
let mut app = declarations(long);
|
||||
let ready = app
|
||||
.source_rename
|
||||
.form
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.approval
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.clone();
|
||||
let invocation = app.source_rename.form.as_ref().unwrap().invocation;
|
||||
let before = evidence(&app);
|
||||
for (width, height) in [(1100., 700.), (580., 700.)] {
|
||||
app.window_size = Size::new(width, height);
|
||||
let host = Size::new((width - 84.).min(708.), height - 120.);
|
||||
let total = segment_total(app.source_rename.form.as_ref().unwrap());
|
||||
if long {
|
||||
assert!(
|
||||
total >= 3,
|
||||
"include unbroken ASCII and wide Unicode across segments"
|
||||
);
|
||||
}
|
||||
for segment in 0..total {
|
||||
assert_page_glyphs_visible(&app, host);
|
||||
if segment + 1 < total {
|
||||
event(&mut app, Event::Segment(invocation, ready.id, true));
|
||||
}
|
||||
}
|
||||
for _ in 1..total {
|
||||
event(&mut app, Event::Segment(invocation, ready.id, false));
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
evidence(&app),
|
||||
before,
|
||||
"preview geometry/paging cannot alter transaction inputs"
|
||||
);
|
||||
assert_eq!(
|
||||
app.source_rename
|
||||
.form
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.approval
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.id,
|
||||
ready.id
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,310 +0,0 @@
|
||||
//! Execute the controller's scroll operations on a retained, scrolled dialog.
|
||||
use super::*;
|
||||
use crate::views::source_tests::FontLayout;
|
||||
use iced::advanced::{Layout, layout, widget};
|
||||
use iced::futures::StreamExt;
|
||||
use iced::{Rectangle, Size, Vector};
|
||||
|
||||
const HOST: Size = Size::new(720., 660.);
|
||||
|
||||
fn paged_fixture() -> App {
|
||||
let mut source = bundle();
|
||||
let name = "Long engineering name 雨 ".repeat(18);
|
||||
source.files.get_mut("defs.arcadia").unwrap().replace_range(
|
||||
..,
|
||||
&format!(
|
||||
"oa_activity pump \"{name}\" id \"00000000-0000-0000-0000-000000000002\" {{}}\r\n\
|
||||
oa_activity backup \"Backup\" id \"00000000-0000-0000-0000-000000000004\" {{}}\r\n"
|
||||
),
|
||||
);
|
||||
let mut uses = String::new();
|
||||
for index in 0..16 {
|
||||
if index == 12 {
|
||||
uses.push_str(&"// unchanged gap\r\n".repeat(10));
|
||||
}
|
||||
uses.push_str(&format!(
|
||||
"interaction use_{index} \"{name}\" id \"00000000-0000-0000-0001-{index:012}\" {{ source @pump; target @backup; }}\r\n"
|
||||
));
|
||||
}
|
||||
source.files.insert("uses.arcadia".into(), uses);
|
||||
let mut app = fixture();
|
||||
app.window_size = HOST;
|
||||
app.document = Document::from_bundle(source, LIMITS).unwrap();
|
||||
app.source_workspace = Workspace::new(app.document.bundle(), true);
|
||||
app.source_workspace.active = "uses.arcadia".into();
|
||||
app.show_source_file(false);
|
||||
let start = app.source_workspace.text().find("source @pump").unwrap() + "source @".len();
|
||||
app.source_editor
|
||||
.move_to(cursor(app.source_workspace.text(), start, start + 4));
|
||||
begin(&mut app);
|
||||
input(&mut app, "pump_long");
|
||||
let _ = preview(&mut app);
|
||||
app
|
||||
}
|
||||
|
||||
fn scroll_id(app: &App) -> widget::Id {
|
||||
let form = app.source_rename.form.as_ref().unwrap();
|
||||
preview_scroll_id(form, form.approval.as_ref().unwrap().id)
|
||||
}
|
||||
|
||||
struct Geometry {
|
||||
id: widget::Id,
|
||||
offset: Option<Vector>,
|
||||
}
|
||||
impl widget::Operation for Geometry {
|
||||
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) {
|
||||
visit(self);
|
||||
}
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
id: Option<&widget::Id>,
|
||||
_: Rectangle,
|
||||
_: Rectangle,
|
||||
offset: Vector,
|
||||
_: &mut dyn widget::operation::Scrollable,
|
||||
) {
|
||||
if id == Some(&self.id) {
|
||||
self.offset = Some(offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Mounted {
|
||||
editor: text_editor::Content<FontLayout>,
|
||||
tree: widget::Tree,
|
||||
}
|
||||
impl Mounted {
|
||||
fn new(app: &App) -> Self {
|
||||
let editor = text_editor::Content::<FontLayout>::new();
|
||||
let tree = widget::Tree::new(dialog_content(app, &editor).as_widget());
|
||||
Self { editor, tree }
|
||||
}
|
||||
fn operate(&mut self, app: &App, operation: &mut dyn widget::Operation) {
|
||||
let mut view = dialog_content(app, &self.editor);
|
||||
// Preserve the actual scroll state across the new page's view diff.
|
||||
self.tree.diff(view.as_widget());
|
||||
let node = view.as_widget_mut().layout(
|
||||
&mut self.tree,
|
||||
&FontLayout,
|
||||
&layout::Limits::new(Size::ZERO, HOST),
|
||||
);
|
||||
view.as_widget_mut()
|
||||
.operate(&mut self.tree, Layout::new(&node), &FontLayout, operation);
|
||||
}
|
||||
fn offset(&mut self, app: &App) -> f32 {
|
||||
let mut geometry = Geometry {
|
||||
id: scroll_id(app),
|
||||
offset: None,
|
||||
};
|
||||
self.operate(app, &mut geometry);
|
||||
geometry.offset.expect("mounted preview scrollable").y
|
||||
}
|
||||
fn scroll_down(&mut self, app: &App) -> f32 {
|
||||
self.operate(
|
||||
app,
|
||||
&mut widget::operation::scrollable::scroll_to(
|
||||
scroll_id(app),
|
||||
widget::operation::scrollable::AbsoluteOffset {
|
||||
x: None,
|
||||
y: Some(10_000.),
|
||||
},
|
||||
),
|
||||
);
|
||||
let offset = self.offset(app);
|
||||
assert!(offset > 10., "fixture must really be scrolled");
|
||||
offset
|
||||
}
|
||||
async fn execute(&mut self, app: &App, task: Task<Message>) -> usize {
|
||||
let mut count = 0;
|
||||
if let Some(mut actions) = iced_runtime::task::into_stream(task) {
|
||||
while let Some(action) = actions.next().await {
|
||||
let iced_runtime::Action::Widget(mut operation) = action else {
|
||||
panic!("preview paging must only schedule a widget operation");
|
||||
};
|
||||
self.operate(app, operation.as_mut());
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn explicit_navigation_resets_retained_scroll_but_id_change_alone_does_not() {
|
||||
let mut app = paged_fixture();
|
||||
let before = evidence(&app);
|
||||
let form = app.source_rename.form.as_ref().unwrap();
|
||||
let invocation = form.invocation;
|
||||
let approval = form.approval.as_ref().unwrap().id;
|
||||
let mut mounted = Mounted::new(&app);
|
||||
mounted.scroll_down(&app);
|
||||
let old_id = scroll_id(&app);
|
||||
let task = app.update(Message::SourceRename(Event::File(
|
||||
invocation, approval, true,
|
||||
)));
|
||||
assert_ne!(old_id, scroll_id(&app));
|
||||
// Negative control: even after diff/layout with the new id and new file,
|
||||
// Iced retains the previous offset until the returned operation executes.
|
||||
assert!(mounted.offset(&app) > 10.);
|
||||
assert_eq!(mounted.execute(&app, task).await, 1);
|
||||
assert_eq!(mounted.offset(&app), 0.);
|
||||
|
||||
for navigation in [
|
||||
Event::Line(invocation, approval, true),
|
||||
Event::Segment(invocation, approval, true),
|
||||
Event::Hunk(invocation, approval, true),
|
||||
Event::File(invocation, approval, false),
|
||||
Event::Search(invocation, "uses".into()),
|
||||
Event::Search(invocation, "no-path-match".into()),
|
||||
Event::Search(invocation, String::new()),
|
||||
] {
|
||||
mounted.scroll_down(&app);
|
||||
let task = app.update(Message::SourceRename(navigation));
|
||||
assert_eq!(mounted.execute(&app, task).await, 1);
|
||||
assert_eq!(mounted.offset(&app), 0.);
|
||||
assert_eq!(evidence(&app), before);
|
||||
}
|
||||
// Passive redraw and unchanged filter input preserve deliberate scrolling.
|
||||
let offset = mounted.scroll_down(&app);
|
||||
assert_eq!(mounted.offset(&app), offset);
|
||||
let task = app.update(Message::SourceRename(Event::Search(
|
||||
invocation,
|
||||
String::new(),
|
||||
)));
|
||||
assert_eq!(mounted.execute(&app, task).await, 0);
|
||||
assert_eq!(mounted.offset(&app), offset);
|
||||
let task = app.update(Message::SourceRename(Event::Input(
|
||||
invocation,
|
||||
"pump_long".into(),
|
||||
)));
|
||||
assert_eq!(mounted.execute(&app, task).await, 0);
|
||||
assert_eq!(mounted.offset(&app), offset);
|
||||
let task = app.update(Message::SourceRename(Event::File(
|
||||
invocation, approval, false,
|
||||
)));
|
||||
assert_eq!(
|
||||
mounted.execute(&app, task).await,
|
||||
0,
|
||||
"clamped first-page request is a no-op"
|
||||
);
|
||||
assert_eq!(mounted.offset(&app), offset);
|
||||
assert_eq!(evidence(&app), before);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delayed_page_and_approval_operations_cannot_reset_a_new_destination() {
|
||||
let mut app = paged_fixture();
|
||||
let before = evidence(&app);
|
||||
let form = app.source_rename.form.as_ref().unwrap();
|
||||
let invocation = form.invocation;
|
||||
let old_approval = form.approval.as_ref().unwrap().id;
|
||||
let mut mounted = Mounted::new(&app);
|
||||
let old_page_task = app.update(Message::SourceRename(Event::File(
|
||||
invocation,
|
||||
old_approval,
|
||||
true,
|
||||
)));
|
||||
let next_page_task = app.update(Message::SourceRename(Event::Line(
|
||||
invocation,
|
||||
old_approval,
|
||||
true,
|
||||
)));
|
||||
assert_eq!(mounted.execute(&app, next_page_task).await, 1);
|
||||
let offset = mounted.scroll_down(&app);
|
||||
assert_eq!(mounted.execute(&app, old_page_task).await, 1);
|
||||
assert_eq!(mounted.offset(&app), offset, "old page target is absent");
|
||||
|
||||
let old_approval_task = app.update(Message::SourceRename(Event::Search(
|
||||
invocation,
|
||||
"defs".into(),
|
||||
)));
|
||||
input(&mut app, "pump_new");
|
||||
let _ = preview(&mut app); // Real new approval in the same invocation/page.
|
||||
assert_ne!(
|
||||
app.source_rename
|
||||
.form
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.approval
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.id,
|
||||
old_approval
|
||||
);
|
||||
let offset = mounted.scroll_down(&app);
|
||||
assert_eq!(mounted.execute(&app, old_approval_task).await, 1);
|
||||
assert_eq!(
|
||||
mounted.offset(&app),
|
||||
offset,
|
||||
"old approval target is absent"
|
||||
);
|
||||
let task = app.update(Message::SourceRename(Event::File(
|
||||
invocation,
|
||||
old_approval,
|
||||
true,
|
||||
)));
|
||||
assert_eq!(
|
||||
mounted.execute(&app, task).await,
|
||||
0,
|
||||
"stale envelope is rejected before scheduling"
|
||||
);
|
||||
assert_eq!(mounted.offset(&app), offset);
|
||||
assert_eq!(evidence(&app), before);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn repeated_page_visits_filters_and_fast_preview_completion_have_distinct_destinations() {
|
||||
let mut app = paged_fixture();
|
||||
let before = evidence(&app);
|
||||
let form = app.source_rename.form.as_ref().unwrap();
|
||||
let invocation = form.invocation;
|
||||
let approval = form.approval.as_ref().unwrap().id;
|
||||
let mut mounted = Mounted::new(&app);
|
||||
|
||||
let first_visit = app.update(Message::SourceRename(Event::File(
|
||||
invocation, approval, true,
|
||||
)));
|
||||
let next_page = app.update(Message::SourceRename(Event::Line(
|
||||
invocation, approval, true,
|
||||
)));
|
||||
assert_eq!(mounted.execute(&app, next_page).await, 1);
|
||||
let return_visit = app.update(Message::SourceRename(Event::Line(
|
||||
invocation, approval, false,
|
||||
)));
|
||||
assert_eq!(mounted.execute(&app, return_visit).await, 1);
|
||||
let offset = mounted.scroll_down(&app);
|
||||
assert_eq!(mounted.execute(&app, first_visit).await, 1);
|
||||
assert_eq!(
|
||||
mounted.offset(&app),
|
||||
offset,
|
||||
"old visit to the identical page is stale"
|
||||
);
|
||||
|
||||
let old_filter = app.update(Message::SourceRename(Event::Search(
|
||||
invocation,
|
||||
"uses".into(),
|
||||
)));
|
||||
let new_filter = app.update(Message::SourceRename(Event::Search(
|
||||
invocation,
|
||||
"us".into(),
|
||||
)));
|
||||
assert_eq!(mounted.execute(&app, new_filter).await, 1);
|
||||
let offset = mounted.scroll_down(&app);
|
||||
assert_eq!(mounted.execute(&app, old_filter).await, 1);
|
||||
assert_eq!(
|
||||
mounted.offset(&app),
|
||||
offset,
|
||||
"old filter on the same page is stale"
|
||||
);
|
||||
|
||||
input(&mut app, "pump_new");
|
||||
let job = start_preview(&mut app);
|
||||
let completion = preview_completion(&app, job);
|
||||
// No intermediate frame mounts the temporary no-approval view. The old
|
||||
// Scrollable survives even though the new approval has a different id.
|
||||
let ready = app.update(Message::SourceRenameReady(completion));
|
||||
assert!(mounted.offset(&app) > 10.);
|
||||
assert_eq!(mounted.execute(&app, ready).await, 1);
|
||||
assert_eq!(mounted.offset(&app), 0.);
|
||||
assert_eq!(evidence(&app), before);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,313 +0,0 @@
|
||||
//! Manual replacement + next-layout/highlight evaluator; no GPU or native paint.
|
||||
//! Child of source_rename_timing_tests, preserving the separate real App test.
|
||||
use super::*;
|
||||
use crate::{highlight, views::source_tests::FontLayout};
|
||||
use iced::advanced::widget::operation::Focusable;
|
||||
use iced::advanced::{Layout, graphics, layout, renderer, text as shaped, widget::Tree};
|
||||
use iced::{
|
||||
Background, Color, Element, Font, Length, Pixels, Point, Rectangle, Size, Transformation,
|
||||
};
|
||||
|
||||
// The same real Paragraph and Editor as FontLayout. During draw, retain only a
|
||||
// Weak and fixed geometry; inspect glyphs after timing, never in fill_editor.
|
||||
#[derive(Default)]
|
||||
struct ObservedFontLayout {
|
||||
drawn: Option<(graphics::text::editor::Weak, Point, Rectangle)>,
|
||||
}
|
||||
impl renderer::Renderer for ObservedFontLayout {
|
||||
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) {
|
||||
self.drawn = None;
|
||||
}
|
||||
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 FontLayout, handle, callback);
|
||||
}
|
||||
}
|
||||
impl shaped::Renderer for ObservedFontLayout {
|
||||
type Font = Font;
|
||||
type Paragraph = <FontLayout as shaped::Renderer>::Paragraph;
|
||||
type Editor = <FontLayout as shaped::Renderer>::Editor;
|
||||
const ICON_FONT: Font = <FontLayout as shaped::Renderer>::ICON_FONT;
|
||||
const CHECKMARK_ICON: char = <FontLayout as shaped::Renderer>::CHECKMARK_ICON;
|
||||
const ARROW_DOWN_ICON: char = <FontLayout as shaped::Renderer>::ARROW_DOWN_ICON;
|
||||
const SCROLL_UP_ICON: char = <FontLayout as shaped::Renderer>::SCROLL_UP_ICON;
|
||||
const SCROLL_DOWN_ICON: char = <FontLayout as shaped::Renderer>::SCROLL_DOWN_ICON;
|
||||
const SCROLL_LEFT_ICON: char = <FontLayout as shaped::Renderer>::SCROLL_LEFT_ICON;
|
||||
const SCROLL_RIGHT_ICON: char = <FontLayout as shaped::Renderer>::SCROLL_RIGHT_ICON;
|
||||
const ICED_LOGO: char = <FontLayout as shaped::Renderer>::ICED_LOGO;
|
||||
fn default_font(&self) -> Font {
|
||||
shaped::Renderer::default_font(&FontLayout)
|
||||
}
|
||||
fn default_size(&self) -> Pixels {
|
||||
shaped::Renderer::default_size(&FontLayout)
|
||||
}
|
||||
fn fill_paragraph(&mut self, _: &Self::Paragraph, _: Point, _: Color, _: Rectangle) {}
|
||||
fn fill_editor(&mut self, editor: &Self::Editor, position: Point, _: Color, clip: Rectangle) {
|
||||
self.drawn = Some((editor.downgrade(), position, clip));
|
||||
}
|
||||
fn fill_text(&mut self, _: shaped::Text, _: Point, _: Color, _: Rectangle) {}
|
||||
}
|
||||
|
||||
type Content = text_editor::Content<ObservedFontLayout>;
|
||||
const HOST: Size = Size::new(640., 350.);
|
||||
|
||||
fn view(editor: &Content, source: Arc<str>) -> Element<'_, (), iced::Theme, ObservedFontLayout> {
|
||||
text_editor(editor)
|
||||
.on_action(|_| ())
|
||||
.font(Font::MONOSPACE)
|
||||
.size(14)
|
||||
.padding(16)
|
||||
.height(Length::Fill)
|
||||
.highlight_with::<highlight::ArcadiaHighlighter>(source, highlight::format)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn check_text(editor: &Content, expected: &str, cursor: text_editor::Cursor) {
|
||||
assert_eq!(editor.text(), expected);
|
||||
assert_eq!(editor.cursor(), cursor);
|
||||
let lines: Vec<_> = expected.split('\n').collect();
|
||||
assert_eq!(editor.line_count(), lines.len());
|
||||
for (index, part) in lines.iter().enumerate() {
|
||||
let (text, ending) = if index + 1 == lines.len() {
|
||||
(*part, text_editor::LineEnding::None)
|
||||
} else if let Some(text) = part.strip_suffix('\r') {
|
||||
(text, text_editor::LineEnding::CrLf)
|
||||
} else {
|
||||
(*part, text_editor::LineEnding::Lf)
|
||||
};
|
||||
let actual = editor.line(index).unwrap();
|
||||
assert_eq!(actual.text, text);
|
||||
assert_eq!(actual.ending, ending);
|
||||
}
|
||||
}
|
||||
|
||||
fn check_draw(renderer: &ObservedFontLayout, cursor: text_editor::Cursor, alias: &str) {
|
||||
let (weak, position, clip) = renderer.drawn.as_ref().expect("real editor draw");
|
||||
let editor = weak.upgrade().expect("drawn editor is still live");
|
||||
let buffer = editor.buffer();
|
||||
assert_eq!(weak.bounds, Size::new(608., 318.));
|
||||
assert_eq!(buffer.size(), (Some(608.), Some(318.)));
|
||||
assert_eq!(buffer.metrics().font_size, 14.);
|
||||
assert_eq!(
|
||||
buffer.scroll().horizontal,
|
||||
0.,
|
||||
"fixture origin fits the viewport"
|
||||
);
|
||||
let start = cursor.position;
|
||||
let end = cursor.selection.expect("approved alias selection");
|
||||
assert_eq!(start.line, end.line);
|
||||
assert_eq!(end.column - start.column, alias.len());
|
||||
let line = &buffer.lines[start.line];
|
||||
assert_eq!(&line.text()[start.column..end.column], alias);
|
||||
let color = graphics::text::to_color(Color::from_rgb8(20, 93, 139));
|
||||
let font = graphics::text::to_attributes(Font::MONOSPACE);
|
||||
for byte in start.column..end.column {
|
||||
let attrs = line.attrs_list().get_span(byte);
|
||||
assert_eq!(
|
||||
attrs.color_opt,
|
||||
Some(color),
|
||||
"accepted alias syntax attributes"
|
||||
);
|
||||
assert_eq!(attrs.family, font.family);
|
||||
}
|
||||
let mut covered = vec![false; alias.len()];
|
||||
for run in buffer.layout_runs().filter(|run| run.line_i == start.line) {
|
||||
for glyph in run.glyphs {
|
||||
let from = glyph.start.max(start.column);
|
||||
let to = glyph.end.min(end.column);
|
||||
if from >= to {
|
||||
continue;
|
||||
}
|
||||
assert_eq!(glyph.color_opt, Some(color), "drawn alias glyph color");
|
||||
let x = position.x + glyph.x + glyph.x_offset * glyph.font_size;
|
||||
let y = position.y + run.line_top;
|
||||
assert!(x >= clip.x - 0.5 && x + glyph.w <= clip.x + clip.width + 0.5);
|
||||
assert!(y + run.line_height > clip.y && y < clip.y + clip.height);
|
||||
covered[from - start.column..to - start.column].fill(true);
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
covered.into_iter().all(|seen| seen),
|
||||
"entire alias has visible shaped glyphs"
|
||||
);
|
||||
// This temporary strong upgrade is dropped before the next editor mutation.
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Arm {
|
||||
Constructor,
|
||||
Reuse,
|
||||
}
|
||||
impl Arm {
|
||||
fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Constructor => "with_text",
|
||||
Self::Reuse => "same_line_reuse",
|
||||
}
|
||||
}
|
||||
fn phases(self) -> [&'static str; 4] {
|
||||
match self {
|
||||
Self::Constructor => [
|
||||
"constructor_replace_origin",
|
||||
"constructor_next_layout",
|
||||
"constructor_highlight_draw",
|
||||
"constructor_combined",
|
||||
],
|
||||
Self::Reuse => [
|
||||
"reuse_replace_origin",
|
||||
"reuse_next_layout",
|
||||
"reuse_highlight_draw",
|
||||
"reuse_combined",
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn trial(
|
||||
fixture: &Fixture,
|
||||
cursor: text_editor::Cursor,
|
||||
arm: Arm,
|
||||
trial: usize,
|
||||
samples: &mut Samples,
|
||||
) {
|
||||
let before = &fixture.bundle.files[ORIGIN];
|
||||
let after = &fixture.expected_bundle.files[ORIGIN];
|
||||
let mut editor = Content::with_text(before);
|
||||
editor.move_to(fixture.cursor);
|
||||
let mut renderer = ObservedFontLayout::default();
|
||||
let theme = iced::Theme::Light;
|
||||
let defaults = renderer::Style::default();
|
||||
let viewport = Rectangle::with_size(HOST);
|
||||
let limits = layout::Limits::new(Size::ZERO, HOST);
|
||||
let mut tree = {
|
||||
let mut widget = view(&editor, Arc::from(before.as_str()));
|
||||
let mut tree = Tree::new(widget.as_widget());
|
||||
let node = widget.as_widget_mut().layout(&mut tree, &renderer, &limits);
|
||||
tree.state
|
||||
.downcast_mut::<text_editor::State<highlight::ArcadiaHighlighter>>()
|
||||
.focus();
|
||||
widget.as_widget().draw(
|
||||
&tree,
|
||||
&mut renderer,
|
||||
&theme,
|
||||
&defaults,
|
||||
Layout::new(&node),
|
||||
iced::mouse::Cursor::Unavailable,
|
||||
&viewport,
|
||||
);
|
||||
tree
|
||||
};
|
||||
check_text(&editor, before, fixture.cursor);
|
||||
check_draw(&renderer, fixture.cursor, OLD);
|
||||
// No recorder Weak/Strong from setup may perturb replacement ownership.
|
||||
renderer.drawn = None;
|
||||
let next_source: Arc<str> = Arc::from(after.as_str());
|
||||
|
||||
// Only normal operations and four timestamps occur between start and end.
|
||||
// Source/model oracles, logging and recorder glyph inspection are outside.
|
||||
let started = Instant::now();
|
||||
let replaced = match arm {
|
||||
Arm::Constructor => {
|
||||
editor = Content::with_text(after);
|
||||
true
|
||||
}
|
||||
Arm::Reuse => super::super::editor_patch::replace(&mut editor, before, after),
|
||||
};
|
||||
editor.move_to(cursor);
|
||||
let replaced_at = Instant::now();
|
||||
let mut widget = view(&editor, next_source);
|
||||
let node = widget.as_widget_mut().layout(&mut tree, &renderer, &limits);
|
||||
let laid_out_at = Instant::now();
|
||||
widget.as_widget().draw(
|
||||
&tree,
|
||||
&mut renderer,
|
||||
&theme,
|
||||
&defaults,
|
||||
Layout::new(&node),
|
||||
iced::mouse::Cursor::Unavailable,
|
||||
&viewport,
|
||||
);
|
||||
let drawn_at = Instant::now();
|
||||
drop(widget);
|
||||
|
||||
assert!(replaced, "reuse must not silently time a fallback/refusal");
|
||||
check_text(&editor, after, cursor);
|
||||
check_draw(&renderer, cursor, NEW);
|
||||
renderer.drawn = None;
|
||||
for (phase, duration) in arm.phases().into_iter().zip([
|
||||
replaced_at.duration_since(started),
|
||||
laid_out_at.duration_since(replaced_at),
|
||||
drawn_at.duration_since(laid_out_at),
|
||||
drawn_at.duration_since(started),
|
||||
]) {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::json!({
|
||||
"record": "editor_layout_timing", "arm": arm.name(), "phase": phase,
|
||||
"trial": trial, "warmup": trial == 0,
|
||||
"elapsed_ns": duration.as_nanos().to_string(),
|
||||
"elapsed_ms": duration.as_secs_f64() * 1000.0,
|
||||
})
|
||||
);
|
||||
if trial != 0 {
|
||||
samples.0.entry(phase).or_default().push(duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "manual Windows dev/debug editor replacement + layout/highlight trial; run alone with --ignored --nocapture --test-threads=1"]
|
||||
fn accepted_release1_constructor_vs_reuse_through_next_highlight() {
|
||||
let fixture = Fixture::load();
|
||||
// Resolve the exact production approval and verify full Project/source once,
|
||||
// outside all intervals. The actual App Confirm evaluator remains separate.
|
||||
let cursor = {
|
||||
let app = fixture.app();
|
||||
let target = resolve(&app, &app.rename_context());
|
||||
let prepared = app
|
||||
.document
|
||||
.alias_rename_request(target.id, NEW.into())
|
||||
.unwrap()
|
||||
.prepare()
|
||||
.unwrap();
|
||||
fixture.check_candidate(&prepared);
|
||||
prepare_approval(prepared, &target, ORIGIN).unwrap().cursor
|
||||
};
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::json!({
|
||||
"record": "editor_layout_fixture", "host_width": HOST.width, "host_height": HOST.height,
|
||||
"font": "MONOSPACE", "font_size": 14, "padding": 16, "height": "Fill",
|
||||
"active_file": ORIGIN, "before_bytes": fixture.bundle.files[ORIGIN].len(),
|
||||
"after_bytes": fixture.expected_bundle.files[ORIGIN].len(), "warmups_per_arm": 1,
|
||||
"measured_trials_per_arm": 5, "debug_assertions": cfg!(debug_assertions),
|
||||
"scope": "real editor replacement, retained Tree layout and Widget draw/highlight; no GPU, native paint or App Confirm",
|
||||
})
|
||||
);
|
||||
let mut samples = Samples::default();
|
||||
for index in 0..=5 {
|
||||
let arms = if index % 2 == 0 {
|
||||
[Arm::Constructor, Arm::Reuse]
|
||||
} else {
|
||||
[Arm::Reuse, Arm::Constructor]
|
||||
};
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::json!({"record": "editor_layout_order", "trial": index, "first": arms[0].name()})
|
||||
);
|
||||
for arm in arms {
|
||||
trial(&fixture, cursor, arm, index, &mut samples);
|
||||
}
|
||||
}
|
||||
samples.report();
|
||||
}
|
||||
@@ -1,347 +0,0 @@
|
||||
//! Manual timing through the real Document and App paths. No disk writes,
|
||||
//! executor polling, layout, GPU work or first-paint measurement occurs here.
|
||||
//! Register as a test child of app::source_rename to use its actual worker helpers.
|
||||
use super::*;
|
||||
use crate::source_workspace::Workspace;
|
||||
use oa_core::source_project::SourceBundle;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
path::PathBuf,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
#[path = "source_rename_layout_timing_tests.rs"]
|
||||
mod layout_timing_tests;
|
||||
|
||||
const OLD: &str = "oa_restore_work";
|
||||
const NEW: &str = "oa_restore_pressure";
|
||||
const DECLARATIONS: &str = "oa/activities/activity_oa_restore_work.arcadia";
|
||||
const ORIGIN: &str = "traceability.arcadia";
|
||||
|
||||
struct Fixture {
|
||||
bundle: SourceBundle,
|
||||
project: Project,
|
||||
expected_bundle: SourceBundle,
|
||||
expected_project: Project,
|
||||
target: Id,
|
||||
cursor: text_editor::Cursor,
|
||||
}
|
||||
|
||||
impl Fixture {
|
||||
fn load() -> Self {
|
||||
// The optional entry selects a parent-pinned copy of this exact fixture,
|
||||
// not an arbitrary performance generator or a mutable GUI destination.
|
||||
let entry = std::env::var_os("ARCADIA_RENAME_TIMING_ENTRY")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../examples/release1/project.arcadia")
|
||||
});
|
||||
let loaded = oa_core::source_files::load_project(&entry, LIMITS).unwrap();
|
||||
let bundle = loaded.reachable_bundle();
|
||||
assert_eq!(
|
||||
bundle.files.len(),
|
||||
30,
|
||||
"use the preserved release1 walkthrough"
|
||||
);
|
||||
let document = Document::from_bundle(bundle.clone(), LIMITS).unwrap();
|
||||
assert_eq!(
|
||||
document.bundle(),
|
||||
&bundle,
|
||||
"fixture must need no stabilization/migration"
|
||||
);
|
||||
assert!(!document.dirty());
|
||||
let project = document.project().clone();
|
||||
let target = Id::parse_str("1b91f8a7-0bdb-514e-b1cd-955658e09927").unwrap();
|
||||
assert_eq!(project.elements[&target].alias, OLD);
|
||||
assert!(!project.identities().iter().any(|(_, alias)| *alias == NEW));
|
||||
let mut expected_project = project.clone();
|
||||
expected_project.elements.get_mut(&target).unwrap().alias = NEW.into();
|
||||
|
||||
// Independent literal source oracle, authored before running the rename.
|
||||
// Do not replace the alias substring in filenames or the trace's alias.
|
||||
let mut expected_bundle = bundle.clone();
|
||||
let declarations = expected_bundle.files.get_mut(DECLARATIONS).unwrap();
|
||||
assert_eq!(
|
||||
declarations.matches("oa_activity oa_restore_work ").count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(declarations.matches("parent @oa_restore_work;").count(), 4);
|
||||
*declarations = declarations
|
||||
.replace(
|
||||
"oa_activity oa_restore_work ",
|
||||
"oa_activity oa_restore_pressure ",
|
||||
)
|
||||
.replace("parent @oa_restore_work;", "parent @oa_restore_pressure;");
|
||||
let traces = expected_bundle.files.get_mut(ORIGIN).unwrap();
|
||||
assert_eq!(traces.matches("oa @oa_restore_work;").count(), 1);
|
||||
*traces = traces.replace("oa @oa_restore_work;", "oa @oa_restore_pressure;");
|
||||
let source = &bundle.files[ORIGIN];
|
||||
let start = source.rfind("@oa_restore_work;").unwrap() + 1;
|
||||
let position = |offset| {
|
||||
let prefix = &source[..offset];
|
||||
text_editor::Position {
|
||||
line: prefix.bytes().filter(|byte| *byte == b'\n').count(),
|
||||
column: prefix
|
||||
.rsplit_once('\n')
|
||||
.map_or(prefix.len(), |(_, tail)| tail.len()),
|
||||
}
|
||||
};
|
||||
let cursor = text_editor::Cursor {
|
||||
position: position(start),
|
||||
selection: Some(position(start + OLD.len())),
|
||||
};
|
||||
assert!(nav::exact_cursor(source, cursor));
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::json!({
|
||||
"record": "fixture", "entry": entry, "reachable_files": bundle.files.len(),
|
||||
"source_bytes": bundle.files.values().map(String::len).sum::<usize>(),
|
||||
"identities": project.identities().len(), "active_file": ORIGIN,
|
||||
"active_file_bytes": source.len(), "old": OLD, "new": NEW,
|
||||
"warmups": 1, "measured_trials": 5, "debug_assertions": cfg!(debug_assertions),
|
||||
"scope": "real worker helpers, guarded Document commit, App sync and Confirm update; no task polling or rendering"
|
||||
})
|
||||
);
|
||||
Self {
|
||||
bundle,
|
||||
project,
|
||||
expected_bundle,
|
||||
expected_project,
|
||||
target,
|
||||
cursor,
|
||||
}
|
||||
}
|
||||
|
||||
fn app(&self) -> App {
|
||||
let mut app = App::with_project(Project::empty());
|
||||
app.document = Document::from_bundle(self.bundle.clone(), LIMITS).unwrap();
|
||||
app.source_workspace = Workspace::new(app.document.bundle(), true);
|
||||
app.source_workspace.active = ORIGIN.into();
|
||||
app.selected = app.project().id;
|
||||
app.page = Page::Text;
|
||||
app.sync();
|
||||
app.source_editor.move_to(self.cursor);
|
||||
assert_eq!(app.project(), &self.project);
|
||||
assert_eq!(app.document.bundle(), &self.bundle);
|
||||
assert_eq!(app.document.revision(), 0);
|
||||
assert!(!app.document.dirty() && !app.document.can_undo() && !app.document.can_redo());
|
||||
assert!(app.rename_admission().is_ok());
|
||||
app
|
||||
}
|
||||
|
||||
fn check_candidate(&self, prepared: &PreparedAliasRename) {
|
||||
assert!(prepared.changed());
|
||||
assert_eq!(prepared.before().bundle, self.bundle);
|
||||
assert_eq!(prepared.before().decoded.project, self.project);
|
||||
assert_eq!(prepared.after().bundle, self.expected_bundle);
|
||||
assert_eq!(prepared.after().decoded.project, self.expected_project);
|
||||
}
|
||||
|
||||
fn check_adopted(&self, app: &mut App, candidate_address: *const CompiledSourceProject) {
|
||||
assert!(
|
||||
std::ptr::eq(app.document.compiled(), candidate_address),
|
||||
"adopt exact candidate allocation"
|
||||
);
|
||||
assert_eq!(app.project(), &self.expected_project);
|
||||
assert_eq!(app.document.bundle(), &self.expected_bundle);
|
||||
assert_eq!(app.source_workspace.bundle(), self.expected_bundle);
|
||||
assert_eq!(app.source_editor.text(), self.expected_bundle.files[ORIGIN]);
|
||||
assert_eq!(app.document.revision(), 1);
|
||||
assert!(app.document.dirty() && app.document.can_undo() && !app.document.can_redo());
|
||||
assert!(app.document.undo());
|
||||
assert_eq!(app.project(), &self.project);
|
||||
assert_eq!(app.document.bundle(), &self.bundle);
|
||||
assert!(
|
||||
!app.document.dirty(),
|
||||
"live known saved baseline remains authoritative"
|
||||
);
|
||||
assert!(
|
||||
!app.document.can_undo(),
|
||||
"exactly one preceding model transaction"
|
||||
);
|
||||
assert!(app.document.redo());
|
||||
assert_eq!(app.project(), &self.expected_project);
|
||||
assert_eq!(app.document.bundle(), &self.expected_bundle);
|
||||
assert!(std::ptr::eq(app.document.compiled(), candidate_address));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Samples(BTreeMap<&'static str, Vec<Duration>>);
|
||||
|
||||
impl Samples {
|
||||
fn time<T>(&mut self, phase: &'static str, trial: usize, action: impl FnOnce() -> T) -> T {
|
||||
let started = Instant::now();
|
||||
let result = action();
|
||||
let elapsed = started.elapsed();
|
||||
// Printing, assertions and dropping the returned value are outside this
|
||||
// interval. Full Confirm still includes production's own local drops.
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::json!({
|
||||
"record": "timing", "phase": phase, "trial": trial,
|
||||
"warmup": trial == 0, "elapsed_ns": elapsed.as_nanos().to_string(),
|
||||
"elapsed_ms": elapsed.as_secs_f64() * 1000.0
|
||||
})
|
||||
);
|
||||
if trial != 0 {
|
||||
self.0.entry(phase).or_default().push(elapsed);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn report(mut self) {
|
||||
for (phase, values) in &mut self.0 {
|
||||
assert_eq!(values.len(), 5);
|
||||
values.sort();
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::json!({
|
||||
"record": "summary", "phase": phase, "samples": values.len(),
|
||||
"min_ms": values[0].as_secs_f64() * 1000.0,
|
||||
"median_ms": values[2].as_secs_f64() * 1000.0,
|
||||
"max_ms": values[4].as_secs_f64() * 1000.0
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve(app: &App, context: &Context) -> Target {
|
||||
let snapshot = app
|
||||
.with_source_input(|input| query::admit_owned(input, query::Limits::default()))
|
||||
.unwrap()
|
||||
.capture()
|
||||
.unwrap();
|
||||
let offset = nav::cursor_offset(app.source_workspace.text(), context.begin.cursor).unwrap();
|
||||
resolve_target(&snapshot, context, offset).unwrap()
|
||||
}
|
||||
|
||||
fn active_job(app: &App) -> Job {
|
||||
let form = app.source_rename.form.as_ref().unwrap();
|
||||
let id = form.pending.unwrap();
|
||||
assert_eq!(app.source_rename.in_flight, Some(id));
|
||||
Job {
|
||||
id,
|
||||
invocation: form.invocation,
|
||||
generation: form.generation,
|
||||
context: form.context.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn separate_phases(fixture: &Fixture, samples: &mut Samples, trial: usize) {
|
||||
let mut app = fixture.app();
|
||||
let target = resolve(&app, &app.rename_context());
|
||||
assert_eq!(target.id, fixture.target);
|
||||
let request = samples
|
||||
.time("document_request_mint", trial, || {
|
||||
app.document.alias_rename_request(target.id, NEW.into())
|
||||
})
|
||||
.unwrap();
|
||||
let prepared = samples
|
||||
.time("worker_document_prepare", trial, || request.prepare())
|
||||
.unwrap();
|
||||
fixture.check_candidate(&prepared);
|
||||
let approval = samples
|
||||
.time("worker_preview_and_origin", trial, || {
|
||||
prepare_approval(prepared, &target, ORIGIN)
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(approval.preview.files().len(), 2);
|
||||
assert_eq!(approval.preview.changed_line_count(), 6);
|
||||
let candidate_address = approval.prepared.after() as *const CompiledSourceProject;
|
||||
// Same-destination save completion is a legitimate live update after Preview.
|
||||
app.document.mark_saved(app.document.snapshot());
|
||||
let changed = samples
|
||||
.time("document_guarded_commit", trial, || {
|
||||
app.document.commit_alias_rename(&approval.prepared)
|
||||
})
|
||||
.unwrap();
|
||||
assert!(changed);
|
||||
samples.time("app_sync", trial, || app.sync());
|
||||
fixture.check_adopted(&mut app, candidate_address);
|
||||
}
|
||||
|
||||
fn whole_confirm(fixture: &Fixture, samples: &mut Samples, trial: usize) {
|
||||
let mut app = fixture.app();
|
||||
let begin = app.source_rename_message();
|
||||
// These Tasks are deliberately not polled: deterministic worker helpers
|
||||
// supply the same results through the real readiness messages below.
|
||||
drop(app.update(begin));
|
||||
let target_job = active_job(&app);
|
||||
let target = Arc::new(resolve(&app, &target_job.context));
|
||||
assert_eq!(target.id, fixture.target);
|
||||
drop(app.update(Message::SourceRenameReady(Completion {
|
||||
job: target_job,
|
||||
payload: Payload::Target(Ok(target.clone())),
|
||||
})));
|
||||
let invocation = app.source_rename.form.as_ref().unwrap().invocation;
|
||||
drop(app.update(Message::SourceRename(Event::Input(invocation, NEW.into()))));
|
||||
let generation = app.source_rename.form.as_ref().unwrap().generation;
|
||||
drop(app.update(Message::SourceRename(Event::Preview(
|
||||
invocation, generation,
|
||||
))));
|
||||
let preview_job = active_job(&app);
|
||||
let prepared = app
|
||||
.document
|
||||
.alias_rename_request(target.id, NEW.into())
|
||||
.unwrap()
|
||||
.prepare()
|
||||
.unwrap();
|
||||
fixture.check_candidate(&prepared);
|
||||
let approval = Arc::new(prepare_approval(prepared, &target, ORIGIN).unwrap());
|
||||
drop(target);
|
||||
let candidate_address = approval.prepared.after() as *const CompiledSourceProject;
|
||||
let expected_cursor = approval.cursor;
|
||||
let approval_id = approval.id;
|
||||
drop(app.update(Message::SourceRenameReady(Completion {
|
||||
job: preview_job,
|
||||
payload: Payload::Preview(Ok(approval)),
|
||||
})));
|
||||
assert_eq!(
|
||||
app.source_rename
|
||||
.form
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.approval
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.id,
|
||||
approval_id
|
||||
);
|
||||
app.document.mark_saved(app.document.snapshot());
|
||||
let confirm = Message::SourceRename(Event::Confirm(invocation, generation, approval_id));
|
||||
let task = samples.time("app_confirm_update", trial, || app.update(confirm));
|
||||
samples.time("confirm_returned_task_drop_without_polling", trial, || {
|
||||
drop(task)
|
||||
});
|
||||
assert!(!app.source_rename.open());
|
||||
assert!(app.source_rename.in_flight.is_none());
|
||||
assert!(app.source_rename_undo_message().is_some());
|
||||
assert_eq!(app.source_editor.cursor(), expected_cursor);
|
||||
fixture.check_adopted(&mut app, candidate_address);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "manual Windows dev/debug rename latency trial; run alone with --ignored --nocapture --test-threads=1"]
|
||||
fn accepted_release1_prepare_commit_sync_and_confirm_timing() {
|
||||
let fixture = Fixture::load();
|
||||
let mut samples = Samples::default();
|
||||
for trial in 0..=5 {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::json!({ "record": "order", "trial": trial,
|
||||
"warmup": trial == 0, "separate_phases_first": trial % 2 == 0 })
|
||||
);
|
||||
if trial % 2 == 0 {
|
||||
separate_phases(&fixture, &mut samples, trial);
|
||||
whole_confirm(&fixture, &mut samples, trial);
|
||||
} else {
|
||||
whole_confirm(&fixture, &mut samples, trial);
|
||||
separate_phases(&fixture, &mut samples, trial);
|
||||
}
|
||||
}
|
||||
samples.report();
|
||||
}
|
||||
@@ -1,464 +0,0 @@
|
||||
//! Current draft navigation, isolated from model application and disk discovery.
|
||||
use super::*;
|
||||
use crate::source_navigation::{self as nav, query};
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
fn admission_notice(error: query::AdmissionError) -> String {
|
||||
use nav::symbols::{BuildError, InputProblem, Resource};
|
||||
let (limit, resource) = match error {
|
||||
query::AdmissionError::Index(BuildError::Limit { limit, resource, .. }) => (limit, resource),
|
||||
query::AdmissionError::Outline { error, .. } => (error.limit, match error.resource {
|
||||
language::OutlineResource::SourceBytes => Resource::SourceBytes,
|
||||
language::OutlineResource::Tokens => Resource::Tokens,
|
||||
language::OutlineResource::Declarations => Resource::Records,
|
||||
}),
|
||||
query::AdmissionError::Index(BuildError::Input { problem, .. }) => return match problem {
|
||||
InputProblem::NonCanonicalPath | InputProblem::DuplicateOrCaseCollidingPath =>
|
||||
"Navigation needs distinct relative source paths. Check file names and include paths.".into(),
|
||||
_ => "Navigation could not verify the current source locations. Correct source errors and try again.".into(),
|
||||
},
|
||||
};
|
||||
let unit = match resource {
|
||||
Resource::Files => "loaded files",
|
||||
Resource::SourceBytes => "source bytes",
|
||||
Resource::PathBytes => "bytes in file paths",
|
||||
Resource::Tokens => "source tokens",
|
||||
Resource::Records => "declarations and references",
|
||||
};
|
||||
format!(
|
||||
"Navigation reached its limit of {limit} {unit}. Reduce the loaded source size and try again."
|
||||
)
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn source_symbol_context(&self) -> nav::SymbolContext {
|
||||
nav::SymbolContext {
|
||||
session: self.source_workspace.session(),
|
||||
path: self.source_workspace.active.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn source_symbol_message(&self, event: nav::SymbolEvent) -> Message {
|
||||
Message::SourceSymbols(self.source_symbol_context(), event)
|
||||
}
|
||||
|
||||
pub fn source_find_close_message(&self) -> Message {
|
||||
Message::SourceFindClose(self.source_symbol_context(), self.source_find_instance)
|
||||
}
|
||||
pub fn source_key(&self) -> query::SnapshotKey {
|
||||
query::SnapshotKey {
|
||||
workspace: self.source_workspace.session(),
|
||||
revision: self.source_workspace.revision(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn with_source_input<T>(&self, use_input: impl FnOnce(query::Input<'_>) -> T) -> T {
|
||||
let files: Vec<_> = self
|
||||
.source_workspace
|
||||
.files
|
||||
.iter()
|
||||
.map(|(path, buffer)| query::DraftFile {
|
||||
path,
|
||||
text: &buffer.text,
|
||||
})
|
||||
.collect();
|
||||
let accepted = self.document.bundle();
|
||||
let validated: Vec<_> = accepted
|
||||
.files
|
||||
.iter()
|
||||
.map(|(path, text)| query::DraftFile { path, text })
|
||||
.collect();
|
||||
use_input(query::Input {
|
||||
key: self.source_key(),
|
||||
entry: &self.source_workspace.entry,
|
||||
files: &files,
|
||||
applied: self.source_workspace.applied,
|
||||
validation: self
|
||||
.source_workspace
|
||||
.applied
|
||||
.then_some(query::ValidatedSources {
|
||||
entry: &accepted.entry,
|
||||
files: &validated,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn source_snapshot_matches(&self, snapshot: &query::Snapshot) -> bool {
|
||||
snapshot.key() == self.source_key()
|
||||
&& self.with_source_input(|input| snapshot.matches(input))
|
||||
}
|
||||
|
||||
pub fn source_navigation_available(&self) -> bool {
|
||||
self.page == Page::Text
|
||||
&& !self.source_rename.open()
|
||||
&& !self.busy
|
||||
&& self.pending.is_none()
|
||||
&& self.file_prompt.is_none()
|
||||
&& !self.delete_pending
|
||||
&& self.workflow.is_none()
|
||||
&& !self.hosted.visible
|
||||
&& self.model_menu.is_none()
|
||||
&& self.architecture.is_none()
|
||||
&& !self.behavior.has_pending()
|
||||
}
|
||||
|
||||
/// Mounted widget IDs change when another interaction supersedes a reveal.
|
||||
/// A queued focus operation therefore cannot find a subsequently reused editor.
|
||||
pub fn source_editor_id(&self) -> iced::widget::Id {
|
||||
iced::widget::Id::from(format!(
|
||||
"source-editor-{}-{}-{}",
|
||||
self.source_workspace.session(),
|
||||
self.source_workspace.active,
|
||||
self.source_symbols.interaction
|
||||
))
|
||||
}
|
||||
|
||||
pub fn source_reveal_ticket(&self) -> Option<Id> {
|
||||
self.source_symbols
|
||||
.reveal
|
||||
.as_ref()
|
||||
.map(|reveal| reveal.ticket)
|
||||
}
|
||||
|
||||
pub(super) fn source_interaction(&mut self) {
|
||||
self.source_symbols.interaction = Id::new_v4();
|
||||
self.source_symbols.request = None;
|
||||
self.source_symbols.reveal = None;
|
||||
self.source_jump = None;
|
||||
}
|
||||
|
||||
pub(super) fn queue_source_reveal(&mut self, cursor: text_editor::Cursor, select_line: bool) {
|
||||
self.source_jump = Some(cursor);
|
||||
self.source_select_line = select_line;
|
||||
self.source_symbols.reveal = Some(nav::Reveal {
|
||||
ticket: Id::new_v4(),
|
||||
interaction: self.source_symbols.interaction,
|
||||
raw_interaction: self.source_symbols.raw_interaction.load(Ordering::SeqCst),
|
||||
snapshot: self.source_key(),
|
||||
path: self.source_workspace.active.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn restore_source_editor_focus(&mut self) {
|
||||
let cursor = self.source_editor.cursor();
|
||||
if self.source_navigation_available()
|
||||
&& nav::exact_cursor(self.source_workspace.text(), cursor)
|
||||
{
|
||||
self.queue_source_reveal(cursor, false);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn source_viewport(&mut self, ticket: Option<Id>) -> Task<Message> {
|
||||
let Some(reveal) = &self.source_symbols.reveal else {
|
||||
return Task::none();
|
||||
};
|
||||
if Some(reveal.ticket) != ticket
|
||||
|| reveal.interaction != self.source_symbols.interaction
|
||||
|| !nav::interaction::is_current(
|
||||
&self.source_symbols.raw_interaction,
|
||||
reveal.raw_interaction,
|
||||
)
|
||||
|| reveal.snapshot != self.source_key()
|
||||
|| reveal.path != self.source_workspace.active
|
||||
|| !self.source_navigation_available()
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
let raw_interaction = reveal.raw_interaction;
|
||||
self.source_symbols.reveal = None;
|
||||
if let Some(cursor) = self.source_jump.take() {
|
||||
if !nav::exact_cursor(self.source_workspace.text(), cursor) {
|
||||
return Task::none();
|
||||
}
|
||||
self.source_editor.move_to(cursor);
|
||||
if self.source_select_line && cursor.selection.is_none() {
|
||||
self.source_editor.perform(text_editor::Action::SelectLine);
|
||||
}
|
||||
return nav::interaction::guarded_focus(
|
||||
self.source_editor_id(),
|
||||
self.source_symbols.raw_interaction.clone(),
|
||||
raw_interaction,
|
||||
);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(super) fn source_symbol_event(&mut self, event: nav::SymbolEvent) -> Task<Message> {
|
||||
if !self.source_navigation_available() {
|
||||
return Task::none();
|
||||
}
|
||||
match event {
|
||||
nav::SymbolEvent::Close => {
|
||||
self.source_symbols.open = false;
|
||||
self.source_symbols.results = None;
|
||||
self.source_symbols.notice = None;
|
||||
}
|
||||
nav::SymbolEvent::Back => {
|
||||
self.source_symbol_back();
|
||||
// A successful Back needs no empty results panel. Retain an
|
||||
// already open panel, or reveal an actionable return notice.
|
||||
self.source_symbols.open |= self.source_symbols.notice.is_some();
|
||||
}
|
||||
nav::SymbolEvent::Choose {
|
||||
stamp,
|
||||
reference,
|
||||
index,
|
||||
} => {
|
||||
let Some(results) = self.source_symbols.results.clone() else {
|
||||
return Task::none();
|
||||
};
|
||||
if results.stamp != stamp
|
||||
|| stamp.snapshot != self.source_key()
|
||||
|| self
|
||||
.source_symbols
|
||||
.cache
|
||||
.as_ref()
|
||||
.is_none_or(|cache| !self.source_snapshot_matches(cache))
|
||||
{
|
||||
self.source_symbols.notice =
|
||||
Some("Source changed. Run navigation again for current locations.".into());
|
||||
return Task::none();
|
||||
}
|
||||
let destination = if reference {
|
||||
results
|
||||
.references
|
||||
.get(index)
|
||||
.map(|row| (&row.path, row.position, row.end_position))
|
||||
} else {
|
||||
results
|
||||
.definitions
|
||||
.get(index)
|
||||
.map(|row| (&row.path, row.position, row.end_position))
|
||||
};
|
||||
if let Some((path, start, end)) = destination {
|
||||
self.source_symbol_jump(path, start, end);
|
||||
}
|
||||
}
|
||||
nav::SymbolEvent::Query(action) => return self.start_source_query(action),
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn start_source_query(&mut self, action: query::Action) -> Task<Message> {
|
||||
self.source_symbols.open = true;
|
||||
if !nav::interaction::is_current(
|
||||
&self.source_symbols.raw_interaction,
|
||||
self.source_symbols.raw_interaction.load(Ordering::SeqCst),
|
||||
) {
|
||||
self.source_symbols.notice = Some("Reopen this workspace to reset navigation.".into());
|
||||
return Task::none();
|
||||
}
|
||||
if self.source_symbols.in_flight.is_some() {
|
||||
self.source_symbols.notice = Some("Navigation is finishing. Try again shortly.".into());
|
||||
return Task::none();
|
||||
}
|
||||
let cursor = self.source_editor.cursor();
|
||||
let Some(offset) = nav::cursor_offset(self.source_workspace.text(), cursor) else {
|
||||
self.source_symbols.notice =
|
||||
Some("Place the cursor on an alias or its @ reference.".into());
|
||||
return Task::none();
|
||||
};
|
||||
self.source_symbols.serial = match self.source_symbols.serial.checked_add(1) {
|
||||
Some(serial) => serial,
|
||||
None => {
|
||||
self.source_workspace.reset_navigation_session();
|
||||
self.source_symbols.back.clear();
|
||||
self.source_symbols.cache = None;
|
||||
0
|
||||
}
|
||||
};
|
||||
let request = nav::Request {
|
||||
job: Id::new_v4(),
|
||||
interaction: self.source_symbols.interaction,
|
||||
raw_interaction: self.source_symbols.raw_interaction.load(Ordering::SeqCst),
|
||||
stamp: query::QueryStamp {
|
||||
snapshot: self.source_key(),
|
||||
serial: self.source_symbols.serial,
|
||||
},
|
||||
path: self.source_workspace.active.clone(),
|
||||
offset,
|
||||
cursor,
|
||||
action,
|
||||
};
|
||||
let cache = self
|
||||
.source_symbols
|
||||
.cache
|
||||
.as_ref()
|
||||
.filter(|cache| self.source_snapshot_matches(cache))
|
||||
.cloned();
|
||||
let input = if cache.is_none() {
|
||||
match self
|
||||
.with_source_input(|input| query::admit_owned(input, query::Limits::default()))
|
||||
{
|
||||
Ok(input) => Some(input),
|
||||
Err(error) => {
|
||||
self.source_symbols.notice = Some(admission_notice(error));
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.source_symbols.in_flight = Some(request.job);
|
||||
self.source_symbols.request = Some(request.clone());
|
||||
self.source_symbols.results = None;
|
||||
self.source_symbols.notice = None;
|
||||
Task::perform(
|
||||
async move {
|
||||
let worker_request = request.clone();
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
let snapshot = match cache {
|
||||
Some(cache) => cache,
|
||||
None => Arc::new(
|
||||
input
|
||||
.expect("admitted input for uncached request")
|
||||
.capture()
|
||||
.map_err(admission_notice)?,
|
||||
),
|
||||
};
|
||||
let results = snapshot
|
||||
.query(query::Query {
|
||||
stamp: worker_request.stamp,
|
||||
path: &worker_request.path,
|
||||
byte_offset: worker_request.offset,
|
||||
action: worker_request.action,
|
||||
})
|
||||
.map_err(|error| match error {
|
||||
query::QueryError::ResultEnvelopeLimit => "These result details exceed the display limit. Try literal Find for this alias.".to_owned(),
|
||||
_ => "Navigation could not verify this location. Place the cursor on an alias and try again.".to_owned(),
|
||||
})?;
|
||||
Ok((snapshot, Arc::new(results)))
|
||||
})
|
||||
.await
|
||||
.unwrap_or_else(|error| Err(format!("Navigation task failed: {error}")));
|
||||
nav::Completion { request, result }
|
||||
},
|
||||
Message::SourceSymbolsReady,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn source_symbols_ready(&mut self, completion: nav::Completion) -> Task<Message> {
|
||||
let request = completion.request;
|
||||
if self.source_symbols.in_flight != Some(request.job) {
|
||||
return Task::none();
|
||||
}
|
||||
self.source_symbols.in_flight = None;
|
||||
let wanted = self
|
||||
.source_symbols
|
||||
.request
|
||||
.take()
|
||||
.is_some_and(|current| current.job == request.job)
|
||||
&& request.interaction == self.source_symbols.interaction
|
||||
&& request.stamp.snapshot == self.source_key()
|
||||
&& nav::interaction::is_current(
|
||||
&self.source_symbols.raw_interaction,
|
||||
request.raw_interaction,
|
||||
)
|
||||
&& request.path == self.source_workspace.active
|
||||
&& request.cursor == self.source_editor.cursor()
|
||||
&& self.source_navigation_available();
|
||||
if !wanted {
|
||||
return Task::none();
|
||||
}
|
||||
match completion.result {
|
||||
Ok((snapshot, results)) if self.source_snapshot_matches(&snapshot) => {
|
||||
self.source_symbols.cache = Some(snapshot);
|
||||
self.source_symbols.results = Some(results.clone());
|
||||
if results.disposition == query::Disposition::Direct
|
||||
&& let Some(row) = results.definitions.first()
|
||||
{
|
||||
self.source_symbol_jump(&row.path, row.position, row.end_position);
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
self.source_symbols.notice = Some("Source changed. Run navigation again.".into())
|
||||
}
|
||||
Err(error) => self.source_symbols.notice = Some(error),
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn source_symbol_jump(&mut self, path: &str, start: query::Position, end: query::Position) {
|
||||
let cursor = text_editor::Cursor {
|
||||
position: text_editor::Position {
|
||||
line: start.line,
|
||||
column: start.byte_column,
|
||||
},
|
||||
selection: Some(text_editor::Position {
|
||||
line: end.line,
|
||||
column: end.byte_column,
|
||||
}),
|
||||
};
|
||||
if self
|
||||
.source_workspace
|
||||
.files
|
||||
.get(path)
|
||||
.is_none_or(|buffer| !nav::exact_cursor(&buffer.text, cursor))
|
||||
{
|
||||
self.source_symbols.notice =
|
||||
Some("That source location is no longer available. Run navigation again.".into());
|
||||
return;
|
||||
}
|
||||
if let Some(buffer) = self
|
||||
.source_workspace
|
||||
.files
|
||||
.get(&self.source_workspace.active)
|
||||
{
|
||||
let origin = nav::HistoryEntry {
|
||||
session: self.source_workspace.session(),
|
||||
path: self.source_workspace.active.clone(),
|
||||
generation: buffer.generation(),
|
||||
cursor: self.source_editor.cursor(),
|
||||
};
|
||||
self.source_symbols
|
||||
.back
|
||||
.retain(|entry| entry.session == origin.session);
|
||||
if self.source_symbols.back.len() == 64 {
|
||||
self.source_symbols.back.remove(0);
|
||||
}
|
||||
self.source_symbols.back.push(origin);
|
||||
}
|
||||
self.source_workspace
|
||||
.switch(path, self.source_editor.cursor());
|
||||
self.show_source_file(false);
|
||||
self.queue_source_reveal(cursor, false);
|
||||
}
|
||||
|
||||
fn source_symbol_back(&mut self) {
|
||||
while let Some(origin) = self.source_symbols.back.pop() {
|
||||
if origin.session != self.source_workspace.session() {
|
||||
continue;
|
||||
}
|
||||
let Some(buffer) = self.source_workspace.files.get(&origin.path) else {
|
||||
self.source_symbols.notice = Some("The previous file is no longer loaded.".into());
|
||||
return;
|
||||
};
|
||||
let unchanged = origin.generation == buffer.generation();
|
||||
let cursor = if unchanged {
|
||||
origin.cursor
|
||||
} else if origin.path == self.source_workspace.active {
|
||||
self.source_editor.cursor()
|
||||
} else {
|
||||
buffer.cursor
|
||||
};
|
||||
if !nav::exact_cursor(&buffer.text, cursor) {
|
||||
self.source_symbols.notice = Some(
|
||||
"The previous location changed. Open the file to choose a current position."
|
||||
.into(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
self.source_workspace
|
||||
.switch(&origin.path, self.source_editor.cursor());
|
||||
self.show_source_file(false);
|
||||
self.queue_source_reveal(cursor, false);
|
||||
self.source_symbols.notice =
|
||||
(!unchanged).then(|| "This draft changed; its current cursor was restored.".into());
|
||||
return;
|
||||
}
|
||||
self.source_symbols.notice = Some("No previous source location in this workspace.".into());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ use std::collections::BTreeMap;
|
||||
mod contextual_source_tests;
|
||||
pub(crate) mod creation;
|
||||
pub(crate) mod scenario;
|
||||
mod scenario_lifecycle;
|
||||
pub(crate) mod source_draft;
|
||||
pub(crate) mod state_canvas;
|
||||
pub(crate) mod state_edit;
|
||||
@@ -1140,6 +1141,17 @@ struct BoxItem {
|
||||
detail: String,
|
||||
marker: Option<Kind>,
|
||||
}
|
||||
fn box_text_clip(rect: Rectangle, scale: f32, viewport: Size) -> Option<Rectangle> {
|
||||
if !scale.is_finite() || scale <= 0. || rect.width <= 20. || rect.height <= 16. {
|
||||
return None;
|
||||
}
|
||||
Rectangle::new(
|
||||
Point::new((rect.x + 10.) * scale, (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>,
|
||||
@@ -1161,6 +1173,7 @@ 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,
|
||||
@@ -1219,6 +1232,7 @@ impl Scene {
|
||||
rows: BTreeMap::new(),
|
||||
open_calls: vec![],
|
||||
sequence_bounds: BTreeMap::new(),
|
||||
lifecycle: scenario_lifecycle::render::Rendering::default(),
|
||||
};
|
||||
let mut lanes = p
|
||||
.children(owner)
|
||||
@@ -1305,7 +1319,17 @@ 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()
|
||||
@@ -1330,23 +1354,28 @@ 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];
|
||||
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,
|
||||
},
|
||||
);
|
||||
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.boxes.push(BoxItem {
|
||||
id,
|
||||
rect: Rectangle::new(Point::new(x - 102., 15.), Size::new(204., 70.)),
|
||||
title: name,
|
||||
title: if scenario {
|
||||
format!("Lane · {name}")
|
||||
} else {
|
||||
name
|
||||
},
|
||||
detail,
|
||||
marker: None,
|
||||
});
|
||||
@@ -1388,6 +1417,16 @@ 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,
|
||||
@@ -1607,17 +1646,27 @@ impl Scene {
|
||||
});
|
||||
}
|
||||
}
|
||||
fn activation_segment(&mut self, call: Id, x: f32, top: f32, bottom: f32) {
|
||||
if bottom <= top {
|
||||
return;
|
||||
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,
|
||||
});
|
||||
}
|
||||
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
|
||||
@@ -1630,6 +1679,18 @@ 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() {
|
||||
@@ -1643,6 +1704,21 @@ 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 {
|
||||
@@ -1652,14 +1728,19 @@ impl Scene {
|
||||
},
|
||||
..
|
||||
}) if *correlation == call => {
|
||||
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,
|
||||
});
|
||||
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,
|
||||
});
|
||||
}
|
||||
start = None;
|
||||
}
|
||||
Some(SharedBody::Occurrence {
|
||||
@@ -1667,7 +1748,7 @@ impl Scene {
|
||||
..
|
||||
}) => {
|
||||
split = true;
|
||||
self.activation_segment(call, x, top, y);
|
||||
self.activation_segment(call, owner, receiver, x, top, y);
|
||||
let bottom = self
|
||||
.boxes
|
||||
.iter()
|
||||
@@ -1702,7 +1783,7 @@ impl Scene {
|
||||
} else {
|
||||
top + 26.
|
||||
};
|
||||
self.activation_segment(call, x, top, bottom);
|
||||
self.activation_segment(call, owner, receiver, x, top, bottom);
|
||||
self.open_calls.push(Point::new(x + 8., bottom - 7.));
|
||||
}
|
||||
start.is_some()
|
||||
@@ -1735,6 +1816,12 @@ 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()
|
||||
@@ -1749,7 +1836,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.),
|
||||
@@ -1757,13 +1844,10 @@ 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()
|
||||
@@ -1771,6 +1855,7 @@ 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
|
||||
@@ -1785,6 +1870,34 @@ 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;
|
||||
@@ -2025,6 +2138,7 @@ impl canvas::Program<Message> for Scene {
|
||||
let mut f = Frame::new(renderer, bounds.size());
|
||||
f.fill_rectangle(Point::ORIGIN, bounds.size(), Color::WHITE);
|
||||
f.scale(self.scale);
|
||||
self.lifecycle.draw_lanes(&mut f, self.selected);
|
||||
for b in &self.boxes {
|
||||
let outline = matches!(
|
||||
b.marker,
|
||||
@@ -2103,24 +2217,31 @@ impl canvas::Program<Message> for Scene {
|
||||
}
|
||||
}
|
||||
for b in &self.boxes {
|
||||
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()
|
||||
if let Some(clip) = box_text_clip(b.rect, self.scale, f.size()) {
|
||||
// Clipped child frames start with an identity transform. Match
|
||||
// the card's scale explicitly, as for lifecycle Create labels.
|
||||
f.with_clip(clip, |label| {
|
||||
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()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2133,6 +2254,8 @@ impl canvas::Program<Message> for Scene {
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
self.lifecycle
|
||||
.draw_markers(&mut f, self.selected, self.scale);
|
||||
if let Some((context, tool, editable)) = self.interaction {
|
||||
let active = editable && state.context == Some(context) && state.tool == tool;
|
||||
let point = cursor
|
||||
|
||||
@@ -709,7 +709,6 @@ fn canvas_pane<'a, R: iced::advanced::text::Renderer + 'a>(
|
||||
.width(Fill)
|
||||
.height(Fill),
|
||||
))
|
||||
// A new owner/document needs its first measurement even at the same size.
|
||||
.key((context.document, context.scenario))
|
||||
.on_show(move |size| Message::ScenarioCanvas(CanvasEvent::Viewport(context, size)))
|
||||
.on_resize(move |size| Message::ScenarioCanvas(CanvasEvent::Viewport(context, size))),
|
||||
|
||||
@@ -342,7 +342,7 @@ fn scenario_workspace_fit_uses_measured_inner_viewport_and_rejects_nonfinite_inp
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scenario_workspace_fit_reobserves_same_sized_owner_and_document_changes() {
|
||||
fn scenario_workspace_same_size_owner_and_document_changes_remeasure_fit_viewport() {
|
||||
use super::super::direct_tests::{navigation_event, navigation_fixture};
|
||||
let (project, [a, b, _], [reference, _, _]) = navigation_fixture();
|
||||
let mut app = App::with_project(project.clone());
|
||||
@@ -358,14 +358,13 @@ fn scenario_workspace_fit_reobserves_same_sized_owner_and_document_changes() {
|
||||
);
|
||||
let mut tree = widget::Tree::new(view.as_widget());
|
||||
redraw(&mut view, &mut tree, size, &mut app);
|
||||
let (_, first) = geometry(&mut view, &mut tree, size);
|
||||
let measured = first.scrolls["scenario-canvas"].0.size();
|
||||
assert_eq!(app.behavior.scenario.presentation.viewport, Some(measured));
|
||||
let first_viewport = app.behavior.scenario.presentation.viewport.unwrap();
|
||||
navigation_event(&mut app, |ctx| CanvasEvent::Zoom(ctx, None));
|
||||
assert!(app.behavior.scenario.presentation.scale(graph) < 1.);
|
||||
|
||||
let accepted = app.document.snapshot();
|
||||
navigation_event(&mut app, |ctx| CanvasEvent::OpenUse(ctx, reference));
|
||||
assert_eq!(app.scenario_context().unwrap().scenario, b);
|
||||
assert_eq!(app.behavior.scenario.presentation.viewport, None);
|
||||
assert!(app.behavior.scenario.presentation.viewport.is_none());
|
||||
view = fixture_view(
|
||||
&app.behavior.scenario,
|
||||
app.scenario_context().unwrap(),
|
||||
@@ -374,50 +373,38 @@ fn scenario_workspace_fit_reobserves_same_sized_owner_and_document_changes() {
|
||||
);
|
||||
tree.diff(view.as_widget());
|
||||
redraw(&mut view, &mut tree, size, &mut app);
|
||||
let (_, second) = geometry(&mut view, &mut tree, size);
|
||||
assert_eq!(second.scrolls["scenario-canvas"].0.size(), measured);
|
||||
assert_eq!(app.behavior.scenario.presentation.viewport, Some(measured));
|
||||
assert_eq!(
|
||||
app.behavior.scenario.presentation.viewport,
|
||||
Some(first_viewport)
|
||||
);
|
||||
navigation_event(&mut app, |ctx| CanvasEvent::Zoom(ctx, None));
|
||||
let fitted = app.behavior.scenario.presentation.scale(graph);
|
||||
assert!(
|
||||
fitted < 1.
|
||||
&& graph.width * fitted <= measured.width - 16.
|
||||
&& graph.height * fitted <= measured.height - 16.
|
||||
assert!(graph.width * fitted <= first_viewport.width - 16.);
|
||||
assert!(graph.height * fitted <= first_viewport.height - 16.);
|
||||
|
||||
let prior_document = app.scenario_context().unwrap().document;
|
||||
app = App::with_project(project);
|
||||
let _ = app.update(Message::Select(b));
|
||||
let _ = app.update(Message::Page(crate::app::Page::Behavior));
|
||||
let context = app.scenario_context().unwrap();
|
||||
assert_ne!(context.document, prior_document);
|
||||
assert_eq!(
|
||||
context.scenario, b,
|
||||
"semantic UUID is retained across documents"
|
||||
);
|
||||
assert!(app.behavior.scenario.presentation.viewport.is_none());
|
||||
let accepted = app.document.snapshot();
|
||||
view = fixture_view(&app.behavior.scenario, context, false, 68.);
|
||||
tree.diff(view.as_widget());
|
||||
redraw(&mut view, &mut tree, size, &mut app);
|
||||
assert_eq!(
|
||||
app.behavior.scenario.presentation.viewport,
|
||||
Some(first_viewport)
|
||||
);
|
||||
navigation_event(&mut app, |ctx| CanvasEvent::Zoom(ctx, None));
|
||||
assert_eq!(app.behavior.scenario.presentation.scale(graph), fitted);
|
||||
assert!(std::sync::Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
||||
assert!(!app.document.can_undo());
|
||||
|
||||
let document = app.scenario_context().unwrap().document;
|
||||
let mut replacement = App::with_project(project);
|
||||
let _ = replacement.update(Message::Select(b));
|
||||
let _ = replacement.update(Message::Page(crate::app::Page::Behavior));
|
||||
assert_ne!(replacement.scenario_context().unwrap().document, document);
|
||||
assert_eq!(replacement.behavior.scenario.presentation.viewport, None);
|
||||
let accepted = replacement.document.snapshot();
|
||||
view = fixture_view(
|
||||
&replacement.behavior.scenario,
|
||||
replacement.scenario_context().unwrap(),
|
||||
false,
|
||||
68.,
|
||||
);
|
||||
tree.diff(view.as_widget());
|
||||
redraw(&mut view, &mut tree, size, &mut replacement);
|
||||
let (_, replaced) = geometry(&mut view, &mut tree, size);
|
||||
assert_eq!(replaced.scrolls["scenario-canvas"].0.size(), measured);
|
||||
assert_eq!(
|
||||
replacement.behavior.scenario.presentation.viewport,
|
||||
Some(measured)
|
||||
);
|
||||
navigation_event(&mut replacement, |ctx| CanvasEvent::Zoom(ctx, None));
|
||||
assert_eq!(
|
||||
replacement.behavior.scenario.presentation.scale(graph),
|
||||
fitted
|
||||
);
|
||||
assert!(std::sync::Arc::ptr_eq(
|
||||
&accepted,
|
||||
&replacement.document.snapshot()
|
||||
));
|
||||
assert!(!replacement.document.can_undo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
//! Read-only lifecycle geometry. Operand screen order is never execution order.
|
||||
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)>,
|
||||
pub scopes: &'a BTreeMap<Id, (f32, f32)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(crate) struct Span {
|
||||
pub start: f32,
|
||||
pub end: f32,
|
||||
}
|
||||
impl Span {
|
||||
fn valid(self) -> bool {
|
||||
self.start.is_finite() && self.end.is_finite() && self.start <= self.end
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum Life {
|
||||
Alive,
|
||||
Absent,
|
||||
Unresolved,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(crate) struct Interval {
|
||||
pub scope: Id,
|
||||
pub span: Span,
|
||||
pub life: Life,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum MarkerKind {
|
||||
Birth,
|
||||
Death,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(crate) struct Marker {
|
||||
pub occurrence: Id,
|
||||
pub scope: Id,
|
||||
pub y: f32,
|
||||
pub kind: MarkerKind,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum UncertaintyReason {
|
||||
Conditional,
|
||||
Repeated,
|
||||
Parallel,
|
||||
MissingGeometry,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct Uncertainty {
|
||||
pub scope: Id,
|
||||
pub reason: UncertaintyReason,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct LaneLifetime {
|
||||
pub lifeline: Id,
|
||||
pub intervals: Vec<Interval>,
|
||||
pub markers: Vec<Marker>,
|
||||
pub uncertainties: Vec<Uncertainty>,
|
||||
}
|
||||
|
||||
/// Stable lane-ID order; source/model/history and caller-owned geometry are untouched.
|
||||
/// An opaque ScenarioUse is a local use, not a caller birth/death contract.
|
||||
pub(crate) fn project_lifetimes(
|
||||
project: &Project,
|
||||
scenario: Id,
|
||||
geometry: &Geometry<'_>,
|
||||
) -> Vec<LaneLifetime> {
|
||||
if project
|
||||
.elements
|
||||
.get(&scenario)
|
||||
.is_none_or(|e| e.kind() != Kind::Scenario)
|
||||
{
|
||||
return vec![];
|
||||
}
|
||||
project
|
||||
.elements
|
||||
.values()
|
||||
.filter(|e| e.kind() == Kind::Lifeline && e.parent == Some(scenario))
|
||||
.map(|e| {
|
||||
let mut projection = Projection {
|
||||
project,
|
||||
geometry,
|
||||
lane: e.id,
|
||||
visiting: BTreeSet::new(),
|
||||
result: LaneLifetime {
|
||||
lifeline: e.id,
|
||||
intervals: vec![],
|
||||
markers: vec![],
|
||||
uncertainties: vec![],
|
||||
},
|
||||
};
|
||||
let initial = if projection.contains(scenario, Some(MarkerKind::Birth)) {
|
||||
Life::Unresolved
|
||||
} else {
|
||||
Life::Alive
|
||||
};
|
||||
projection.sequence(scenario, initial);
|
||||
projection.result
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
struct Projection<'a, 'g> {
|
||||
project: &'a Project,
|
||||
geometry: &'a Geometry<'g>,
|
||||
lane: Id,
|
||||
visiting: BTreeSet<Id>,
|
||||
result: LaneLifetime,
|
||||
}
|
||||
impl Projection<'_, '_> {
|
||||
fn body(&self, id: Id) -> Option<&OccurrenceBody> {
|
||||
match self.project.elements.get(&id)?.shared()? {
|
||||
SharedBody::Occurrence { body, .. } => Some(body),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn event(&self, id: Id) -> Option<MarkerKind> {
|
||||
match self.body(id)? {
|
||||
OccurrenceBody::Exchange {
|
||||
receiver: Some(line),
|
||||
message_kind,
|
||||
..
|
||||
} if *line == self.lane => match message_kind {
|
||||
MessageKind::Create => Some(MarkerKind::Birth),
|
||||
MessageKind::Delete => Some(MarkerKind::Death),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn contains(&self, scope: Id, kind: Option<MarkerKind>) -> bool {
|
||||
// Local structural walk only: no guards, iteration simulation or reuse expansion.
|
||||
let mut pending = vec![scope];
|
||||
let mut seen = BTreeSet::new();
|
||||
while let Some(id) = pending.pop() {
|
||||
if !seen.insert(id) {
|
||||
continue;
|
||||
}
|
||||
if self
|
||||
.event(id)
|
||||
.is_some_and(|event| kind.is_none_or(|k| event == k))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if let Some(steps) = self.project.elements.get(&id).and_then(|e| e.steps()) {
|
||||
pending.extend(steps);
|
||||
}
|
||||
if let Some(OccurrenceBody::Fragment { branches, .. }) = self.body(id) {
|
||||
pending.extend(branches);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
fn span(&self, scope: Id) -> Option<Span> {
|
||||
let &(start, end) = self.geometry.scopes.get(&scope)?;
|
||||
let span = Span { start, end };
|
||||
span.valid().then_some(span)
|
||||
}
|
||||
fn uncertain(&mut self, scope: Id, reason: UncertaintyReason) {
|
||||
let value = Uncertainty { scope, reason };
|
||||
if !self.result.uncertainties.contains(&value) {
|
||||
self.result.uncertainties.push(value);
|
||||
}
|
||||
}
|
||||
fn interval(&mut self, scope: Id, start: f32, end: f32, life: Life) {
|
||||
if start < end && start.is_finite() && end.is_finite() {
|
||||
self.result.intervals.push(Interval {
|
||||
scope,
|
||||
span: Span { start, end },
|
||||
life,
|
||||
});
|
||||
}
|
||||
}
|
||||
fn sequence(&mut self, scope: Id, entry: Life) -> Life {
|
||||
let Some(span) = self.span(scope) else {
|
||||
self.uncertain(scope, UncertaintyReason::MissingGeometry);
|
||||
return Life::Unresolved;
|
||||
};
|
||||
if !self.visiting.insert(scope) {
|
||||
self.uncertain(scope, UncertaintyReason::MissingGeometry);
|
||||
return Life::Unresolved;
|
||||
}
|
||||
let steps = self
|
||||
.project
|
||||
.elements
|
||||
.get(&scope)
|
||||
.and_then(|e| e.steps())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
// A creation directly in this sequence fixes its local birth boundary.
|
||||
// A creation only in a nested operand cannot fix its ancestor's entry.
|
||||
let mut life = if steps
|
||||
.iter()
|
||||
.any(|&id| self.event(id) == Some(MarkerKind::Birth))
|
||||
{
|
||||
Life::Absent
|
||||
} else {
|
||||
entry
|
||||
};
|
||||
let mut cursor = span.start;
|
||||
for id in steps {
|
||||
let event = self.event(id);
|
||||
let fragment = match self.body(id) {
|
||||
Some(OccurrenceBody::Fragment { operator, branches }) => {
|
||||
Some((*operator, branches.clone()))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if event.is_none() && fragment.is_none() {
|
||||
continue;
|
||||
}
|
||||
let y = self
|
||||
.geometry
|
||||
.rows
|
||||
.get(&id)
|
||||
.filter(|(owner, y)| {
|
||||
*owner == scope && y.is_finite() && *y >= cursor && *y <= span.end
|
||||
})
|
||||
.map(|(_, y)| *y);
|
||||
let Some(y) = y else {
|
||||
self.uncertain(id, UncertaintyReason::MissingGeometry);
|
||||
life = Life::Unresolved;
|
||||
continue;
|
||||
};
|
||||
self.interval(scope, cursor, y, life);
|
||||
cursor = y;
|
||||
if let Some(kind) = event {
|
||||
self.result.markers.push(Marker {
|
||||
occurrence: id,
|
||||
scope,
|
||||
y,
|
||||
kind,
|
||||
});
|
||||
life = match kind {
|
||||
MarkerKind::Birth => Life::Alive,
|
||||
MarkerKind::Death => Life::Absent,
|
||||
};
|
||||
}
|
||||
if let Some((operator, branches)) = fragment {
|
||||
let entry = life;
|
||||
let mut missing = false;
|
||||
for branch in branches {
|
||||
let bounds = self
|
||||
.span(branch)
|
||||
.filter(|s| s.start >= cursor && s.end <= span.end);
|
||||
if let Some(bounds) = bounds {
|
||||
// Every sibling inherits the same entry, never its displayed predecessor's exit.
|
||||
self.sequence(branch, entry);
|
||||
cursor = bounds.end;
|
||||
} else {
|
||||
self.uncertain(branch, UncertaintyReason::MissingGeometry);
|
||||
missing = true;
|
||||
}
|
||||
}
|
||||
if self.contains(id, None) {
|
||||
let reason = match operator {
|
||||
FragmentOperator::Alternative | FragmentOperator::Optional => {
|
||||
UncertaintyReason::Conditional
|
||||
}
|
||||
FragmentOperator::Loop => UncertaintyReason::Repeated,
|
||||
FragmentOperator::Parallel => UncertaintyReason::Parallel,
|
||||
};
|
||||
self.uncertain(id, reason);
|
||||
life = Life::Unresolved;
|
||||
} else if missing {
|
||||
life = Life::Unresolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.interval(scope, cursor, span.end, life);
|
||||
self.visiting.remove(&scope);
|
||||
life
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "scenario_lifecycle/tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,377 @@
|
||||
//! 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) {
|
||||
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(marker.capsule, scale, frame.size()) {
|
||||
// WGPU clipped frames start with an identity transform;
|
||||
// neither the parent's scale nor its clip is inherited.
|
||||
frame.with_clip(clip, |label| {
|
||||
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(capsule: Rectangle, scale: f32, viewport: Size) -> Option<Rectangle> {
|
||||
if !scale.is_finite() || scale <= 0. {
|
||||
return None;
|
||||
}
|
||||
Rectangle::new(
|
||||
Point::new((capsule.x + 8.) * scale, (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;
|
||||
@@ -0,0 +1,796 @@
|
||||
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(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(card, 2., Size::new(300., 500.)),
|
||||
Some(Rectangle::new(Point::new(220., 416.), Size::new(80., 84.)))
|
||||
);
|
||||
assert_eq!(
|
||||
box_text_clip(
|
||||
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(card, 2., viewport), None);
|
||||
}
|
||||
for scale in [0., -1., f32::NAN, f32::INFINITY] {
|
||||
assert_eq!(box_text_clip(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(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(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(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(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(capsule, 2., viewport), None);
|
||||
}
|
||||
for scale in [0., -1., f32::NAN, f32::INFINITY] {
|
||||
assert_eq!(
|
||||
birth_label_clip(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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
use super::*;
|
||||
use oa_core::*;
|
||||
|
||||
struct Fixture {
|
||||
p: Project,
|
||||
owner: Id,
|
||||
a: Id,
|
||||
b: Id,
|
||||
exchange: Id,
|
||||
rows: BTreeMap<Id, (Id, f32)>,
|
||||
scopes: BTreeMap<Id, (f32, f32)>,
|
||||
}
|
||||
impl Fixture {
|
||||
fn new(layer: Layer) -> Self {
|
||||
let mut p = Project::empty();
|
||||
let kind = if layer == Layer::Oa {
|
||||
Kind::Activity
|
||||
} else {
|
||||
Kind::Function
|
||||
};
|
||||
let a = p.add_element(kind, layer, "Request", None).unwrap();
|
||||
let b = p.add_element(kind, layer, "Provide", None).unwrap();
|
||||
let owner = p
|
||||
.add_element(Kind::Scenario, layer, "Lifecycle", None)
|
||||
.unwrap();
|
||||
let exchange = p.relate(
|
||||
if layer == Layer::Oa {
|
||||
RelationKind::Interaction
|
||||
} else {
|
||||
RelationKind::FunctionalExchange
|
||||
},
|
||||
"Service",
|
||||
a,
|
||||
b,
|
||||
);
|
||||
let line = |p: &mut Project, represented| {
|
||||
let id = p
|
||||
.add_element(Kind::Lifeline, layer, "Participant", Some(owner))
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
|
||||
represented: Some(represented),
|
||||
};
|
||||
id
|
||||
};
|
||||
let a = line(&mut p, a);
|
||||
let b = line(&mut p, b);
|
||||
Self {
|
||||
p,
|
||||
owner,
|
||||
a,
|
||||
b,
|
||||
exchange,
|
||||
rows: BTreeMap::new(),
|
||||
scopes: [(owner, (0., 600.))].into(),
|
||||
}
|
||||
}
|
||||
fn occurrence(&mut self, owner: Id, y: f32, body: OccurrenceBody) -> Id {
|
||||
let mut e = self
|
||||
.p
|
||||
.make_element(
|
||||
Kind::Occurrence,
|
||||
self.p.elements[&owner].layer(),
|
||||
"Occurrence",
|
||||
Some(owner),
|
||||
)
|
||||
.unwrap();
|
||||
let id = e.id;
|
||||
*e.shared_mut().unwrap() = SharedBody::Occurrence {
|
||||
body,
|
||||
precondition: String::new(),
|
||||
postcondition: String::new(),
|
||||
};
|
||||
self.p.elements.insert(id, e);
|
||||
if let Some(steps) = self.p.elements.get_mut(&owner).unwrap().steps_mut() {
|
||||
steps.push(id);
|
||||
} else if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Fragment { branches, .. },
|
||||
..
|
||||
}) = self.p.elements.get_mut(&owner).unwrap().shared_mut()
|
||||
{
|
||||
branches.push(id);
|
||||
}
|
||||
self.rows.insert(id, (owner, y));
|
||||
id
|
||||
}
|
||||
fn message(&mut self, owner: Id, y: f32, kind: MessageKind) -> Id {
|
||||
self.occurrence(
|
||||
owner,
|
||||
y,
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: Some(self.exchange),
|
||||
sender: Some(self.a),
|
||||
receiver: Some(self.b),
|
||||
message_kind: kind,
|
||||
operation: None,
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
},
|
||||
)
|
||||
}
|
||||
fn fragment(&mut self, owner: Id, y: f32, operator: FragmentOperator) -> Id {
|
||||
self.occurrence(
|
||||
owner,
|
||||
y,
|
||||
OccurrenceBody::Fragment {
|
||||
operator,
|
||||
branches: vec![],
|
||||
},
|
||||
)
|
||||
}
|
||||
fn branch(&mut self, fragment: Id, start: f32, end: f32) -> Id {
|
||||
let id = self.occurrence(
|
||||
fragment,
|
||||
start,
|
||||
OccurrenceBody::Branch {
|
||||
guard: "condition".into(),
|
||||
occurrences: vec![],
|
||||
},
|
||||
);
|
||||
self.scopes.insert(id, (start, end));
|
||||
id
|
||||
}
|
||||
fn project(&self) -> Vec<LaneLifetime> {
|
||||
let errors: Vec<_> = validation::validate(&self.p)
|
||||
.into_iter()
|
||||
.filter(|i| i.severity == validation::Severity::Error)
|
||||
.collect();
|
||||
assert!(
|
||||
errors.is_empty(),
|
||||
"fixture must be ordinarily admitted: {errors:?}"
|
||||
);
|
||||
let before = self.p.clone();
|
||||
let source = source_project::format_bundle(&self.p);
|
||||
let geometry = Geometry {
|
||||
rows: &self.rows,
|
||||
scopes: &self.scopes,
|
||||
};
|
||||
let result = project_lifetimes(&self.p, self.owner, &geometry);
|
||||
assert_eq!(result, project_lifetimes(&self.p, self.owner, &geometry));
|
||||
assert_eq!(self.p, before);
|
||||
assert_eq!(source_project::format_bundle(&self.p), source);
|
||||
assert!(
|
||||
result
|
||||
.windows(2)
|
||||
.all(|pair| pair[0].lifeline < pair[1].lifeline)
|
||||
);
|
||||
result
|
||||
}
|
||||
fn lane(&self) -> LaneLifetime {
|
||||
self.project()
|
||||
.into_iter()
|
||||
.find(|lane| lane.lifeline == self.b)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
fn intervals(lane: &LaneLifetime, scope: Id) -> Vec<(f32, f32, Life)> {
|
||||
lane.intervals
|
||||
.iter()
|
||||
.filter(|i| i.scope == scope)
|
||||
.map(|i| (i.span.start, i.span.end, i.life))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_lifetime_has_exact_birth_death_boundaries_without_source_mutation() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = Fixture::new(layer);
|
||||
let create = f.message(f.owner, 100., MessageKind::Create);
|
||||
f.message(f.owner, 200., MessageKind::Asynchronous);
|
||||
let delete = f.message(f.owner, 300., MessageKind::Delete);
|
||||
let lane = f.lane();
|
||||
assert_eq!(
|
||||
intervals(&lane, f.owner),
|
||||
vec![
|
||||
(0., 100., Life::Absent),
|
||||
(100., 300., Life::Alive),
|
||||
(300., 600., Life::Absent)
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
lane.markers,
|
||||
vec![
|
||||
Marker {
|
||||
occurrence: create,
|
||||
scope: f.owner,
|
||||
y: 100.,
|
||||
kind: MarkerKind::Birth
|
||||
},
|
||||
Marker {
|
||||
occurrence: delete,
|
||||
scope: f.owner,
|
||||
y: 300.,
|
||||
kind: MarkerKind::Death
|
||||
},
|
||||
]
|
||||
);
|
||||
let sender = f
|
||||
.project()
|
||||
.into_iter()
|
||||
.find(|lane| lane.lifeline == f.a)
|
||||
.unwrap();
|
||||
assert_eq!(intervals(&sender, f.owner), vec![(0., 600., Life::Alive)]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alternative_death_does_not_kill_its_displayed_sibling() {
|
||||
let mut f = Fixture::new(Layer::Sa);
|
||||
let fragment = f.fragment(f.owner, 100., FragmentOperator::Alternative);
|
||||
let left = f.branch(fragment, 120., 250.);
|
||||
let right = f.branch(fragment, 300., 450.);
|
||||
f.message(left, 200., MessageKind::Delete);
|
||||
f.message(right, 350., MessageKind::Asynchronous);
|
||||
let lane = f.lane();
|
||||
assert_eq!(
|
||||
intervals(&lane, left),
|
||||
vec![(120., 200., Life::Alive), (200., 250., Life::Absent)]
|
||||
);
|
||||
assert_eq!(intervals(&lane, right), vec![(300., 450., Life::Alive)]);
|
||||
assert_eq!(
|
||||
intervals(&lane, f.owner),
|
||||
vec![(0., 100., Life::Alive), (450., 600., Life::Unresolved)]
|
||||
);
|
||||
assert_eq!(
|
||||
lane.uncertainties,
|
||||
vec![Uncertainty {
|
||||
scope: fragment,
|
||||
reason: UncertaintyReason::Conditional
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conditional_birth_does_not_infer_absence_in_siblings_or_ancestors() {
|
||||
for operator in [FragmentOperator::Alternative, FragmentOperator::Optional] {
|
||||
let mut f = Fixture::new(Layer::Oa);
|
||||
let fragment = f.fragment(f.owner, 100., operator);
|
||||
let left = f.branch(fragment, 120., 250.);
|
||||
let right = f.branch(fragment, 300., 450.);
|
||||
f.message(left, 200., MessageKind::Create);
|
||||
let lane = f.lane();
|
||||
assert_eq!(
|
||||
intervals(&lane, left),
|
||||
vec![(120., 200., Life::Absent), (200., 250., Life::Alive)]
|
||||
);
|
||||
assert_eq!(
|
||||
intervals(&lane, right),
|
||||
vec![(300., 450., Life::Unresolved)]
|
||||
);
|
||||
assert_eq!(
|
||||
intervals(&lane, f.owner),
|
||||
vec![(0., 100., Life::Unresolved), (450., 600., Life::Unresolved)]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loop_lifetime_is_local_and_warning_bearing_loops_remain_drawable() {
|
||||
for balanced in [true, false] {
|
||||
let mut f = Fixture::new(Layer::Sa);
|
||||
let fragment = f.fragment(f.owner, 100., FragmentOperator::Loop);
|
||||
let iteration = f.branch(fragment, 120., 450.);
|
||||
let create = f.message(iteration, 160., MessageKind::Create);
|
||||
f.message(iteration, 250., MessageKind::Asynchronous);
|
||||
if balanced {
|
||||
f.message(iteration, 350., MessageKind::Delete);
|
||||
}
|
||||
let lane = f.lane();
|
||||
assert_eq!(lane.markers.len(), if balanced { 2 } else { 1 });
|
||||
assert_eq!(lane.markers[0].occurrence, create);
|
||||
assert!(lane.uncertainties.contains(&Uncertainty {
|
||||
scope: fragment,
|
||||
reason: UncertaintyReason::Repeated
|
||||
}));
|
||||
assert_eq!(
|
||||
intervals(&lane, f.owner).last().unwrap().2,
|
||||
Life::Unresolved
|
||||
);
|
||||
assert_eq!(
|
||||
validation::validate(&f.p)
|
||||
.iter()
|
||||
.any(|i| i.code == "lifeline_repetition"),
|
||||
!balanced
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_cleanup_inside_loop_does_not_claim_balanced_or_definite_exit() {
|
||||
let mut f = Fixture::new(Layer::Sa);
|
||||
let repeated = f.fragment(f.owner, 100., FragmentOperator::Loop);
|
||||
let iteration = f.branch(repeated, 120., 500.);
|
||||
f.message(iteration, 160., MessageKind::Create);
|
||||
let optional = f.fragment(iteration, 280., FragmentOperator::Optional);
|
||||
let cleanup = f.branch(optional, 320., 400.);
|
||||
f.message(cleanup, 350., MessageKind::Delete);
|
||||
let lane = f.lane();
|
||||
assert_eq!(
|
||||
intervals(&lane, cleanup),
|
||||
vec![(320., 350., Life::Alive), (350., 400., Life::Absent)]
|
||||
);
|
||||
assert_eq!(
|
||||
intervals(&lane, iteration),
|
||||
vec![
|
||||
(120., 160., Life::Absent),
|
||||
(160., 280., Life::Alive),
|
||||
(400., 500., Life::Unresolved)
|
||||
]
|
||||
);
|
||||
assert!(
|
||||
validation::validate(&f.p)
|
||||
.iter()
|
||||
.any(|i| i.code == "lifeline_repetition")
|
||||
);
|
||||
assert!(lane.uncertainties.contains(&Uncertainty {
|
||||
scope: optional,
|
||||
reason: UncertaintyReason::Conditional
|
||||
}));
|
||||
assert!(lane.uncertainties.contains(&Uncertainty {
|
||||
scope: repeated,
|
||||
reason: UncertaintyReason::Repeated
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lifecycle_free_fragments_do_not_poison_other_lanes() {
|
||||
let mut f = Fixture::new(Layer::Oa);
|
||||
let fragment = f.fragment(f.owner, 100., FragmentOperator::Parallel);
|
||||
let left = f.branch(fragment, 120., 250.);
|
||||
let right = f.branch(fragment, 300., 450.);
|
||||
f.message(left, 200., MessageKind::Delete);
|
||||
let result = f.project();
|
||||
let sender = result.iter().find(|lane| lane.lifeline == f.a).unwrap();
|
||||
assert!(sender.intervals.iter().all(|i| i.life == Life::Alive));
|
||||
assert!(sender.uncertainties.is_empty());
|
||||
let target = result.iter().find(|lane| lane.lifeline == f.b).unwrap();
|
||||
assert!(target.uncertainties.contains(&Uncertainty {
|
||||
scope: fragment,
|
||||
reason: UncertaintyReason::Parallel
|
||||
}));
|
||||
assert_eq!(intervals(target, right), vec![(300., 450., Life::Alive)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_receiver_is_authoritative_even_with_missing_exchange() {
|
||||
let mut f = Fixture::new(Layer::Oa);
|
||||
let create = f.message(f.owner, 100., MessageKind::Create);
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { exchange, .. },
|
||||
..
|
||||
}) = f.p.elements.get_mut(&create).unwrap().shared_mut()
|
||||
{
|
||||
*exchange = None;
|
||||
}
|
||||
assert_eq!(f.lane().markers[0].occurrence, create);
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
receiver, exchange, ..
|
||||
},
|
||||
..
|
||||
}) = f.p.elements.get_mut(&create).unwrap().shared_mut()
|
||||
{
|
||||
*receiver = None;
|
||||
*exchange = Some(f.exchange);
|
||||
}
|
||||
assert!(
|
||||
f.lane().markers.is_empty(),
|
||||
"exchange endpoints never invent a receiver"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_geometry_is_reported_without_nonfinite_or_guessed_markers() {
|
||||
let mut f = Fixture::new(Layer::Oa);
|
||||
let create = f.message(f.owner, 100., MessageKind::Create);
|
||||
f.rows.insert(create, (f.owner, f32::NAN));
|
||||
let lane = f.lane();
|
||||
assert!(lane.markers.is_empty());
|
||||
assert_eq!(
|
||||
lane.uncertainties,
|
||||
vec![Uncertainty {
|
||||
scope: create,
|
||||
reason: UncertaintyReason::MissingGeometry
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
intervals(&lane, f.owner),
|
||||
vec![(0., 600., Life::Unresolved)]
|
||||
);
|
||||
f.scopes.remove(&f.owner);
|
||||
let lane = f.lane();
|
||||
assert!(lane.intervals.is_empty());
|
||||
assert_eq!(lane.uncertainties[0].scope, f.owner);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scenario_use_is_opaque_and_never_imports_callee_lifecycle_events() {
|
||||
let mut f = Fixture::new(Layer::Oa);
|
||||
let callee =
|
||||
f.p.add_element(Kind::Scenario, Layer::Oa, "Callee", None)
|
||||
.unwrap();
|
||||
let remote_a =
|
||||
f.p.add_element(Kind::Lifeline, Layer::Oa, "Remote A", Some(callee))
|
||||
.unwrap();
|
||||
let remote_b =
|
||||
f.p.add_element(Kind::Lifeline, Layer::Oa, "Remote B", Some(callee))
|
||||
.unwrap();
|
||||
for (remote, local) in [(remote_a, f.a), (remote_b, f.b)] {
|
||||
let body = f.p.elements[&local].shared().unwrap().clone();
|
||||
*f.p.elements.get_mut(&remote).unwrap().shared_mut().unwrap() = body;
|
||||
}
|
||||
f.occurrence(
|
||||
callee,
|
||||
200.,
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: Some(f.exchange),
|
||||
sender: Some(remote_a),
|
||||
receiver: Some(remote_b),
|
||||
message_kind: MessageKind::Create,
|
||||
operation: None,
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
},
|
||||
);
|
||||
f.occurrence(
|
||||
f.owner,
|
||||
100.,
|
||||
OccurrenceBody::ScenarioUse {
|
||||
scenario: Some(callee),
|
||||
bindings: vec![
|
||||
LifelineBinding {
|
||||
referenced: remote_a,
|
||||
local: f.a,
|
||||
},
|
||||
LifelineBinding {
|
||||
referenced: remote_b,
|
||||
local: f.b,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
let lane = f.lane();
|
||||
assert_eq!(intervals(&lane, f.owner), vec![(0., 600., Life::Alive)]);
|
||||
assert!(lane.markers.is_empty());
|
||||
assert!(lane.uncertainties.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_retains_exact_authored_source_and_does_not_enter_history() {
|
||||
use crate::document::Document;
|
||||
let mut f = Fixture::new(Layer::Oa);
|
||||
f.message(f.owner, 100., MessageKind::Create);
|
||||
f.message(f.owner, 300., MessageKind::Delete);
|
||||
let mut source = source_project::format_bundle(&f.p);
|
||||
let text = source.files.get_mut(&source.entry).unwrap();
|
||||
*text = format!("// Retained author note — 雨\r\n{text}\n");
|
||||
let document =
|
||||
Document::from_bundle(source.clone(), source_project::ProjectLimits::default()).unwrap();
|
||||
let accepted = document.project().clone();
|
||||
let geometry = Geometry {
|
||||
rows: &f.rows,
|
||||
scopes: &f.scopes,
|
||||
};
|
||||
let result = project_lifetimes(document.project(), f.owner, &geometry);
|
||||
assert_eq!(result, f.project());
|
||||
assert_eq!(document.bundle(), &source);
|
||||
assert_eq!(document.project(), &accepted);
|
||||
assert!(!document.can_undo());
|
||||
assert!(!document.can_redo());
|
||||
}
|
||||
@@ -10,10 +10,6 @@ use std::{fmt, sync::Arc};
|
||||
const HISTORY_LIMIT: usize = 100;
|
||||
const HISTORY_BYTES: usize = 32 * 1024 * 1024;
|
||||
|
||||
#[path = "document_alias_rename.rs"]
|
||||
mod alias_rename;
|
||||
pub use alias_rename::PreparedAliasRename;
|
||||
|
||||
struct HistoryEntry {
|
||||
snapshot: Arc<CompiledSourceProject>,
|
||||
bytes: usize,
|
||||
@@ -95,13 +91,11 @@ impl From<Vec<SourceDiagnostic>> for DocumentError {
|
||||
}
|
||||
|
||||
pub struct Document {
|
||||
// Distinguishes equal accepted snapshots belonging to different documents.
|
||||
alias_owner: Arc<()>,
|
||||
current: Arc<CompiledSourceProject>,
|
||||
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,
|
||||
@@ -127,12 +121,11 @@ impl Document {
|
||||
let current = Arc::new(source_project::compile_bundle(&bundle, limits)?);
|
||||
let current_bytes = estimated_bytes(¤t);
|
||||
Ok(Self {
|
||||
alias_owner: Arc::new(()),
|
||||
current,
|
||||
current_bytes,
|
||||
past: Vec::new(),
|
||||
future: Vec::new(),
|
||||
saved: Some(Arc::new(bundle)),
|
||||
saved: Arc::new(bundle),
|
||||
revision: 0,
|
||||
view_index: Default::default(),
|
||||
limits,
|
||||
@@ -163,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.
|
||||
@@ -188,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> {
|
||||
@@ -401,17 +380,8 @@ impl Document {
|
||||
return false;
|
||||
}
|
||||
let bytes = estimated_bytes(&compiled);
|
||||
self.commit_snapshot(Arc::new(compiled), bytes)
|
||||
}
|
||||
|
||||
// Only validated internal paths supply a changed snapshot and its exact
|
||||
// estimate. The ordinary commit wrapper checks bundle inequality; the alias
|
||||
// path checks its private changed flag and exact accepted-snapshot authority.
|
||||
// Alias preparation shares this algorithm while retaining the live save and
|
||||
// history state, and does not clone/recompile the candidate on adoption.
|
||||
fn commit_snapshot(&mut self, compiled: Arc<CompiledSourceProject>, bytes: usize) -> bool {
|
||||
self.past.push(HistoryEntry {
|
||||
snapshot: std::mem::replace(&mut self.current, compiled),
|
||||
snapshot: std::mem::replace(&mut self.current, Arc::new(compiled)),
|
||||
bytes: std::mem::replace(&mut self.current_bytes, bytes),
|
||||
});
|
||||
self.future.clear();
|
||||
|
||||
@@ -1,233 +0,0 @@
|
||||
//! Alias-only worker preparation and guarded adoption. Public compiled DTOs do
|
||||
//! not authorize adoption; only this module can mint a prepared rename.
|
||||
use super::*;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// A pinned intent minted by its owning Document. Move it to a worker and call
|
||||
/// `prepare`; preparation has no access to the live Document or its histories.
|
||||
#[derive(Debug)]
|
||||
pub struct AliasRenameRequest {
|
||||
owner: Arc<()>,
|
||||
before: Arc<CompiledSourceProject>,
|
||||
revision: u64,
|
||||
limits: ProjectLimits,
|
||||
target: Id,
|
||||
old_alias: String,
|
||||
new_alias: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PreparedData {
|
||||
request: AliasRenameRequest,
|
||||
after: Arc<CompiledSourceProject>,
|
||||
before_bytes: usize,
|
||||
after_bytes: usize,
|
||||
changed: bool,
|
||||
}
|
||||
|
||||
/// Immutable, Clone-compatible transport for preview and approval. Neither
|
||||
/// mutable candidate access nor deserialization can manufacture this authority.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PreparedAliasRename(Arc<PreparedData>);
|
||||
|
||||
impl PreparedAliasRename {
|
||||
pub fn before(&self) -> &CompiledSourceProject {
|
||||
&self.0.request.before
|
||||
}
|
||||
|
||||
pub fn after(&self) -> &CompiledSourceProject {
|
||||
&self.0.after
|
||||
}
|
||||
|
||||
pub fn target(&self) -> Id {
|
||||
self.0.request.target
|
||||
}
|
||||
|
||||
pub fn old_alias(&self) -> &str {
|
||||
&self.0.request.old_alias
|
||||
}
|
||||
|
||||
pub fn new_alias(&self) -> &str {
|
||||
&self.0.request.new_alias
|
||||
}
|
||||
|
||||
pub fn revision(&self) -> u64 {
|
||||
self.0.request.revision
|
||||
}
|
||||
|
||||
pub fn changed(&self) -> bool {
|
||||
self.0.changed
|
||||
}
|
||||
}
|
||||
|
||||
fn refusal(message: &str) -> DocumentError {
|
||||
ModelError(message.into()).into()
|
||||
}
|
||||
|
||||
fn check_history_capacity(before_bytes: usize, after_bytes: usize) -> Result<(), DocumentError> {
|
||||
if before_bytes.saturating_add(after_bytes) > HISTORY_BYTES {
|
||||
return Err(refusal(
|
||||
"This rename exceeds the model Undo budget. No rename was applied.",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Document {
|
||||
pub fn alias_rename_request(
|
||||
&self,
|
||||
target: Id,
|
||||
replacement: String,
|
||||
) -> Result<AliasRenameRequest, DocumentError> {
|
||||
if self.revision == u64::MAX {
|
||||
return Err(refusal(
|
||||
"The document revision is exhausted. Reopen before renaming.",
|
||||
));
|
||||
}
|
||||
let old_alias = self
|
||||
.project()
|
||||
.identities()
|
||||
.into_iter()
|
||||
.find_map(|(id, alias)| (id == target).then(|| alias.to_owned()))
|
||||
.ok_or_else(|| refusal("The alias target is no longer present."))?;
|
||||
// A candidate has a nonzero estimate. Avoid expensive preparation when
|
||||
// even the current snapshot leaves no room for its immediate Undo.
|
||||
if old_alias != replacement && self.current_bytes >= HISTORY_BYTES {
|
||||
return Err(refusal(
|
||||
"This rename exceeds the model Undo budget. No rename was applied.",
|
||||
));
|
||||
}
|
||||
Ok(AliasRenameRequest {
|
||||
owner: self.alias_owner.clone(),
|
||||
before: self.current.clone(),
|
||||
revision: self.revision,
|
||||
limits: self.limits,
|
||||
target,
|
||||
old_alias,
|
||||
new_alias: replacement,
|
||||
})
|
||||
}
|
||||
|
||||
/// App must separately consume its current approval and check workspace,
|
||||
/// invocation and draft freshness. This checks accepted Document authority.
|
||||
pub fn commit_alias_rename(
|
||||
&mut self,
|
||||
prepared: &PreparedAliasRename,
|
||||
) -> Result<bool, DocumentError> {
|
||||
let data = &prepared.0;
|
||||
let request = &data.request;
|
||||
if self.revision == u64::MAX
|
||||
|| self.revision != request.revision
|
||||
|| !Arc::ptr_eq(&self.alias_owner, &request.owner)
|
||||
|| !Arc::ptr_eq(&self.current, &request.before)
|
||||
{
|
||||
return Err(refusal(
|
||||
"This rename preview is stale. Preview the current alias again.",
|
||||
));
|
||||
}
|
||||
if !data.changed {
|
||||
return Ok(false);
|
||||
}
|
||||
if self.current_bytes != data.before_bytes {
|
||||
return Err(refusal(
|
||||
"The rename history estimate is stale. Preview again.",
|
||||
));
|
||||
}
|
||||
check_history_capacity(self.current_bytes, data.after_bytes)?;
|
||||
Ok(self.commit_snapshot(data.after.clone(), data.after_bytes))
|
||||
}
|
||||
}
|
||||
|
||||
impl AliasRenameRequest {
|
||||
/// Run on the preparation worker. The typed command and source updater are
|
||||
/// the only candidate construction path, followed by alias-only invariants.
|
||||
pub fn prepare(self) -> Result<PreparedAliasRename, DocumentError> {
|
||||
let before_project = &self.before.decoded.project;
|
||||
let candidate = prepare_command(
|
||||
before_project,
|
||||
&Command::SetAlias(self.target, self.new_alias.clone()),
|
||||
)?;
|
||||
let mut expected = before_project.clone();
|
||||
assign_expected_alias(&mut expected, self.target, &self.new_alias)?;
|
||||
let identities: BTreeMap<_, _> = before_project
|
||||
.identities()
|
||||
.into_iter()
|
||||
.map(|(id, _)| (id, id))
|
||||
.collect();
|
||||
if candidate.project != expected || candidate.identity_map != identities {
|
||||
return Err(refusal(
|
||||
"Alias preparation changed unrelated model data or identities.",
|
||||
));
|
||||
}
|
||||
let after = source_project::update_bundle_with_identity_map(
|
||||
&self.before,
|
||||
&candidate.project,
|
||||
&candidate.identity_map,
|
||||
self.limits,
|
||||
)?;
|
||||
let before = &self.before;
|
||||
if after.decoded.project != expected
|
||||
|| before.bundle.entry != after.bundle.entry
|
||||
|| !before.bundle.files.keys().eq(after.bundle.files.keys())
|
||||
|| before.index.lexical_owners != after.index.lexical_owners
|
||||
|| !before
|
||||
.index
|
||||
.declarations
|
||||
.iter()
|
||||
.map(|(id, loc)| (id, &loc.path))
|
||||
.eq(after
|
||||
.index
|
||||
.declarations
|
||||
.iter()
|
||||
.map(|(id, loc)| (id, &loc.path)))
|
||||
{
|
||||
return Err(refusal(
|
||||
"Alias preparation changed source membership or ownership.",
|
||||
));
|
||||
}
|
||||
let changed = before.bundle != after.bundle;
|
||||
let before_bytes = estimated_bytes(before);
|
||||
let after_bytes = estimated_bytes(&after);
|
||||
if changed {
|
||||
check_history_capacity(before_bytes, after_bytes)?;
|
||||
}
|
||||
Ok(PreparedAliasRename(Arc::new(PreparedData {
|
||||
request: self,
|
||||
after: Arc::new(after),
|
||||
before_bytes,
|
||||
after_bytes,
|
||||
changed,
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
// Independent full-model expectation: SetAlias may change exactly one header
|
||||
// field, across every supported identity family. Do not reuse command execution
|
||||
// to derive the expected result, which would hide collateral command changes.
|
||||
fn assign_expected_alias(project: &mut Project, id: Id, alias: &str) -> Result<(), DocumentError> {
|
||||
if project.id == id {
|
||||
project.alias = alias.to_owned();
|
||||
} else if let Some(record) = project.elements.get_mut(&id) {
|
||||
record.alias = alias.to_owned();
|
||||
} else if let Some(record) = project.relations.get_mut(&id) {
|
||||
record.alias = alias.to_owned();
|
||||
} else if let Some(record) = project.diagrams.get_mut(&id) {
|
||||
record.alias = alias.to_owned();
|
||||
} else if let Some(record) = project.artifact_links.get_mut(&id) {
|
||||
record.alias = alias.to_owned();
|
||||
} else if let Some(record) = project.traces.get_mut(&id) {
|
||||
record.alias = alias.to_owned();
|
||||
} else {
|
||||
project
|
||||
.diagrams
|
||||
.values_mut()
|
||||
.find_map(|diagram| diagram.nodes.get_mut(&id))
|
||||
.ok_or_else(|| refusal("The alias target is no longer present."))?
|
||||
.alias = alias.to_owned();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "document_alias_rename_tests.rs"]
|
||||
mod tests;
|
||||
@@ -1,459 +0,0 @@
|
||||
use super::*;
|
||||
|
||||
fn fixture() -> Document {
|
||||
let bundle = SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("project.arcadia".into(), r#"arcadia 7;
|
||||
project mission "Испытание" id "10000000-0000-4000-8000-000000000001" {
|
||||
include "work.arcadia"; include "views.arcadia";
|
||||
}
|
||||
"#.replace('\n', "\r\n")),
|
||||
("work.arcadia".into(), r#"// @assess and assess are explanatory prose.
|
||||
oa_activity assess "Оценка\u0020ущерба" id "10000000-0000-4000-8000-000000000002" { description "Keep @assess and assess"; }
|
||||
oa_activity assist "Оценка\u0020ущерба" id "10000000-0000-4000-8000-000000000003" { parent @ // owner evidence
|
||||
assess; }
|
||||
sa_function service "Service" id "10000000-0000-4000-8000-000000000004" {}
|
||||
interaction transfer "Transfer" id "10000000-0000-4000-8000-000000000005" { source @assess; target @assist; }
|
||||
trace review "Reviewed correspondence" id "10000000-0000-4000-8000-000000000006" { oa @assess; sa @service; kind Refinement; baseline { oa { name "Old assess"; description "@assess"; }; sa { name "Old service"; description "Kept"; }; }; }
|
||||
artifact evidence "Evidence" id "10000000-0000-4000-8000-000000000007" { owner @review; uri "docs/assess.md"; }
|
||||
"#.replace('\n', "\r\n")),
|
||||
("views.arcadia".into(), r#"diagram overview "Overview" id "10000000-0000-4000-8000-000000000008" { layer Oa; routes [{ edge @ // routed identity
|
||||
transfer; waypoints [{ x 4.20e2; y -16; }]; }]; }
|
||||
node placement id "10000000-0000-4000-8000-000000000009" { diagram @overview; element @assess; position { x -0.0; y 12.375; }; size { width 260; height 94; }; }
|
||||
"#.replace('\n', "\r\n")),
|
||||
]),
|
||||
};
|
||||
let document = Document::from_bundle(bundle.clone(), ProjectLimits::default()).unwrap();
|
||||
assert_eq!(
|
||||
document.bundle(),
|
||||
&bundle,
|
||||
"fixture must need no migration or ID insertion"
|
||||
);
|
||||
document
|
||||
}
|
||||
|
||||
fn id(number: u8) -> Id {
|
||||
Id::parse_str(&format!("10000000-0000-4000-8000-{number:012}")).unwrap()
|
||||
}
|
||||
|
||||
fn prepare(document: &Document, target: Id, replacement: &str) -> PreparedAliasRename {
|
||||
document
|
||||
.alias_rename_request(target, replacement.into())
|
||||
.unwrap()
|
||||
.prepare()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
// Capture every live Document state relevant to refusal/no-op. Arc identity also
|
||||
// catches replacing an equal snapshot or saved marker behind the user's back.
|
||||
struct Unchanged {
|
||||
current: Arc<CompiledSourceProject>,
|
||||
saved: Option<Arc<SourceBundle>>,
|
||||
past: Vec<(Arc<CompiledSourceProject>, usize)>,
|
||||
future: Vec<(Arc<CompiledSourceProject>, usize)>,
|
||||
revision: u64,
|
||||
current_bytes: usize,
|
||||
recovered: bool,
|
||||
}
|
||||
|
||||
impl Unchanged {
|
||||
fn capture(document: &Document) -> Self {
|
||||
Self {
|
||||
current: document.snapshot(),
|
||||
saved: document.saved.clone(),
|
||||
past: document
|
||||
.past
|
||||
.iter()
|
||||
.map(|h| (h.snapshot.clone(), h.bytes))
|
||||
.collect(),
|
||||
future: document
|
||||
.future
|
||||
.iter()
|
||||
.map(|h| (h.snapshot.clone(), h.bytes))
|
||||
.collect(),
|
||||
revision: document.revision,
|
||||
current_bytes: document.current_bytes,
|
||||
recovered: document.recovered_unsaved,
|
||||
}
|
||||
}
|
||||
|
||||
fn assert(&self, document: &Document) {
|
||||
assert!(Arc::ptr_eq(&self.current, &document.current));
|
||||
match (&self.saved, &document.saved) {
|
||||
(Some(before), Some(after)) => assert!(Arc::ptr_eq(before, after)),
|
||||
(None, None) => {}
|
||||
_ => panic!("saved baseline changed"),
|
||||
}
|
||||
for (before, after) in [
|
||||
(&self.past, &document.past),
|
||||
(&self.future, &document.future),
|
||||
] {
|
||||
assert_eq!(before.len(), after.len());
|
||||
for ((snapshot, bytes), entry) in before.iter().zip(after) {
|
||||
assert!(Arc::ptr_eq(snapshot, &entry.snapshot));
|
||||
assert_eq!(*bytes, entry.bytes);
|
||||
}
|
||||
}
|
||||
assert_eq!(self.revision, document.revision);
|
||||
assert_eq!(self.current_bytes, document.current_bytes);
|
||||
assert_eq!(self.recovered, document.recovered_unsaved);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_identity_family_changes_only_authored_alias_roles_and_one_history_entry() {
|
||||
// Literal role-specific replacements are independent of the updater/tokenizer.
|
||||
// They deliberately exclude prose, historical review text and URI spelling.
|
||||
type AliasCase<'a> = (u8, &'a str, &'a str, &'a [(&'a str, &'a str)]);
|
||||
let cases: &[AliasCase<'_>] = &[
|
||||
(
|
||||
1,
|
||||
"mission",
|
||||
"renamed",
|
||||
&[("project mission ", "project renamed ")],
|
||||
),
|
||||
(
|
||||
2,
|
||||
"assess",
|
||||
"renamed",
|
||||
&[
|
||||
("oa_activity assess ", "oa_activity renamed "),
|
||||
("\r\n assess;", "\r\n renamed;"),
|
||||
("@assess;", "@renamed;"),
|
||||
],
|
||||
),
|
||||
(
|
||||
5,
|
||||
"transfer",
|
||||
"renamed",
|
||||
&[
|
||||
("interaction transfer ", "interaction renamed "),
|
||||
("\r\n transfer;", "\r\n renamed;"),
|
||||
],
|
||||
),
|
||||
(
|
||||
6,
|
||||
"review",
|
||||
"renamed",
|
||||
&[
|
||||
("trace review ", "trace renamed "),
|
||||
("owner @review;", "owner @renamed;"),
|
||||
],
|
||||
),
|
||||
(
|
||||
7,
|
||||
"evidence",
|
||||
"renamed",
|
||||
&[("artifact evidence ", "artifact renamed ")],
|
||||
),
|
||||
(
|
||||
8,
|
||||
"overview",
|
||||
"renamed",
|
||||
&[
|
||||
("diagram overview ", "diagram renamed "),
|
||||
("diagram @overview;", "diagram @renamed;"),
|
||||
],
|
||||
),
|
||||
(
|
||||
9,
|
||||
"placement",
|
||||
"renamed",
|
||||
&[("node placement ", "node renamed ")],
|
||||
),
|
||||
];
|
||||
for &(number, old, new, replacements) in cases {
|
||||
let mut document = fixture();
|
||||
let original = document.snapshot();
|
||||
let mut expected_model = document.project().clone();
|
||||
// Explicit per-family oracle, separate from production expectation helper.
|
||||
match number {
|
||||
1 => expected_model.alias = new.into(),
|
||||
2 => expected_model.elements.get_mut(&id(2)).unwrap().alias = new.into(),
|
||||
5 => expected_model.relations.get_mut(&id(5)).unwrap().alias = new.into(),
|
||||
6 => expected_model.traces.get_mut(&id(6)).unwrap().alias = new.into(),
|
||||
7 => expected_model.artifact_links.get_mut(&id(7)).unwrap().alias = new.into(),
|
||||
8 => expected_model.diagrams.get_mut(&id(8)).unwrap().alias = new.into(),
|
||||
9 => {
|
||||
expected_model
|
||||
.diagrams
|
||||
.get_mut(&id(8))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(9))
|
||||
.unwrap()
|
||||
.alias = new.into()
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
let mut expected_bundle = document.bundle().clone();
|
||||
for source in expected_bundle.files.values_mut() {
|
||||
for &(before, after) in replacements {
|
||||
*source = source.replace(before, after);
|
||||
}
|
||||
}
|
||||
let state = Unchanged::capture(&document);
|
||||
let prepared = prepare(&document, id(number), new);
|
||||
state.assert(&document);
|
||||
assert!(prepared.changed());
|
||||
assert_eq!(prepared.old_alias(), old);
|
||||
assert_eq!(prepared.new_alias(), new);
|
||||
assert_eq!(prepared.target(), id(number));
|
||||
assert_eq!(prepared.revision(), 0);
|
||||
assert_eq!(prepared.before().bundle, original.bundle);
|
||||
assert_eq!(prepared.after().bundle, expected_bundle);
|
||||
assert_eq!(prepared.after().decoded.project, expected_model);
|
||||
assert!(document.commit_alias_rename(&prepared).unwrap());
|
||||
assert!(
|
||||
Arc::ptr_eq(&document.current, &prepared.0.after),
|
||||
"adopt the exact prepared Arc"
|
||||
);
|
||||
assert_eq!(document.past.len(), 1);
|
||||
assert!(document.dirty());
|
||||
let after = Unchanged::capture(&document);
|
||||
assert!(document.commit_alias_rename(&prepared.clone()).is_err());
|
||||
after.assert(&document);
|
||||
assert!(document.undo());
|
||||
assert!(Arc::ptr_eq(&document.current, &original));
|
||||
assert!(!document.dirty());
|
||||
assert!(!document.can_undo());
|
||||
assert!(document.redo());
|
||||
assert_eq!(document.bundle(), &expected_bundle);
|
||||
assert_eq!(document.project(), &expected_model);
|
||||
document.mark_saved(document.snapshot());
|
||||
let reopened =
|
||||
Document::from_bundle(document.bundle().clone(), ProjectLimits::default()).unwrap();
|
||||
assert_eq!(reopened.project(), &expected_model);
|
||||
assert_eq!(reopened.bundle(), &expected_bundle);
|
||||
assert!(!reopened.dirty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn highlighted_keyword_alias_is_valid_without_normalizing_input() {
|
||||
assert!(matches!(
|
||||
language::tokenize("oa_activity")[0].class,
|
||||
language::TokenClass::Keyword
|
||||
));
|
||||
let mut document = fixture();
|
||||
let prepared = prepare(&document, id(2), "oa_activity");
|
||||
document.commit_alias_rename(&prepared).unwrap();
|
||||
assert_eq!(document.project().elements[&id(2)].alias, "oa_activity");
|
||||
assert_eq!(document.project().elements[&id(2)].name, "Оценка ущерба");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_and_identical_aliases_preserve_live_state_and_redo() {
|
||||
let mut document = fixture();
|
||||
let first = prepare(&document, id(2), "changed");
|
||||
document.commit_alias_rename(&first).unwrap();
|
||||
assert!(document.undo());
|
||||
document.invalidate_saved_baseline();
|
||||
document.mark_recovered();
|
||||
let state = Unchanged::capture(&document);
|
||||
for replacement in ["", "has space", "project", "assist", " assess", "assess "] {
|
||||
assert!(
|
||||
document
|
||||
.alias_rename_request(id(2), replacement.into())
|
||||
.unwrap()
|
||||
.prepare()
|
||||
.is_err()
|
||||
);
|
||||
state.assert(&document);
|
||||
}
|
||||
assert!(
|
||||
document
|
||||
.alias_rename_request(Id::nil(), "new".into())
|
||||
.is_err()
|
||||
);
|
||||
let no_op = prepare(&document, id(2), "assess");
|
||||
assert!(!no_op.changed());
|
||||
assert!(!document.commit_alias_rename(&no_op).unwrap());
|
||||
state.assert(&document);
|
||||
assert!(document.redo());
|
||||
assert_eq!(document.project().elements[&id(2)].alias, "changed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equal_bytes_undo_replay_cross_document_and_exhausted_revision_refuse() {
|
||||
let mut document = fixture();
|
||||
let old = prepare(&document, id(2), "renamed");
|
||||
let mut second = fixture();
|
||||
let second_state = Unchanged::capture(&second);
|
||||
assert!(second.commit_alias_rename(&old).is_err());
|
||||
second_state.assert(&second);
|
||||
// Even an identical shared accepted Arc cannot confer this Document's owner.
|
||||
second.current = document.current.clone();
|
||||
let second_state = Unchanged::capture(&second);
|
||||
assert!(second.commit_alias_rename(&old).is_err());
|
||||
second_state.assert(&second);
|
||||
document
|
||||
.execute(Command::Rename(id(2), "Later name".into()))
|
||||
.unwrap();
|
||||
let edited = Unchanged::capture(&document);
|
||||
assert!(document.commit_alias_rename(&old).is_err());
|
||||
edited.assert(&document);
|
||||
assert!(document.undo());
|
||||
assert!(Arc::ptr_eq(&document.current, &old.0.request.before));
|
||||
let undone = Unchanged::capture(&document);
|
||||
assert!(document.commit_alias_rename(&old).is_err());
|
||||
undone.assert(&document);
|
||||
document.revision = u64::MAX - 1;
|
||||
let last = prepare(&document, id(2), "renamed");
|
||||
document
|
||||
.execute(Command::Rename(id(2), "Exhaust revision".into()))
|
||||
.unwrap();
|
||||
document.undo();
|
||||
assert!(Arc::ptr_eq(&document.current, &last.0.request.before));
|
||||
let exhausted = Unchanged::capture(&document);
|
||||
assert!(document.commit_alias_rename(&last).is_err());
|
||||
assert!(
|
||||
document
|
||||
.alias_rename_request(id(2), "renamed".into())
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
document
|
||||
.alias_rename_request(id(2), "assess".into())
|
||||
.is_err()
|
||||
);
|
||||
exhausted.assert(&document);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_retains_latest_saved_baseline_and_recovery_state() {
|
||||
for baseline in ["saved_before", "unknown", "raw_repair", "recovered"] {
|
||||
let mut document = fixture();
|
||||
let prepared = prepare(&document, id(2), "renamed");
|
||||
let original = document.snapshot();
|
||||
match baseline {
|
||||
"saved_before" => document.mark_saved(original.clone()),
|
||||
"unknown" => document.invalidate_saved_baseline(),
|
||||
"raw_repair" => {
|
||||
let mut raw = document.bundle().clone();
|
||||
raw.files
|
||||
.insert("project.arcadia".into(), "invalid repair source".into());
|
||||
document.bind_repair_baseline(raw);
|
||||
}
|
||||
"recovered" => document.mark_recovered(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
let saved = document.saved.clone();
|
||||
let recovered = document.recovered_unsaved;
|
||||
assert!(document.commit_alias_rename(&prepared).unwrap());
|
||||
assert_eq!(document.saved, saved);
|
||||
assert_eq!(document.recovered_unsaved, recovered);
|
||||
assert!(document.dirty());
|
||||
assert!(document.undo());
|
||||
assert_eq!(document.saved, saved);
|
||||
assert_eq!(document.recovered_unsaved, recovered);
|
||||
assert_eq!(document.dirty(), baseline != "saved_before");
|
||||
}
|
||||
// A completed save after Preview remains authoritative, including when it
|
||||
// clears recovery; a worker must not restore its earlier state on Confirm.
|
||||
let mut document = fixture();
|
||||
document.mark_recovered();
|
||||
document.invalidate_saved_baseline();
|
||||
let prepared = prepare(&document, id(2), "renamed");
|
||||
document.mark_saved(document.snapshot());
|
||||
document.commit_alias_rename(&prepared).unwrap();
|
||||
assert!(!document.recovered_unsaved);
|
||||
document.undo();
|
||||
assert!(!document.dirty());
|
||||
// Undo from saved C to B, then a new B->C rename can correctly finish clean.
|
||||
document.redo();
|
||||
document.mark_saved(document.snapshot());
|
||||
document.undo();
|
||||
let return_to_saved = prepare(&document, id(2), "renamed");
|
||||
document.commit_alias_rename(&return_to_saved).unwrap();
|
||||
assert!(!document.dirty());
|
||||
assert!(!document.can_redo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mutable_public_preview_copies_cannot_change_prepared_authority() {
|
||||
let mut document = fixture();
|
||||
let prepared = prepare(&document, id(2), "renamed");
|
||||
let exact = prepared.after().clone();
|
||||
let mut counterfeit = prepared.after().clone();
|
||||
counterfeit.decoded.project.name = "Injected unrelated edit".into();
|
||||
counterfeit.index.declarations.clear();
|
||||
let unchanged = Unchanged::capture(&document);
|
||||
assert!(document.apply_compiled(counterfeit).is_err());
|
||||
unchanged.assert(&document);
|
||||
let mut unrelated = exact.decoded.project.clone();
|
||||
unrelated.name = "A self-consistent unrelated candidate".into();
|
||||
let forged_dto = source_project::compile_bundle(
|
||||
&source_project::format_bundle(&unrelated),
|
||||
ProjectLimits::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_ne!(forged_dto.decoded.project, prepared.after().decoded.project);
|
||||
// No API converts either DTO to PreparedAliasRename. Mutating owned copies
|
||||
// leaves the opaque capability and its exact adopted model/source intact.
|
||||
document.commit_alias_rename(&prepared).unwrap();
|
||||
assert_eq!(document.project(), &exact.decoded.project);
|
||||
assert_eq!(document.bundle(), &exact.bundle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_history_capacity_admits_immediate_undo_and_rejects_larger_candidate() {
|
||||
let mut document = fixture();
|
||||
let mut bundle = document.bundle().clone();
|
||||
let padding = HISTORY_BYTES / 2 - document.current_bytes;
|
||||
bundle
|
||||
.files
|
||||
.get_mut("views.arcadia")
|
||||
.unwrap()
|
||||
.push_str(&format!("//{}\r\n", "x".repeat(padding - 4)));
|
||||
document.apply_bundle(bundle).unwrap();
|
||||
assert_eq!(document.current_bytes, HISTORY_BYTES / 2);
|
||||
// A same-length alias retains the exact estimate; older history is evictable.
|
||||
let boundary = prepare(&document, id(2), "rating");
|
||||
assert_eq!(
|
||||
boundary.0.before_bytes + boundary.0.after_bytes,
|
||||
HISTORY_BYTES
|
||||
);
|
||||
let unchanged = Unchanged::capture(&document);
|
||||
assert!(
|
||||
document
|
||||
.alias_rename_request(id(2), "longer_alias".into())
|
||||
.unwrap()
|
||||
.prepare()
|
||||
.is_err()
|
||||
);
|
||||
unchanged.assert(&document);
|
||||
document.commit_alias_rename(&boundary).unwrap();
|
||||
assert_eq!(document.past.len(), 1);
|
||||
assert!(document.undo());
|
||||
assert!(Arc::ptr_eq(&document.current, &boundary.0.request.before));
|
||||
assert!(!document.can_undo());
|
||||
assert!(document.redo());
|
||||
assert!(Arc::ptr_eq(&document.current, &boundary.0.after));
|
||||
assert!(check_history_capacity(usize::MAX, 1).is_err());
|
||||
assert!(check_history_capacity(HISTORY_BYTES, 1).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mint_and_commit_refuse_exhausted_or_inconsistent_live_history_estimate() {
|
||||
let mut document = fixture();
|
||||
let prepared = prepare(&document, id(2), "renamed");
|
||||
// Inject cache corruption to falsify the commit-side check independently of
|
||||
// the preparation admission. Normal edits also change revision and Arc.
|
||||
document.current_bytes = HISTORY_BYTES;
|
||||
let unchanged = Unchanged::capture(&document);
|
||||
assert!(
|
||||
document
|
||||
.alias_rename_request(id(2), "renamed".into())
|
||||
.is_err()
|
||||
);
|
||||
assert!(document.commit_alias_rename(&prepared).is_err());
|
||||
// No-op intent needs no Undo reservation even at the admission boundary.
|
||||
assert!(
|
||||
document
|
||||
.alias_rename_request(id(2), "assess".into())
|
||||
.is_ok()
|
||||
);
|
||||
unchanged.assert(&document);
|
||||
}
|
||||
@@ -484,28 +484,36 @@ 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 sender receiver fragment branch alternative loop guard parallel strict optional occurrence exchange action state interface operation allocation call reply unresolved",
|
||||
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",
|
||||
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.",
|
||||
"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 + Message and its style. Call + correlated reply creates both occurrences in one edit. Select the exchange and compatible sending/receiving lifelines. Reply to existing call offers valid unanswered calls.",
|
||||
"Use Action, Observe state or Fragment for other behavior. 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 a message style and Draw message. Start at a highlighted sequence position on a lifeline, then click or drag to the receiving lifeline. The form retains the chosen position while you select the exchange and details. Escape cancels the gesture.",
|
||||
"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.",
|
||||
],
|
||||
),
|
||||
walkthrough(
|
||||
"Describe alternatives",
|
||||
&[
|
||||
"Choose Fragment and select ALT, PAR, LOOP or OPT. Enter the branch conditions; the tool creates the fragment and its operands together.",
|
||||
"Use the Destination selector in subsequent message or action forms to choose the intended operand. New occurrences append to that operand's ordered behavior.",
|
||||
"Use Reuse scenario to refer to existing behavior. Bind every referenced lifeline to a compatible local lifeline explicitly. Open source follows the original scenario without copying its contents.",
|
||||
"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 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",
|
||||
&[
|
||||
@@ -698,6 +706,10 @@ fn article(topic: Topic) -> Article {
|
||||
"Map references by meaning",
|
||||
"Reference mapping connect exact OA and SA identities used by the proposed change. Similar names are not sufficient. Map the records that play the same engineering role, including enclosing fields in qualified message-value paths. Mapping a referenced record does not authorize creating, moving or editing that record. If it needs a counterpart, establish that correspondence first.",
|
||||
),
|
||||
explain(
|
||||
"Edit stored observations deliberately",
|
||||
"In a trace's Properties, Edit stored history (advanced)… edits the observations retained by that trace. Choose the fact and enter its Operational and System observations explicitly. Not applicable, an observed absent value, and no stored fact have different meanings. Historical references may retain the exact UUID of a deleted record. Apply changes validates the complete history; Undo restores it. These history edits leave both counterpart records unchanged. Use Review counterpart changes… for Keep/Take decisions about their current engineering content.",
|
||||
),
|
||||
walkthrough(
|
||||
"Review one message value or lifeline binding",
|
||||
&[
|
||||
@@ -785,7 +797,7 @@ fn article(topic: Topic) -> Article {
|
||||
const {
|
||||
Article {
|
||||
summary: "The text editor and graphical forms edit the same model. Source projects preserve file organization and comments for human review, automation and version control.",
|
||||
keywords: "text textual source language syntax highlighting lint parser git aliases uuid identity rename F2 preview diff merge restructuredtext spec canonical parity definition references lexical navigation F12 back",
|
||||
keywords: "text textual source language syntax highlighting lint parser git aliases uuid identity rename diff merge restructuredtext spec canonical parity",
|
||||
sections: &[
|
||||
walkthrough(
|
||||
"Edit and review source",
|
||||
@@ -798,27 +810,10 @@ fn article(topic: Topic) -> Article {
|
||||
"Save all writes the entry and its included UTF-8 files. Review the Git diff before committing, including relationships, traces and diagram changes that belong to the same engineering edit.",
|
||||
],
|
||||
),
|
||||
explain(
|
||||
"Follow definitions and lexical references",
|
||||
"Place the caret on a declaration alias or an @reference. Choose Definition (F12) to locate its declaration, or References (Shift+F12) to list lexical uses. Choose a row when a definition is ambiguous, draft or detached. Lexical uses exclude comments and strings; they do not establish typed relationships or resolve an ambiguous alias to one declaration. Back (Alt+Left) returns to the previous source location; if that draft changed, it restores the current cursor instead of an obsolete selection. These shortcuts work while the source editor has focus; the Text toolbar also offers the actions.",
|
||||
),
|
||||
explain(
|
||||
"Read the navigation scope",
|
||||
"Queries inspect bounded snapshots of currently loaded drafts, without applying them or loading missing included files. The panel identifies partial scope, draft evidence, detached or unverified files, and alias or ID collisions. Counts show total, displayed and omitted rows. A missing result in partial scope does not establish that the whole project has no match. If sources change, stale rows are disabled; run Definition or References again. Row locations use one-based lines and UTF-8 byte columns. Navigation preserves your source drafts and the accepted model.",
|
||||
),
|
||||
explain(
|
||||
"Names, aliases and IDs serve different purposes",
|
||||
"The displayed name is engineering prose. A stable @alias is the readable source reference and need not change when the name changes. UUID identity distinguishes the persistent record. Preserve IDs when revising the same engineering object; changing identity is a deliberate operation that affects references.",
|
||||
),
|
||||
walkthrough(
|
||||
"Rename a source alias",
|
||||
&[
|
||||
"Place the caret on a declaration alias or live @reference, then choose Rename alias or press F2 in the source editor. First finish or revert pending source, property and behavior drafts; close or reattach any extra loaded detached files. Accepted edits do not need to be saved first.",
|
||||
"Edit Source alias and choose Preview (Enter also previews). Pasting line breaks, tabs or other control characters is rejected without changing the field; choose Preview again after correcting the paste. Review the changed files, hunks and lines; long lines have additional segments. The engineering name and UUID stay unchanged. Comments and quoted prose keep their original spelling. This preview covers the rename; a later Save can include earlier accepted edits too.",
|
||||
"Choose Rename alias to accept the complete preview, or Cancel/Escape to leave everything unchanged. An invalid, unchanged, stale or oversized preview cannot be confirmed. After editing the replacement, choose Preview again. If the source context changes, close the form and invoke Rename again on the current alias.",
|
||||
"Use Undo rename immediately, or the model Undo/Redo controls, to restore the complete source/model snapshot. Ctrl+Z in Text still undoes source drafting. Save all and reopen use the normal project workflow; an unconfirmed preview is not saved.",
|
||||
],
|
||||
),
|
||||
example(
|
||||
"Readable references",
|
||||
"For example, renaming an activity does not require renaming @assess_incident. A related record can still refer to that alias.",
|
||||
|
||||
@@ -24,7 +24,6 @@ mod recovery;
|
||||
mod route_edit;
|
||||
mod semantic_review;
|
||||
mod source_navigation;
|
||||
mod source_rename;
|
||||
mod source_tree;
|
||||
mod source_workspace;
|
||||
mod style;
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
//! Literal source search and line navigation, independent of accepted model state.
|
||||
mod controller;
|
||||
pub mod interaction;
|
||||
pub mod query;
|
||||
pub mod symbols;
|
||||
pub use controller::*;
|
||||
use iced::widget::text_editor::{Cursor, Position};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
//! Ephemeral editor navigation; no model commands or filesystem operations.
|
||||
use super::query;
|
||||
use iced::widget::text_editor::{Cursor, Position};
|
||||
use oa_core::Id;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SymbolEvent {
|
||||
Query(query::Action),
|
||||
Back,
|
||||
Close,
|
||||
Choose {
|
||||
stamp: query::QueryStamp,
|
||||
reference: bool,
|
||||
index: usize,
|
||||
},
|
||||
}
|
||||
|
||||
/// Identity of the mounted source view that emitted an action. Message queues
|
||||
/// may outlive that view; request-time stamping alone cannot detect replay.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SymbolContext {
|
||||
pub session: Id,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HistoryEntry {
|
||||
pub session: Id,
|
||||
pub path: String,
|
||||
pub generation: u64,
|
||||
pub cursor: Cursor,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Request {
|
||||
pub job: Id,
|
||||
pub interaction: Id,
|
||||
pub raw_interaction: u64,
|
||||
pub stamp: query::QueryStamp,
|
||||
pub path: String,
|
||||
pub offset: usize,
|
||||
pub cursor: Cursor,
|
||||
pub action: query::Action,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Completion {
|
||||
pub request: Request,
|
||||
pub result: Result<(Arc<query::Snapshot>, Arc<query::Results>), String>,
|
||||
}
|
||||
|
||||
pub struct Reveal {
|
||||
pub ticket: Id,
|
||||
pub interaction: Id,
|
||||
pub raw_interaction: u64,
|
||||
pub snapshot: query::SnapshotKey,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
pub struct SymbolState {
|
||||
pub open: bool,
|
||||
pub interaction: Id,
|
||||
pub raw_interaction: Arc<AtomicU64>,
|
||||
pub serial: u64,
|
||||
pub in_flight: Option<Id>,
|
||||
pub request: Option<Request>,
|
||||
pub cache: Option<Arc<query::Snapshot>>,
|
||||
pub results: Option<Arc<query::Results>>,
|
||||
pub notice: Option<String>,
|
||||
pub back: Vec<HistoryEntry>,
|
||||
pub reveal: Option<Reveal>,
|
||||
}
|
||||
|
||||
impl Default for SymbolState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
open: false,
|
||||
interaction: Id::new_v4(),
|
||||
raw_interaction: Arc::new(AtomicU64::new(0)),
|
||||
serial: 0,
|
||||
in_flight: None,
|
||||
request: None,
|
||||
cache: None,
|
||||
results: None,
|
||||
notice: None,
|
||||
back: Vec::new(),
|
||||
reveal: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iced uses zero-based UTF-8 byte columns. Reject stale or split-codepoint
|
||||
/// coordinates; do not normalize CRLF or silently clamp query destinations.
|
||||
pub fn byte_offset(text: &str, position: Position) -> Option<usize> {
|
||||
let start = if position.line == 0 {
|
||||
0
|
||||
} else {
|
||||
text.match_indices('\n').nth(position.line - 1)?.0 + 1
|
||||
};
|
||||
let end = text[start..]
|
||||
.find('\n')
|
||||
.map_or(text.len(), |end| start + end);
|
||||
let offset = start.checked_add(position.column)?;
|
||||
(offset <= end && text.is_char_boundary(offset)).then_some(offset)
|
||||
}
|
||||
|
||||
pub fn cursor_offset(text: &str, cursor: Cursor) -> Option<usize> {
|
||||
let position = cursor.selection.map_or(cursor.position, |selection| {
|
||||
if (selection.line, selection.column) < (cursor.position.line, cursor.position.column) {
|
||||
selection
|
||||
} else {
|
||||
cursor.position
|
||||
}
|
||||
});
|
||||
byte_offset(text, position)
|
||||
}
|
||||
|
||||
pub fn exact_cursor(text: &str, cursor: Cursor) -> bool {
|
||||
byte_offset(text, cursor.position).is_some()
|
||||
&& cursor
|
||||
.selection
|
||||
.is_none_or(|position| byte_offset(text, position).is_some())
|
||||
}
|
||||
@@ -1,284 +0,0 @@
|
||||
//! Raw input invalidation and focus checks at widget-operation execution time.
|
||||
use iced::advanced::{Clipboard, Layout, Shell, Widget, layout, overlay, renderer, widget};
|
||||
use iced::{Element, Event, Length, Rectangle, Size, Task, Vector, keyboard, mouse, touch};
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
};
|
||||
|
||||
/// Exhaustion fails closed: a saturated clock can never authorize navigation.
|
||||
/// Replace the clock with a new workspace/controller, rather than wrapping it.
|
||||
pub fn is_current(clock: &AtomicU64, expected: u64) -> bool {
|
||||
expected != u64::MAX && clock.load(Ordering::Acquire) == expected
|
||||
}
|
||||
|
||||
fn invalidate(clock: &AtomicU64, event: &Event) {
|
||||
if matches!(
|
||||
event,
|
||||
Event::Keyboard(keyboard::Event::KeyPressed { .. })
|
||||
| Event::Mouse(mouse::Event::ButtonPressed(_) | mouse::Event::WheelScrolled { .. })
|
||||
| Event::Touch(touch::Event::FingerPressed { .. })
|
||||
| Event::Window(iced::window::Event::Unfocused)
|
||||
) {
|
||||
let _ = clock.fetch_update(Ordering::AcqRel, Ordering::Acquire, |value| {
|
||||
Some(value.saturating_add(1))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Transparent, including overlays: invalidate before the child can capture input.
|
||||
pub fn observe<'a, Message: 'a, Theme: 'a, Renderer: renderer::Renderer + 'a>(
|
||||
content: Element<'a, Message, Theme, Renderer>,
|
||||
clock: Arc<AtomicU64>,
|
||||
) -> Element<'a, Message, Theme, Renderer> {
|
||||
Element::new(Observed { content, clock })
|
||||
}
|
||||
|
||||
struct Observed<'a, Message, Theme, Renderer> {
|
||||
content: Element<'a, Message, Theme, Renderer>,
|
||||
clock: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
impl<Message, Theme, Renderer: renderer::Renderer> Widget<Message, Theme, Renderer>
|
||||
for Observed<'_, Message, Theme, Renderer>
|
||||
{
|
||||
fn size(&self) -> Size<Length> {
|
||||
self.content.as_widget().size()
|
||||
}
|
||||
fn size_hint(&self) -> Size<Length> {
|
||||
self.content.as_widget().size_hint()
|
||||
}
|
||||
fn tag(&self) -> widget::tree::Tag {
|
||||
self.content.as_widget().tag()
|
||||
}
|
||||
fn state(&self) -> widget::tree::State {
|
||||
self.content.as_widget().state()
|
||||
}
|
||||
fn children(&self) -> Vec<widget::Tree> {
|
||||
self.content.as_widget().children()
|
||||
}
|
||||
fn diff(&self, tree: &mut widget::Tree) {
|
||||
self.content.as_widget().diff(tree);
|
||||
}
|
||||
fn layout(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
self.content.as_widget_mut().layout(tree, renderer, limits)
|
||||
}
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.content
|
||||
.as_widget()
|
||||
.draw(tree, renderer, theme, style, layout, cursor, viewport);
|
||||
}
|
||||
fn operate(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.operate(tree, layout, renderer, operation);
|
||||
}
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
invalidate(&self.clock, event);
|
||||
self.content.as_widget_mut().update(
|
||||
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
|
||||
);
|
||||
}
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.content
|
||||
.as_widget()
|
||||
.mouse_interaction(tree, layout, cursor, viewport, renderer)
|
||||
}
|
||||
fn overlay<'a>(
|
||||
&'a mut self,
|
||||
tree: &'a mut widget::Tree,
|
||||
layout: Layout<'a>,
|
||||
renderer: &Renderer,
|
||||
viewport: &Rectangle,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'a, Message, Theme, Renderer>> {
|
||||
let clock = self.clock.clone();
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.overlay(tree, layout, renderer, viewport, translation)
|
||||
.map(|content| overlay::Element::new(Box::new(ObservedOverlay { content, clock })))
|
||||
}
|
||||
}
|
||||
|
||||
struct ObservedOverlay<'a, Message, Theme, Renderer> {
|
||||
content: overlay::Element<'a, Message, Theme, Renderer>,
|
||||
clock: Arc<AtomicU64>,
|
||||
}
|
||||
impl<Message, Theme, Renderer: renderer::Renderer> overlay::Overlay<Message, Theme, Renderer>
|
||||
for ObservedOverlay<'_, Message, Theme, Renderer>
|
||||
{
|
||||
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
|
||||
self.content.as_overlay_mut().layout(renderer, bounds)
|
||||
}
|
||||
fn draw(
|
||||
&self,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
) {
|
||||
self.content
|
||||
.as_overlay()
|
||||
.draw(renderer, theme, style, layout, cursor);
|
||||
}
|
||||
fn operate(
|
||||
&mut self,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
self.content
|
||||
.as_overlay_mut()
|
||||
.operate(layout, renderer, operation);
|
||||
}
|
||||
fn update(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) {
|
||||
invalidate(&self.clock, event);
|
||||
self.content
|
||||
.as_overlay_mut()
|
||||
.update(event, layout, cursor, renderer, clipboard, shell);
|
||||
}
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.content
|
||||
.as_overlay()
|
||||
.mouse_interaction(layout, cursor, renderer)
|
||||
}
|
||||
fn overlay<'a>(
|
||||
&'a mut self,
|
||||
layout: Layout<'a>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'a, Message, Theme, Renderer>> {
|
||||
let clock = self.clock.clone();
|
||||
self.content
|
||||
.as_overlay_mut()
|
||||
.overlay(layout, renderer)
|
||||
.map(|content| overlay::Element::new(Box::new(ObservedOverlay { content, clock })))
|
||||
}
|
||||
fn index(&self) -> f32 {
|
||||
self.content.as_overlay().index()
|
||||
}
|
||||
}
|
||||
|
||||
/// Inspect all focus owners, then focus only the mounted target. Iced executes
|
||||
/// Operation::Chain synchronously, without an intervening input event. Both
|
||||
/// traversals recheck the raw clock; no result message authorizes later focus.
|
||||
pub fn guarded_focus<Message: Send + 'static>(
|
||||
target: widget::Id,
|
||||
clock: Arc<AtomicU64>,
|
||||
expected: u64,
|
||||
) -> Task<Message> {
|
||||
widget::operate(GuardedFocus::new(target, clock, expected))
|
||||
}
|
||||
|
||||
struct GuardedFocus {
|
||||
target: widget::Id,
|
||||
clock: Arc<AtomicU64>,
|
||||
expected: u64,
|
||||
target_count: usize,
|
||||
foreign_focus: bool,
|
||||
apply: bool,
|
||||
}
|
||||
impl GuardedFocus {
|
||||
fn new(target: widget::Id, clock: Arc<AtomicU64>, expected: u64) -> Self {
|
||||
Self {
|
||||
target,
|
||||
clock,
|
||||
expected,
|
||||
target_count: 0,
|
||||
foreign_focus: false,
|
||||
apply: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<Message: 'static> widget::Operation<Message> for GuardedFocus {
|
||||
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation<Message>)) {
|
||||
if is_current(&self.clock, self.expected) {
|
||||
visit(self);
|
||||
}
|
||||
}
|
||||
fn focusable(
|
||||
&mut self,
|
||||
id: Option<&widget::Id>,
|
||||
_: Rectangle,
|
||||
state: &mut dyn widget::operation::Focusable,
|
||||
) {
|
||||
if !is_current(&self.clock, self.expected) {
|
||||
return;
|
||||
}
|
||||
if id == Some(&self.target) {
|
||||
self.target_count += 1;
|
||||
if self.apply {
|
||||
state.focus();
|
||||
}
|
||||
} else if state.is_focused() {
|
||||
self.foreign_focus = true;
|
||||
}
|
||||
}
|
||||
fn finish(&self) -> widget::operation::Outcome<Message> {
|
||||
if !self.apply
|
||||
&& self.target_count == 1
|
||||
&& !self.foreign_focus
|
||||
&& is_current(&self.clock, self.expected)
|
||||
{
|
||||
widget::operation::Outcome::Chain(Box::new(Self {
|
||||
apply: true,
|
||||
..Self::new(self.target.clone(), self.clock.clone(), self.expected)
|
||||
}))
|
||||
} else {
|
||||
widget::operation::Outcome::None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, debug_assertions))]
|
||||
mod tests;
|
||||
@@ -1,190 +0,0 @@
|
||||
use super::*;
|
||||
use iced::advanced::widget::operation::{Focusable, Outcome};
|
||||
use iced::widget::{column, text_input};
|
||||
use iced::{Point, Theme};
|
||||
|
||||
#[derive(Default)]
|
||||
struct FocusMap(Vec<(Option<widget::Id>, Rectangle, bool)>);
|
||||
impl widget::Operation for FocusMap {
|
||||
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) {
|
||||
visit(self);
|
||||
}
|
||||
fn focusable(&mut self, id: Option<&widget::Id>, bounds: Rectangle, state: &mut dyn Focusable) {
|
||||
self.0.push((id.cloned(), bounds, state.is_focused()));
|
||||
}
|
||||
}
|
||||
|
||||
struct Mounted {
|
||||
element: Element<'static, (), Theme, ()>,
|
||||
tree: widget::Tree,
|
||||
node: layout::Node,
|
||||
}
|
||||
impl Mounted {
|
||||
fn new(clock: Arc<AtomicU64>) -> Self {
|
||||
let content = column![
|
||||
text_input("Editor", "source")
|
||||
.id("editor")
|
||||
.on_input(|_| ())
|
||||
.padding(12),
|
||||
text_input("Find", "")
|
||||
.id("find")
|
||||
.on_input(|_| ())
|
||||
.padding(12),
|
||||
]
|
||||
.spacing(10)
|
||||
.into();
|
||||
let mut element = observe(content, clock);
|
||||
let mut tree = widget::Tree::new(element.as_widget());
|
||||
let node = element.as_widget_mut().layout(
|
||||
&mut tree,
|
||||
&(),
|
||||
&layout::Limits::new(Size::ZERO, Size::new(400., 200.)),
|
||||
);
|
||||
Self {
|
||||
element,
|
||||
tree,
|
||||
node,
|
||||
}
|
||||
}
|
||||
fn pass(&mut self, operation: &mut dyn widget::Operation) {
|
||||
self.element.as_widget_mut().operate(
|
||||
&mut self.tree,
|
||||
Layout::new(&self.node),
|
||||
&(),
|
||||
operation,
|
||||
);
|
||||
}
|
||||
fn run(&mut self, operation: impl widget::Operation + 'static) {
|
||||
let mut next: Box<dyn widget::Operation> = Box::new(operation);
|
||||
loop {
|
||||
self.pass(next.as_mut());
|
||||
match next.finish() {
|
||||
Outcome::Chain(chained) => next = chained,
|
||||
Outcome::None => break,
|
||||
Outcome::Some(()) => panic!("focus must not publish an application message"),
|
||||
}
|
||||
}
|
||||
}
|
||||
fn focus(&mut self) -> FocusMap {
|
||||
let mut focus = FocusMap::default();
|
||||
self.pass(&mut focus);
|
||||
focus
|
||||
}
|
||||
fn click(&mut self, id: &str) {
|
||||
let bounds = self
|
||||
.focus()
|
||||
.0
|
||||
.into_iter()
|
||||
.find(|(found, _, _)| found.as_ref() == Some(&widget::Id::from(id.to_owned())))
|
||||
.unwrap()
|
||||
.1;
|
||||
let mut messages = Vec::new();
|
||||
let mut shell = Shell::new(&mut messages);
|
||||
self.element.as_widget_mut().update(
|
||||
&mut self.tree,
|
||||
&Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
||||
Layout::new(&self.node),
|
||||
mouse::Cursor::Available(bounds.center()),
|
||||
&(),
|
||||
&mut iced::advanced::clipboard::Null,
|
||||
&mut shell,
|
||||
&Rectangle::with_size(Size::new(400., 200.)),
|
||||
);
|
||||
assert!(shell.is_event_captured());
|
||||
assert!(
|
||||
messages.is_empty(),
|
||||
"focus-only click must not rely on App messages"
|
||||
);
|
||||
}
|
||||
fn focused(&mut self, id: &str) -> bool {
|
||||
self.focus()
|
||||
.0
|
||||
.into_iter()
|
||||
.find(|(found, _, _)| found.as_ref() == Some(&widget::Id::from(id.to_owned())))
|
||||
.unwrap()
|
||||
.2
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn captured_find_click_invalidates_without_app_message_and_preserves_focus() {
|
||||
let clock = Arc::new(AtomicU64::new(0));
|
||||
let mut ui = Mounted::new(clock.clone());
|
||||
ui.run(GuardedFocus::new("editor".into(), clock.clone(), 0));
|
||||
assert!(ui.focused("editor"));
|
||||
ui.click("find");
|
||||
assert!(!is_current(&clock, 0));
|
||||
assert!(ui.focused("find"));
|
||||
ui.run(GuardedFocus::new("editor".into(), clock.clone(), 0));
|
||||
assert!(ui.focused("find"));
|
||||
// Even a matching clock cannot authorize stealing another widget's focus.
|
||||
ui.run(GuardedFocus::new(
|
||||
"editor".into(),
|
||||
clock.clone(),
|
||||
clock.load(Ordering::Acquire),
|
||||
));
|
||||
assert!(ui.focused("find"));
|
||||
assert!(!ui.focused("editor"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_rechecks_clock_between_inspection_and_application_and_missing_target_is_inert() {
|
||||
let clock = Arc::new(AtomicU64::new(0));
|
||||
let mut ui = Mounted::new(clock.clone());
|
||||
let mut operation = GuardedFocus::new("editor".into(), clock.clone(), 0);
|
||||
ui.pass(&mut operation);
|
||||
let Outcome::Chain(mut apply) = <GuardedFocus as widget::Operation<()>>::finish(&operation)
|
||||
else {
|
||||
panic!("mounted, unopposed target must be eligible");
|
||||
};
|
||||
clock.store(1, Ordering::Release);
|
||||
ui.pass(apply.as_mut());
|
||||
assert!(!ui.focused("editor"));
|
||||
ui.run(GuardedFocus::new("editor".into(), clock.clone(), 1));
|
||||
assert!(ui.focused("editor"));
|
||||
ui.run(GuardedFocus::new("removed-editor".into(), clock, 1));
|
||||
assert!(ui.focused("editor"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_navigation_inputs_advance_but_passive_geometry_does_not_and_exhaustion_refuses() {
|
||||
let clock = AtomicU64::new(0);
|
||||
let events = [
|
||||
Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key: keyboard::Key::Named(keyboard::key::Named::ArrowLeft),
|
||||
modified_key: keyboard::Key::Named(keyboard::key::Named::ArrowLeft),
|
||||
physical_key: keyboard::key::Physical::Code(keyboard::key::Code::ArrowLeft),
|
||||
location: keyboard::Location::Standard,
|
||||
modifiers: keyboard::Modifiers::empty(),
|
||||
text: None,
|
||||
repeat: false,
|
||||
}),
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
||||
Event::Mouse(mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Lines { x: 0., y: 1. },
|
||||
}),
|
||||
Event::Touch(touch::Event::FingerPressed {
|
||||
id: touch::Finger(1),
|
||||
position: Point::ORIGIN,
|
||||
}),
|
||||
Event::Window(iced::window::Event::Unfocused),
|
||||
];
|
||||
for (index, event) in events.iter().enumerate() {
|
||||
invalidate(&clock, event);
|
||||
assert_eq!(clock.load(Ordering::Acquire), index as u64 + 1);
|
||||
}
|
||||
invalidate(
|
||||
&clock,
|
||||
&Event::Mouse(mouse::Event::CursorMoved {
|
||||
position: Point::ORIGIN,
|
||||
}),
|
||||
);
|
||||
invalidate(&clock, &Event::Window(iced::window::Event::Focused));
|
||||
assert!(is_current(&clock, 5));
|
||||
clock.store(u64::MAX - 1, Ordering::Release);
|
||||
invalidate(&clock, &events[0]);
|
||||
assert!(!is_current(&clock, u64::MAX));
|
||||
invalidate(&clock, &events[0]);
|
||||
assert_eq!(clock.load(Ordering::Acquire), u64::MAX);
|
||||
assert!(!is_current(&clock, 0));
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,3 +0,0 @@
|
||||
//! Pure preview and exact origin mapping for the guarded alias-rename controller.
|
||||
pub mod origin;
|
||||
pub mod preview;
|
||||
@@ -1,278 +0,0 @@
|
||||
//! Map a role-proven alias occurrence through the exact prepared source change.
|
||||
//! This is cursor restoration, not target resolution or commit authority.
|
||||
use oa_core::language::{self, Outline, OutlineLimits, Span, TokenClass};
|
||||
use std::fmt;
|
||||
|
||||
const LIMITS: OutlineLimits = OutlineLimits {
|
||||
max_source_bytes: 64 * 1024 * 1024,
|
||||
max_tokens: 1_000_000,
|
||||
max_declarations: 250_000,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum OriginError {
|
||||
Budget,
|
||||
InvalidSource,
|
||||
MissingOrigin,
|
||||
ChangedStructure,
|
||||
}
|
||||
|
||||
impl fmt::Display for OriginError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::Budget => "The alias cursor preview exceeds its source scan budget. No rename was applied.",
|
||||
Self::InvalidSource => "The alias cursor could not be verified in this source. No rename was applied.",
|
||||
Self::MissingOrigin => "The original alias occurrence no longer matches. Preview again; no rename was applied.",
|
||||
Self::ChangedStructure => "The preview changed source outside the expected alias tokens. No rename was applied.",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for OriginError {}
|
||||
|
||||
fn word(class: TokenClass) -> bool {
|
||||
matches!(class, TokenClass::Identifier | TokenClass::Keyword)
|
||||
}
|
||||
|
||||
/// The controller passes the exact alias span returned by the admitted symbol
|
||||
/// hit. `marker` is Some only for a reference (its separate @ span), including
|
||||
/// invocation with the caret on @. A declaration passes None.
|
||||
pub fn map(
|
||||
before: &str,
|
||||
after: &str,
|
||||
old_alias: &str,
|
||||
new_alias: &str,
|
||||
origin: Span,
|
||||
marker: Option<Span>,
|
||||
) -> Result<Span, OriginError> {
|
||||
let old = language::outline(before, LIMITS).map_err(|_| OriginError::Budget)?;
|
||||
let new = language::outline(after, LIMITS).map_err(|_| OriginError::Budget)?;
|
||||
if old.has_lexical_errors || new.has_lexical_errors {
|
||||
return Err(OriginError::InvalidSource);
|
||||
}
|
||||
if old.tokens.len() != new.tokens.len() {
|
||||
return Err(OriginError::ChangedStructure);
|
||||
}
|
||||
let roles_before = roles(&old);
|
||||
let roles_after = roles(&new);
|
||||
let mut mapped = None;
|
||||
for (index, (a, b)) in old.tokens.iter().zip(&new.tokens).enumerate() {
|
||||
let left = &before[a.span.start..a.span.end];
|
||||
let right = &after[b.span.start..b.span.end];
|
||||
let alias_pair = word(a.class)
|
||||
&& word(b.class)
|
||||
&& left == old_alias
|
||||
&& right == new_alias
|
||||
&& roles_before[index].is_some()
|
||||
&& roles_after[index].is_some()
|
||||
&& roles_before[index].map(|role| role.is_some())
|
||||
== roles_after[index].map(|role| role.is_some());
|
||||
if a.span == origin {
|
||||
if !alias_pair || roles_before[index] != Some(marker) {
|
||||
return Err(OriginError::MissingOrigin);
|
||||
}
|
||||
mapped = Some(b.span);
|
||||
}
|
||||
if !alias_pair && (left != right || a.class != b.class) {
|
||||
return Err(OriginError::ChangedStructure);
|
||||
}
|
||||
}
|
||||
mapped.ok_or(OriginError::MissingOrigin)
|
||||
}
|
||||
|
||||
/// Outer None is not an alias role; Some(None) is a declaration alias;
|
||||
/// Some(Some(span)) is a live @ reference with its exact separate marker.
|
||||
/// Scan sorted declaration spans and tokens together, without repeated searches.
|
||||
fn roles(outline: &Outline<'_>) -> Vec<Option<Option<Span>>> {
|
||||
let mut result = vec![None; outline.tokens.len()];
|
||||
let mut declarations = outline.declarations.iter().peekable();
|
||||
let mut significant = None;
|
||||
for (index, token) in outline.tokens.iter().enumerate() {
|
||||
while declarations
|
||||
.peek()
|
||||
.is_some_and(|d| d.alias_span.start < token.span.start)
|
||||
{
|
||||
declarations.next();
|
||||
}
|
||||
if word(token.class) {
|
||||
if declarations
|
||||
.peek()
|
||||
.is_some_and(|d| d.alias_span == token.span)
|
||||
{
|
||||
result[index] = Some(None);
|
||||
} else if let Some(previous) = significant {
|
||||
let span: Span = previous;
|
||||
if &outline.source[span.start..span.end] == "@" {
|
||||
result[index] = Some(Some(span));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !matches!(token.class, TokenClass::Whitespace | TokenClass::Comment) {
|
||||
significant = Some(token.span);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn occurrence(source: &str, needle: &str, which: usize) -> Span {
|
||||
let start = source.match_indices(needle).nth(which).unwrap().0;
|
||||
Span {
|
||||
start,
|
||||
end: start + needle.len(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_last_repeated_reference_with_unicode_crlf_and_comment_separator() {
|
||||
let before =
|
||||
"oa_activity a \"救援\" {}\r\n// @a is prose\r\nsource @a; target @ // a\r\n a;\r\n";
|
||||
let after = "oa_activity renamed \"救援\" {}\r\n// @a is prose\r\nsource @renamed; target @ // a\r\n renamed;\r\n";
|
||||
let start = before.rfind(" a;").unwrap() + 1;
|
||||
let marker = before.rfind("@ //").unwrap();
|
||||
let mapped = map(
|
||||
before,
|
||||
after,
|
||||
"a",
|
||||
"renamed",
|
||||
Span {
|
||||
start,
|
||||
end: start + 1,
|
||||
},
|
||||
Some(Span {
|
||||
start: marker,
|
||||
end: marker + 1,
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&after[mapped.start..mapped.end], "renamed");
|
||||
assert_eq!(mapped.start, after.rfind(" renamed;").unwrap() + 1);
|
||||
assert_eq!(
|
||||
before[..start].bytes().filter(|b| *b == b'\n').count(),
|
||||
after[..mapped.start]
|
||||
.bytes()
|
||||
.filter(|b| *b == b'\n')
|
||||
.count()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_final_same_line_occurrence_not_first_or_prose() {
|
||||
let before = "oa_activity a \"a @a\" {} source @a; target @a;";
|
||||
let after = "oa_activity longer \"a @a\" {} source @longer; target @longer;";
|
||||
let start = before.rfind("@a").unwrap();
|
||||
let result = map(
|
||||
before,
|
||||
after,
|
||||
"a",
|
||||
"longer",
|
||||
Span {
|
||||
start: start + 1,
|
||||
end: start + 2,
|
||||
},
|
||||
Some(Span {
|
||||
start,
|
||||
end: start + 1,
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result.start, after.rfind("@longer").unwrap() + 1);
|
||||
let prose = occurrence(before, "a @a", 0);
|
||||
assert!(
|
||||
map(
|
||||
before,
|
||||
after,
|
||||
"a",
|
||||
"longer",
|
||||
Span {
|
||||
start: prose.start,
|
||||
end: prose.start + 1
|
||||
},
|
||||
None
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permits_identifier_keyword_transitions_in_alias_roles() {
|
||||
let before = "oa_activity a \"Name\" {} source @a;";
|
||||
let alias = occurrence(before, "a \"Name", 0);
|
||||
let origin = Span {
|
||||
start: alias.start,
|
||||
end: alias.start + 1,
|
||||
};
|
||||
for replacement in ["none", "include", "oa_activity"] {
|
||||
assert!(oa_core::valid_alias(replacement));
|
||||
assert_eq!(
|
||||
language::tokenize(replacement)[0].class,
|
||||
TokenClass::Keyword
|
||||
);
|
||||
let after = format!("oa_activity {replacement} \"Name\" {{}} source @{replacement};");
|
||||
let mapped = map(before, &after, "a", replacement, origin, None).unwrap();
|
||||
assert_eq!(&after[mapped.start..mapped.end], replacement);
|
||||
assert_eq!(
|
||||
map(&after, before, replacement, "a", mapped, None).unwrap(),
|
||||
origin
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_changed_trivia_non_alias_words_and_wrong_marker() {
|
||||
let before = "oa_activity a \"Name\" {} source @a;";
|
||||
let after = "oa_activity b \"Name\" {} source @b;";
|
||||
let start = before.rfind("@a").unwrap();
|
||||
let span = Span {
|
||||
start: start + 1,
|
||||
end: start + 2,
|
||||
};
|
||||
let marker = Some(Span {
|
||||
start,
|
||||
end: start + 1,
|
||||
});
|
||||
assert_eq!(
|
||||
map(
|
||||
before,
|
||||
&after.replace("Name", "Other"),
|
||||
"a",
|
||||
"b",
|
||||
span,
|
||||
marker
|
||||
),
|
||||
Err(OriginError::ChangedStructure)
|
||||
);
|
||||
assert_eq!(
|
||||
map(
|
||||
before,
|
||||
&after.replace(" source", " source"),
|
||||
"a",
|
||||
"b",
|
||||
span,
|
||||
marker
|
||||
),
|
||||
Err(OriginError::ChangedStructure)
|
||||
);
|
||||
assert_eq!(
|
||||
map(before, after, "a", "b", span, None),
|
||||
Err(OriginError::MissingOrigin)
|
||||
);
|
||||
assert!(
|
||||
map(
|
||||
before,
|
||||
after,
|
||||
"a",
|
||||
"b",
|
||||
Span {
|
||||
start: 0,
|
||||
end: usize::MAX
|
||||
},
|
||||
marker
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,578 +0,0 @@
|
||||
//! Complete, bounded line-ordinal previews of an already prepared alias rename.
|
||||
//!
|
||||
//! Source text remains in the caller's immutable before/after snapshots. This
|
||||
//! module owns only paths and indexes; it is not rename or commit authority.
|
||||
use oa_core::source_project::SourceBundle;
|
||||
use std::{fmt, ops::Range};
|
||||
|
||||
const CONTEXT: usize = 3;
|
||||
// A byte-grid boundary can move back by at most three UTF-8 continuation bytes,
|
||||
// so a 252-byte grid yields fragments of at most 255 bytes, independent of input.
|
||||
const SEGMENT_GRID: usize = 252;
|
||||
|
||||
/// Count all bounded fragments of exact source text; an empty line has one
|
||||
/// empty fragment. Page these as well as lines, never mount an unbounded line.
|
||||
pub fn segment_count(text: &str) -> usize {
|
||||
text.len().div_ceil(SEGMENT_GRID).max(1)
|
||||
}
|
||||
|
||||
/// Constant-time UTF-8-safe page lookup. Concatenating every fragment gives the
|
||||
/// original bytes, including CRLF. No ellipsis or truncation replaces content.
|
||||
pub fn text_segment(text: &str, index: usize) -> Option<&str> {
|
||||
if index >= segment_count(text) {
|
||||
return None;
|
||||
}
|
||||
let mut start = index.checked_mul(SEGMENT_GRID)?;
|
||||
let mut end = start.saturating_add(SEGMENT_GRID).min(text.len());
|
||||
while !text.is_char_boundary(start) {
|
||||
start -= 1;
|
||||
}
|
||||
while !text.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
text.get(start..end)
|
||||
}
|
||||
const LIMITS: Limits = Limits {
|
||||
files: 256,
|
||||
changed_lines: 50_000,
|
||||
display_bytes: 8 * 1024 * 1024,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Limits {
|
||||
files: usize,
|
||||
changed_lines: usize,
|
||||
display_bytes: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Preview {
|
||||
files: Vec<File>,
|
||||
changed_lines: usize,
|
||||
display_bytes: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct File {
|
||||
path: String,
|
||||
hunks: Vec<Hunk>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Hunk {
|
||||
lines: Vec<Line>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Line {
|
||||
number: usize,
|
||||
before: Range<usize>,
|
||||
after: Range<usize>,
|
||||
changed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum PreviewError {
|
||||
MembershipChanged,
|
||||
LineCountChanged(String),
|
||||
LineEndingChanged { path: String, line: usize },
|
||||
TooManyFiles { limit: usize },
|
||||
TooManyChangedLines { limit: usize },
|
||||
TooManyDisplayBytes { limit: usize },
|
||||
}
|
||||
|
||||
impl fmt::Display for PreviewError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::MembershipChanged => write!(
|
||||
f,
|
||||
"Rename preview refused: the entry or source file membership changed. No rename was applied."
|
||||
),
|
||||
Self::LineCountChanged(path) => write!(
|
||||
f,
|
||||
"Rename preview refused: the line count changed in {path}. No rename was applied."
|
||||
),
|
||||
Self::LineEndingChanged { path, line } => write!(
|
||||
f,
|
||||
"Rename preview refused: the line ending changed in {path}:{line}. No rename was applied."
|
||||
),
|
||||
Self::TooManyFiles { limit } => write!(
|
||||
f,
|
||||
"Rename preview exceeds {limit} changed files. A complete preview is required; no rename was applied."
|
||||
),
|
||||
Self::TooManyChangedLines { limit } => write!(
|
||||
f,
|
||||
"Rename preview exceeds {limit} changed source lines. A complete preview is required; no rename was applied."
|
||||
),
|
||||
Self::TooManyDisplayBytes { limit } => write!(
|
||||
f,
|
||||
"Rename preview exceeds {limit} display bytes (paths and both versions of changed/context lines). A complete preview is required; no rename was applied."
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for PreviewError {}
|
||||
|
||||
impl Preview {
|
||||
pub fn build(before: &SourceBundle, after: &SourceBundle) -> Result<Self, PreviewError> {
|
||||
Self::with_limits(before, after, LIMITS)
|
||||
}
|
||||
|
||||
fn with_limits(
|
||||
before: &SourceBundle,
|
||||
after: &SourceBundle,
|
||||
limits: Limits,
|
||||
) -> Result<Self, PreviewError> {
|
||||
if before.entry != after.entry
|
||||
|| !before.files.contains_key(&before.entry)
|
||||
|| !before.files.keys().eq(after.files.keys())
|
||||
{
|
||||
return Err(PreviewError::MembershipChanged);
|
||||
}
|
||||
let mut preview = Self {
|
||||
files: Vec::new(),
|
||||
changed_lines: 0,
|
||||
display_bytes: 0,
|
||||
};
|
||||
for ((path, old), (_, new)) in before.files.iter().zip(&after.files) {
|
||||
if old == new {
|
||||
continue;
|
||||
}
|
||||
if preview.files.len() == limits.files {
|
||||
return Err(PreviewError::TooManyFiles {
|
||||
limit: limits.files,
|
||||
});
|
||||
}
|
||||
// A changed source line can contain arbitrarily many renamed tokens.
|
||||
// Count the line once; this count is never a semantic-reference count.
|
||||
let mut changed = Vec::new();
|
||||
let mut old_lines = Lines::new(old);
|
||||
let mut new_lines = Lines::new(new);
|
||||
let mut count = 0;
|
||||
loop {
|
||||
match (old_lines.next(), new_lines.next()) {
|
||||
(None, None) => break,
|
||||
(Some(a), Some(b)) => {
|
||||
count += 1;
|
||||
if old[a.content_end..a.end] != new[b.content_end..b.end] {
|
||||
return Err(PreviewError::LineEndingChanged {
|
||||
path: path.clone(),
|
||||
line: count,
|
||||
});
|
||||
}
|
||||
if old[a.start..a.end] != new[b.start..b.end] {
|
||||
if preview.changed_lines == limits.changed_lines {
|
||||
return Err(PreviewError::TooManyChangedLines {
|
||||
limit: limits.changed_lines,
|
||||
});
|
||||
}
|
||||
preview.changed_lines += 1;
|
||||
changed.push(count - 1);
|
||||
}
|
||||
}
|
||||
_ => return Err(PreviewError::LineCountChanged(path.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
// Merge touching context intervals in order, without pairwise diff.
|
||||
// At most 7 indexed lines per changed line are retained; unchanged
|
||||
// lines outside these intervals are scanned but never indexed.
|
||||
let mut ranges: Vec<Range<usize>> = Vec::new();
|
||||
for line in changed {
|
||||
let range =
|
||||
line.saturating_sub(CONTEXT)..line.saturating_add(CONTEXT + 1).min(count);
|
||||
if let Some(previous) = ranges.last_mut()
|
||||
&& range.start <= previous.end
|
||||
{
|
||||
previous.end = previous.end.max(range.end);
|
||||
continue;
|
||||
}
|
||||
ranges.push(range);
|
||||
}
|
||||
preview.charge(path.len(), limits)?;
|
||||
let mut file = File {
|
||||
path: path.clone(),
|
||||
hunks: Vec::new(),
|
||||
};
|
||||
let mut pairs = Lines::new(old).zip(Lines::new(new)).enumerate();
|
||||
for range in ranges {
|
||||
let mut hunk = Hunk { lines: Vec::new() };
|
||||
for (ordinal, (a, b)) in pairs.by_ref() {
|
||||
if ordinal < range.start {
|
||||
continue;
|
||||
}
|
||||
// Charge exact UTF-8 bytes, including preserved endings and
|
||||
// both copies of shared context, before adding the index.
|
||||
// No render-time omission or shortening grants admission.
|
||||
preview.charge(a.end - a.start, limits)?;
|
||||
preview.charge(b.end - b.start, limits)?;
|
||||
hunk.lines.push(Line {
|
||||
number: ordinal + 1,
|
||||
changed: old[a.start..a.end] != new[b.start..b.end],
|
||||
before: a.start..a.end,
|
||||
after: b.start..b.end,
|
||||
});
|
||||
if ordinal + 1 == range.end {
|
||||
break;
|
||||
}
|
||||
}
|
||||
file.hunks.push(hunk);
|
||||
}
|
||||
preview.files.push(file);
|
||||
}
|
||||
Ok(preview)
|
||||
}
|
||||
|
||||
fn charge(&mut self, bytes: usize, limits: Limits) -> Result<(), PreviewError> {
|
||||
let Some(total) = self.display_bytes.checked_add(bytes) else {
|
||||
return Err(PreviewError::TooManyDisplayBytes {
|
||||
limit: limits.display_bytes,
|
||||
});
|
||||
};
|
||||
if total > limits.display_bytes {
|
||||
return Err(PreviewError::TooManyDisplayBytes {
|
||||
limit: limits.display_bytes,
|
||||
});
|
||||
}
|
||||
self.display_bytes = total;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// All admitted files in source path order; suitable for a file picker/search.
|
||||
pub fn files(&self) -> &[File] {
|
||||
&self.files
|
||||
}
|
||||
|
||||
/// Changed source lines, not occurrences, declarations or semantic uses.
|
||||
pub fn changed_line_count(&self) -> usize {
|
||||
self.changed_lines
|
||||
}
|
||||
|
||||
/// Exact displayed source bytes for both sides plus each stored path once.
|
||||
/// Index metadata is separately bounded by 7 lines per changed line.
|
||||
pub fn display_bytes(&self) -> usize {
|
||||
self.display_bytes
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.files.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl File {
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
/// Every hunk is present; the caller may page these indexes, never truncate.
|
||||
pub fn hunks(&self) -> &[Hunk] {
|
||||
&self.hunks
|
||||
}
|
||||
}
|
||||
|
||||
impl Hunk {
|
||||
/// The complete ordered line slice supports paging even a large single hunk.
|
||||
pub fn lines(&self) -> &[Line] {
|
||||
&self.lines
|
||||
}
|
||||
}
|
||||
|
||||
impl Line {
|
||||
/// One-based source line number, identical on both sides.
|
||||
pub fn number(&self) -> usize {
|
||||
self.number
|
||||
}
|
||||
|
||||
pub fn changed(&self) -> bool {
|
||||
self.changed
|
||||
}
|
||||
|
||||
/// Borrow from the immutable original file; includes its exact line ending.
|
||||
pub fn before_text<'a>(&self, original: &'a str) -> Option<&'a str> {
|
||||
original.get(self.before.clone())
|
||||
}
|
||||
|
||||
/// Borrow from the immutable candidate file; includes its exact line ending.
|
||||
pub fn after_text<'a>(&self, candidate: &'a str) -> Option<&'a str> {
|
||||
candidate.get(self.after.clone())
|
||||
}
|
||||
}
|
||||
|
||||
struct RawLine {
|
||||
start: usize,
|
||||
content_end: usize,
|
||||
end: usize,
|
||||
}
|
||||
|
||||
struct Lines<'a> {
|
||||
bytes: &'a [u8],
|
||||
offset: usize,
|
||||
}
|
||||
|
||||
impl<'a> Lines<'a> {
|
||||
fn new(text: &'a str) -> Self {
|
||||
Self {
|
||||
bytes: text.as_bytes(),
|
||||
offset: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Lines<'_> {
|
||||
type Item = RawLine;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.offset == self.bytes.len() {
|
||||
return None;
|
||||
}
|
||||
let start = self.offset;
|
||||
// The compiler and source navigation count LF, not bare CR, as a line
|
||||
// boundary. Preserve CRLF as an ending and bare CR as source content.
|
||||
while self.offset < self.bytes.len() && self.bytes[self.offset] != b'\n' {
|
||||
self.offset += 1;
|
||||
}
|
||||
let mut content_end = self.offset;
|
||||
if self.offset < self.bytes.len() {
|
||||
if self.offset > start && self.bytes[self.offset - 1] == b'\r' {
|
||||
content_end -= 1;
|
||||
}
|
||||
self.offset += 1;
|
||||
}
|
||||
Some(RawLine {
|
||||
start,
|
||||
content_end,
|
||||
end: self.offset,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn bundle(source: &str) -> SourceBundle {
|
||||
SourceBundle {
|
||||
entry: "救援.arcadia".into(),
|
||||
files: [("救援.arcadia".into(), source.into())].into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_unicode_lines_are_fully_reachable_as_bounded_fragments() {
|
||||
let text = format!("{}救援🙂{}\r\n", "a".repeat(251), "🙂".repeat(200));
|
||||
let pieces: Vec<_> = (0..segment_count(&text))
|
||||
.map(|i| text_segment(&text, i).unwrap())
|
||||
.collect();
|
||||
assert!(pieces.len() > 2);
|
||||
assert!(pieces.iter().all(|part| part.len() <= 255));
|
||||
assert_eq!(pieces.concat(), text);
|
||||
assert_eq!(text_segment(&text, segment_count(&text)), None);
|
||||
assert_eq!(text_segment(&text, usize::MAX), None);
|
||||
assert_eq!(segment_count(""), 1);
|
||||
assert_eq!(text_segment("", 0), Some(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_unicode_mixed_endings_and_counts_lines_not_repeated_uses() {
|
||||
let old = bundle("// 救援\r\n@a @a\ncontext\r@a");
|
||||
let new = bundle("// 救援\r\n@long @long\ncontext\r@long");
|
||||
let preview = Preview::build(&old, &new).unwrap();
|
||||
assert_eq!(preview.changed_line_count(), 2);
|
||||
let file = &preview.files()[0];
|
||||
assert_eq!(file.path(), "救援.arcadia");
|
||||
assert_eq!(file.hunks().len(), 1);
|
||||
let lines = file.hunks()[0].lines();
|
||||
assert_eq!(
|
||||
lines.iter().map(Line::number).collect::<Vec<_>>(),
|
||||
[1, 2, 3]
|
||||
);
|
||||
assert_eq!(
|
||||
lines[0].before_text(&old.files[&old.entry]),
|
||||
Some("// 救援\r\n")
|
||||
);
|
||||
assert_eq!(
|
||||
lines[1].after_text(&new.files[&new.entry]),
|
||||
Some("@long @long\n")
|
||||
);
|
||||
assert_eq!(
|
||||
lines[2].after_text(&new.files[&new.entry]),
|
||||
Some("context\r@long")
|
||||
);
|
||||
assert!(lines[1].changed() && lines[2].changed());
|
||||
assert!(!lines[0].changed());
|
||||
assert_eq!(
|
||||
preview.display_bytes(),
|
||||
old.entry.len() + old.files[&old.entry].len() + new.files[&new.entry].len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_and_last_lines_form_complete_separate_hunks_and_pages() {
|
||||
let old = bundle("a\n1\n2\n3\n4\n5\n6\n7\n8\n9\na");
|
||||
let new = bundle("long\n1\n2\n3\n4\n5\n6\n7\n8\n9\nlong");
|
||||
let preview = Preview::build(&old, &new).unwrap();
|
||||
let hunks = preview.files()[0].hunks();
|
||||
assert_eq!(hunks.len(), 2);
|
||||
let page_numbers: Vec<_> = hunks
|
||||
.iter()
|
||||
.flat_map(|h| h.lines().chunks(2))
|
||||
.flat_map(|page| page.iter().map(Line::number))
|
||||
.collect();
|
||||
assert_eq!(page_numbers, [1, 2, 3, 4, 8, 9, 10, 11]);
|
||||
assert_eq!(
|
||||
hunks[1].lines()[3].after_text(&new.files[&new.entry]),
|
||||
Some("long")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nearby_changes_merge_and_all_changed_files_are_reachable() {
|
||||
let mut old = bundle("a\n1\n2\n3\n4\n5\na\n");
|
||||
let mut new = bundle("b\n1\n2\n3\n4\n5\nb\n");
|
||||
old.files.insert("other.arcadia".into(), "a a a".into());
|
||||
new.files.insert("other.arcadia".into(), "b b b".into());
|
||||
let preview = Preview::build(&old, &new).unwrap();
|
||||
assert_eq!(preview.changed_line_count(), 3);
|
||||
assert_eq!(preview.files().len(), 2);
|
||||
assert_eq!(preview.files()[0].path(), "other.arcadia");
|
||||
assert_eq!(preview.files()[0].hunks()[0].lines().len(), 1);
|
||||
assert_eq!(preview.files()[1].hunks().len(), 1);
|
||||
assert_eq!(preview.files()[1].hunks()[0].lines().len(), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unchanged_and_empty_sources_have_no_preview() {
|
||||
for source in ["", "a\r\nb\n", "a"] {
|
||||
let original = bundle(source);
|
||||
let preview = Preview::build(&original, &original).unwrap();
|
||||
assert!(preview.is_empty());
|
||||
assert_eq!(preview.changed_line_count(), 0);
|
||||
assert_eq!(preview.display_bytes(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refuses_membership_entry_line_count_and_line_ending_changes() {
|
||||
let old = bundle("a\nb");
|
||||
let mut new = old.clone();
|
||||
new.files.insert("extra.arcadia".into(), "".into());
|
||||
assert_eq!(
|
||||
Preview::build(&old, &new).unwrap_err(),
|
||||
PreviewError::MembershipChanged
|
||||
);
|
||||
let mut new = old.clone();
|
||||
new.entry = "missing.arcadia".into();
|
||||
assert_eq!(
|
||||
Preview::build(&old, &new).unwrap_err(),
|
||||
PreviewError::MembershipChanged
|
||||
);
|
||||
assert!(matches!(
|
||||
Preview::build(&old, &bundle("a\nb\nc")),
|
||||
Err(PreviewError::LineEndingChanged { .. }) | Err(PreviewError::LineCountChanged(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
Preview::build(&bundle("a\nb\n"), &bundle("a\n")),
|
||||
Err(PreviewError::LineCountChanged(_))
|
||||
));
|
||||
for (a, b) in [("a\r\n", "a\n"), ("a\r", "a\n"), ("a", "a\n")] {
|
||||
assert_eq!(
|
||||
Preview::build(&bundle(a), &bundle(b)).unwrap_err(),
|
||||
PreviewError::LineEndingChanged {
|
||||
path: old.entry.clone(),
|
||||
line: 1
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_bound_is_exact_and_overflow_never_returns_a_partial_preview() {
|
||||
let old = bundle("a\ncontext\na");
|
||||
let new = bundle("bbb\ncontext\nbbb");
|
||||
let exact = old.entry.len() + old.files[&old.entry].len() + new.files[&new.entry].len();
|
||||
let limits = Limits {
|
||||
files: 1,
|
||||
changed_lines: 2,
|
||||
display_bytes: exact,
|
||||
};
|
||||
assert!(Preview::with_limits(&old, &new, limits).is_ok());
|
||||
assert_eq!(
|
||||
Preview::with_limits(&old, &new, Limits { files: 0, ..limits }).unwrap_err(),
|
||||
PreviewError::TooManyFiles { limit: 0 }
|
||||
);
|
||||
assert_eq!(
|
||||
Preview::with_limits(
|
||||
&old,
|
||||
&new,
|
||||
Limits {
|
||||
changed_lines: 1,
|
||||
..limits
|
||||
}
|
||||
)
|
||||
.unwrap_err(),
|
||||
PreviewError::TooManyChangedLines { limit: 1 }
|
||||
);
|
||||
assert_eq!(
|
||||
Preview::with_limits(
|
||||
&old,
|
||||
&new,
|
||||
Limits {
|
||||
display_bytes: exact - 1,
|
||||
..limits
|
||||
}
|
||||
)
|
||||
.unwrap_err(),
|
||||
PreviewError::TooManyDisplayBytes { limit: exact - 1 }
|
||||
);
|
||||
let long = bundle(&"a".repeat(LIMITS.display_bytes));
|
||||
assert!(matches!(
|
||||
Preview::build(&bundle("b"), &long),
|
||||
Err(PreviewError::TooManyDisplayBytes { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn limits_apply_to_the_whole_preview_not_each_file() {
|
||||
let mut old = bundle("a");
|
||||
let mut new = bundle("b");
|
||||
old.files.insert("second.arcadia".into(), "a".into());
|
||||
new.files.insert("second.arcadia".into(), "b".into());
|
||||
let limits = Limits {
|
||||
files: 2,
|
||||
changed_lines: 2,
|
||||
display_bytes: old.entry.len() + "second.arcadia".len() + 4,
|
||||
};
|
||||
let complete = Preview::with_limits(&old, &new, limits).unwrap();
|
||||
assert_eq!(complete.files().len(), 2);
|
||||
assert_eq!(complete.changed_line_count(), 2);
|
||||
assert_eq!(complete.display_bytes(), limits.display_bytes);
|
||||
assert!(matches!(
|
||||
Preview::with_limits(&old, &new, Limits { files: 1, ..limits }),
|
||||
Err(PreviewError::TooManyFiles { limit: 1 })
|
||||
));
|
||||
assert!(matches!(
|
||||
Preview::with_limits(
|
||||
&old,
|
||||
&new,
|
||||
Limits {
|
||||
changed_lines: 1,
|
||||
..limits
|
||||
}
|
||||
),
|
||||
Err(PreviewError::TooManyChangedLines { limit: 1 })
|
||||
));
|
||||
assert!(matches!(
|
||||
Preview::with_limits(
|
||||
&old,
|
||||
&new,
|
||||
Limits {
|
||||
display_bytes: limits.display_bytes - 1,
|
||||
..limits
|
||||
}
|
||||
),
|
||||
Err(PreviewError::TooManyDisplayBytes { .. })
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Per-file drafts and navigation. Accepted model/source history lives in Document.
|
||||
use iced::widget::text_editor::Cursor;
|
||||
use oa_core::{
|
||||
Id, language,
|
||||
language,
|
||||
source_project::{self, CompiledSourceProject, ProjectLimits, SourceBundle, SourceDiagnostic},
|
||||
};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
@@ -86,7 +86,6 @@ pub struct Buffer {
|
||||
pub cursor: Cursor,
|
||||
past: Vec<String>,
|
||||
future: Vec<String>,
|
||||
generation: u64,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
@@ -100,20 +99,14 @@ impl Buffer {
|
||||
},
|
||||
past: Vec::new(),
|
||||
future: Vec::new(),
|
||||
generation: 0,
|
||||
}
|
||||
}
|
||||
pub fn dirty(&self) -> bool {
|
||||
self.text != self.baseline
|
||||
}
|
||||
pub fn generation(&self) -> u64 {
|
||||
self.generation
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Workspace {
|
||||
session: Id,
|
||||
revision: u64,
|
||||
pub entry: String,
|
||||
pub active: String,
|
||||
pub files: BTreeMap<String, Buffer>,
|
||||
@@ -124,35 +117,6 @@ pub struct Workspace {
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
/// Ephemeral editor identity, never a model identity or recovery payload.
|
||||
pub fn session(&self) -> Id {
|
||||
self.session
|
||||
}
|
||||
pub fn revision(&self) -> u64 {
|
||||
self.revision
|
||||
}
|
||||
fn advance(&mut self) -> u64 {
|
||||
if let Some(revision) = self.revision.checked_add(1) {
|
||||
self.revision = revision;
|
||||
} else {
|
||||
self.reset_navigation_session();
|
||||
}
|
||||
self.revision
|
||||
}
|
||||
/// A changed filesystem root must not reuse queued navigation from Save As.
|
||||
pub fn reset_navigation_session(&mut self) {
|
||||
self.session = Id::new_v4();
|
||||
self.revision = 0;
|
||||
for buffer in self.files.values_mut() {
|
||||
buffer.generation = 0;
|
||||
}
|
||||
}
|
||||
pub fn set_reachable(&mut self, reachable: BTreeSet<String>) {
|
||||
if self.reachable != reachable {
|
||||
self.advance();
|
||||
self.reachable = reachable;
|
||||
}
|
||||
}
|
||||
pub fn recovery_snapshot(&self) -> RecoveryWorkspace {
|
||||
RecoveryWorkspace {
|
||||
entry: self.entry.clone(),
|
||||
@@ -172,8 +136,6 @@ impl Workspace {
|
||||
pub fn from_recovery(saved: RecoveryWorkspace) -> Result<Self, String> {
|
||||
saved.validate()?;
|
||||
Ok(Self {
|
||||
session: Id::new_v4(),
|
||||
revision: 0,
|
||||
entry: saved.entry,
|
||||
active: saved.active,
|
||||
files: saved
|
||||
@@ -192,8 +154,6 @@ impl Workspace {
|
||||
}
|
||||
pub fn new(bundle: &SourceBundle, applied: bool) -> Self {
|
||||
Self {
|
||||
session: Id::new_v4(),
|
||||
revision: 0,
|
||||
entry: bundle.entry.clone(),
|
||||
active: bundle.entry.clone(),
|
||||
files: bundle
|
||||
@@ -231,20 +191,11 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
pub fn edit(&mut self, text: String, cursor: Cursor) {
|
||||
let changed = self.files.get(&self.active).is_none_or(|b| b.text != text);
|
||||
let generation = if changed {
|
||||
self.advance()
|
||||
} else {
|
||||
self.revision
|
||||
};
|
||||
let buffer = self
|
||||
.files
|
||||
.entry(self.active.clone())
|
||||
.or_insert_with(|| Buffer::new(String::new()));
|
||||
buffer.cursor = cursor;
|
||||
if changed {
|
||||
buffer.generation = generation;
|
||||
}
|
||||
if text != buffer.text {
|
||||
buffer.past.push(std::mem::replace(&mut buffer.text, text));
|
||||
if buffer.past.len() > 100 {
|
||||
@@ -317,8 +268,6 @@ impl Workspace {
|
||||
} else {
|
||||
buffer.future.push(previous);
|
||||
}
|
||||
let generation = self.advance();
|
||||
self.files.get_mut(&self.active).unwrap().generation = generation;
|
||||
self.trim_history(DRAFT_HISTORY_BYTES);
|
||||
true
|
||||
}
|
||||
@@ -332,7 +281,6 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
pub fn accept(&mut self, bundle: &SourceBundle) {
|
||||
let generation = self.advance();
|
||||
if self.entry != bundle.entry
|
||||
&& !bundle.files.contains_key(&self.entry)
|
||||
&& self.files.get(&self.entry).is_some_and(|b| !b.dirty())
|
||||
@@ -343,13 +291,7 @@ impl Workspace {
|
||||
self.reachable = bundle.files.keys().cloned().collect();
|
||||
for (path, text) in &bundle.files {
|
||||
let cursor = self.files.get(path).map(|b| b.cursor);
|
||||
let previous_generation = self
|
||||
.files
|
||||
.get(path)
|
||||
.filter(|b| b.text == *text)
|
||||
.map(|b| b.generation);
|
||||
let mut buffer = Buffer::new(text.clone());
|
||||
buffer.generation = previous_generation.unwrap_or(generation);
|
||||
if let Some(cursor) = cursor {
|
||||
buffer.cursor = cursor;
|
||||
}
|
||||
@@ -361,22 +303,10 @@ impl Workspace {
|
||||
self.applied = true;
|
||||
}
|
||||
pub fn discovered(&mut self, bundle: &SourceBundle, reachable: BTreeSet<String>) {
|
||||
let changed = self.reachable != reachable
|
||||
|| bundle
|
||||
.files
|
||||
.keys()
|
||||
.any(|path| !self.files.contains_key(path));
|
||||
let generation = if changed {
|
||||
self.advance()
|
||||
} else {
|
||||
self.revision
|
||||
};
|
||||
for (path, text) in &bundle.files {
|
||||
self.files.entry(path.clone()).or_insert_with(|| {
|
||||
let mut buffer = Buffer::new(text.clone());
|
||||
buffer.generation = generation;
|
||||
buffer
|
||||
});
|
||||
self.files
|
||||
.entry(path.clone())
|
||||
.or_insert_with(|| Buffer::new(text.clone()));
|
||||
}
|
||||
self.reachable = reachable;
|
||||
}
|
||||
@@ -384,7 +314,6 @@ impl Workspace {
|
||||
if self.reachable.contains(path) || self.files.get(path).is_none_or(Buffer::dirty) {
|
||||
return false;
|
||||
}
|
||||
self.advance();
|
||||
self.files.remove(path);
|
||||
if self.active == path {
|
||||
self.active = self.entry.clone();
|
||||
@@ -447,148 +376,6 @@ pub fn lint(bundle: &SourceBundle) -> Vec<SourceDiagnostic> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use oa_core::Project;
|
||||
|
||||
#[test]
|
||||
fn fresh_session_cannot_reuse_a_retained_buffer_generation() {
|
||||
let bundle = SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("main.arcadia".into(), "first".into()),
|
||||
("other.arcadia".into(), "other".into()),
|
||||
]),
|
||||
};
|
||||
let mut workspace = Workspace::new(&bundle, true);
|
||||
let cursor = workspace.files["main.arcadia"].cursor;
|
||||
workspace.edit("second".into(), cursor);
|
||||
workspace.reset_navigation_session();
|
||||
let before = workspace.files["main.arcadia"].generation();
|
||||
workspace.edit("third".into(), cursor);
|
||||
assert_ne!(workspace.files["main.arcadia"].generation(), before);
|
||||
workspace.switch("other.arcadia", cursor);
|
||||
workspace.revision = u64::MAX;
|
||||
workspace.edit("changed other".into(), cursor);
|
||||
let after_wrap = workspace.files["main.arcadia"].generation();
|
||||
workspace.switch("main.arcadia", cursor);
|
||||
workspace.edit("fourth".into(), cursor);
|
||||
assert_ne!(workspace.files["main.arcadia"].generation(), after_wrap);
|
||||
assert!(workspace.undo(false));
|
||||
assert_eq!(workspace.text(), "third");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn navigation_versions_distinguish_reopened_recovered_and_changed_drafts() {
|
||||
let bundle = SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("project.arcadia".into(), "entry\r\n".into()),
|
||||
("part.arcadia".into(), "雨 draft\r\n".into()),
|
||||
]),
|
||||
};
|
||||
let mut workspace = Workspace::new(&bundle, true);
|
||||
let reopened = Workspace::new(&bundle, true);
|
||||
let recovered = Workspace::from_recovery(workspace.recovery_snapshot()).unwrap();
|
||||
assert_ne!(workspace.session(), reopened.session());
|
||||
assert_ne!(workspace.session(), recovered.session());
|
||||
assert_eq!(workspace.bundle(), recovered.bundle());
|
||||
let saved = serde_json::to_value(workspace.recovery_snapshot()).unwrap();
|
||||
assert!(saved.get("session").is_none() && saved.get("revision").is_none());
|
||||
|
||||
let cursor = Cursor {
|
||||
position: iced::widget::text_editor::Position { line: 0, column: 3 },
|
||||
selection: None,
|
||||
};
|
||||
let revision = workspace.revision();
|
||||
workspace.edit(workspace.text().into(), cursor);
|
||||
assert_eq!(
|
||||
workspace.revision(),
|
||||
revision,
|
||||
"selection is not a text change"
|
||||
);
|
||||
workspace.switch("part.arcadia", cursor);
|
||||
assert_eq!(
|
||||
workspace.revision(),
|
||||
revision,
|
||||
"switching retains the snapshot"
|
||||
);
|
||||
let untouched = workspace.files["project.arcadia"].generation();
|
||||
workspace.edit("雨 unfinished !\r\n".into(), cursor);
|
||||
let edited = workspace.files["part.arcadia"].generation();
|
||||
assert!(workspace.revision() > revision);
|
||||
assert_eq!(workspace.files["project.arcadia"].generation(), untouched);
|
||||
assert!(workspace.undo(false));
|
||||
assert_eq!(workspace.bundle(), bundle);
|
||||
assert!(workspace.files["part.arcadia"].generation() > edited);
|
||||
assert!(workspace.undo(true));
|
||||
assert_eq!(workspace.text(), "雨 unfinished !\r\n");
|
||||
let previous_session = workspace.session();
|
||||
let previous_text = workspace.bundle();
|
||||
workspace.reset_navigation_session();
|
||||
assert_ne!(workspace.session(), previous_session);
|
||||
assert_eq!(workspace.bundle(), previous_text);
|
||||
assert!(
|
||||
workspace.undo(false),
|
||||
"session reset preserves text history"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn navigation_scope_changes_invalidate_without_changing_unchanged_positions() {
|
||||
let bundle = SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("project.arcadia".into(), "entry".into()),
|
||||
("part.arcadia".into(), "part".into()),
|
||||
]),
|
||||
};
|
||||
let mut workspace = Workspace::new(&bundle, true);
|
||||
let entry_generation = workspace.files[&bundle.entry].generation();
|
||||
let initial_revision = workspace.revision();
|
||||
workspace.set_reachable(BTreeSet::from([bundle.entry.clone()]));
|
||||
assert!(workspace.revision() > initial_revision);
|
||||
let membership_revision = workspace.revision();
|
||||
workspace.set_reachable(workspace.reachable.clone());
|
||||
assert_eq!(workspace.revision(), membership_revision);
|
||||
assert!(workspace.discard_detached("part.arcadia"));
|
||||
assert!(workspace.revision() > membership_revision);
|
||||
let closed_revision = workspace.revision();
|
||||
workspace.discovered(&bundle, BTreeSet::from([bundle.entry.clone()]));
|
||||
assert!(workspace.revision() > closed_revision);
|
||||
assert_eq!(
|
||||
workspace.files[&bundle.entry].generation(),
|
||||
entry_generation
|
||||
);
|
||||
assert!(workspace.files["part.arcadia"].generation() > closed_revision);
|
||||
let discovered_revision = workspace.revision();
|
||||
workspace.discovered(&bundle, workspace.reachable.clone());
|
||||
assert_eq!(workspace.revision(), discovered_revision);
|
||||
workspace.accept(&bundle);
|
||||
assert!(
|
||||
workspace.revision() > discovered_revision,
|
||||
"validation evidence changed"
|
||||
);
|
||||
assert_eq!(
|
||||
workspace.files[&bundle.entry].generation(),
|
||||
entry_generation
|
||||
);
|
||||
assert_eq!(workspace.bundle(), bundle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn navigation_revision_wrap_cannot_reuse_an_old_workspace_key() {
|
||||
let bundle = SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([("project.arcadia".into(), "original".into())]),
|
||||
};
|
||||
let mut workspace = Workspace::new(&bundle, true);
|
||||
workspace.revision = u64::MAX;
|
||||
let before = workspace.session();
|
||||
let cursor = workspace.files[&bundle.entry].cursor;
|
||||
workspace.edit("changed".into(), cursor);
|
||||
assert_ne!(workspace.session(), before);
|
||||
assert_eq!(workspace.revision(), 0);
|
||||
assert!(workspace.undo(false));
|
||||
assert_eq!(workspace.bundle(), bundle);
|
||||
}
|
||||
#[test]
|
||||
fn draft_history_budget_covers_inactive_files_without_discarding_drafts() {
|
||||
let bundle = SourceBundle {
|
||||
|
||||
@@ -377,8 +377,7 @@ fn source_navigation_waits_for_editor_layout_and_cancels_on_edit() {
|
||||
send(&mut app, Message::SourceForSelection);
|
||||
assert_eq!(app.page, Page::Text);
|
||||
assert!(app.source_jump.is_some());
|
||||
let ticket = app.source_reveal_ticket();
|
||||
send(&mut app, Message::SourceViewport(ticket));
|
||||
send(&mut app, Message::SourceViewport);
|
||||
assert!(app.source_jump.is_none());
|
||||
send(&mut app, Message::SourceForSelection);
|
||||
assert!(app.source_jump.is_some());
|
||||
@@ -410,8 +409,7 @@ fn source_find_navigates_an_invalid_draft_without_applying_or_losing_it() {
|
||||
cursor.selection,
|
||||
Some(text_editor::Position { line: 0, column: 9 })
|
||||
);
|
||||
let ticket = app.source_reveal_ticket();
|
||||
send(&mut app, Message::SourceViewport(ticket));
|
||||
send(&mut app, Message::SourceViewport);
|
||||
assert_eq!(app.source_editor.cursor(), cursor);
|
||||
send(
|
||||
&mut app,
|
||||
@@ -1665,3 +1663,388 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
mod source_symbols;
|
||||
pub(crate) mod traceability;
|
||||
use crate::{
|
||||
app::{App, Choice, FileOperation, Message, Page, Pending},
|
||||
@@ -350,13 +349,6 @@ pub fn view(app: &App) -> View<'_, Message> {
|
||||
if app.pending.is_some() {
|
||||
return modal(root,column![text("Keep your changes?").size(21),text("Save the model and pending edits before continuing. Invalid drafts stay available for correction.").size(13),row![action("Cancel",Message::Cancel),action("Discard changes",Message::Discard),button("Save & continue").padding(10).on_press(Message::SaveAndContinue)].spacing(8)].spacing(16).into());
|
||||
}
|
||||
if app.source_rename.open() {
|
||||
return modal_width(
|
||||
root,
|
||||
crate::app::source_rename::dialog(app),
|
||||
(app.window_size.width - 32.).clamp(120., 760.),
|
||||
);
|
||||
}
|
||||
root
|
||||
}
|
||||
fn modal<'a>(base: View<'a, Message>, content: View<'a, Message>) -> View<'a, Message> {
|
||||
@@ -1011,13 +1003,6 @@ fn source_files(app: &App) -> View<'_, Message> {
|
||||
|
||||
fn source(app: &App) -> View<'_, Message> {
|
||||
use crate::source_navigation::Event as SourceEvent;
|
||||
let (undo_label, undo_message) = match app.source_rename_undo_message() {
|
||||
Some(message) => ("Undo rename", Some(message)),
|
||||
None => (
|
||||
"Undo model",
|
||||
(app.document.can_undo() && !app.source_dirty()).then_some(Message::Undo),
|
||||
),
|
||||
};
|
||||
let navigation: View<'_, Message> = if app.source_navigation.open {
|
||||
column![
|
||||
row![
|
||||
@@ -1034,7 +1019,7 @@ fn source(app: &App) -> View<'_, Message> {
|
||||
),
|
||||
action("Next", Message::SourceNavigation(SourceEvent::Next(false))),
|
||||
label(app.source_navigation.label()),
|
||||
action("Close", app.source_find_close_message()),
|
||||
action("Close", Message::SourceNavigation(SourceEvent::Close)),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center),
|
||||
@@ -1128,13 +1113,8 @@ fn source(app: &App) -> View<'_, Message> {
|
||||
}
|
||||
}
|
||||
let editor = widget::text_editor(&app.source_editor)
|
||||
.id(app.source_editor_id())
|
||||
.id("source-editor")
|
||||
.on_action(Message::SourceEdit)
|
||||
.key_binding({
|
||||
let context = app.source_symbol_context();
|
||||
let rename = app.source_rename_begin();
|
||||
move |press| source_binding(press, &context, Some(&rename))
|
||||
})
|
||||
.font(Font::MONOSPACE)
|
||||
.size(14)
|
||||
.padding(16)
|
||||
@@ -1143,7 +1123,8 @@ fn source(app: &App) -> View<'_, Message> {
|
||||
app.source_snapshot.clone(),
|
||||
crate::highlight::format,
|
||||
);
|
||||
let header = container(
|
||||
column![
|
||||
container(
|
||||
column![
|
||||
row![
|
||||
container(heading(
|
||||
@@ -1161,8 +1142,8 @@ fn source(app: &App) -> View<'_, Message> {
|
||||
row![
|
||||
action("Undo text", Message::SourceUndo),
|
||||
action("Redo text", Message::SourceRedo),
|
||||
button(text(undo_label).size(12)).padding([8,10]).style(style::nav)
|
||||
.on_press_maybe(undo_message),
|
||||
button(text("Undo model").size(12)).padding([8,10]).style(style::nav)
|
||||
.on_press_maybe((app.document.can_undo() && !app.source_dirty()).then_some(Message::Undo)),
|
||||
button(text("Redo model").size(12)).padding([8,10]).style(style::nav)
|
||||
.on_press_maybe((app.document.can_redo() && !app.source_dirty()).then_some(Message::Redo)),
|
||||
action("Find / go to…", Message::SourceNavigation(SourceEvent::Open(false))),
|
||||
@@ -1186,511 +1167,17 @@ fn source(app: &App) -> View<'_, Message> {
|
||||
"Undo text affects this file's draft. Model Undo restores accepted files together (100 changes / about 32 MiB)."
|
||||
}),
|
||||
navigation,
|
||||
row![
|
||||
action("Definition · F12", app.source_symbol_message(crate::source_navigation::SymbolEvent::Query(crate::source_navigation::query::Action::Definition))),
|
||||
action("References · Shift F12", app.source_symbol_message(crate::source_navigation::SymbolEvent::Query(crate::source_navigation::query::Action::References))),
|
||||
action("Rename alias · F2", app.source_rename_message()),
|
||||
button(text("Back · Alt ←").size(12)).padding([8, 10]).style(style::nav)
|
||||
.on_press_maybe((app.source_symbols.back.iter().any(|entry| entry.session == app.source_workspace.session())).then(|| app.source_symbol_message(crate::source_navigation::SymbolEvent::Back))),
|
||||
].spacing(7),
|
||||
scrollable(diagnostics)
|
||||
.height((visible_diagnostics as f32 * 32.).min(132.))
|
||||
]
|
||||
.spacing(12)
|
||||
)
|
||||
.padding(18);
|
||||
let editor = widget::sensor(editor)
|
||||
.key((
|
||||
app.source_workspace.active.clone(),
|
||||
app.source_reveal_ticket(),
|
||||
))
|
||||
.on_show({
|
||||
let ticket = app.source_reveal_ticket();
|
||||
move |_| Message::SourceViewport(ticket)
|
||||
})
|
||||
.on_resize({
|
||||
let ticket = app.source_reveal_ticket();
|
||||
move |_| Message::SourceViewport(ticket)
|
||||
});
|
||||
let workspace: View<'_, Message> = if app.source_symbols.open {
|
||||
row![
|
||||
container(editor).width(Fill).height(Fill),
|
||||
container(scrollable(source_symbols::panel(app)))
|
||||
.width(300)
|
||||
.height(Fill),
|
||||
]
|
||||
.height(Fill)
|
||||
.into()
|
||||
} else {
|
||||
container(editor).width(Fill).height(Fill).into()
|
||||
};
|
||||
source_layout(header.into(), workspace, app.window_size.height)
|
||||
}
|
||||
|
||||
fn source_layout<'a, Renderer: iced::advanced::text::Renderer + 'a>(
|
||||
header: View<'a, Message, iced::Theme, Renderer>,
|
||||
workspace: View<'a, Message, iced::Theme, Renderer>,
|
||||
window_height: f32,
|
||||
) -> View<'a, Message, iced::Theme, Renderer> {
|
||||
// Lint clears and repopulates diagnostics asynchronously. Reserve the same
|
||||
// outer slot so a pointer aimed at the editor cannot land in the header.
|
||||
column![
|
||||
container(scrollable(header).id("source-header")).height((window_height * 0.42).max(180.)),
|
||||
workspace,
|
||||
.padding(18),
|
||||
widget::sensor(editor).key((app.source_workspace.active.clone(),app.source_jump))
|
||||
.on_show(|_| Message::SourceViewport).on_resize(|_| Message::SourceViewport)
|
||||
]
|
||||
.height(Fill)
|
||||
.into()
|
||||
}
|
||||
pub(crate) fn source_binding(
|
||||
press: widget::text_editor::KeyPress,
|
||||
context: &crate::source_navigation::SymbolContext,
|
||||
rename: Option<&crate::app::source_rename::Begin>,
|
||||
) -> Option<widget::text_editor::Binding<Message>> {
|
||||
use crate::source_navigation::{SymbolEvent, query::Action};
|
||||
use iced::keyboard::{Key, key::Named};
|
||||
use widget::text_editor::{Binding, Status};
|
||||
if matches!(press.status, Status::Focused { .. })
|
||||
&& press.modifiers.is_empty()
|
||||
&& press.key == Key::Named(Named::F2)
|
||||
&& let Some(begin) = rename
|
||||
{
|
||||
return Some(Binding::Custom(Message::SourceRename(
|
||||
crate::app::source_rename::Event::Begin(begin.clone()),
|
||||
)));
|
||||
}
|
||||
// Windows owns Alt+Space for its window menu. Iced's default binding would
|
||||
// otherwise insert a supplied space payload, even after that menu closes.
|
||||
// Keep Ctrl+Alt (AltGr), shifted input, and ordinary Space unchanged.
|
||||
#[cfg(target_os = "windows")]
|
||||
if matches!(press.status, Status::Focused { .. })
|
||||
&& press.modifiers == iced::keyboard::Modifiers::ALT
|
||||
&& matches!(
|
||||
press.key.as_ref(),
|
||||
Key::Named(Named::Space) | Key::Character(" ")
|
||||
)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if matches!(press.status, Status::Focused { .. })
|
||||
&& !press.modifiers.control()
|
||||
&& !press.modifiers.logo()
|
||||
{
|
||||
let event = match &press.key {
|
||||
Key::Named(Named::F12) if !press.modifiers.alt() => {
|
||||
Some(SymbolEvent::Query(if press.modifiers.shift() {
|
||||
Action::References
|
||||
} else {
|
||||
Action::Definition
|
||||
}))
|
||||
}
|
||||
Key::Named(Named::ArrowLeft) if press.modifiers.alt() && !press.modifiers.shift() => {
|
||||
Some(SymbolEvent::Back)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(event) = event {
|
||||
return Some(Binding::Custom(Message::SourceSymbols(
|
||||
context.clone(),
|
||||
event,
|
||||
)));
|
||||
}
|
||||
}
|
||||
Binding::from_key_press(press)
|
||||
}
|
||||
|
||||
#[cfg(all(test, debug_assertions))]
|
||||
pub(crate) mod source_tests {
|
||||
use super::*;
|
||||
use iced::advanced::{
|
||||
Layout, Shell, graphics, layout, renderer, text as shaped, widget as core_widget,
|
||||
};
|
||||
use iced::widget::column;
|
||||
use iced::{Background, Color, Pixels, Point, Rectangle, Size, Transformation, Vector};
|
||||
|
||||
// Measure real Iced paragraphs/editor text without constructing a window or GPU.
|
||||
pub(crate) 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 = graphics::text::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 {
|
||||
header: Option<(Rectangle, Rectangle, Vector)>,
|
||||
editor: Option<Rectangle>,
|
||||
}
|
||||
impl core_widget::Operation for Geometry {
|
||||
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn core_widget::Operation)) {
|
||||
visit(self);
|
||||
}
|
||||
fn container(&mut self, id: Option<&core_widget::Id>, bounds: Rectangle) {
|
||||
if id == Some(&core_widget::Id::new("test-source-editor")) {
|
||||
self.editor = Some(bounds);
|
||||
}
|
||||
}
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
id: Option<&core_widget::Id>,
|
||||
bounds: Rectangle,
|
||||
content: Rectangle,
|
||||
offset: Vector,
|
||||
_: &mut dyn core_widget::operation::Scrollable,
|
||||
) {
|
||||
if id == Some(&core_widget::Id::new("source-header")) {
|
||||
self.header = Some((bounds, content, offset));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mounted_source<'a>(
|
||||
content: &'a widget::text_editor::Content<FontLayout>,
|
||||
diagnostics: usize,
|
||||
find: bool,
|
||||
window_height: f32,
|
||||
) -> View<'a, Message, iced::Theme, FontLayout> {
|
||||
let mut notices = column![].spacing(5);
|
||||
for index in 0..diagnostics {
|
||||
notices = notices.push(button(text(format!(
|
||||
"救援/assess.arcadia:{}:5 · Warning: verify this participant's engineering allocation", index + 1,
|
||||
)).size(12)).padding([4, 6]).width(Fill));
|
||||
}
|
||||
let find: View<'_, Message, iced::Theme, FontLayout> = if find {
|
||||
column![
|
||||
text_input("Find literal text", "assess").on_input(|s| Message::SourceNavigation(
|
||||
crate::source_navigation::Event::Query(s)
|
||||
)),
|
||||
text_input("Line", "1").on_input(|s| Message::SourceNavigation(
|
||||
crate::source_navigation::Event::Line(s)
|
||||
)),
|
||||
]
|
||||
.spacing(8)
|
||||
.into()
|
||||
} else {
|
||||
Space::new().into()
|
||||
};
|
||||
let header = container(
|
||||
column![
|
||||
text("Textual model").size(24),
|
||||
text("Edit the current file. Apply text validates the complete included model.")
|
||||
.size(11),
|
||||
row![
|
||||
button(text("Apply text")),
|
||||
button(text("Undo text")),
|
||||
button(text("Model Undo"))
|
||||
]
|
||||
.spacing(8),
|
||||
text("救援/assess.arcadia · All files applied to the accepted model").size(11),
|
||||
text("Undo text affects this draft. Model Undo restores accepted files together.")
|
||||
.size(11),
|
||||
find,
|
||||
row![
|
||||
button(text("Definition · F12")),
|
||||
button(text("References · Shift F12")),
|
||||
button(text("Back"))
|
||||
]
|
||||
.spacing(7),
|
||||
scrollable(notices).height((diagnostics as f32 * 32.).min(132.)),
|
||||
]
|
||||
.spacing(12),
|
||||
)
|
||||
.padding(18);
|
||||
let editor = container(
|
||||
widget::text_editor(content)
|
||||
.on_action(Message::SourceEdit)
|
||||
.font(Font::MONOSPACE)
|
||||
.size(14)
|
||||
.padding(16)
|
||||
.height(Fill),
|
||||
)
|
||||
.id("test-source-editor")
|
||||
.height(Fill)
|
||||
.width(Fill);
|
||||
source_layout(header.into(), editor.into(), window_height)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_header_keeps_editor_bounds_stable_through_lint_and_find_rebuilds() {
|
||||
for (host, window_height) in [
|
||||
(Size::new(720., 540.), 700.),
|
||||
(Size::new(1180., 890.), 1080.),
|
||||
] {
|
||||
let content = widget::text_editor::Content::<FontLayout>::with_text(
|
||||
"// is excluded\r\n// 雨\r\n",
|
||||
);
|
||||
let mut tree = core_widget::Tree::empty();
|
||||
let mut baseline = None;
|
||||
for (diagnostics, find) in [
|
||||
(0, false),
|
||||
(6, false),
|
||||
(0, false),
|
||||
(6, true),
|
||||
(1, true),
|
||||
(0, false),
|
||||
] {
|
||||
let mut ui = mounted_source(&content, diagnostics, find, window_height);
|
||||
tree.diff(ui.as_widget());
|
||||
let node = ui.as_widget_mut().layout(
|
||||
&mut tree,
|
||||
&FontLayout,
|
||||
&layout::Limits::new(Size::ZERO, host),
|
||||
);
|
||||
let mut geometry = Geometry::default();
|
||||
ui.as_widget_mut().operate(
|
||||
&mut tree,
|
||||
Layout::new(&node),
|
||||
&FontLayout,
|
||||
&mut geometry,
|
||||
);
|
||||
let editor = geometry.editor.unwrap();
|
||||
let (header, _, _) = geometry.header.unwrap();
|
||||
let slot = node.children()[0].bounds();
|
||||
assert_eq!(slot.height, (window_height * 0.42).max(180.));
|
||||
assert!(
|
||||
header.height <= slot.height,
|
||||
"short content may shrink inside the fixed slot"
|
||||
);
|
||||
assert!(
|
||||
editor.height >= 240.,
|
||||
"remaining editor must stay usable: {editor:?}"
|
||||
);
|
||||
assert_eq!(editor.y, slot.y + slot.height);
|
||||
if let Some(expected) = baseline {
|
||||
assert_eq!(editor, expected, "diagnostics={diagnostics}, find={find}");
|
||||
} else {
|
||||
baseline = Some(editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_header_remains_scrollable_and_preserves_offset_across_diagnostic_refresh() {
|
||||
let content = widget::text_editor::Content::<FontLayout>::with_text("// 雨\r\n");
|
||||
let host = Size::new(720., 540.);
|
||||
let mut tree = core_widget::Tree::empty();
|
||||
let mut saved = None;
|
||||
for diagnostics in [6, 7] {
|
||||
let mut ui = mounted_source(&content, diagnostics, true, 700.);
|
||||
tree.diff(ui.as_widget());
|
||||
let node = ui.as_widget_mut().layout(
|
||||
&mut tree,
|
||||
&FontLayout,
|
||||
&layout::Limits::new(Size::ZERO, host),
|
||||
);
|
||||
let mut geometry = Geometry::default();
|
||||
ui.as_widget_mut()
|
||||
.operate(&mut tree, Layout::new(&node), &FontLayout, &mut geometry);
|
||||
let (bounds, contents, _) = geometry.header.unwrap();
|
||||
assert!(
|
||||
contents.height > bounds.height,
|
||||
"fixture must actually overflow"
|
||||
);
|
||||
if saved.is_none() {
|
||||
let mut messages = Vec::new();
|
||||
let mut shell = Shell::new(&mut messages);
|
||||
ui.as_widget_mut().update(
|
||||
&mut tree,
|
||||
&iced::Event::Mouse(iced::mouse::Event::WheelScrolled {
|
||||
delta: iced::mouse::ScrollDelta::Pixels { x: 0., y: -45. },
|
||||
}),
|
||||
Layout::new(&node),
|
||||
iced::mouse::Cursor::Available(Point::new(bounds.x + 8., bounds.y + 8.)),
|
||||
&FontLayout,
|
||||
&mut iced::advanced::clipboard::Null,
|
||||
&mut shell,
|
||||
&Rectangle::with_size(host),
|
||||
);
|
||||
assert!(shell.is_event_captured());
|
||||
}
|
||||
let mut geometry = Geometry::default();
|
||||
ui.as_widget_mut()
|
||||
.operate(&mut tree, Layout::new(&node), &FontLayout, &mut geometry);
|
||||
let offset = geometry.header.unwrap().2;
|
||||
assert!(offset.y > 0., "the actual mounted Scrollable must move");
|
||||
if let Some(before) = saved {
|
||||
assert_eq!(offset, before);
|
||||
} else {
|
||||
saved = Some(offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn source_editor_windows_menu_payload_is_inert_but_space_and_altgr_still_edit() {
|
||||
use core_widget::operation::Focusable;
|
||||
use iced::keyboard::{
|
||||
self, Key, Modifiers,
|
||||
key::{Code, Named, Physical},
|
||||
};
|
||||
use widget::text_editor::{Binding, Content, KeyPress, State, Status};
|
||||
let baseline = "// is excluded\r\n// 雨\r\n";
|
||||
let context = crate::source_navigation::SymbolContext {
|
||||
session: Id::new_v4(),
|
||||
path: "救援/assess.arcadia".into(),
|
||||
};
|
||||
for (key, physical_key, modifiers, payload, inserted) in [
|
||||
(
|
||||
Key::Named(Named::Space),
|
||||
Physical::Code(Code::Space),
|
||||
Modifiers::ALT,
|
||||
" ",
|
||||
false,
|
||||
),
|
||||
(
|
||||
Key::Character(" ".into()),
|
||||
Physical::Code(Code::Space),
|
||||
Modifiers::ALT,
|
||||
" ",
|
||||
false,
|
||||
),
|
||||
(
|
||||
Key::Named(Named::Space),
|
||||
Physical::Code(Code::Space),
|
||||
Modifiers::empty(),
|
||||
" ",
|
||||
true,
|
||||
),
|
||||
(
|
||||
Key::Named(Named::Space),
|
||||
Physical::Code(Code::Space),
|
||||
Modifiers::CTRL | Modifiers::ALT,
|
||||
" ",
|
||||
true,
|
||||
),
|
||||
(
|
||||
Key::Character("e".into()),
|
||||
Physical::Code(Code::KeyE),
|
||||
Modifiers::CTRL | Modifiers::ALT,
|
||||
"€",
|
||||
true,
|
||||
),
|
||||
(
|
||||
Key::Named(Named::Space),
|
||||
Physical::Code(Code::Space),
|
||||
Modifiers::ALT | Modifiers::SHIFT,
|
||||
" ",
|
||||
true,
|
||||
),
|
||||
] {
|
||||
let mut content = Content::<FontLayout>::with_text(baseline);
|
||||
content.move_to(widget::text_editor::Cursor {
|
||||
position: widget::text_editor::Position { line: 0, column: 5 },
|
||||
selection: None,
|
||||
});
|
||||
let press = KeyPress {
|
||||
key: key.clone(),
|
||||
modified_key: key.clone(),
|
||||
physical_key,
|
||||
modifiers,
|
||||
text: Some(payload.into()),
|
||||
status: Status::Focused { is_hovered: false },
|
||||
};
|
||||
assert!(
|
||||
matches!(
|
||||
Binding::<Message>::from_key_press(press),
|
||||
Some(Binding::Insert(_))
|
||||
),
|
||||
"the supplied event must expose the default insertion path"
|
||||
);
|
||||
let mut messages = Vec::new();
|
||||
{
|
||||
let mut editor: View<'_, Message, iced::Theme, FontLayout> =
|
||||
widget::text_editor(&content)
|
||||
.on_action(Message::SourceEdit)
|
||||
.font(Font::MONOSPACE)
|
||||
.size(14)
|
||||
.padding(16)
|
||||
.height(Fill)
|
||||
.key_binding(|press| source_binding(press, &context, None))
|
||||
.into();
|
||||
let mut tree = core_widget::Tree::new(editor.as_widget());
|
||||
let size = Size::new(600., 240.);
|
||||
let node = editor.as_widget_mut().layout(
|
||||
&mut tree,
|
||||
&FontLayout,
|
||||
&layout::Limits::new(Size::ZERO, size),
|
||||
);
|
||||
tree.state
|
||||
.downcast_mut::<State<shaped::highlighter::PlainText>>()
|
||||
.focus();
|
||||
let event = iced::Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key: key.clone(),
|
||||
modified_key: key,
|
||||
physical_key,
|
||||
location: keyboard::Location::Standard,
|
||||
modifiers,
|
||||
text: Some(payload.into()),
|
||||
repeat: false,
|
||||
});
|
||||
let mut shell = Shell::new(&mut messages);
|
||||
editor.as_widget_mut().update(
|
||||
&mut tree,
|
||||
&event,
|
||||
Layout::new(&node),
|
||||
iced::mouse::Cursor::Unavailable,
|
||||
&FontLayout,
|
||||
&mut iced::advanced::clipboard::Null,
|
||||
&mut shell,
|
||||
&Rectangle::with_size(size),
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
messages.len(),
|
||||
usize::from(inserted),
|
||||
"modifiers={modifiers:?}"
|
||||
);
|
||||
for message in messages {
|
||||
let Message::SourceEdit(action) = message else {
|
||||
panic!("unexpected navigation action");
|
||||
};
|
||||
content.perform(action);
|
||||
}
|
||||
let expected = if inserted {
|
||||
format!("// is{payload} excluded\r\n// 雨\r\n")
|
||||
} else {
|
||||
baseline.into()
|
||||
};
|
||||
assert_eq!(content.text(), expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn checks(app: &App) -> View<'_, Message> {
|
||||
let issues = &app.model_issues;
|
||||
let mut out = column![heading(
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
use crate::{
|
||||
app::{App, Message},
|
||||
source_navigation::{
|
||||
SymbolEvent,
|
||||
query::{self, Declaration, Disposition, Evidence, Identity, Membership, Position},
|
||||
},
|
||||
style,
|
||||
};
|
||||
use iced::{
|
||||
Element, Fill,
|
||||
widget::{Space, button, column, container, row, scrollable, text, tooltip},
|
||||
};
|
||||
|
||||
pub(super) fn panel(app: &App) -> Element<'_, Message> {
|
||||
let state = &app.source_symbols;
|
||||
if !state.open
|
||||
|| (state.results.is_none() && state.in_flight.is_none() && state.notice.is_none())
|
||||
{
|
||||
return column![].into();
|
||||
}
|
||||
let title = state.results.as_ref().map_or_else(
|
||||
|| "Source symbols".to_owned(),
|
||||
|results| {
|
||||
let action = match results.action {
|
||||
query::Action::Definition => "Definitions",
|
||||
query::Action::References => "References",
|
||||
};
|
||||
results.alias.as_ref().map_or_else(
|
||||
|| action.to_owned(),
|
||||
|alias| format!("{action} · @{}", preview(alias, 40)),
|
||||
)
|
||||
},
|
||||
);
|
||||
let mut content = column![
|
||||
row![
|
||||
text(title).size(13),
|
||||
Space::new().width(Fill),
|
||||
button(text("Close").size(11))
|
||||
.padding([3, 8])
|
||||
.style(style::nav)
|
||||
.on_press(app.source_symbol_message(SymbolEvent::Close)),
|
||||
]
|
||||
.align_y(iced::Center)
|
||||
]
|
||||
.spacing(5);
|
||||
|
||||
if let Some(notice) = &state.notice {
|
||||
content = content.push(super::label(preview(notice, 240)));
|
||||
}
|
||||
if state.in_flight.is_some() {
|
||||
content = content.push(super::label("Finding source symbols…"));
|
||||
} else if let Some(results) = &state.results {
|
||||
let stale = results.stamp.snapshot != app.source_key();
|
||||
if stale {
|
||||
content = content.push(super::label(
|
||||
"Stale results — source changed. Run Definition or References again.",
|
||||
));
|
||||
}
|
||||
let scope = &results.scope;
|
||||
content = content.push(super::label(format!(
|
||||
"{} · {} · {} included / {} detached / {} unverified files · {} scope issues",
|
||||
if scope.partial() {
|
||||
"Partial scope"
|
||||
} else {
|
||||
"Complete include scope"
|
||||
},
|
||||
if !scope.applied {
|
||||
"Unapplied drafts"
|
||||
} else if scope.validated {
|
||||
"Validated sources"
|
||||
} else {
|
||||
"Draft sources"
|
||||
},
|
||||
scope.included_files,
|
||||
scope.detached_files,
|
||||
scope.unverified_files,
|
||||
scope.issues.len(),
|
||||
)));
|
||||
let explanation = match results.disposition {
|
||||
Disposition::NoSymbol => "Place the caret on a declaration alias or an @reference.",
|
||||
Disposition::NoDefinition => "No matching declaration in the loaded sources.",
|
||||
Disposition::Choices => {
|
||||
"Choose a declaration; draft and detached candidates need an explicit choice."
|
||||
}
|
||||
Disposition::Direct => "Definition in the current sources.",
|
||||
Disposition::LexicalReferences => {
|
||||
"Lexical uses of this alias; these do not establish a semantic target."
|
||||
}
|
||||
};
|
||||
content = content.push(super::label(explanation));
|
||||
if results.total_definitions > 1 {
|
||||
content = content.push(super::label(format!(
|
||||
"Alias collision: {} declarations. Uses are not attributed to a single declaration.",
|
||||
results.total_definitions,
|
||||
)));
|
||||
}
|
||||
let enabled = !stale && !app.busy;
|
||||
let mut rows = column![super::label(counts(
|
||||
"Declarations",
|
||||
results.definitions.len(),
|
||||
results.total_definitions,
|
||||
results.omitted_definitions,
|
||||
))]
|
||||
.spacing(4);
|
||||
for (index, definition) in results.definitions.iter().enumerate() {
|
||||
rows = rows.push(location_row(
|
||||
declaration_title(&definition.declaration),
|
||||
&definition.path,
|
||||
definition.position,
|
||||
provenance(
|
||||
definition.membership,
|
||||
definition.evidence,
|
||||
definition.declaration.identity,
|
||||
false,
|
||||
),
|
||||
enabled.then(|| {
|
||||
app.source_symbol_message(SymbolEvent::Choose {
|
||||
stamp: results.stamp,
|
||||
reference: false,
|
||||
index,
|
||||
})
|
||||
}),
|
||||
));
|
||||
}
|
||||
rows = rows.push(super::label(counts(
|
||||
"Lexical uses",
|
||||
results.references.len(),
|
||||
results.total_references,
|
||||
results.omitted_references,
|
||||
)));
|
||||
for (index, reference) in results.references.iter().enumerate() {
|
||||
let title = reference.context.as_ref().map_or_else(
|
||||
|| format!("@{} · lexical use", preview(&reference.alias, 48)),
|
||||
|context| {
|
||||
format!(
|
||||
"@{} · inside {}",
|
||||
preview(&reference.alias, 48),
|
||||
declaration_title(context)
|
||||
)
|
||||
},
|
||||
);
|
||||
rows = rows.push(location_row(
|
||||
title,
|
||||
&reference.path,
|
||||
reference.position,
|
||||
provenance(
|
||||
reference.membership,
|
||||
reference.evidence,
|
||||
reference
|
||||
.context
|
||||
.as_ref()
|
||||
.map_or(Identity::Missing, |context| context.identity),
|
||||
true,
|
||||
),
|
||||
enabled.then(|| {
|
||||
app.source_symbol_message(SymbolEvent::Choose {
|
||||
stamp: results.stamp,
|
||||
reference: true,
|
||||
index,
|
||||
})
|
||||
}),
|
||||
));
|
||||
}
|
||||
let row_count = results.definitions.len() + results.references.len();
|
||||
content =
|
||||
content.push(scrollable(rows).height((44.0 + row_count as f32 * 48.0).min(180.0)));
|
||||
}
|
||||
container(content)
|
||||
.padding(8)
|
||||
.width(Fill)
|
||||
.style(style::subtle)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn counts(label: &str, shown: usize, total: usize, omitted: usize) -> String {
|
||||
format!("{label}: {total} total · {shown} shown · {omitted} omitted")
|
||||
}
|
||||
|
||||
fn declaration_title(declaration: &Declaration) -> String {
|
||||
let name = declaration
|
||||
.name
|
||||
.as_ref()
|
||||
.map_or_else(String::new, |name| format!(" \"{}\"", preview(name, 48)));
|
||||
format!(
|
||||
"{}{} · @{}",
|
||||
preview(&declaration.kind, 24),
|
||||
name,
|
||||
preview(&declaration.alias, 48)
|
||||
)
|
||||
}
|
||||
|
||||
fn provenance(
|
||||
membership: Membership,
|
||||
evidence: Evidence,
|
||||
identity: Identity,
|
||||
context: bool,
|
||||
) -> String {
|
||||
let membership = match membership {
|
||||
Membership::Included => "Included",
|
||||
Membership::Detached => "Detached",
|
||||
Membership::Unverified => "Unverified",
|
||||
};
|
||||
let evidence = match evidence {
|
||||
Evidence::Validated => "Validated",
|
||||
Evidence::Draft => "Draft",
|
||||
};
|
||||
let collision = match identity {
|
||||
Identity::Collision { count } => format!(
|
||||
" · {}ID collision ({count})",
|
||||
if context { "Context " } else { "" },
|
||||
),
|
||||
Identity::Missing | Identity::Unique(_) => String::new(),
|
||||
};
|
||||
format!("{membership} · {evidence}{collision}")
|
||||
}
|
||||
|
||||
fn location_row(
|
||||
title: String,
|
||||
path: &str,
|
||||
position: Position,
|
||||
status: String,
|
||||
event: Option<Message>,
|
||||
) -> Element<'static, Message> {
|
||||
let line = position.line.saturating_add(1);
|
||||
let column = position.byte_column.saturating_add(1);
|
||||
let location = format!("{}:{line}:{column}", preview(path, 72));
|
||||
tooltip(
|
||||
button(
|
||||
column![
|
||||
text(title).size(12),
|
||||
super::label(format!("{location} · {status}"))
|
||||
]
|
||||
.spacing(2),
|
||||
)
|
||||
.padding([4, 6])
|
||||
.width(Fill)
|
||||
.style(style::nav)
|
||||
.on_press_maybe(event),
|
||||
text(format!(
|
||||
"{path}:{line}:{column} (1-based line and UTF-8 byte column)"
|
||||
))
|
||||
.size(11),
|
||||
tooltip::Position::Top,
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Keep headings compact without copying the remainder of a long admitted value.
|
||||
fn preview(value: &str, limit: usize) -> String {
|
||||
let mut chars = value.chars();
|
||||
let mut result: String = chars
|
||||
.by_ref()
|
||||
.take(limit)
|
||||
.map(|ch| if ch.is_whitespace() { ' ' } else { ch })
|
||||
.collect();
|
||||
if chars.next().is_some() {
|
||||
result.push('…');
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
Run from any directory with Python 3: semantic_schema.py [--check] [--validate].
|
||||
--validate additionally runs source-7 structural fixtures with jsonschema.
|
||||
This generates documentation only, never runtime selectors or model mutations.
|
||||
The desktop stored-history editor reads the checked-in descriptor for field
|
||||
shapes. Core types and validation remain authoritative for model mutations.
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
|
||||
+49
-61
@@ -1319,16 +1319,25 @@ 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
|
||||
on that guaranteed path is an error. Missing reply correlation is a draft
|
||||
that can follow another reply on a shared execution path, including conditional
|
||||
or parallel operands, 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. 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
|
||||
<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
|
||||
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 [@parameter]; field @data_field; expression "value expression"; }</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
|
||||
@@ -2633,62 +2642,6 @@ trace links, occurrence identity, comments and file ownership. Each open file
|
||||
retains its own unapplied draft across file navigation. Located lint diagnostics
|
||||
refer to the raw draft bytes rather than an identity-stabilized intermediate.
|
||||
Property editing MUST NOT silently discard a pending source draft.</p>
|
||||
<p>In Textual model, place the caret on a declaration alias or an <span class="docutils literal">@reference</span>
|
||||
and choose <strong>Definition</strong> (<span class="docutils literal">F12</span>) or <strong>References</strong> (<span class="docutils literal">Shift+F12</span>).
|
||||
<strong>Back</strong> (<span class="docutils literal">Alt+Left</span>) returns to the previous source location in this
|
||||
workspace. These shortcuts act while the source editor has focus; the Text
|
||||
toolbar also exposes the actions. If the previous draft changed, Back restores
|
||||
its current cursor rather than reusing an obsolete selection.</p>
|
||||
<p>Symbol navigation reads bounded snapshots of the currently loaded source
|
||||
drafts. It does not apply drafts or load missing included files. The results
|
||||
identify partial scope, draft evidence, detached or unverified files, alias
|
||||
ambiguity and authored-ID collisions. Draft, detached and ambiguous definition
|
||||
candidates require an explicit row choice. References are lexical uses of an
|
||||
alias, not a list of resolved typed relationships; comments and strings are
|
||||
excluded. Declaration and use counts include any rows omitted by the result
|
||||
limits. If sources change, stale rows cannot navigate: run the query again.
|
||||
Navigation MUST preserve the accepted model and every source draft, and select
|
||||
locations in the original UTF-8 bytes. Displayed locations use one-based lines
|
||||
and UTF-8 byte columns.</p>
|
||||
<p><strong>Rename alias</strong> (<span class="docutils literal">F2</span> while the source editor has focus) changes the alias
|
||||
under the caret or its exact selection. It requires a unique authored identity
|
||||
in the complete accepted source bundle, with no pending source, property or
|
||||
behavior drafts and no extra loaded detached files. Accepted but unsaved source
|
||||
is eligible. The operation MUST NOT implicitly apply a draft or allocate an ID.
|
||||
The engineering name and UUID remain unchanged.</p>
|
||||
<p>The <strong>Source alias</strong> field does not trim or normalize the entered alias. Pasting
|
||||
line breaks, tabs or other control characters is explicitly rejected as a whole:
|
||||
the field and its selection remain unchanged, and any previous preview approval
|
||||
is invalidated. <strong>Preview</strong>
|
||||
prepares a typed alias change and displays every changed file and source line;
|
||||
comments, strings, engineering data, file membership and declaration ownership
|
||||
remain unchanged. Enter in the field requests Preview, never confirmation.
|
||||
The preview is for this operation against accepted source, so a later Save may
|
||||
also contain earlier accepted edits. A changing rename requires enough model
|
||||
history capacity to retain its immediate Undo. An invalid, identical, stale or
|
||||
oversized candidate cannot enable confirmation.</p>
|
||||
<p>The preview MUST provide access to all admitted files, hunks, lines and long-line
|
||||
segments. Refuse a candidate whose complete preview exceeds the editor's bounds:
|
||||
256 changed files, 50,000 changed lines or 8 MiB of displayed source bytes
|
||||
(both versions of changed/context lines and file paths). Index metadata is
|
||||
separately bounded to seven retained line records per changed line. These
|
||||
limits do not restrict the language itself. A changed-line count is not a count
|
||||
of semantic references. Cancel, Preview and Rename alias remain reachable
|
||||
outside the independently scrolling source preview.</p>
|
||||
<p>Explicit <strong>Rename alias</strong> confirms the current preview once, as one accepted
|
||||
model/source transaction. A source, document, recovery or replacement change
|
||||
invalidates its approval even if later bytes match the old preview. Closing the
|
||||
form cancels approval; a late worker result MUST NOT reopen it. Rejected,
|
||||
canceled and unchanged operations preserve accepted bytes, drafts, save state,
|
||||
revisions, selection and both histories. Successful rename selects the new alias
|
||||
at the original occurrence rather than the first matching spelling in the file.
|
||||
Editing the replacement permits a new Preview in the same form. After a source
|
||||
context change, close the form and invoke Rename again on the current alias.</p>
|
||||
<p><strong>Undo rename</strong> is guarded against intervening edits and uses model history;
|
||||
the persistent <strong>Undo model</strong> and <strong>Redo model</strong> controls restore the complete
|
||||
accepted snapshots. Text <span class="docutils literal">Ctrl+Z</span> remains <strong>Undo text</strong>. Save is separate and
|
||||
uses the ordinary guarded source-save contract. An unconfirmed preview and Undo
|
||||
history are not persisted on reopening.</p>
|
||||
<p>Changing a graphical UUID must atomically update map keys and every reference.
|
||||
Alias changes preserve UUID identity. Deletion must account for owned elements,
|
||||
incident relationships, ordered occurrences, and diagram placements without
|
||||
@@ -2696,6 +2649,41 @@ 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>
|
||||
|
||||
+54
-68
@@ -680,18 +680,28 @@ 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
|
||||
on that guaranteed path is an error. Missing reply correlation is a draft
|
||||
that can follow another reply on a shared execution path, including conditional
|
||||
or parallel operands, 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. 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
|
||||
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
|
||||
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
|
||||
@@ -2054,69 +2064,6 @@ retains its own unapplied draft across file navigation. Located lint diagnostics
|
||||
refer to the raw draft bytes rather than an identity-stabilized intermediate.
|
||||
Property editing MUST NOT silently discard a pending source draft.
|
||||
|
||||
In Textual model, place the caret on a declaration alias or an ``@reference``
|
||||
and choose **Definition** (``F12``) or **References** (``Shift+F12``).
|
||||
**Back** (``Alt+Left``) returns to the previous source location in this
|
||||
workspace. These shortcuts act while the source editor has focus; the Text
|
||||
toolbar also exposes the actions. If the previous draft changed, Back restores
|
||||
its current cursor rather than reusing an obsolete selection.
|
||||
|
||||
Symbol navigation reads bounded snapshots of the currently loaded source
|
||||
drafts. It does not apply drafts or load missing included files. The results
|
||||
identify partial scope, draft evidence, detached or unverified files, alias
|
||||
ambiguity and authored-ID collisions. Draft, detached and ambiguous definition
|
||||
candidates require an explicit row choice. References are lexical uses of an
|
||||
alias, not a list of resolved typed relationships; comments and strings are
|
||||
excluded. Declaration and use counts include any rows omitted by the result
|
||||
limits. If sources change, stale rows cannot navigate: run the query again.
|
||||
Navigation MUST preserve the accepted model and every source draft, and select
|
||||
locations in the original UTF-8 bytes. Displayed locations use one-based lines
|
||||
and UTF-8 byte columns.
|
||||
|
||||
**Rename alias** (``F2`` while the source editor has focus) changes the alias
|
||||
under the caret or its exact selection. It requires a unique authored identity
|
||||
in the complete accepted source bundle, with no pending source, property or
|
||||
behavior drafts and no extra loaded detached files. Accepted but unsaved source
|
||||
is eligible. The operation MUST NOT implicitly apply a draft or allocate an ID.
|
||||
The engineering name and UUID remain unchanged.
|
||||
|
||||
The **Source alias** field does not trim or normalize the entered alias. Pasting
|
||||
line breaks, tabs or other control characters is explicitly rejected as a whole:
|
||||
the field and its selection remain unchanged, and any previous preview approval
|
||||
is invalidated. **Preview**
|
||||
prepares a typed alias change and displays every changed file and source line;
|
||||
comments, strings, engineering data, file membership and declaration ownership
|
||||
remain unchanged. Enter in the field requests Preview, never confirmation.
|
||||
The preview is for this operation against accepted source, so a later Save may
|
||||
also contain earlier accepted edits. A changing rename requires enough model
|
||||
history capacity to retain its immediate Undo. An invalid, identical, stale or
|
||||
oversized candidate cannot enable confirmation.
|
||||
|
||||
The preview MUST provide access to all admitted files, hunks, lines and long-line
|
||||
segments. Refuse a candidate whose complete preview exceeds the editor's bounds:
|
||||
256 changed files, 50,000 changed lines or 8 MiB of displayed source bytes
|
||||
(both versions of changed/context lines and file paths). Index metadata is
|
||||
separately bounded to seven retained line records per changed line. These
|
||||
limits do not restrict the language itself. A changed-line count is not a count
|
||||
of semantic references. Cancel, Preview and Rename alias remain reachable
|
||||
outside the independently scrolling source preview.
|
||||
|
||||
Explicit **Rename alias** confirms the current preview once, as one accepted
|
||||
model/source transaction. A source, document, recovery or replacement change
|
||||
invalidates its approval even if later bytes match the old preview. Closing the
|
||||
form cancels approval; a late worker result MUST NOT reopen it. Rejected,
|
||||
canceled and unchanged operations preserve accepted bytes, drafts, save state,
|
||||
revisions, selection and both histories. Successful rename selects the new alias
|
||||
at the original occurrence rather than the first matching spelling in the file.
|
||||
Editing the replacement permits a new Preview in the same form. After a source
|
||||
context change, close the form and invoke Rename again on the current alias.
|
||||
|
||||
**Undo rename** is guarded against intervening edits and uses model history;
|
||||
the persistent **Undo model** and **Redo model** controls restore the complete
|
||||
accepted snapshots. Text ``Ctrl+Z`` remains **Undo text**. Save is separate and
|
||||
uses the ordinary guarded source-save contract. An unconfirmed preview and Undo
|
||||
history are not persisted on reopening.
|
||||
|
||||
Changing a graphical UUID must atomically update map keys and every reference.
|
||||
Alias changes preserve UUID identity. Deletion must account for owned elements,
|
||||
incident relationships, ordered occurrences, and diagram placements without
|
||||
@@ -2125,6 +2072,45 @@ 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
|
||||
-------------------------------------
|
||||
|
||||
|
||||
+10
-8
@@ -1,6 +1,6 @@
|
||||
# Linux packaging and deployment inputs
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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,23 +9,25 @@ 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 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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Contents and provenance
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
`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.
|
||||
`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.
|
||||
|
||||
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` 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.
|
||||
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.
|
||||
|
||||
## Native extracted-bundle 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, 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.
|
||||
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.
|
||||
|
||||
For a software Vulkan trial, first locate the installed lavapipe ICD; only if it exists, use:
|
||||
|
||||
@@ -33,7 +35,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, 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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -278,3 +278,71 @@ This is a selected design under implementation, not a native acceptance claim.
|
||||
Native reverse scenario Apply selected a dependency and inserted five overlapping behavior placements into an unrelated architecture view. Core transition correctness did not make that presentation useful. Select the counterpart of the explicit request root and resolve its nearest scenario/process/chain/state owner. Without an explicit destination diagram, keep the exact core candidate and open Behavior; preserve unrelated architecture views. Retain ordinary architecture transition placement and honor explicit diagram choices.
|
||||
|
||||
Automatically creating another architecture view would retain the wrong editing workspace and introduce unrequested model state. Merely navigating to Behavior after adding placements would conceal the presentation damage. Both alternatives are rejected. The selected correction retains one source transaction and Undo operation and guards stale canvas events. Revisit if mixed-root requests or realistic explicit destination workflows require a separate user choice; do not infer such a choice from dependency ordering. Test/build/native acceptance is recorded separately in release-behavior-transition-context-20260906.md.
|
||||
|
||||
## Property baselines and explicit alternative cases — 2026-09-11
|
||||
|
||||
Problem: complete independent bindings repeat shared engineering assumptions in
|
||||
every ordinary study case. The user requires practical text-only development and
|
||||
reduced cognitive load for small and large models. Evaluator implementation
|
||||
simplicity alone cannot justify that authoring cost.
|
||||
|
||||
Select property-owned base values/formulas and one-level case overrides as the next
|
||||
active-study contract to formalize. Every evaluator caller explicitly requests
|
||||
ModelBaseline or a particular Case identity; no active UI selection, unnamed
|
||||
default case or case-to-case inheritance determines meaning. The baseline is the
|
||||
model's explicit property values, not a mandatory extra record for entering the
|
||||
first value. Inherited formulas read the requested effective case. An explicit
|
||||
Unbound override shadows a known base; clearing an override uses the base.
|
||||
|
||||
The [executed comparison](release-analysis-context-experiment-20260911.md) passed
|
||||
218 assertions. Twenty cases with20 fixed/two varying inputs used62 binding rows
|
||||
under B versus480 under D, with identical results. Updating one shared assumption
|
||||
required one B binding versus20 D bindings. Reusable predicates eliminated D's
|
||||
threshold-edit disadvantage, so repeated formulas are not the reason for selection.
|
||||
|
||||
Retain an explicit complete independent freeze/export for intentionally separate
|
||||
assumptions. It must capture effective values/formulas and the full exact definition
|
||||
environment, including package pins; copying only case rows leaves hidden live
|
||||
dependencies. Captured revisions and per-input origins must make propagation
|
||||
visible. Effective dependency/cycle validation and stale result handling must cover
|
||||
the whole requested context.
|
||||
|
||||
Reject compulsory complete cases for ordinary shared-assumption modeling; retain
|
||||
their useful isolation as an explicit independent snapshot. Reject arbitrary
|
||||
inheritance and UI-dependent evaluation. Do not import the tiny experiment
|
||||
interpreter as production architecture. Exact check identity, type/operator
|
||||
algebra, source syntax, library closure/compatibility, transitions and public
|
||||
snapshot formats still require the coordinated precise contract. Current source7
|
||||
does not implement this decision.
|
||||
|
||||
Revisit if complete text/native tasks expose unacceptable hidden propagation,
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
+66
-124
@@ -1,135 +1,77 @@
|
||||
> Historical record — superseded on 2026-09-11. Live plans, ownership and blockers
|
||||
> were migrated once to [Gitea issue 1, comment 408](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/1#issuecomment-408)
|
||||
> and [issue 2](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/2).
|
||||
> Do not update this as a live ledger. [STATE.md](STATE.md) links the verified integrated result.
|
||||
> The historical text below is preserved; its old active-status wording is not current guidance.
|
||||
> 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, Codex-9e8r6t `/root`.
|
||||
[PROJECT_GOAL.md](../PROJECT_GOAL.md) defines the outcome;
|
||||
[TASK_LEDGER.md](TASK_LEDGER.md) separates evidence from assumptions;
|
||||
[STATE.md](STATE.md) owns active leases and results.
|
||||
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.
|
||||
|
||||
Resumed after the user's virtualization/reboot update. STATE.md supersedes the
|
||||
historical [RESTART_HANDOFF.md](RESTART_HANDOFF.md) for live status. Postboot
|
||||
focused3 ran25 passing tests and one failing overflow-fixture assertion; its
|
||||
test-only correction passed all26 focused4 tests. Strict Clippy, formatting and
|
||||
matching GUI5657 build also passed; fresh native acceptance continues.
|
||||
Nondeleting compression completed across282 generated binaries with identical
|
||||
hashes and11.78GB free; supported Ubuntu26.04 installation passed exit0. Actual
|
||||
root shell identifies Ubuntu26.04/WSL2. User328 replaces failed mirrored mode
|
||||
with VirtioProxy; the VPN is immutable. Both new guest startups timed out before
|
||||
execution, then the distinct WslService restart failed at OS access permissions.
|
||||
All60 apt archives and five Rust components are downloaded and hash-verified;
|
||||
guest signature verification/install and Linux Cargo remain pending. No compiler
|
||||
or WSL client lease is active. The user has now paused Linux builds/checking
|
||||
until they report WSL networking resolved; do not schedule more Linux setup,
|
||||
launches, diagnostics or checks before that notice (Gitea comment356).
|
||||
Lifeline/Chain history and Machine Create/Undo/postboot reopen passed on GUI52;
|
||||
Fresh GUI5657 cancellation/recovery, Lifeline direct creation/history and Machine
|
||||
minimum-size owner/Create/Undo/Redo/reopen now pass; full679 desktop tests pass.
|
||||
## Immediate integration and verification
|
||||
|
||||
## Immediate implementation and acceptance
|
||||
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.
|
||||
|
||||
1. Preserve and publish the completed native acceptance of GUI `5657BAB5…D3846CE` using
|
||||
the [v2 plan](release-contextual-source-native-v2-plan-20260911.md). All26
|
||||
affected tests, strict all-target Clippy, formatting and matching build pass;
|
||||
the full679 immutable Windows test run also passed. Minimum-size
|
||||
child visibility, exact recovery/cancel and direct Lifeline Create/Undo/Redo
|
||||
now pass, along with Machine owner visibility/Create/Undo/Redo/reopen. Preserve
|
||||
independent first UUID carriers and serialize future compiler/source leases.
|
||||
Prepare an updated Windows prerelease only with honest per-binary source
|
||||
identity, package verification and clean packaged runtime checks.
|
||||
2. Exercise the implemented source subform for Lifeline, process/chain Work and
|
||||
machine owner, plus semantic first-action prompts. Verify parent-draft
|
||||
retention, explicit prerequisite and consumer adoption, exact source/history,
|
||||
cancellation and recovery against the independent staged-source oracle.
|
||||
Final source guards and guide corrections are complete and reviewed; native
|
||||
acceptance remains; source/evidence are integrated in `df6f595`. Named creation, nine-file reopen and
|
||||
SA pending-draft recovery already have their separate native passes.
|
||||
3. Complete local SA B2 and the saved basic
|
||||
supplementary move/camera/invalid-draft and minimum-size story; the main basic
|
||||
model and explicit transition passed full Project+SourceBundle, Undo/Redo and
|
||||
reopen on candidate 3 at wide size. OA B2 Action, State, bound Reuse and Fragment
|
||||
now pass complete model/two-source Create, one Undo, one Redo and final OA
|
||||
reopen/Save with exact original identities. Preserve the existing B3 layer/size
|
||||
scope; do not infer SA B2 or broader camera/focus coverage from these passes.
|
||||
4. Integrate standalone B4 scenario lifetimes and the external independent
|
||||
scenario contract without overwriting local layout work. Resolve source-derived
|
||||
lifetime versus insertion bounds, activation-after-Delete and lane-map issues.
|
||||
Close remaining
|
||||
contextual operand, missing-source and state-naming workflow gaps identified
|
||||
by the current behavior audit and native study.
|
||||
## Capability milestones
|
||||
|
||||
## Foundational semantic additions
|
||||
| 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. |
|
||||
|
||||
The updated objective explicitly requires functional constraints/calculations,
|
||||
quantities/units and maintained model libraries. Current metadata/includes are
|
||||
insufficient. This work runs independently of useful behavior improvements.
|
||||
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.
|
||||
|
||||
Peer comment 231 ratifies schema 8/local-definition core steps in an isolated
|
||||
worktree. Coordinate those actual shared types and ownership before local
|
||||
integration; the current local core remains descriptive. Source grammar,
|
||||
evaluation, server/CLI/UI representation and acceptance are separate unfinished
|
||||
steps. Peer956 now reports focus regression5/5 and isolated numeric26/26 plus
|
||||
analysis28/28; their codec/lifecycle work remains isolated. Local integration and
|
||||
coherent union gates remain necessary. Do not replace peer efforts with a
|
||||
parallel local implementation. Comment338 separately reserves our bounded
|
||||
syntax-only outline seam for current-text navigation; its private query module
|
||||
and prepared tests remain uncompiled pending review.
|
||||
## Evaluator boundaries and replanning
|
||||
|
||||
The newest objective also requires complete uncluttered text-only workflows.
|
||||
Combine the local three-task usability audit with the other host's language/
|
||||
identity/API design. Two concise current-grammar recipes now pass their bounded
|
||||
REST identity/source/persistence experiment. The published CLI now covers checked
|
||||
stabilization and typed alias rename with exact source-preserving preview/write.
|
||||
Next evaluate cross-file definitions/references and engineering task completion
|
||||
on repeated-name modular models. Do not equate shorter examples
|
||||
or parser throughput with task usability, and do not silently change omissions.
|
||||
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.
|
||||
|
||||
- Compare real candidate semantics before large changes. Use primary Capella,
|
||||
SysML and quantity/unit evidence, with explicit compatibility limits.
|
||||
- Falsify candidates through mass/power/latency budgets, mixed/affine units,
|
||||
unknown/error/false results, dependency cycles, reference identity and bounded
|
||||
evaluation. Separate descriptive existing text from executable declarations.
|
||||
- Probe libraries through two projects/two instances, pinned offline content,
|
||||
exports/dependency closure, bindings, distinct origin/local identities, updates
|
||||
with local divergence, detachment and exact transactional rollback.
|
||||
- Review and test the peer-ratified pure core and its explicit migration/admission
|
||||
boundaries before local integration. Retain independent falsifiers and current
|
||||
typed Project, Command and source-writer guarantees across representations.
|
||||
- Implement the ratified contracts across validation, visitors, deletion,
|
||||
identity, OA/SA transitions, source/reST, schema/REST, ergonomic graphical
|
||||
authoring and help. Add real examples and independent parity acceptance.
|
||||
|
||||
## Integration and release
|
||||
|
||||
- GUI/server release 1, `v0.2.0-preview.authoring.20260911`, and standalone CLI
|
||||
release 2, `cli-v0.2.0-preview.authoring.20260911`, are published and read back.
|
||||
Their exact notices, archive, runtime and uploaded-asset evidence is preserved.
|
||||
CLI packaging is integrated at `27df4a3`; instruction sync `07eb3cd` and
|
||||
contextual source checkpoint `df6f595` and its raw evidence are integrated.
|
||||
CLI publication `d95b1ab` and OA native checkpoint `c4bad4b` are also integrated;
|
||||
then publish later coherent milestones under the assigned owner. Preserve each
|
||||
artifact's source/profile/native scope and complete final-release gates separately.
|
||||
- Publish coherent commits and verified bundle transfers through Gitea; keep
|
||||
concise host-scoped handoffs and Wiki. Do not overwrite another host's branch.
|
||||
- Refresh relevant source/schema/example/doc gates and run current core,
|
||||
desktop/server and real REST transport/restart/ETag/draft-conflict workflows.
|
||||
- Build matching optimized desktop/server packages. Regenerate complete notices
|
||||
for actual inputs and verify archive contents, hashes and clean-runtime launch.
|
||||
Historical ELF fixtures and build preparation do not replace execution.
|
||||
- Follow the external owner's supported Linux guest route and run current
|
||||
GUI/server workflows when viable. This local host has no usable Linux runtime;
|
||||
supported WSL2 failed. Preserve failed attempts rather than repeating rejected
|
||||
workarounds, and continue independent Windows/semantic work.
|
||||
- After the user reboot, check local Ubuntu registration and launch first. A
|
||||
version2 default alone is not an installed/running Linux environment.
|
||||
- Conduct fresh independent requirements, semantic, usability, architecture,
|
||||
correctness, runtime and documentation reviews. Resolve all material findings,
|
||||
reverify affected behavior and only then declare release completion.
|
||||
|
||||
Historical milestones and evidence remain intact in
|
||||
[the dated plan archive](history/2026-09-10-ledgers-before-second-consolidation/PLAN.md).
|
||||
They do not supersede current user scope or imply current-tree acceptance.
|
||||
[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.
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# 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.
|
||||
@@ -1,63 +0,0 @@
|
||||
# Restart handoff — 2026-09-11
|
||||
|
||||
Historical reboot checkpoint. User has returned and enabled virtualization;
|
||||
current STATE.md and the postboot native/layout/WSL reports supersede the pause
|
||||
and pending-result wording below. Preserve this record for exact resume history.
|
||||
|
||||
Codex-9e8r6t /root. User explicitly requested a reboot pause to make WSL2/Ubuntu
|
||||
available. Full PROJECT_GOAL remains active and incomplete. Resume only after
|
||||
the user returns; do not schedule automatic work during the reboot.
|
||||
|
||||
## Saved state
|
||||
|
||||
- Branch `codex/release1-9e8r6t`: contextual source `df6f595`, CLI publication
|
||||
evidence `d95b1ab` and complete local OA scenario evidence `c4bad4b` were pushed
|
||||
and read back. The following reboot commit also preserves reviewed WIP below.
|
||||
- Both Windows Studio/server and standalone CLI prereleases remain published.
|
||||
This checkpoint does not update either release or establish Linux acceptance.
|
||||
- Immutable GUI52DE82 native source trial: Lifeline Save-adoption and Chain
|
||||
direct Create each passed complete model/source, one Undo and one Redo with
|
||||
original IDs. Machine direct Create/Save passed six exact files; one Undo/Save
|
||||
passed four reachable/six physical files. Machine Redo/reopen remain pending.
|
||||
Test GUI closed normally, and its returned window was absent at 13:38 UTC.
|
||||
`work` contains the saved Machine Undo state. Do not infer persistent history;
|
||||
use preserved first machine checkpoint for isolated reopen and a fresh trial
|
||||
for missing history coverage. See release-contextual-source-native-20260911.md
|
||||
and experiments/contextual-source-native-20260911/README.md.
|
||||
- Reviewed WIP: source_draft.rs essentials-first layout, explicit wrapped kinds
|
||||
before unbounded parent text, plus creation.rs staged-owner caption fix.
|
||||
Three real Paragraph tests are prepared. First compile failed in test helpers
|
||||
(ambiguous macro and wrong Id constructor); corrected only those helpers.
|
||||
Corrected focused2 was interrupted for this reboot before any tests ran;
|
||||
Clippy/fmt/GUI did not launch. There is NO new passing suite or matching GUI.
|
||||
- Worker release_audit stopped its command with Ctrl+C; all Cargo/rustc/linker
|
||||
processes were absent. All 238 frozen input hashes were unchanged.
|
||||
Final source_draft SHA prefix C8D36BD6; creation SHA prefix 0FB19BE3.
|
||||
Exact freeze inputs3 SHA256
|
||||
`6B3F3088FF18AF3899C3764B3F3671DA9240A5DE184D871BF08F8012456E556B`.
|
||||
|
||||
## Next actions after reboot
|
||||
|
||||
1. Read AGENTS, PROJECT_GOAL, STATE, PLAN and latest Gitea issue 1/2 comments and
|
||||
relevant Wiki. Preserve Codex-956atf canvas/chooser/schema8 and Codex-ytr83r
|
||||
B4/Linux ownership. User issue2 comment250 allows domain-constrained diagram
|
||||
manipulation; arbitrary free movement is not required.
|
||||
2. Recheck `wsl --list --verbose`. Before reboot it reported no installed
|
||||
distribution despite default version2. Launch/setup Ubuntu as needed with
|
||||
the user handling any authentication. Verify a real usable Linux environment
|
||||
before claiming a build. Do not repeat rejected VM workarounds.
|
||||
3. Validate the frozen input manifest, then resume a new focused3 run. Keep
|
||||
focused1 failure and focused2 interrupted evidence. On a pass, run strict
|
||||
desktop all-target Clippy, formatting and matching GUI build; use a distinct
|
||||
immutable stage. No broad full-suite repeat without a new reason.
|
||||
4. Root alone performs native Computer Use with @oai/sky. Relaunch the corrected
|
||||
artifact in a fresh profile and verify initial Name/kind/parent visibility at
|
||||
1100×700, the owner label, direct Lifeline Create, remaining cancellation/
|
||||
recovery and machine history/reopen using independent expected contracts.
|
||||
5. Continue SA B2, basic supplementary workflows, peer integration, complete
|
||||
semantic/text/REST/library work, Windows/Linux packaging and fresh final
|
||||
cross-disciplinary acceptance. The full release remains unfinished.
|
||||
|
||||
Private runtime and build stages under `.local` are retained; no temporary
|
||||
binary, token or credential was committed. Raw interruption/native records are
|
||||
archived separately with exact hashes. All workers are idle for the reboot.
|
||||
@@ -0,0 +1,64 @@
|
||||
# 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.
|
||||
+28
-121
@@ -1,142 +1,49 @@
|
||||
# Integrated checkpoint — first release
|
||||
# Integrated checkpoint — 2026-09-11
|
||||
|
||||
Updated 2026-09-13 UTC (September 12 locally) by Codex-9e8r6t `/root`, Windows host.
|
||||
**Full OA/SA release acceptance is incomplete. Linux work remains user-paused.**
|
||||
Codex-ytr83r, branch `codex/release1-ytr83r`. The full [user goal](../PROJECT_GOAL.md) remains incomplete. Live integration/acceptance work is in [Gitea issue5](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/5); the [project overview](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/1) and [process/scenario requirement](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/2) link other work. This file scopes integrated code and evidence.
|
||||
|
||||
[PROJECT_GOAL.md](../PROJECT_GOAL.md) owns the application requirements.
|
||||
Current objective attachment `9467fa86-4606-4fe2-93ec-122137d03079` was read in full.
|
||||
Instruction synchronization `b70bf1d` preserves the user specification and
|
||||
[AGENTS.md](../AGENTS.md). It changes instructions, not the verified package inputs.
|
||||
## Verified code and capabilities
|
||||
|
||||
## Delivered Windows milestone
|
||||
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.
|
||||
|
||||
[Source alias rename prerelease](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/releases/tag/v0.2.0-preview.rename.20260912)
|
||||
was published and read back on September 13 at 00:47:52 UTC. Extract the whole ZIP
|
||||
and run `arcadia-studio.exe`, keeping its sibling `libunwind.dll`. Studio, the
|
||||
optional REST server, examples, offline help and dependency notices are included.
|
||||
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.
|
||||
|
||||
Both executables come from **cbc49a7802fb1bba0dd284fd904c700a0703ae70**; the tag
|
||||
identifies that exact source. Evidence-only checkpoint **506a349b** holds the
|
||||
completed source/package/runtime/native reports and independent reviews. All
|
||||
559 package files and all three downloaded release assets were verified.
|
||||
The binaries use development profiles; this is a prerelease.
|
||||
- [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).
|
||||
|
||||
The GUI adds F2 alias rename with a complete preview, explicit confirmation,
|
||||
origin selection, immediate Undo Rename and explicit Save. Strict Clippy,
|
||||
805 desktop tests, the isolated Confirm timing check and 501 core/server tests
|
||||
passed. Actual packaged GUI checks covered minimum-size simple and 30-source
|
||||
workflows, all six representative changed lines, Undo/Redo/Save and representative
|
||||
reopening. Eight exact saved-state checks preserve model/identity/source and all
|
||||
41 final physical fixture files. Normal GUI closure returned 0 with empty logs.
|
||||
The packaged server separately passed 23 HTTP/disk persistence/restart checks.
|
||||
Each result retains its precise scope.
|
||||
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).
|
||||
|
||||
See the [publication record](release-source-alias-rename-published-20260912.md),
|
||||
[package acceptance and limits](release-source-alias-rename-package-20260912.md),
|
||||
and [source validation](release-source-alias-rename-validation-20260912.md).
|
||||
A known P2 Details-to-Preview delayed-scroll-reset lifetime gap remains disclosed;
|
||||
the separately owned correction/union is not part of this frozen package.
|
||||
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 extracted Studio `BF9C2E61…` passed actual OA first-child, sibling and grandchild creation plus Save/Undo/Redo comparisons against original complete model/source checkpoints. Reopening and the SA native counterpart remain unverified. [Child-work fixture and oracle](fixtures/child-work-native/README.md); [native OA evidence](experiments/native-child-oa-865483b-20260911/README.md); [live acceptance boundary](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/4).
|
||||
|
||||
## Previous Windows milestone
|
||||
## Verification scope
|
||||
|
||||
[Source navigation prerelease](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/releases/tag/v0.2.0-preview.navigation.20260911)
|
||||
was published and read back on September 12 at 10:54:02 UTC. Extract the whole ZIP and run
|
||||
`arcadia-studio.exe`; keep its sibling `libunwind.dll`. Studio, the optional REST
|
||||
server, examples, offline help and dependency notices are included.
|
||||
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 tag points to exact raw-input reconstruction **802855f7**, based on corrected
|
||||
source **33c6d83** and distinct from the original build capture. The
|
||||
package pairs GUI B183 with the unchanged, separately verified EFEE server.
|
||||
All 559 package files and all three downloaded release assets were verified.
|
||||
The binaries use the development profile; this is a prerelease.
|
||||
- [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 GUI adds Definition/References/Back for current Text drafts and corrects
|
||||
repair/recovery saved-state handling. All 755 desktop tests passed (one ignored),
|
||||
with strict Clippy, formatting and a matching build. Earlier navigation native
|
||||
checks cover minimum-size interaction, ambiguous/partial scope and the 30-file
|
||||
engineering workflow; the corrected binary separately passed repair/rollback,
|
||||
Close/Cancel and exact Save checks. Fresh package acceptance verified Home/help,
|
||||
native Open, navigation, edit/Undo, Apply, Save and complete saved-model/source
|
||||
equality. Independent server checks verified exact API content and persistence
|
||||
across restart. Each result retains its specific build and workflow scope.
|
||||
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.
|
||||
|
||||
Authoritative records:
|
||||
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.
|
||||
|
||||
- [Publication, artifacts and limitations](release-navigation-prerelease-published-20260912.md)
|
||||
- [Per-binary preparation](release-navigation-prerelease-preparation-20260911.md)
|
||||
and [independent archive/server review](release-navigation-prerelease-review-20260911.md)
|
||||
- [Fresh native package acceptance](release-navigation-prerelease-native-20260911.md)
|
||||
and [integrated navigation evidence](release-text-navigation-integrated-20260911.md)
|
||||
- [Previous contextual authoring milestone](release-contextual-source-prerelease-published-20260911.md)
|
||||
- [Verified code/evidence checkpoint](release-postboot-checkpoint-review-20260911.md)
|
||||
- [Standalone CLI prerelease](release-cli-prerelease-published-20260911.md),
|
||||
supplying checked source stabilization and typed alias rename separately
|
||||
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 completes bounded server runtime acceptance, while the new Windows preview remains unpublished pending the separate GUI checks.
|
||||
|
||||
## Material unfinished scope
|
||||
## Material limitations
|
||||
|
||||
Current-draft Definition/References/Back was implemented in source checkpoint
|
||||
**[36db9356](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/commit/36db9356cce3c7614ae37eec9fe17678ca210afe)**.
|
||||
The [initial native trial](release-text-navigation-native-initial-20260911.md)
|
||||
found four interaction defects. Their [correction](release-text-navigation-native-corrections-20260911.md)
|
||||
at **eeeaa126** passed 188 focused tests, the full 750-test desktop suite and
|
||||
strict Clippy, with exact input readback and independent review. Matching GUI5478
|
||||
[native evaluation](release-text-navigation-native-v3-20260911.md) verified those
|
||||
fixes, ambiguous/partial source navigation, and the 30-file engineering workflow;
|
||||
explicit Save preserved complete models, identities and source bytes. It also
|
||||
found a repair-mode false-clean status against the prior saved destination.
|
||||
Its [saved-baseline correction](release-text-repair-save-state-20260911.md)
|
||||
at **33c6d83** passes the full 755-test suite, strict Clippy and matching GUI B183
|
||||
build. Its [native repair verification](release-text-repair-save-native-v4-20260911.md)
|
||||
passed missing-include and duplicate-alias repairs plus interrupted-save rollback:
|
||||
Apply/Recover retain unsaved status, Close warns, Cancel retains work, and explicit
|
||||
Save yields exact complete model/identity/source equality. The fresh package
|
||||
passed its separate native and independent server checks and is now delivered above.
|
||||
The [integrated evaluation](release-text-navigation-integrated-20260911.md) links
|
||||
exact input archives, earlier full-test scope and independent findings.
|
||||
The published contextual preview remains unchanged.
|
||||
Native wide Latin/CJK checks found neutral lifeline header text overflowing its card; Create labels remained contained. The [header correction](lifecycle-header-overflow-correction-ytr83r-20260911.md) is included in the tested 865483b source, but matching corrected pixels and combining/emoji native appearance remain unverified. The old GUI's wide-name passive Save pass establishes identity/model/source/file equality, not corrected rendering. Complete combined native and packaged acceptance remain separate. During the OA reopen check, the Computer helper repeatedly failed with `window capture timed out: timed out waiting on channel`; fresh window selection and one clean helper restart did not recover capture. Native input stopped at the Open dialog with the accepted grandchild result already saved. This is a validation-tool failure, not evidence of a model failure or a successful reopen.
|
||||
|
||||
The latest rename release does not yet contain newer peer canvas/chooser and
|
||||
scenario-lifetime work, complete constraints/calculations, dimensional quantities,
|
||||
maintained model libraries.
|
||||
Those additions require coherent graphical/text/REST realization and validation.
|
||||
Some scenario forms retain old scroll positions and dense labels remain crowded.
|
||||
Local SA action/state/reuse/fragment and supplementary camera/focus tasks retain
|
||||
explicit unfinished scopes in [scenario evidence](release-scenario-native-local-20260911.md).
|
||||
Other chooser/canvas and text-navigation contributions still require their own integration and verification. 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.
|
||||
|
||||
Optimized builds, remaining real engineering workflows and a fresh independent
|
||||
cross-disciplinary end-state review remain necessary. Old MVP exclusions and
|
||||
individual passing tests do not reduce the user's full requirements.
|
||||
## Platform and published deliverables
|
||||
|
||||
Ubuntu/WSL setup and all Linux diagnostics/builds/checks are paused until the user
|
||||
explicitly reports networking resolved. Preserve existing configuration, caches
|
||||
and evidence. [WSL readiness](release-wsl-readiness-9e8r6t-20260911.md) owns the
|
||||
historical attempts; it is not a current Linux application acceptance claim.
|
||||
**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.
|
||||
|
||||
## Coordination and history
|
||||
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.
|
||||
|
||||
The user requested splitting oversized [issue 1](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/1).
|
||||
Live coordination now uses focused [behavior/canvas](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/2),
|
||||
[activity movement](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/3),
|
||||
[child work](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/4),
|
||||
[lifecycle Windows acceptance](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/5),
|
||||
[source navigation](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/6),
|
||||
[quantities/calculations](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/7),
|
||||
[maintained libraries](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/8),
|
||||
and [Text alias rename](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/9).
|
||||
Read current comments for ownership and execution state; this checkpoint does
|
||||
not track active leases.
|
||||
[Authoring Wiki](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/wiki/Authoring20260911)
|
||||
is the shared entrypoint. Durable decisions remain in [DECISIONS.md](DECISIONS.md)
|
||||
and topic reports; the [text-navigation design](release-text-navigation-design-20260911.md)
|
||||
records the current bounded design and its falsifiers.
|
||||
The [Text alias-rename proposal](release-source-alias-rename-design-20260911.md)
|
||||
and [critique](release-source-alias-rename-critique-20260911.md) retain the original
|
||||
design rationale; current implementation and verified scope are recorded in the
|
||||
[published package report](release-source-alias-rename-package-20260912.md).
|
||||
## Record navigation
|
||||
|
||||
Legacy TASK_LEDGER/PLAN live content was migrated once in
|
||||
[comment 408](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/1#issuecomment-408).
|
||||
Those files are marked historical. Previous detailed STATE was preserved
|
||||
[byte-for-byte](history/2026-09-11-state-before-gitea-migration/STATE.md), including
|
||||
failed attempts, prior releases, transfer paths and earlier evidence links.
|
||||
Do not restore historical active-status or environment-next-step wording.
|
||||
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.
|
||||
|
||||
+125
-176
@@ -1,196 +1,145 @@
|
||||
> Historical record — superseded on 2026-09-11. Live plans, ownership and blockers
|
||||
> were migrated once to [Gitea issue 1, comment 408](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/1#issuecomment-408)
|
||||
> and [issue 2](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/2).
|
||||
> Do not update this as a live ledger. [STATE.md](STATE.md) links the verified integrated result.
|
||||
> The historical text below is preserved; its old active-status wording is not current guidance.
|
||||
> 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 by Codex-9e8r6t `/root`. Full release not accepted.
|
||||
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.
|
||||
|
||||
User resumed after enabling virtualization and explicitly authorized installing
|
||||
Ubuntu26.04 in WSL2. [RESTART_HANDOFF.md](RESTART_HANDOFF.md) is the historical
|
||||
pause; STATE.md owns current capacity, compiler and native status.
|
||||
User also explicitly permits disk cleanup including caches. Both scoped cache
|
||||
deletion commands were rejected before execution; nondeleting compression
|
||||
recovered space with all282 generated-binary hashes unchanged. Ordinary Ubuntu
|
||||
installation and an initial actual WSL2 root shell passed. Current VirtioProxy
|
||||
startups fail before guest execution, and a distinct WslService restart was
|
||||
denied by OS permissions. Owner328 remains the newest WSL solution after the
|
||||
latest issue check through348; its exact configuration is already applied.
|
||||
Five Rust components and60 apt packages are downloaded and hash-verified;
|
||||
guest signature verification/install and application acceptance remain pending.
|
||||
The user's latest direction pauses Linux work until they notify us that WSL
|
||||
networking is resolved. Continue Windows/product work; preserve existing Linux
|
||||
configuration, verified inputs and evidence without further setup/checks.
|
||||
## Goal and given requirements
|
||||
|
||||
## Goal and success criteria
|
||||
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.
|
||||
|
||||
[PROJECT_GOAL.md](../PROJECT_GOAL.md), copied from attachment
|
||||
`815ff7b9-73c4-41a2-a809-635ecf8389d6/goal-objective.md`, is authoritative.
|
||||
Deliver a usable, substantially complete OA/SA systems modeling product in
|
||||
Rust/Iced on Windows and Linux, with equivalent graphical/textual/REST editing,
|
||||
strict reST semantics/API, arbitrary development-artifact traceability and
|
||||
navigable offline help. Constraints/calculations, quantities/units and reusable
|
||||
model libraries and complete uncluttered text-only small/large modeling are
|
||||
explicit requirements. Later Arcadia layers remain future
|
||||
releases. Completion requires real engineering tasks, sound semantics, relevant
|
||||
passing checks and fresh independent cross-disciplinary review.
|
||||
|
||||
## Given facts
|
||||
|
||||
- User prioritizes Capella's domain capability and useful visual nesting while
|
||||
rejecting clutter, intrusive creation rows, static behavior diagrams and long
|
||||
homogeneous option lists. Reduce cognitive load in small and large models.
|
||||
- Source must be understandable to engineers and AI, work with Git and preserve
|
||||
full parity. OA/SA transitions in both directions must be clear and reviewed.
|
||||
- User authorizes autonomous reversible work, research, native Computer Use and
|
||||
parallel agents. Use GPT-6 Astra exclusively with task-appropriate effort.
|
||||
- Gitea coordination and shared Wiki updates are required. Identify host and
|
||||
worker consistently and preserve overlapping work. Explicit project messages
|
||||
are authorized; unrelated external messages are not.
|
||||
- Updated AGENTS requires runnable versioned milestone prereleases. User
|
||||
issue 1 comment 178 authorizes reusing existing MCP credentials programmatically for
|
||||
Git push and REST asset upload, without printing or committing credentials.
|
||||
- Remote AGENTS.md, blob `4b8465c8993107062ad2974c15a958d449530e92`, adds concise
|
||||
harness/Gitea maintenance and preservation of useful historical knowledge
|
||||
(user comment 229). Root's instruction sync `07eb3cd` is integrated;
|
||||
contextual source `df6f595`, CLI publication `d95b1ab` and OA native evidence
|
||||
`c4bad4b` are committed and pushed.
|
||||
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.
|
||||
|
||||
## Verified facts
|
||||
|
||||
[STATE.md](STATE.md) records exact commits, live leases and scoped verification.
|
||||
The current model has one typed core, validated atomic commands and coordinated
|
||||
Project/SourceBundle history shared by desktop and REST. Included files share
|
||||
one global namespace. Current Constraint expressions are strings; numeric unit
|
||||
strings do not convert values or establish dimensional compatibility. There is
|
||||
no locally integrated package/version/instance/update contract for cross-project
|
||||
libraries; peer implementation is tracked separately below.
|
||||
- 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 main basic/transition/history/reopen story passed exact independent model
|
||||
and four/six-source checks; supplementary native cases remain. Named creation
|
||||
passed 7/8/9-source oracles, one Undo/Redo and byte-exact reopen at actual minimum
|
||||
size. Cancellation retained its six-file baseline; SA draft recovery retained
|
||||
fields/identities, autofocus and Escape/Save As behavior. The D4D9 blank-canvas
|
||||
finding motivated the newer first-action implementation. Detailed historical
|
||||
suite and native scopes remain in [STATE.md](STATE.md) and its linked reports.
|
||||
Mounted geometry and bounded package passes do not establish full release
|
||||
acceptance. This local host has installed Ubuntu26.04 but lacks a currently
|
||||
usable guest session, installed build prerequisites or Linux application pass.
|
||||
## Assumptions and unresolved questions
|
||||
|
||||
Current grammar supports omitted initial IDs/defaults, lexical ownership, forward
|
||||
references and preserved source. The earlier desktop audit found no exposed
|
||||
project-wide reference navigation/alias refactor; its Find searched the active
|
||||
buffer. The later CLI supplies typed alias rename. The audited process example
|
||||
had 128 lines, 14 UUIDs and 31 empty-default lines, while meaningful shorter
|
||||
source is possible. Two concise snippets passed 26 real REST requests,
|
||||
including identity-only normalization, exact name-only edits, rejected writes and
|
||||
restart persistence. Counts and this small trial prove neither improved native
|
||||
usability nor large-project performance.
|
||||
- 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.
|
||||
|
||||
CLI tooling is implemented, integrated and published: semantic source `01b76a1`,
|
||||
packaging assembly `a0a48e8`, release integration `27df4a3`. Author 12 tests and
|
||||
independent 183 semantic assertions pass; independent clean-package review adds
|
||||
156 assertions across 10 processes. This is a bounded source-editing tool, not
|
||||
proof of complete large-model language ergonomics.
|
||||
## Candidate designs and selected directions
|
||||
|
||||
GUI/server release 1 and standalone CLI release 2 are published with runnable
|
||||
Windows artifacts, full dependency notices and downloaded asset verification.
|
||||
Independent archive/source and clean runtime checks pass in their declared scopes.
|
||||
See [CLI publication evidence](release-cli-prerelease-published-20260911.md).
|
||||
The newer 13-file contextual-source slice is committed/pushed as `df6f595`,
|
||||
with its independent oracle and exact raw evidence. It passed focused 23, guide 3, strict
|
||||
Clippy, matching GUI build and formatting. The full 675 suite covers only its
|
||||
predecessor; no full 676 suite has run. Root launched the exact 52DE82 GUI in
|
||||
isolation: Lifeline Save-adoption and Chain direct Create pass complete source,
|
||||
one Undo and one Redo. Machine direct Create and one Undo pass; Redo/reopen are
|
||||
pending for Redo. An isolated postboot real Open/SA view/Save passed the complete
|
||||
machine prediction and exact six-file equality; the app closed normally.
|
||||
Later layout/caption source was independently reviewed. Postboot focused3 ran25
|
||||
passing tests and one overflowing-fixture assertion failure. The stronger
|
||||
test-only fixture is frozen; its execution and new GUI remain pending.
|
||||
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.
|
||||
|
||||
Local OA B2 now passes all four Action/State/explicitly bound Reuse/Fragment
|
||||
Create, one Undo/Save, one Redo/Save and final OA reopen/Save against independent
|
||||
complete model/two-source predictions and original exact identities. SA B2 is
|
||||
pending locally. Existing B3 OA wide/minimum and SA minimum return scopes remain
|
||||
unchanged; neither these passes nor mounted geometry tests imply broader acceptance.
|
||||
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.
|
||||
|
||||
## Assumptions
|
||||
- Contextual named creation, remaining-height canvas and local source insertion
|
||||
can reduce actual authoring detours. Native engineer tasks must establish this.
|
||||
- A bounded pure calculation/quantity model and pinned library content may fit
|
||||
the existing typed core better than a general-purpose metamodel rewrite.
|
||||
Peer-ratified core staging must still be checked against independent engineering
|
||||
tasks and integrated across all required representations.
|
||||
- Existing OA/SA semantics provide a useful foundation; neither their presence
|
||||
nor old acceptance proves full conformance to the updated release objective.
|
||||
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.
|
||||
|
||||
## Open questions
|
||||
## Retained implementation decisions
|
||||
|
||||
- Which precise calculation, quantity-kind/unit and library contracts satisfy
|
||||
practical budgets, reuse and update workflows without ambiguous defaults or
|
||||
implicit execution? How should they migrate and participate in transitions?
|
||||
- Which behavior workflows still force cancellation/navigation to create missing
|
||||
context, and which local actions best remove those detours?
|
||||
- Which text-only assistance best preserves engineering context and identity:
|
||||
native language services, external-editor services, source recipes or explicit
|
||||
reviewed patch operations? Which grammar changes are actually necessary?
|
||||
- Does the new scenario pane remain readable and editable through actual native
|
||||
window sizes, camera changes, nested fragments and reopened caller contexts?
|
||||
- Which viable Linux environment can run current GUI/server acceptance? Current
|
||||
VM limitation does not block remaining Windows, semantics or source work.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
## Candidate designs and decisions
|
||||
## Current strategy and coordination
|
||||
|
||||
- Behavior creation: compact New → three task choices → focused name/owner/
|
||||
viewpoint card, explicit missing inputs, atomic creation and reserved recovery
|
||||
IDs. Implemented, reviewed and scoped native creation/history/reopen passed.
|
||||
- Contextual prerequisite creation: stage an explicit typed source inside the
|
||||
current parent draft, then validate/adopt source and consumer atomically. This
|
||||
preserves intent and one-step history; a separate Architecture excursion or
|
||||
early source commit loses those guarantees. Initial Lifeline/process Work/
|
||||
machine-owner adapters and semantic first-action prompts are implemented,
|
||||
reviewed and integrated with focused gates passed; scoped native results are
|
||||
above, with remaining acceptance still open. See the
|
||||
[bounded plan](release-contextual-source-creation-plan-20260911.md).
|
||||
- Scenario workspace: compact header, optional Tools/Selection shelves, actual
|
||||
remaining-height canvas/form, pinned actions, independent scrolling and
|
||||
explicit Fit/Focus. Mounted tests support this choice; native proof pending.
|
||||
- User issue2/comment250 clarifies that diagram manipulation may respect
|
||||
sequence/domain constraints. Codex-956atf owns a shared canvas-shell redesign;
|
||||
the existing controls and supporting source-card fixes do not close the issue.
|
||||
- Quantities/calculations: peer comment 231 records ratified schema 8/local
|
||||
definitions and actual shared analysis/core types under implementation in
|
||||
isolated `codex/analysis-core-20260911`. This is not a local schema admission,
|
||||
evaluator or graphical/source/API parity pass; the local core remains descriptive.
|
||||
- Libraries: compare copied source, live references, generic definition/usages
|
||||
and pinned typed packages with explicit mappings and reviewed updates.
|
||||
[Falsification brief](release-library-candidate-review-20260910.md) records
|
||||
identity, dependency, offline and conflict requirements. Refined peer baseline,
|
||||
Case, full-freeze and exact-pin/equivalence contracts are ratified externally;
|
||||
their local integration and executable lifecycle acceptance remain pending.
|
||||
- Text authoring: use existing omission/default/identity contracts for concise
|
||||
experiments; compare source navigation/refactoring/patch assistance before
|
||||
grammar redesign. No namespace, hidden identity or omission change ratified.
|
||||
- Reference chooser: peer comment 224's corrected focus ownership, nested-scroll
|
||||
reveal and mounted regressions are prepared, not executed. Preserve that
|
||||
ownership and wait for its source/test handoff.
|
||||
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.
|
||||
|
||||
Long-lived decisions remain in [DECISIONS.md](DECISIONS.md). New user scope and
|
||||
normative contracts supersede older prototype exclusions.
|
||||
|
||||
## Current strategy
|
||||
|
||||
Finish coherent creation gates and test the rebuilt product against independent
|
||||
saved-source oracles. Close remaining scenario lifecycle integration boundaries.
|
||||
Use the native Capella study and independent criticism to address real authoring
|
||||
friction. In parallel, resolve foundational semantic additions and text-only
|
||||
authoring through bounded competing designs and executable experiments. Then implement all representations,
|
||||
verify real REST and Windows/Linux workflows, build actual optimized packages
|
||||
and conduct fresh end-state review. Do not weaken acceptance to match current
|
||||
implementation or stop because a narrow technical milestone passed.
|
||||
|
||||
Pre-consolidation content is preserved byte-for-byte in
|
||||
[the dated archive](history/2026-09-10-ledgers-before-second-consolidation/TASK_LEDGER.md).
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
//! 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"),
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
# Authoring prerelease evidence
|
||||
|
||||
Exact preserved producer, independent reviewer and root native evidence for
|
||||
Windows preview `v0.2.0-preview.authoring.20260911`. `hashes.json` records each
|
||||
copied artifact's original private path, length and SHA256. Git must retain these
|
||||
measured bytes. The executable ZIP and validation notes are release assets.
|
||||
|
||||
The preserved Python files are the original host-specific harnesses. Their
|
||||
relative paths and source/toolchain prerequisites refer to the recorded original
|
||||
`.local` layout; they are not portable standalone tests when run from this archive.
|
||||
To reproduce them, restore that layout in a disposable checkout and use fresh
|
||||
output directories. They never contain Gitea credentials. The GUI launch script
|
||||
only launches the application; native actions were performed using Computer Use.
|
||||
|
||||
See the three `harness/release-authoring-prerelease-*-20260911.md` reports for
|
||||
methods, initial GUI harness failure, passed checks, source provenance and limits.
|
||||
`published-verified.json` records the final release readback; upload records bind
|
||||
the exact downloaded assets. Publication does not complete the wider OA/SA goal.
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"pid": 10800,
|
||||
"exe": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\authoring-prerelease-gui-20260911-v2\\extracted\\ArcadiaStudio-0.2.0-preview.authoring.20260911-x86_64-pc-windows-gnullvm\\arcadia-studio.exe",
|
||||
"archive_sha256": "664f30b6217689aca1492eb263b3d15777067d732fa890eeb028f435209dadfa",
|
||||
"files": 559,
|
||||
"path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\authoring-prerelease-gui-20260911-v2\\extracted\\ArcadiaStudio-0.2.0-preview.authoring.20260911-x86_64-pc-windows-gnullvm;C:\\Windows\\System32;C:\\Windows",
|
||||
"local_appdata": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\authoring-prerelease-gui-20260911-v2\\profile\\local",
|
||||
"appdata": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\authoring-prerelease-gui-20260911-v2\\profile\\roaming"
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import zipfile
|
||||
|
||||
root = Path.cwd()
|
||||
name = 'ArcadiaStudio-0.2.0-preview.authoring.20260911-x86_64-pc-windows-gnullvm'
|
||||
archive = root / '.local/authoring-prerelease-20260911/packages' / (name + '.zip')
|
||||
assert hashlib.sha256(archive.read_bytes()).hexdigest() == '664f30b6217689aca1492eb263b3d15777067d732fa890eeb028f435209dadfa'
|
||||
trial = root / '.local/authoring-prerelease-gui-20260911-v2'
|
||||
trial.mkdir(exist_ok=False)
|
||||
extract = trial / 'extracted'
|
||||
extract.mkdir()
|
||||
with zipfile.ZipFile(archive) as z:
|
||||
names = z.namelist()
|
||||
assert len(names) == len(set(n.casefold() for n in names)) == 559
|
||||
assert all(n.startswith(name + '/') and '..' not in Path(n).parts for n in names)
|
||||
z.extractall(extract)
|
||||
package = extract / name
|
||||
for line in (package / 'SHA256SUMS.txt').read_text().splitlines():
|
||||
digest, path = line.split(' ', 1)
|
||||
assert hashlib.sha256((package / path).read_bytes()).hexdigest() == digest, path
|
||||
for folder in ['profile/local', 'profile/roaming']:
|
||||
(trial / folder).mkdir(parents=True)
|
||||
env = os.environ.copy()
|
||||
env['LOCALAPPDATA'] = str(trial / 'profile/local')
|
||||
env['APPDATA'] = str(trial / 'profile/roaming')
|
||||
env['PATH'] = os.pathsep.join([str(package), str(Path(env['SYSTEMROOT']) / 'System32'), env['SYSTEMROOT']])
|
||||
exe = package / 'arcadia-studio.exe'
|
||||
assert hashlib.sha256(exe.read_bytes()).hexdigest() == 'd4d9c2bd937e301ff1009d7cb93a34ee5aa6dfc837ff7e87d468c85371b20c31'
|
||||
with (trial / 'stdout.log').open('wb') as out, (trial / 'stderr.log').open('wb') as err:
|
||||
process = subprocess.Popen([str(exe)], cwd=package, env=env, stdout=out, stderr=err)
|
||||
result = {'pid': process.pid, 'exe': str(exe), 'archive_sha256': hashlib.sha256(archive.read_bytes()).hexdigest(), 'files': len(names), 'path': env['PATH'], 'local_appdata': env['LOCALAPPDATA'], 'appdata': env['APPDATA']}
|
||||
(trial / 'launch.json').write_text(json.dumps(result, indent=2), encoding='utf-8')
|
||||
print(json.dumps(result, indent=2))
|
||||
@@ -1,64 +0,0 @@
|
||||
[
|
||||
"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\authoring-prerelease-gui-20260911-v2\\extracted\\ArcadiaStudio-0.2.0-preview.authoring.20260911-x86_64-pc-windows-gnullvm\\arcadia-studio.exe",
|
||||
"C:\\Windows\\SYSTEM32\\ntdll.dll",
|
||||
"C:\\Windows\\System32\\KERNEL32.DLL",
|
||||
"C:\\Windows\\System32\\KERNELBASE.dll",
|
||||
"C:\\Windows\\SYSTEM32\\apphelp.dll",
|
||||
"C:\\Windows\\System32\\user32.dll",
|
||||
"C:\\Windows\\System32\\win32u.dll",
|
||||
"C:\\Windows\\System32\\GDI32.dll",
|
||||
"C:\\Windows\\System32\\gdi32full.dll",
|
||||
"C:\\Windows\\System32\\msvcp_win.dll",
|
||||
"C:\\Windows\\System32\\ucrtbase.dll",
|
||||
"C:\\Windows\\System32\\ole32.dll",
|
||||
"C:\\Windows\\System32\\combase.dll",
|
||||
"C:\\Windows\\System32\\RPCRT4.dll",
|
||||
"C:\\Windows\\System32\\advapi32.dll",
|
||||
"C:\\Windows\\System32\\msvcrt.dll",
|
||||
"C:\\Windows\\System32\\sechost.dll",
|
||||
"C:\\Windows\\System32\\bcryptprimitives.dll",
|
||||
"C:\\Windows\\System32\\shell32.dll",
|
||||
"C:\\Windows\\System32\\imm32.dll",
|
||||
"C:\\Windows\\System32\\ws2_32.dll",
|
||||
"C:\\Windows\\System32\\oleaut32.dll",
|
||||
"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\authoring-prerelease-gui-20260911-v2\\extracted\\ArcadiaStudio-0.2.0-preview.authoring.20260911-x86_64-pc-windows-gnullvm\\libunwind.dll",
|
||||
"C:\\Windows\\SYSTEM32\\bcrypt.dll",
|
||||
"C:\\Windows\\SYSTEM32\\dwmapi.dll",
|
||||
"C:\\Windows\\SYSTEM32\\uxtheme.dll",
|
||||
"C:\\Windows\\SYSTEM32\\opengl32.dll",
|
||||
"C:\\Windows\\SYSTEM32\\GLU32.dll",
|
||||
"C:\\Windows\\SYSTEM32\\CRYPTBASE.DLL",
|
||||
"C:\\Windows\\SYSTEM32\\dxcore.dll",
|
||||
"C:\\Windows\\System32\\MSCTF.dll",
|
||||
"C:\\Windows\\SYSTEM32\\kernel.appcore.dll",
|
||||
"C:\\Windows\\System32\\clbcatq.dll",
|
||||
"C:\\Windows\\system32\\dataexchange.dll",
|
||||
"C:\\Windows\\system32\\twinapi.appcore.dll",
|
||||
"C:\\Windows\\system32\\explorerframe.dll",
|
||||
"C:\\Windows\\System32\\SHCORE.dll",
|
||||
"C:\\Windows\\System32\\SHLWAPI.dll",
|
||||
"C:\\Windows\\System32\\Windows.Storage.dll",
|
||||
"C:\\Windows\\SYSTEM32\\powrprof.dll",
|
||||
"C:\\Windows\\SYSTEM32\\UMPDC.dll",
|
||||
"C:\\Windows\\SYSTEM32\\CFGMGR32.dll",
|
||||
"C:\\Windows\\system32\\dxgi.dll",
|
||||
"C:\\Windows\\SYSTEM32\\windows.staterepositorycore.dll",
|
||||
"C:\\Windows\\SYSTEM32\\directxdatabasehelper.dll",
|
||||
"C:\\Windows\\SYSTEM32\\d3d12.dll",
|
||||
"C:\\Windows\\SYSTEM32\\d3dcompiler_47.dll",
|
||||
"C:\\Windows\\SYSTEM32\\cryptsp.dll",
|
||||
"C:\\Windows\\System32\\DriverStore\\FileRepository\\vm3d.inf_amd64_39d3dfd189710365\\vm3dgl64.dll",
|
||||
"C:\\Windows\\System32\\DriverStore\\FileRepository\\vm3d.inf_amd64_39d3dfd189710365\\vm3dglhelper64.dll",
|
||||
"C:\\Windows\\SYSTEM32\\WINMM.dll",
|
||||
"C:\\Windows\\SYSTEM32\\D3D12Core.dll",
|
||||
"C:\\Windows\\SYSTEM32\\d3d10warp.dll",
|
||||
"C:\\Windows\\SYSTEM32\\D3DSCache.dll",
|
||||
"C:\\Windows\\SYSTEM32\\USERENV.dll",
|
||||
"C:\\Windows\\SYSTEM32\\dxilconv.dll",
|
||||
"C:\\Windows\\SYSTEM32\\dcomp.dll",
|
||||
"C:\\Windows\\SYSTEM32\\Microsoft.Internal.WarpPal.dll",
|
||||
"C:\\Windows\\SYSTEM32\\textinputframework.dll",
|
||||
"C:\\Windows\\SYSTEM32\\uiautomationcore.dll",
|
||||
"C:\\Windows\\SYSTEM32\\OLEACC.dll",
|
||||
"C:\\Windows\\System32\\threadpoolwinrt.dll"
|
||||
]
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"matched_files": 558,
|
||||
"exact_physical_inventory": 559,
|
||||
"result": "PASS after native packaged walkthrough open and Save"
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
[
|
||||
{
|
||||
"path": "producer-inventory.json",
|
||||
"original": ".local/authoring-prerelease-20260911/inventory.json",
|
||||
"bytes": 66465,
|
||||
"sha256": "3c78dea4e93dae186fd24ab826c6680178a2c7849c270a22deae2ff732f1857b"
|
||||
},
|
||||
{
|
||||
"path": "producer-build-evidence.json",
|
||||
"original": ".local/authoring-prerelease-20260911/build-evidence.json",
|
||||
"bytes": 1230,
|
||||
"sha256": "ccc1ccd087f1de9605ba23a4680e1cb6d37ce5ccf0f8db03c03c4797ae32db25"
|
||||
},
|
||||
{
|
||||
"path": "producer-package-result.json",
|
||||
"original": ".local/authoring-prerelease-20260911/package-initial-result.json",
|
||||
"bytes": 216,
|
||||
"sha256": "45f372960931fa34ca75086df5d346eab9127a7ae70b53aed86751e372c24bae"
|
||||
},
|
||||
{
|
||||
"path": "producer-roundtrip.json",
|
||||
"original": ".local/authoring-prerelease-20260911/committed-roundtrip.json",
|
||||
"bytes": 40443,
|
||||
"sha256": "27e4cd2d6130a9ced76560cee130b5d363478ee0da62925e785691c63fadb098"
|
||||
},
|
||||
{
|
||||
"path": "reviewer-artifact-result.json",
|
||||
"original": ".local/authoring-prerelease-review-20260911/artifact-result.json",
|
||||
"bytes": 338784,
|
||||
"sha256": "8b20e9d294338754ddc9b3320b59f55fc19ab979b8da18819fb47a8036db0bba"
|
||||
},
|
||||
{
|
||||
"path": "reviewer-server-result.json",
|
||||
"original": ".local/authoring-prerelease-review-20260911/server-result.json",
|
||||
"bytes": 128779,
|
||||
"sha256": "086bf21e79f3bc7deddfd3278c8ade9b470b11031f7fb278ca0ead3a75b6cfb1"
|
||||
},
|
||||
{
|
||||
"path": "reviewer.py",
|
||||
"original": ".local/authoring-prerelease-review-20260911/review.py",
|
||||
"bytes": 21434,
|
||||
"sha256": "af9eb20946dfcec78bf8959f1d5dc175e82ebb9fabba9f94cefe590fea516b92"
|
||||
},
|
||||
{
|
||||
"path": "gui-launch.json",
|
||||
"original": ".local/authoring-prerelease-gui-20260911-v2/launch.json",
|
||||
"bytes": 827,
|
||||
"sha256": "ade93cb1abea623f1cc63b8124857cad822e4f75e8d1c389f75fd8a9089b2c39"
|
||||
},
|
||||
{
|
||||
"path": "gui-loaded-modules.json",
|
||||
"original": ".local/authoring-prerelease-gui-20260911-v2/loaded-modules.json",
|
||||
"bytes": 3123,
|
||||
"sha256": "ccae3d91310644e171bf35028f1c0758ef330ebb051983e0524b8cd714652b6b"
|
||||
},
|
||||
{
|
||||
"path": "gui-post-save-exact.json",
|
||||
"original": ".local/authoring-prerelease-gui-20260911-v2/post-save-package-exact.json",
|
||||
"bytes": 133,
|
||||
"sha256": "e340289a2c09058c539e63de33a361326b146fc181a8bd1349a88bfc813f2420"
|
||||
},
|
||||
{
|
||||
"path": "upload-binaries-verified.json",
|
||||
"original": ".local/authoring-prerelease-20260911/upload-binaries-verified.json",
|
||||
"bytes": 685,
|
||||
"sha256": "16799aa759e7287f4c50af1068d311f6f916f97e0cfbd7aaddf7432770d281f3"
|
||||
},
|
||||
{
|
||||
"path": "gui-launch.py",
|
||||
"original": ".local/authoring-prerelease-gui-20260911.py",
|
||||
"bytes": 2001,
|
||||
"sha256": "db256498e44cbb67cd84738a1a37f0e9ab0931d130a3b8c830af2c02cff0924c"
|
||||
},
|
||||
{
|
||||
"path": "home.png",
|
||||
"original": ".local/authoring-prerelease-gui-20260911-v2/home.png",
|
||||
"bytes": 97695,
|
||||
"sha256": "e9c24489e949096347b11234d8d2d343716fd5895903c4978d139107a70e3e5a"
|
||||
},
|
||||
{
|
||||
"path": "oa-walkthrough-save.png",
|
||||
"original": ".local/authoring-prerelease-gui-20260911-v2/oa-walkthrough-save.png",
|
||||
"bytes": 131037,
|
||||
"sha256": "61b2cbdd2dddb6fa176000198ae845554147cbc80abe52fa992211157f6297b5"
|
||||
},
|
||||
{
|
||||
"path": "sa-walkthrough.png",
|
||||
"original": ".local/authoring-prerelease-gui-20260911-v2/sa-walkthrough.png",
|
||||
"bytes": 132180,
|
||||
"sha256": "7f3a2bbed9fa69f9d5d844f9a52f81fbc3928244f49d7a9ff1e475c3f2411dbf"
|
||||
},
|
||||
{
|
||||
"path": "state-workspace.png",
|
||||
"original": ".local/authoring-prerelease-gui-20260911-v2/state-workspace.png",
|
||||
"bytes": 135827,
|
||||
"sha256": "79010527e5e1bb8ccf428788a00dea80e6d8253f3d7c249089218e2df9edd6da"
|
||||
},
|
||||
{
|
||||
"path": "offline-guide.png",
|
||||
"original": ".local/authoring-prerelease-gui-20260911-v2/offline-guide.png",
|
||||
"bytes": 185496,
|
||||
"sha256": "58812d2bd0d6e73daa5187f2dd87564189960387ac4bb1e399af4b8d114c05df"
|
||||
},
|
||||
{
|
||||
"path": "published-verified.json",
|
||||
"original": ".local/authoring-prerelease-20260911/published-verified.json",
|
||||
"bytes": 374,
|
||||
"sha256": "668f28c4d8cf753386085271eb181513b0127940f324f423a2b87614ec8cc85a"
|
||||
},
|
||||
{
|
||||
"path": "upload-validation-verified.json",
|
||||
"original": ".local/authoring-prerelease-20260911/upload-validation-verified.json",
|
||||
"bytes": 277,
|
||||
"sha256": "f39859baea544662fc4bdd1fe14dbcb9bdab5639b173754a34b58b853f0d47a7"
|
||||
}
|
||||
]
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 95 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 128 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 181 KiB |
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"source_identity": "Exact-byte reconstructed input snapshot 234e01e27d6935d213097b345d91d369f808af82, derived from release base 2ec72d4241fa8971fa6fe9f626937de9dfd7c7eb by seven documented LF-to-CRLF restorations and preservation attributes. All 253 GUI manifest inputs and 120 preserved server source files matched. This derived commit is not claimed as the original build commit; GUI and server original build records are retained separately.",
|
||||
"profile": "Prebuilt Windows debug/development artifacts: GUI cargo build --locked -p oa-desktop --bin arcadia-studio, September 11 2026; preserved server from September 7 workspace bins build. Not a release-profile or reproducible-build claim.",
|
||||
"compiler": "rustc 1.98.1, commit 48a229ceaefd4985c50990b14116b6d856af0985, x86_64-pc-windows-gnullvm, LLVM 22.1.8. Current rustc -vV, all 58 prior-notice target-library hashes and both embedded compiler paths matched; original recorded build evidence retained.",
|
||||
"target": "x86_64-pc-windows-gnullvm",
|
||||
"binary_sha256": {
|
||||
"arcadia-studio.exe": "d4d9c2bd937e301ff1009d7cb93a34ee5aa6dfc837ff7e87d468c85371b20c31",
|
||||
"arcadia-server.exe": "efeeb89c0d2e0f93df4472cb3e9f48b2b722a37ae1043c90a5a56f2e845fe665"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"started": "2026-09-11T12:17:30.7730570Z",
|
||||
"source_commit": "234e01e27d6935d213097b345d91d369f808af82",
|
||||
"success": true,
|
||||
"finished": "2026-09-11T12:21:23.0302800Z",
|
||||
"elapsed_seconds": 232.2812442
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"id": 1,
|
||||
"tag": "v0.2.0-preview.authoring.20260911",
|
||||
"url": "http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/releases/tag/v0.2.0-preview.authoring.20260911",
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"asset_count": 3,
|
||||
"notes_sha256": "e48399db78b18c6bd779412f7aeccf13fe78bf8c9bac22a4c418755acfab175c",
|
||||
"published_at": "2026-09-11T12:40:51Z"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,335 +0,0 @@
|
||||
"""Independent frozen archive and packaged-server checks; never runs Cargo or GUI."""
|
||||
from pathlib import Path, PurePosixPath
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import stat
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import time
|
||||
import tomllib
|
||||
import traceback
|
||||
import urllib.request
|
||||
import uuid
|
||||
import zipfile
|
||||
|
||||
OUT = Path(__file__).resolve().parent
|
||||
ROOT = OUT.parents[1]
|
||||
PREP = ROOT / '.local/authoring-prerelease-20260911'
|
||||
NAME = 'ArcadiaStudio-0.2.0-preview.authoring.20260911-x86_64-pc-windows-gnullvm'
|
||||
ZIP = PREP / 'packages' / (NAME + '.zip')
|
||||
EXTRACT = OUT / 'extracted'
|
||||
PACKAGE = EXTRACT / NAME
|
||||
SOURCE = PREP / 'source'
|
||||
SOURCE_COMMIT = '234e01e27d6935d213097b345d91d369f808af82'
|
||||
BASE_COMMIT = '2ec72d4241fa8971fa6fe9f626937de9dfd7c7eb'
|
||||
EXPECTED_ZIP = '664f30b6217689aca1492eb263b3d15777067d732fa890eeb028f435209dadfa'
|
||||
BINARIES = {
|
||||
'arcadia-studio.exe': 'd4d9c2bd937e301ff1009d7cb93a34ee5aa6dfc837ff7e87d468c85371b20c31',
|
||||
'arcadia-server.exe': 'efeeb89c0d2e0f93df4472cb3e9f48b2b722a37ae1043c90a5a56f2e845fe665',
|
||||
'libunwind.dll': 'f45604a12e8bcd839fd86f667c1be1cc7c4f9da2b3c69ff381ac708f0e1ae7d4',
|
||||
}
|
||||
RESULT = {'checks': [], 'scope': sys.argv[1], 'started_utc': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}
|
||||
|
||||
|
||||
def check(condition, label):
|
||||
if not condition:
|
||||
raise AssertionError(label)
|
||||
RESULT['checks'].append(label)
|
||||
|
||||
|
||||
def sha(data):
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def read_json(path):
|
||||
return json.loads(path.read_text(encoding='utf-8-sig'))
|
||||
|
||||
|
||||
def write_json(path, value):
|
||||
with path.open('x', encoding='utf-8', newline='\n') as f:
|
||||
json.dump(value, f, indent=2, ensure_ascii=False)
|
||||
f.write('\n')
|
||||
|
||||
|
||||
def file_map(directory):
|
||||
result = {}
|
||||
for p in directory.rglob('*'):
|
||||
check(not p.is_symlink() and not (getattr(p.lstat(), 'st_file_attributes', 0) & 0x400), 'ordinary path: ' + p.relative_to(directory).as_posix())
|
||||
if p.is_file():
|
||||
result[p.relative_to(directory).as_posix()] = sha(p.read_bytes())
|
||||
return result
|
||||
|
||||
|
||||
def safe_name(name):
|
||||
p = PurePosixPath(name)
|
||||
return not p.is_absolute() and p.as_posix() == name and '\\' not in name and ':' not in name and all(c not in ('', '.', '..') and not c.endswith(('.', ' ')) and not re.match(r'(?i)^(con|nul|prn|aux|com[1-9]|lpt[1-9])(\.|$)', c) for c in p.parts)
|
||||
|
||||
|
||||
def git_blobs(ref, paths):
|
||||
raw = subprocess.run(['git', 'cat-file', '--batch'], input=''.join(ref + ':' + p + '\n' for p in paths).encode(), cwd=ROOT, capture_output=True, check=True).stdout
|
||||
result, cursor = {}, 0
|
||||
for p in paths:
|
||||
end = raw.index(b'\n', cursor)
|
||||
header = raw[cursor:end].split()
|
||||
check(len(header) == 3 and header[1] == b'blob', 'Git blob exists: ' + ref[:7] + ':' + p)
|
||||
size = int(header[2])
|
||||
result[p] = raw[end + 1:end + 1 + size]
|
||||
cursor = end + 2 + size
|
||||
check(cursor == len(raw), 'Git batch framing complete: ' + ref[:7])
|
||||
return result
|
||||
|
||||
|
||||
def artifact():
|
||||
check(not EXTRACT.exists(), 'clean extraction destination absent')
|
||||
check(ZIP.stat().st_size == 32618262 and sha(ZIP.read_bytes()) == EXPECTED_ZIP, 'frozen ZIP size and SHA256')
|
||||
with zipfile.ZipFile(ZIP) as archive:
|
||||
infos = archive.infolist()
|
||||
names = [i.filename for i in infos]
|
||||
check(len(infos) == 559, '559 ZIP members')
|
||||
check(len({n.casefold() for n in names}) == len(names), 'no duplicate or case-colliding ZIP names')
|
||||
check(all(safe_name(n) and n.startswith(NAME + '/') for n in names), 'one safe ZIP root without traversal or Windows special paths')
|
||||
check(all(not i.is_dir() and stat.S_IFMT(i.external_attr >> 16) in (0, stat.S_IFREG) for i in infos), 'ordinary file-only ZIP entries')
|
||||
check(all(i.date_time == (2000, 1, 1, 0, 0, 0) and i.flag_bits & 1 == 0 for i in infos), 'fixed timestamps and no encrypted entries')
|
||||
check(archive.testzip() is None, 'all ZIP CRCs')
|
||||
EXTRACT.mkdir()
|
||||
archive.extractall(EXTRACT)
|
||||
archive_hashes = {i.filename[len(NAME) + 1:]: sha(archive.read(i)) for i in infos}
|
||||
actual = file_map(PACKAGE)
|
||||
check(actual == archive_hashes, 'complete extracted file set and bytes equal ZIP')
|
||||
sums = {}
|
||||
for line in (PACKAGE / 'SHA256SUMS.txt').read_text().splitlines():
|
||||
match = re.fullmatch(r'([0-9a-f]{64}) (.+)', line)
|
||||
check(match is not None and safe_name(match[2]) and match[2] not in sums, 'well-formed unique checksum row')
|
||||
sums[match[2]] = match[1]
|
||||
check(sums == {p: h for p, h in actual.items() if p != 'SHA256SUMS.txt'}, 'checksum inventory covers every other file exactly')
|
||||
package = read_json(PACKAGE / 'package.json')
|
||||
inputs = package['input_sha256']
|
||||
generated = {'SHA256SUMS.txt', 'START-HERE.txt', 'THIRD-PARTY-NOTICES.txt', 'dependency-notices.json', 'documentation-check.json', 'package.json'}
|
||||
check(set(actual) - set(inputs) == generated and all(actual[p] == h.lower() for p, h in inputs.items()), '553 package input hashes plus exactly six generated files')
|
||||
check(package['version'] == '0.2.0-preview.authoring.20260911' and package['target'] == 'x86_64-pc-windows-gnullvm', 'version and target')
|
||||
check(all(actual[p] == h for p, h in BINARIES.items()), 'exact GUI, server and runtime hashes')
|
||||
for p in BINARIES:
|
||||
data = (PACKAGE / p).read_bytes()
|
||||
offset = struct.unpack_from('<I', data, 0x3c)[0]
|
||||
check(data[:2] == b'MZ' and data[offset:offset + 4] == b'PE\0\0' and struct.unpack_from('<H', data, offset + 4)[0] == 0x8664, 'ordinary PE x86_64: ' + p)
|
||||
check({p for p in actual if '/' not in p} == {'arcadia-server.exe', 'arcadia-studio.exe', 'libunwind.dll', 'LICENSE', 'README.md'} | generated, 'only declared portable top-level files')
|
||||
check({p.split('/')[0] for p in actual if '/' in p} == {'deployment', 'docs', 'examples', 'licenses'}, 'no harness, private profile, source checkout or credential-directory payload')
|
||||
for p in actual:
|
||||
if p.startswith(('docs/', 'examples/', 'deployment/')) or p == 'LICENSE':
|
||||
check((SOURCE / p).is_file() and sha((SOURCE / p).read_bytes()) == actual[p], 'public source document/example bytes: ' + p)
|
||||
RESULT['archive'] = {'path': str(ZIP), 'sha256': EXPECTED_ZIP, 'bytes': ZIP.stat().st_size, 'file_count': len(actual), 'files': actual}
|
||||
RESULT['package_claims'] = {k: package[k] for k in ('version', 'target', 'acceptance', 'build', 'build_evidence')}
|
||||
provenance()
|
||||
notices(actual)
|
||||
|
||||
|
||||
def provenance():
|
||||
gui_path = ROOT / '.local/behavior-creation-20260911/gui-verified/manifest.json'
|
||||
gui = read_json(gui_path)
|
||||
expected = {i['path']: i['sha256'].lower() for i in gui['inputs']}
|
||||
check(len(expected) == 253, 'original GUI manifest has 253 unique inputs')
|
||||
derived = git_blobs(SOURCE_COMMIT, list(expected))
|
||||
base = git_blobs(BASE_COMMIT, list(expected))
|
||||
differences = []
|
||||
for p, h in expected.items():
|
||||
check(sha(derived[p]) == h == sha((SOURCE / p).read_bytes()), 'original GUI input equals derived Git blob and checkout: ' + p)
|
||||
if sha(base[p]) != h:
|
||||
check(sha(base[p].replace(b'\r\n', b'\n').replace(b'\n', b'\r\n')) == h, 'exact LF-to-CRLF-only restoration: ' + p)
|
||||
differences.append(p)
|
||||
check(set(differences) == {'docs/openapi.json'} | {'crates/oa-desktop/src/semantic_review/' + p + '.rs' for p in ('artifacts_view', 'attachments_view', 'keyed_rows', 'keyed_tests', 'keyed', 'ordered')}, 'exact seven documented CRLF restorations')
|
||||
delta = subprocess.run(['git', 'diff', '--name-only', '--ignore-space-at-eol', BASE_COMMIT, SOURCE_COMMIT], cwd=ROOT, check=True, capture_output=True).stdout.decode().splitlines()
|
||||
check(delta == ['.gitattributes'], 'derived commit changes only preservation attributes beyond EOL bytes')
|
||||
origin = read_json(ROOT / 'harness/experiments/textual-authoring-20260911-local/provenance.json')
|
||||
tar_path = ROOT / '.local/linux-qa/runtime/source7-20260907T082753190288Z.tar.gz'
|
||||
check(sha(tar_path.read_bytes()) == 'cbb371a93a562e1e641d7629fe351e016d63e66fc9112499b3fed6d5cd171f78', 'original server source tar identity')
|
||||
server_expected = {i['path']: i['snapshot_sha256'].lower() for i in origin['source_comparisons']}
|
||||
check(len(server_expected) == 120, '120 independently recorded server source files')
|
||||
server_base = git_blobs(BASE_COMMIT, list(server_expected))
|
||||
tar_manifest = read_json(tar_path.with_suffix('').with_suffix('.tar.manifest.json'))
|
||||
with tarfile.open(tar_path) as tar:
|
||||
members = {m.name.removeprefix('./'): m for m in tar.getmembers() if m.isfile()}
|
||||
for p, h in server_expected.items():
|
||||
check(sha(tar.extractfile(members[p]).read()) == h == sha(server_base[p]) == sha((SOURCE / p).read_bytes()), 'server recorded tar equals base Git and staged input: ' + p)
|
||||
shared = [p for p in members if p in ('Cargo.toml', 'Cargo.lock', 'docs/openapi.json') or p.startswith('build/')]
|
||||
for p in shared:
|
||||
check(sha(tar.extractfile(members[p]).read()) == expected[p] == tar_manifest['files'][p].lower(), 'original server/GUI shared manifest or build input: ' + p)
|
||||
evidence = read_json(PACKAGE / 'package.json')['build_evidence']['evidence']
|
||||
check(evidence['binary_sha256'] == {k: v for k, v in BINARIES.items() if k.endswith('.exe')}, 'packaged build evidence binds both exact executables')
|
||||
check(SOURCE_COMMIT in evidence['source_identity'] and BASE_COMMIT in evidence['source_identity'], 'evidence identifies auxiliary and base sources distinctly')
|
||||
check('not claimed as the original build commit' in evidence['source_identity'] and 'Not a release-profile or reproducible-build claim' in evidence['profile'], 'packaged provenance retains explicit limitations')
|
||||
RESULT['provenance'] = {'gui_manifest_sha256': sha(gui_path.read_bytes()), 'gui_build_record': gui['build'], 'historical_gui_head': gui['head'], 'gui_inputs': 253, 'source_commit': SOURCE_COMMIT, 'base_commit': BASE_COMMIT, 'eol_restorations': differences, 'server_source_files': 120, 'shared_input_files': shared, 'tar_sha256': sha(tar_path.read_bytes())}
|
||||
|
||||
|
||||
def notices(actual):
|
||||
notice = read_json(PACKAGE / 'dependency-notices.json')
|
||||
check(notice['root_packages'] == ['oa-desktop', 'oa-server'] and len(notice['packages']) == 266, '266 notice packages and exact desktop/server roots')
|
||||
check(notice['cargo_lock_sha256'].lower() == sha((SOURCE / 'Cargo.lock').read_bytes()), 'notice lock equals exact historical source lock')
|
||||
for p, h in notice['workspace_build_inputs_sha256'].items():
|
||||
check(sha((SOURCE / p).read_bytes()) == h, 'notice common build input: ' + p)
|
||||
lock = tomllib.loads((SOURCE / 'Cargo.lock').read_text())
|
||||
locked = {(p['name'], p['version'], p.get('source')): p for p in lock['package']}
|
||||
referenced = set()
|
||||
def refs(items):
|
||||
for i in items:
|
||||
p = i['path']
|
||||
check(safe_name(p) and p.startswith('licenses/') and actual.get(p) == i['sha256'].lower(), 'bundled exact notice reference: ' + p)
|
||||
referenced.add(p)
|
||||
fonts = []
|
||||
for p in notice['packages']:
|
||||
key = (p['name'], p['version'], p['source'])
|
||||
check(key in locked and p['registry_checksum'] == locked[key].get('checksum'), 'notice identity/checksum occurs in historical lock: ' + p['name'] + '@' + p['version'])
|
||||
check(len(p['notices']) > 0, 'package has notice text: ' + p['name'])
|
||||
refs(p['notices'])
|
||||
for f in p['embedded_fonts']:
|
||||
check(len(f['notices']) > 0, 'embedded font notice present: ' + p['name'] + '/' + f['path'])
|
||||
refs(f['notices'])
|
||||
fonts.append({'package': p['name'], 'path': f['path'], 'sha256': f['sha256'], 'license': f['license']})
|
||||
if p['source'] is None:
|
||||
directory = SOURCE / 'crates' / p['name']
|
||||
files = {f.relative_to(directory).as_posix(): sha(f.read_bytes()) for f in directory.rglob('*') if f.is_file() and 'target' not in f.relative_to(directory).parts and f.name != '.cargo-ok'}
|
||||
check(files == p['local_source_sha256'], 'complete ordinary local-crate inventory: ' + p['name'])
|
||||
refs(notice['rust']['notices'])
|
||||
refs(notice['runtime']['notices'])
|
||||
check(notice['runtime']['sha256'] == BINARIES['libunwind.dll'], 'runtime notice is bound to shipped DLL')
|
||||
library_dir = ROOT / '.local/toolchain/rustup/toolchains/stable-x86_64-pc-windows-gnullvm/lib/rustlib/x86_64-pc-windows-gnullvm/lib'
|
||||
check(len(notice['rust']['target_libraries_sha256']) == 58, '58 compiler target libraries recorded')
|
||||
for name, h in notice['rust']['target_libraries_sha256'].items():
|
||||
check(sha((library_dir / name).read_bytes()) == h, 'retained compiler target-library bytes: ' + name)
|
||||
RESULT['notices'] = {'packages': len(notice['packages']), 'roots': notice['root_packages'], 'lock_sha256': notice['cargo_lock_sha256'], 'notice_reference_files': len(referenced), 'additional_license_files': sorted({p for p in actual if p.startswith('licenses/')} - referenced), 'embedded_fonts': fonts, 'rust_version': notice['rust']['version'], 'runtime_provenance': notice['runtime']['provenance'], 'scope': notice['scope']}
|
||||
|
||||
|
||||
def server():
|
||||
prior = read_json(OUT / 'artifact-result.json')
|
||||
check(prior['status'] == 'PASS' and file_map(PACKAGE) == prior['archive']['files'], 'previously verified extracted package is unchanged')
|
||||
runtime = OUT / 'server-run'
|
||||
runtime.mkdir()
|
||||
data_dir = runtime / 'data'
|
||||
temp = runtime / 'temp'
|
||||
temp.mkdir()
|
||||
windows = Path(os.environ['SystemRoot'])
|
||||
env = {'SystemRoot': str(windows), 'WINDIR': str(windows), 'PATH': str(windows / 'System32') + ';' + str(windows), 'TEMP': str(temp), 'TMP': str(temp)}
|
||||
exe = PACKAGE / 'arcadia-server.exe'
|
||||
RESULT['environment'] = env
|
||||
RESULT['processes'] = []
|
||||
for arg in ('--version', '--help'):
|
||||
output = subprocess.run([str(exe), arg], cwd=PACKAGE, env=env, capture_output=True, timeout=15, creationflags=subprocess.CREATE_NO_WINDOW)
|
||||
(runtime / (arg[2:] + '.stdout')).write_bytes(output.stdout)
|
||||
(runtime / (arg[2:] + '.stderr')).write_bytes(output.stderr)
|
||||
check(output.returncode == 0 and not output.stderr, arg + ' succeeds under clean runtime PATH')
|
||||
if arg == '--version':
|
||||
check(output.stdout.decode().strip() == 'arcadia-server 0.2.0', 'server reports underlying application version 0.2.0')
|
||||
check(not data_dir.exists(), 'help/version do not create explicit test data directory')
|
||||
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||||
request_number = 0
|
||||
def request(port, method, path, payload=None):
|
||||
nonlocal request_number
|
||||
request_number += 1
|
||||
body = None if payload is None else json.dumps(payload, ensure_ascii=False).encode()
|
||||
req = urllib.request.Request(f'http://127.0.0.1:{port}' + path, data=body, method=method, headers={} if body is None else {'Content-Type': 'application/json'})
|
||||
with opener.open(req, timeout=10) as response:
|
||||
raw = response.read()
|
||||
meta = {'method': method, 'path': path, 'status': response.status, 'headers': dict(response.headers), 'body_sha256': sha(raw)}
|
||||
prefix = runtime / f'http-{request_number:02d}'
|
||||
prefix.with_suffix('.body').write_bytes(raw)
|
||||
write_json(prefix.with_suffix('.json'), meta)
|
||||
return meta, raw
|
||||
def start(index):
|
||||
with socket.socket() as s:
|
||||
s.bind(('127.0.0.1', 0))
|
||||
port = s.getsockname()[1]
|
||||
out = (runtime / f'server-{index}.stdout').open('xb')
|
||||
err = (runtime / f'server-{index}.stderr').open('xb')
|
||||
args = [str(exe), '--bind', f'127.0.0.1:{port}', '--data-dir', str(data_dir)]
|
||||
process = subprocess.Popen(args, cwd=PACKAGE, env=env, stdout=out, stderr=err, creationflags=subprocess.CREATE_NO_WINDOW)
|
||||
out.close()
|
||||
err.close()
|
||||
RESULT['processes'].append({'pid': process.pid, 'port': port, 'arguments': args})
|
||||
deadline = time.monotonic() + 15
|
||||
while True:
|
||||
if process.poll() is not None:
|
||||
raise AssertionError('packaged server exited before listening')
|
||||
try:
|
||||
with socket.create_connection(('127.0.0.1', port), timeout=.2):
|
||||
break
|
||||
except OSError:
|
||||
if time.monotonic() > deadline:
|
||||
process.terminate()
|
||||
process.wait(timeout=5)
|
||||
raise AssertionError('packaged server did not listen within 15s')
|
||||
time.sleep(.05)
|
||||
return process, port
|
||||
def stop(process):
|
||||
process.terminate()
|
||||
code = process.wait(timeout=10)
|
||||
RESULT['processes'][-1]['exit_after_explicit_terminate'] = code
|
||||
process = None
|
||||
try:
|
||||
process, port = start(1)
|
||||
for path in ('/health', '/health/ready'):
|
||||
meta, raw = request(port, 'GET', path)
|
||||
check(meta['status'] == 200 and json.loads(raw) == {'status': 'ok'}, path + ' contract')
|
||||
meta, raw = request(port, 'GET', '/api/v1/openapi.json')
|
||||
check(meta['status'] == 200 and raw == (PACKAGE / 'docs/openapi.json').read_bytes(), 'served OpenAPI equals complete packaged source bytes')
|
||||
name = 'Independent package persistence — 救助 20260911'
|
||||
meta, raw = request(port, 'POST', '/api/v1/projects', {'name': name})
|
||||
created = json.loads(raw)
|
||||
check(meta['status'] == 201, 'one create returns 201')
|
||||
resource = str(uuid.UUID(created['resource_id']))
|
||||
model_id = str(uuid.UUID(created['model_id']))
|
||||
expected_summary = {'resource_id': resource, 'revision': 1, 'model_id': model_id, 'schema_version': 7, 'name': name, 'elements': 0, 'relations': 0, 'migration': None}
|
||||
check(created == expected_summary and resource != model_id, 'complete create summary with distinct fresh resource/model IDs')
|
||||
etag = f'"p-{resource}-r1"'
|
||||
headers = {k.lower(): v for k, v in meta['headers'].items()}
|
||||
path = '/api/v1/projects/' + resource
|
||||
check(headers.get('etag') == etag and headers.get('location') == path, 'create ETag and resource Location')
|
||||
meta, raw = request(port, 'GET', path)
|
||||
stored = json.loads(raw)
|
||||
model = {'schema_version': 7, 'id': model_id, 'alias': 'mission', 'name': name, 'description': '', 'elements': {}, 'relations': {}, 'diagrams': {}, 'artifact_links': {}, 'traces': {}}
|
||||
source = f'arcadia 7;\nproject mission {json.dumps(name, ensure_ascii=False)} id "{model_id}" {{\n description "";\n}}\n'
|
||||
expected = {'resource_id': resource, 'revision': 1, 'model': model, 'source_project': {'entry': 'project.arcadia', 'files': {'project.arcadia': source}}}
|
||||
check(meta['status'] == 200 and stored == expected, 'GET equals independently predicted complete Project and SourceBundle')
|
||||
check({k.lower(): v for k, v in meta['headers'].items()}.get('etag') == etag, 'GET ETag equals created revision')
|
||||
disk = data_dir / (resource + '.json')
|
||||
disk_bytes = disk.read_bytes()
|
||||
check(json.loads(disk_bytes) == expected, 'durable disk envelope equals complete expected GET')
|
||||
check({p.name for p in data_dir.iterdir()} == {'.arcadia.lock', disk.name}, 'private store contains only one project and lock')
|
||||
stop(process)
|
||||
process = None
|
||||
process, port = start(2)
|
||||
meta, raw = request(port, 'GET', '/health/ready')
|
||||
check(meta['status'] == 200 and json.loads(raw) == {'status': 'ok'}, 'restarted packaged server ready')
|
||||
meta, raw = request(port, 'GET', path)
|
||||
check(meta['status'] == 200 and json.loads(raw) == expected, 'restart retains exact complete Project and SourceBundle')
|
||||
check({k.lower(): v for k, v in meta['headers'].items()}.get('etag') == etag and disk.read_bytes() == disk_bytes, 'restart retains revision, ETag and every durable envelope byte')
|
||||
RESULT['persistence'] = {'resource_id': resource, 'model_id': model_id, 'revision': 1, 'etag': etag, 'disk_sha256': sha(disk_bytes), 'expected': expected, 'stop_kind': 'explicit process termination after acknowledged durable create; not a graceful-drain claim'}
|
||||
finally:
|
||||
if process is not None and process.poll() is None:
|
||||
stop(process)
|
||||
check(file_map(PACKAGE) == prior['archive']['files'], 'server execution leaves complete extracted package unchanged')
|
||||
check(sha(ZIP.read_bytes()) == EXPECTED_ZIP, 'frozen ZIP unchanged after independent runtime checks')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
result_path = OUT / (sys.argv[1] + '-result.json')
|
||||
if result_path.exists():
|
||||
raise SystemExit('Refusing to overwrite prior evidence: ' + str(result_path))
|
||||
try:
|
||||
{'artifact': artifact, 'server': server}[sys.argv[1]]()
|
||||
RESULT['status'] = 'PASS'
|
||||
except Exception as error:
|
||||
RESULT['status'] = 'FAIL'
|
||||
RESULT['error'] = repr(error)
|
||||
RESULT['traceback'] = traceback.format_exc()
|
||||
RESULT['finished_utc'] = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
|
||||
RESULT['script_sha256'] = sha(Path(__file__).read_bytes())
|
||||
write_json(result_path, RESULT)
|
||||
print(json.dumps({k: RESULT[k] for k in ('scope', 'status', 'started_utc', 'finished_utc')}, indent=2))
|
||||
print('checks:', len(RESULT['checks']))
|
||||
if RESULT['status'] == 'FAIL':
|
||||
print(RESULT['traceback'])
|
||||
sys.exit(1)
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 129 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 133 KiB |
@@ -1,18 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "ArcadiaStudio-0.2.0-preview.authoring.20260911-x86_64-pc-windows-gnullvm.zip",
|
||||
"bytes": 32618262,
|
||||
"sha256": "664f30b6217689aca1492eb263b3d15777067d732fa890eeb028f435209dadfa",
|
||||
"url": "http://217.177.34.108:3000/attachments/48a283b7-5391-4677-9427-02a772e5e7e4",
|
||||
"readback": "PASS"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "ArcadiaStudio-0.2.0-preview.authoring.20260911-x86_64-pc-windows-gnullvm.zip.sha256",
|
||||
"bytes": 143,
|
||||
"sha256": "ea553a85f35d2d48f229dd4cd00ac0c3d4a6e9dafa58273004b2c49d1279f856",
|
||||
"url": "http://217.177.34.108:3000/attachments/08164a39-9292-4b2d-9856-c6ec41d9b8e2",
|
||||
"readback": "PASS"
|
||||
}
|
||||
]
|
||||
@@ -1,10 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": 3,
|
||||
"name": "VALIDATION.md",
|
||||
"bytes": 29308,
|
||||
"sha256": "c2a70be735c780d3e468d5f12643fb00be75673434211366739926536bbbe9b9",
|
||||
"url": "http://217.177.34.108:3000/attachments/9ff53361-eedf-46a0-aa4e-34f11bb0f302",
|
||||
"readback": "PASS"
|
||||
}
|
||||
]
|
||||
@@ -1,15 +0,0 @@
|
||||
# Standalone CLI package evidence
|
||||
|
||||
Exact frozen author and independent reviewer evidence for the Windows
|
||||
`ArcadiaCLI-0.2.0-preview.authoring.20260911` artifact. `hashes.json` records
|
||||
original private paths, lengths and hashes; preserve these bytes through Git.
|
||||
|
||||
The original host-specific reviewer script expects its recorded `.local` layout,
|
||||
source snapshot and fresh disposable outputs. Restore that layout to reproduce
|
||||
it; running the archived file in this directory is not a portable test command.
|
||||
The production packaging tests remain under `scripts/`.
|
||||
|
||||
The author report records 15 inert checks, 13 Python tests, default-preserving
|
||||
notice selection and strict package assembly. Independent review verifies source
|
||||
equivalence/defaults, all 114 archive files and 156 assertions across 10 clean
|
||||
runtime invocations. Neither completed the wider OA/SA product or Linux acceptance.
|
||||
@@ -1,5 +0,0 @@
|
||||
.............
|
||||
----------------------------------------------------------------------
|
||||
Ran 13 tests in 1.252s
|
||||
|
||||
OK
|
||||
@@ -1,454 +0,0 @@
|
||||
{
|
||||
"assembly_commit": "a0a48e82bd43be181caca1fe2c1514c21f2bc405",
|
||||
"semantic_commit": "01b76a1e89e93b7992f39e63560aa8b2ca2f69e0",
|
||||
"packaging_files": [
|
||||
{
|
||||
"path": "scripts/package-cli-windows.ps1",
|
||||
"sha256": "b6ec59bb55ff1e5624c782f492ce1d169131e708f257dcca195957edf7341c1a"
|
||||
},
|
||||
{
|
||||
"path": "scripts/package-common.ps1",
|
||||
"sha256": "d859453a8230b4f305d822c8742b371a71e35df698109a5d55c948f6dfaf0640"
|
||||
},
|
||||
{
|
||||
"path": "scripts/package-windows.ps1",
|
||||
"sha256": "28997ff78565619ed9c9b185f66dc922808f1f0be17f522b430c6c415448663d"
|
||||
},
|
||||
{
|
||||
"path": "scripts/windows-notices.ps1",
|
||||
"sha256": "a5328c05579e1b9ca09142ee99e87c12994d12764f7c69448e392883d718ef01"
|
||||
},
|
||||
{
|
||||
"path": "scripts/package-artifacts.py",
|
||||
"sha256": "24515fdbf4eebfb47b82383c6b3ccf290261cbd919f7f46a586b6488af769c8d"
|
||||
},
|
||||
{
|
||||
"path": "scripts/package-cli-readme.md",
|
||||
"sha256": "ea7bc971993e02751f24bc3cb313503a5f5fefd788540974e114398115c7c8e2"
|
||||
},
|
||||
{
|
||||
"path": "scripts/test-package-cli-windows.ps1",
|
||||
"sha256": "2c18f91149ede5c7b677a165bd0d8b31f3c4691642cc10dd1007a7a77bde912a"
|
||||
},
|
||||
{
|
||||
"path": "scripts/test-package-artifacts.py",
|
||||
"sha256": "881718181a36fa5f7abfcd2e746bc1339446f9aeed6b2c039b8d2ce230ec202d"
|
||||
},
|
||||
{
|
||||
"path": "scripts/test-windows-notices.ps1",
|
||||
"sha256": "00a6ad1ed28fdb9044ceb7cc0714ceb7af095db2fb18f4828a053aa872a3147d"
|
||||
}
|
||||
],
|
||||
"exact_cli_inputs": [
|
||||
{
|
||||
"path": "Cargo.toml",
|
||||
"sha256": "369f5b4b2b60705a94623c8c0b68f8e0b048f263280ce07ae0629ad353797d62"
|
||||
},
|
||||
{
|
||||
"path": "Cargo.lock",
|
||||
"sha256": "2b960242c33eb839ff008d01731f6d5cdb4e047ca8cd6e13151cbf9c09913510"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-cli/Cargo.toml",
|
||||
"sha256": "49c567f1af74a826683b4d96845feb3806fd9bcb7dc2767e16b5509d4e5e1cde"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-cli/src/lib.rs",
|
||||
"sha256": "97bebab63b4a611a1d36ad0c20fe452cbe1985fe3a6fbd39c954603da11410fc"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-cli/src/main.rs",
|
||||
"sha256": "de964f9b6c59c0f31c080afbd3b31d442b07b1b44f6a187a248d3409c45d35fb"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-cli/tests/source_cli.rs",
|
||||
"sha256": "a3a1b856c7a9fd3d80bc8074150a24fbb0ef0e9ddf736c7cea036725fc3dc2b8"
|
||||
}
|
||||
],
|
||||
"unchanged_core_shared_build_inputs": [
|
||||
{
|
||||
"path": "build/windows_manifest.rs",
|
||||
"sha256": "168da2c42ecccfebc11ee3e1b455e653a68fe9c6d30a9842a8b9e7441d086c80"
|
||||
},
|
||||
{
|
||||
"path": "build/windows.manifest",
|
||||
"sha256": "9c19c5a02f858ece84c44839ad2b145d9c1d90442da0b15c87fd045f975974eb"
|
||||
},
|
||||
{
|
||||
"path": "build/windows.rc",
|
||||
"sha256": "a27385edcae0bcf96f55361056cb7be5f36d76fcddcaefd20647c91959563e1e"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/build.rs",
|
||||
"sha256": "ab1be6ae1fd8bf3ebaeef6348347a9d5e666bde693126f199e041659e0b094a9"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/Cargo.toml",
|
||||
"sha256": "64824ec447be777841a972bf06c894ef2624a37e4725583bcfe0389deb9f00a3"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/examples/generate_demo.rs",
|
||||
"sha256": "e26f432c95331aeca50985dbb3ad2c558affd43023b585811f0f7bfaa59f6661"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/examples/generate_release1.rs",
|
||||
"sha256": "1d5f11165d74893e4bc18dbf3a2f2111f4026bd89fd06ee7f0bae69e45c4d4b5"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/examples/release_dense_views.rs",
|
||||
"sha256": "a83158488ef9b7394f136c52b1496719be935c4e5bdfe457cab5c9894f5f9412"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/examples/routing_workshop.rs",
|
||||
"sha256": "571307b18b6d4fa04700c5a4d4b0c2154c419e93a21292b2028e7b6f46a2faa7"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/data.rs",
|
||||
"sha256": "7f9c7c7dd8353cb5a38597b92cde6d3e890203d392473187eb387d41ff37414a"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/demo.rs",
|
||||
"sha256": "ad62123f96c20ff023cafc1a9849b0fa06bc62edd1fb9701799e4599b458600c"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/language_nesting_tests.rs",
|
||||
"sha256": "bd061611e8cb6f28ab203c73e544ac98bf275eb464cd314ad5fbd25f773df33b"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/language.rs",
|
||||
"sha256": "638f08a42e9cf4996b5d07f69b4d6f517a518369f3604d25be1006e40e080765"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/language/semantic_source.rs",
|
||||
"sha256": "584b5bb59d87a7b8d44d5dd2d124c3a1323b21492b138a17bc9526c0087c9e29"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/lib.rs",
|
||||
"sha256": "52cb6ee7a4ffc00711ecf121c1a1b14ca154863f7ca27b0d2a8e239edce8aca7"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/migration_v2/adapter.rs",
|
||||
"sha256": "66741b4c8354a23b8a932ecc5bf1bc3bb6e74279558757b8c35bc2157a7e51b5"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/migration_v2/mod.rs",
|
||||
"sha256": "857d8bb13b6814daba6780ca770c16352943c01cb130245d055f11b3b957b3e9"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/migration_v2/model.rs",
|
||||
"sha256": "8ac63eb59b2e57bdecff2477ee0634096090924d3dd1851e1c454880a889dacb"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/migration_v2/references.rs",
|
||||
"sha256": "33a6819abc83139fdaebeebd1b553238a328637106cce7dd5a535f1791d43e20"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/migration_v2/source.rs",
|
||||
"sha256": "74fee9b24e7c912182c998ab9d9aeec125e2009c3214959229e844c67afe2bf6"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/migration_v2/state_validation.rs",
|
||||
"sha256": "e29ef071bab0a68460ddf55c46131c6f66c022d52c3c602c37c013c3649da1b0"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/migration_v2/transitions.rs",
|
||||
"sha256": "f74174c532ba56b57c80b719a11fb4adea0a78ccc2828fff72fe2b9eda5a10b1"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/migration_v2/validation.rs",
|
||||
"sha256": "6d5e4a4e2805ae5045bf789a0485e14a61c63346587bace2b4a91173a688f780"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/migration_v3/mod.rs",
|
||||
"sha256": "f4edb6a0b50ecfe8670973776d703047b064420cb1f05913129c00fc656b3722"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/migration_v4.rs",
|
||||
"sha256": "50c652964f0f309e7f3f54b6ef12c40d42fcecc0490f005995c713df1179af70"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/model.rs",
|
||||
"sha256": "4585c895c64c4a183941c92cf047a9e52ff0cbe41971e16e78d229ea9dc101fb"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/operations.rs",
|
||||
"sha256": "a5ffbbf54cd440316a106d1188bfb699b57dae59f466edcb33618c333cd19f41"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/persistence.rs",
|
||||
"sha256": "1c01474f906a63e552c0d590b272c95821c00a100b3e79e58e67dbbeac341a14"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/ports.rs",
|
||||
"sha256": "5cac12eea70a54ebebbd6be77b63ce92e3195325c4bc7d27705c518686cbac80"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/projection.rs",
|
||||
"sha256": "69ed12bace2cc5024e151752a174ab14034ac2d05eb741fcf13e9a27521c059b"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/projection/port_layout.rs",
|
||||
"sha256": "a08d17d10a58bb8b206dd001b19b91d5aa3a9f743b298a4da57d503223c20079"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/projection/state.rs",
|
||||
"sha256": "73a607bac713ec7ff49a509fefb1a53c7686dca93ee241ffc835564d743afb28"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/references.rs",
|
||||
"sha256": "93553c234f32da35f1f41a7b83ad81c471c9803efa78ca222cf7de3c2f86c0bb"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/routing.rs",
|
||||
"sha256": "c5ebb350ba76f731f5c48d968bfbba4b6ef68ec046eb5893d116c4637ef15080"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/semantic.rs",
|
||||
"sha256": "f9895dd5b8fb27d10d4cfbb41c3542ef2018365a589b8d5778e5674ae5a1f239"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/semantic/catalog.rs",
|
||||
"sha256": "1ff74fcca18084d178681d7ff38ee1ccac632c27c4de0595ba1ceab8067ab3a4"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/semantic/historical.rs",
|
||||
"sha256": "eaf3284bf234312698514cdb636bc8348e1b5d746cb16b18df777e77d312abfb"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/semantic/linked.rs",
|
||||
"sha256": "6f335b14e809aecfc35defcaf77cb821b080d1506dbc46297912b25af98c07a2"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/semantic/mutate.rs",
|
||||
"sha256": "dc2b4c4a6eb156fe468eaa8c85fd37a505c0cd8ea1f226cb69cd01689afd3d9d"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/semantic/review.rs",
|
||||
"sha256": "0c157d2c327e72e89cca3b423e71ee73798105c5bb7271439c1a101642c91e3e"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/semantic/types.rs",
|
||||
"sha256": "6922fe35089f12fad43ab0ce1c6886d01a3855f90b8e2543e99802ac5e6f1bbb"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/session.rs",
|
||||
"sha256": "1eb77b329b2986fc6f5dc0171e77fd860375cdfd8c769a1367b445d6c49ebe00"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/source_files.rs",
|
||||
"sha256": "669491dbddb8e8e6556161426dffa1a49ac1c826d31020bff0956d81a4d84683"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/source_project.rs",
|
||||
"sha256": "1394370bcc5908873b830c2c11312e6e1611af41f46c4f43ca0a6bcb713bb466"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/source_project/source_migration.rs",
|
||||
"sha256": "add463ae85c420f853d324b00a221f0f874164cc3f3ed6bbb07557a9ed7a8c95"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/source_project/source_structure.rs",
|
||||
"sha256": "9f1f83315856a7025fe662c0fbb13452f31613224a03663150d6225d1ddc162f"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/state_validation.rs",
|
||||
"sha256": "e28d7f74385e2a78763716b34491bd73a00bfeccc085970ceb130627e3faee18"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/topology_context.rs",
|
||||
"sha256": "16de69b297de95d93c73808621607833b713af811dd082f0169186ae1e5c3e2d"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/topology_context/analysis_tests.rs",
|
||||
"sha256": "17839263410815e96fe027d4eba3aae0a9ab3b82b16947e2b29ebe0d09782b56"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/topology_context/analysis.rs",
|
||||
"sha256": "dead6194f0aa60cc46045c0e5ee10dbdbb689158c38634dfbbbc45d2ba88ac65"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/transitions.rs",
|
||||
"sha256": "fadd1b07b89cae4117b91abb4f1ac9ea81ff818ef20f2175e76ee8cf21fb708d"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/transitions/communication.rs",
|
||||
"sha256": "2b476630a88edd9021b9156ca12a5402e01604095f0d4002afb452671903a028"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/validation.rs",
|
||||
"sha256": "7c1b4ae5e4ebba13e99af9158ccde138dfc5e9a7afb580e76fba72d0b64ba74e"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/validation/coverage.rs",
|
||||
"sha256": "b08e4b344ecad055714168c83beff7fe29ba4edc53edbffa2eae04e5580a4d5f"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/view_options_tests.rs",
|
||||
"sha256": "64aac44de2df42a1012239a3dd75c473daf0c8990b3170b7bf7e78a556080208"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/view_options.rs",
|
||||
"sha256": "421ede0a04c593ca1df0e6e0d06fabcfcc8113cdbcfb0adc7f5c8ef4d1724208"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/communication_boundary_scenarios.rs",
|
||||
"sha256": "e1100b8f363910a487d90c73acc7df543dc6d532158c3c63ad11cd3d40d8e12b"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/communication_means.rs",
|
||||
"sha256": "a25f2749180bc6f4beb1445bfad3351eedf0669131a7439b633ab804439e95a4"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/coverage_diagnostics.rs",
|
||||
"sha256": "e2e9c3996daab47f2bd23f5d8c48b4e372af33331152f5fc85b76f06d04fcdf4"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/language.rs",
|
||||
"sha256": "77f6cb2a69976bfb5f60b243b71c07fb0edc7505002c1e9e73fcd6ddf6d9496e"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/port_anchors.rs",
|
||||
"sha256": "efbc0fa18733b1aaac501f9785a4c392fc49c05c45a5778d26e9b5dd951ca6fa"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/port_migration.rs",
|
||||
"sha256": "1801c376a5435ac7efb2a8342348b2c7ffef930bba65deca4acb66b06e0483d7"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/port_source.rs",
|
||||
"sha256": "8d2ddaca4045d330f6eda2591561497b858aad53769a823249d0bea4f5916e12"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/prepared_commands.rs",
|
||||
"sha256": "52cab1c668064777412df0a6a3c20d74d579b33794b4b00525b9f151c06eac9b"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/process_diagnostics.rs",
|
||||
"sha256": "1dbbc590239ef7542be4a2bdc9543aaa64d108417b03dd312a0894e364745906"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/r3_constructor_ack.rs",
|
||||
"sha256": "e5c44676ea0aab97f2e091ddac2f4bc4a4a90f5877f7bbf08b61229600944dd0"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/r3_migration.rs",
|
||||
"sha256": "ebd04120f5215b3956fdc79ee78f6e38734c7d13392f1fc28aaba957912265bd"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/r3_semantic.rs",
|
||||
"sha256": "49c3763eaf2131070a7c46cc23a84f917d292d41610b6404510e5607f8890c32"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/r3_source.rs",
|
||||
"sha256": "f31f60659df3c7ef601650d1cff140589d8729f01a3f65a4a6a6c2272f15c408"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/r6_data.rs",
|
||||
"sha256": "5f7ffb3bc73c3c85423623fe56f8680646dc97040327bdbead9d47a37e80f155"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/r6_migration.rs",
|
||||
"sha256": "81519bc1512f5e069432e9a1cdad04c10216d492df01b4b75b02102480ebd6a1"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/r6_source.rs",
|
||||
"sha256": "88a0aa087a7678e2898ff6057538072ca92072190712f05fff08372cc56ebed6"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/release_capabilities.rs",
|
||||
"sha256": "b92cba2a562e78287ddd7dc3e94557848d17faa0c8a102601b50286b9af48a30"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/release_diagrams.rs",
|
||||
"sha256": "cefcc0582d91266d5de6ee5daefcfe41b606ab971ab235b59034a8e1dbc24b7d"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/release_language_limits.rs",
|
||||
"sha256": "8578da4f76bbd2d0e1bfe295ab1259e2162a3fe3d0d2ba8901915d7a40942806"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/release_ports.rs",
|
||||
"sha256": "506e62beab0c7ec0cece83e9e0c1a810fa1f99285c9e1cab0c77f06de55a40e1"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/release_routing.rs",
|
||||
"sha256": "ae168faaba23769b7ca1fd65f217b9db671726e67276eb5a04e5e72b1447a462"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/release_scenarios.rs",
|
||||
"sha256": "0f5d876badeec653fc7b12610a77d867e9a53582e605253deff8d379da21572f"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/release_states.rs",
|
||||
"sha256": "b1a26acee95eaba7f9920f9da738a1e33606552eb078e6d298895daff7e3d811"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/release_topology.rs",
|
||||
"sha256": "c51dbe5aa5a038f9ed830daa02aa67714472179eb7fd6da5ce36dc6931c8b3a9"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/release1_teaching.rs",
|
||||
"sha256": "be179290d871125dc97cfd2a3067c7ac89c8a538fdb0afafc5b825bcdfb2bef2"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/review_regressions.rs",
|
||||
"sha256": "f154e5c88d35e31512e2b07733a1327ea3e2fcfd1ef8eb23066a4e9573f387c1"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/route_source.rs",
|
||||
"sha256": "1a73fdbac0d0ae0e84d8d67b6ad398e11223958f5dca007f73757c91827decfb"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/routing_geometry.rs",
|
||||
"sha256": "2d8eea4a2bba535ed0ea6daabccdaf87b375828c404baa10518c7bd7fd6253d3"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/routing_labels.rs",
|
||||
"sha256": "dff884d799c3c5c9257210248c55767e294e3cb317c605cbd0ddc9600c216eb6"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/semantic_artifact_absence_oracle.rs",
|
||||
"sha256": "d93eaf9667f4221232168bef0fe9f069e5be1b1f23410bda5dd67010b96e5c8a"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/semantic_attachments_gui_oracle.rs",
|
||||
"sha256": "7f526d363ba05e7ac89faf0fead0796e419c34ee204edf44a1b46c78308b0970"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/source_files.rs",
|
||||
"sha256": "f0429a0b89a443458ea9af7083c1e4e7411417a7e6b47e618bfbb9266dc164f9"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/source_identity_updates.rs",
|
||||
"sha256": "f52aa38b4d317e532c641ad7463472e4b40e802e60052cf0017556ffc63da06c"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/source_nested_updates.rs",
|
||||
"sha256": "9ac3390936c0d250651ea94a1b8faf0adfb680f061ececc56f2a52909b5ba855"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/source_project.rs",
|
||||
"sha256": "cef58adb16aa4d2527ddc0628aec3e26f0b42b4be70014a7e360fafdac13a942"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/source6_view_contract.rs",
|
||||
"sha256": "fa566de53946d40bbbd9033edfeed177180c4dc44afa2f2f4667ffdc622eb3bb"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/source7_contract.rs",
|
||||
"sha256": "cad4f1a135ddbd947bdf3c71d1b44f1e438725095ea8c4d893464bc721c2363f"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/state_diagrams.rs",
|
||||
"sha256": "6b02b387a3ae559cb3ad72de24292a5abcb76aaac0adbab5d2aa5211b22cd2e5"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/workflows.rs",
|
||||
"sha256": "bd843dfddbd5724ec08f657571f887496203056fe2fa5864ef50a168a613b89b"
|
||||
}
|
||||
],
|
||||
"binary_sha256": {
|
||||
"arcadia.exe": "bad169f8be78dcfa74b32714569c886dd2132727080684fc2ba618d5822e3559",
|
||||
"libunwind.dll": "f45604a12e8bcd839fd86f667c1be1cc7c4f9da2b3c69ff381ac708f0e1ae7d4"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"source_identity": "CLI semantic/build-input basis 01b76a1e89e93b7992f39e63560aa8b2ca2f69e0. Six original CLI manifest/workspace/lock/source/test hashes and 95 unchanged core/shared build inputs independently matched. Packaging assembly basis a0a48e82bd43be181caca1fe2c1514c21f2bc405 changes packaging only and is not claimed as the original compiler input commit.",
|
||||
"profile": "Preserved Windows debug/development artifact from cargo build --locked --offline -p oa-cli --bin arcadia, successful September11 2026 11:45:43.6035381Z,9.7336637seconds. No release-profile or reproducible-build claim.",
|
||||
"compiler": "rustc1.98.1 commit48a229ceaefd4985c50990b14116b6d856af0985,x86_64-pc-windows-gnullvm,LLVM22.1.8. Frozen executable embeds that compiler path; current toolchain target-library hashes match preserved inventory. Original compile/test/lint and independent frozen-binary review records retained separately.",
|
||||
"target": "x86_64-pc-windows-gnullvm",
|
||||
"binary_sha256": {
|
||||
"arcadia.exe": "bad169f8be78dcfa74b32714569c886dd2132727080684fc2ba618d5822e3559"
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
[
|
||||
{
|
||||
"path": "assembly.json",
|
||||
"original": ".local/cli-packaging-20260911/assembly.json",
|
||||
"bytes": 17452,
|
||||
"sha256": "284ecc5e3caaf9e2201785619f81ad200166e7d489c2a65276857f71df7f0dc0"
|
||||
},
|
||||
{
|
||||
"path": "preparation.json",
|
||||
"original": ".local/cli-packaging-20260911/preparation.json",
|
||||
"bytes": 17497,
|
||||
"sha256": "0ec70eef7f487693c4e698cbe54d2b99e6f4d3fc10bb1e7edabc3d236c2a7aff"
|
||||
},
|
||||
{
|
||||
"path": "build-evidence.json",
|
||||
"original": ".local/cli-packaging-20260911/build-evidence.json",
|
||||
"bytes": 1086,
|
||||
"sha256": "b75c6f0b78caf508c046de1dd7e67a1aade15924d50452635da8b6b1eba4a7d5"
|
||||
},
|
||||
{
|
||||
"path": "package-initial-result.json",
|
||||
"original": ".local/cli-packaging-20260911/package-initial-result.json",
|
||||
"bytes": 216,
|
||||
"sha256": "8c474fc7c7ce254fc17c0df9c0e46bb23403bc11be052fda121e49184157dc15"
|
||||
},
|
||||
{
|
||||
"path": "windows-initial.log",
|
||||
"original": ".local/cli-packaging-20260911/windows-initial.log",
|
||||
"bytes": 131,
|
||||
"sha256": "c74a669d68754f45a1649636d70cab82132569ec53810a63c490bfd430e451cf"
|
||||
},
|
||||
{
|
||||
"path": "notices-initial.log",
|
||||
"original": ".local/cli-packaging-20260911/notices-initial.log",
|
||||
"bytes": 430,
|
||||
"sha256": "f37409b4d36959bc03dcb35ec053301da9cf4c6effbb6e9a68fca534133c36b0"
|
||||
},
|
||||
{
|
||||
"path": "artifacts-final.log",
|
||||
"original": ".local/cli-packaging-20260911/artifacts-final.log",
|
||||
"bytes": 117,
|
||||
"sha256": "729fe10826e06b3b0997d6c4879349f388978a5fcb5bea62d55cc4ee6603cc16"
|
||||
},
|
||||
{
|
||||
"path": "reviewer.py",
|
||||
"original": ".local/cli-packaging-review-20260911/review.py",
|
||||
"bytes": 18543,
|
||||
"sha256": "f85d1f34ab75b4067efe09886e447bf9fd54dc232020bcb1a46404ec57b2351e"
|
||||
},
|
||||
{
|
||||
"path": "reviewer-result.json",
|
||||
"original": ".local/cli-packaging-review-20260911/result.json",
|
||||
"bytes": 33967,
|
||||
"sha256": "3b92a816a62de386b5927b208c79c2a7c541e6e2e2e30bde31b4c11bbbc4509e"
|
||||
}
|
||||
]
|
||||
@@ -1,2 +0,0 @@
|
||||
PASS: normal/build and server-only Linux selection, cross-target runtime refusal, Rust reports, actual source hashes, stale archive refusal, doc-comment font exclusion, production font path/mapping refusals, exact font mapping and changed-byte refusal, unsupported source refusal, traversal refusal, altered supplement refusal.
|
||||
Synthetic fixture retained for inspection: C:\Temp\arcadia-notices-c72f98e0978c45618416fe3d2cfdd38e
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"started": "2026-09-11T12:36:52.6900250Z",
|
||||
"assembly_commit": "a0a48e82bd43be181caca1fe2c1514c21f2bc405",
|
||||
"success": true,
|
||||
"finished": "2026-09-11T12:37:33.7683084Z",
|
||||
"elapsed_seconds": 41.084165
|
||||
}
|
||||
@@ -1,454 +0,0 @@
|
||||
{
|
||||
"semantic_source_commit": "01b76a1e89e93b7992f39e63560aa8b2ca2f69e0",
|
||||
"original_cli_manifest_sha256": "dd73fc2bb2190b23fffe16d6c63d4acddf52338062fac298379ddfd241d82eb8",
|
||||
"exact_cli_inputs": [
|
||||
{
|
||||
"path": "Cargo.toml",
|
||||
"sha256": "369f5b4b2b60705a94623c8c0b68f8e0b048f263280ce07ae0629ad353797d62"
|
||||
},
|
||||
{
|
||||
"path": "Cargo.lock",
|
||||
"sha256": "2b960242c33eb839ff008d01731f6d5cdb4e047ca8cd6e13151cbf9c09913510"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-cli/Cargo.toml",
|
||||
"sha256": "49c567f1af74a826683b4d96845feb3806fd9bcb7dc2767e16b5509d4e5e1cde"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-cli/src/lib.rs",
|
||||
"sha256": "97bebab63b4a611a1d36ad0c20fe452cbe1985fe3a6fbd39c954603da11410fc"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-cli/src/main.rs",
|
||||
"sha256": "de964f9b6c59c0f31c080afbd3b31d442b07b1b44f6a187a248d3409c45d35fb"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-cli/tests/source_cli.rs",
|
||||
"sha256": "a3a1b856c7a9fd3d80bc8074150a24fbb0ef0e9ddf736c7cea036725fc3dc2b8"
|
||||
}
|
||||
],
|
||||
"unchanged_core_shared_build_inputs": [
|
||||
{
|
||||
"path": "build/windows_manifest.rs",
|
||||
"sha256": "168da2c42ecccfebc11ee3e1b455e653a68fe9c6d30a9842a8b9e7441d086c80"
|
||||
},
|
||||
{
|
||||
"path": "build/windows.manifest",
|
||||
"sha256": "9c19c5a02f858ece84c44839ad2b145d9c1d90442da0b15c87fd045f975974eb"
|
||||
},
|
||||
{
|
||||
"path": "build/windows.rc",
|
||||
"sha256": "a27385edcae0bcf96f55361056cb7be5f36d76fcddcaefd20647c91959563e1e"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/build.rs",
|
||||
"sha256": "ab1be6ae1fd8bf3ebaeef6348347a9d5e666bde693126f199e041659e0b094a9"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/Cargo.toml",
|
||||
"sha256": "64824ec447be777841a972bf06c894ef2624a37e4725583bcfe0389deb9f00a3"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/examples/generate_demo.rs",
|
||||
"sha256": "e26f432c95331aeca50985dbb3ad2c558affd43023b585811f0f7bfaa59f6661"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/examples/generate_release1.rs",
|
||||
"sha256": "1d5f11165d74893e4bc18dbf3a2f2111f4026bd89fd06ee7f0bae69e45c4d4b5"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/examples/release_dense_views.rs",
|
||||
"sha256": "a83158488ef9b7394f136c52b1496719be935c4e5bdfe457cab5c9894f5f9412"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/examples/routing_workshop.rs",
|
||||
"sha256": "571307b18b6d4fa04700c5a4d4b0c2154c419e93a21292b2028e7b6f46a2faa7"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/data.rs",
|
||||
"sha256": "7f9c7c7dd8353cb5a38597b92cde6d3e890203d392473187eb387d41ff37414a"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/demo.rs",
|
||||
"sha256": "ad62123f96c20ff023cafc1a9849b0fa06bc62edd1fb9701799e4599b458600c"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/language_nesting_tests.rs",
|
||||
"sha256": "bd061611e8cb6f28ab203c73e544ac98bf275eb464cd314ad5fbd25f773df33b"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/language.rs",
|
||||
"sha256": "638f08a42e9cf4996b5d07f69b4d6f517a518369f3604d25be1006e40e080765"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/language/semantic_source.rs",
|
||||
"sha256": "584b5bb59d87a7b8d44d5dd2d124c3a1323b21492b138a17bc9526c0087c9e29"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/lib.rs",
|
||||
"sha256": "52cb6ee7a4ffc00711ecf121c1a1b14ca154863f7ca27b0d2a8e239edce8aca7"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/migration_v2/adapter.rs",
|
||||
"sha256": "66741b4c8354a23b8a932ecc5bf1bc3bb6e74279558757b8c35bc2157a7e51b5"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/migration_v2/mod.rs",
|
||||
"sha256": "857d8bb13b6814daba6780ca770c16352943c01cb130245d055f11b3b957b3e9"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/migration_v2/model.rs",
|
||||
"sha256": "8ac63eb59b2e57bdecff2477ee0634096090924d3dd1851e1c454880a889dacb"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/migration_v2/references.rs",
|
||||
"sha256": "33a6819abc83139fdaebeebd1b553238a328637106cce7dd5a535f1791d43e20"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/migration_v2/source.rs",
|
||||
"sha256": "74fee9b24e7c912182c998ab9d9aeec125e2009c3214959229e844c67afe2bf6"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/migration_v2/state_validation.rs",
|
||||
"sha256": "e29ef071bab0a68460ddf55c46131c6f66c022d52c3c602c37c013c3649da1b0"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/migration_v2/transitions.rs",
|
||||
"sha256": "f74174c532ba56b57c80b719a11fb4adea0a78ccc2828fff72fe2b9eda5a10b1"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/migration_v2/validation.rs",
|
||||
"sha256": "6d5e4a4e2805ae5045bf789a0485e14a61c63346587bace2b4a91173a688f780"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/migration_v3/mod.rs",
|
||||
"sha256": "f4edb6a0b50ecfe8670973776d703047b064420cb1f05913129c00fc656b3722"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/migration_v4.rs",
|
||||
"sha256": "50c652964f0f309e7f3f54b6ef12c40d42fcecc0490f005995c713df1179af70"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/model.rs",
|
||||
"sha256": "4585c895c64c4a183941c92cf047a9e52ff0cbe41971e16e78d229ea9dc101fb"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/operations.rs",
|
||||
"sha256": "a5ffbbf54cd440316a106d1188bfb699b57dae59f466edcb33618c333cd19f41"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/persistence.rs",
|
||||
"sha256": "1c01474f906a63e552c0d590b272c95821c00a100b3e79e58e67dbbeac341a14"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/ports.rs",
|
||||
"sha256": "5cac12eea70a54ebebbd6be77b63ce92e3195325c4bc7d27705c518686cbac80"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/projection.rs",
|
||||
"sha256": "69ed12bace2cc5024e151752a174ab14034ac2d05eb741fcf13e9a27521c059b"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/projection/port_layout.rs",
|
||||
"sha256": "a08d17d10a58bb8b206dd001b19b91d5aa3a9f743b298a4da57d503223c20079"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/projection/state.rs",
|
||||
"sha256": "73a607bac713ec7ff49a509fefb1a53c7686dca93ee241ffc835564d743afb28"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/references.rs",
|
||||
"sha256": "93553c234f32da35f1f41a7b83ad81c471c9803efa78ca222cf7de3c2f86c0bb"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/routing.rs",
|
||||
"sha256": "c5ebb350ba76f731f5c48d968bfbba4b6ef68ec046eb5893d116c4637ef15080"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/semantic.rs",
|
||||
"sha256": "f9895dd5b8fb27d10d4cfbb41c3542ef2018365a589b8d5778e5674ae5a1f239"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/semantic/catalog.rs",
|
||||
"sha256": "1ff74fcca18084d178681d7ff38ee1ccac632c27c4de0595ba1ceab8067ab3a4"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/semantic/historical.rs",
|
||||
"sha256": "eaf3284bf234312698514cdb636bc8348e1b5d746cb16b18df777e77d312abfb"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/semantic/linked.rs",
|
||||
"sha256": "6f335b14e809aecfc35defcaf77cb821b080d1506dbc46297912b25af98c07a2"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/semantic/mutate.rs",
|
||||
"sha256": "dc2b4c4a6eb156fe468eaa8c85fd37a505c0cd8ea1f226cb69cd01689afd3d9d"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/semantic/review.rs",
|
||||
"sha256": "0c157d2c327e72e89cca3b423e71ee73798105c5bb7271439c1a101642c91e3e"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/semantic/types.rs",
|
||||
"sha256": "6922fe35089f12fad43ab0ce1c6886d01a3855f90b8e2543e99802ac5e6f1bbb"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/session.rs",
|
||||
"sha256": "1eb77b329b2986fc6f5dc0171e77fd860375cdfd8c769a1367b445d6c49ebe00"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/source_files.rs",
|
||||
"sha256": "669491dbddb8e8e6556161426dffa1a49ac1c826d31020bff0956d81a4d84683"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/source_project.rs",
|
||||
"sha256": "1394370bcc5908873b830c2c11312e6e1611af41f46c4f43ca0a6bcb713bb466"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/source_project/source_migration.rs",
|
||||
"sha256": "add463ae85c420f853d324b00a221f0f874164cc3f3ed6bbb07557a9ed7a8c95"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/source_project/source_structure.rs",
|
||||
"sha256": "9f1f83315856a7025fe662c0fbb13452f31613224a03663150d6225d1ddc162f"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/state_validation.rs",
|
||||
"sha256": "e28d7f74385e2a78763716b34491bd73a00bfeccc085970ceb130627e3faee18"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/topology_context.rs",
|
||||
"sha256": "16de69b297de95d93c73808621607833b713af811dd082f0169186ae1e5c3e2d"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/topology_context/analysis_tests.rs",
|
||||
"sha256": "17839263410815e96fe027d4eba3aae0a9ab3b82b16947e2b29ebe0d09782b56"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/topology_context/analysis.rs",
|
||||
"sha256": "dead6194f0aa60cc46045c0e5ee10dbdbb689158c38634dfbbbc45d2ba88ac65"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/transitions.rs",
|
||||
"sha256": "fadd1b07b89cae4117b91abb4f1ac9ea81ff818ef20f2175e76ee8cf21fb708d"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/transitions/communication.rs",
|
||||
"sha256": "2b476630a88edd9021b9156ca12a5402e01604095f0d4002afb452671903a028"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/validation.rs",
|
||||
"sha256": "7c1b4ae5e4ebba13e99af9158ccde138dfc5e9a7afb580e76fba72d0b64ba74e"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/validation/coverage.rs",
|
||||
"sha256": "b08e4b344ecad055714168c83beff7fe29ba4edc53edbffa2eae04e5580a4d5f"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/view_options_tests.rs",
|
||||
"sha256": "64aac44de2df42a1012239a3dd75c473daf0c8990b3170b7bf7e78a556080208"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/src/view_options.rs",
|
||||
"sha256": "421ede0a04c593ca1df0e6e0d06fabcfcc8113cdbcfb0adc7f5c8ef4d1724208"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/communication_boundary_scenarios.rs",
|
||||
"sha256": "e1100b8f363910a487d90c73acc7df543dc6d532158c3c63ad11cd3d40d8e12b"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/communication_means.rs",
|
||||
"sha256": "a25f2749180bc6f4beb1445bfad3351eedf0669131a7439b633ab804439e95a4"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/coverage_diagnostics.rs",
|
||||
"sha256": "e2e9c3996daab47f2bd23f5d8c48b4e372af33331152f5fc85b76f06d04fcdf4"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/language.rs",
|
||||
"sha256": "77f6cb2a69976bfb5f60b243b71c07fb0edc7505002c1e9e73fcd6ddf6d9496e"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/port_anchors.rs",
|
||||
"sha256": "efbc0fa18733b1aaac501f9785a4c392fc49c05c45a5778d26e9b5dd951ca6fa"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/port_migration.rs",
|
||||
"sha256": "1801c376a5435ac7efb2a8342348b2c7ffef930bba65deca4acb66b06e0483d7"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/port_source.rs",
|
||||
"sha256": "8d2ddaca4045d330f6eda2591561497b858aad53769a823249d0bea4f5916e12"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/prepared_commands.rs",
|
||||
"sha256": "52cab1c668064777412df0a6a3c20d74d579b33794b4b00525b9f151c06eac9b"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/process_diagnostics.rs",
|
||||
"sha256": "1dbbc590239ef7542be4a2bdc9543aaa64d108417b03dd312a0894e364745906"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/r3_constructor_ack.rs",
|
||||
"sha256": "e5c44676ea0aab97f2e091ddac2f4bc4a4a90f5877f7bbf08b61229600944dd0"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/r3_migration.rs",
|
||||
"sha256": "ebd04120f5215b3956fdc79ee78f6e38734c7d13392f1fc28aaba957912265bd"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/r3_semantic.rs",
|
||||
"sha256": "49c3763eaf2131070a7c46cc23a84f917d292d41610b6404510e5607f8890c32"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/r3_source.rs",
|
||||
"sha256": "f31f60659df3c7ef601650d1cff140589d8729f01a3f65a4a6a6c2272f15c408"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/r6_data.rs",
|
||||
"sha256": "5f7ffb3bc73c3c85423623fe56f8680646dc97040327bdbead9d47a37e80f155"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/r6_migration.rs",
|
||||
"sha256": "81519bc1512f5e069432e9a1cdad04c10216d492df01b4b75b02102480ebd6a1"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/r6_source.rs",
|
||||
"sha256": "88a0aa087a7678e2898ff6057538072ca92072190712f05fff08372cc56ebed6"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/release_capabilities.rs",
|
||||
"sha256": "b92cba2a562e78287ddd7dc3e94557848d17faa0c8a102601b50286b9af48a30"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/release_diagrams.rs",
|
||||
"sha256": "cefcc0582d91266d5de6ee5daefcfe41b606ab971ab235b59034a8e1dbc24b7d"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/release_language_limits.rs",
|
||||
"sha256": "8578da4f76bbd2d0e1bfe295ab1259e2162a3fe3d0d2ba8901915d7a40942806"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/release_ports.rs",
|
||||
"sha256": "506e62beab0c7ec0cece83e9e0c1a810fa1f99285c9e1cab0c77f06de55a40e1"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/release_routing.rs",
|
||||
"sha256": "ae168faaba23769b7ca1fd65f217b9db671726e67276eb5a04e5e72b1447a462"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/release_scenarios.rs",
|
||||
"sha256": "0f5d876badeec653fc7b12610a77d867e9a53582e605253deff8d379da21572f"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/release_states.rs",
|
||||
"sha256": "b1a26acee95eaba7f9920f9da738a1e33606552eb078e6d298895daff7e3d811"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/release_topology.rs",
|
||||
"sha256": "c51dbe5aa5a038f9ed830daa02aa67714472179eb7fd6da5ce36dc6931c8b3a9"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/release1_teaching.rs",
|
||||
"sha256": "be179290d871125dc97cfd2a3067c7ac89c8a538fdb0afafc5b825bcdfb2bef2"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/review_regressions.rs",
|
||||
"sha256": "f154e5c88d35e31512e2b07733a1327ea3e2fcfd1ef8eb23066a4e9573f387c1"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/route_source.rs",
|
||||
"sha256": "1a73fdbac0d0ae0e84d8d67b6ad398e11223958f5dca007f73757c91827decfb"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/routing_geometry.rs",
|
||||
"sha256": "2d8eea4a2bba535ed0ea6daabccdaf87b375828c404baa10518c7bd7fd6253d3"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/routing_labels.rs",
|
||||
"sha256": "dff884d799c3c5c9257210248c55767e294e3cb317c605cbd0ddc9600c216eb6"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/semantic_artifact_absence_oracle.rs",
|
||||
"sha256": "d93eaf9667f4221232168bef0fe9f069e5be1b1f23410bda5dd67010b96e5c8a"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/semantic_attachments_gui_oracle.rs",
|
||||
"sha256": "7f526d363ba05e7ac89faf0fead0796e419c34ee204edf44a1b46c78308b0970"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/source_files.rs",
|
||||
"sha256": "f0429a0b89a443458ea9af7083c1e4e7411417a7e6b47e618bfbb9266dc164f9"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/source_identity_updates.rs",
|
||||
"sha256": "f52aa38b4d317e532c641ad7463472e4b40e802e60052cf0017556ffc63da06c"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/source_nested_updates.rs",
|
||||
"sha256": "9ac3390936c0d250651ea94a1b8faf0adfb680f061ececc56f2a52909b5ba855"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/source_project.rs",
|
||||
"sha256": "cef58adb16aa4d2527ddc0628aec3e26f0b42b4be70014a7e360fafdac13a942"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/source6_view_contract.rs",
|
||||
"sha256": "fa566de53946d40bbbd9033edfeed177180c4dc44afa2f2f4667ffdc622eb3bb"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/source7_contract.rs",
|
||||
"sha256": "cad4f1a135ddbd947bdf3c71d1b44f1e438725095ea8c4d893464bc721c2363f"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/state_diagrams.rs",
|
||||
"sha256": "6b02b387a3ae559cb3ad72de24292a5abcb76aaac0adbab5d2aa5211b22cd2e5"
|
||||
},
|
||||
{
|
||||
"path": "crates/oa-core/tests/workflows.rs",
|
||||
"sha256": "bd843dfddbd5724ec08f657571f887496203056fe2fa5864ef50a168a613b89b"
|
||||
}
|
||||
],
|
||||
"packaging_files": [
|
||||
{
|
||||
"path": "scripts/package-cli-windows.ps1",
|
||||
"sha256": "b6ec59bb55ff1e5624c782f492ce1d169131e708f257dcca195957edf7341c1a"
|
||||
},
|
||||
{
|
||||
"path": "scripts/package-common.ps1",
|
||||
"sha256": "d859453a8230b4f305d822c8742b371a71e35df698109a5d55c948f6dfaf0640"
|
||||
},
|
||||
{
|
||||
"path": "scripts/package-windows.ps1",
|
||||
"sha256": "28997ff78565619ed9c9b185f66dc922808f1f0be17f522b430c6c415448663d"
|
||||
},
|
||||
{
|
||||
"path": "scripts/windows-notices.ps1",
|
||||
"sha256": "a5328c05579e1b9ca09142ee99e87c12994d12764f7c69448e392883d718ef01"
|
||||
},
|
||||
{
|
||||
"path": "scripts/package-artifacts.py",
|
||||
"sha256": "24515fdbf4eebfb47b82383c6b3ccf290261cbd919f7f46a586b6488af769c8d"
|
||||
},
|
||||
{
|
||||
"path": "scripts/package-cli-readme.md",
|
||||
"sha256": "39171afa7dab4be10a8566153d7384aa439f1494851ce2ca1da989c6f370ef2b"
|
||||
},
|
||||
{
|
||||
"path": "scripts/test-package-cli-windows.ps1",
|
||||
"sha256": "2c18f91149ede5c7b677a165bd0d8b31f3c4691642cc10dd1007a7a77bde912a"
|
||||
},
|
||||
{
|
||||
"path": "scripts/test-package-artifacts.py",
|
||||
"sha256": "881718181a36fa5f7abfcd2e746bc1339446f9aeed6b2c039b8d2ce230ec202d"
|
||||
},
|
||||
{
|
||||
"path": "scripts/test-windows-notices.ps1",
|
||||
"sha256": "00a6ad1ed28fdb9044ceb7cc0714ceb7af095db2fb18f4828a053aa872a3147d"
|
||||
}
|
||||
],
|
||||
"binary_sha256": {
|
||||
"arcadia.exe": "bad169f8be78dcfa74b32714569c886dd2132727080684fc2ba618d5822e3559",
|
||||
"libunwind.dll": "f45604a12e8bcd839fd86f667c1be1cc7c4f9da2b3c69ff381ac708f0e1ae7d4"
|
||||
}
|
||||
}
|
||||
@@ -1,458 +0,0 @@
|
||||
{
|
||||
"started_utc": "2026-09-11T12:42:56Z",
|
||||
"checks": [
|
||||
"preserve earlier evidence; no overwrite",
|
||||
"all original ELF/tar/document/notice helpers unchanged outside product selection",
|
||||
"windows actual full Studio document closure unchanged",
|
||||
"linux actual full Studio document closure unchanged",
|
||||
"only notice source delta is oa-cli allowed-root check; default/body unchanged",
|
||||
"Linux entry/deployment unchanged: scripts/package-linux.ps1",
|
||||
"Linux entry/deployment unchanged: deployment/Dockerfile",
|
||||
"Linux entry/deployment unchanged: deployment/Dockerfile.dockerignore",
|
||||
"PE reader extraction is byte-equivalent after function rename/EOL normalization",
|
||||
"Windows system DLL allowlist unchanged",
|
||||
"Studio explicitly retains both original binary inputs",
|
||||
"Studio post-inspection packaging pipeline unchanged",
|
||||
"assembly delta exactly nine packaging/script paths",
|
||||
"reviewed main packaging sources equal frozen assembly bytes",
|
||||
"fresh extraction directory",
|
||||
"frozen ZIP size and hash",
|
||||
"114 unique ZIP files",
|
||||
"single safe normalized Windows ZIP root",
|
||||
"ordinary fixed-time ZIP members and CRCs",
|
||||
"complete clean extraction equals ZIP bytes",
|
||||
"complete checksum inventory equals every other extracted file",
|
||||
"all package inputs plus exactly six generated records",
|
||||
"frozen CLI and runtime hashes",
|
||||
"no unrelated executable payload",
|
||||
"exact public document/example: LICENSE",
|
||||
"exact public document/example: docs/api.rst",
|
||||
"exact public document/example: docs/language.html",
|
||||
"exact public document/example: docs/language.rst",
|
||||
"exact public document/example: docs/source-cli.md",
|
||||
"exact public document/example: examples/release1/expected-diagnostics.json",
|
||||
"exact public document/example: examples/release1/model.json",
|
||||
"exact public document/example: examples/release1/model.rs",
|
||||
"exact public document/example: examples/release1/project.arcadia",
|
||||
"exact public document/example: examples/release1/README.md",
|
||||
"exact public document/example: examples/release1/traceability.arcadia",
|
||||
"exact public document/example: examples/release1/transition-before.json",
|
||||
"exact public document/example: examples/release1/transition-request.json",
|
||||
"exact public document/example: examples/text-first/project.arcadia",
|
||||
"exact public document/example: examples/text-first/README.md",
|
||||
"exact public document/example: examples/release1/evidence/repair-check.md",
|
||||
"exact public document/example: examples/release1/oa/capabilities.arcadia",
|
||||
"exact public document/example: examples/release1/oa/model.arcadia",
|
||||
"exact public document/example: examples/release1/oa/participants.arcadia",
|
||||
"exact public document/example: examples/release1/oa/types.arcadia",
|
||||
"exact public document/example: examples/release1/sa/capabilities.arcadia",
|
||||
"exact public document/example: examples/release1/sa/model.arcadia",
|
||||
"exact public document/example: examples/release1/sa/participants.arcadia",
|
||||
"exact public document/example: examples/release1/sa/types.arcadia",
|
||||
"exact public document/example: examples/release1/views/diagram_oa_allocation_view.arcadia",
|
||||
"exact public document/example: examples/release1/views/diagram_sa_allocation_view.arcadia",
|
||||
"exact public document/example: examples/release1/views/diagram_sa_capability_view.arcadia",
|
||||
"exact public document/example: examples/release1/views/diagram_sa_service_view.arcadia",
|
||||
"exact public document/example: examples/release1/views/diagram_sa_state_view.arcadia",
|
||||
"exact public document/example: examples/release1/oa/activities/activity_oa_restore_work.arcadia",
|
||||
"exact public document/example: examples/release1/oa/behavior/process_oa_assessment_procedure.arcadia",
|
||||
"exact public document/example: examples/release1/oa/behavior/process_oa_field_procedure.arcadia",
|
||||
"exact public document/example: examples/release1/oa/behavior/process_oa_response.arcadia",
|
||||
"exact public document/example: examples/release1/oa/behavior/scenario_oa_conversation.arcadia",
|
||||
"exact public document/example: examples/release1/oa/behavior/scenario_oa_full_scenario.arcadia",
|
||||
"exact public document/example: examples/release1/oa/behavior/state_machine_oa_monitor.arcadia",
|
||||
"exact public document/example: examples/release1/sa/behavior/process_sa_assessment_procedure.arcadia",
|
||||
"exact public document/example: examples/release1/sa/behavior/process_sa_field_procedure.arcadia",
|
||||
"exact public document/example: examples/release1/sa/behavior/process_sa_response.arcadia",
|
||||
"exact public document/example: examples/release1/sa/behavior/scenario_sa_conversation.arcadia",
|
||||
"exact public document/example: examples/release1/sa/behavior/scenario_sa_full_scenario.arcadia",
|
||||
"exact public document/example: examples/release1/sa/behavior/state_machine_sa_monitor.arcadia",
|
||||
"exact public document/example: examples/release1/sa/functions/function_sa_audit_thresholds.arcadia",
|
||||
"exact public document/example: examples/release1/sa/functions/function_sa_restore_work.arcadia",
|
||||
"CLI-specific README",
|
||||
"27 dependency packages; only CLI root",
|
||||
"original CLI Cargo lock identity",
|
||||
"locked dependency: oa-cli",
|
||||
"complete local crate inputs: oa-cli",
|
||||
"locked dependency: oa-core",
|
||||
"complete local crate inputs: oa-core",
|
||||
"locked dependency: cfg-if",
|
||||
"locked dependency: fastrand",
|
||||
"locked dependency: getrandom",
|
||||
"locked dependency: itoa",
|
||||
"locked dependency: memchr",
|
||||
"locked dependency: once_cell",
|
||||
"locked dependency: proc-macro2",
|
||||
"locked dependency: quote",
|
||||
"locked dependency: rand_core",
|
||||
"locked dependency: same-file",
|
||||
"locked dependency: serde_core",
|
||||
"locked dependency: serde_derive",
|
||||
"locked dependency: serde_json",
|
||||
"locked dependency: serde",
|
||||
"locked dependency: sha1_smol",
|
||||
"locked dependency: syn",
|
||||
"locked dependency: tempfile",
|
||||
"locked dependency: thiserror-impl",
|
||||
"locked dependency: thiserror",
|
||||
"locked dependency: unicode-ident",
|
||||
"locked dependency: uuid",
|
||||
"locked dependency: winapi-util",
|
||||
"locked dependency: windows-link",
|
||||
"locked dependency: windows-sys",
|
||||
"locked dependency: zmij",
|
||||
"all package/compiler/runtime notice references and bytes",
|
||||
"runtime notice binds packaged DLL",
|
||||
"all shared build input hashes",
|
||||
"six original CLI manifest inputs",
|
||||
"95 original core/shared build inputs",
|
||||
"CLI expected exit: ['--version']",
|
||||
"underlying CLI version",
|
||||
"CLI expected exit: ['--help']",
|
||||
"packaged help has command/preview contract",
|
||||
"CLI expected exit: ['check', WindowsPath('C:/Users/User/Documents/Git/ARCADIA-Studio/.local/cli-packaging-review-20260911/runtime/examples/text-first/project.arcadia'), '--json']",
|
||||
"JSON invocation keeps diagnostic envelope on stdout",
|
||||
"versioned JSON report agrees with process exit",
|
||||
"packaged example check: text-first/project.arcadia",
|
||||
"CLI expected exit: ['check', WindowsPath('C:/Users/User/Documents/Git/ARCADIA-Studio/.local/cli-packaging-review-20260911/runtime/examples/release1/project.arcadia'), '--json']",
|
||||
"JSON invocation keeps diagnostic envelope on stdout",
|
||||
"versioned JSON report agrees with process exit",
|
||||
"packaged example check: release1/project.arcadia",
|
||||
"checks preserve complete copied examples byte-for-byte",
|
||||
"CLI expected exit: ['stabilize', WindowsPath('C:/Users/User/Documents/Git/ARCADIA-Studio/.local/cli-packaging-review-20260911/runtime/exact-source/project.arcadia'), '--json']",
|
||||
"JSON invocation keeps diagnostic envelope on stdout",
|
||||
"versioned JSON report agrees with process exit",
|
||||
"stabilization is preview by default",
|
||||
"only missing-ID source changes; exact captured raw bytes",
|
||||
"fresh ID is attached to literal intended activity",
|
||||
"allocated identity differs from every preexisting identity",
|
||||
"stabilization preserves every other character, CRLF, comment, name and reference",
|
||||
"preview preserves all physical source bytes including detached file",
|
||||
"CLI expected exit: ['stabilize', WindowsPath('C:/Users/User/Documents/Git/ARCADIA-Studio/.local/cli-packaging-review-20260911/runtime/exact-source/project.arcadia'), '--write', '--json']",
|
||||
"JSON invocation keeps diagnostic envelope on stdout",
|
||||
"versioned JSON report agrees with process exit",
|
||||
"explicit stabilization writes",
|
||||
"only missing-ID source changes; exact captured raw bytes",
|
||||
"fresh ID is attached to literal intended activity",
|
||||
"allocated identity differs from every preexisting identity",
|
||||
"stabilization preserves every other character, CRLF, comment, name and reference",
|
||||
"stabilized complete physical file inventory and bytes",
|
||||
"CLI expected exit: ['rename-alias', WindowsPath('C:/Users/User/Documents/Git/ARCADIA-Studio/.local/cli-packaging-review-20260911/runtime/exact-source/project.arcadia'), 'crew', 'rescue_team', '--json']",
|
||||
"JSON invocation keeps diagnostic envelope on stdout",
|
||||
"versioned JSON report agrees with process exit",
|
||||
"alias preview changes exact declaration and typed reference only",
|
||||
"rename preview retains exact source bytes and identities",
|
||||
"CLI expected exit: ['rename-alias', WindowsPath('C:/Users/User/Documents/Git/ARCADIA-Studio/.local/cli-packaging-review-20260911/runtime/exact-source/project.arcadia'), 'crew', 'rescue_team', '--write', '--json']",
|
||||
"JSON invocation keeps diagnostic envelope on stdout",
|
||||
"versioned JSON report agrees with process exit",
|
||||
"explicit alias write matches full independent raw expected files",
|
||||
"all four identities remain ordered and exact through rename",
|
||||
"CLI expected exit: ['stabilize', WindowsPath('C:/Users/User/Documents/Git/ARCADIA-Studio/.local/cli-packaging-review-20260911/runtime/exact-source/project.arcadia'), '--write', '--json']",
|
||||
"JSON invocation keeps diagnostic envelope on stdout",
|
||||
"versioned JSON report agrees with process exit",
|
||||
"explicit no-op reports unchanged",
|
||||
"no-op preserves all file bytes and mtimes",
|
||||
"CLI expected exit: ['stabilize', WindowsPath('C:/Users/User/Documents/Git/ARCADIA-Studio/.local/cli-packaging-review-20260911/runtime/invalid.arcadia'), '--write', '--json']",
|
||||
"JSON invocation keeps diagnostic envelope on stdout",
|
||||
"versioned JSON report agrees with process exit",
|
||||
"invalid source write refuses with exact raw retention",
|
||||
"all packaged files and frozen ZIP unchanged after runtime checks"
|
||||
],
|
||||
"source_review": {
|
||||
"semantic_source": "01b76a1e89e93b7992f39e63560aa8b2ca2f69e0",
|
||||
"assembly_source": "a0a48e82bd43be181caca1fe2c1514c21f2bc405",
|
||||
"packaging_paths": {
|
||||
"scripts/package-artifacts.py": "24515fdbf4eebfb47b82383c6b3ccf290261cbd919f7f46a586b6488af769c8d",
|
||||
"scripts/package-cli-readme.md": "ea7bc971993e02751f24bc3cb313503a5f5fefd788540974e114398115c7c8e2",
|
||||
"scripts/package-cli-windows.ps1": "b6ec59bb55ff1e5624c782f492ce1d169131e708f257dcca195957edf7341c1a",
|
||||
"scripts/package-common.ps1": "d859453a8230b4f305d822c8742b371a71e35df698109a5d55c948f6dfaf0640",
|
||||
"scripts/package-windows.ps1": "28997ff78565619ed9c9b185f66dc922808f1f0be17f522b430c6c415448663d",
|
||||
"scripts/test-package-artifacts.py": "881718181a36fa5f7abfcd2e746bc1339446f9aeed6b2c039b8d2ce230ec202d",
|
||||
"scripts/test-package-cli-windows.ps1": "2c18f91149ede5c7b677a165bd0d8b31f3c4691642cc10dd1007a7a77bde912a",
|
||||
"scripts/test-windows-notices.ps1": "00a6ad1ed28fdb9044ceb7cc0714ceb7af095db2fb18f4828a053aa872a3147d",
|
||||
"scripts/windows-notices.ps1": "a5328c05579e1b9ca09142ee99e87c12994d12764f7c69448e392883d718ef01"
|
||||
},
|
||||
"studio_document_counts": {
|
||||
"windows": 65,
|
||||
"linux": 64
|
||||
}
|
||||
},
|
||||
"artifact": {
|
||||
"zip_sha256": "2b051f8ce1ea4f59ee7e233e1fbd516d423aadbfbca5d549755cbe78fcde1af6",
|
||||
"zip_bytes": 5091267,
|
||||
"file_count": 114,
|
||||
"files": {
|
||||
"arcadia.exe": "bad169f8be78dcfa74b32714569c886dd2132727080684fc2ba618d5822e3559",
|
||||
"dependency-notices.json": "d42920dc9958536d2d617bca17f453fc44b5e35f869b544c91abceb334cd51c1",
|
||||
"documentation-check.json": "c660725a9b9076f51c4e735c077ec149e84d9ec2db1748a129209a36b5542130",
|
||||
"libunwind.dll": "f45604a12e8bcd839fd86f667c1be1cc7c4f9da2b3c69ff381ac708f0e1ae7d4",
|
||||
"LICENSE": "51009f2cb07e5d19645858b2fbc037a672a25f8cbf7999522942b86807ecf365",
|
||||
"package.json": "efe3e2227477a0f0db49ddb3a8d4ed3f48c65f9d060dd911bb3dd82e569fc446",
|
||||
"README.md": "ea7bc971993e02751f24bc3cb313503a5f5fefd788540974e114398115c7c8e2",
|
||||
"SHA256SUMS.txt": "b0b3d1c32dce6a0d5b1551e5b1c1cc9aa67d50ab2143a8645cde1dffdff6a4f9",
|
||||
"START-HERE.txt": "58e0cb3c20dcb0e94d9acf7d47f390c32e47ed55f41e2d7be775e969be4a2626",
|
||||
"THIRD-PARTY-NOTICES.txt": "8d1ffe4a7841bd8220cae95b1625e4c715d799c5dee1ba54274e9c95a3d8fb44",
|
||||
"docs/api.rst": "ca7d310138c3b4d429d80a1d03d1ac8d4a9296069e205da573034256d10438a7",
|
||||
"docs/language.html": "67dcfa3477b48a0b07298ec81741840cc3ccff12c7d58164873d746d41d611c8",
|
||||
"docs/language.rst": "ac6b1d982c55f246de2f3c844b24b874599a3c20cfbd7ed22efe4561ac622b83",
|
||||
"docs/source-cli.md": "34f12b72cc6d6ead4f47c7818c1897db592f8ad493c70776db2ab48c24d8f051",
|
||||
"licenses/llvm-mingw-LICENSE.TXT": "8d85c1057d742e597985c7d4e6320b015a9139385cff4cbae06ffc0ebe89afee",
|
||||
"examples/release1/expected-diagnostics.json": "3700393d76b940cfc076bf52861bd7b1f43370f8eabce3594531bd0bf1fc851e",
|
||||
"examples/release1/model.json": "ef0ef4006c3e09d5db6e1b621256b3f7d6642f3a7b09cc34607cfbcca830c727",
|
||||
"examples/release1/model.rs": "15f30f6bde655808fe754665c26d560e36f2a9bad340f346b09137d0d05045f5",
|
||||
"examples/release1/project.arcadia": "b2674e6bf77cfe0f6f0ba79c81d2e96d92f39c02ca2c6266d80e91723bfb9fda",
|
||||
"examples/release1/README.md": "113cfb1cf38a98b51bff60442d86a35d636a3d80ef206bece7d53d0aae67f2d5",
|
||||
"examples/release1/traceability.arcadia": "48a96f543456c9b5a5798fc795594e4119147fe45f11a6f109f141b7f514560f",
|
||||
"examples/release1/transition-before.json": "2253e6434f741687e22327c37d520ed15c043bc36025498650bc2366bb6a874d",
|
||||
"examples/release1/transition-request.json": "43b9a22e5f95ad3bc0ebd2478ead0e14406854abc989ae94ded9367b013d812a",
|
||||
"examples/text-first/project.arcadia": "f441535c623e0cf20e1875eb2cd33b587ed00e078c37af39cb36d89bb5edc19a",
|
||||
"examples/text-first/README.md": "4ec8250d5ed556f1d496e8c85137e973e802ecb11df1a36488411e8cdc5a9f9e",
|
||||
"examples/release1/evidence/repair-check.md": "f4db17bdca4ecbb1bf0caef01b2da5f1f7f798ab3152b648ce6dd8696a6a68e5",
|
||||
"examples/release1/oa/capabilities.arcadia": "fc8012e5554350b12e1cb80080f8feab80d933c9b33c0f2a7bb500cb7d609936",
|
||||
"examples/release1/oa/model.arcadia": "f7cf914766ea73ec53b85e32d117ea9a9dba52d3e038246863541ae7069b1a15",
|
||||
"examples/release1/oa/participants.arcadia": "951764c14179f27c10e315acf2ea37d1fff54c77b8441e23addd239e8c715ce3",
|
||||
"examples/release1/oa/types.arcadia": "9d0396fb82dd5733edd8d1cf52f968ca84906927c704f9cf0944a7fae215cf49",
|
||||
"examples/release1/sa/capabilities.arcadia": "b6912fb3ece439912345fa71724acd462e716a1e5a88203a1058a5c3b2c20a4f",
|
||||
"examples/release1/sa/model.arcadia": "d431509251acb859fbb98733b6d18add952495e10a78703fbe8f31e40bc083c4",
|
||||
"examples/release1/sa/participants.arcadia": "780d2eb08b217b67b55febb3d34846f727b8ee9018df6c5741748647e36b2ff8",
|
||||
"examples/release1/sa/types.arcadia": "101fc0fbd99f1101514aa7b6da4f8e535aae0bb94c9e2b9e7e6c151135b28660",
|
||||
"examples/release1/views/diagram_oa_allocation_view.arcadia": "dfc0eb61e0e2aa5bda45524571fbfa3494f44ce19696f3e15e07438074a07bc7",
|
||||
"examples/release1/views/diagram_sa_allocation_view.arcadia": "b78b43a26a82efb8fbd5374b09c329fa341f57b29f635d397b9ef8aede71d389",
|
||||
"examples/release1/views/diagram_sa_capability_view.arcadia": "75e8dc9fb30f25d6f8fd3c6b595593ab4f98958a25798f961e336e0fac557649",
|
||||
"examples/release1/views/diagram_sa_service_view.arcadia": "b08c84f3ea633e7c78dfdad3272f75415f6256cf3614b6d99bfd4f0269076c9b",
|
||||
"examples/release1/views/diagram_sa_state_view.arcadia": "5f42f4c605e57b817d8400ed21f2b14bfc4e1592aebe941ff113767504c2b869",
|
||||
"examples/release1/oa/activities/activity_oa_restore_work.arcadia": "83e73d0a013d5c3e1a4af0d9139ab0cfa362d25a846671fc2dac59181061a9d4",
|
||||
"examples/release1/oa/behavior/process_oa_assessment_procedure.arcadia": "172c32c044ef4b5bc58a1f4635cc81f48dabb4014479b47dce7b25fedf2dbd0d",
|
||||
"examples/release1/oa/behavior/process_oa_field_procedure.arcadia": "d2552dc5383e2e91cb302a790364e197f663c2edc013a2ab1978789825c9c324",
|
||||
"examples/release1/oa/behavior/process_oa_response.arcadia": "cf86f46fa5c5d70592ef43f1bc1cb4d968efd8add384ba88036bef6b88aa4abc",
|
||||
"examples/release1/oa/behavior/scenario_oa_conversation.arcadia": "b25be524e98a35ebac5e4481b7609c027b32baf9605e8097f807560f94a77ca6",
|
||||
"examples/release1/oa/behavior/scenario_oa_full_scenario.arcadia": "00a1edf2c9707a0eb62ac070c2d02467874f2c9c1f273cc6ddb53d6d65999a1c",
|
||||
"examples/release1/oa/behavior/state_machine_oa_monitor.arcadia": "447c9ad1359f4a31ecf975ad07c0b08d8e7e03191fb0858e2fc56f377b223cd5",
|
||||
"examples/release1/sa/behavior/process_sa_assessment_procedure.arcadia": "73928ced61c06cbc08728d7956676a50fd4c05aa5b7f1ff4f29ce27a0b0f2088",
|
||||
"examples/release1/sa/behavior/process_sa_field_procedure.arcadia": "886d462af83110cbfa55dd232a667ea1446761234471357cbb878794e98e8768",
|
||||
"examples/release1/sa/behavior/process_sa_response.arcadia": "041785fc22842423822431bd5e6e8d16094ebd0716965e7c765fb8c1d7549e8a",
|
||||
"examples/release1/sa/behavior/scenario_sa_conversation.arcadia": "97276f5757ee9aa12a0dbd9d3ccd58174b3091162bb8bcce357cec20a45f5d85",
|
||||
"examples/release1/sa/behavior/scenario_sa_full_scenario.arcadia": "e12fc406b99d60a2f7768aaa0b25ba560e8605f0b8e37f3a7cdd3b8443562b74",
|
||||
"examples/release1/sa/behavior/state_machine_sa_monitor.arcadia": "05ce29af99424c491c47fe2450dc6f5f8ce995511c01ec7e17bb5855be3c9736",
|
||||
"examples/release1/sa/functions/function_sa_audit_thresholds.arcadia": "e5a04325d52183956dca814dd90f4e56c96c6b7db0f4f202f203ecb49c3e0f4f",
|
||||
"examples/release1/sa/functions/function_sa_restore_work.arcadia": "fddeb67308051325041fee641fe758ddf43345b7c47b66d433616ad07fdccdd6",
|
||||
"licenses/llvm-libunwind/llvm-ea7d852a70e8-libunwind-LICENSE.TXT": "b5efebcaca80879234098e52d1725e6d9eb8fb96a19fce625d39184b705f7b6d",
|
||||
"licenses/rust/COPYING.RUNTIME": "28e85c5aa4af9b4f1dfe6b4817aa3eefb3eaaee7fd735045016f29ccf50276a1",
|
||||
"licenses/rust/COPYING3": "8ceb4b9ee5adedde47b31e975c1d90c73ad27b6b165a1dcd80c7c545eb65b903",
|
||||
"licenses/rust/COPYRIGHT-library.html": "68129500b616d5838629e68f55ff3aed5e096dacf60ce9eb41bbe599a563afa6",
|
||||
"licenses/rust/COPYRIGHT.html": "5b0c93fc4e6d4b072eaa521b1762d1e746f4c368f65e81a0e5308e2772dac85e",
|
||||
"licenses/dependencies/cfg-if-1.0.4/LICENSE-APACHE": "a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2",
|
||||
"licenses/dependencies/cfg-if-1.0.4/LICENSE-MIT": "378f5840b258e2779c39418f3f2d7b2ba96f1c7917dd6be0713f88305dbda397",
|
||||
"licenses/dependencies/fastrand-2.5.0/LICENSE-APACHE": "a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2",
|
||||
"licenses/dependencies/fastrand-2.5.0/LICENSE-MIT": "23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3",
|
||||
"licenses/dependencies/getrandom-0.4.3/LICENSE-APACHE": "aaff376532ea30a0cd5330b9502ad4a4c8bf769c539c87ffe78819d188a18ebf",
|
||||
"licenses/dependencies/getrandom-0.4.3/LICENSE-MIT": "523a42c25d245dde9c015f882cec7f4555aad883382a6cf19b4b7d9b2cd5419b",
|
||||
"licenses/dependencies/itoa-1.0.18/LICENSE-APACHE": "62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a",
|
||||
"licenses/dependencies/itoa-1.0.18/LICENSE-MIT": "23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3",
|
||||
"licenses/dependencies/memchr-2.8.3/COPYING": "01c266bced4a434da0051174d6bee16a4c82cf634e2679b6155d40d75012390f",
|
||||
"licenses/dependencies/memchr-2.8.3/LICENSE-MIT": "0f96a83840e146e43c0ec96a22ec1f392e0680e6c1226e6f3ba87e0740af850f",
|
||||
"licenses/dependencies/memchr-2.8.3/UNLICENSE": "7e12e5df4bae12cb21581ba157ced20e1986a0508dd10d0e8a4ab9a4cf94e85c",
|
||||
"licenses/dependencies/oa-cli-0.2.0/workspace-LICENSE": "51009f2cb07e5d19645858b2fbc037a672a25f8cbf7999522942b86807ecf365",
|
||||
"licenses/dependencies/oa-core-0.2.0/workspace-LICENSE": "51009f2cb07e5d19645858b2fbc037a672a25f8cbf7999522942b86807ecf365",
|
||||
"licenses/dependencies/once_cell-1.21.4/LICENSE-APACHE": "a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2",
|
||||
"licenses/dependencies/once_cell-1.21.4/LICENSE-MIT": "23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3",
|
||||
"licenses/dependencies/proc-macro2-1.0.107/LICENSE-APACHE": "62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a",
|
||||
"licenses/dependencies/proc-macro2-1.0.107/LICENSE-MIT": "23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3",
|
||||
"licenses/dependencies/quote-1.0.47/LICENSE-APACHE": "62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a",
|
||||
"licenses/dependencies/quote-1.0.47/LICENSE-MIT": "23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3",
|
||||
"licenses/dependencies/rand_core-0.10.1/COPYRIGHT": "92b81db30f7ab6d693e0af5661a40d273ca1947e891123ed823924192c10cbc7",
|
||||
"licenses/dependencies/rand_core-0.10.1/LICENSE-APACHE": "6df43f6f4b5d4587f3d8d71e45532c688fd168afa5fe89d571cb32fa09c4ef51",
|
||||
"licenses/dependencies/rand_core-0.10.1/LICENSE-MIT": "8b6e9feec03e7c9a5facb26855cecd31662bf989b636bcfe79521bdf8ac863f0",
|
||||
"licenses/dependencies/same-file-1.0.6/COPYING": "01c266bced4a434da0051174d6bee16a4c82cf634e2679b6155d40d75012390f",
|
||||
"licenses/dependencies/same-file-1.0.6/LICENSE-MIT": "cb3c929a05e6cbc9de9ab06a4c57eeb60ca8c724bef6c138c87d3a577e27aa14",
|
||||
"licenses/dependencies/same-file-1.0.6/UNLICENSE": "7e12e5df4bae12cb21581ba157ced20e1986a0508dd10d0e8a4ab9a4cf94e85c",
|
||||
"licenses/dependencies/serde-1.0.229/LICENSE-APACHE": "62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a",
|
||||
"licenses/dependencies/serde-1.0.229/LICENSE-MIT": "23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3",
|
||||
"licenses/dependencies/serde_core-1.0.229/LICENSE-APACHE": "62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a",
|
||||
"licenses/dependencies/serde_core-1.0.229/LICENSE-MIT": "23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3",
|
||||
"licenses/dependencies/serde_derive-1.0.229/LICENSE-APACHE": "62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a",
|
||||
"licenses/dependencies/serde_derive-1.0.229/LICENSE-MIT": "23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3",
|
||||
"licenses/dependencies/serde_json-1.0.151/LICENSE-APACHE": "62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a",
|
||||
"licenses/dependencies/serde_json-1.0.151/LICENSE-MIT": "23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3",
|
||||
"licenses/dependencies/sha1_smol-1.0.1/LICENSE": "282a21b0710441c364025461a447362daf18f5798a7b72ae2f801a2aa536da51",
|
||||
"licenses/dependencies/syn-3.0.5/LICENSE-APACHE": "62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a",
|
||||
"licenses/dependencies/syn-3.0.5/LICENSE-MIT": "23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3",
|
||||
"licenses/dependencies/tempfile-3.27.0/LICENSE-APACHE": "a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2",
|
||||
"licenses/dependencies/tempfile-3.27.0/LICENSE-MIT": "8b427f5bc501764575e52ba4f9d95673cf8f6d80a86d0d06599852e1a9a20a36",
|
||||
"licenses/dependencies/thiserror-2.0.20/LICENSE-APACHE": "62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a",
|
||||
"licenses/dependencies/thiserror-2.0.20/LICENSE-MIT": "23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3",
|
||||
"licenses/dependencies/thiserror-impl-2.0.20/LICENSE-APACHE": "62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a",
|
||||
"licenses/dependencies/thiserror-impl-2.0.20/LICENSE-MIT": "23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3",
|
||||
"licenses/dependencies/unicode-ident-1.0.24/LICENSE-APACHE": "62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a",
|
||||
"licenses/dependencies/unicode-ident-1.0.24/LICENSE-MIT": "23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3",
|
||||
"licenses/dependencies/unicode-ident-1.0.24/LICENSE-UNICODE": "f7db81051789b729fea528a63ec4c938fdcb93d9d61d97dc8cc2e9df6d47f2a1",
|
||||
"licenses/dependencies/uuid-1.26.0/LICENSE-APACHE": "a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2",
|
||||
"licenses/dependencies/uuid-1.26.0/LICENSE-MIT": "436bc5a105d8e57dcd8778730f3754f7bf39c14d2f530e4cde4bd2d17a83ec3d",
|
||||
"licenses/dependencies/winapi-util-0.1.11/COPYING": "01c266bced4a434da0051174d6bee16a4c82cf634e2679b6155d40d75012390f",
|
||||
"licenses/dependencies/winapi-util-0.1.11/LICENSE-MIT": "cb3c929a05e6cbc9de9ab06a4c57eeb60ca8c724bef6c138c87d3a577e27aa14",
|
||||
"licenses/dependencies/winapi-util-0.1.11/UNLICENSE": "7e12e5df4bae12cb21581ba157ced20e1986a0508dd10d0e8a4ab9a4cf94e85c",
|
||||
"licenses/dependencies/windows-link-0.2.1/license-apache-2.0": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b",
|
||||
"licenses/dependencies/windows-link-0.2.1/license-mit": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383",
|
||||
"licenses/dependencies/windows-sys-0.61.2/license-apache-2.0": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b",
|
||||
"licenses/dependencies/windows-sys-0.61.2/license-mit": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383",
|
||||
"licenses/dependencies/zmij-1.0.23/LICENSE-MIT": "23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"
|
||||
},
|
||||
"notice_packages": 27,
|
||||
"notice_reference_files": 60,
|
||||
"package_claims": {
|
||||
"product": "Arcadia CLI",
|
||||
"version": "0.2.0-preview.authoring.20260911",
|
||||
"target": "x86_64-pc-windows-gnullvm",
|
||||
"acceptance": "Packaging integrity only; clean-environment CLI verification required.",
|
||||
"build": "Prebuilt input; source/compiler/profile assertions are recorded, not inferred.",
|
||||
"build_evidence": {
|
||||
"assertion": "Caller-supplied build evidence; target and binary hashes checked, source/compiler/profile statements not inferred or independently proven by packaging.",
|
||||
"evidence": {
|
||||
"source_identity": "CLI semantic/build-input basis 01b76a1e89e93b7992f39e63560aa8b2ca2f69e0. Six original CLI manifest/workspace/lock/source/test hashes and 95 unchanged core/shared build inputs independently matched. Packaging assembly basis a0a48e82bd43be181caca1fe2c1514c21f2bc405 changes packaging only and is not claimed as the original compiler input commit.",
|
||||
"profile": "Preserved Windows debug/development artifact from cargo build --locked --offline -p oa-cli --bin arcadia, successful September11 2026 11:45:43.6035381Z,9.7336637seconds. No release-profile or reproducible-build claim.",
|
||||
"compiler": "rustc1.98.1 commit48a229ceaefd4985c50990b14116b6d856af0985,x86_64-pc-windows-gnullvm,LLVM22.1.8. Frozen executable embeds that compiler path; current toolchain target-library hashes match preserved inventory. Original compile/test/lint and independent frozen-binary review records retained separately.",
|
||||
"target": "x86_64-pc-windows-gnullvm",
|
||||
"binary_sha256": {
|
||||
"arcadia.exe": "bad169f8be78dcfa74b32714569c886dd2132727080684fc2ba618d5822e3559"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"environment": {
|
||||
"SystemRoot": "C:\\Windows",
|
||||
"WINDIR": "C:\\Windows",
|
||||
"PATH": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\cli-packaging-review-20260911\\extracted\\ArcadiaCLI-0.2.0-preview.authoring.20260911-x86_64-pc-windows-gnullvm;C:\\Windows\\System32;C:\\Windows",
|
||||
"TEMP": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\cli-packaging-review-20260911\\runtime\\temp",
|
||||
"TMP": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\cli-packaging-review-20260911\\runtime\\temp"
|
||||
},
|
||||
"invocations": [
|
||||
{
|
||||
"arguments": [
|
||||
"--version"
|
||||
],
|
||||
"exit": 0,
|
||||
"stdout_sha256": "0ebfaa91da207b98ef56f307b64a3f083359812b2655870f4a61459733d8590c",
|
||||
"stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"arguments": [
|
||||
"--help"
|
||||
],
|
||||
"exit": 0,
|
||||
"stdout_sha256": "e41dd6452098023fd333c52cc210e5c2a7e08d20c377e0622deadb90d274f28f",
|
||||
"stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"arguments": [
|
||||
"check",
|
||||
"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\cli-packaging-review-20260911\\runtime\\examples\\text-first\\project.arcadia",
|
||||
"--json"
|
||||
],
|
||||
"exit": 0,
|
||||
"stdout_sha256": "64e6c3cb866801ec441a040406ce5111747fcb0c04a0cbfae3ab4fe0acac6fca",
|
||||
"stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"arguments": [
|
||||
"check",
|
||||
"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\cli-packaging-review-20260911\\runtime\\examples\\release1\\project.arcadia",
|
||||
"--json"
|
||||
],
|
||||
"exit": 0,
|
||||
"stdout_sha256": "0c29e2b2d297cfb06f0359ea1c6565997b1f396f442fc5684f12dbff8eca84bf",
|
||||
"stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"arguments": [
|
||||
"stabilize",
|
||||
"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\cli-packaging-review-20260911\\runtime\\exact-source\\project.arcadia",
|
||||
"--json"
|
||||
],
|
||||
"exit": 0,
|
||||
"stdout_sha256": "849cff20bd541febf6133a4206fd4dddab396857b361add78093cf01f55aeff4",
|
||||
"stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"arguments": [
|
||||
"stabilize",
|
||||
"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\cli-packaging-review-20260911\\runtime\\exact-source\\project.arcadia",
|
||||
"--write",
|
||||
"--json"
|
||||
],
|
||||
"exit": 0,
|
||||
"stdout_sha256": "beec44418f383d205522b6784b0aba096466e41da3b0728eb8798991149e2143",
|
||||
"stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"arguments": [
|
||||
"rename-alias",
|
||||
"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\cli-packaging-review-20260911\\runtime\\exact-source\\project.arcadia",
|
||||
"crew",
|
||||
"rescue_team",
|
||||
"--json"
|
||||
],
|
||||
"exit": 0,
|
||||
"stdout_sha256": "1267c7a17e2701a2dd6b91cbaacdd0e52fdb8b031660d1f788542921aa5948bb",
|
||||
"stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"arguments": [
|
||||
"rename-alias",
|
||||
"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\cli-packaging-review-20260911\\runtime\\exact-source\\project.arcadia",
|
||||
"crew",
|
||||
"rescue_team",
|
||||
"--write",
|
||||
"--json"
|
||||
],
|
||||
"exit": 0,
|
||||
"stdout_sha256": "0967d3dff03ebf5c5bfcfe716af8e369e23e0289885c02b83beeb4f92d5e008d",
|
||||
"stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"arguments": [
|
||||
"stabilize",
|
||||
"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\cli-packaging-review-20260911\\runtime\\exact-source\\project.arcadia",
|
||||
"--write",
|
||||
"--json"
|
||||
],
|
||||
"exit": 0,
|
||||
"stdout_sha256": "b95dfd2d8effc49df2a7fe893cd414a5edad9c4686c5dada4faca72ef40360b7",
|
||||
"stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"arguments": [
|
||||
"stabilize",
|
||||
"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\cli-packaging-review-20260911\\runtime\\invalid.arcadia",
|
||||
"--write",
|
||||
"--json"
|
||||
],
|
||||
"exit": 1,
|
||||
"stdout_sha256": "546ab1d5bb19dd8fa22dd6f017cc0e58b91602ccc24a048c41602b0dae45c70b",
|
||||
"stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
}
|
||||
],
|
||||
"runtime": {
|
||||
"fixed_source_ids": [
|
||||
"c073d2cf-83e5-4c98-8eaf-4a465ff0cf71",
|
||||
"1987c990-e3cb-4f0e-9be9-fe61f007b997",
|
||||
"9ba9e3cd-96f5-4f9a-865e-6b31ba94b4e6",
|
||||
"ffcf7e41-f94b-4f7c-99c1-1a30a0b46959"
|
||||
],
|
||||
"allocated_activity_id": "9ba9e3cd-96f5-4f9a-865e-6b31ba94b4e6",
|
||||
"final_source_sha256": {
|
||||
"detached.arcadia": "ae79cfc57c5e227c281f1ad8493c1cdb483700fe0fe78ec8e448135afc98d239",
|
||||
"project.arcadia": "1352c29a8cce5b5dbd7563b086680f889bdf35198f010d0083e99dd25e1007be",
|
||||
"modules/oa.arcadia": "a781e165aaddeecb05fc9110873dd1e00e7a30687cef9cded11107ee2846c162"
|
||||
},
|
||||
"source_exact_expected": {
|
||||
"detached.arcadia": "// known detached bytes remain\r\n",
|
||||
"project.arcadia": "arcadia 7;\r\nproject review \"CLI package 救助\" id \"c073d2cf-83e5-4c98-8eaf-4a465ff0cf71\" {\r\n description \"Literal @crew remains\";\r\n include \"modules/oa.arcadia\";\r\n}\r\n",
|
||||
"modules/oa.arcadia": "// @crew stays in this comment; Ω\r\noa_actor rescue_team \"Crew literal @crew\" id \"1987c990-e3cb-4f0e-9be9-fe61f007b997\" {}\r\noa_activity observe \"Observe Ω\" id \"9ba9e3cd-96f5-4f9a-865e-6b31ba94b4e6\" {}\r\nallocation responsibility \"Assigned @crew\" id \"ffcf7e41-f94b-4f7c-99c1-1a30a0b46959\" { source @rescue_team; target @observe; }\r\n"
|
||||
},
|
||||
"stabilization_note": "Separate preview UUID remains provisional; each result is checked by literal declaration role, not assumed equal across invocations."
|
||||
},
|
||||
"status": "PASS",
|
||||
"finished_utc": "2026-09-11T12:43:12Z",
|
||||
"script_sha256": "f85d1f34ab75b4067efe09886e447bf9fd54dc232020bcb1a46404ec57b2351e"
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
"""Independent CLI packaging review: read-only source checks and isolated CLI runs."""
|
||||
from pathlib import Path, PurePosixPath
|
||||
import ast
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import tomllib
|
||||
import traceback
|
||||
import uuid
|
||||
import zipfile
|
||||
|
||||
OUT = Path(__file__).resolve().parent
|
||||
ROOT = OUT.parents[1]
|
||||
SOURCE = ROOT / '.local/cli-packaging-20260911/source'
|
||||
NAME = 'ArcadiaCLI-0.2.0-preview.authoring.20260911-x86_64-pc-windows-gnullvm'
|
||||
ZIP = SOURCE.parent / 'packages' / (NAME + '.zip')
|
||||
PACKAGE = OUT / 'extracted' / NAME
|
||||
BASE = '01b76a1e89e93b7992f39e63560aa8b2ca2f69e0'
|
||||
ASSEMBLY = 'a0a48e82bd43be181caca1fe2c1514c21f2bc405'
|
||||
ZIP_SHA = '2b051f8ce1ea4f59ee7e233e1fbd516d423aadbfbca5d549755cbe78fcde1af6'
|
||||
EXE_SHA = 'bad169f8be78dcfa74b32714569c886dd2132727080684fc2ba618d5822e3559'
|
||||
DLL_SHA = 'f45604a12e8bcd839fd86f667c1be1cc7c4f9da2b3c69ff381ac708f0e1ae7d4'
|
||||
RESULT = {'started_utc': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), 'checks': []}
|
||||
|
||||
|
||||
def check(ok, label):
|
||||
if not ok:
|
||||
raise AssertionError(label)
|
||||
RESULT['checks'].append(label)
|
||||
|
||||
|
||||
def sha(b):
|
||||
return hashlib.sha256(b).hexdigest()
|
||||
|
||||
|
||||
def read_json(p):
|
||||
return json.loads(p.read_text(encoding='utf-8-sig'))
|
||||
|
||||
|
||||
def record(path, obj):
|
||||
with path.open('x', encoding='utf-8', newline='\n') as f:
|
||||
json.dump(obj, f, indent=2, ensure_ascii=False)
|
||||
f.write('\n')
|
||||
|
||||
|
||||
def snapshot(directory):
|
||||
files = {}
|
||||
for p in directory.rglob('*'):
|
||||
if p.is_symlink() or getattr(p.lstat(), 'st_file_attributes', 0) & 0x400:
|
||||
raise AssertionError('unexpected reparse path ' + str(p))
|
||||
if p.is_file():
|
||||
files[p.relative_to(directory).as_posix()] = p.read_bytes()
|
||||
return files
|
||||
|
||||
|
||||
def git(path, ref=BASE):
|
||||
return subprocess.run(['git', 'show', ref + ':' + path], cwd=ROOT, check=True, capture_output=True).stdout
|
||||
|
||||
|
||||
def source_checks():
|
||||
old = git('scripts/package-artifacts.py').decode()
|
||||
new = (ROOT / 'scripts/package-artifacts.py').read_text()
|
||||
original_functions = {f.name: ast.dump(f, include_attributes=False) for f in ast.parse(old).body if isinstance(f, ast.FunctionDef)}
|
||||
new_functions = {f.name: ast.dump(f, include_attributes=False) for f in ast.parse(new).body if isinstance(f, ast.FunctionDef)}
|
||||
check(all(new_functions[k] == v for k, v in original_functions.items() if k not in ('document_inputs', 'main')), 'all original ELF/tar/document/notice helpers unchanged outside product selection')
|
||||
before, after = {'__name__': 'review_baseline'}, {'__name__': 'review_current'}
|
||||
exec(compile(old, '<baseline packaging>', 'exec'), before)
|
||||
exec(compile(new, '<current packaging>', 'exec'), after)
|
||||
counts = {}
|
||||
for platform in ('windows', 'linux'):
|
||||
expected = before['document_inputs'](SOURCE, platform)
|
||||
default = after['document_inputs'](SOURCE, platform)
|
||||
explicit = after['document_inputs'](SOURCE, platform, 'studio')
|
||||
check(default == explicit == expected, platform + ' actual full Studio document closure unchanged')
|
||||
counts[platform] = len(default['files'])
|
||||
old_notice = git('scripts/windows-notices.ps1').decode().splitlines()
|
||||
new_notice = (ROOT / 'scripts/windows-notices.ps1').read_text().splitlines()
|
||||
check(len(old_notice) == len(new_notice) and [i for i, pair in enumerate(zip(old_notice, new_notice)) if pair[0] != pair[1]] == [3], 'only notice source delta is oa-cli allowed-root check; default/body unchanged')
|
||||
for p in ('scripts/package-linux.ps1', 'deployment/Dockerfile', 'deployment/Dockerfile.dockerignore'):
|
||||
check(git(p).decode().splitlines() == (ROOT / p).read_text().splitlines(), 'Linux entry/deployment unchanged: ' + p)
|
||||
old_windows = git('scripts/package-windows.ps1').decode().replace('\r\n', '\n')
|
||||
common = (ROOT / 'scripts/package-common.ps1').read_text()
|
||||
reader_old = old_windows.split('function Read-PeImports', 1)[1].split('\n# Windows system components', 1)[0].strip()
|
||||
reader_new = common.split('function Read-PackagePeImports', 1)[1].split('\nfunction Get-PackageWindowsInputs', 1)[0].strip()
|
||||
check(reader_old == reader_new, 'PE reader extraction is byte-equivalent after function rename/EOL normalization')
|
||||
old_allowlist = re.search(r'\$systemDlls = @\((.*?)\n\)', old_windows, re.S)[1]
|
||||
new_allowlist = re.search(r'\$systemDlls = @\((.*?)\n \)', common, re.S)[1]
|
||||
check(re.findall(r"'([^']+)'", old_allowlist) == re.findall(r"'([^']+)'", new_allowlist), 'Windows system DLL allowlist unchanged')
|
||||
new_windows = (ROOT / 'scripts/package-windows.ps1').read_text()
|
||||
check("Get-PackageWindowsInputs $BinaryDirectory $Target $LlvmMinGWRoot @('arcadia-studio.exe', 'arcadia-server.exe')" in new_windows, 'Studio explicitly retains both original binary inputs')
|
||||
check(old_windows.split('$evidence = Read-PackageBuildEvidence', 1)[1].splitlines() == new_windows.split('$evidence = Read-PackageBuildEvidence', 1)[1].splitlines(), 'Studio post-inspection packaging pipeline unchanged')
|
||||
delta = subprocess.run(['git', 'diff', '--name-only', BASE, ASSEMBLY], cwd=ROOT, capture_output=True, check=True).stdout.decode().splitlines()
|
||||
check(len(delta) == 9 and all(p.startswith('scripts/') for p in delta), 'assembly delta exactly nine packaging/script paths')
|
||||
check(all((ROOT / p).read_bytes() == (SOURCE / p).read_bytes() for p in delta), 'reviewed main packaging sources equal frozen assembly bytes')
|
||||
RESULT['source_review'] = {'semantic_source': BASE, 'assembly_source': ASSEMBLY, 'packaging_paths': {p: sha((ROOT / p).read_bytes()) for p in delta}, 'studio_document_counts': counts}
|
||||
|
||||
|
||||
def artifact():
|
||||
check(not PACKAGE.parent.exists(), 'fresh extraction directory')
|
||||
check(ZIP.stat().st_size == 5091267 and sha(ZIP.read_bytes()) == ZIP_SHA, 'frozen ZIP size and hash')
|
||||
with zipfile.ZipFile(ZIP) as z:
|
||||
names = z.namelist()
|
||||
check(len(names) == 114 and len({n.casefold() for n in names}) == 114, '114 unique ZIP files')
|
||||
check(all(n.startswith(NAME + '/') and PurePosixPath(n).as_posix() == n and '..' not in PurePosixPath(n).parts and '\\' not in n and ':' not in n and all(not p.endswith((' ', '.')) for p in PurePosixPath(n).parts) for n in names), 'single safe normalized Windows ZIP root')
|
||||
check(all(not i.is_dir() and stat.S_IFMT(i.external_attr >> 16) in (0, stat.S_IFREG) and i.date_time == (2000, 1, 1, 0, 0, 0) for i in z.infolist()) and z.testzip() is None, 'ordinary fixed-time ZIP members and CRCs')
|
||||
PACKAGE.parent.mkdir()
|
||||
z.extractall(PACKAGE.parent)
|
||||
archived = {n[len(NAME) + 1:]: z.read(n) for n in names}
|
||||
actual = snapshot(PACKAGE)
|
||||
check(actual == archived, 'complete clean extraction equals ZIP bytes')
|
||||
hashes = {p: sha(b) for p, b in actual.items()}
|
||||
lines = actual['SHA256SUMS.txt'].decode().splitlines()
|
||||
sums = dict((line[66:], line[:64]) for line in lines)
|
||||
check(len(sums) == len(lines) and sums == {p: h for p, h in hashes.items() if p != 'SHA256SUMS.txt'}, 'complete checksum inventory equals every other extracted file')
|
||||
package = read_json(PACKAGE / 'package.json')
|
||||
generated = {'SHA256SUMS.txt', 'START-HERE.txt', 'THIRD-PARTY-NOTICES.txt', 'dependency-notices.json', 'documentation-check.json', 'package.json'}
|
||||
check(set(actual) - set(package['input_sha256']) == generated and all(hashes[p] == h for p, h in package['input_sha256'].items()), 'all package inputs plus exactly six generated records')
|
||||
check(hashes['arcadia.exe'] == EXE_SHA and hashes['libunwind.dll'] == DLL_SHA, 'frozen CLI and runtime hashes')
|
||||
check(not {'arcadia-studio.exe', 'arcadia-server.exe'} & set(actual), 'no unrelated executable payload')
|
||||
for p in actual:
|
||||
if p.startswith(('docs/', 'examples/')) or p == 'LICENSE':
|
||||
check((SOURCE / p).read_bytes() == actual[p], 'exact public document/example: ' + p)
|
||||
check(actual['README.md'] == (SOURCE / 'scripts/package-cli-readme.md').read_bytes(), 'CLI-specific README')
|
||||
notice = read_json(PACKAGE / 'dependency-notices.json')
|
||||
check(notice['root_packages'] == ['oa-cli'] and len(notice['packages']) == 27, '27 dependency packages; only CLI root')
|
||||
check(notice['cargo_lock_sha256'] == sha((SOURCE / 'Cargo.lock').read_bytes()) == '2b960242c33eb839ff008d01731f6d5cdb4e047ca8cd6e13151cbf9c09913510', 'original CLI Cargo lock identity')
|
||||
lock = {(p['name'], p['version'], p.get('source')): p for p in tomllib.loads((SOURCE / 'Cargo.lock').read_text())['package']}
|
||||
references = []
|
||||
for p in notice['packages']:
|
||||
check(lock[p['name'], p['version'], p['source']].get('checksum') == p['registry_checksum'], 'locked dependency: ' + p['name'])
|
||||
references.extend(p['notices'])
|
||||
for f in p['embedded_fonts']:
|
||||
references.extend(f['notices'])
|
||||
if p['source'] is None:
|
||||
local = snapshot(SOURCE / 'crates' / p['name'])
|
||||
local = {k: sha(v) for k, v in local.items() if 'target' not in PurePosixPath(k).parts and PurePosixPath(k).name != '.cargo-ok'}
|
||||
check(local == p['local_source_sha256'], 'complete local crate inputs: ' + p['name'])
|
||||
references += notice['rust']['notices'] + notice['runtime']['notices']
|
||||
check(all(hashes[i['path']] == i['sha256'] for i in references), 'all package/compiler/runtime notice references and bytes')
|
||||
check(notice['runtime']['sha256'] == DLL_SHA, 'runtime notice binds packaged DLL')
|
||||
check(all(sha((SOURCE / p).read_bytes()) == h for p, h in notice['workspace_build_inputs_sha256'].items()), 'all shared build input hashes')
|
||||
original = read_json(ROOT / '.local/source-cli-20260911/verified/manifest.json')
|
||||
cli_inputs = [i for i in original['Artifacts'] if not i['Path'].startswith('.local/')]
|
||||
check(len(cli_inputs) == 6 and all(sha((SOURCE / i['Path']).read_bytes()) == i['SHA256'].lower() for i in cli_inputs), 'six original CLI manifest inputs')
|
||||
core_inputs = [i for i in read_json(ROOT / '.local/behavior-creation-20260911/gui-verified/manifest.json')['inputs'] if i['path'].startswith(('crates/oa-core/', 'build/'))]
|
||||
check(len(core_inputs) == 95 and all(sha((SOURCE / i['path']).read_bytes()) == i['sha256'].lower() for i in core_inputs), '95 original core/shared build inputs')
|
||||
RESULT['artifact'] = {'zip_sha256': ZIP_SHA, 'zip_bytes': ZIP.stat().st_size, 'file_count': len(actual), 'files': hashes, 'notice_packages': 27, 'notice_reference_files': len({i['path'] for i in references}), 'package_claims': {k: package[k] for k in ('product', 'version', 'target', 'acceptance', 'build', 'build_evidence')}}
|
||||
return actual
|
||||
|
||||
|
||||
def runtime(original_package):
|
||||
run = OUT / 'runtime'
|
||||
run.mkdir()
|
||||
temp = run / 'temp'
|
||||
temp.mkdir()
|
||||
windows = Path(os.environ['SystemRoot'])
|
||||
env = {'SystemRoot': str(windows), 'WINDIR': str(windows), 'PATH': ';'.join(map(str, (PACKAGE, windows / 'System32', windows))), 'TEMP': str(temp), 'TMP': str(temp)}
|
||||
RESULT['environment'] = env
|
||||
RESULT['invocations'] = []
|
||||
def invoke(args, expect=0, report=True):
|
||||
index = len(RESULT['invocations']) + 1
|
||||
r = subprocess.run([str(PACKAGE / 'arcadia.exe'), *map(str, args)], cwd=run, env=env, capture_output=True, timeout=30, creationflags=subprocess.CREATE_NO_WINDOW)
|
||||
(run / f'{index:02d}.stdout').write_bytes(r.stdout)
|
||||
(run / f'{index:02d}.stderr').write_bytes(r.stderr)
|
||||
RESULT['invocations'].append({'arguments': list(map(str, args)), 'exit': r.returncode, 'stdout_sha256': sha(r.stdout), 'stderr_sha256': sha(r.stderr)})
|
||||
check(r.returncode == expect, 'CLI expected exit: ' + str(args))
|
||||
if not report:
|
||||
return r.stdout
|
||||
check(not r.stderr, 'JSON invocation keeps diagnostic envelope on stdout')
|
||||
value = json.loads(r.stdout)
|
||||
check(value['format_version'] == 1 and value['exit_code'] == expect, 'versioned JSON report agrees with process exit')
|
||||
return value
|
||||
check(invoke(['--version'], report=False).decode().strip() == 'arcadia 0.2.0', 'underlying CLI version')
|
||||
help_text = invoke(['--help'], report=False).decode()
|
||||
check(all(word in help_text for word in ('check ROOT', 'stabilize ROOT', 'rename-alias ROOT OLD NEW', '--write', 'provisional')), 'packaged help has command/preview contract')
|
||||
examples = run / 'examples'
|
||||
shutil.copytree(PACKAGE / 'examples', examples)
|
||||
before_examples = snapshot(examples)
|
||||
for relative in ('text-first/project.arcadia', 'release1/project.arcadia'):
|
||||
r = invoke(['check', examples / relative, '--json'])
|
||||
check(r['status'] == 'checked' and not r['written'] and not r['changes'], 'packaged example check: ' + relative)
|
||||
check(snapshot(examples) == before_examples, 'checks preserve complete copied examples byte-for-byte')
|
||||
fixture = run / 'exact-source'
|
||||
(fixture / 'modules').mkdir(parents=True)
|
||||
entry = 'arcadia 7;\r\nproject review "CLI package 救助" id "c073d2cf-83e5-4c98-8eaf-4a465ff0cf71" {\r\n description "Literal @crew remains";\r\n include "modules/oa.arcadia";\r\n}\r\n'
|
||||
module = '// @crew stays in this comment; Ω\r\noa_actor crew "Crew literal @crew" id "1987c990-e3cb-4f0e-9be9-fe61f007b997" {}\r\noa_activity observe "Observe Ω" {}\r\nallocation responsibility "Assigned @crew" id "ffcf7e41-f94b-4f7c-99c1-1a30a0b46959" { source @crew; target @observe; }\r\n'
|
||||
(fixture / 'project.arcadia').write_bytes(entry.encode())
|
||||
(fixture / 'modules/oa.arcadia').write_bytes(module.encode())
|
||||
(fixture / 'detached.arcadia').write_bytes(b'// known detached bytes remain\r\n')
|
||||
before = snapshot(fixture)
|
||||
root = fixture / 'project.arcadia'
|
||||
def stabilized(report):
|
||||
check(len(report['changes']) == 1 and report['changes'][0]['path'] == 'modules/oa.arcadia' and report['changes'][0]['before'] == module, 'only missing-ID source changes; exact captured raw bytes')
|
||||
text = report['changes'][0]['after']
|
||||
match = re.search(r'oa_activity observe "Observe Ω" id "([0-9a-f-]{36})"', text)
|
||||
check(match is not None, 'fresh ID is attached to literal intended activity')
|
||||
identity = str(uuid.UUID(match[1]))
|
||||
check(identity not in (entry + module), 'allocated identity differs from every preexisting identity')
|
||||
expected = module.replace('oa_activity observe "Observe Ω" {}', f'oa_activity observe "Observe Ω" id "{identity}" {{}}')
|
||||
check(text == expected, 'stabilization preserves every other character, CRLF, comment, name and reference')
|
||||
return text, identity
|
||||
preview = invoke(['stabilize', root, '--json'])
|
||||
check(preview['status'] == 'preview' and not preview['written'] and preview['identities_to_stabilize'] == 1, 'stabilization is preview by default')
|
||||
stabilized(preview)
|
||||
check(snapshot(fixture) == before, 'preview preserves all physical source bytes including detached file')
|
||||
written = invoke(['stabilize', root, '--write', '--json'])
|
||||
check(written['status'] == 'written' and written['written'], 'explicit stabilization writes')
|
||||
text, identity = stabilized(written)
|
||||
stable_expected = dict(before, **{'modules/oa.arcadia': text.encode()})
|
||||
check(snapshot(fixture) == stable_expected, 'stabilized complete physical file inventory and bytes')
|
||||
renamed = text.replace('oa_actor crew ', 'oa_actor rescue_team ').replace('source @crew;', 'source @rescue_team;')
|
||||
preview = invoke(['rename-alias', root, 'crew', 'rescue_team', '--json'])
|
||||
check(not preview['written'] and preview['status'] == 'preview' and [(c['path'], c['before'], c['after']) for c in preview['changes']] == [('modules/oa.arcadia', text, renamed)], 'alias preview changes exact declaration and typed reference only')
|
||||
check(snapshot(fixture) == stable_expected, 'rename preview retains exact source bytes and identities')
|
||||
saved = invoke(['rename-alias', root, 'crew', 'rescue_team', '--write', '--json'])
|
||||
renamed_expected = dict(before, **{'modules/oa.arcadia': renamed.encode()})
|
||||
check(saved['written'] and snapshot(fixture) == renamed_expected, 'explicit alias write matches full independent raw expected files')
|
||||
expected_ids = re.findall(r'\bid "([0-9a-f-]{36})"', entry + text)
|
||||
check(re.findall(r'\bid "([0-9a-f-]{36})"', entry + renamed) == expected_ids and len(expected_ids) == 4, 'all four identities remain ordered and exact through rename')
|
||||
timestamps = {p: (fixture / p).stat().st_mtime_ns for p in renamed_expected}
|
||||
noop = invoke(['stabilize', root, '--write', '--json'])
|
||||
check(noop['status'] == 'unchanged' and not noop['written'] and not noop['changes'] and noop['identities_to_stabilize'] == 0, 'explicit no-op reports unchanged')
|
||||
check(snapshot(fixture) == renamed_expected and timestamps == {p: (fixture / p).stat().st_mtime_ns for p in renamed_expected}, 'no-op preserves all file bytes and mtimes')
|
||||
invalid = run / 'invalid.arcadia'
|
||||
invalid.write_bytes(b'arcadia 7;\r\nproject broken "Unclosed" {\r\n')
|
||||
invalid_before = invalid.read_bytes()
|
||||
refusal = invoke(['stabilize', invalid, '--write', '--json'], expect=1)
|
||||
check(not refusal['written'] and invalid.read_bytes() == invalid_before, 'invalid source write refuses with exact raw retention')
|
||||
check(snapshot(PACKAGE) == original_package and sha(ZIP.read_bytes()) == ZIP_SHA, 'all packaged files and frozen ZIP unchanged after runtime checks')
|
||||
RESULT['runtime'] = {'fixed_source_ids': expected_ids, 'allocated_activity_id': identity, 'final_source_sha256': {p: sha(b) for p, b in renamed_expected.items()}, 'source_exact_expected': {p: b.decode() for p, b in renamed_expected.items()}, 'stabilization_note': 'Separate preview UUID remains provisional; each result is checked by literal declaration role, not assumed equal across invocations.'}
|
||||
|
||||
|
||||
try:
|
||||
check(not (OUT / 'result.json').exists(), 'preserve earlier evidence; no overwrite')
|
||||
source_checks()
|
||||
frozen = artifact()
|
||||
runtime(frozen)
|
||||
RESULT['status'] = 'PASS'
|
||||
except Exception as error:
|
||||
RESULT['status'] = 'FAIL'
|
||||
RESULT['error'] = repr(error)
|
||||
RESULT['traceback'] = traceback.format_exc()
|
||||
RESULT['finished_utc'] = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
|
||||
RESULT['script_sha256'] = sha(Path(__file__).read_bytes())
|
||||
record(OUT / 'result.json', RESULT)
|
||||
print(json.dumps({k: RESULT[k] for k in ('status', 'started_utc', 'finished_utc')}, indent=2))
|
||||
print('checks:', len(RESULT['checks']), 'invocations:', len(RESULT.get('invocations', [])))
|
||||
if RESULT['status'] == 'FAIL':
|
||||
print(RESULT['traceback'])
|
||||
sys.exit(1)
|
||||
@@ -1 +0,0 @@
|
||||
PASS: 15 CLI/Windows packaging assertions; inert fixture retained at C:\Temp\arcadia-cli-package-2a5110e4fc074b6f9db9bfa623db315c
|
||||
@@ -1,47 +0,0 @@
|
||||
# CLI prerelease publication records — 2026-09-11
|
||||
|
||||
These four files are exact byte copies from `.local/cli-packaging-20260911/`.
|
||||
They contain sanitized publication results and release prose only. No credential,
|
||||
configuration, authentication helper or upload script is included.
|
||||
|
||||
| File | Bytes | SHA256 |
|
||||
| --- | ---: | --- |
|
||||
| `upload-binaries-verified.json` | 678 | `e945e2724020c48bfe15be7b4258861bd6c01291b98eae036eca786bb022ebde` |
|
||||
| `upload-validation-verified.json` | 277 | `73581acbb5b5fe4aca1fda1b626f80a28bcb3d6c036f7ce793c62bf5a8ef4a72` |
|
||||
| `published-verified.json` | 382 | `b5fe8a38579fb8399c78bca9db9fd91b64e8b7b7de7c664049cf5e615a671100` |
|
||||
| `RELEASE-NOTES.md` | 2010 | `86a1b5b0744c8f63f6ed640e14b23bdbcb34b63a25320d67eb1fa7da4128a0a5` |
|
||||
|
||||
All four originals use CRLF. `preserve.py` rereads source and destination bytes,
|
||||
refuses any unequal existing destination, and records the inventory in
|
||||
`manifest.json`. It does not upload or execute application binaries. Running it
|
||||
again requires the original private source records and preserved MCP readbacks.
|
||||
|
||||
`mcp-release-readback.json` and `mcp-tag-readback.json` are locally serialized
|
||||
parsed responses from fresh read-only Gitea MCP calls. They are observations,
|
||||
not original HTTP wire bytes. Release 2 is published as a prerelease at
|
||||
`cli-v0.2.0-preview.authoring.20260911`, pointing to integration commit
|
||||
`27df4a3945ebc97cd7cbde61f6aeea7a91c680d3`.
|
||||
|
||||
The publisher's `notes_sha256` is
|
||||
`1b83b9c904a79ff7acbde421851c9da9b520f3fa0aea920bf55fb71bea392a9d`.
|
||||
It binds the **1988-byte UTF-8 LF public body**, not the raw CRLF Markdown file
|
||||
above. Converting only CRLF to LF makes the preserved prose exactly equal to
|
||||
the MCP public body, including its final newline. `preservation.log` records
|
||||
the corrected successful check. `preservation-initial-failure.txt` discloses
|
||||
the initial checker assumption and its correction; no publication changed.
|
||||
|
||||
The asset readback PASS fields are the original publisher's downloaded-asset
|
||||
verification, preserved without re-downloading or repeating runtime checks.
|
||||
The semantic source (`01b76a1`), packaging assembly (`a0a48e8`) and release
|
||||
integration (`27df4a3`) remain distinct in `manifest.json` and the report.
|
||||
|
||||
Proposed repository attribute rule for root integration:
|
||||
|
||||
```gitattributes
|
||||
harness/experiments/cli-prerelease-20260911/** -text
|
||||
```
|
||||
|
||||
The preserving worker did not edit `.gitattributes` or Git state. Root must
|
||||
apply the rule before adding these exact evidence bytes and verify index or
|
||||
checkout hashes. The standalone report is
|
||||
`harness/release-cli-prerelease-published-20260911.md`.
|
||||
@@ -1,22 +0,0 @@
|
||||
Standalone Windows x64 tools for `.arcadia` projects: `check`/`lint`, missing-ID stabilization and semantic alias rename. Editing commands preview by default; `--write` saves through the shared source conflict/recovery guards. Studio and a server are not required.
|
||||
|
||||
Extract the complete ArcadiaCLI ZIP, keep `libunwind.dll` beside `arcadia.exe`, and open a terminal in that folder:
|
||||
|
||||
```powershell
|
||||
.\arcadia.exe --help
|
||||
.\arcadia.exe check examples/text-first/project.arcadia
|
||||
```
|
||||
|
||||
Copy a complete example folder before editing. Read `docs/source-cli.md` for command, diagnostic and exit-code contracts. Preview IDs remain provisional until saved; a later invocation can generate different missing IDs.
|
||||
|
||||
Verified: 12 source CLI tests, strict lint/format/build, independent 183 semantic assertions across 21 processes, all 114 packaged files and 27 dependency packages, plus independent source/default compatibility and 156 assertions across 10 clean extracted CLI invocations. Preview, exact stabilization/rename, preserved identities/comments/Unicode/CRLF, no-op timestamps and invalid-source refusal passed. ZIP, checksum and validation asset were downloaded and hash-verified after upload.
|
||||
|
||||
This is a development-profile Windows preview. It does not provide a saved-plan/digest protocol, complete large-model text ergonomics or current Linux runtime acceptance. It is separate from the [Studio preview](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/releases/tag/v0.2.0-preview.authoring.20260911). The full OA/SA release remains under development.
|
||||
|
||||
Release checkpoint: [27df4a3](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/commit/27df4a3945ebc97cd7cbde61f6aeea7a91c680d3). Preserved semantic build source is `01b76a1`; packaging-only assembly is `a0a48e8`. Attached `VALIDATION.md` records exact evidence and the distinct source/build roles.
|
||||
|
||||
ZIP size: **5,091,267 bytes**. SHA256:
|
||||
|
||||
```text
|
||||
2B051F8CE1EA4F59EE7E233E1FBD516D423AADBFBCA5D549755CBE78FCDE1AF6
|
||||
```
|
||||
@@ -1,96 +0,0 @@
|
||||
{
|
||||
"scope": "Publication evidence preservation and read-only MCP release/tag comparison; no runtime tests or asset re-download.",
|
||||
"release_id": 2,
|
||||
"tag": "cli-v0.2.0-preview.authoring.20260911",
|
||||
"publication_url": "http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/releases/tag/cli-v0.2.0-preview.authoring.20260911",
|
||||
"published_at": "2026-09-11T12:50:39Z",
|
||||
"source_roles": {
|
||||
"semantic_source": "01b76a1e89e93b7992f39e63560aa8b2ca2f69e0",
|
||||
"packaging_assembly": "a0a48e82bd43be181caca1fe2c1514c21f2bc405",
|
||||
"release_integration": "27df4a3945ebc97cd7cbde61f6aeea7a91c680d3"
|
||||
},
|
||||
"assets": [
|
||||
{
|
||||
"id": 4,
|
||||
"name": "ArcadiaCLI-0.2.0-preview.authoring.20260911-x86_64-pc-windows-gnullvm.zip",
|
||||
"bytes": 5091267,
|
||||
"sha256": "2b051f8ce1ea4f59ee7e233e1fbd516d423aadbfbca5d549755cbe78fcde1af6",
|
||||
"url": "http://217.177.34.108:3000/attachments/024ac74c-8806-4da2-9ca4-726c01a7735f",
|
||||
"readback": "PASS"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "ArcadiaCLI-0.2.0-preview.authoring.20260911-x86_64-pc-windows-gnullvm.zip.sha256",
|
||||
"bytes": 140,
|
||||
"sha256": "0dc3f12d2f79edfb735e311298d1528cbdb828b775ec885b3badde24e66bceef",
|
||||
"url": "http://217.177.34.108:3000/attachments/24af1b92-ce23-41e3-bc25-60fed95a6270",
|
||||
"readback": "PASS"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "VALIDATION.md",
|
||||
"bytes": 50297,
|
||||
"sha256": "5a0206b493bf1ca98fe931243452a9092cdf2acf10f4e3fd0a7ee9a7d8f56ec8",
|
||||
"url": "http://217.177.34.108:3000/attachments/628863de-e4ff-4fc7-afdb-45b071705d7b",
|
||||
"readback": "PASS"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "upload-binaries-verified.json",
|
||||
"source": ".local/cli-packaging-20260911/upload-binaries-verified.json",
|
||||
"bytes": 678,
|
||||
"sha256": "e945e2724020c48bfe15be7b4258861bd6c01291b98eae036eca786bb022ebde",
|
||||
"crlf_sequences": 17,
|
||||
"bare_lf_sequences": 0,
|
||||
"method": "exact byte copy; source and destination reread equal"
|
||||
},
|
||||
{
|
||||
"path": "upload-validation-verified.json",
|
||||
"source": ".local/cli-packaging-20260911/upload-validation-verified.json",
|
||||
"bytes": 277,
|
||||
"sha256": "73581acbb5b5fe4aca1fda1b626f80a28bcb3d6c036f7ce793c62bf5a8ef4a72",
|
||||
"crlf_sequences": 9,
|
||||
"bare_lf_sequences": 0,
|
||||
"method": "exact byte copy; source and destination reread equal"
|
||||
},
|
||||
{
|
||||
"path": "published-verified.json",
|
||||
"source": ".local/cli-packaging-20260911/published-verified.json",
|
||||
"bytes": 382,
|
||||
"sha256": "b5fe8a38579fb8399c78bca9db9fd91b64e8b7b7de7c664049cf5e615a671100",
|
||||
"crlf_sequences": 9,
|
||||
"bare_lf_sequences": 0,
|
||||
"method": "exact byte copy; source and destination reread equal"
|
||||
},
|
||||
{
|
||||
"path": "RELEASE-NOTES.md",
|
||||
"source": ".local/cli-packaging-20260911/RELEASE-NOTES.md",
|
||||
"bytes": 2010,
|
||||
"sha256": "86a1b5b0744c8f63f6ed640e14b23bdbcb34b63a25320d67eb1fa7da4128a0a5",
|
||||
"crlf_sequences": 22,
|
||||
"bare_lf_sequences": 0,
|
||||
"method": "exact byte copy; source and destination reread equal"
|
||||
},
|
||||
{
|
||||
"path": "mcp-release-readback.json",
|
||||
"bytes": 2495,
|
||||
"sha256": "e0a1d9324cd7a115e22c87c7905489aec81d30277a17b4480866380cbec8edb1",
|
||||
"method": "MCP read-only parsed response, locally serialized as UTF-8 JSON; not original HTTP wire bytes"
|
||||
},
|
||||
{
|
||||
"path": "mcp-tag-readback.json",
|
||||
"bytes": 196,
|
||||
"sha256": "bb770f30f292b65ca4af28d688f0f1dc1d04fb918b10f527a56ba847d44e9b2e",
|
||||
"method": "MCP read-only parsed response, locally serialized as UTF-8 JSON; not original HTTP wire bytes"
|
||||
}
|
||||
],
|
||||
"checks": {
|
||||
"four_exact_copies": true,
|
||||
"release_and_tag_match": true,
|
||||
"published_flags_and_time_match": true,
|
||||
"normalized_notes_hash_matches_publisher_record": true,
|
||||
"notes_match_public_body_after_CRLF_to_LF_only": true,
|
||||
"asset_records_are_original_publisher_readbacks": true
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"author": "Codex-9e8r6t",
|
||||
"body": "Standalone Windows x64 tools for `.arcadia` projects: `check`/`lint`, missing-ID stabilization and semantic alias rename. Editing commands preview by default; `--write` saves through the shared source conflict/recovery guards. Studio and a server are not required.\n\nExtract the complete ArcadiaCLI ZIP, keep `libunwind.dll` beside `arcadia.exe`, and open a terminal in that folder:\n\n```powershell\n.\\arcadia.exe --help\n.\\arcadia.exe check examples/text-first/project.arcadia\n```\n\nCopy a complete example folder before editing. Read `docs/source-cli.md` for command, diagnostic and exit-code contracts. Preview IDs remain provisional until saved; a later invocation can generate different missing IDs.\n\nVerified: 12 source CLI tests, strict lint/format/build, independent 183 semantic assertions across 21 processes, all 114 packaged files and 27 dependency packages, plus independent source/default compatibility and 156 assertions across 10 clean extracted CLI invocations. Preview, exact stabilization/rename, preserved identities/comments/Unicode/CRLF, no-op timestamps and invalid-source refusal passed. ZIP, checksum and validation asset were downloaded and hash-verified after upload.\n\nThis is a development-profile Windows preview. It does not provide a saved-plan/digest protocol, complete large-model text ergonomics or current Linux runtime acceptance. It is separate from the [Studio preview](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/releases/tag/v0.2.0-preview.authoring.20260911). The full OA/SA release remains under development.\n\nRelease checkpoint: [27df4a3](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/commit/27df4a3945ebc97cd7cbde61f6aeea7a91c680d3). Preserved semantic build source is `01b76a1`; packaging-only assembly is `a0a48e8`. Attached `VALIDATION.md` records exact evidence and the distinct source/build roles.\n\nZIP size: **5,091,267 bytes**. SHA256:\n\n```text\n2B051F8CE1EA4F59EE7E233E1FBD516D423AADBFBCA5D549755CBE78FCDE1AF6\n```\n",
|
||||
"created_at": "2026-09-11T12:50:39Z",
|
||||
"draft": false,
|
||||
"html_url": "http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/releases/tag/cli-v0.2.0-preview.authoring.20260911",
|
||||
"id": 2,
|
||||
"prerelease": true,
|
||||
"published_at": "2026-09-11T12:50:39Z",
|
||||
"tag_name": "cli-v0.2.0-preview.authoring.20260911",
|
||||
"target": "27df4a3945ebc97cd7cbde61f6aeea7a91c680d3",
|
||||
"title": "Arcadia CLI — standalone Windows authoring preview"
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"commit_sha": "27df4a3945ebc97cd7cbde61f6aeea7a91c680d3",
|
||||
"message": "Package standalone source CLI with verified runtime and notices\n",
|
||||
"name": "cli-v0.2.0-preview.authoring.20260911"
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
Initial preservation checker stopped after copying the four files unchanged:
|
||||
|
||||
Traceback (most recent call last):
|
||||
File "C:\Users\User\Documents\Git\ARCADIA-Studio\harness\experiments\cli-prerelease-20260911\preserve.py", line 35, in <module>
|
||||
assert hashlib.sha256(notes).hexdigest() == publication['notes_sha256']
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
AssertionError
|
||||
|
||||
The checker incorrectly assumed notes_sha256 meant local raw Markdown bytes.
|
||||
Read-only comparison established that it instead exactly matches the LF public
|
||||
release body. Local CRLF bytes remain preserved unchanged; CRLF-to-LF conversion
|
||||
alone yields the public body and the publisher's recorded hash. The corrected
|
||||
checker records both identities, without changing any publication asset or the
|
||||
four source records. A subsequent temporary PowerShell comparison command had
|
||||
an empty-pipeline parse error and was corrected before producing the comparison;
|
||||
it made no file changes.
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"result": "PASS",
|
||||
"copied_files": [
|
||||
{
|
||||
"path": "upload-binaries-verified.json",
|
||||
"source": ".local/cli-packaging-20260911/upload-binaries-verified.json",
|
||||
"bytes": 678,
|
||||
"sha256": "e945e2724020c48bfe15be7b4258861bd6c01291b98eae036eca786bb022ebde",
|
||||
"crlf_sequences": 17,
|
||||
"bare_lf_sequences": 0,
|
||||
"method": "exact byte copy; source and destination reread equal"
|
||||
},
|
||||
{
|
||||
"path": "upload-validation-verified.json",
|
||||
"source": ".local/cli-packaging-20260911/upload-validation-verified.json",
|
||||
"bytes": 277,
|
||||
"sha256": "73581acbb5b5fe4aca1fda1b626f80a28bcb3d6c036f7ce793c62bf5a8ef4a72",
|
||||
"crlf_sequences": 9,
|
||||
"bare_lf_sequences": 0,
|
||||
"method": "exact byte copy; source and destination reread equal"
|
||||
},
|
||||
{
|
||||
"path": "published-verified.json",
|
||||
"source": ".local/cli-packaging-20260911/published-verified.json",
|
||||
"bytes": 382,
|
||||
"sha256": "b5fe8a38579fb8399c78bca9db9fd91b64e8b7b7de7c664049cf5e615a671100",
|
||||
"crlf_sequences": 9,
|
||||
"bare_lf_sequences": 0,
|
||||
"method": "exact byte copy; source and destination reread equal"
|
||||
},
|
||||
{
|
||||
"path": "RELEASE-NOTES.md",
|
||||
"source": ".local/cli-packaging-20260911/RELEASE-NOTES.md",
|
||||
"bytes": 2010,
|
||||
"sha256": "86a1b5b0744c8f63f6ed640e14b23bdbcb34b63a25320d67eb1fa7da4128a0a5",
|
||||
"crlf_sequences": 22,
|
||||
"bare_lf_sequences": 0,
|
||||
"method": "exact byte copy; source and destination reread equal"
|
||||
}
|
||||
],
|
||||
"release_target": "27df4a3945ebc97cd7cbde61f6aeea7a91c680d3"
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
"""Preserve four sanitized publication records; never read credentials or upload."""
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
destination = Path(__file__).resolve().parent
|
||||
repository = destination.parents[2]
|
||||
source = repository / '.local/cli-packaging-20260911'
|
||||
names = ['upload-binaries-verified.json', 'upload-validation-verified.json',
|
||||
'published-verified.json', 'RELEASE-NOTES.md']
|
||||
records = []
|
||||
for name in names:
|
||||
data = (source / name).read_bytes()
|
||||
target = destination / name
|
||||
if not target.exists():
|
||||
with target.open('xb') as output:
|
||||
output.write(data)
|
||||
assert target.read_bytes() == data
|
||||
records.append({'path': name, 'source': '.local/cli-packaging-20260911/' + name,
|
||||
'bytes': len(data), 'sha256': hashlib.sha256(data).hexdigest(),
|
||||
'crlf_sequences': data.count(b'\r\n'),
|
||||
'bare_lf_sequences': data.count(b'\n') - data.count(b'\r\n'),
|
||||
'method': 'exact byte copy; source and destination reread equal'})
|
||||
def read(name):
|
||||
return json.loads((destination / name).read_text(encoding='utf-8-sig'))
|
||||
publication = read('published-verified.json')
|
||||
release = read('mcp-release-readback.json')
|
||||
tag = read('mcp-tag-readback.json')
|
||||
assert publication['id'] == release['id'] == 2
|
||||
assert publication['tag'] == release['tag_name'] == tag['name'] == 'cli-v0.2.0-preview.authoring.20260911'
|
||||
assert publication['draft'] is release['draft'] is False
|
||||
assert publication['prerelease'] is release['prerelease'] is True
|
||||
assert release['target'] == tag['commit_sha'] == '27df4a3945ebc97cd7cbde61f6aeea7a91c680d3'
|
||||
assert publication['published_at'] == release['published_at'] == '2026-09-11T12:50:39Z'
|
||||
notes = (destination / 'RELEASE-NOTES.md').read_bytes()
|
||||
# The publisher's recorded hash binds the LF public body, not the local CRLF
|
||||
# file. Keep both exact identities and prove the sole normalization explicitly.
|
||||
normalized_notes = notes.decode('utf-8-sig').replace('\r\n', '\n')
|
||||
assert hashlib.sha256(normalized_notes.encode('utf-8')).hexdigest() == publication['notes_sha256']
|
||||
assert normalized_notes == release['body']
|
||||
assets = read('upload-binaries-verified.json') + read('upload-validation-verified.json')
|
||||
assert len(assets) == publication['asset_count'] == 3
|
||||
assert all(item['readback'] == 'PASS' for item in assets)
|
||||
assert assets[0]['sha256'] == '2b051f8ce1ea4f59ee7e233e1fbd516d423aadbfbca5d549755cbe78fcde1af6'
|
||||
assert assets[0]['bytes'] == 5091267
|
||||
for name in ['mcp-release-readback.json', 'mcp-tag-readback.json']:
|
||||
data = (destination / name).read_bytes()
|
||||
records.append({'path': name, 'bytes': len(data), 'sha256': hashlib.sha256(data).hexdigest(),
|
||||
'method': 'MCP read-only parsed response, locally serialized as UTF-8 JSON; not original HTTP wire bytes'})
|
||||
result = {'scope': 'Publication evidence preservation and read-only MCP release/tag comparison; no runtime tests or asset re-download.',
|
||||
'release_id': 2, 'tag': tag['name'], 'publication_url': release['html_url'],
|
||||
'published_at': release['published_at'], 'source_roles': {
|
||||
'semantic_source': '01b76a1e89e93b7992f39e63560aa8b2ca2f69e0',
|
||||
'packaging_assembly': 'a0a48e82bd43be181caca1fe2c1514c21f2bc405',
|
||||
'release_integration': release['target']},
|
||||
'assets': assets, 'files': records,
|
||||
'checks': {'four_exact_copies': True, 'release_and_tag_match': True,
|
||||
'published_flags_and_time_match': True, 'normalized_notes_hash_matches_publisher_record': True,
|
||||
'notes_match_public_body_after_CRLF_to_LF_only': True,
|
||||
'asset_records_are_original_publisher_readbacks': True}}
|
||||
(destination / 'manifest.json').write_text(json.dumps(result, indent=2) + '\n', encoding='utf-8', newline='\n')
|
||||
print(json.dumps({'result': 'PASS', 'copied_files': records[:4], 'release_target': release['target']}, indent=2))
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"id": 2,
|
||||
"tag": "cli-v0.2.0-preview.authoring.20260911",
|
||||
"url": "http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/releases/tag/cli-v0.2.0-preview.authoring.20260911",
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"asset_count": 3,
|
||||
"notes_sha256": "1b83b9c904a79ff7acbde421851c9da9b520f3fa0aea920bf55fb71bea392a9d",
|
||||
"published_at": "2026-09-11T12:50:39Z"
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": 4,
|
||||
"name": "ArcadiaCLI-0.2.0-preview.authoring.20260911-x86_64-pc-windows-gnullvm.zip",
|
||||
"bytes": 5091267,
|
||||
"sha256": "2b051f8ce1ea4f59ee7e233e1fbd516d423aadbfbca5d549755cbe78fcde1af6",
|
||||
"url": "http://217.177.34.108:3000/attachments/024ac74c-8806-4da2-9ca4-726c01a7735f",
|
||||
"readback": "PASS"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "ArcadiaCLI-0.2.0-preview.authoring.20260911-x86_64-pc-windows-gnullvm.zip.sha256",
|
||||
"bytes": 140,
|
||||
"sha256": "0dc3f12d2f79edfb735e311298d1528cbdb828b775ec885b3badde24e66bceef",
|
||||
"url": "http://217.177.34.108:3000/attachments/24af1b92-ce23-41e3-bc25-60fed95a6270",
|
||||
"readback": "PASS"
|
||||
}
|
||||
]
|
||||
@@ -1,10 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": 6,
|
||||
"name": "VALIDATION.md",
|
||||
"bytes": 50297,
|
||||
"sha256": "5a0206b493bf1ca98fe931243452a9092cdf2acf10f4e3fd0a7ee9a7d8f56ec8",
|
||||
"url": "http://217.177.34.108:3000/attachments/628863de-e4ff-4fc7-afdb-45b071705d7b",
|
||||
"readback": "PASS"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
# Contextual/lifecycle integration checkpoint evidence — 2026-09-11
|
||||
|
||||
Codex-ytr83r /root/wsl_startup_review. Closed archival readback only: **11 payloads, 137778 bytes**, frozen at 2026-09-11T17:38:52.1137417Z. Original names/extensions and file bytes are preserved under their paths relative to .local. manifest.json records the exact inventory, original/copy SHA256, sizes and original modification times; readback.json records an independent second read of every original and destination. No discrepancy was found.
|
||||
|
||||
The seven gate payloads preserve the server/CLI all-targets exit 0 at 17:28:56.5857591 UTC (68 passed across seven targets), the initial semantic-schema exit 1 caused by missing jsonschema, its task-local installation log, and the successful repeated semantic-schema exit 0 at 17:28:34.201 UTC (72 fixtures; all 204 component schemas well formed). The failed attempt remains explicit evidence. The schema status records identify source 4da92ab481a2f5f9fcd88f29c90fa4785ac17c3a; the server/CLI record retains its own actual command/timestamps without inventing an absent commit field.
|
||||
|
||||
The other four payloads are the two-file wide-after-passive-save checkpoint (project.arcadia and rationale.arcadia), its same-oracle log, and wide-cjk-sa-resumed-min-125-saved.jpg. Root reports recovering the intended Studio, clicking Save all, and completing the snapshot/oracle comparison; the archived log says all identities, fields, reachable source and physical bytes match. This is the **old 47332A6E… / production 29084050 typography-wide passive-save check**, not corrected header pixels or new combined-desktop acceptance. The earlier 12-screenshot pre-correction archive remains separate and untouched.
|
||||
|
||||
This worker performed no native/oracle/compiler/schema/install execution, source edit, Git/index operation or WSL action. The installation log records root's already-completed work; archiving it did not install anything. No executable, DLL, private profile, growing log or Linux input is included. Root owns attributes, staging, integrated interpretation and the authoritative [issue 5](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/5) record.
|
||||
+692
@@ -0,0 +1,692 @@
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user