Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55198ee511 | ||
|
|
05e4c83054 | ||
|
|
d0b7c28501 | ||
|
|
fe43730bec | ||
|
|
d46854c2dc | ||
|
|
acbfb6cc92 | ||
|
|
3f9e6a18af | ||
|
|
7836a93edb | ||
|
|
f714a25bbc | ||
|
|
cb941367d7 | ||
|
|
96b756bece | ||
|
|
e624d5b9c4 | ||
|
|
d7c065991d |
@@ -3,6 +3,8 @@
|
||||
mod semantic_source;
|
||||
#[path = "migration_v2/source.rs"]
|
||||
mod v2_source;
|
||||
pub(crate) use crate::source_defaults::Mode as DeclarationFormat;
|
||||
use crate::source_defaults::{self, Context as DefaultContext};
|
||||
use crate::*;
|
||||
use serde_json::{Map, Value};
|
||||
use std::{collections::BTreeMap, fmt};
|
||||
@@ -645,25 +647,34 @@ fn offset_for(source: &str, line: usize, column: usize) -> usize {
|
||||
source.len()
|
||||
}
|
||||
|
||||
/// Canonical output uses persistent aliases and explicit identities. Typed
|
||||
/// nested records are ordinary language fields; JSON documents are not embedded.
|
||||
/// Canonical output uses persistent aliases and explicit identities, omitting
|
||||
/// only values supplied by source defaults. Typed nested records are ordinary
|
||||
/// language fields; JSON documents are not embedded.
|
||||
pub fn format(project: &Project) -> String {
|
||||
let mut out = std::format!(
|
||||
"arcadia {SCHEMA_VERSION};\nproject {} {} id \"{}\" {{\n description {};\n\n",
|
||||
"arcadia {SCHEMA_VERSION};\nproject {} {} id \"{}\" {{\n",
|
||||
project.alias,
|
||||
quote(&project.name),
|
||||
project.id,
|
||||
quote(&project.description)
|
||||
);
|
||||
for (_, declaration) in format_declarations(project) {
|
||||
if !project.description.is_empty() {
|
||||
out.push_str(&std::format!(
|
||||
" description {};\n",
|
||||
quote(&project.description)
|
||||
));
|
||||
}
|
||||
out.push('\n');
|
||||
for (_, declaration) in format_declarations(project, DeclarationFormat::Compact) {
|
||||
out.push_str(&declaration);
|
||||
}
|
||||
out.push_str("}\n");
|
||||
out
|
||||
}
|
||||
|
||||
/// The same typed declaration writer is used by flat and modular source output.
|
||||
pub(crate) fn format_declarations(project: &Project) -> Vec<(Id, String)> {
|
||||
/// Shared typed writer for canonical output and lossless editing. Complete edit
|
||||
/// comparisons retain defaults; emitted declarations and value fragments apply
|
||||
/// only the omission rules valid in their selected source context.
|
||||
pub(crate) fn format_declarations(project: &Project, mode: DeclarationFormat) -> Vec<(Id, String)> {
|
||||
let aliases = project
|
||||
.identities()
|
||||
.into_iter()
|
||||
@@ -675,6 +686,7 @@ pub(crate) fn format_declarations(project: &Project) -> Vec<(Id, String)> {
|
||||
let mut fields = payload(&serde_json::to_value(&e.body).unwrap());
|
||||
fields.insert("description".into(), Value::String(e.description.clone()));
|
||||
fields.insert("parent".into(), serde_json::to_value(e.parent).unwrap());
|
||||
let fields = source_defaults::fields(DefaultContext::Element(e.kind()), &fields, mode);
|
||||
write_declaration(
|
||||
&mut out,
|
||||
&element_word(e.kind(), e.layer()),
|
||||
@@ -690,6 +702,7 @@ pub(crate) fn format_declarations(project: &Project) -> Vec<(Id, String)> {
|
||||
let mut out = String::new();
|
||||
let mut fields = payload(&serde_json::to_value(&r.body).unwrap());
|
||||
fields.insert("description".into(), Value::String(r.description.clone()));
|
||||
let fields = source_defaults::fields(DefaultContext::Relation(r.kind()), &fields, mode);
|
||||
write_declaration(
|
||||
&mut out,
|
||||
&relation_word(r.kind()),
|
||||
@@ -705,6 +718,7 @@ pub(crate) fn format_declarations(project: &Project) -> Vec<(Id, String)> {
|
||||
let mut out = String::new();
|
||||
let mut fields = record_fields(d);
|
||||
fields.remove("nodes");
|
||||
let fields = source_defaults::fields(DefaultContext::Diagram, &fields, mode);
|
||||
write_declaration(
|
||||
&mut out,
|
||||
"diagram",
|
||||
@@ -719,6 +733,7 @@ pub(crate) fn format_declarations(project: &Project) -> Vec<(Id, String)> {
|
||||
let mut out = String::new();
|
||||
let mut fields = record_fields(n);
|
||||
fields.insert("diagram".into(), Value::String(d.id.to_string()));
|
||||
let fields = source_defaults::fields(DefaultContext::Node, &fields, mode);
|
||||
write_declaration(&mut out, "node", &n.alias, None, n.id, &fields, &aliases);
|
||||
declarations.push((n.id, out));
|
||||
}
|
||||
@@ -731,7 +746,7 @@ pub(crate) fn format_declarations(project: &Project) -> Vec<(Id, String)> {
|
||||
&a.alias,
|
||||
Some(&a.name),
|
||||
a.id,
|
||||
&record_fields(a),
|
||||
&source_defaults::fields(DefaultContext::Artifact, &record_fields(a), mode),
|
||||
&aliases,
|
||||
);
|
||||
declarations.push((a.id, out));
|
||||
@@ -744,7 +759,7 @@ pub(crate) fn format_declarations(project: &Project) -> Vec<(Id, String)> {
|
||||
&t.alias,
|
||||
Some(&t.name),
|
||||
t.id,
|
||||
&record_fields(t),
|
||||
&source_defaults::fields(DefaultContext::Trace, &record_fields(t), mode),
|
||||
&aliases,
|
||||
);
|
||||
declarations.push((t.id, out));
|
||||
@@ -1742,7 +1757,9 @@ pub(crate) fn resolve_declarations(
|
||||
*viewpoint = ScenarioViewpoint::MixedLegacy;
|
||||
}
|
||||
let mut body = serde_json::to_value(initial).unwrap();
|
||||
let defaults = if legacy {
|
||||
let defaults = if project.schema_version >= 7 {
|
||||
source_defaults::parser_fields(DefaultContext::Element(kind))
|
||||
} else if legacy {
|
||||
crate::persistence::v3::legacy_defaults(kind)
|
||||
.and_then(|value| value.as_object().cloned())
|
||||
.unwrap_or_else(|| payload(&body))
|
||||
@@ -1799,7 +1816,12 @@ pub(crate) fn resolve_declarations(
|
||||
let mut body =
|
||||
serde_json::to_value(RelationBody::new(kind, Id::nil(), Id::nil()))
|
||||
.unwrap();
|
||||
let merged = merge_fields(payload(&body), fields).map_err(&conversion_error)?;
|
||||
let defaults = if project.schema_version >= 7 {
|
||||
source_defaults::parser_fields(DefaultContext::Relation(kind))
|
||||
} else {
|
||||
payload(&body)
|
||||
};
|
||||
let merged = merge_fields(defaults, fields).map_err(&conversion_error)?;
|
||||
replace_payload(&mut body, merged);
|
||||
let body = serde_json::from_value(body)
|
||||
.map_err(|e| conversion_error(e.to_string()))?;
|
||||
@@ -1824,7 +1846,10 @@ pub(crate) fn resolve_declarations(
|
||||
}
|
||||
let defaults =
|
||||
serde_json::json!({"description":"","layer":"Oa","projection":"Overview","routes":[],"view_options":crate::view_options::ViewOptions::default()});
|
||||
let mut record = merge_fields(defaults.as_object().unwrap().clone(), fields)
|
||||
let defaults = if project.schema_version >= 7 {
|
||||
source_defaults::parser_fields(DefaultContext::Diagram)
|
||||
} else { defaults.as_object().unwrap().clone() };
|
||||
let mut record = merge_fields(defaults, fields)
|
||||
.map_err(&conversion_error)?;
|
||||
header(&mut record, d, true);
|
||||
record.insert("nodes".into(), serde_json::json!({}));
|
||||
@@ -1839,7 +1864,10 @@ pub(crate) fn resolve_declarations(
|
||||
return Err(conversion_error("Missing required field 'element'.".into()));
|
||||
}
|
||||
let defaults = serde_json::json!({"element":null,"position":{"x":0.0,"y":0.0},"size":{"width":224.0,"height":94.0},"collapsed":false,"port_anchors":[]});
|
||||
let mut record = merge_fields(defaults.as_object().unwrap().clone(), fields)
|
||||
let defaults = if project.schema_version >= 7 {
|
||||
source_defaults::parser_fields(DefaultContext::Node)
|
||||
} else { defaults.as_object().unwrap().clone() };
|
||||
let mut record = merge_fields(defaults, fields)
|
||||
.map_err(&conversion_error)?;
|
||||
header(&mut record, d, false);
|
||||
let node: NodePlacement = serde_json::from_value(Value::Object(record))
|
||||
@@ -1851,7 +1879,10 @@ pub(crate) fn resolve_declarations(
|
||||
return Err(conversion_error("Missing required field 'owner'.".into()));
|
||||
}
|
||||
let defaults = serde_json::json!({"description":"","owner":null,"relation":"References","uri":"","revision":null,"fragment":null});
|
||||
let mut record = merge_fields(defaults.as_object().unwrap().clone(), fields)
|
||||
let defaults = if project.schema_version >= 7 {
|
||||
source_defaults::parser_fields(DefaultContext::Artifact)
|
||||
} else { defaults.as_object().unwrap().clone() };
|
||||
let mut record = merge_fields(defaults, fields)
|
||||
.map_err(&conversion_error)?;
|
||||
header(&mut record, d, true);
|
||||
let artifact = serde_json::from_value(Value::Object(record))
|
||||
@@ -1867,7 +1898,10 @@ pub(crate) fn resolve_declarations(
|
||||
}
|
||||
}
|
||||
let defaults = serde_json::json!({"description":"","oa":null,"sa":null,"kind":"Refinement","baseline":null});
|
||||
let mut record = merge_fields(defaults.as_object().unwrap().clone(), fields)
|
||||
let defaults = if project.schema_version >= 7 {
|
||||
source_defaults::parser_fields(DefaultContext::Trace)
|
||||
} else { defaults.as_object().unwrap().clone() };
|
||||
let mut record = merge_fields(defaults, fields)
|
||||
.map_err(&conversion_error)?;
|
||||
header(&mut record, d, true);
|
||||
let trace = if project.schema_version < 5 {
|
||||
|
||||
@@ -11,6 +11,7 @@ pub mod references;
|
||||
pub mod routing;
|
||||
pub mod semantic;
|
||||
pub mod session;
|
||||
mod source_defaults;
|
||||
pub mod source_files;
|
||||
pub mod source_project;
|
||||
pub mod topology_context;
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
//! Ordinary source defaults shared by current parsing and declaration rendering.
|
||||
//!
|
||||
//! These are source omission defaults, not graphical creation defaults. The
|
||||
//! policy operates on complete typed declaration maps. Required named fields
|
||||
//! and selected constructor payloads remain distinct from omission defaults.
|
||||
//! Historical versions use their existing frozen parser paths. Semantic baseline
|
||||
//! history is never traversed or normalized here.
|
||||
use crate::{Kind, RelationKind};
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum Context {
|
||||
Element(Kind),
|
||||
Relation(RelationKind),
|
||||
Diagram,
|
||||
Node,
|
||||
Artifact,
|
||||
Trace,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum Mode {
|
||||
CompleteForEdit,
|
||||
Compact,
|
||||
/// Internal lossless-edit fragments: retain every top-level value while
|
||||
/// compacting its selected constructor's optional nested fields.
|
||||
CompactValues,
|
||||
}
|
||||
|
||||
/// Top-level field inventory for the current ordinary reader. Callers must
|
||||
/// check required fields before merging; null entries only admit their names.
|
||||
/// This never supplies defaults inside a selected constructor.
|
||||
pub(crate) fn parser_fields(context: Context) -> Map<String, Value> {
|
||||
let mut fields = declaration_defaults(context);
|
||||
let required: &[&str] = match context {
|
||||
Context::Element(kind) => {
|
||||
fields.remove("description");
|
||||
fields.remove("parent");
|
||||
if kind == Kind::Port {
|
||||
&["contract"]
|
||||
} else {
|
||||
&[]
|
||||
}
|
||||
}
|
||||
Context::Relation(kind) => {
|
||||
fields.remove("description");
|
||||
match kind {
|
||||
RelationKind::ContextualLink => &["owner", "source", "target", "purpose"],
|
||||
RelationKind::FunctionalExchange => {
|
||||
&["source", "target", "source_port", "target_port"]
|
||||
}
|
||||
_ => &["source", "target"],
|
||||
}
|
||||
}
|
||||
Context::Diagram => &[],
|
||||
Context::Node => &["element"],
|
||||
Context::Artifact => &["owner"],
|
||||
Context::Trace => &["oa", "sa"],
|
||||
};
|
||||
for name in required {
|
||||
fields.insert((*name).into(), Value::Null);
|
||||
}
|
||||
fields
|
||||
}
|
||||
|
||||
pub(crate) fn fields(
|
||||
context: Context,
|
||||
input: &Map<String, Value>,
|
||||
mode: Mode,
|
||||
) -> Map<String, Value> {
|
||||
let mut result = input.clone();
|
||||
restore_serialization_skips(context, &mut result);
|
||||
if mode != Mode::CompleteForEdit {
|
||||
compact_nested(context, &mut result);
|
||||
}
|
||||
if mode == Mode::Compact {
|
||||
// Compact a composite and its declared default by the same nested rules
|
||||
// before whole-field comparison. This also makes compact input stable:
|
||||
// e.g. String{minimum_length:0} still denotes the default definition.
|
||||
let mut defaults = declaration_defaults(context);
|
||||
compact_nested(context, &mut defaults);
|
||||
omit_defaults(&mut result, &defaults);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn restore_serialization_skips(context: Context, fields: &mut Map<String, Value>) {
|
||||
match context {
|
||||
Context::Element(Kind::Occurrence) => {
|
||||
if let Some(exchange) = constructor_payload(fields, "body", "Exchange") {
|
||||
exchange
|
||||
.entry("operation".to_owned())
|
||||
.or_insert(Value::Null);
|
||||
}
|
||||
}
|
||||
Context::Node => {
|
||||
fields.entry("port_anchors".to_owned()).or_insert(json!([]));
|
||||
}
|
||||
Context::Trace => {
|
||||
if let Some(baseline) = fields.get_mut("baseline").and_then(Value::as_object_mut) {
|
||||
baseline.entry("semantic".to_owned()).or_insert(Value::Null);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn declaration_defaults(context: Context) -> Map<String, Value> {
|
||||
let mut defaults = match context {
|
||||
Context::Element(kind) => element_defaults(kind),
|
||||
Context::Relation(kind) => relation_defaults(kind),
|
||||
Context::Diagram => object(json!({
|
||||
"description": "", "layer": "Oa", "projection": "Overview",
|
||||
"routes": [], "view_options": {
|
||||
"hidden_edges": [], "relation_kinds": "All",
|
||||
"work_categories": "All", "participant_categories": "All",
|
||||
"coverage": "All", "work_contexts": "All", "exchange_labels": "Name"
|
||||
}
|
||||
})),
|
||||
Context::Node => object(json!({
|
||||
"position": {"x": 0.0, "y": 0.0},
|
||||
"size": {"width": 224.0, "height": 94.0},
|
||||
"collapsed": false, "port_anchors": []
|
||||
})),
|
||||
Context::Artifact => object(json!({
|
||||
"description": "", "relation": "References", "uri": "",
|
||||
"revision": null, "fragment": null
|
||||
})),
|
||||
Context::Trace => object(json!({
|
||||
"description": "", "kind": "Refinement", "baseline": null
|
||||
})),
|
||||
};
|
||||
match context {
|
||||
Context::Element(_) => {
|
||||
defaults.insert("description".into(), json!(""));
|
||||
defaults.insert("parent".into(), Value::Null);
|
||||
}
|
||||
Context::Relation(_) => {
|
||||
defaults.insert("description".into(), json!(""));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
defaults
|
||||
}
|
||||
|
||||
fn element_defaults(kind: Kind) -> Map<String, Value> {
|
||||
if let Some(state_kind) = kind.state_kind() {
|
||||
// The declaration selects this discriminator; no GUI factory is used.
|
||||
return object(json!({
|
||||
"kind": state_kind, "entry": "", "exit": "", "do_activity": ""
|
||||
}));
|
||||
}
|
||||
match kind {
|
||||
Kind::Process | Kind::FunctionalChain => object(json!({"steps": []})),
|
||||
Kind::ExchangeItem => object(json!({
|
||||
"kind": "Information", "mechanism": "Unspecified", "supertype": null
|
||||
})),
|
||||
Kind::ExchangeCategory => object(json!({"family": "Work", "exchanges": []})),
|
||||
Kind::DataType => object(json!({
|
||||
"definition": {"String": {"minimum_length": 0, "maximum_length": null}},
|
||||
"default": null, "unit": ""
|
||||
})),
|
||||
Kind::DataField => object(json!({
|
||||
"type_ref": "String", "minimum": 1, "maximum": 1, "unit": "",
|
||||
"direction": "Unspecified", "storage": "Value", "default": null
|
||||
})),
|
||||
Kind::Interface => object(json!({"supertype": null})),
|
||||
// Port.contract is required, even when its explicit value is Legacy.
|
||||
Kind::Port => object(json!({"legacy": null})),
|
||||
Kind::Scenario => object(json!({
|
||||
"viewpoint": "MixedLegacy", "context": null,
|
||||
"precondition": "", "postcondition": "", "steps": []
|
||||
})),
|
||||
Kind::Lifeline => object(json!({"represented": null})),
|
||||
Kind::Occurrence => object(json!({
|
||||
"body": {"Exchange": {
|
||||
"exchange": null, "sender": null, "receiver": null,
|
||||
"message_kind": "Asynchronous", "reply_to": null,
|
||||
"values": [], "operation": null
|
||||
}},
|
||||
"precondition": "", "postcondition": ""
|
||||
})),
|
||||
Kind::Event => object(json!({"body": {"Signal": {"items": []}}})),
|
||||
Kind::Constraint => object(json!({"expression": "", "constrained": []})),
|
||||
// Unit-body declarations have no additional omission defaults. A future
|
||||
// Kind reaches this branch without gaining speculative payload rules.
|
||||
_ => Map::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn relation_defaults(kind: RelationKind) -> Map<String, Value> {
|
||||
match kind {
|
||||
RelationKind::InterfaceItemAllocation => object(json!({
|
||||
"order": 0, "transmission": "Unspecified", "acquisition": "Unspecified"
|
||||
})),
|
||||
RelationKind::DataAssociation => object(json!({
|
||||
"source_role": "", "target_role": "",
|
||||
"source_minimum": 0, "target_minimum": 0,
|
||||
"source_maximum": null, "target_maximum": null,
|
||||
"source_navigable": false, "target_navigable": false
|
||||
})),
|
||||
RelationKind::Interaction | RelationKind::FunctionalExchange => {
|
||||
object(json!({"items": []}))
|
||||
}
|
||||
RelationKind::BoundaryExchange => object(json!({"items": [], "oriented": true})),
|
||||
RelationKind::CommunicationMean => object(json!({"items": [], "oriented": false})),
|
||||
RelationKind::CapabilityExtend => object(json!({"condition": ""})),
|
||||
RelationKind::SequenceLink => object(json!({"guard": ""})),
|
||||
RelationKind::StateTransition => object(json!({
|
||||
"transition_kind": "External", "trigger": "", "guard": "", "effect": "",
|
||||
"triggers": [], "effects": []
|
||||
})),
|
||||
RelationKind::StateAction => object(json!({"phase": "Do"})),
|
||||
// source/target, FunctionalExchange ports and ContextualLink's owner,
|
||||
// endpoints and purpose are required, despite factory placeholder values.
|
||||
_ => Map::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn compact_nested(context: Context, fields: &mut Map<String, Value>) {
|
||||
match context {
|
||||
Context::Element(Kind::DataType) => compact_data_definition(fields),
|
||||
Context::Element(Kind::Port) => {
|
||||
if let Some(legacy) = fields.get_mut("legacy").and_then(Value::as_object_mut) {
|
||||
omit_nulls(legacy, &["interface"]);
|
||||
}
|
||||
// Every selected PortContract payload field remains required.
|
||||
}
|
||||
Context::Element(Kind::Occurrence) => compact_occurrence(fields),
|
||||
Context::Diagram => {
|
||||
if let Some(options) = fields
|
||||
.get_mut("view_options")
|
||||
.and_then(Value::as_object_mut)
|
||||
{
|
||||
omit_defaults(
|
||||
options,
|
||||
&object(json!({
|
||||
"hidden_edges": [], "relation_kinds": "All",
|
||||
"work_categories": "All", "participant_categories": "All",
|
||||
"coverage": "All", "work_contexts": "All", "exchange_labels": "Name"
|
||||
})),
|
||||
);
|
||||
}
|
||||
if let Some(routes) = fields.get_mut("routes").and_then(Value::as_array_mut) {
|
||||
for route in routes {
|
||||
if let Some(route) = route.as_object_mut() {
|
||||
omit_defaults(route, &object(json!({"waypoints": [], "label": null})));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Route entries, EdgeLabel coordinates, projection machine IDs, and
|
||||
// Only([])/Any([]) selectors remain even when their values are empty.
|
||||
}
|
||||
Context::Trace => {
|
||||
if let Some(baseline) = fields.get_mut("baseline").and_then(Value::as_object_mut) {
|
||||
omit_nulls(baseline, &["semantic"]);
|
||||
}
|
||||
// No recursion through snapshots or semantic historical facets.
|
||||
}
|
||||
// DataValue/TypeRef payloads, EventBody fields, Node geometry/anchors,
|
||||
// contextual endpoints/purposes, and other required records stay intact.
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn compact_data_definition(fields: &mut Map<String, Value>) {
|
||||
for tag in ["Integer", "Real"] {
|
||||
if let Some(payload) = constructor_payload(fields, "definition", tag) {
|
||||
omit_nulls(payload, &["minimum", "maximum"]);
|
||||
}
|
||||
}
|
||||
if let Some(payload) = constructor_payload(fields, "definition", "String") {
|
||||
omit_nulls(payload, &["maximum_length"]);
|
||||
// minimum_length is required inside a selected String constructor.
|
||||
}
|
||||
if let Some(payload) = constructor_payload(fields, "definition", "Record") {
|
||||
omit_nulls(payload, &["supertype"]);
|
||||
}
|
||||
// Boolean has no payload; Enumeration.literals remains required even [].
|
||||
}
|
||||
|
||||
fn compact_occurrence(fields: &mut Map<String, Value>) {
|
||||
if let Some(exchange) = constructor_payload(fields, "body", "Exchange") {
|
||||
omit_defaults(
|
||||
exchange,
|
||||
&object(json!({
|
||||
"exchange": null, "sender": null, "receiver": null,
|
||||
"message_kind": "Asynchronous", "reply_to": null,
|
||||
"values": [], "operation": null
|
||||
})),
|
||||
);
|
||||
if let Some(operation) = exchange.get_mut("operation").and_then(Value::as_object_mut) {
|
||||
omit_nulls(operation, &["allocation"]);
|
||||
// Keep the containing operation field even if its record becomes {}:
|
||||
// explicit unresolved operation intent is not an absent operation.
|
||||
}
|
||||
if let Some(values) = exchange.get_mut("values").and_then(Value::as_array_mut) {
|
||||
for value in values {
|
||||
if let Some(message_value) = value.as_object_mut() {
|
||||
omit_defaults(message_value, &object(json!({"path": []})));
|
||||
}
|
||||
}
|
||||
}
|
||||
// MessageKind::Timer.expression, MessageValue.field/expression remain.
|
||||
}
|
||||
for (tag, optional) in [
|
||||
("Action", &["action", "lifeline"][..]),
|
||||
("State", &["state", "lifeline"][..]),
|
||||
("SequenceReference", &["sequence"][..]),
|
||||
("ScenarioUse", &["scenario"][..]),
|
||||
] {
|
||||
if let Some(payload) = constructor_payload(fields, "body", tag) {
|
||||
omit_nulls(payload, optional);
|
||||
}
|
||||
}
|
||||
// ScenarioUse.bindings, Control.kind, Fragment.operator/branches and
|
||||
// Branch.guard/occurrences are required, including empty strings/lists.
|
||||
}
|
||||
|
||||
fn constructor_payload<'a>(
|
||||
fields: &'a mut Map<String, Value>,
|
||||
key: &str,
|
||||
tag: &str,
|
||||
) -> Option<&'a mut Map<String, Value>> {
|
||||
let wrapper = fields.get_mut(key)?.as_object_mut()?;
|
||||
if wrapper.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
wrapper.get_mut(tag)?.as_object_mut()
|
||||
}
|
||||
|
||||
fn omit_nulls(fields: &mut Map<String, Value>, names: &[&str]) {
|
||||
for name in names {
|
||||
if fields.get(*name).is_some_and(Value::is_null) {
|
||||
fields.remove(*name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn omit_defaults(fields: &mut Map<String, Value>, defaults: &Map<String, Value>) {
|
||||
for (name, default) in defaults {
|
||||
if fields.get(name).is_some_and(|value| same(value, default)) {
|
||||
fields.remove(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Comparison recurses only to compare a known field with its exact declared
|
||||
// default. It never traverses arbitrary data looking for values to remove.
|
||||
fn same(value: &Value, default: &Value) -> bool {
|
||||
match (value, default) {
|
||||
(Value::Object(a), Value::Object(b)) => {
|
||||
a.len() == b.len()
|
||||
&& a.iter()
|
||||
.all(|(key, value)| b.get(key).is_some_and(|d| same(value, d)))
|
||||
}
|
||||
(Value::Array(a), Value::Array(b)) => {
|
||||
a.len() == b.len() && a.iter().zip(b).all(|(value, d)| same(value, d))
|
||||
}
|
||||
(Value::Number(a), Value::Number(b)) => {
|
||||
// serde_json numeric equality alone can equate -0.0 with +0.0.
|
||||
// Extracted f32 coordinates are represented exactly as f64 here.
|
||||
a == b
|
||||
&& match (a.as_f64(), b.as_f64()) {
|
||||
(Some(x), Some(y)) if x == 0.0 && y == 0.0 => x.to_bits() == y.to_bits(),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
_ => value == default,
|
||||
}
|
||||
}
|
||||
|
||||
fn object(value: Value) -> Map<String, Value> {
|
||||
match value {
|
||||
Value::Object(fields) => fields,
|
||||
_ => unreachable!("literal source default is an object"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "source_defaults_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,718 @@
|
||||
//! Independent literal oracles for the production source-default policy.
|
||||
use super::{self as policy, Context, Mode};
|
||||
use crate::{Kind, RelationKind};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
#[test]
|
||||
fn complete_comparison_keeps_populated_serializer_skip_values() {
|
||||
for (context, input) in [
|
||||
(
|
||||
Context::Element(Kind::Occurrence),
|
||||
json!({"body":{"Exchange":{"operation":{"allocation":"allocation-id"},"values":[]}}}),
|
||||
),
|
||||
(
|
||||
Context::Node,
|
||||
json!({"port_anchors":[{"port":"port-id","side":"Left","offset":0.0}]}),
|
||||
),
|
||||
(
|
||||
Context::Trace,
|
||||
json!({"baseline":{"semantic":{"historical":"payload"}}}),
|
||||
),
|
||||
] {
|
||||
assert_eq!(rendered(context, &input, Mode::CompleteForEdit), input);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compact_value_fragments_keep_selected_constructor_requirements() {
|
||||
for (context, input, expected) in [
|
||||
(
|
||||
Context::Element(Kind::DataType),
|
||||
json!({"definition":{"String":{"minimum_length":0,"maximum_length":null}},"default":null,"unit":""}),
|
||||
json!({"definition":{"String":{"minimum_length":0}},"default":null,"unit":""}),
|
||||
),
|
||||
(
|
||||
Context::Element(Kind::Event),
|
||||
json!({"body":{"Signal":{"items":[]}}}),
|
||||
json!({"body":{"Signal":{"items":[]}}}),
|
||||
),
|
||||
(
|
||||
Context::Element(Kind::Occurrence),
|
||||
json!({"body":{"Exchange":{"operation":{"allocation":null},"values":[]}},"precondition":""}),
|
||||
json!({"body":{"Exchange":{"operation":{}}},"precondition":""}),
|
||||
),
|
||||
(
|
||||
Context::Element(Kind::Occurrence),
|
||||
json!({"body":{"ScenarioUse":{"scenario":null,"bindings":[]}}}),
|
||||
json!({"body":{"ScenarioUse":{"bindings":[]}}}),
|
||||
),
|
||||
] {
|
||||
assert_eq!(rendered(context, &input, Mode::CompactValues), expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_scenario_default_is_independent_of_graphical_creation() {
|
||||
use crate::{ElementBody, Layer, ScenarioViewpoint, SharedBody, language};
|
||||
let project = language::parse(
|
||||
"arcadia 7; project p \"P\" { oa_scenario o \"O\" {} sa_scenario s \"S\" {} }",
|
||||
)
|
||||
.unwrap();
|
||||
for element in project.elements.values() {
|
||||
assert!(matches!(
|
||||
element.body.shared(),
|
||||
Some(SharedBody::Scenario {
|
||||
viewpoint: ScenarioViewpoint::MixedLegacy,
|
||||
..
|
||||
})
|
||||
));
|
||||
assert!(!matches!(
|
||||
ElementBody::new(Kind::Scenario, element.layer())
|
||||
.unwrap()
|
||||
.shared(),
|
||||
Some(SharedBody::Scenario {
|
||||
viewpoint: ScenarioViewpoint::MixedLegacy,
|
||||
..
|
||||
})
|
||||
));
|
||||
}
|
||||
let source = language::format(&project);
|
||||
assert!(!source.contains("description"));
|
||||
assert!(!source.contains("parent"));
|
||||
assert!(!source.contains("viewpoint"));
|
||||
assert_eq!(language::parse(&source).unwrap(), project);
|
||||
|
||||
let mut typed = project;
|
||||
for element in typed.elements.values_mut() {
|
||||
let viewpoint = if element.layer() == Layer::Oa {
|
||||
ScenarioViewpoint::OperationalActivity
|
||||
} else {
|
||||
ScenarioViewpoint::Functional
|
||||
};
|
||||
*element.body.shared_mut().unwrap() = SharedBody::Scenario {
|
||||
viewpoint,
|
||||
context: None,
|
||||
precondition: String::new(),
|
||||
postcondition: String::new(),
|
||||
steps: vec![],
|
||||
};
|
||||
}
|
||||
let source = language::format(&typed);
|
||||
assert!(source.contains("viewpoint OperationalActivity;"));
|
||||
assert!(source.contains("viewpoint Functional;"));
|
||||
assert_eq!(language::parse(&source).unwrap(), typed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_parser_defaults_preserve_required_fields_and_kind_checks() {
|
||||
for (declaration, message) in [
|
||||
(
|
||||
"oa_port p0 \"Port\" {}",
|
||||
"Missing required field 'contract'",
|
||||
),
|
||||
(
|
||||
"allocation r \"R\" { source @owner; }",
|
||||
"Missing required field 'target'",
|
||||
),
|
||||
(
|
||||
"functional_exchange r \"R\" { source @owner; target @work; }",
|
||||
"Missing required field 'source_port'",
|
||||
),
|
||||
(
|
||||
"contextual_link r \"R\" {}",
|
||||
"Missing required field 'owner'",
|
||||
),
|
||||
("node n { diagram @d; }", "Missing required field 'element'"),
|
||||
("artifact a \"A\" {}", "Missing required field 'owner'"),
|
||||
("trace t \"T\" {}", "Missing required field 'oa'"),
|
||||
(
|
||||
"oa_state s \"S\" { kind Initial; }",
|
||||
"Declaration kind disagrees",
|
||||
),
|
||||
(
|
||||
"oa_activity a \"A\" { unknown true; }",
|
||||
"Unknown field 'unknown'",
|
||||
),
|
||||
(
|
||||
"oa_data_type t \"T\" { definition { String {}; }; }",
|
||||
"minimum_length",
|
||||
),
|
||||
(
|
||||
"oa_occurrence o \"O\" { body { ScenarioUse {}; }; }",
|
||||
"bindings",
|
||||
),
|
||||
] {
|
||||
let source = format!(
|
||||
"arcadia 7; project p \"P\" {{ oa_entity owner \"Owner\" {{}} oa_activity work \"Work\" {{}} diagram d \"D\" {{}} {declaration} }}"
|
||||
);
|
||||
let errors = crate::language::parse(&source).unwrap_err();
|
||||
assert!(
|
||||
errors.iter().any(|error| error.message.contains(message)),
|
||||
"{declaration}: {errors:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn rendered(context: Context, input: &Value, mode: Mode) -> Value {
|
||||
Value::Object(policy::fields(context, input.as_object().unwrap(), mode))
|
||||
}
|
||||
|
||||
fn compact(context: Context, input: Value, expected: Value) {
|
||||
let original = input.clone();
|
||||
let result = rendered(context, &input, Mode::Compact);
|
||||
assert_eq!(result, expected, "context {context:?}");
|
||||
assert_eq!(input, original, "the input remains immutable");
|
||||
assert_eq!(
|
||||
rendered(context, &result, Mode::Compact),
|
||||
result,
|
||||
"compact policy is idempotent"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_comparison_restores_only_the_three_known_skips() {
|
||||
let exchange = json!({"body":{"Exchange":{"exchange":null,"sender":null,"receiver":null,"message_kind":"Asynchronous","reply_to":null,"values":[]}}});
|
||||
let mut complete = exchange.clone();
|
||||
complete["body"]["Exchange"]["operation"] = Value::Null;
|
||||
assert_eq!(
|
||||
rendered(
|
||||
Context::Element(Kind::Occurrence),
|
||||
&exchange,
|
||||
Mode::CompleteForEdit
|
||||
),
|
||||
complete
|
||||
);
|
||||
assert_eq!(
|
||||
rendered(
|
||||
Context::Node,
|
||||
&json!({"element":"e","diagram":"d"}),
|
||||
Mode::CompleteForEdit
|
||||
),
|
||||
json!({"element":"e","diagram":"d","port_anchors":[]})
|
||||
);
|
||||
assert_eq!(
|
||||
rendered(
|
||||
Context::Trace,
|
||||
&json!({"baseline":{"oa":{"name":"","description":""},"sa":{"name":"","description":""}}}),
|
||||
Mode::CompleteForEdit
|
||||
),
|
||||
json!({"baseline":{"oa":{"name":"","description":""},"sa":{"name":"","description":""},"semantic":null}})
|
||||
);
|
||||
for input in [
|
||||
json!({"body":{"Action":{"action":null,"lifeline":null}}}),
|
||||
json!({"body":{"State":{"state":null,"lifeline":null}}}),
|
||||
] {
|
||||
assert_eq!(
|
||||
rendered(
|
||||
Context::Element(Kind::Occurrence),
|
||||
&input,
|
||||
Mode::CompleteForEdit
|
||||
),
|
||||
input
|
||||
);
|
||||
}
|
||||
for input in [
|
||||
json!({"baseline":null}),
|
||||
json!({"baseline":{"semantic":{"historical":[null,[],false,0,""]},"oa":{"name":"n","description":""},"sa":{"name":"s","description":""}}}),
|
||||
] {
|
||||
assert_eq!(
|
||||
rendered(Context::Trace, &input, Mode::CompleteForEdit),
|
||||
input
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_element_omits_only_empty_common_defaults() {
|
||||
for kind in Kind::ALL {
|
||||
// Future/unknown payloads are preservation sentinels, not admitted source.
|
||||
compact(
|
||||
Context::Element(kind),
|
||||
json!({"description":"","parent":null,"future":{"empty":[],"absent":null,"zero":0,"flag":false}}),
|
||||
json!({"future":{"empty":[],"absent":null,"zero":0,"flag":false}}),
|
||||
);
|
||||
compact(
|
||||
Context::Element(kind),
|
||||
json!({"description":" \n","parent":"owner"}),
|
||||
json!({"description":" \n","parent":"owner"}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_element_defaults_are_context_specific() {
|
||||
for kind in [Kind::Process, Kind::FunctionalChain] {
|
||||
compact(Context::Element(kind), json!({"steps":[]}), json!({}));
|
||||
compact(
|
||||
Context::Element(kind),
|
||||
json!({"steps":["second","first"]}),
|
||||
json!({"steps":["second","first"]}),
|
||||
);
|
||||
}
|
||||
compact(
|
||||
Context::Element(Kind::ExchangeItem),
|
||||
json!({"kind":"Information","mechanism":"Unspecified","supertype":null}),
|
||||
json!({}),
|
||||
);
|
||||
compact(
|
||||
Context::Element(Kind::ExchangeItem),
|
||||
json!({"kind":"Energy","mechanism":"Flow","supertype":"base"}),
|
||||
json!({"kind":"Energy","mechanism":"Flow","supertype":"base"}),
|
||||
);
|
||||
compact(
|
||||
Context::Element(Kind::ExchangeCategory),
|
||||
json!({"family":"Work","exchanges":[]}),
|
||||
json!({}),
|
||||
);
|
||||
compact(
|
||||
Context::Element(Kind::ExchangeCategory),
|
||||
json!({"family":"Participant","exchanges":["x"]}),
|
||||
json!({"family":"Participant","exchanges":["x"]}),
|
||||
);
|
||||
compact(
|
||||
Context::Element(Kind::Interface),
|
||||
json!({"supertype":null}),
|
||||
json!({}),
|
||||
);
|
||||
compact(
|
||||
Context::Element(Kind::Lifeline),
|
||||
json!({"represented":null}),
|
||||
json!({}),
|
||||
);
|
||||
compact(
|
||||
Context::Element(Kind::Constraint),
|
||||
json!({"expression":"","constrained":[]}),
|
||||
json!({}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scenario_source_default_never_uses_the_graphical_viewpoint_default() {
|
||||
compact(
|
||||
Context::Element(Kind::Scenario),
|
||||
json!({"viewpoint":"MixedLegacy","context":null,"precondition":"","postcondition":"","steps":[]}),
|
||||
json!({}),
|
||||
);
|
||||
for viewpoint in [
|
||||
"OperationalActivity",
|
||||
"Functional",
|
||||
"ParticipantInteraction",
|
||||
"BoundaryInterface",
|
||||
] {
|
||||
compact(
|
||||
Context::Element(Kind::Scenario),
|
||||
json!({"viewpoint":viewpoint,"context":"scope","precondition":"ready","postcondition":"","steps":["two","one"]}),
|
||||
json!({"viewpoint":viewpoint,"context":"scope","precondition":"ready","steps":["two","one"]}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_state_header_supplies_its_own_kind() {
|
||||
for (kind, name) in [
|
||||
(Kind::State, "State"),
|
||||
(Kind::Mode, "Mode"),
|
||||
(Kind::Initial, "Initial"),
|
||||
(Kind::Final, "Final"),
|
||||
(Kind::Choice, "Choice"),
|
||||
(Kind::Junction, "Junction"),
|
||||
(Kind::Fork, "Fork"),
|
||||
(Kind::Join, "Join"),
|
||||
(Kind::ShallowHistory, "ShallowHistory"),
|
||||
(Kind::DeepHistory, "DeepHistory"),
|
||||
(Kind::EntryPoint, "EntryPoint"),
|
||||
(Kind::ExitPoint, "ExitPoint"),
|
||||
(Kind::Terminate, "Terminate"),
|
||||
] {
|
||||
compact(
|
||||
Context::Element(kind),
|
||||
json!({"kind":name,"entry":"","exit":"","do_activity":""}),
|
||||
json!({}),
|
||||
);
|
||||
}
|
||||
// Invalid mismatches must not be silently normalized by the writer policy.
|
||||
compact(
|
||||
Context::Element(Kind::Mode),
|
||||
json!({"kind":"State","entry":"work"}),
|
||||
json!({"kind":"State","entry":"work"}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_field_unbounded_and_present_empty_values_are_not_defaults() {
|
||||
compact(
|
||||
Context::Element(Kind::DataField),
|
||||
json!({"type_ref":"String","minimum":1,"maximum":1,"unit":"","direction":"Unspecified","storage":"Value","default":null}),
|
||||
json!({}),
|
||||
);
|
||||
compact(
|
||||
Context::Element(Kind::DataField),
|
||||
json!({"type_ref":{"Named":"record"},"minimum":0,"maximum":null,"unit":"m","direction":"Out","storage":"Reference","default":[]}),
|
||||
json!({"type_ref":{"Named":"record"},"minimum":0,"maximum":null,"unit":"m","direction":"Out","storage":"Reference","default":[]}),
|
||||
);
|
||||
for value in [
|
||||
json!({"Boolean":false}),
|
||||
json!({"Integer":0}),
|
||||
json!({"Real":0.0}),
|
||||
json!({"String":""}),
|
||||
] {
|
||||
compact(
|
||||
Context::Element(Kind::DataField),
|
||||
json!({"default":[value.clone()]}),
|
||||
json!({"default":[value]}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_definition_whole_default_and_selected_required_fields_differ() {
|
||||
compact(
|
||||
Context::Element(Kind::DataType),
|
||||
json!({"definition":{"String":{"minimum_length":0,"maximum_length":null}},"default":null,"unit":""}),
|
||||
json!({}),
|
||||
);
|
||||
for (input, expected) in [
|
||||
(
|
||||
json!({"Integer":{"minimum":null,"maximum":null}}),
|
||||
json!({"Integer":{}}),
|
||||
),
|
||||
(
|
||||
json!({"Real":{"minimum":null,"maximum":2.5}}),
|
||||
json!({"Real":{"maximum":2.5}}),
|
||||
),
|
||||
(
|
||||
json!({"String":{"minimum_length":4,"maximum_length":null}}),
|
||||
json!({"String":{"minimum_length":4}}),
|
||||
),
|
||||
(json!({"Record":{"supertype":null}}), json!({"Record":{}})),
|
||||
(
|
||||
json!({"Enumeration":{"literals":[]}}),
|
||||
json!({"Enumeration":{"literals":[]}}),
|
||||
),
|
||||
] {
|
||||
compact(
|
||||
Context::Element(Kind::DataType),
|
||||
json!({"definition":input}),
|
||||
json!({"definition":expected}),
|
||||
);
|
||||
}
|
||||
compact(
|
||||
Context::Element(Kind::DataType),
|
||||
json!({"definition":"Boolean","default":{"Boolean":false}}),
|
||||
json!({"definition":"Boolean","default":{"Boolean":false}}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn port_contract_and_its_required_payload_stay_explicit() {
|
||||
compact(
|
||||
Context::Element(Kind::Port),
|
||||
json!({"contract":"Legacy","legacy":{"direction":"Out","interface":null}}),
|
||||
json!({"contract":"Legacy","legacy":{"direction":"Out"}}),
|
||||
);
|
||||
for contract in [
|
||||
json!({"FunctionInput":{"incoming_items":[]}}),
|
||||
json!({"FunctionOutput":{"outgoing_items":[]}}),
|
||||
json!({"Component":{"kind":"Standard","orientation":"Unset"}}),
|
||||
] {
|
||||
compact(
|
||||
Context::Element(Kind::Port),
|
||||
json!({"contract":contract.clone(),"legacy":null}),
|
||||
json!({"contract":contract}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn occurrence_whole_default_is_distinct_from_selected_constructor_payload() {
|
||||
compact(
|
||||
Context::Element(Kind::Occurrence),
|
||||
json!({"body":{"Exchange":{"exchange":null,"sender":null,"receiver":null,"message_kind":"Asynchronous","reply_to":null,"values":[]}},"precondition":"","postcondition":""}),
|
||||
json!({}),
|
||||
);
|
||||
for (input, expected) in [
|
||||
(
|
||||
json!({"Action":{"action":null,"lifeline":null}}),
|
||||
json!({"Action":{}}),
|
||||
),
|
||||
(
|
||||
json!({"State":{"state":null,"lifeline":null}}),
|
||||
json!({"State":{}}),
|
||||
),
|
||||
(
|
||||
json!({"SequenceReference":{"sequence":null}}),
|
||||
json!({"SequenceReference":{}}),
|
||||
),
|
||||
(
|
||||
json!({"ScenarioUse":{"scenario":null,"bindings":[]}}),
|
||||
json!({"ScenarioUse":{"bindings":[]}}),
|
||||
),
|
||||
(
|
||||
json!({"Branch":{"guard":"","occurrences":[]}}),
|
||||
json!({"Branch":{"guard":"","occurrences":[]}}),
|
||||
),
|
||||
(
|
||||
json!({"Fragment":{"operator":"Parallel","branches":[]}}),
|
||||
json!({"Fragment":{"operator":"Parallel","branches":[]}}),
|
||||
),
|
||||
(
|
||||
json!({"Control":{"kind":"Choice"}}),
|
||||
json!({"Control":{"kind":"Choice"}}),
|
||||
),
|
||||
] {
|
||||
compact(
|
||||
Context::Element(Kind::Occurrence),
|
||||
json!({"body":input}),
|
||||
json!({"body":expected}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unresolved_operation_and_required_message_value_are_preserved() {
|
||||
compact(
|
||||
Context::Element(Kind::Occurrence),
|
||||
json!({"body":{"Exchange":{"exchange":null,"sender":null,"receiver":null,"message_kind":"Asynchronous","reply_to":null,"values":[{"field":"f","path":[],"expression":""}],"operation":{"allocation":null}}}}),
|
||||
json!({"body":{"Exchange":{"values":[{"field":"f","expression":""}],"operation":{}}}}),
|
||||
);
|
||||
compact(
|
||||
Context::Element(Kind::Occurrence),
|
||||
json!({"body":{"Exchange":{"exchange":"flow","sender":"a","receiver":"b","message_kind":{"Timer":{"expression":""}},"reply_to":"request","values":[],"operation":null}}}),
|
||||
json!({"body":{"Exchange":{"exchange":"flow","sender":"a","receiver":"b","message_kind":{"Timer":{"expression":""}},"reply_to":"request"}}}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_body_whole_default_does_not_prune_selected_required_fields() {
|
||||
compact(
|
||||
Context::Element(Kind::Event),
|
||||
json!({"body":{"Signal":{"items":[]}}}),
|
||||
json!({}),
|
||||
);
|
||||
for body in [
|
||||
json!({"Signal":{"items":["i"]}}),
|
||||
json!({"Time":{"kind":"After","expression":""}}),
|
||||
json!({"Change":{"expression":""}}),
|
||||
] {
|
||||
compact(
|
||||
Context::Element(Kind::Event),
|
||||
json!({"body":body.clone()}),
|
||||
json!({"body":body}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relation_required_endpoints_and_contextual_payloads_remain() {
|
||||
for kind in RelationKind::ALL {
|
||||
compact(
|
||||
Context::Relation(kind),
|
||||
json!({"description":"","source":"s","target":"t","future":[]}),
|
||||
json!({"source":"s","target":"t","future":[]}),
|
||||
);
|
||||
}
|
||||
compact(
|
||||
Context::Relation(RelationKind::FunctionalExchange),
|
||||
json!({"source":"s","target":"t","source_port":"sp","target_port":"tp","items":[]}),
|
||||
json!({"source":"s","target":"t","source_port":"sp","target_port":"tp"}),
|
||||
);
|
||||
let contextual = json!({"owner":"o","source":{"references":[],"occurrence":"s"},"target":{"references":[],"occurrence":"t"},"purpose":{"Precedence":{"guard":""}}});
|
||||
compact(
|
||||
Context::Relation(RelationKind::ContextualLink),
|
||||
contextual.clone(),
|
||||
contextual,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relation_defaults_preserve_contextual_orientation_and_multiplicity() {
|
||||
for (kind, orientation) in [
|
||||
(RelationKind::BoundaryExchange, true),
|
||||
(RelationKind::CommunicationMean, false),
|
||||
] {
|
||||
compact(
|
||||
Context::Relation(kind),
|
||||
json!({"oriented":orientation,"items":[]}),
|
||||
json!({}),
|
||||
);
|
||||
compact(
|
||||
Context::Relation(kind),
|
||||
json!({"oriented":!orientation}),
|
||||
json!({"oriented":!orientation}),
|
||||
);
|
||||
}
|
||||
compact(
|
||||
Context::Relation(RelationKind::DataAssociation),
|
||||
json!({"source_role":"","target_role":"","source_minimum":0,"target_minimum":0,"source_maximum":null,"target_maximum":null,"source_navigable":false,"target_navigable":false}),
|
||||
json!({}),
|
||||
);
|
||||
compact(
|
||||
Context::Relation(RelationKind::DataAssociation),
|
||||
json!({"source_maximum":0,"target_maximum":1,"source_navigable":true}),
|
||||
json!({"source_maximum":0,"target_maximum":1,"source_navigable":true}),
|
||||
);
|
||||
compact(
|
||||
Context::Relation(RelationKind::InterfaceItemAllocation),
|
||||
json!({"order":0,"transmission":"Unspecified","acquisition":"Unspecified"}),
|
||||
json!({}),
|
||||
);
|
||||
compact(
|
||||
Context::Relation(RelationKind::CapabilityExtend),
|
||||
json!({"condition":""}),
|
||||
json!({}),
|
||||
);
|
||||
compact(
|
||||
Context::Relation(RelationKind::SequenceLink),
|
||||
json!({"guard":""}),
|
||||
json!({}),
|
||||
);
|
||||
compact(
|
||||
Context::Relation(RelationKind::StateAction),
|
||||
json!({"phase":"Do"}),
|
||||
json!({}),
|
||||
);
|
||||
compact(
|
||||
Context::Relation(RelationKind::StateTransition),
|
||||
json!({"transition_kind":"External","trigger":"","guard":"","effect":"","triggers":[],"effects":[]}),
|
||||
json!({}),
|
||||
);
|
||||
compact(
|
||||
Context::Relation(RelationKind::StateTransition),
|
||||
json!({"transition_kind":"Internal","triggers":["t"],"effects":["e"]}),
|
||||
json!({"transition_kind":"Internal","triggers":["t"],"effects":["e"]}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagram_options_remove_only_unrestricted_defaults() {
|
||||
let defaults = json!({"hidden_edges":[],"relation_kinds":"All","work_categories":"All","participant_categories":"All","coverage":"All","work_contexts":"All","exchange_labels":"Name"});
|
||||
compact(
|
||||
Context::Diagram,
|
||||
json!({"description":"","layer":"Oa","projection":"Overview","routes":[],"view_options":defaults}),
|
||||
json!({}),
|
||||
);
|
||||
compact(
|
||||
Context::Diagram,
|
||||
json!({"layer":"Sa","projection":{"StateMachine":{"machine":"m"}},"view_options":{"hidden_edges":[],"relation_kinds":{"Only":[]},"work_categories":{"Any":[]},"participant_categories":"All","coverage":"All","work_contexts":{"Any":[]},"exchange_labels":"Name"}}),
|
||||
json!({"layer":"Sa","projection":{"StateMachine":{"machine":"m"}},"view_options":{"relation_kinds":{"Only":[]},"work_categories":{"Any":[]},"work_contexts":{"Any":[]}}}),
|
||||
);
|
||||
compact(
|
||||
Context::Diagram,
|
||||
json!({"routes":[{"edge":"e","waypoints":[],"label":null}]}),
|
||||
json!({"routes":[{"edge":"e"}]}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_whole_defaults_preserve_negative_zero_and_required_coordinates() {
|
||||
compact(
|
||||
Context::Node,
|
||||
json!({"diagram":"d","element":"e","position":{"x":0.0,"y":0.0},"size":{"width":224.0,"height":94.0},"collapsed":false}),
|
||||
json!({"diagram":"d","element":"e"}),
|
||||
);
|
||||
for position in [
|
||||
json!({"x":-0.0,"y":0.0}),
|
||||
json!({"x":0.0,"y":-0.0}),
|
||||
json!({"x":1.0,"y":0.0}),
|
||||
] {
|
||||
let result = rendered(
|
||||
Context::Node,
|
||||
&json!({"position":position.clone()}),
|
||||
Mode::Compact,
|
||||
);
|
||||
assert_eq!(result, json!({"position":position.clone()}));
|
||||
for coordinate in ["x", "y"] {
|
||||
assert_eq!(
|
||||
result["position"][coordinate].as_f64().unwrap().to_bits(),
|
||||
position[coordinate].as_f64().unwrap().to_bits(),
|
||||
"coordinate sign is part of fidelity"
|
||||
);
|
||||
}
|
||||
}
|
||||
compact(
|
||||
Context::Node,
|
||||
json!({"size":{"width":300.0,"height":94.0},"port_anchors":[{"port":"p","side":"Left","offset":0.0}]}),
|
||||
json!({"size":{"width":300.0,"height":94.0},"port_anchors":[{"port":"p","side":"Left","offset":0.0}]}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artifact_present_empty_strings_and_trace_history_stay_distinct() {
|
||||
compact(
|
||||
Context::Artifact,
|
||||
json!({"owner":"o","description":"","relation":"References","uri":"","revision":null,"fragment":null}),
|
||||
json!({"owner":"o"}),
|
||||
);
|
||||
compact(
|
||||
Context::Artifact,
|
||||
json!({"owner":"o","relation":"Implements","revision":"","fragment":""}),
|
||||
json!({"owner":"o","relation":"Implements","revision":"","fragment":""}),
|
||||
);
|
||||
compact(
|
||||
Context::Trace,
|
||||
json!({"oa":"o","sa":"s","description":"","kind":"Refinement","baseline":null}),
|
||||
json!({"oa":"o","sa":"s"}),
|
||||
);
|
||||
compact(
|
||||
Context::Trace,
|
||||
json!({"baseline":{"oa":{"name":"","description":""},"sa":{"name":"s","description":""}}}),
|
||||
json!({"baseline":{"oa":{"name":"","description":""},"sa":{"name":"s","description":""}}}),
|
||||
);
|
||||
let baseline = json!({"oa":{"name":"o","description":""},"sa":{"name":"s","description":""},"semantic":{"facets":[{"Value":null},{"Value":[]},{"Value":false},{"Value":0},{"Value":""}]}});
|
||||
compact(
|
||||
Context::Trace,
|
||||
json!({"kind":"Manual","baseline":baseline.clone()}),
|
||||
json!({"kind":"Manual","baseline":baseline}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_future_fields_prevent_whole_default_removal() {
|
||||
// These sentinels exercise preservation, not current parser admission of
|
||||
// unknown fields. Only known slots may lose their redundant defaults.
|
||||
compact(
|
||||
Context::Element(Kind::DataType),
|
||||
json!({"definition":{"String":{"minimum_length":0,"maximum_length":null,"future":[]}}}),
|
||||
json!({"definition":{"String":{"minimum_length":0,"future":[]}}}),
|
||||
);
|
||||
compact(
|
||||
Context::Element(Kind::Occurrence),
|
||||
json!({"body":{"Exchange":{"exchange":null,"sender":null,"receiver":null,"message_kind":"Asynchronous","reply_to":null,"values":[],"operation":null,"future":false}}}),
|
||||
json!({"body":{"Exchange":{"future":false}}}),
|
||||
);
|
||||
compact(
|
||||
Context::Diagram,
|
||||
json!({"view_options":{"hidden_edges":[],"relation_kinds":"All","work_categories":"All","participant_categories":"All","coverage":"All","work_contexts":"All","exchange_labels":"Name","future":null}}),
|
||||
json!({"view_options":{"future":null}}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_and_multikey_constructor_wrappers_are_not_reinterpreted() {
|
||||
for definition in [
|
||||
json!({"Future":{"minimum":null,"maximum":null}}),
|
||||
json!({"String":{"minimum_length":0,"maximum_length":null},"Future":{}}),
|
||||
] {
|
||||
compact(
|
||||
Context::Element(Kind::DataType),
|
||||
json!({"definition":definition.clone()}),
|
||||
json!({"definition":definition}),
|
||||
);
|
||||
}
|
||||
for body in [
|
||||
json!({"Future":{"operation":null,"values":[]}}),
|
||||
json!({"Exchange":{"exchange":null,"values":[]},"Future":{}}),
|
||||
] {
|
||||
let fields = json!({"body":body});
|
||||
assert_eq!(
|
||||
rendered(
|
||||
Context::Element(Kind::Occurrence),
|
||||
&fields,
|
||||
Mode::CompleteForEdit
|
||||
),
|
||||
fields,
|
||||
"do not invent a selected Exchange constructor in an unknown wrapper",
|
||||
);
|
||||
compact(Context::Element(Kind::Occurrence), fields.clone(), fields);
|
||||
}
|
||||
}
|
||||
@@ -726,7 +726,8 @@ fn apply_edits(bundle: &mut SourceBundle, edits: Edits) -> Result<(), Vec<Source
|
||||
pub fn format_bundle(project: &Project) -> SourceBundle {
|
||||
let ownership = default_ownership(project);
|
||||
let aliases: BTreeMap<_, _> = project.identities().into_iter().collect();
|
||||
let mut declarations = language::format_declarations(project);
|
||||
let mut declarations =
|
||||
language::format_declarations(project, language::DeclarationFormat::Compact);
|
||||
declarations.sort_by_key(|(id, _)| declaration_order(project, *id, &aliases));
|
||||
let mut files: BTreeMap<LogicalPath, String> = BTreeMap::new();
|
||||
for (id, declaration) in declarations {
|
||||
@@ -735,11 +736,13 @@ pub fn format_bundle(project: &Project) -> SourceBundle {
|
||||
.or_default()
|
||||
.push_str(&unindent(&declaration));
|
||||
}
|
||||
let mut entry = format!(
|
||||
"arcadia {SCHEMA_VERSION};\n{}{{\n description {};\n",
|
||||
project_header(project),
|
||||
quote(&project.description)
|
||||
);
|
||||
let mut entry = format!("arcadia {SCHEMA_VERSION};\n{}{{\n", project_header(project));
|
||||
if !project.description.is_empty() {
|
||||
entry.push_str(&format!(
|
||||
" description {};\n",
|
||||
quote(&project.description)
|
||||
));
|
||||
}
|
||||
for path in files.keys() {
|
||||
entry.push_str(&format!(" include {};\n", quote(path)));
|
||||
}
|
||||
@@ -850,8 +853,25 @@ pub fn update_bundle_with_identity_map(
|
||||
)]);
|
||||
}
|
||||
let origins: BTreeMap<_, _> = identities.iter().map(|(old, new)| (*new, *old)).collect();
|
||||
let before: BTreeMap<_, _> = language::format_declarations(old).into_iter().collect();
|
||||
let after: BTreeMap<_, _> = language::format_declarations(project).into_iter().collect();
|
||||
// Comparison must include defaults even when persistence serialization or
|
||||
// compact output omits them. Otherwise an unrelated payload edit erases
|
||||
// unchanged explicit fields from the authored document.
|
||||
let before: BTreeMap<_, _> =
|
||||
language::format_declarations(old, language::DeclarationFormat::CompleteForEdit)
|
||||
.into_iter()
|
||||
.collect();
|
||||
let after: BTreeMap<_, _> =
|
||||
language::format_declarations(project, language::DeclarationFormat::CompleteForEdit)
|
||||
.into_iter()
|
||||
.collect();
|
||||
let compact: BTreeMap<_, _> =
|
||||
language::format_declarations(project, language::DeclarationFormat::Compact)
|
||||
.into_iter()
|
||||
.collect();
|
||||
let compact_values: BTreeMap<_, _> =
|
||||
language::format_declarations(project, language::DeclarationFormat::CompactValues)
|
||||
.into_iter()
|
||||
.collect();
|
||||
let old_aliases: BTreeMap<_, _> = old
|
||||
.identities()
|
||||
.into_iter()
|
||||
@@ -907,6 +927,27 @@ pub fn update_bundle_with_identity_map(
|
||||
};
|
||||
let mut previous = parse(old_text)?;
|
||||
let mut next = parse(new_text)?;
|
||||
let next_id = identities[&id];
|
||||
let value_text = &compact_values[&next_id];
|
||||
let insertion = InsertionSyntax::new(
|
||||
&next.fields,
|
||||
&parse(value_text)?.fields,
|
||||
value_text,
|
||||
&parse(&compact[&next_id])?.fields,
|
||||
);
|
||||
let old_kind = previous.header["kind"];
|
||||
let new_kind = next.header["kind"];
|
||||
if old_text[old_kind.start..old_kind.end] != new_text[new_kind.start..new_kind.end]
|
||||
&& let language::EditKind::Record(actual_fields) = &actual.fields.kind
|
||||
&& let language::EditKind::Record(previous_fields) = &mut previous.fields.kind
|
||||
{
|
||||
// An implicit value is supplied by its declaration context.
|
||||
// After a kind change, an unchanged value may need explicit
|
||||
// syntax (e.g. BoundaryExchange true -> CommunicationMean true).
|
||||
// Reconsider only absent fields; authored explicit values still
|
||||
// compare against their complete old values and keep trivia.
|
||||
previous_fields.retain(|key, _| actual_fields.contains_key(key));
|
||||
}
|
||||
if actual.lexical_owner.is_some()
|
||||
&& let language::EditKind::Record(fields) = &actual.fields.kind
|
||||
&& !fields.contains_key("parent")
|
||||
@@ -923,6 +964,7 @@ pub fn update_bundle_with_identity_map(
|
||||
source,
|
||||
before: old_text,
|
||||
after: new_text,
|
||||
insertion: Some(&insertion),
|
||||
old_aliases: &old_aliases,
|
||||
new_aliases: &new_aliases,
|
||||
legacy_value_paths: false,
|
||||
@@ -953,6 +995,7 @@ pub fn update_bundle_with_identity_map(
|
||||
source,
|
||||
before: &before,
|
||||
after: &after,
|
||||
insertion: None,
|
||||
old_aliases: &old_aliases,
|
||||
new_aliases: &new_aliases,
|
||||
legacy_value_paths: false,
|
||||
@@ -1011,7 +1054,7 @@ pub fn update_bundle_with_identity_map(
|
||||
project,
|
||||
identities,
|
||||
&original_order,
|
||||
&after,
|
||||
&compact,
|
||||
limits,
|
||||
)?;
|
||||
let result = compile_bundle(&bundle, limits)?;
|
||||
@@ -1119,10 +1162,73 @@ fn inserted_value(text: &str, indent: &str, source: &str) -> String {
|
||||
output
|
||||
}
|
||||
|
||||
// Value fragments retain required payload fields even when the enclosing
|
||||
// declaration field is itself omittable. For example, a selected String type
|
||||
// needs minimum_length; an Event Signal needs items. The complete tree remains
|
||||
// the comparison authority; this parallel tree is only an insertion template.
|
||||
struct InsertionSyntax<'a> {
|
||||
source: &'a str,
|
||||
values: BTreeMap<usize, Span>,
|
||||
omitted_fields: BTreeSet<usize>,
|
||||
}
|
||||
impl<'a> InsertionSyntax<'a> {
|
||||
fn new(
|
||||
complete: &language::EditValue,
|
||||
compact_values: &language::EditValue,
|
||||
source: &'a str,
|
||||
compact: &language::EditValue,
|
||||
) -> Self {
|
||||
let mut result = Self {
|
||||
source,
|
||||
values: BTreeMap::new(),
|
||||
omitted_fields: BTreeSet::new(),
|
||||
};
|
||||
result.collect(complete, compact_values);
|
||||
if let (language::EditKind::Record(all), language::EditKind::Record(kept)) =
|
||||
(&complete.kind, &compact.kind)
|
||||
{
|
||||
for (key, field) in all {
|
||||
if !kept.contains_key(key) {
|
||||
result.omitted_fields.insert(field.value.span.start);
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
fn collect(&mut self, complete: &language::EditValue, compact: &language::EditValue) {
|
||||
// Borrow the existing compact text once. Copying each containing record
|
||||
// here would duplicate a large nested payload at every depth.
|
||||
self.values.insert(complete.span.start, compact.span);
|
||||
match (&complete.kind, &compact.kind) {
|
||||
(language::EditKind::Record(all), language::EditKind::Record(kept)) => {
|
||||
for (key, field) in all {
|
||||
if let Some(next) = kept.get(key) {
|
||||
self.collect(&field.value, &next.value);
|
||||
} else {
|
||||
self.omitted_fields.insert(field.value.span.start);
|
||||
}
|
||||
}
|
||||
}
|
||||
(language::EditKind::List(all), language::EditKind::List(kept)) => {
|
||||
assert_eq!(
|
||||
all.len(),
|
||||
kept.len(),
|
||||
"compaction preserves array membership"
|
||||
);
|
||||
for (field, next) in all.iter().zip(kept) {
|
||||
self.collect(field, next);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SyntaxUpdate<'a> {
|
||||
source: &'a str,
|
||||
before: &'a str,
|
||||
after: &'a str,
|
||||
insertion: Option<&'a InsertionSyntax<'a>>,
|
||||
old_aliases: &'a BTreeMap<String, String>,
|
||||
new_aliases: &'a BTreeMap<String, String>,
|
||||
// Source3 migration only adds path qualification; the existing field identity
|
||||
@@ -1131,6 +1237,16 @@ struct SyntaxUpdate<'a> {
|
||||
identity_origins: Option<&'a BTreeMap<Id, Id>>,
|
||||
}
|
||||
impl SyntaxUpdate<'_> {
|
||||
fn inserted(&self, value: &language::EditValue) -> &str {
|
||||
self.insertion
|
||||
.and_then(|syntax| {
|
||||
syntax
|
||||
.values
|
||||
.get(&value.span.start)
|
||||
.map(|span| &syntax.source[span.start..span.end])
|
||||
})
|
||||
.unwrap_or(&self.after[value.span.start..value.span.end])
|
||||
}
|
||||
fn header(
|
||||
&self,
|
||||
actual: &BTreeMap<&str, Span>,
|
||||
@@ -1243,7 +1359,7 @@ impl SyntaxUpdate<'_> {
|
||||
_ => {
|
||||
erase_syntax(self.source, actual.span, edits);
|
||||
let text = inserted_value(
|
||||
&self.after[after.span.start..after.span.end],
|
||||
self.inserted(after),
|
||||
indent_at(self.source, actual.span.start),
|
||||
self.source,
|
||||
);
|
||||
@@ -1308,6 +1424,9 @@ impl SyntaxUpdate<'_> {
|
||||
.iter()
|
||||
.filter(|(key, field)| {
|
||||
!actual.contains_key(*key)
|
||||
&& self.insertion.is_none_or(|syntax| {
|
||||
!syntax.omitted_fields.contains(&field.value.span.start)
|
||||
})
|
||||
&& before
|
||||
.and_then(|fields| fields.get(*key))
|
||||
.is_none_or(|old| !self.same(&old.value, &field.value))
|
||||
@@ -1326,11 +1445,7 @@ impl SyntaxUpdate<'_> {
|
||||
.unwrap_or_else(|| format!("{} ", indent_at(self.source, span.start)));
|
||||
let mut text = String::new();
|
||||
for (key, field) in added {
|
||||
let value = inserted_value(
|
||||
&self.after[field.value.span.start..field.value.span.end],
|
||||
&indent,
|
||||
self.source,
|
||||
);
|
||||
let value = inserted_value(self.inserted(&field.value), &indent, self.source);
|
||||
if multiline {
|
||||
text.push_str(&indent);
|
||||
text.push_str(&format!("{key} {value};{}", newline(self.source)));
|
||||
@@ -1450,11 +1565,7 @@ impl SyntaxUpdate<'_> {
|
||||
} else {
|
||||
text.push(' ');
|
||||
}
|
||||
text.push_str(&inserted_value(
|
||||
&self.after[next.span.start..next.span.end],
|
||||
&indent,
|
||||
self.source,
|
||||
));
|
||||
text.push_str(&inserted_value(self.inserted(next), &indent, self.source));
|
||||
suffix = "";
|
||||
}
|
||||
if i + 1 < after.len() {
|
||||
|
||||
@@ -11,7 +11,10 @@ pub(super) fn upgrade(
|
||||
.as_ref()
|
||||
.expect("an explicit source migration report");
|
||||
let project = ¤t.decoded.project;
|
||||
let canonical: BTreeMap<_, _> = language::format_declarations(project).into_iter().collect();
|
||||
let canonical: BTreeMap<_, _> =
|
||||
language::format_declarations(project, language::DeclarationFormat::CompleteForEdit)
|
||||
.into_iter()
|
||||
.collect();
|
||||
let changed: BTreeSet<_> = report
|
||||
.semantic_defaults
|
||||
.iter()
|
||||
@@ -59,6 +62,7 @@ pub(super) fn upgrade(
|
||||
source,
|
||||
before: source,
|
||||
after,
|
||||
insertion: None,
|
||||
old_aliases: &aliases,
|
||||
new_aliases: &aliases,
|
||||
legacy_value_paths: true,
|
||||
|
||||
@@ -750,7 +750,7 @@ pub fn plan_transition(
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if !compatible_existing(project, *source, target, request.direction) {
|
||||
if !counterpart_compatible(project, *source, target, request.direction) {
|
||||
conflict(
|
||||
&mut plan,
|
||||
Some(*source),
|
||||
@@ -1317,12 +1317,22 @@ fn compatible_kinds(source: Kind, target: Kind, direction: TransitionDirection)
|
||||
}
|
||||
inferred_kind(source, direction) == Some(target)
|
||||
}
|
||||
fn compatible_existing(
|
||||
/// Whether existing records have compatible layers, categories and kinds for
|
||||
/// an ordinary transition. This is candidate eligibility, not approval of a
|
||||
/// complete plan: properties, dependencies and structural conflicts still need
|
||||
/// the normal preview and apply validation. Manual traces are not restricted
|
||||
/// by this predicate.
|
||||
pub fn counterpart_compatible(
|
||||
p: &Project,
|
||||
source: Id,
|
||||
target: Id,
|
||||
direction: TransitionDirection,
|
||||
) -> bool {
|
||||
if p.layer_of(source) != Some(direction.source_layer())
|
||||
|| p.layer_of(target) != Some(direction.target_layer())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
match (p.elements.get(&source), p.elements.get(&target)) {
|
||||
(Some(a), Some(b)) => compatible_kinds(a.kind(), b.kind(), direction),
|
||||
_ => match (p.relations.get(&source), p.relations.get(&target)) {
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
//! Existing source-7 nested omission compatibility, independently of formatting.
|
||||
//!
|
||||
//! Selected constructors use their own field contracts. These handwritten
|
||||
//! sources and literal models distinguish nullable optional slots from required
|
||||
//! fields; they do not prescribe compact canonical output or broaden admission.
|
||||
|
||||
use oa_core::{source_project::*, *};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn id(number: u8) -> Id {
|
||||
Id::parse_str(&format!("00000000-0000-4000-8000-{number:012}")).unwrap()
|
||||
}
|
||||
|
||||
fn bundle(declarations: &str) -> SourceBundle {
|
||||
let source = format!(
|
||||
"arcadia 7;\nproject p \"Admission\" id \"00000000-0000-4000-8000-000000000001\" {{\n{declarations}\n}}\n"
|
||||
);
|
||||
SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([("project.arcadia".into(), source)]),
|
||||
}
|
||||
}
|
||||
|
||||
fn element(number: u8, alias: &str, name: &str, parent: Option<Id>, body: ElementBody) -> Element {
|
||||
Element {
|
||||
id: id(number),
|
||||
alias: alias.into(),
|
||||
name: name.into(),
|
||||
description: String::new(),
|
||||
parent,
|
||||
body,
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_admitted(input: &SourceBundle, elements: Vec<Element>) -> Project {
|
||||
let expected = Project {
|
||||
schema_version: 7,
|
||||
id: id(1),
|
||||
alias: "p".into(),
|
||||
name: "Admission".into(),
|
||||
description: String::new(),
|
||||
elements: elements.into_iter().map(|e| (e.id, e)).collect(),
|
||||
relations: BTreeMap::new(),
|
||||
diagrams: BTreeMap::new(),
|
||||
artifact_links: BTreeMap::new(),
|
||||
traces: BTreeMap::new(),
|
||||
};
|
||||
let compiled = compile_bundle(input, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(compiled.decoded.project, expected, "complete literal model");
|
||||
assert_eq!(
|
||||
compiled.bundle, *input,
|
||||
"fixed IDs require no source stabilization"
|
||||
);
|
||||
compiled.decoded.project
|
||||
}
|
||||
|
||||
fn prefix(layer: Layer) -> &'static str {
|
||||
match layer {
|
||||
Layer::Oa => "oa",
|
||||
Layer::Sa => "sa",
|
||||
}
|
||||
}
|
||||
|
||||
fn data_bundle(layer: Layer, definition: &str) -> SourceBundle {
|
||||
bundle(&format!(
|
||||
" {}_data_type subject \"Subject\" id \"00000000-0000-4000-8000-000000000002\" {{ definition {{ {definition}; }}; }}",
|
||||
prefix(layer)
|
||||
))
|
||||
}
|
||||
|
||||
fn admit_data(layer: Layer, source: &str, definition: DataTypeDefinition) -> Project {
|
||||
assert_admitted(
|
||||
&data_bundle(layer, source),
|
||||
vec![element(
|
||||
2,
|
||||
"subject",
|
||||
"Subject",
|
||||
None,
|
||||
ElementBody::shared_in(
|
||||
layer,
|
||||
SharedBody::DataType {
|
||||
definition,
|
||||
default: None,
|
||||
unit: String::new(),
|
||||
},
|
||||
),
|
||||
)],
|
||||
)
|
||||
}
|
||||
|
||||
fn assert_missing(input: &SourceBundle, field: &str) {
|
||||
let errors = compile_bundle(input, ProjectLimits::default()).unwrap_err();
|
||||
assert_eq!(
|
||||
errors.len(),
|
||||
1,
|
||||
"expect the targeted admission error: {errors:?}"
|
||||
);
|
||||
let diagnostic = &errors[0].diagnostic;
|
||||
assert_eq!(diagnostic.code, "invalid_source", "{errors:?}");
|
||||
assert!(
|
||||
diagnostic.message.to_lowercase().contains("missing") && diagnostic.message.contains(field),
|
||||
"must reject the missing constructor field {field}, not an unrelated model rule: {errors:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integer_optional_bounds_can_be_omitted_individually_and_together() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for (omitted, explicit, minimum, maximum) in [
|
||||
(
|
||||
"Integer {}",
|
||||
"Integer { minimum none; maximum none; }",
|
||||
None,
|
||||
None,
|
||||
),
|
||||
(
|
||||
"Integer { maximum 9; }",
|
||||
"Integer { minimum none; maximum 9; }",
|
||||
None,
|
||||
Some(9),
|
||||
),
|
||||
(
|
||||
"Integer { minimum -3; }",
|
||||
"Integer { minimum -3; maximum none; }",
|
||||
Some(-3),
|
||||
None,
|
||||
),
|
||||
] {
|
||||
let expected = DataTypeDefinition::Integer { minimum, maximum };
|
||||
assert_eq!(
|
||||
admit_data(layer, omitted, expected.clone()),
|
||||
admit_data(layer, explicit, expected),
|
||||
"omitted and explicit optional Integer bounds agree"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_optional_bounds_can_be_omitted_individually_and_together() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for (omitted, explicit, minimum, maximum) in [
|
||||
(
|
||||
"Real {}",
|
||||
"Real { minimum none; maximum none; }",
|
||||
None,
|
||||
None,
|
||||
),
|
||||
(
|
||||
"Real { maximum 2.5; }",
|
||||
"Real { minimum none; maximum 2.5; }",
|
||||
None,
|
||||
Some(2.5),
|
||||
),
|
||||
(
|
||||
"Real { minimum -1.25; }",
|
||||
"Real { minimum -1.25; maximum none; }",
|
||||
Some(-1.25),
|
||||
None,
|
||||
),
|
||||
] {
|
||||
let expected = DataTypeDefinition::Real { minimum, maximum };
|
||||
assert_eq!(
|
||||
admit_data(layer, omitted, expected.clone()),
|
||||
admit_data(layer, explicit, expected),
|
||||
"omitted and explicit optional Real bounds agree"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_optional_maximum_can_be_omitted_without_resetting_required_minimum() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let expected = DataTypeDefinition::String {
|
||||
minimum_length: 4,
|
||||
maximum_length: None,
|
||||
};
|
||||
assert_eq!(
|
||||
admit_data(layer, "String { minimum_length 4; }", expected.clone()),
|
||||
admit_data(
|
||||
layer,
|
||||
"String { minimum_length 4; maximum_length none; }",
|
||||
expected,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicitly_selected_string_requires_minimum_even_when_whole_definition_has_a_default() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
assert_missing(&data_bundle(layer, "String {}"), "minimum_length");
|
||||
assert_missing(
|
||||
&data_bundle(layer, "String { maximum_length none; }"),
|
||||
"minimum_length",
|
||||
);
|
||||
admit_data(
|
||||
layer,
|
||||
"String { minimum_length 0; }",
|
||||
DataTypeDefinition::String {
|
||||
minimum_length: 0,
|
||||
maximum_length: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absent_numeric_maximum_is_distinct_from_an_explicit_zero_bound() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let unbounded = admit_data(
|
||||
layer,
|
||||
"Integer { minimum -3; }",
|
||||
DataTypeDefinition::Integer {
|
||||
minimum: Some(-3),
|
||||
maximum: None,
|
||||
},
|
||||
);
|
||||
let bounded = admit_data(
|
||||
layer,
|
||||
"Integer { minimum -3; maximum 0; }",
|
||||
DataTypeDefinition::Integer {
|
||||
minimum: Some(-3),
|
||||
maximum: Some(0),
|
||||
},
|
||||
);
|
||||
assert_ne!(unbounded, bounded, "zero is a bound, not omitted syntax");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_has_no_required_payload_field_and_omitted_supertype_means_none() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let expected = DataTypeDefinition::Record { supertype: None };
|
||||
assert_eq!(
|
||||
admit_data(layer, "Record {}", expected.clone()),
|
||||
admit_data(layer, "Record { supertype none; }", expected),
|
||||
);
|
||||
let errors = compile_bundle(
|
||||
&data_bundle(layer, "Record { fields []; }"),
|
||||
ProjectLimits::default(),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(errors.len(), 1, "{errors:?}");
|
||||
assert_eq!(errors[0].diagnostic.code, "invalid_source", "{errors:?}");
|
||||
assert!(
|
||||
errors[0]
|
||||
.diagnostic
|
||||
.message
|
||||
.contains("unknown field `fields`"),
|
||||
"Record remains a closed constructor: {errors:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enumeration_still_requires_its_list_even_when_empty_list_is_admitted() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
assert_missing(&data_bundle(layer, "Enumeration {}"), "literals");
|
||||
admit_data(
|
||||
layer,
|
||||
"Enumeration { literals []; }",
|
||||
DataTypeDefinition::Enumeration { literals: vec![] },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn port_bundle(layer: Layer, fields: &str) -> SourceBundle {
|
||||
let owner_kind = match layer {
|
||||
Layer::Oa => "oa_entity",
|
||||
Layer::Sa => "sa_system",
|
||||
};
|
||||
bundle(&format!(
|
||||
" {owner_kind} owner \"Owner\" id \"00000000-0000-4000-8000-000000000002\" {{}}\n {}_port subject \"Subject\" id \"00000000-0000-4000-8000-000000000003\" {{ parent @owner; {fields} }}",
|
||||
prefix(layer)
|
||||
))
|
||||
}
|
||||
|
||||
fn admit_port(layer: Layer, fields: &str) -> Project {
|
||||
let owner_body = match layer {
|
||||
Layer::Oa => ElementBody::Oa(OaBody::Entity),
|
||||
Layer::Sa => ElementBody::Sa(SaBody::System),
|
||||
};
|
||||
assert_admitted(
|
||||
&port_bundle(layer, fields),
|
||||
vec![
|
||||
element(2, "owner", "Owner", None, owner_body),
|
||||
element(
|
||||
3,
|
||||
"subject",
|
||||
"Subject",
|
||||
Some(id(2)),
|
||||
ElementBody::shared_in(
|
||||
layer,
|
||||
SharedBody::Port {
|
||||
contract: PortContract::Legacy,
|
||||
legacy: Some(LegacyPortContract {
|
||||
direction: PortDirection::Out,
|
||||
interface: None,
|
||||
}),
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_port_interface_can_be_omitted_but_preserves_direction() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
assert_eq!(
|
||||
admit_port(layer, "contract Legacy; legacy { direction Out; };"),
|
||||
admit_port(
|
||||
layer,
|
||||
"contract Legacy; legacy { direction Out; interface none; };"
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_port_direction_remains_required_inside_selected_record() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
assert_missing(
|
||||
&port_bundle(layer, "contract Legacy; legacy {};"),
|
||||
"direction",
|
||||
);
|
||||
assert_missing(
|
||||
&port_bundle(layer, "contract Legacy; legacy { interface none; };"),
|
||||
"direction",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn port_declaration_still_requires_explicit_contract() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
assert_missing(
|
||||
&port_bundle(layer, "legacy { direction Out; };"),
|
||||
"contract",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
//! Public flat/modular formatter fidelity over frozen input and bounded mutations.
|
||||
//! Legacy fixture migration uses the public parser. The original adapter tests
|
||||
//! and exact inputs remain archived at source commit 2a64344; this suite exercises
|
||||
//! production formatters, not that frozen experiment or lossless model edits.
|
||||
|
||||
use oa_core::{language, source_project::*, *};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
const COASTAL: &str = include_str!("../../../examples/coastal-rescue.arcadia");
|
||||
const KEYED: &str =
|
||||
include_str!("../../../harness/fixtures/release1-native/semantic-keyed.arcadia");
|
||||
const STATES: &str =
|
||||
include_str!("../../../harness/fixtures/release1-native/state-actions.arcadia");
|
||||
const DENSE: &str =
|
||||
include_str!("../../../harness/fixtures/release1-native/dense-40/dense-40.arcadia");
|
||||
const OPERATIONS: &str = include_str!(
|
||||
"../../../harness/fixtures/interface-operation-probe/output/boundary-only.arcadia"
|
||||
);
|
||||
|
||||
fn read(source: &str) -> Project {
|
||||
language::parse(source).unwrap()
|
||||
}
|
||||
|
||||
fn identity(project: &Project, alias: &str) -> Id {
|
||||
project
|
||||
.identities()
|
||||
.into_iter()
|
||||
.find(|(_, name)| *name == alias)
|
||||
.unwrap()
|
||||
.0
|
||||
}
|
||||
|
||||
fn assert_common_omissions(project: &Project, compiled: &CompiledSourceProject) {
|
||||
if project.description.is_empty() {
|
||||
assert!(compiled.index.project_description.is_none());
|
||||
}
|
||||
let descriptions = project
|
||||
.elements
|
||||
.values()
|
||||
.map(|e| (e.id, &e.description))
|
||||
.chain(project.relations.values().map(|r| (r.id, &r.description)))
|
||||
.chain(project.diagrams.values().map(|d| (d.id, &d.description)))
|
||||
.chain(
|
||||
project
|
||||
.artifact_links
|
||||
.values()
|
||||
.map(|a| (a.id, &a.description)),
|
||||
)
|
||||
.chain(project.traces.values().map(|t| (t.id, &t.description)));
|
||||
for (id, description) in descriptions {
|
||||
if description.is_empty() {
|
||||
assert!(
|
||||
!compiled
|
||||
.index
|
||||
.fields
|
||||
.contains_key(&(id, "description".into()))
|
||||
);
|
||||
}
|
||||
}
|
||||
for element in project.elements.values().filter(|e| e.parent.is_none()) {
|
||||
assert!(
|
||||
!compiled
|
||||
.index
|
||||
.fields
|
||||
.contains_key(&(element.id, "parent".into()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_fidelity(label: &str, project: &Project) -> (Project, String) {
|
||||
let flat = language::format(project);
|
||||
let compact_model = read(&flat);
|
||||
assert_eq!(
|
||||
compact_model, *project,
|
||||
"{label}: every persistent field survives"
|
||||
);
|
||||
|
||||
let input = SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([("project.arcadia".into(), flat.clone())]),
|
||||
};
|
||||
let compiled = compile_bundle(&input, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(
|
||||
compiled.decoded.project, *project,
|
||||
"{label}: source-project compile"
|
||||
);
|
||||
assert_eq!(
|
||||
compiled.bundle, input,
|
||||
"{label}: compact identities/source are stable"
|
||||
);
|
||||
|
||||
assert_common_omissions(project, &compiled);
|
||||
assert_eq!(
|
||||
language::format(&compact_model),
|
||||
flat,
|
||||
"{label}: flat idempotence"
|
||||
);
|
||||
|
||||
let modular = format_bundle(project);
|
||||
assert!(
|
||||
modular.files.len() > 1,
|
||||
"{label}: exercise generated includes"
|
||||
);
|
||||
let modular_compiled = compile_bundle(&modular, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(
|
||||
modular_compiled.decoded.project, *project,
|
||||
"{label}: complete modular model"
|
||||
);
|
||||
assert_eq!(
|
||||
modular_compiled.bundle, modular,
|
||||
"{label}: modular source/IDs are stable"
|
||||
);
|
||||
assert_common_omissions(project, &modular_compiled);
|
||||
assert_eq!(
|
||||
format_bundle(&compact_model),
|
||||
modular,
|
||||
"{label}: flat-to-modular determinism"
|
||||
);
|
||||
assert_eq!(
|
||||
format_bundle(&modular_compiled.decoded.project),
|
||||
modular,
|
||||
"{label}: modular idempotence"
|
||||
);
|
||||
|
||||
// Pinned measured compact bytes from the published fidelity-1 stderr.log at
|
||||
// 2a64344, not a comparison against today's (now compact) canonical writer.
|
||||
// A small header-layout allowance avoids enforcing incidental blank lines.
|
||||
let measured_compact_bytes = match label {
|
||||
"coastal-source3" => 28_465,
|
||||
"keyed-source5-history" => 12_066,
|
||||
"state-actions-source5" => 6_673,
|
||||
"dense40-source6" => 19_342,
|
||||
"boundary-operations-source6" => 3_660,
|
||||
"state-negative-zero" => 6_672,
|
||||
"keyed-present-empty-default" => 12_108,
|
||||
"coastal-present-empty-artifact-strings" => 28_483,
|
||||
"boundary-selected-and-unresolved-operations" => 3_806,
|
||||
_ => panic!("missing independent compact measurement for {label}"),
|
||||
};
|
||||
assert!(
|
||||
flat.len() <= measured_compact_bytes + 16,
|
||||
"{label}: {} bytes exceeds compact evidence budget {}",
|
||||
flat.len(),
|
||||
measured_compact_bytes + 16
|
||||
);
|
||||
(compact_model, flat)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migrated_coastal_oa_sa_views_ports_artifacts_and_traces_roundtrip() {
|
||||
assert!(COASTAL.starts_with("arcadia 3;"));
|
||||
let project = read(COASTAL);
|
||||
assert!(project.elements.values().any(|e| e.layer() == Layer::Oa));
|
||||
assert!(project.elements.values().any(|e| e.layer() == Layer::Sa));
|
||||
assert!(
|
||||
project
|
||||
.elements
|
||||
.values()
|
||||
.any(|e| matches!(e.shared(), Some(SharedBody::Port { .. })))
|
||||
);
|
||||
assert!(project.elements.values().any(
|
||||
|e| matches!(e.shared(), Some(SharedBody::Scenario { steps, .. }) if !steps.is_empty())
|
||||
));
|
||||
assert!(project.diagrams.values().any(|d| d.layer == Layer::Oa
|
||||
&& d.projection == ProjectionKind::FunctionalDataflow
|
||||
&& !d.nodes.is_empty()));
|
||||
assert!(project.diagrams.values().any(|d| d.layer == Layer::Sa
|
||||
&& d.projection == ProjectionKind::Allocation
|
||||
&& !d.nodes.is_empty()));
|
||||
assert!(!project.artifact_links.is_empty());
|
||||
assert!(project.traces.values().any(|t| t.baseline.is_some()));
|
||||
assert_fidelity("coastal-source3", &project);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyed_scenarios_records_and_historical_binding_annotation_facets_roundtrip() {
|
||||
let project = read(KEYED);
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
assert!(project.elements.values().any(|e| e.layer() == layer
|
||||
&& matches!(
|
||||
e.shared(),
|
||||
Some(SharedBody::DataType {
|
||||
definition: DataTypeDefinition::Record { .. },
|
||||
..
|
||||
})
|
||||
)));
|
||||
assert!(project.elements.values().any(|e| e.layer() == layer && matches!(e.shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::ScenarioUse { bindings, .. }, .. }) if !bindings.is_empty())));
|
||||
}
|
||||
assert!(project.elements.values().any(|e| matches!(e.shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { values, .. }, .. }) if values.iter().any(|v| !v.path.is_empty() && !v.expression.is_empty()))));
|
||||
let facets: Vec<_> = project
|
||||
.traces
|
||||
.values()
|
||||
.filter_map(|t| t.baseline.as_ref()?.semantic.as_ref())
|
||||
.flat_map(|baseline| &baseline.facets)
|
||||
.collect();
|
||||
assert!(
|
||||
facets
|
||||
.iter()
|
||||
.any(|facet| matches!(facet, semantic::FacetReview::Binding { .. }))
|
||||
);
|
||||
assert!(
|
||||
facets
|
||||
.iter()
|
||||
.any(|facet| matches!(facet, semantic::FacetReview::Annotation { .. }))
|
||||
);
|
||||
assert!(
|
||||
KEYED.contains("uuid \""),
|
||||
"fixture contains historical identity atoms"
|
||||
);
|
||||
let (_, source) = assert_fidelity("keyed-source5-history", &project);
|
||||
assert!(
|
||||
source.contains("uuid \""),
|
||||
"historical atoms remain historical"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_machine_projection_typed_actions_and_bounded_integer_roundtrip() {
|
||||
let project = read(STATES);
|
||||
let machine = identity(&project, "operating");
|
||||
let view = &project.diagrams[&identity(&project, "state_view")];
|
||||
assert_eq!(view.projection, ProjectionKind::StateMachine { machine });
|
||||
assert!(!view.nodes.is_empty());
|
||||
for phase in [
|
||||
StateActionKind::Entry,
|
||||
StateActionKind::Do,
|
||||
StateActionKind::Exit,
|
||||
] {
|
||||
assert!(project.relations.values().any(|r| matches!(&r.body, RelationBody::StateAction { phase: actual, .. } if *actual == phase)));
|
||||
}
|
||||
assert!(project.relations.values().any(|r| matches!(&r.body, RelationBody::StateTransition { transition_kind: TransitionKind::Internal, triggers, effects, .. } if !triggers.is_empty() && !effects.is_empty())));
|
||||
assert!(
|
||||
matches!(project.elements[&identity(&project, "retry_budget")].shared(), Some(SharedBody::DataType { definition: DataTypeDefinition::Integer { minimum: Some(0), maximum: Some(4) }, unit, .. }) if unit == "attempts")
|
||||
);
|
||||
assert_fidelity("state-actions-source5", &project);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forty_exchange_diagram_with_categories_and_processes_roundtrip() {
|
||||
let project = read(DENSE);
|
||||
assert_eq!(
|
||||
project
|
||||
.relations
|
||||
.values()
|
||||
.filter(|r| r.kind() == RelationKind::Interaction)
|
||||
.count(),
|
||||
40
|
||||
);
|
||||
assert_eq!(
|
||||
project
|
||||
.elements
|
||||
.values()
|
||||
.filter(|e| e.kind() == Kind::ExchangeCategory)
|
||||
.count(),
|
||||
5
|
||||
);
|
||||
assert_eq!(
|
||||
project
|
||||
.elements
|
||||
.values()
|
||||
.filter(|e| e.kind() == Kind::Process)
|
||||
.count(),
|
||||
3
|
||||
);
|
||||
let view = &project.diagrams[&identity(&project, "forty_exchange_view")];
|
||||
assert_eq!(view.projection, ProjectionKind::FunctionalDataflow);
|
||||
assert_eq!(view.nodes.len(), 2);
|
||||
assert!(view.routes.is_empty());
|
||||
assert_eq!(
|
||||
view.view_options.relation_kinds,
|
||||
view_options::RelationKinds::All
|
||||
);
|
||||
assert_fidelity("dense40-source6", &project);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn component_ports_interface_operations_and_boundary_calls_roundtrip() {
|
||||
let project = read(OPERATIONS);
|
||||
assert_eq!(
|
||||
project
|
||||
.relations
|
||||
.values()
|
||||
.filter(|r| r.kind() == RelationKind::InterfaceItemAllocation)
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
project
|
||||
.elements
|
||||
.values()
|
||||
.filter(|e| matches!(
|
||||
e.shared(),
|
||||
Some(SharedBody::Port {
|
||||
contract: PortContract::Component {
|
||||
kind: ComponentPortKind::Standard,
|
||||
..
|
||||
},
|
||||
..
|
||||
})
|
||||
))
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
assert!(project.elements.values().any(|e| matches!(
|
||||
e.shared(),
|
||||
Some(SharedBody::ExchangeItem {
|
||||
mechanism: ExchangeMechanism::Operation,
|
||||
..
|
||||
})
|
||||
)));
|
||||
assert!(project.elements.values().any(|e| matches!(e.shared(), Some(SharedBody::Scenario { viewpoint: ScenarioViewpoint::BoundaryInterface, steps, .. }) if steps.len() == 2)));
|
||||
assert_eq!(
|
||||
project
|
||||
.elements
|
||||
.values()
|
||||
.filter(|e| matches!(
|
||||
e.shared(),
|
||||
Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
message_kind: MessageKind::Call,
|
||||
operation: None,
|
||||
..
|
||||
},
|
||||
..
|
||||
})
|
||||
))
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
assert_fidelity("boundary-operations-source6", &project);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_zero_node_coordinate_survives_canonical_compact_and_bundle_compile() {
|
||||
let mut project = read(STATES);
|
||||
let diagram = identity(&project, "state_view");
|
||||
let node = identity(&project, "view_machine");
|
||||
project
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&node)
|
||||
.unwrap()
|
||||
.position = Position { x: -0.0, y: 0.0 };
|
||||
let (decoded, flat) = assert_fidelity("state-negative-zero", &project);
|
||||
let bundle = SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([("project.arcadia".into(), flat)]),
|
||||
};
|
||||
let compiled = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
|
||||
let modular = compile_bundle(&format_bundle(&project), ProjectLimits::default()).unwrap();
|
||||
// Project/Position PartialEq alone equates the signs of zero. Check bits in
|
||||
// each independently decoded representation, including source-project compile.
|
||||
for actual in [
|
||||
&decoded,
|
||||
&compiled.decoded.project,
|
||||
&modular.decoded.project,
|
||||
] {
|
||||
let position = &actual.diagrams[&diagram].nodes[&node].position;
|
||||
assert_eq!(position.x.to_bits(), (-0.0_f32).to_bits());
|
||||
assert_eq!(position.y.to_bits(), 0.0_f32.to_bits());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn present_empty_field_default_and_unbounded_maximum_remain_distinct_from_omission() {
|
||||
let mut project = read(KEYED);
|
||||
let field_id = identity(&project, "oa_navigation_latitude");
|
||||
let Some(SharedBody::DataField {
|
||||
minimum,
|
||||
maximum,
|
||||
default,
|
||||
storage,
|
||||
..
|
||||
}) = project.elements.get_mut(&field_id).unwrap().shared_mut()
|
||||
else {
|
||||
panic!("fixture must contain the claimed DataField");
|
||||
};
|
||||
assert_eq!(*minimum, 0);
|
||||
assert_eq!(*maximum, Some(1));
|
||||
assert_eq!(*default, None);
|
||||
assert_eq!(*storage, FieldStorage::Value);
|
||||
*maximum = None;
|
||||
*default = Some(vec![]);
|
||||
let (decoded, _) = assert_fidelity("keyed-present-empty-default", &project);
|
||||
assert!(
|
||||
matches!(decoded.elements[&field_id].shared(), Some(SharedBody::DataField { maximum: None, default: Some(values), .. }) if values.is_empty())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn present_empty_artifact_revision_and_fragment_survive() {
|
||||
let mut project = read(COASTAL);
|
||||
let artifact_id = identity(&project, "console_enclosure_cad");
|
||||
let artifact = project.artifact_links.get_mut(&artifact_id).unwrap();
|
||||
artifact.revision = Some(String::new());
|
||||
artifact.fragment = Some(String::new());
|
||||
let (decoded, _) = assert_fidelity("coastal-present-empty-artifact-strings", &project);
|
||||
assert_eq!(
|
||||
decoded.artifact_links[&artifact_id].revision.as_deref(),
|
||||
Some("")
|
||||
);
|
||||
assert_eq!(
|
||||
decoded.artifact_links[&artifact_id].fragment.as_deref(),
|
||||
Some("")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_and_unresolved_operation_intent_survive_as_distinct_present_records() {
|
||||
let mut project = read(OPERATIONS);
|
||||
let allocation = identity(&project, "operation_probe_30");
|
||||
assert_eq!(
|
||||
project.relations[&allocation].kind(),
|
||||
RelationKind::InterfaceItemAllocation
|
||||
);
|
||||
let selected = identity(&project, "operation_probe_43");
|
||||
let unresolved = identity(&project, "operation_probe_44");
|
||||
for (subject, value) in [(selected, Some(allocation)), (unresolved, None)] {
|
||||
let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { operation, .. },
|
||||
..
|
||||
}) = project.elements.get_mut(&subject).unwrap().shared_mut()
|
||||
else {
|
||||
panic!("fixture must contain a Call Exchange");
|
||||
};
|
||||
assert_eq!(*operation, None);
|
||||
*operation = Some(OperationUse { allocation: value });
|
||||
}
|
||||
let (decoded, _) = assert_fidelity("boundary-selected-and-unresolved-operations", &project);
|
||||
assert!(
|
||||
matches!(decoded.elements[&selected].shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { operation: Some(OperationUse { allocation: Some(actual) }), .. }, .. }) if *actual == allocation)
|
||||
);
|
||||
assert!(matches!(
|
||||
decoded.elements[&unresolved].shared(),
|
||||
Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
operation: Some(OperationUse { allocation: None }),
|
||||
..
|
||||
},
|
||||
..
|
||||
})
|
||||
));
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
//! Focused public-output contracts. Field locations come from the real compiler;
|
||||
//! token comparisons ignore layout while retaining typed tags and required syntax.
|
||||
use oa_core::{language, source_project::*, *};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn project(declarations: &str) -> Project {
|
||||
language::parse(&format!(
|
||||
"arcadia 7; project p \"P\" id \"00000000-0000-4000-8000-000000000001\" {{ {declarations} }}"
|
||||
)).unwrap()
|
||||
}
|
||||
|
||||
fn exports(project: &Project) -> Vec<CompiledSourceProject> {
|
||||
let flat = language::format(project);
|
||||
assert_eq!(language::parse(&flat).unwrap(), *project);
|
||||
let flat_bundle = SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([("project.arcadia".into(), flat)]),
|
||||
};
|
||||
[flat_bundle, format_bundle(project)]
|
||||
.into_iter()
|
||||
.map(|bundle| {
|
||||
let compiled = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(compiled.decoded.project, *project);
|
||||
assert_eq!(
|
||||
compiled.bundle, bundle,
|
||||
"all identities are explicit and stable"
|
||||
);
|
||||
compiled
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn identity(project: &Project, alias: &str) -> Id {
|
||||
project
|
||||
.identities()
|
||||
.into_iter()
|
||||
.find(|(_, name)| *name == alias)
|
||||
.unwrap()
|
||||
.0
|
||||
}
|
||||
|
||||
fn field(compiled: &CompiledSourceProject, alias: &str, name: &str) -> Option<String> {
|
||||
let location = compiled
|
||||
.index
|
||||
.fields
|
||||
.get(&(identity(&compiled.decoded.project, alias), name.into()))?;
|
||||
let text = &compiled.bundle.files[&location.path][location.span.start..location.span.end];
|
||||
Some(
|
||||
language::tokenize(text)
|
||||
.into_iter()
|
||||
.filter(|token| {
|
||||
!matches!(
|
||||
token.class,
|
||||
language::TokenClass::Whitespace | language::TokenClass::Comment
|
||||
)
|
||||
})
|
||||
.map(|token| &text[token.span.start..token.span.end])
|
||||
.collect::<Vec<_>>()
|
||||
.join(" "),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_project_has_no_synthetic_description_or_module() {
|
||||
let empty = project("");
|
||||
for compiled in exports(&empty) {
|
||||
assert!(compiled.index.project_description.is_none());
|
||||
assert!(compiled.index.fields.is_empty());
|
||||
assert!(compiled.index.includes.is_empty());
|
||||
assert_eq!(compiled.bundle.files.len(), 1);
|
||||
assert_eq!(compiled.index.declarations.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_empty_descriptions_disappear_whitespace_and_text_survive_exactly() {
|
||||
let mut model =
|
||||
project("oa_activity work \"Work\" id \"00000000-0000-4000-8000-000000000002\" {}");
|
||||
let work = identity(&model, "work");
|
||||
for description in ["", " \t\n", "A quoted \"requirement\" — 雨"] {
|
||||
model.description = description.into();
|
||||
model.elements.get_mut(&work).unwrap().description = description.into();
|
||||
for compiled in exports(&model) {
|
||||
assert_eq!(
|
||||
compiled.index.project_description.is_some(),
|
||||
!description.is_empty()
|
||||
);
|
||||
assert_eq!(
|
||||
field(&compiled, "work", "description").is_some(),
|
||||
!description.is_empty()
|
||||
);
|
||||
assert!(field(&compiled, "work", "parent").is_none());
|
||||
// exports compares the complete model, including exact whitespace
|
||||
// and escapes in both project and element descriptions.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sa_view_keeps_its_layer_and_node_references_but_omits_presentation_defaults() {
|
||||
let model = project(
|
||||
r#"
|
||||
sa_function work "Work" id "00000000-0000-4000-8000-000000000002" {}
|
||||
diagram view "View" id "00000000-0000-4000-8000-000000000003" { layer Sa; }
|
||||
node placement id "00000000-0000-4000-8000-000000000004" {
|
||||
diagram @view; element @work;
|
||||
position { x 0.0; y 0.0; }; size { width 224.0; height 94.0; };
|
||||
collapsed false; port_anchors [];
|
||||
}
|
||||
"#,
|
||||
);
|
||||
for compiled in exports(&model) {
|
||||
assert_eq!(field(&compiled, "view", "layer").as_deref(), Some("Sa"));
|
||||
for name in ["description", "projection", "routes", "view_options"] {
|
||||
assert!(field(&compiled, "view", name).is_none(), "view.{name}");
|
||||
}
|
||||
assert_eq!(
|
||||
field(&compiled, "placement", "diagram").as_deref(),
|
||||
Some("@ view")
|
||||
);
|
||||
assert_eq!(
|
||||
field(&compiled, "placement", "element").as_deref(),
|
||||
Some("@ work")
|
||||
);
|
||||
for name in ["position", "size", "collapsed", "port_anchors"] {
|
||||
assert!(
|
||||
field(&compiled, "placement", name).is_none(),
|
||||
"placement.{name}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_type_defaults_disappear_without_losing_required_or_present_empty_values() {
|
||||
let model = project(
|
||||
r#"
|
||||
sa_data_type implicit "Implicit" id "00000000-0000-4000-8000-000000000002" {}
|
||||
sa_data_type integer "Integer" id "00000000-0000-4000-8000-000000000003" {
|
||||
definition { Integer { minimum none; maximum none; }; };
|
||||
}
|
||||
sa_data_type text "Text" id "00000000-0000-4000-8000-000000000004" {
|
||||
definition { String { minimum_length 4; maximum_length none; }; };
|
||||
}
|
||||
sa_data_type record "Record" id "00000000-0000-4000-8000-000000000005" {
|
||||
definition { Record { supertype none; }; };
|
||||
}
|
||||
sa_data_type flag "Flag" id "00000000-0000-4000-8000-000000000006" {
|
||||
definition Boolean; default { Boolean false; };
|
||||
}
|
||||
sa_data_field optional "Optional" id "00000000-0000-4000-8000-000000000007" {
|
||||
parent @record; minimum 0; maximum none; default [];
|
||||
}
|
||||
"#,
|
||||
);
|
||||
for compiled in exports(&model) {
|
||||
assert!(field(&compiled, "implicit", "definition").is_none());
|
||||
assert_eq!(
|
||||
field(&compiled, "integer", "definition").as_deref(),
|
||||
Some("{ Integer { } ; }")
|
||||
);
|
||||
assert_eq!(
|
||||
field(&compiled, "text", "definition").as_deref(),
|
||||
Some("{ String { minimum_length 4 ; } ; }")
|
||||
);
|
||||
assert_eq!(
|
||||
field(&compiled, "record", "definition").as_deref(),
|
||||
Some("{ Record { } ; }")
|
||||
);
|
||||
assert_eq!(
|
||||
field(&compiled, "flag", "definition").as_deref(),
|
||||
Some("Boolean")
|
||||
);
|
||||
assert_eq!(
|
||||
field(&compiled, "flag", "default").as_deref(),
|
||||
Some("{ Boolean false ; }")
|
||||
);
|
||||
assert_eq!(
|
||||
field(&compiled, "optional", "minimum").as_deref(),
|
||||
Some("0")
|
||||
);
|
||||
assert_eq!(
|
||||
field(&compiled, "optional", "maximum").as_deref(),
|
||||
Some("none")
|
||||
);
|
||||
assert_eq!(
|
||||
field(&compiled, "optional", "default").as_deref(),
|
||||
Some("[ ]")
|
||||
);
|
||||
assert!(field(&compiled, "optional", "type_ref").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn required_port_contract_and_empty_item_list_survive_while_optional_interface_disappears() {
|
||||
let model = project(
|
||||
r#"
|
||||
sa_function work "Work" id "00000000-0000-4000-8000-000000000002" {}
|
||||
sa_port sink "Sink" id "00000000-0000-4000-8000-000000000003" {
|
||||
parent @work; contract { FunctionInput { incoming_items []; }; };
|
||||
}
|
||||
sa_system host "Host" id "00000000-0000-4000-8000-000000000004" {}
|
||||
sa_port preserved "Preserved" id "00000000-0000-4000-8000-000000000005" {
|
||||
parent @host; contract Legacy; legacy { direction Out; interface none; };
|
||||
}
|
||||
"#,
|
||||
);
|
||||
for compiled in exports(&model) {
|
||||
assert_eq!(
|
||||
field(&compiled, "sink", "contract").as_deref(),
|
||||
Some("{ FunctionInput { incoming_items [ ] ; } ; }")
|
||||
);
|
||||
assert!(field(&compiled, "sink", "legacy").is_none());
|
||||
assert_eq!(
|
||||
field(&compiled, "preserved", "contract").as_deref(),
|
||||
Some("Legacy")
|
||||
);
|
||||
assert_eq!(
|
||||
field(&compiled, "preserved", "legacy").as_deref(),
|
||||
Some("{ direction Out ; }")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_scenario_default_and_selected_occurrence_constructor_remain_distinct() {
|
||||
let model = project(
|
||||
r#"
|
||||
sa_scenario mixed "Mixed" id "00000000-0000-4000-8000-000000000002" { viewpoint MixedLegacy; }
|
||||
sa_scenario strict "Strict" id "00000000-0000-4000-8000-000000000003" { viewpoint Functional; }
|
||||
oa_process flow "Flow" id "00000000-0000-4000-8000-000000000004" { steps [@exchange, @action]; }
|
||||
oa_occurrence exchange "Exchange" id "00000000-0000-4000-8000-000000000005" {
|
||||
parent @flow; body { Exchange {}; };
|
||||
}
|
||||
oa_occurrence action "Action" id "00000000-0000-4000-8000-000000000006" {
|
||||
parent @flow; body { Action { action none; lifeline none; }; };
|
||||
}
|
||||
"#,
|
||||
);
|
||||
for compiled in exports(&model) {
|
||||
assert!(field(&compiled, "mixed", "viewpoint").is_none());
|
||||
assert_eq!(
|
||||
field(&compiled, "strict", "viewpoint").as_deref(),
|
||||
Some("Functional")
|
||||
);
|
||||
assert!(field(&compiled, "exchange", "body").is_none());
|
||||
assert_eq!(
|
||||
field(&compiled, "action", "body").as_deref(),
|
||||
Some("{ Action { } ; }")
|
||||
);
|
||||
assert_eq!(
|
||||
field(&compiled, "action", "parent").as_deref(),
|
||||
Some("@ flow")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,612 @@
|
||||
//! Exact-source regressions for fields skipped by persistence serialization.
|
||||
//!
|
||||
//! The archived Windows baseline confirmed three exact-source failures after
|
||||
//! full model equality passed. Complete comparison now preserves these fields;
|
||||
//! the former ignored witnesses are normal regression tests.
|
||||
//! No-op and same-record header controls remain normal tests: they must preserve
|
||||
//! the same fixtures without entering the changed-payload record-erasure branch.
|
||||
|
||||
use oa_core::{source_project::*, *};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
const EXCHANGE_SOURCE: &str = r#"arcadia 7;
|
||||
project p "Preservation" id "00000000-0000-4000-8000-000000000001" {
|
||||
oa_activity first "First" id "00000000-0000-4000-8000-000000000002" {}
|
||||
oa_activity second "Second" id "00000000-0000-4000-8000-000000000003" {}
|
||||
interaction flow "Flow" id "00000000-0000-4000-8000-000000000004" {
|
||||
source @first; target @second;
|
||||
}
|
||||
oa_process owner "Owner" id "00000000-0000-4000-8000-000000000005" {
|
||||
steps [@subject];
|
||||
}
|
||||
oa_occurrence subject "Subject" id "00000000-0000-4000-8000-000000000006" {
|
||||
parent @owner;
|
||||
body { Exchange {
|
||||
exchange none;
|
||||
sender none; receiver none;
|
||||
message_kind Asynchronous; reply_to none; values [];
|
||||
operation none ; // Keep this authored default and its spelling.
|
||||
}; };
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
const NODE_SOURCE: &str = r#"arcadia 7;
|
||||
project p "Preservation" id "00000000-0000-4000-8000-000000000001" {
|
||||
oa_activity work "Work" id "00000000-0000-4000-8000-000000000002" {}
|
||||
diagram view "View" id "00000000-0000-4000-8000-000000000003" {}
|
||||
node placement id "00000000-0000-4000-8000-000000000004" {
|
||||
diagram @view; element @work;
|
||||
position { x 12.5; y -4.0; };
|
||||
port_anchors [] ; // Keep explicit automatic placement.
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
const TRACE_SOURCE: &str = r#"arcadia 7;
|
||||
project p "Preservation" id "00000000-0000-4000-8000-000000000001" {
|
||||
oa_activity work "Work" id "00000000-0000-4000-8000-000000000002" {}
|
||||
sa_function function "Function" id "00000000-0000-4000-8000-000000000003" {}
|
||||
trace mapping "Mapping" id "00000000-0000-4000-8000-000000000004" {
|
||||
oa @work; sa @function; kind Manual;
|
||||
baseline {
|
||||
oa { name "Work"; description "Old OA snapshot"; };
|
||||
sa { name "Function"; description "Old SA snapshot"; };
|
||||
semantic none ; // Keep the explicit prose-only baseline.
|
||||
};
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
const CONSTRUCTOR_SOURCE: &str = r#"arcadia 7;
|
||||
project p "Preservation" id "00000000-0000-4000-8000-000000000001" {
|
||||
oa_scenario owner "Owner" id "00000000-0000-4000-8000-000000000002" {
|
||||
steps [@subject];
|
||||
}
|
||||
oa_occurrence subject "Subject" id "00000000-0000-4000-8000-000000000003" {
|
||||
parent @owner;
|
||||
body { Action {
|
||||
action none;
|
||||
lifeline none ; // Keep this shared field's distinctive spelling.
|
||||
}; };
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
const RELATION_CONTEXT_SOURCE: &str = r#"arcadia 7;
|
||||
project p "Preservation" id "00000000-0000-4000-8000-000000000001" {
|
||||
oa_entity first "First" id "00000000-0000-4000-8000-000000000002" {}
|
||||
oa_entity second "Second" id "00000000-0000-4000-8000-000000000003" {}
|
||||
// These port declarations are untouched by the relation context change.
|
||||
oa_port output "Output" id "00000000-0000-4000-8000-000000000005" {
|
||||
parent @first; contract Legacy;
|
||||
legacy { direction Out; interface none; };
|
||||
}
|
||||
oa_port input "Input" id "00000000-0000-4000-8000-000000000006" {
|
||||
parent @second; contract Legacy;
|
||||
legacy { direction In; interface none; };
|
||||
}
|
||||
boundary_exchange link "Link" id "00000000-0000-4000-8000-000000000004" {
|
||||
source @output; target @input;
|
||||
oriented true ; // Retain this rationale when changing the default value.
|
||||
items [];
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
fn id(number: u8) -> Id {
|
||||
Id::parse_str(&format!("00000000-0000-4000-8000-{number:012}")).unwrap()
|
||||
}
|
||||
|
||||
fn source_bundle(source: &str) -> SourceBundle {
|
||||
SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([("project.arcadia".into(), source.into())]),
|
||||
}
|
||||
}
|
||||
|
||||
fn project() -> Project {
|
||||
Project {
|
||||
schema_version: 7,
|
||||
id: id(1),
|
||||
alias: "p".into(),
|
||||
name: "Preservation".into(),
|
||||
description: String::new(),
|
||||
elements: BTreeMap::new(),
|
||||
relations: BTreeMap::new(),
|
||||
diagrams: BTreeMap::new(),
|
||||
artifact_links: BTreeMap::new(),
|
||||
traces: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn element(number: u8, alias: &str, name: &str, parent: Option<Id>, body: ElementBody) -> Element {
|
||||
Element {
|
||||
id: id(number),
|
||||
alias: alias.into(),
|
||||
name: name.into(),
|
||||
description: String::new(),
|
||||
parent,
|
||||
body,
|
||||
}
|
||||
}
|
||||
|
||||
// Handwritten typed expectations are independent of canonical serialization and
|
||||
// source hydration. No model factories or generated/random identities are used.
|
||||
fn exchange_project() -> Project {
|
||||
let mut expected = project();
|
||||
expected.elements = [
|
||||
element(2, "first", "First", None, ElementBody::Oa(OaBody::Activity)),
|
||||
element(
|
||||
3,
|
||||
"second",
|
||||
"Second",
|
||||
None,
|
||||
ElementBody::Oa(OaBody::Activity),
|
||||
),
|
||||
element(
|
||||
5,
|
||||
"owner",
|
||||
"Owner",
|
||||
None,
|
||||
ElementBody::Oa(OaBody::Process { steps: vec![id(6)] }),
|
||||
),
|
||||
element(
|
||||
6,
|
||||
"subject",
|
||||
"Subject",
|
||||
Some(id(5)),
|
||||
ElementBody::Oa(OaBody::Shared(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
exchange: None,
|
||||
sender: None,
|
||||
receiver: None,
|
||||
message_kind: MessageKind::Asynchronous,
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
operation: None,
|
||||
},
|
||||
precondition: String::new(),
|
||||
postcondition: String::new(),
|
||||
})),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|element| (element.id, element))
|
||||
.collect();
|
||||
expected.relations.insert(
|
||||
id(4),
|
||||
Relation {
|
||||
id: id(4),
|
||||
alias: "flow".into(),
|
||||
name: "Flow".into(),
|
||||
description: String::new(),
|
||||
body: RelationBody::Interaction {
|
||||
source: id(2),
|
||||
target: id(3),
|
||||
items: vec![],
|
||||
},
|
||||
},
|
||||
);
|
||||
expected
|
||||
}
|
||||
|
||||
fn node_project() -> Project {
|
||||
let mut expected = project();
|
||||
expected.elements.insert(
|
||||
id(2),
|
||||
element(2, "work", "Work", None, ElementBody::Oa(OaBody::Activity)),
|
||||
);
|
||||
expected.diagrams.insert(
|
||||
id(3),
|
||||
Diagram {
|
||||
id: id(3),
|
||||
alias: "view".into(),
|
||||
name: "View".into(),
|
||||
description: String::new(),
|
||||
layer: Layer::Oa,
|
||||
projection: ProjectionKind::Overview,
|
||||
view_options: view_options::ViewOptions {
|
||||
hidden_edges: vec![],
|
||||
relation_kinds: view_options::RelationKinds::All,
|
||||
work_categories: view_options::CategorySelection::All,
|
||||
participant_categories: view_options::CategorySelection::All,
|
||||
coverage: view_options::Coverage::All,
|
||||
work_contexts: view_options::ContextSelection::All,
|
||||
exchange_labels: view_options::ExchangeLabels::Name,
|
||||
},
|
||||
nodes: BTreeMap::from([(
|
||||
id(4),
|
||||
NodePlacement {
|
||||
id: id(4),
|
||||
alias: "placement".into(),
|
||||
element: id(2),
|
||||
position: Position { x: 12.5, y: -4.0 },
|
||||
size: Size {
|
||||
width: 224.0,
|
||||
height: 94.0,
|
||||
},
|
||||
collapsed: false,
|
||||
port_anchors: vec![],
|
||||
},
|
||||
)]),
|
||||
routes: vec![],
|
||||
},
|
||||
);
|
||||
expected
|
||||
}
|
||||
|
||||
fn trace_project() -> Project {
|
||||
let mut expected = project();
|
||||
expected.elements = [
|
||||
element(2, "work", "Work", None, ElementBody::Oa(OaBody::Activity)),
|
||||
element(
|
||||
3,
|
||||
"function",
|
||||
"Function",
|
||||
None,
|
||||
ElementBody::Sa(SaBody::Function),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|element| (element.id, element))
|
||||
.collect();
|
||||
expected.traces.insert(
|
||||
id(4),
|
||||
Trace {
|
||||
id: id(4),
|
||||
alias: "mapping".into(),
|
||||
name: "Mapping".into(),
|
||||
description: String::new(),
|
||||
oa: id(2),
|
||||
sa: id(3),
|
||||
kind: TraceKind::Manual,
|
||||
baseline: Some(TraceBaseline {
|
||||
oa: TracedProperties {
|
||||
name: "Work".into(),
|
||||
description: "Old OA snapshot".into(),
|
||||
},
|
||||
sa: TracedProperties {
|
||||
name: "Function".into(),
|
||||
description: "Old SA snapshot".into(),
|
||||
},
|
||||
semantic: None,
|
||||
}),
|
||||
},
|
||||
);
|
||||
expected
|
||||
}
|
||||
|
||||
fn checked_fixture(source: &str, expected: &Project) -> CompiledSourceProject {
|
||||
let input = source_bundle(source);
|
||||
let compiled = compile_bundle(&input, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(
|
||||
compiled.decoded.project, *expected,
|
||||
"fixture model admission"
|
||||
);
|
||||
assert_eq!(
|
||||
compiled.bundle, input,
|
||||
"fixed IDs must avoid stabilization edits"
|
||||
);
|
||||
compiled
|
||||
}
|
||||
|
||||
fn replacement(source: &str, before: &str, after: &str) -> String {
|
||||
assert_eq!(
|
||||
source.matches(before).count(),
|
||||
1,
|
||||
"unique authored edit token"
|
||||
);
|
||||
source.replacen(before, after, 1)
|
||||
}
|
||||
|
||||
fn assert_preserved_update(
|
||||
original: &CompiledSourceProject,
|
||||
expected: &Project,
|
||||
expected_source: &str,
|
||||
) {
|
||||
// Independently admit the exact expected spelling before testing the updater.
|
||||
let expected_bundle = source_bundle(expected_source);
|
||||
let authored = compile_bundle(&expected_bundle, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(
|
||||
authored.decoded.project, *expected,
|
||||
"expected source/model agree"
|
||||
);
|
||||
assert_eq!(
|
||||
authored.bundle, expected_bundle,
|
||||
"expected source is already stable"
|
||||
);
|
||||
|
||||
let updated = update_bundle(original, expected, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(
|
||||
updated.decoded.project, *expected,
|
||||
"entire intended model survives"
|
||||
);
|
||||
assert_eq!(
|
||||
updated.bundle, expected_bundle,
|
||||
"only the requested token may change; preserve explicit defaults and all trivia"
|
||||
);
|
||||
}
|
||||
|
||||
// Context changes may introduce/remove redundant default syntax. Admit each
|
||||
// narrowly enumerated spelling against the same literal target model, and then
|
||||
// compare complete source bundles. No normalization may mask edits to unrelated
|
||||
// fields, declarations, whitespace, or comments.
|
||||
fn assert_context_update(
|
||||
original: &CompiledSourceProject,
|
||||
expected: &Project,
|
||||
allowed_sources: &[String],
|
||||
) {
|
||||
let allowed: Vec<_> = allowed_sources
|
||||
.iter()
|
||||
.map(|source| {
|
||||
let bundle = source_bundle(source);
|
||||
let authored = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(
|
||||
authored.decoded.project, *expected,
|
||||
"allowed source/model agree"
|
||||
);
|
||||
assert_eq!(authored.bundle, bundle, "allowed source is already stable");
|
||||
bundle
|
||||
})
|
||||
.collect();
|
||||
let updated = update_bundle(original, expected, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(
|
||||
updated.decoded.project, *expected,
|
||||
"entire context-change model survives"
|
||||
);
|
||||
assert!(
|
||||
allowed.contains(&updated.bundle),
|
||||
"preserve unchanged source exactly; only listed changed/default syntax alternatives are allowed\nactual: {:?}\nallowed: {:?}",
|
||||
updated.bundle,
|
||||
allowed
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_exchange_operation_none_survives_noop() {
|
||||
let expected = exchange_project();
|
||||
let original = checked_fixture(EXCHANGE_SOURCE, &expected);
|
||||
assert_preserved_update(&original, &expected, EXCHANGE_SOURCE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_exchange_operation_none_survives_header_rename() {
|
||||
let mut expected = exchange_project();
|
||||
let original = checked_fixture(EXCHANGE_SOURCE, &expected);
|
||||
expected.elements.get_mut(&id(6)).unwrap().name = "Renamed subject".into();
|
||||
let source = replacement(EXCHANGE_SOURCE, "\"Subject\"", "\"Renamed subject\"");
|
||||
assert_preserved_update(&original, &expected, &source);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_exchange_operation_none_survives_exchange_binding_edit() {
|
||||
let mut expected = exchange_project();
|
||||
let original = checked_fixture(EXCHANGE_SOURCE, &expected);
|
||||
let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { exchange, .. },
|
||||
..
|
||||
}) = expected.elements.get_mut(&id(6)).unwrap().body.shared_mut()
|
||||
else {
|
||||
panic!("literal fixture must be an Exchange occurrence");
|
||||
};
|
||||
*exchange = Some(id(4));
|
||||
let source = replacement(EXCHANGE_SOURCE, "exchange none;", "exchange @flow;");
|
||||
assert_preserved_update(&original, &expected, &source);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_node_empty_port_anchors_survive_noop() {
|
||||
let expected = node_project();
|
||||
let original = checked_fixture(NODE_SOURCE, &expected);
|
||||
assert_preserved_update(&original, &expected, NODE_SOURCE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_node_empty_port_anchors_survive_header_alias_rename() {
|
||||
let mut expected = node_project();
|
||||
let original = checked_fixture(NODE_SOURCE, &expected);
|
||||
// Nodes have no display-name header. Rename the same node's header alias.
|
||||
expected
|
||||
.diagrams
|
||||
.get_mut(&id(3))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.alias = "renamed_placement".into();
|
||||
let source = replacement(
|
||||
NODE_SOURCE,
|
||||
"node placement id",
|
||||
"node renamed_placement id",
|
||||
);
|
||||
assert_preserved_update(&original, &expected, &source);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_node_empty_port_anchors_survive_position_edit() {
|
||||
let mut expected = node_project();
|
||||
let original = checked_fixture(NODE_SOURCE, &expected);
|
||||
expected
|
||||
.diagrams
|
||||
.get_mut(&id(3))
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.position
|
||||
.x = 25.0;
|
||||
let source = replacement(NODE_SOURCE, "x 12.5;", "x 25.0;");
|
||||
assert_preserved_update(&original, &expected, &source);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_trace_semantic_none_survives_noop() {
|
||||
let expected = trace_project();
|
||||
let original = checked_fixture(TRACE_SOURCE, &expected);
|
||||
assert_preserved_update(&original, &expected, TRACE_SOURCE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_trace_semantic_none_survives_header_rename() {
|
||||
let mut expected = trace_project();
|
||||
let original = checked_fixture(TRACE_SOURCE, &expected);
|
||||
expected.traces.get_mut(&id(4)).unwrap().name = "Renamed mapping".into();
|
||||
let source = replacement(TRACE_SOURCE, "\"Mapping\"", "\"Renamed mapping\"");
|
||||
assert_preserved_update(&original, &expected, &source);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_trace_semantic_none_survives_snapshot_prose_edit() {
|
||||
let mut expected = trace_project();
|
||||
let original = checked_fixture(TRACE_SOURCE, &expected);
|
||||
expected
|
||||
.traces
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.oa
|
||||
.description = "Revised OA snapshot".into();
|
||||
let source = replacement(
|
||||
TRACE_SOURCE,
|
||||
"\"Old OA snapshot\"",
|
||||
"\"Revised OA snapshot\"",
|
||||
);
|
||||
assert_preserved_update(&original, &expected, &source);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constructor_change_removes_inapplicable_default_and_keeps_shared_default() {
|
||||
let mut expected = project();
|
||||
expected.elements = [
|
||||
element(
|
||||
2,
|
||||
"owner",
|
||||
"Owner",
|
||||
None,
|
||||
ElementBody::Oa(OaBody::Shared(SharedBody::Scenario {
|
||||
viewpoint: ScenarioViewpoint::MixedLegacy,
|
||||
context: None,
|
||||
precondition: String::new(),
|
||||
postcondition: String::new(),
|
||||
steps: vec![id(3)],
|
||||
})),
|
||||
),
|
||||
element(
|
||||
3,
|
||||
"subject",
|
||||
"Subject",
|
||||
Some(id(2)),
|
||||
ElementBody::Oa(OaBody::Shared(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Action {
|
||||
action: None,
|
||||
lifeline: None,
|
||||
},
|
||||
precondition: String::new(),
|
||||
postcondition: String::new(),
|
||||
})),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|element| (element.id, element))
|
||||
.collect();
|
||||
let original = checked_fixture(CONSTRUCTOR_SOURCE, &expected);
|
||||
expected.elements.get_mut(&id(3)).unwrap().body =
|
||||
ElementBody::Oa(OaBody::Shared(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::State {
|
||||
state: None,
|
||||
lifeline: None,
|
||||
},
|
||||
precondition: String::new(),
|
||||
postcondition: String::new(),
|
||||
}));
|
||||
// Remove only the inapplicable field's syntax: its separating space and all
|
||||
// surrounding trivia stay. The shared lifeline spelling/comment is exact.
|
||||
let source = replacement(CONSTRUCTOR_SOURCE, "body { Action {", "body { State {");
|
||||
let compact = replacement(&source, "action none;", " ");
|
||||
let expanded = replacement(
|
||||
&compact,
|
||||
" }; };",
|
||||
" state none;\n }; };",
|
||||
);
|
||||
// Omission of newly applicable state and explicit state none both mean the
|
||||
// literal State target; neither may retain the old action field.
|
||||
assert_context_update(&original, &expected, &[compact, expanded]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relation_kind_change_updates_explicit_value_when_context_default_changes() {
|
||||
let mut expected = project();
|
||||
expected.elements = [
|
||||
element(2, "first", "First", None, ElementBody::Oa(OaBody::Entity)),
|
||||
element(3, "second", "Second", None, ElementBody::Oa(OaBody::Entity)),
|
||||
element(
|
||||
5,
|
||||
"output",
|
||||
"Output",
|
||||
Some(id(2)),
|
||||
ElementBody::Oa(OaBody::Shared(SharedBody::Port {
|
||||
contract: PortContract::Legacy,
|
||||
legacy: Some(LegacyPortContract {
|
||||
direction: PortDirection::Out,
|
||||
interface: None,
|
||||
}),
|
||||
})),
|
||||
),
|
||||
element(
|
||||
6,
|
||||
"input",
|
||||
"Input",
|
||||
Some(id(3)),
|
||||
ElementBody::Oa(OaBody::Shared(SharedBody::Port {
|
||||
contract: PortContract::Legacy,
|
||||
legacy: Some(LegacyPortContract {
|
||||
direction: PortDirection::In,
|
||||
interface: None,
|
||||
}),
|
||||
})),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|element| (element.id, element))
|
||||
.collect();
|
||||
expected.relations.insert(
|
||||
id(4),
|
||||
Relation {
|
||||
id: id(4),
|
||||
alias: "link".into(),
|
||||
name: "Link".into(),
|
||||
description: String::new(),
|
||||
body: RelationBody::BoundaryExchange {
|
||||
source: id(5),
|
||||
target: id(6),
|
||||
oriented: true,
|
||||
items: vec![],
|
||||
},
|
||||
},
|
||||
);
|
||||
let original = checked_fixture(RELATION_CONTEXT_SOURCE, &expected);
|
||||
// BoundaryExchange connects participant ports; CommunicationMean connects
|
||||
// their OA entity owners. Endpoint changes are explicit parts of this target,
|
||||
// not inferred or normalized away. Port records remain exactly unchanged.
|
||||
expected.relations.get_mut(&id(4)).unwrap().body = RelationBody::CommunicationMean {
|
||||
source: id(2),
|
||||
target: id(3),
|
||||
oriented: false,
|
||||
items: vec![],
|
||||
};
|
||||
let source = replacement(
|
||||
RELATION_CONTEXT_SOURCE,
|
||||
"boundary_exchange link",
|
||||
"communication_mean link",
|
||||
);
|
||||
let source = replacement(&source, "source @output;", "source @first;");
|
||||
let source = replacement(&source, "target @input;", "target @second;");
|
||||
let explicit = replacement(&source, "oriented true ;", "oriented false ;");
|
||||
// Three spaces between name/value plus one before the semicolon survive
|
||||
// syntax removal. The following rationale comment and all other bytes stay.
|
||||
let omitted = replacement(&source, "oriented true ;", " ");
|
||||
assert_context_update(&original, &expected, &[explicit, omitted]);
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
//! Public update witnesses for compact insertion and complete comparison.
|
||||
use oa_core::{language, source_project::*, *};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn compile(body: &str) -> CompiledSourceProject {
|
||||
compile_bundle(
|
||||
&SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
(
|
||||
"project.arcadia".into(),
|
||||
"arcadia 7; project p \"P\" { include \"model.arcadia\"; }".into(),
|
||||
),
|
||||
("model.arcadia".into(), body.into()),
|
||||
]),
|
||||
},
|
||||
ProjectLimits::default(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
fn id(project: &Project, alias: &str) -> Id {
|
||||
project
|
||||
.identities()
|
||||
.into_iter()
|
||||
.find(|(_, a)| *a == alias)
|
||||
.unwrap()
|
||||
.0
|
||||
}
|
||||
fn update(before: &CompiledSourceProject, project: &Project) -> CompiledSourceProject {
|
||||
let result = update_bundle(before, project, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(result.decoded.project, *project);
|
||||
assert_eq!(
|
||||
result.bundle.files["project.arcadia"],
|
||||
before.bundle.files["project.arcadia"]
|
||||
);
|
||||
assert_eq!(
|
||||
compile_bundle(&result.bundle, ProjectLimits::default())
|
||||
.unwrap()
|
||||
.bundle,
|
||||
result.bundle
|
||||
);
|
||||
assert_eq!(
|
||||
update_bundle(&result, project, ProjectLimits::default())
|
||||
.unwrap()
|
||||
.bundle,
|
||||
result.bundle
|
||||
);
|
||||
result
|
||||
}
|
||||
fn words(text: &str) -> Vec<&str> {
|
||||
language::tokenize(text)
|
||||
.into_iter()
|
||||
.filter(|t| {
|
||||
!matches!(
|
||||
t.class,
|
||||
language::TokenClass::Whitespace | language::TokenClass::Comment
|
||||
)
|
||||
})
|
||||
.map(|t| &text[t.span.start..t.span.end])
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn omitted_exchange_body_changes_to_one_compact_action_constructor() {
|
||||
let before = compile(
|
||||
"oa_scenario s \"S\" { steps [@o]; }\noa_occurrence o \"O\" { parent @s; // keep this\n}\n",
|
||||
);
|
||||
let mut project = before.decoded.project.clone();
|
||||
let occurrence = id(&project, "o");
|
||||
let Some(SharedBody::Occurrence { body, .. }) =
|
||||
project.elements.get_mut(&occurrence).unwrap().shared_mut()
|
||||
else {
|
||||
panic!("occurrence")
|
||||
};
|
||||
assert!(matches!(body, OccurrenceBody::Exchange { .. }));
|
||||
*body = OccurrenceBody::Action {
|
||||
action: None,
|
||||
lifeline: None,
|
||||
};
|
||||
let result = update(&before, &project);
|
||||
let text = &result.bundle.files["model.arcadia"];
|
||||
let tokens = words(text);
|
||||
assert_eq!(tokens.iter().filter(|&&t| t == "body").count(), 1);
|
||||
assert!(tokens.contains(&"Action"));
|
||||
for absent in ["Exchange", "action", "lifeline", "operation", "description"] {
|
||||
assert!(!tokens.contains(&absent), "{absent}: {text}");
|
||||
}
|
||||
assert!(text.contains("parent @s; // keep this"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_signal_constructor_still_inserts_its_required_empty_items() {
|
||||
let before =
|
||||
compile("oa_event e \"E\" { body { Time { expression \"1 s\"; kind After; }; }; }\n");
|
||||
let mut project = before.decoded.project.clone();
|
||||
let event = id(&project, "e");
|
||||
let Some(SharedBody::Event { body }) = project.elements.get_mut(&event).unwrap().shared_mut()
|
||||
else {
|
||||
panic!("event")
|
||||
};
|
||||
*body = EventBody::Signal { items: vec![] };
|
||||
let result = update(&before, &project);
|
||||
let tokens = words(&result.bundle.files["model.arcadia"]);
|
||||
assert!(tokens.contains(&"Signal"));
|
||||
assert!(tokens.windows(4).any(|w| w == ["items", "[", "]", ";"]));
|
||||
assert!(!tokens.contains(&"expression"));
|
||||
assert!(!tokens.contains(&"kind"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scalar_to_default_string_record_retains_required_minimum_only() {
|
||||
let before = compile("oa_data_type t \"T\" { definition Boolean; // typed rationale\n}\n");
|
||||
let mut project = before.decoded.project.clone();
|
||||
let datatype = id(&project, "t");
|
||||
let Some(SharedBody::DataType { definition, .. }) =
|
||||
project.elements.get_mut(&datatype).unwrap().shared_mut()
|
||||
else {
|
||||
panic!("data type")
|
||||
};
|
||||
*definition = DataTypeDefinition::String {
|
||||
minimum_length: 0,
|
||||
maximum_length: None,
|
||||
};
|
||||
let result = update(&before, &project);
|
||||
let text = &result.bundle.files["model.arcadia"];
|
||||
let tokens = words(text);
|
||||
assert!(tokens.contains(&"String"));
|
||||
assert!(tokens.windows(3).any(|w| w == ["minimum_length", "0", ";"]));
|
||||
assert!(!tokens.contains(&"maximum_length"));
|
||||
assert!(text.contains("// typed rationale"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inserted_route_list_item_omits_only_optional_fields() {
|
||||
let before = compile(
|
||||
"oa_activity a \"A\" {}\noa_activity b \"B\" {}\ninteraction flow \"Flow\" { source @a; target @b; }\ndiagram d \"D\" { routes []; // routing rationale\n}\n",
|
||||
);
|
||||
let mut project = before.decoded.project.clone();
|
||||
let diagram = id(&project, "d");
|
||||
let edge = id(&project, "flow");
|
||||
// Deserialize only the explicit witness value, independently of omission policy.
|
||||
project.diagrams.get_mut(&diagram).unwrap().routes =
|
||||
serde_json::from_value(serde_json::json!([{"edge":edge,"waypoints":[],"label":null}]))
|
||||
.unwrap();
|
||||
let result = update(&before, &project);
|
||||
let text = &result.bundle.files["model.arcadia"];
|
||||
let tokens = words(text);
|
||||
assert!(tokens.windows(4).any(|w| w == ["edge", "@", "flow", ";"]));
|
||||
assert!(!tokens.contains(&"waypoints"));
|
||||
assert!(!tokens.contains(&"label"));
|
||||
assert!(text.contains("// routing rationale"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_sign_change_inserts_position_without_default_siblings() {
|
||||
let before = compile(
|
||||
"oa_activity a \"A\" {}\ndiagram d \"D\" {}\nnode n { diagram @d; element @a; // placement rationale\n}\n",
|
||||
);
|
||||
let mut project = before.decoded.project.clone();
|
||||
let diagram = id(&project, "d");
|
||||
let node = id(&project, "n");
|
||||
assert_eq!(
|
||||
project.diagrams[&diagram].nodes[&node].position.x.to_bits(),
|
||||
0.0_f32.to_bits()
|
||||
);
|
||||
project
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&node)
|
||||
.unwrap()
|
||||
.position
|
||||
.x = -0.0;
|
||||
let result = update(&before, &project);
|
||||
assert_eq!(
|
||||
result.decoded.project.diagrams[&diagram].nodes[&node]
|
||||
.position
|
||||
.x
|
||||
.to_bits(),
|
||||
(-0.0_f32).to_bits()
|
||||
);
|
||||
let text = &result.bundle.files["model.arcadia"];
|
||||
let tokens = words(text);
|
||||
assert!(tokens.windows(3).any(|w| w == ["x", "-0.0", ";"]));
|
||||
for absent in ["size", "collapsed", "port_anchors"] {
|
||||
assert!(!tokens.contains(&absent));
|
||||
}
|
||||
assert!(text.contains("// placement rationale"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kind_change_materializes_unchanged_value_when_its_old_omission_default_changes() {
|
||||
let before = compile(
|
||||
"oa_entity a \"A\" {}\noa_entity b \"B\" {}\noa_port out \"Out\" { parent @a; contract Legacy; legacy { direction Out; }; }\noa_port input \"In\" { parent @b; contract Legacy; legacy { direction In; }; }\nboundary_exchange flow \"Flow\" { source @out; target @input; // orientation inherited\n}\n",
|
||||
);
|
||||
let mut project = before.decoded.project.clone();
|
||||
let flow = id(&project, "flow");
|
||||
assert!(matches!(
|
||||
project.relations[&flow].body,
|
||||
RelationBody::BoundaryExchange { oriented: true, .. }
|
||||
));
|
||||
project.relations.get_mut(&flow).unwrap().body = RelationBody::CommunicationMean {
|
||||
source: id(&before.decoded.project, "a"),
|
||||
target: id(&before.decoded.project, "b"),
|
||||
oriented: true,
|
||||
items: vec![],
|
||||
};
|
||||
let result = update(&before, &project);
|
||||
let text = &result.bundle.files["model.arcadia"];
|
||||
assert!(
|
||||
words(text)
|
||||
.windows(3)
|
||||
.any(|w| w == ["oriented", "true", ";"])
|
||||
);
|
||||
assert!(text.contains("// orientation inherited"));
|
||||
let before_ports = &before.bundle.files["model.arcadia"];
|
||||
assert_eq!(
|
||||
text.split("communication_mean").next().unwrap(),
|
||||
before_ports.split("boundary_exchange").next().unwrap()
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,16 @@ use oa_core::{language, persistence, validation, *};
|
||||
fn read(source: &str) -> Project {
|
||||
language::parse(source).unwrap_or_else(|e| panic!("{e:?}\n{source}"))
|
||||
}
|
||||
fn replace_once(source: &str, before: &str, after: &str) -> String {
|
||||
assert_eq!(
|
||||
source.matches(before).count(),
|
||||
1,
|
||||
"unique mutation anchor {before:?}"
|
||||
);
|
||||
let changed = source.replacen(before, after, 1);
|
||||
assert_ne!(changed, source, "test mutation must change the source");
|
||||
changed
|
||||
}
|
||||
fn rejected(source: &str, fragment: &str) {
|
||||
let errors = language::parse(source).expect_err(source);
|
||||
assert!(
|
||||
@@ -753,8 +763,8 @@ fn typed_events_vertices_and_transition_kinds_round_trip_in_both_layers() {
|
||||
source.push('}');
|
||||
let model = read(&source);
|
||||
let canonical = language::format(&model);
|
||||
assert!(!canonical.contains("transition_kind External;"));
|
||||
for fragment in [
|
||||
"transition_kind External;",
|
||||
"transition_kind Local;",
|
||||
"transition_kind Internal;",
|
||||
"triggers [@deadline, @assess];",
|
||||
@@ -776,6 +786,7 @@ fn typed_events_vertices_and_transition_kinds_round_trip_in_both_layers() {
|
||||
.find(|r| r.alias == "recover")
|
||||
.unwrap();
|
||||
let RelationBody::StateTransition {
|
||||
transition_kind,
|
||||
trigger,
|
||||
guard,
|
||||
effect,
|
||||
@@ -784,32 +795,42 @@ fn typed_events_vertices_and_transition_kinds_round_trip_in_both_layers() {
|
||||
else {
|
||||
panic!("expected transition")
|
||||
};
|
||||
assert_eq!(*transition_kind, TransitionKind::External);
|
||||
assert_eq!(trigger, "Operator confirms\nНе запускать код");
|
||||
assert_eq!(guard, "available && status == \"Ready\"");
|
||||
assert_eq!(effect, "Record the assessment");
|
||||
// External is omitted in compact output. Insert an explicit invalid
|
||||
// discriminator into Recover using its unique, nondefault trigger list.
|
||||
let invalid_kind = replace_once(
|
||||
&canonical,
|
||||
"triggers [@deadline, @assess];",
|
||||
"transition_kind Unsupported;\n triggers [@deadline, @assess];",
|
||||
);
|
||||
rejected(&invalid_kind, "Unsupported");
|
||||
for invalid in [
|
||||
canonical.replace("transition_kind External;", "transition_kind Unsupported;"),
|
||||
canonical.replace(
|
||||
replace_once(
|
||||
&canonical,
|
||||
"triggers [@deadline, @assess];",
|
||||
"triggers [\"deadline\", @assess];",
|
||||
),
|
||||
canonical.replace(
|
||||
replace_once(
|
||||
&canonical,
|
||||
"triggers [@deadline, @assess];",
|
||||
"triggers [@deadline, @deadline];",
|
||||
),
|
||||
canonical.replace("effects [@assess];", "effects [@payload];"),
|
||||
canonical.replace("kind After;", "kind Relative;"),
|
||||
canonical.replace("items [@payload];", "items [@assess];"),
|
||||
replace_once(&canonical, "effects [@assess];", "effects [@payload];"),
|
||||
replace_once(&canonical, "kind After;", "kind Relative;"),
|
||||
replace_once(&canonical, "items [@payload];", "items [@assess];"),
|
||||
] {
|
||||
assert!(
|
||||
language::parse(&invalid).is_err(),
|
||||
"invalid typed state/event source accepted"
|
||||
);
|
||||
}
|
||||
let legacy = canonical
|
||||
.replace("transition_kind External;", "")
|
||||
.replace("triggers [@deadline, @assess];", "")
|
||||
.replace("effects [@assess];", "");
|
||||
// Canonical source already omits External. Removing the supplied lists
|
||||
// independently exercises their empty omission defaults and retained prose.
|
||||
let legacy = replace_once(&canonical, "triggers [@deadline, @assess];", "");
|
||||
let legacy = replace_once(&legacy, "effects [@assess];", "");
|
||||
let legacy = read(&legacy);
|
||||
let RelationBody::StateTransition {
|
||||
transition_kind,
|
||||
@@ -829,7 +850,7 @@ fn typed_events_vertices_and_transition_kinds_round_trip_in_both_layers() {
|
||||
(retained_trigger, retained_guard, retained_effect),
|
||||
(trigger, guard, effect)
|
||||
);
|
||||
let draft = canonical.replace("expression \"30 s\";", "expression \"\";");
|
||||
let draft = replace_once(&canonical, "expression \"30 s\";", "expression \"\";");
|
||||
assert!(language::parse(&draft).is_ok());
|
||||
let deadline = model
|
||||
.elements
|
||||
@@ -1038,7 +1059,7 @@ fn original_v2_text_and_json_preserve_identity_and_mixed_scenarios_after_extensi
|
||||
"fixture must cover actual old scenario content"
|
||||
);
|
||||
let canonical = language::format(&from_source);
|
||||
assert!(canonical.contains("viewpoint MixedLegacy;"));
|
||||
assert!(!canonical.contains("viewpoint MixedLegacy;"));
|
||||
for relation in from_source.relations.values() {
|
||||
if let RelationBody::StateTransition {
|
||||
transition_kind,
|
||||
|
||||
@@ -767,7 +767,15 @@ fn resetting_last_source_anchors_preserves_comments_and_can_add_a_new_manual_rec
|
||||
assert_eq!(cleared.decoded.project, prepared.project);
|
||||
let text = &cleared.bundle.files["project.arcadia"];
|
||||
assert!(text.contains("// first rationale") && text.contains("// second rationale"));
|
||||
assert!(!text.contains("port_anchors"));
|
||||
// Lossless editing may retain the authored empty field and its annotations.
|
||||
// Canonical export, rather than a model update, normalizes default syntax.
|
||||
assert!(!language::format(&cleared.decoded.project).contains("port_anchors"));
|
||||
assert_eq!(
|
||||
update_bundle(&cleared, &cleared.decoded.project, ProjectLimits::default())
|
||||
.unwrap()
|
||||
.bundle,
|
||||
cleared.bundle
|
||||
);
|
||||
let prepared = prepare_command(
|
||||
&cleared.decoded.project,
|
||||
&Command::SetPortAnchor {
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
//! Existing counterpart eligibility is a layer/category/kind decision, not
|
||||
//! validation or acceptance of an entire transition plan. The catalog fixtures
|
||||
//! below intentionally contain partial records (unowned states, etc.). The
|
||||
//! final regression separately starts from a structurally valid real project.
|
||||
use oa_core::{transitions::*, *};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
fn element(p: &mut Project, layer: Layer, kind: Kind) -> Id {
|
||||
let id = Id::new_v4();
|
||||
let body = if kind == Kind::Port {
|
||||
// Legacy ports can exist in preserved OA models. This does not offer
|
||||
// new typed SA implementation ports for creation in portless OA.
|
||||
ElementBody::shared_in(
|
||||
layer,
|
||||
SharedBody::Port {
|
||||
contract: PortContract::Legacy,
|
||||
legacy: None,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
ElementBody::new(kind, layer).unwrap()
|
||||
};
|
||||
p.elements.insert(
|
||||
id,
|
||||
Element {
|
||||
id,
|
||||
alias: format!("candidate_{}", id.simple()),
|
||||
name: "Identical candidate label".into(),
|
||||
description: String::new(),
|
||||
parent: None,
|
||||
body,
|
||||
},
|
||||
);
|
||||
id
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_element_candidates_obey_explicit_engineering_families_in_both_directions() {
|
||||
// Contract: language.rst §5 trace families and the explicit capability /
|
||||
// mission alternative. State constructors remain eligible for explicit
|
||||
// review across the state family; selecting a candidate does not accept a
|
||||
// constructor change or make invalid ownership legal.
|
||||
let mut allowed: BTreeSet<(Kind, Kind)> = [
|
||||
(Kind::Activity, Kind::Function),
|
||||
(Kind::Entity, Kind::System),
|
||||
(Kind::Entity, Kind::SystemActor),
|
||||
(Kind::Actor, Kind::System),
|
||||
(Kind::Actor, Kind::SystemActor),
|
||||
(Kind::Capability, Kind::SystemCapability),
|
||||
(Kind::OperationalMission, Kind::Mission),
|
||||
(Kind::Capability, Kind::Mission),
|
||||
(Kind::Process, Kind::FunctionalChain),
|
||||
]
|
||||
.into_iter()
|
||||
.collect();
|
||||
for kind in [
|
||||
Kind::ExchangeItem,
|
||||
Kind::ExchangeCategory,
|
||||
Kind::DataType,
|
||||
Kind::DataField,
|
||||
Kind::Interface,
|
||||
Kind::Port,
|
||||
Kind::Scenario,
|
||||
Kind::Lifeline,
|
||||
Kind::Occurrence,
|
||||
Kind::StateMachine,
|
||||
Kind::Region,
|
||||
Kind::Event,
|
||||
Kind::Constraint,
|
||||
] {
|
||||
allowed.insert((kind, kind));
|
||||
}
|
||||
let vertices = [
|
||||
Kind::State,
|
||||
Kind::Mode,
|
||||
Kind::Initial,
|
||||
Kind::Final,
|
||||
Kind::Choice,
|
||||
Kind::Junction,
|
||||
Kind::Fork,
|
||||
Kind::Join,
|
||||
Kind::ShallowHistory,
|
||||
Kind::DeepHistory,
|
||||
Kind::EntryPoint,
|
||||
Kind::ExitPoint,
|
||||
Kind::Terminate,
|
||||
];
|
||||
for oa in vertices {
|
||||
for sa in vertices {
|
||||
allowed.insert((oa, sa));
|
||||
}
|
||||
}
|
||||
let mut p = Project::empty();
|
||||
let oa: BTreeMap<_, _> = Kind::ALL
|
||||
.into_iter()
|
||||
.filter(|kind| kind.supports(Layer::Oa))
|
||||
.map(|kind| (kind, element(&mut p, Layer::Oa, kind)))
|
||||
.collect();
|
||||
let sa: BTreeMap<_, _> = Kind::ALL
|
||||
.into_iter()
|
||||
.filter(|kind| kind.supports(Layer::Sa))
|
||||
.map(|kind| (kind, element(&mut p, Layer::Sa, kind)))
|
||||
.collect();
|
||||
let original = p.clone();
|
||||
for (&oa_kind, &oa_id) in &oa {
|
||||
for (&sa_kind, &sa_id) in &sa {
|
||||
let expected = allowed.contains(&(oa_kind, sa_kind));
|
||||
assert_eq!(
|
||||
counterpart_compatible(&p, oa_id, sa_id, TransitionDirection::OaToSa),
|
||||
expected,
|
||||
"OA {oa_kind:?} → SA {sa_kind:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
counterpart_compatible(&p, sa_id, oa_id, TransitionDirection::SaToOa),
|
||||
expected,
|
||||
"SA {sa_kind:?} → OA {oa_kind:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
p, original,
|
||||
"Eligibility must not establish correspondence or edit records"
|
||||
);
|
||||
}
|
||||
|
||||
fn relation(p: &mut Project, kind: RelationKind, anchor: Id) -> Id {
|
||||
let id = Id::new_v4();
|
||||
// Only the semantic layer and relation category participate in candidate
|
||||
// eligibility. Endpoint compatibility belongs to full plan validation.
|
||||
p.relations.insert(
|
||||
id,
|
||||
Relation {
|
||||
id,
|
||||
alias: format!("relation_{}", id.simple()),
|
||||
name: "Identical relation label".into(),
|
||||
description: String::new(),
|
||||
body: RelationBody::new(kind, anchor, anchor),
|
||||
},
|
||||
);
|
||||
id
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_relation_candidates_distinguish_work_boundary_and_shared_relations() {
|
||||
// Work and boundary exchanges are different families. The boundary /
|
||||
// boundary pair permits preserved legacy OA boundary records; ordinary
|
||||
// portless OA communication means map to SA boundary exchanges.
|
||||
let mut allowed = vec![
|
||||
(RelationKind::Interaction, RelationKind::FunctionalExchange),
|
||||
(
|
||||
RelationKind::CommunicationMean,
|
||||
RelationKind::BoundaryExchange,
|
||||
),
|
||||
(
|
||||
RelationKind::BoundaryExchange,
|
||||
RelationKind::BoundaryExchange,
|
||||
),
|
||||
];
|
||||
let shared = [
|
||||
RelationKind::Allocation,
|
||||
RelationKind::InterfaceItemAllocation,
|
||||
RelationKind::DataAssociation,
|
||||
RelationKind::ExchangeAllocation,
|
||||
RelationKind::CapabilityInvolvement,
|
||||
RelationKind::CapabilityInclude,
|
||||
RelationKind::CapabilityExtend,
|
||||
RelationKind::CapabilityGeneralization,
|
||||
RelationKind::MissionInvolvement,
|
||||
RelationKind::MissionCapability,
|
||||
RelationKind::SequenceLink,
|
||||
RelationKind::ContextualLink,
|
||||
RelationKind::ChainInvolvement,
|
||||
RelationKind::InterfaceProvision,
|
||||
RelationKind::InterfaceRequirement,
|
||||
RelationKind::StateTransition,
|
||||
RelationKind::Availability,
|
||||
RelationKind::StateAction,
|
||||
];
|
||||
for kind in shared {
|
||||
allowed.push((kind, kind));
|
||||
}
|
||||
let mut p = Project::empty();
|
||||
let oa_anchor = element(&mut p, Layer::Oa, Kind::Activity);
|
||||
let sa_anchor = element(&mut p, Layer::Sa, Kind::Function);
|
||||
let oa: Vec<_> = shared
|
||||
.into_iter()
|
||||
.chain([
|
||||
RelationKind::Interaction,
|
||||
RelationKind::CommunicationMean,
|
||||
RelationKind::BoundaryExchange,
|
||||
RelationKind::RoleRealization,
|
||||
RelationKind::RoleAssignment,
|
||||
])
|
||||
.map(|kind| (kind, relation(&mut p, kind, oa_anchor)))
|
||||
.collect();
|
||||
let sa: Vec<_> = shared
|
||||
.into_iter()
|
||||
.chain([
|
||||
RelationKind::FunctionalExchange,
|
||||
RelationKind::BoundaryExchange,
|
||||
RelationKind::PortAllocation,
|
||||
])
|
||||
.map(|kind| (kind, relation(&mut p, kind, sa_anchor)))
|
||||
.collect();
|
||||
for &(oa_kind, oa_id) in &oa {
|
||||
for &(sa_kind, sa_id) in &sa {
|
||||
let expected = allowed.contains(&(oa_kind, sa_kind));
|
||||
assert_eq!(
|
||||
counterpart_compatible(&p, oa_id, sa_id, TransitionDirection::OaToSa),
|
||||
expected,
|
||||
"OA {oa_kind:?} → SA {sa_kind:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
counterpart_compatible(&p, sa_id, oa_id, TransitionDirection::SaToOa),
|
||||
expected,
|
||||
"SA {sa_kind:?} → OA {oa_kind:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn candidate_eligibility_rejects_wrong_layers_categories_and_missing_identities() {
|
||||
let mut p = Project::empty();
|
||||
let oa = element(&mut p, Layer::Oa, Kind::Activity);
|
||||
let sa = element(&mut p, Layer::Sa, Kind::Function);
|
||||
let oa_exchange = relation(&mut p, RelationKind::Interaction, oa);
|
||||
let sa_exchange = relation(&mut p, RelationKind::FunctionalExchange, sa);
|
||||
let diagram = p.ensure_diagram(Layer::Sa);
|
||||
let node = p.place(diagram, sa, Position { x: 1., y: 2. });
|
||||
for (direction, source, target, source_relation, target_relation) in [
|
||||
(
|
||||
TransitionDirection::OaToSa,
|
||||
oa,
|
||||
sa,
|
||||
oa_exchange,
|
||||
sa_exchange,
|
||||
),
|
||||
(
|
||||
TransitionDirection::SaToOa,
|
||||
sa,
|
||||
oa,
|
||||
sa_exchange,
|
||||
oa_exchange,
|
||||
),
|
||||
] {
|
||||
assert!(counterpart_compatible(&p, source, target, direction));
|
||||
assert!(counterpart_compatible(
|
||||
&p,
|
||||
source_relation,
|
||||
target_relation,
|
||||
direction
|
||||
));
|
||||
for (a, b) in [
|
||||
(target, source),
|
||||
(source, source),
|
||||
(target, target),
|
||||
(target_relation, source_relation),
|
||||
(source_relation, source_relation),
|
||||
(source, target_relation),
|
||||
(source_relation, target),
|
||||
] {
|
||||
assert!(!counterpart_compatible(&p, a, b, direction));
|
||||
}
|
||||
for absent_or_nonsemantic in [Id::nil(), Id::new_v4(), p.id, diagram, node] {
|
||||
assert!(!counterpart_compatible(
|
||||
&p,
|
||||
source,
|
||||
absent_or_nonsemantic,
|
||||
direction
|
||||
));
|
||||
assert!(!counterpart_compatible(
|
||||
&p,
|
||||
absent_or_nonsemantic,
|
||||
target,
|
||||
direction
|
||||
));
|
||||
}
|
||||
}
|
||||
p.elements.remove(&sa);
|
||||
assert!(!counterpart_compatible(
|
||||
&p,
|
||||
oa,
|
||||
sa,
|
||||
TransitionDirection::OaToSa
|
||||
));
|
||||
assert!(!counterpart_compatible(
|
||||
&p,
|
||||
sa,
|
||||
oa,
|
||||
TransitionDirection::SaToOa
|
||||
));
|
||||
// The retained relation header cannot borrow a layer from a deleted owner.
|
||||
assert!(!counterpart_compatible(
|
||||
&p,
|
||||
oa_exchange,
|
||||
sa_exchange,
|
||||
TransitionDirection::OaToSa
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn activity_picker_rejects_real_sa_port_and_agrees_with_explicit_plan_refusal() {
|
||||
let p = Project::demo();
|
||||
let original = p.clone();
|
||||
assert!(validation::validate(&p).is_empty());
|
||||
let source = p
|
||||
.elements
|
||||
.values()
|
||||
.find(|e| e.kind() == Kind::Activity)
|
||||
.unwrap()
|
||||
.id;
|
||||
let port = p
|
||||
.elements
|
||||
.values()
|
||||
.find(|e| e.layer() == Layer::Sa && e.kind() == Kind::Port)
|
||||
.unwrap()
|
||||
.id;
|
||||
let function = p
|
||||
.elements
|
||||
.values()
|
||||
.find(|e| e.kind() == Kind::Function)
|
||||
.unwrap()
|
||||
.id;
|
||||
assert!(counterpart_compatible(
|
||||
&p,
|
||||
source,
|
||||
function,
|
||||
TransitionDirection::OaToSa
|
||||
));
|
||||
assert!(!counterpart_compatible(
|
||||
&p,
|
||||
source,
|
||||
port,
|
||||
TransitionDirection::OaToSa
|
||||
));
|
||||
let mut request = TransitionRequest::new(TransitionDirection::OaToSa, vec![source]);
|
||||
request.mappings.push(TransitionMapping {
|
||||
source,
|
||||
target: Some(port),
|
||||
target_kind: None,
|
||||
});
|
||||
let plan = plan_transition(&p, &request).unwrap();
|
||||
assert!(
|
||||
plan.conflicts
|
||||
.iter()
|
||||
.any(|conflict| conflict.source == Some(source)
|
||||
&& conflict.target == Some(port)
|
||||
&& conflict.field.is_none())
|
||||
);
|
||||
assert!(apply_transition(&p, &plan).is_err());
|
||||
assert_eq!(p, original);
|
||||
}
|
||||
@@ -0,0 +1,628 @@
|
||||
//! Endpoint lifecycle acceptance for the ordinary, prose-reviewed transition API.
|
||||
//! OA is portless; the SA half contains typed ports. Deletion expectations below
|
||||
//! are fixture-owned identity lists, never results of the deletion implementation.
|
||||
use oa_core::{
|
||||
language, persistence,
|
||||
source_project::{ProjectLimits, SourceBundle, compile_bundle},
|
||||
transitions::*,
|
||||
validation::{self, Severity},
|
||||
*,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
struct Side {
|
||||
root: Id,
|
||||
elements: Vec<Id>,
|
||||
exchange: Id,
|
||||
diagram: Id,
|
||||
nodes: Vec<Id>,
|
||||
artifacts: Vec<Id>,
|
||||
}
|
||||
|
||||
struct Fixture {
|
||||
project: Project,
|
||||
source: Side,
|
||||
target: Side,
|
||||
trace: Id,
|
||||
trace_artifact: Id,
|
||||
}
|
||||
|
||||
fn artifact(p: &mut Project, owner: Id, label: &str) -> Id {
|
||||
let id = Id::new_v4();
|
||||
p.artifact_links.insert(
|
||||
id,
|
||||
ArtifactLink {
|
||||
id,
|
||||
alias: p.fresh_alias(label, "evidence", id),
|
||||
name: label.into(),
|
||||
description: "Independently authored rationale".into(),
|
||||
owner,
|
||||
relation: ArtifactRelation::References,
|
||||
uri: "git:engineering/design.md".into(),
|
||||
revision: Some("review-17".into()),
|
||||
fragment: Some("retained-decision".into()),
|
||||
},
|
||||
);
|
||||
id
|
||||
}
|
||||
|
||||
fn side(p: &mut Project, layer: Layer) -> Side {
|
||||
let (kind, prefix, relation) = match layer {
|
||||
Layer::Oa => (Kind::Activity, "Operational", RelationKind::Interaction),
|
||||
Layer::Sa => (
|
||||
Kind::Function,
|
||||
"Technical",
|
||||
RelationKind::FunctionalExchange,
|
||||
),
|
||||
};
|
||||
let root = p
|
||||
.add_element(kind, layer, &format!("{prefix} design"), None)
|
||||
.unwrap();
|
||||
p.elements.get_mut(&root).unwrap().description =
|
||||
format!("{prefix} independently reviewed intent");
|
||||
let send = p
|
||||
.add_element(kind, layer, &format!("{prefix} send"), Some(root))
|
||||
.unwrap();
|
||||
let receive = p
|
||||
.add_element(kind, layer, &format!("{prefix} receive"), Some(root))
|
||||
.unwrap();
|
||||
p.elements.get_mut(&send).unwrap().description =
|
||||
"Preserve the detailed sending responsibility".into();
|
||||
p.elements.get_mut(&receive).unwrap().description =
|
||||
"Preserve the detailed receiving responsibility".into();
|
||||
// A shared, separately owned definition survives deletion of its users.
|
||||
let item = p
|
||||
.add_element(
|
||||
Kind::ExchangeItem,
|
||||
layer,
|
||||
&format!("{prefix} information"),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&item).unwrap().shared_mut().unwrap() = SharedBody::ExchangeItem {
|
||||
kind: ExchangeItemKind::Information,
|
||||
mechanism: ExchangeMechanism::Flow,
|
||||
supertype: None,
|
||||
};
|
||||
p.add_element(
|
||||
Kind::DataField,
|
||||
layer,
|
||||
&format!("{prefix} payload"),
|
||||
Some(item),
|
||||
)
|
||||
.unwrap();
|
||||
let exchange = p.relate(relation, &format!("{prefix} delivery"), send, receive);
|
||||
let mut elements = vec![root, send, receive];
|
||||
match &mut p.relations.get_mut(&exchange).unwrap().body {
|
||||
RelationBody::Interaction { items, .. } => *items = vec![item],
|
||||
RelationBody::FunctionalExchange {
|
||||
items,
|
||||
source_port,
|
||||
target_port,
|
||||
..
|
||||
} => {
|
||||
*items = vec![item];
|
||||
let (output, input) = (*source_port, *target_port);
|
||||
*p.elements.get_mut(&output).unwrap().shared_mut().unwrap() = SharedBody::Port {
|
||||
contract: PortContract::FunctionOutput {
|
||||
outgoing_items: vec![item],
|
||||
},
|
||||
legacy: None,
|
||||
};
|
||||
*p.elements.get_mut(&input).unwrap().shared_mut().unwrap() = SharedBody::Port {
|
||||
contract: PortContract::FunctionInput {
|
||||
incoming_items: vec![item],
|
||||
},
|
||||
legacy: None,
|
||||
};
|
||||
elements.extend([output, input]);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
p.relations.get_mut(&exchange).unwrap().description =
|
||||
"Retain direction, item contract and relation identity".into();
|
||||
let diagram = p.ensure_diagram(layer);
|
||||
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Decomposition;
|
||||
let nodes = [root, send, receive]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, element)| {
|
||||
p.place(
|
||||
diagram,
|
||||
element,
|
||||
Position {
|
||||
x: 43.25 + index as f32 * 271.,
|
||||
y: 97.5 + index as f32 * 83.,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let root_node = p
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&nodes[0])
|
||||
.unwrap();
|
||||
root_node.size = Size {
|
||||
width: 611.,
|
||||
height: 319.,
|
||||
};
|
||||
root_node.collapsed = true;
|
||||
p.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.routes
|
||||
.push(EdgeRoute {
|
||||
edge: exchange,
|
||||
waypoints: vec![Position {
|
||||
x: 337.5,
|
||||
y: 423.25,
|
||||
}],
|
||||
label: Some(EdgeLabel {
|
||||
fraction: 0.7,
|
||||
offset: Position { x: 11., y: -17. },
|
||||
}),
|
||||
});
|
||||
let artifacts = [root, send, exchange, nodes[0]]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, owner)| artifact(p, owner, &format!("{prefix} evidence {index}")))
|
||||
.collect();
|
||||
artifact(p, item, &format!("{prefix} shared item evidence"));
|
||||
Side {
|
||||
root,
|
||||
elements,
|
||||
exchange,
|
||||
diagram,
|
||||
nodes,
|
||||
artifacts,
|
||||
}
|
||||
}
|
||||
|
||||
fn accepted(p: &Project) {
|
||||
let issues = validation::validate(p);
|
||||
// This fixture deliberately isolates decomposition and correspondence from
|
||||
// performer allocation and process/chain/scenario inclusion. Each surviving
|
||||
// leaf (including a newly transitioned root) must have exactly those two
|
||||
// incomplete-work warnings; each surviving local exchange lacks a behavior
|
||||
// context. Check exact record IDs and fields, including duplicate warnings.
|
||||
// The system of interest and item contracts are complete fixture facts.
|
||||
assert!(
|
||||
issues
|
||||
.iter()
|
||||
.all(|issue| issue.severity == Severity::Warning),
|
||||
"{issues:?}"
|
||||
);
|
||||
let mut expected = Vec::new();
|
||||
for element in p.elements.values().filter(|element| {
|
||||
matches!(element.kind(), Kind::Activity | Kind::Function)
|
||||
&& !p
|
||||
.elements
|
||||
.values()
|
||||
.any(|child| child.parent == Some(element.id) && child.kind() == element.kind())
|
||||
}) {
|
||||
expected.push(("unallocated_leaf", Some(element.id), Some("allocation")));
|
||||
expected.push(("work_without_behavior_context", Some(element.id), None));
|
||||
}
|
||||
for relation in p.relations.values().filter(|relation| {
|
||||
matches!(
|
||||
relation.kind(),
|
||||
RelationKind::Interaction | RelationKind::FunctionalExchange
|
||||
)
|
||||
}) {
|
||||
expected.push(("exchange_without_behavior_context", Some(relation.id), None));
|
||||
}
|
||||
let mut actual = issues
|
||||
.iter()
|
||||
.map(|issue| (issue.code.as_str(), issue.element, issue.field.as_deref()))
|
||||
.collect::<Vec<_>>();
|
||||
expected.sort();
|
||||
actual.sort();
|
||||
assert_eq!(
|
||||
actual, expected,
|
||||
"Unexpected fixture diagnostics: {issues:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn round_trip(p: &Project) -> String {
|
||||
accepted(p);
|
||||
let json = serde_json::to_vec_pretty(p).unwrap();
|
||||
assert_eq!(persistence::decode(&json).unwrap(), *p);
|
||||
let source = language::format(p);
|
||||
let compiled = compile_bundle(
|
||||
&SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([("project.arcadia".into(), source.clone())]),
|
||||
},
|
||||
ProjectLimits::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(compiled.decoded.project, *p);
|
||||
assert_eq!(language::format(&compiled.decoded.project), source);
|
||||
source
|
||||
}
|
||||
|
||||
fn explicit(direction: TransitionDirection, source: Id, target: Id) -> TransitionRequest {
|
||||
let mut request = TransitionRequest::new(direction, vec![source]);
|
||||
request.mappings.push(TransitionMapping {
|
||||
source,
|
||||
target: Some(target),
|
||||
target_kind: None,
|
||||
});
|
||||
request
|
||||
}
|
||||
|
||||
fn fixture(direction: TransitionDirection) -> Fixture {
|
||||
let mut project = Project::empty();
|
||||
project.name = "Endpoint lifecycle conservation".into();
|
||||
project
|
||||
.add_element(Kind::System, Layer::Sa, "System of interest", None)
|
||||
.unwrap();
|
||||
let oa = side(&mut project, Layer::Oa);
|
||||
let sa = side(&mut project, Layer::Sa);
|
||||
let (source, target) = match direction {
|
||||
TransitionDirection::OaToSa => (oa, sa),
|
||||
TransitionDirection::SaToOa => (sa, oa),
|
||||
};
|
||||
accepted(&project);
|
||||
let before = project.clone();
|
||||
let mut request = explicit(direction, source.root, target.root);
|
||||
for field in [TransitionField::Name, TransitionField::Description] {
|
||||
request.resolutions.push(FieldResolution {
|
||||
source: source.root,
|
||||
field,
|
||||
decision: FieldDecision::KeepTarget,
|
||||
});
|
||||
}
|
||||
let plan = plan_transition(&project, &request).unwrap();
|
||||
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
||||
project = apply_transition(&project, &plan).unwrap();
|
||||
let trace = assert_only_new_trace(&before, &project, direction, source.root, target.root);
|
||||
let mut without_trace = project.clone();
|
||||
without_trace.traces.remove(&trace);
|
||||
assert_eq!(
|
||||
without_trace, before,
|
||||
"Initial review must only add correspondence"
|
||||
);
|
||||
let trace_artifact = artifact(&mut project, trace, "Correspondence rationale");
|
||||
round_trip(&project);
|
||||
Fixture {
|
||||
project,
|
||||
source,
|
||||
target,
|
||||
trace,
|
||||
trace_artifact,
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_only_new_trace(
|
||||
before: &Project,
|
||||
after: &Project,
|
||||
direction: TransitionDirection,
|
||||
source: Id,
|
||||
target: Id,
|
||||
) -> Id {
|
||||
let added = after
|
||||
.traces
|
||||
.values()
|
||||
.filter(|trace| !before.traces.contains_key(&trace.id))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(added.len(), 1);
|
||||
assert_eq!(after.traces.len(), before.traces.len() + 1);
|
||||
let trace = added[0];
|
||||
let (oa, sa) = match direction {
|
||||
TransitionDirection::OaToSa => (source, target),
|
||||
TransitionDirection::SaToOa => (target, source),
|
||||
};
|
||||
assert_eq!(
|
||||
(trace.oa, trace.sa, trace.kind),
|
||||
(oa, sa, TraceKind::Refinement)
|
||||
);
|
||||
assert_eq!(
|
||||
trace.baseline,
|
||||
Some(TraceBaseline {
|
||||
oa: after.properties(oa).unwrap(),
|
||||
sa: after.properties(sa).unwrap(),
|
||||
semantic: None,
|
||||
})
|
||||
);
|
||||
assert_eq!(trace_drift(after, trace), Drift::Unchanged);
|
||||
trace.id
|
||||
}
|
||||
|
||||
fn expected_deletion(fixture: &Fixture, side: &Side) -> Project {
|
||||
let mut expected = fixture.project.clone();
|
||||
for id in &side.elements {
|
||||
assert!(expected.elements.remove(id).is_some());
|
||||
}
|
||||
assert!(expected.relations.remove(&side.exchange).is_some());
|
||||
assert!(expected.traces.remove(&fixture.trace).is_some());
|
||||
for id in side.artifacts.iter().chain([&fixture.trace_artifact]) {
|
||||
assert!(expected.artifact_links.remove(id).is_some());
|
||||
}
|
||||
let diagram = expected.diagrams.get_mut(&side.diagram).unwrap();
|
||||
for id in &side.nodes {
|
||||
assert!(diagram.nodes.remove(id).is_some());
|
||||
}
|
||||
// This fixture authored exactly one route, for its removed local exchange.
|
||||
assert_eq!(diagram.routes.len(), 1);
|
||||
assert_eq!(diagram.routes[0].edge, side.exchange);
|
||||
diagram.routes.clear();
|
||||
expected
|
||||
}
|
||||
|
||||
fn unchanged_retry(p: &Project, direction: TransitionDirection, source: Id, target: Id) {
|
||||
let request = TransitionRequest::new(direction, vec![source]);
|
||||
let plan = plan_transition(p, &request).unwrap();
|
||||
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
||||
assert_eq!(plan.changes.len(), 1);
|
||||
assert_eq!(
|
||||
(
|
||||
plan.changes[0].source,
|
||||
plan.changes[0].target,
|
||||
plan.changes[0].action
|
||||
),
|
||||
(source, target, TransitionAction::Reuse)
|
||||
);
|
||||
assert!(plan.changes[0].fields.is_empty());
|
||||
assert!(plan.commands.is_empty(), "{:?}", plan.commands);
|
||||
let mut session = Session::new(p.clone());
|
||||
let revision = session.revision();
|
||||
session
|
||||
.execute(Command::Replace(apply_transition(p, &plan).unwrap()))
|
||||
.unwrap();
|
||||
assert_eq!(session.project(), p);
|
||||
assert_eq!(session.revision(), revision);
|
||||
assert!(!session.can_undo());
|
||||
}
|
||||
|
||||
fn source_deletion(direction: TransitionDirection) {
|
||||
let fixture = fixture(direction);
|
||||
let original_source = round_trip(&fixture.project);
|
||||
let expected = expected_deletion(&fixture, &fixture.source);
|
||||
let mut session = Session::new(fixture.project.clone());
|
||||
session
|
||||
.execute(Command::Delete(fixture.source.root))
|
||||
.unwrap();
|
||||
// Whole-project equality covers every retained target child, typed SA port,
|
||||
// exchange, placement, route, artifact, shared item and unchanged identity.
|
||||
assert_eq!(session.project(), &expected);
|
||||
let deleted_source = round_trip(session.project());
|
||||
session.undo();
|
||||
assert_eq!(session.project(), &fixture.project);
|
||||
assert_eq!(round_trip(session.project()), original_source);
|
||||
assert!(!session.can_undo(), "Deletion is one history transaction");
|
||||
session.redo();
|
||||
assert_eq!(session.project(), &expected);
|
||||
assert_eq!(round_trip(session.project()), deleted_source);
|
||||
}
|
||||
|
||||
fn implicit_creation(
|
||||
p: &Project,
|
||||
direction: TransitionDirection,
|
||||
source: Id,
|
||||
forbidden_target: Id,
|
||||
) -> (Project, Id) {
|
||||
let request = TransitionRequest::new(direction, vec![source]);
|
||||
let plan = plan_transition(p, &request).unwrap();
|
||||
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
||||
assert_eq!(plan.changes.len(), 1);
|
||||
let change = &plan.changes[0];
|
||||
assert_eq!(change.source, source);
|
||||
assert_eq!(change.action, TransitionAction::Create);
|
||||
let target = change.target;
|
||||
assert_ne!(target, forbidden_target);
|
||||
assert!(!p.contains(target));
|
||||
let after = apply_transition(p, &plan).unwrap();
|
||||
let trace = assert_only_new_trace(p, &after, direction, source, target);
|
||||
let created = &after.elements[&target];
|
||||
assert_eq!(created.id, target, "Apply preserves the preview identity");
|
||||
assert_eq!(created.name, p.elements[&source].name);
|
||||
assert_eq!(created.description, p.elements[&source].description);
|
||||
assert_eq!(created.parent, None);
|
||||
assert_eq!(
|
||||
created.body,
|
||||
match direction {
|
||||
TransitionDirection::OaToSa => ElementBody::Sa(SaBody::Function),
|
||||
TransitionDirection::SaToOa => ElementBody::Oa(OaBody::Activity),
|
||||
}
|
||||
);
|
||||
assert!(!created.alias.is_empty());
|
||||
assert!(
|
||||
!p.identities()
|
||||
.iter()
|
||||
.any(|(_, alias)| *alias == created.alias)
|
||||
);
|
||||
// Remove only the independently expected two additions. Any incidental
|
||||
// mutation, clone of target children or extra correspondence fails here.
|
||||
let mut retained = after.clone();
|
||||
retained.elements.remove(&target);
|
||||
retained.traces.remove(&trace);
|
||||
assert_eq!(retained, *p);
|
||||
round_trip(&after);
|
||||
// Replaying a creation plan after acceptance violates its snapshot, while
|
||||
// a fresh unchanged request reuses the accepted counterpart.
|
||||
assert!(
|
||||
apply_transition(&after, &plan)
|
||||
.unwrap_err()
|
||||
.0
|
||||
.contains("stale")
|
||||
);
|
||||
unchanged_retry(&after, direction, source, target);
|
||||
(after, target)
|
||||
}
|
||||
|
||||
fn target_deletion(direction: TransitionDirection) {
|
||||
let fixture = fixture(direction);
|
||||
let request = TransitionRequest::new(direction, vec![fixture.source.root]);
|
||||
let old_plan = plan_transition(&fixture.project, &request).unwrap();
|
||||
assert!(old_plan.conflicts.is_empty());
|
||||
let expected = expected_deletion(&fixture, &fixture.target);
|
||||
let mut session = Session::new(fixture.project.clone());
|
||||
session
|
||||
.execute(Command::Delete(fixture.target.root))
|
||||
.unwrap();
|
||||
assert_eq!(session.project(), &expected);
|
||||
round_trip(session.project());
|
||||
let revision = session.revision();
|
||||
assert!(
|
||||
apply_transition(session.project(), &old_plan)
|
||||
.unwrap_err()
|
||||
.0
|
||||
.contains("stale")
|
||||
);
|
||||
let explicit_plan = plan_transition(
|
||||
session.project(),
|
||||
&explicit(direction, fixture.source.root, fixture.target.root),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
explicit_plan
|
||||
.conflicts
|
||||
.iter()
|
||||
.any(|conflict| conflict.source == Some(fixture.source.root)
|
||||
&& conflict.target == Some(fixture.target.root)
|
||||
&& conflict.field.is_none())
|
||||
);
|
||||
assert!(apply_transition(session.project(), &explicit_plan).is_err());
|
||||
assert_eq!(session.project(), &expected);
|
||||
assert_eq!(session.revision(), revision);
|
||||
let (created, _) = implicit_creation(
|
||||
session.project(),
|
||||
direction,
|
||||
fixture.source.root,
|
||||
fixture.target.root,
|
||||
);
|
||||
session.execute(Command::Replace(created.clone())).unwrap();
|
||||
session.undo();
|
||||
assert_eq!(session.project(), &expected);
|
||||
session.undo();
|
||||
assert_eq!(session.project(), &fixture.project);
|
||||
assert!(!session.can_undo());
|
||||
session.redo();
|
||||
session.redo();
|
||||
assert_eq!(session.project(), &created);
|
||||
round_trip(session.project());
|
||||
}
|
||||
|
||||
fn source_recreation(direction: TransitionDirection) {
|
||||
let fixture = fixture(direction);
|
||||
let mut replacement = fixture.project.elements[&fixture.source.root].clone();
|
||||
replacement.id = Id::new_v4();
|
||||
let source = replacement.id;
|
||||
// Same alias, name and prose, different UUID: name matching must never
|
||||
// silently relink the retained engineering design.
|
||||
assert_ne!(source, fixture.source.root);
|
||||
let deleted = execute_batch(&fixture.project, &[Command::Delete(fixture.source.root)]).unwrap();
|
||||
assert_eq!(deleted, expected_deletion(&fixture, &fixture.source));
|
||||
let recreated =
|
||||
execute_batch(&deleted, &[Command::CreateElement(replacement.clone())]).unwrap();
|
||||
let mut expected = deleted.clone();
|
||||
expected.elements.insert(source, replacement);
|
||||
assert_eq!(recreated, expected);
|
||||
round_trip(&recreated);
|
||||
let (implicit, new_target) =
|
||||
implicit_creation(&recreated, direction, source, fixture.target.root);
|
||||
assert!(implicit.elements.contains_key(&fixture.target.root));
|
||||
assert_ne!(new_target, fixture.target.root);
|
||||
|
||||
// The explicit branch starts from exactly the same recreated project.
|
||||
let mut request = explicit(direction, source, fixture.target.root);
|
||||
let unreviewed = plan_transition(&recreated, &request).unwrap();
|
||||
for field in [TransitionField::Name, TransitionField::Description] {
|
||||
assert!(
|
||||
unreviewed
|
||||
.conflicts
|
||||
.iter()
|
||||
.any(|conflict| conflict.source == Some(source)
|
||||
&& conflict.target == Some(fixture.target.root)
|
||||
&& conflict.field == Some(field))
|
||||
);
|
||||
}
|
||||
assert!(apply_transition(&recreated, &unreviewed).is_err());
|
||||
request.resolutions = vec![
|
||||
FieldResolution {
|
||||
source,
|
||||
field: TransitionField::Name,
|
||||
decision: FieldDecision::TakeSource,
|
||||
},
|
||||
FieldResolution {
|
||||
source,
|
||||
field: TransitionField::Description,
|
||||
decision: FieldDecision::KeepTarget,
|
||||
},
|
||||
];
|
||||
let plan = plan_transition(&recreated, &request).unwrap();
|
||||
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
||||
assert_eq!(plan.changes.len(), 1);
|
||||
assert_eq!(
|
||||
(plan.changes[0].target, plan.changes[0].action),
|
||||
(fixture.target.root, TransitionAction::Update)
|
||||
);
|
||||
assert_eq!(
|
||||
plan.changes[0].fields,
|
||||
vec![FieldChange {
|
||||
field: TransitionField::Name,
|
||||
before: recreated.elements[&fixture.target.root].name.clone(),
|
||||
after: recreated.elements[&source].name.clone(),
|
||||
}]
|
||||
);
|
||||
let relinked = apply_transition(&recreated, &plan).unwrap();
|
||||
let new_trace = assert_only_new_trace(
|
||||
&recreated,
|
||||
&relinked,
|
||||
direction,
|
||||
source,
|
||||
fixture.target.root,
|
||||
);
|
||||
assert_ne!(
|
||||
new_trace, fixture.trace,
|
||||
"Deleted correspondence is not silently resurrected"
|
||||
);
|
||||
let mut expected = recreated.clone();
|
||||
expected
|
||||
.elements
|
||||
.get_mut(&fixture.target.root)
|
||||
.unwrap()
|
||||
.name = recreated.elements[&source].name.clone();
|
||||
let mut without_trace = relinked.clone();
|
||||
without_trace.traces.remove(&new_trace);
|
||||
assert_eq!(
|
||||
without_trace, expected,
|
||||
"Only the reviewed target name may change; rich target design survives"
|
||||
);
|
||||
round_trip(&relinked);
|
||||
unchanged_retry(&relinked, direction, source, fixture.target.root);
|
||||
let original_source = round_trip(&recreated);
|
||||
let mut session = Session::new(recreated.clone());
|
||||
session.execute(Command::Replace(relinked)).unwrap();
|
||||
session.undo();
|
||||
assert_eq!(session.project(), &recreated);
|
||||
assert_eq!(round_trip(session.project()), original_source);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oa_source_deletion_preserves_rich_sa_design_and_exact_undo() {
|
||||
source_deletion(TransitionDirection::OaToSa);
|
||||
}
|
||||
#[test]
|
||||
fn sa_source_deletion_preserves_rich_portless_oa_design_and_exact_undo() {
|
||||
source_deletion(TransitionDirection::SaToOa);
|
||||
}
|
||||
#[test]
|
||||
fn deleted_sa_target_refuses_old_plans_and_implicit_retry_creates_once() {
|
||||
target_deletion(TransitionDirection::OaToSa);
|
||||
}
|
||||
#[test]
|
||||
fn deleted_oa_target_refuses_old_plans_and_implicit_retry_creates_once() {
|
||||
target_deletion(TransitionDirection::SaToOa);
|
||||
}
|
||||
#[test]
|
||||
fn recreated_oa_identity_requires_explicit_relink_to_retained_sa_design() {
|
||||
source_recreation(TransitionDirection::OaToSa);
|
||||
}
|
||||
#[test]
|
||||
fn recreated_sa_identity_requires_explicit_relink_to_retained_oa_design() {
|
||||
source_recreation(TransitionDirection::SaToOa);
|
||||
}
|
||||
@@ -269,9 +269,6 @@ impl App {
|
||||
.map(|request| request.job);
|
||||
return task;
|
||||
}
|
||||
nav::SymbolEvent::Close => {
|
||||
return self.show_text_drawer(None);
|
||||
}
|
||||
nav::SymbolEvent::Back => {
|
||||
self.source_symbol_back();
|
||||
// A successful Back needs no empty results panel. Retain an
|
||||
|
||||
@@ -85,6 +85,11 @@ fn symbol(app: &mut App, event: nav::SymbolEvent) {
|
||||
send(app, message);
|
||||
}
|
||||
|
||||
fn close_drawer(app: &mut App) {
|
||||
let message = Message::TextWorkspace(TextEvent::HideDrawer(app.text_drawer_context()));
|
||||
send(app, message);
|
||||
}
|
||||
|
||||
fn choice(app: &App, stamp: query::QueryStamp, reference: bool, index: usize) -> Message {
|
||||
app.source_symbol_message(nav::SymbolEvent::Choose {
|
||||
stamp,
|
||||
@@ -414,7 +419,7 @@ fn close_during_busy_retains_hidden_completion_without_reopening_or_navigation()
|
||||
let raw = app.source_workspace.bundle();
|
||||
let cursor = app.source_editor.cursor();
|
||||
assert!(references_visible(&app));
|
||||
symbol(&mut app, nav::SymbolEvent::Close);
|
||||
close_drawer(&mut app);
|
||||
assert!(app.text_workspace.drawer().is_none());
|
||||
assert_eq!(app.source_symbols.in_flight, Some(request.job));
|
||||
assert!(app.source_symbols.request.is_none());
|
||||
@@ -561,7 +566,7 @@ fn closed_results_stay_closed_on_exact_back_and_reopen_only_for_a_return_notice(
|
||||
let origin = app.source_editor.cursor();
|
||||
finish(&mut app, query::Action::Definition);
|
||||
reveal(&mut app);
|
||||
symbol(&mut app, nav::SymbolEvent::Close);
|
||||
close_drawer(&mut app);
|
||||
assert!(app.text_workspace.drawer().is_none() && app.source_symbols.results.is_some());
|
||||
if changed {
|
||||
// Use the workspace edit API to give the origin a new generation.
|
||||
@@ -1433,7 +1438,7 @@ fn stale_envelope_trial(reset: bool) {
|
||||
let old_messages = [
|
||||
app.source_symbol_message(nav::SymbolEvent::Query(query::Action::Definition)),
|
||||
app.source_symbol_message(nav::SymbolEvent::Back),
|
||||
app.source_symbol_message(nav::SymbolEvent::Close),
|
||||
Message::TextWorkspace(TextEvent::HideDrawer(app.text_drawer_context())),
|
||||
choice(&app, old_results.stamp, false, 0),
|
||||
];
|
||||
if reset {
|
||||
@@ -1494,7 +1499,7 @@ fn stale_envelope_trial(reset: bool) {
|
||||
// Replay once more with no busy-slot fallback and visible current results.
|
||||
assert_stale_envelopes_preserve_current(&mut app, &old_messages);
|
||||
let interaction = app.source_symbols.interaction;
|
||||
symbol(&mut app, nav::SymbolEvent::Close);
|
||||
close_drawer(&mut app);
|
||||
assert_ne!(app.source_symbols.interaction, interaction);
|
||||
assert!(app.text_workspace.drawer().is_none() && app.source_symbols.results.is_some());
|
||||
let next = begin(&mut app, query::Action::References);
|
||||
|
||||
@@ -78,7 +78,11 @@ impl<Message, R: renderer::Renderer> Widget<Message, Theme, R> for Owner<'_, Mes
|
||||
}
|
||||
if state.context.is_none_or(|previous| {
|
||||
(previous.document, previous.diagram, previous.epoch)
|
||||
!= (self.context.document, self.context.diagram, self.context.epoch)
|
||||
!= (
|
||||
self.context.document,
|
||||
self.context.diagram,
|
||||
self.context.epoch,
|
||||
)
|
||||
}) || !self.enabled
|
||||
{
|
||||
state.focused = false;
|
||||
|
||||
@@ -10,7 +10,6 @@ pub enum SymbolEvent {
|
||||
DiagramUsages,
|
||||
Query(query::Action),
|
||||
Back,
|
||||
Close,
|
||||
Choose {
|
||||
stamp: query::QueryStamp,
|
||||
visit: Id,
|
||||
|
||||
@@ -1531,6 +1531,16 @@ fn checks(app: &App) -> View<'_, Message> {
|
||||
scrollable(out.padding(24)).into()
|
||||
}
|
||||
|
||||
#[cfg(all(test, debug_assertions))]
|
||||
pub(crate) fn captured_text_drawer_resize(app: &App) -> Message {
|
||||
text_workspace::capture_drawer_resize(app)
|
||||
}
|
||||
|
||||
#[cfg(all(test, debug_assertions))]
|
||||
pub(crate) fn captured_source_reference_stale_summary(app: &App) -> bool {
|
||||
text_workspace::summary_visibility_tests::rendered_stale(app)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::short;
|
||||
@@ -1584,13 +1594,3 @@ mod tests {
|
||||
assert_eq!(short("e\u{0301}B", 1), "…");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, debug_assertions))]
|
||||
pub(crate) fn captured_text_drawer_resize(app: &App) -> Message {
|
||||
text_workspace::capture_drawer_resize(app)
|
||||
}
|
||||
|
||||
#[cfg(all(test, debug_assertions))]
|
||||
pub(crate) fn captured_source_reference_stale_summary(app: &App) -> bool {
|
||||
text_workspace::summary_visibility_tests::rendered_stale(app)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use iced::{
|
||||
use oa_core::{transitions::*, *};
|
||||
use std::collections::BTreeSet;
|
||||
mod communication;
|
||||
mod counterparts;
|
||||
pub(crate) mod recovery;
|
||||
#[cfg(test)]
|
||||
mod transition_tests;
|
||||
@@ -34,6 +35,7 @@ pub enum Event {
|
||||
Scope(String),
|
||||
Destination(Choice),
|
||||
MappingTarget(Id, Choice),
|
||||
MappingQuery(Id, String),
|
||||
MappingKind(Id, Kind),
|
||||
CommunicationPort(Id, bool, communication::PortMenuChoice),
|
||||
OperationOmission(Id, bool),
|
||||
@@ -274,6 +276,7 @@ pub struct Transition {
|
||||
pub request: TransitionRequest,
|
||||
pub plan: TransitionPlan,
|
||||
communication_ports: std::collections::BTreeMap<Id, communication::PortDraft>,
|
||||
counterpart_queries: std::collections::BTreeMap<Id, String>,
|
||||
}
|
||||
impl Transition {
|
||||
fn presentation(&self, p: &Project) -> (Project, Vec<Id>) {
|
||||
@@ -515,6 +518,7 @@ impl Workflow {
|
||||
request,
|
||||
plan,
|
||||
communication_ports: Default::default(),
|
||||
counterpart_queries: Default::default(),
|
||||
})))
|
||||
}
|
||||
/// Return a complete accepted candidate only on explicit Apply. Intermediate
|
||||
@@ -619,6 +623,12 @@ impl Workflow {
|
||||
_ => {}
|
||||
},
|
||||
Self::Transition(d) => {
|
||||
if let Event::MappingQuery(source, query) = event {
|
||||
d.counterpart_queries.insert(source, query);
|
||||
// Searching is presentation only: preserve reviewed decisions,
|
||||
// preview identities and the exact recovery snapshot.
|
||||
return Ok(None);
|
||||
}
|
||||
match &event {
|
||||
Event::OperationOmission(source, enabled) => {
|
||||
d.request
|
||||
@@ -1148,48 +1158,17 @@ impl Workflow {
|
||||
content=content.push(text("The SA operation remains unchanged. Correlated replies derive this Call's abstraction.").size(11));
|
||||
}
|
||||
let mapping = d.request.mappings.iter().find(|m| m.source == source);
|
||||
let existing = p
|
||||
.traces
|
||||
.values()
|
||||
.filter_map(|t| {
|
||||
if d.request.direction == TransitionDirection::OaToSa && t.oa == source
|
||||
{
|
||||
Some(t.sa)
|
||||
} else if d.request.direction == TransitionDirection::SaToOa
|
||||
&& t.sa == source
|
||||
{
|
||||
Some(t.oa)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let target = mapping.map(|m| m.target).unwrap_or_else(|| {
|
||||
if existing.len() == 1 {
|
||||
Some(existing[0])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
let target_choice = counterparts::target(p, source, &d.request);
|
||||
let target = target_choice.flatten();
|
||||
let query = d
|
||||
.counterpart_queries
|
||||
.get(&source)
|
||||
.map_or("", String::as_str);
|
||||
let targets = counterparts::choices(p, source, d.request.direction, query);
|
||||
let no_matches = targets.len() == 1;
|
||||
let selected = target_choice.map(|target| {
|
||||
counterparts::selected(p, source, d.request.direction, target)
|
||||
});
|
||||
let mut targets = vec![Choice {
|
||||
id: Id::nil(),
|
||||
label: "Create new counterpart".into(),
|
||||
}];
|
||||
targets.extend(
|
||||
p.elements
|
||||
.keys()
|
||||
.chain(p.relations.keys())
|
||||
.filter(|id| {
|
||||
p.layer_of(**id) == Some(d.request.direction.target_layer())
|
||||
&& p.elements.contains_key(&source)
|
||||
== p.elements.contains_key(id)
|
||||
})
|
||||
.map(|id| choice(p, *id)),
|
||||
);
|
||||
let selected = targets
|
||||
.iter()
|
||||
.find(|c| Some(c.id) == target.or(Some(Id::nil())))
|
||||
.cloned();
|
||||
let mut item = column![
|
||||
text(format!(
|
||||
"{} · {}",
|
||||
@@ -1200,14 +1179,29 @@ impl Workflow {
|
||||
.unwrap_or("Relationship")
|
||||
))
|
||||
.size(14),
|
||||
text_input("Find counterpart by name or @alias", query)
|
||||
.on_input(move |query| Message::Workflow(Event::MappingQuery(
|
||||
source, query
|
||||
)))
|
||||
.size(12)
|
||||
.padding(8),
|
||||
pick_list(targets, selected, move |v| Message::Workflow(
|
||||
Event::MappingTarget(source, v)
|
||||
))
|
||||
.placeholder("Choose a counterpart")
|
||||
.text_size(12)
|
||||
.padding(8)
|
||||
.width(Fill)
|
||||
]
|
||||
.spacing(7);
|
||||
if no_matches {
|
||||
let message = if query.trim().is_empty() {
|
||||
"No compatible existing counterparts. Create a new one."
|
||||
} else {
|
||||
"No matching existing counterparts. Clear the search or create a new one."
|
||||
};
|
||||
item = item.push(text(message).size(11));
|
||||
}
|
||||
if target.is_none()
|
||||
&& let Some(e) = p.elements.get(&source)
|
||||
{
|
||||
@@ -1413,16 +1407,6 @@ impl Workflow {
|
||||
.color(style::MUTED),
|
||||
);
|
||||
}
|
||||
content = content.push(
|
||||
text(format!(
|
||||
"{} changes · {} required boundary choices · {} unresolved conflicts · {} modeling warnings",
|
||||
d.plan.changes.len(),
|
||||
d.plan.conflicts.iter().filter(|c| d.boundary_choice(p, c)).count(),
|
||||
d.plan.conflicts.iter().filter(|c| !d.boundary_choice(p, c)).count(),
|
||||
d.plan.warnings.len()
|
||||
))
|
||||
.size(12),
|
||||
);
|
||||
}
|
||||
}
|
||||
let enabled = match self {
|
||||
@@ -1439,12 +1423,50 @@ impl Workflow {
|
||||
.color(iced::Color::from_rgb8(178, 54, 58)),
|
||||
);
|
||||
}
|
||||
column![
|
||||
scrollable(content.padding(iced::Padding {
|
||||
right: 14.,
|
||||
..Default::default()
|
||||
}))
|
||||
.height(480),
|
||||
let mut footer = column![].spacing(8);
|
||||
if let Self::Transition(d) = self {
|
||||
let boundary_choices = d
|
||||
.plan
|
||||
.conflicts
|
||||
.iter()
|
||||
.filter(|c| d.boundary_choice(p, c))
|
||||
.count();
|
||||
footer = footer.push(
|
||||
text(format!(
|
||||
"{} reviewed items · {} boundary choices · {} conflicts · {} warnings",
|
||||
d.plan.changes.len(),
|
||||
boundary_choices,
|
||||
d.plan.conflicts.len() - boundary_choices,
|
||||
d.plan.warnings.len()
|
||||
))
|
||||
.size(12),
|
||||
);
|
||||
let guidance = if d.destination_error(p).is_some() {
|
||||
Some("Choose a compatible diagram in Show result in, above the mappings.".into())
|
||||
} else if boundary_choices > 0 {
|
||||
Some("Choose the counterpart kind in each mapping that needs a system boundary decision.".into())
|
||||
} else if let Some(field) = d.plan.conflicts.iter().find_map(|c| c.field) {
|
||||
let field = match field {
|
||||
TransitionField::Name => "Name",
|
||||
TransitionField::Description => "Description",
|
||||
};
|
||||
Some(format!(
|
||||
"Review {field} below the mappings: choose Keep target, Take source, or merged text."
|
||||
))
|
||||
} else if !d.plan.conflicts.is_empty() {
|
||||
Some("Review the conflict messages below the mappings and complete the required decisions.".into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(guidance) = guidance {
|
||||
footer = footer.push(
|
||||
text(guidance)
|
||||
.size(12)
|
||||
.color(iced::Color::from_rgb8(178, 54, 58)),
|
||||
);
|
||||
}
|
||||
}
|
||||
footer = footer.push(
|
||||
row![
|
||||
Space::new().width(Fill),
|
||||
button(text("Cancel").size(12))
|
||||
@@ -1464,7 +1486,15 @@ impl Workflow {
|
||||
.padding(10)
|
||||
.on_press_maybe(enabled.then_some(Message::Workflow(Event::Apply)))
|
||||
]
|
||||
.spacing(10)
|
||||
.spacing(10),
|
||||
);
|
||||
column![
|
||||
scrollable(content.padding(iced::Padding {
|
||||
right: 14.,
|
||||
..Default::default()
|
||||
}))
|
||||
.height(480),
|
||||
footer
|
||||
]
|
||||
.spacing(18)
|
||||
.into()
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
//! Search is separate from the committed counterpart mapping.
|
||||
use super::*;
|
||||
|
||||
/// Outer None requires a choice between distinct traced targets; inner None
|
||||
/// means creation (explicit, or no existing correspondence).
|
||||
pub(super) fn target(p: &Project, source: Id, request: &TransitionRequest) -> Option<Option<Id>> {
|
||||
if let Some(mapping) = request.mappings.iter().find(|m| m.source == source) {
|
||||
return Some(mapping.target);
|
||||
}
|
||||
let existing = p
|
||||
.traces
|
||||
.values()
|
||||
.filter_map(|trace| match request.direction {
|
||||
TransitionDirection::OaToSa if trace.oa == source => Some(trace.sa),
|
||||
TransitionDirection::SaToOa if trace.sa == source => Some(trace.oa),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
match existing.len() {
|
||||
0 => Some(None),
|
||||
1 => Some(existing.first().copied()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_choice() -> Choice {
|
||||
Choice {
|
||||
id: Id::nil(),
|
||||
label: "Create new counterpart".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn identity(p: &Project, id: Id) -> Option<(&str, &str, String)> {
|
||||
if let Some(e) = p.elements.get(&id) {
|
||||
Some((&e.name, &e.alias, e.kind().label().into()))
|
||||
} else {
|
||||
p.relations
|
||||
.get(&id)
|
||||
.map(|r| (r.name.as_str(), r.alias.as_str(), r.kind().to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn label(p: &Project, id: Id) -> Choice {
|
||||
let label = match identity(p, id) {
|
||||
Some((name, alias, kind)) => format!("{} · {kind} · @{alias}", short(name, 48)),
|
||||
None => format!("Missing counterpart · {id}"),
|
||||
};
|
||||
Choice { id, label }
|
||||
}
|
||||
|
||||
pub(super) fn selected(
|
||||
p: &Project,
|
||||
source: Id,
|
||||
direction: TransitionDirection,
|
||||
target: Option<Id>,
|
||||
) -> Choice {
|
||||
let Some(target) = target else {
|
||||
return new_choice();
|
||||
};
|
||||
let mut choice = label(p, target);
|
||||
if p.contains(target) && !counterpart_compatible(p, source, target, direction) {
|
||||
choice.label.push_str(" (incompatible)");
|
||||
}
|
||||
choice
|
||||
}
|
||||
|
||||
pub(super) fn choices(
|
||||
p: &Project,
|
||||
source: Id,
|
||||
direction: TransitionDirection,
|
||||
query: &str,
|
||||
) -> Vec<Choice> {
|
||||
let query = query.trim().to_lowercase();
|
||||
let mut candidates = p
|
||||
.elements
|
||||
.keys()
|
||||
.chain(p.relations.keys())
|
||||
.copied()
|
||||
.filter(|id| counterpart_compatible(p, source, *id, direction))
|
||||
.filter_map(|id| {
|
||||
let (name, alias, _) = identity(p, id)?;
|
||||
let name = name.to_lowercase();
|
||||
let alias = alias.to_lowercase();
|
||||
let matches = if let Some(query) = query.strip_prefix('@') {
|
||||
alias.contains(query)
|
||||
} else {
|
||||
name.contains(&query) || alias.contains(&query)
|
||||
};
|
||||
matches.then_some((name, alias, id))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
candidates.sort();
|
||||
std::iter::once(new_choice())
|
||||
.chain(candidates.into_iter().map(|(_, _, id)| label(p, id)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn fixture() -> (Project, Id, Id, Id, Id) {
|
||||
let mut p = Project::empty();
|
||||
let source = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Assess pressure", None)
|
||||
.unwrap();
|
||||
let first = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Assess pressure", None)
|
||||
.unwrap();
|
||||
let second = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Assess pressure", None)
|
||||
.unwrap();
|
||||
let incompatible = p
|
||||
.add_element(Kind::SystemActor, Layer::Sa, "Assess pressure", None)
|
||||
.unwrap();
|
||||
p.elements.get_mut(&first).unwrap().alias = "alpha_assess".into();
|
||||
p.elements.get_mut(&second).unwrap().alias = "beta_assess".into();
|
||||
(p, source, first, second, incompatible)
|
||||
}
|
||||
|
||||
fn ids(choices: Vec<Choice>) -> Vec<Id> {
|
||||
choices.into_iter().map(|c| c.id).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_disambiguates_names_with_aliases_and_retains_creation() {
|
||||
let (p, source, first, second, _) = fixture();
|
||||
let all = choices(&p, source, TransitionDirection::OaToSa, "");
|
||||
assert_eq!(
|
||||
all.iter().map(|c| c.id).collect::<Vec<_>>(),
|
||||
vec![Id::nil(), first, second]
|
||||
);
|
||||
assert!(all[1].label.contains("@alpha_assess"));
|
||||
assert!(all[2].label.contains("@beta_assess"));
|
||||
assert_ne!(all[1].label, all[2].label);
|
||||
for (query, expected) in [
|
||||
(" ASSESS PRESSURE ", vec![Id::nil(), first, second]),
|
||||
(" @BETA_ ", vec![Id::nil(), second]),
|
||||
("ALPHA", vec![Id::nil(), first]),
|
||||
("@pressure", vec![Id::nil()]),
|
||||
("no match", vec![Id::nil()]),
|
||||
] {
|
||||
assert_eq!(
|
||||
ids(choices(&p, source, TransitionDirection::OaToSa, query)),
|
||||
expected,
|
||||
"{query}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
ids(choices(&p, first, TransitionDirection::SaToOa, "PRESSURE")),
|
||||
vec![Id::nil(), source]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_preserves_reviewed_request_plan_ids_and_recovery_exactly() {
|
||||
let (mut p, source, first, _, _) = fixture();
|
||||
let second = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Second source card", None)
|
||||
.unwrap();
|
||||
let before_project = p.clone();
|
||||
// Exercise both an existing reviewed target and generated preview IDs.
|
||||
for chosen in [Some(first), None] {
|
||||
let mut w = Workflow::transition(&p, source).unwrap();
|
||||
let Workflow::Transition(t) = &mut w else {
|
||||
panic!()
|
||||
};
|
||||
t.request.sources.push(second);
|
||||
t.plan = plan_transition(&p, &t.request).unwrap();
|
||||
w.update(
|
||||
&p,
|
||||
Event::MappingTarget(
|
||||
source,
|
||||
selected(&p, source, TransitionDirection::OaToSa, chosen),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
w.update(
|
||||
&p,
|
||||
Event::Resolve(
|
||||
source,
|
||||
TransitionField::Name,
|
||||
FieldDecision::Value("Reviewed name".into()),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
let saved = w.recovery_snapshot().unwrap();
|
||||
let Workflow::Transition(t) = &w else {
|
||||
panic!()
|
||||
};
|
||||
let plan = t.plan.clone();
|
||||
let request = t.request.clone();
|
||||
for query in ["assess", "@beta", "no match", ""] {
|
||||
assert!(
|
||||
w.update(&p, Event::MappingQuery(source, query.into()))
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(w.recovery_snapshot().unwrap(), saved);
|
||||
let Workflow::Transition(t) = &w else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(t.plan, plan);
|
||||
assert_eq!(t.request, request);
|
||||
assert_eq!(t.counterpart_queries[&source], query);
|
||||
assert_eq!(target(&p, source, &t.request), Some(chosen));
|
||||
assert_eq!(
|
||||
selected(&p, source, t.request.direction, chosen).id,
|
||||
chosen.unwrap_or(Id::nil())
|
||||
);
|
||||
}
|
||||
w.update(&p, Event::MappingQuery(second, "other card".into()))
|
||||
.unwrap();
|
||||
let Workflow::Transition(t) = &w else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(t.counterpart_queries[&source], "");
|
||||
assert_eq!(t.counterpart_queries[&second], "other card");
|
||||
assert_eq!(t.plan, plan);
|
||||
let restored = Workflow::from_recovery(&p, saved.clone()).unwrap();
|
||||
assert_eq!(restored.recovery_snapshot().unwrap(), saved);
|
||||
let Workflow::Transition(t) = restored else {
|
||||
panic!()
|
||||
};
|
||||
assert!(t.counterpart_queries.is_empty());
|
||||
assert_eq!(t.request, request);
|
||||
}
|
||||
assert_eq!(p, before_project);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recovered_invalid_or_missing_mapping_stays_visible_and_refuses_apply() {
|
||||
let (p, source, _, _, incompatible) = fixture();
|
||||
for chosen in [incompatible, Id::from_u128(987654321)] {
|
||||
let mut w = Workflow::transition(&p, source).unwrap();
|
||||
w.update(
|
||||
&p,
|
||||
Event::MappingTarget(
|
||||
source,
|
||||
Choice {
|
||||
id: chosen,
|
||||
label: "Recovered choice".into(),
|
||||
},
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
let saved = w.recovery_snapshot().unwrap();
|
||||
let mut restored = Workflow::from_recovery(&p, saved.clone()).unwrap();
|
||||
restored
|
||||
.update(&p, Event::MappingQuery(source, "no match".into()))
|
||||
.unwrap();
|
||||
assert_eq!(restored.recovery_snapshot().unwrap(), saved);
|
||||
let Workflow::Transition(t) = &restored else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(target(&p, source, &t.request), Some(Some(chosen)));
|
||||
let display = selected(&p, source, t.request.direction, Some(chosen));
|
||||
assert_eq!(display.id, chosen);
|
||||
assert!(display.label.contains(if chosen == incompatible {
|
||||
"incompatible"
|
||||
} else {
|
||||
"Missing counterpart"
|
||||
}));
|
||||
assert!(
|
||||
!choices(&p, source, t.request.direction, "")
|
||||
.iter()
|
||||
.any(|c| c.id == chosen)
|
||||
);
|
||||
assert!(restored.update(&p, Event::Apply).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trace_inference_counts_distinct_targets_and_explicit_mapping_wins() {
|
||||
let (mut p, source, first, second, _) = fixture();
|
||||
let mut request = TransitionRequest::new(TransitionDirection::OaToSa, vec![source]);
|
||||
assert_eq!(target(&p, source, &request), Some(None));
|
||||
for (i, counterpart) in [first, first, second].into_iter().enumerate() {
|
||||
let id = Id::from_u128(100 + i as u128);
|
||||
p.traces.insert(
|
||||
id,
|
||||
Trace {
|
||||
id,
|
||||
alias: format!("trace_{i}"),
|
||||
name: format!("Trace {i}"),
|
||||
description: String::new(),
|
||||
oa: source,
|
||||
sa: counterpart,
|
||||
kind: TraceKind::Manual,
|
||||
baseline: None,
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
target(&p, source, &request),
|
||||
if i < 2 { Some(Some(first)) } else { None }
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
target(
|
||||
&p,
|
||||
first,
|
||||
&TransitionRequest::new(TransitionDirection::SaToOa, vec![first])
|
||||
),
|
||||
Some(Some(source))
|
||||
);
|
||||
for chosen in [Some(second), None] {
|
||||
request.mappings = vec![TransitionMapping {
|
||||
source,
|
||||
target: chosen,
|
||||
target_kind: None,
|
||||
}];
|
||||
assert_eq!(target(&p, source, &request), Some(chosen));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,6 +202,7 @@ impl Workflow {
|
||||
request,
|
||||
plan,
|
||||
communication_ports,
|
||||
counterpart_queries: Default::default(),
|
||||
};
|
||||
transition.sync_communication_ports();
|
||||
transition.plan =
|
||||
|
||||
@@ -75,6 +75,164 @@ fn assert_pair(current: &StoredProject) {
|
||||
assert_eq!(compiled.bundle, current.source_project);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn explicit_empty_port_anchors_survive_http_move_and_restart_but_export_compactly() {
|
||||
fn replace_once(source: &mut String, before: &str, after: &str) {
|
||||
assert_eq!(
|
||||
source.matches(before).count(),
|
||||
1,
|
||||
"ambiguous or absent anchor: {before}"
|
||||
);
|
||||
let replacement = source.replacen(before, after, 1);
|
||||
assert_ne!(*source, replacement, "fixture mutation must change source");
|
||||
*source = replacement;
|
||||
}
|
||||
|
||||
async fn assert_exports(server: &TestServer, current: &StoredProject, node: Id) -> String {
|
||||
let response = server
|
||||
.client
|
||||
.get(server.url(&format!("/projects/{}/source-project", current.resource_id)))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(response.headers()["etag"], current.etag());
|
||||
assert_eq!(
|
||||
response.json::<SourceBundle>().await.unwrap(),
|
||||
current.source_project
|
||||
);
|
||||
|
||||
let response = server
|
||||
.client
|
||||
.get(server.url(&format!("/projects/{}/source", current.resource_id)))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let canonical = response.text().await.unwrap();
|
||||
assert_eq!(oa_core::language::parse(&canonical).unwrap(), current.model);
|
||||
assert_eq!(canonical, oa_core::language::format(¤t.model));
|
||||
let exported = compile_bundle(
|
||||
&SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([("project.arcadia".into(), canonical.clone())]),
|
||||
},
|
||||
ProjectLimits::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(exported.decoded.project, current.model);
|
||||
assert!(exported.index.declarations.contains_key(&node));
|
||||
assert!(
|
||||
!exported
|
||||
.index
|
||||
.fields
|
||||
.contains_key(&(node, "port_anchors".into()))
|
||||
);
|
||||
canonical
|
||||
}
|
||||
|
||||
let mut authored = source_bundle();
|
||||
replace_once(
|
||||
authored.files.get_mut("views.arcadia").unwrap(),
|
||||
"diagram @architecture; element @vessel;",
|
||||
"diagram @architecture; element @vessel;\n position { x 12.5; y -4.0; };\n port_anchors [] ; // Keep explicit automatic placement.\n",
|
||||
);
|
||||
let compiled = compile_bundle(&authored, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(
|
||||
compiled.bundle, authored,
|
||||
"the HTTP fixture must already be stable"
|
||||
);
|
||||
let diagram = id(&compiled.decoded.project, "architecture");
|
||||
let node = id(&compiled.decoded.project, "actor_view");
|
||||
assert!(
|
||||
compiled
|
||||
.index
|
||||
.fields
|
||||
.contains_key(&(node, "port_anchors".into()))
|
||||
);
|
||||
assert_eq!(
|
||||
compiled.decoded.project.diagrams[&diagram].nodes[&node].position,
|
||||
oa_core::Position { x: 12.5, y: -4.0 }
|
||||
);
|
||||
assert!(
|
||||
compiled.decoded.project.diagrams[&diagram].nodes[&node]
|
||||
.port_anchors
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
// Independent full-model oracle: only this placement coordinate changes.
|
||||
let mut expected_model = compiled.decoded.project.clone();
|
||||
expected_model
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&node)
|
||||
.unwrap()
|
||||
.position
|
||||
.x = 25.0;
|
||||
let mut expected_source = authored.clone();
|
||||
replace_once(
|
||||
expected_source.files.get_mut("views.arcadia").unwrap(),
|
||||
"x 12.5;",
|
||||
"x 25.0;",
|
||||
);
|
||||
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let server = TestServer::start(directory.path(), None).await;
|
||||
let response = server
|
||||
.client
|
||||
.post(server.url("/projects/import-source-project"))
|
||||
.json(&authored)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let status = response.status();
|
||||
let value: Value = response.json().await.unwrap();
|
||||
assert_eq!(status, StatusCode::CREATED, "{value}");
|
||||
let original = server
|
||||
.get(value["resource_id"].as_str().unwrap().parse().unwrap())
|
||||
.await;
|
||||
assert_eq!(original.model, compiled.decoded.project);
|
||||
assert_eq!(original.source_project, authored);
|
||||
assert_pair(&original);
|
||||
assert_exports(&server, &original, node).await;
|
||||
|
||||
let response = transact(
|
||||
&server,
|
||||
&original,
|
||||
json!([{"MoveNode": {"diagram": diagram, "node": node,
|
||||
"position": {"x": 25.0, "y": -4.0}, "with_descendants": false}}]),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
let status = response.status();
|
||||
let value: Value = response.json().await.unwrap();
|
||||
assert_eq!(status, StatusCode::OK, "{value}");
|
||||
assert_eq!(value["changed"], true);
|
||||
let current = server.get(original.resource_id).await;
|
||||
assert_eq!(current.revision, original.revision + 1);
|
||||
assert_eq!(current.model, expected_model);
|
||||
// Exact bundle equality covers the explicit empty field, its rationale,
|
||||
// sibling placement, and all untouched files and source trivia.
|
||||
assert_eq!(current.source_project, expected_source);
|
||||
assert_pair(¤t);
|
||||
let canonical = assert_exports(&server, ¤t, node).await;
|
||||
|
||||
server.stop().await;
|
||||
let server = TestServer::start(directory.path(), None).await;
|
||||
let reopened = server.get(current.resource_id).await;
|
||||
assert_eq!(reopened.model, expected_model);
|
||||
assert_eq!(reopened.source_project, expected_source);
|
||||
assert_eq!(
|
||||
serde_json::to_value(&reopened).unwrap(),
|
||||
serde_json::to_value(¤t).unwrap()
|
||||
);
|
||||
assert_pair(&reopened);
|
||||
assert_eq!(assert_exports(&server, &reopened, node).await, canonical);
|
||||
server.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn port_anchor_http_commands_and_source_edits_preserve_per_view_state_after_restart() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
|
||||
+107
-16
@@ -2,7 +2,7 @@
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="generator" content="Docutils 0.22.2: https://docutils.sourceforge.io/" />
|
||||
<meta name="generator" content="Docutils 0.23: https://docutils.sourceforge.io/" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Arcadia Studio OA / SA Language and Editing Contract</title>
|
||||
<style type="text/css">
|
||||
@@ -806,11 +806,11 @@ references may be omitted and then mean <span class="docutils literal">none</spa
|
||||
These omissions are accepted in newly authored source 7, not only in schema-2
|
||||
compatibility input. Earlier schema admission of constructors and fields is
|
||||
still governed by its version rules.</p>
|
||||
<p>Canonical formatting writes the hydrated occurrence references and default
|
||||
<span class="docutils literal">message_kind</span>, <span class="docutils literal">reply_to</span> and <span class="docutils literal">values</span> explicitly. An absent
|
||||
<span class="docutils literal">operation</span> remains omitted in canonical output. Lossless no-op updates and
|
||||
unrelated name edits retain authored omissions; canonical formatting is a
|
||||
different operation. An admitted incomplete occurrence can still produce model
|
||||
<p>Canonical formatting omits optional occurrence references equal to <span class="docutils literal">none</span>,
|
||||
the default Asynchronous <span class="docutils literal">message_kind</span>, empty <span class="docutils literal">values</span>, and absent
|
||||
<span class="docutils literal">operation</span>. Required fields of a selected constructor remain present.
|
||||
Lossless updates retain unchanged explicit defaults and authored omissions;
|
||||
canonical formatting is a different operation. An admitted incomplete occurrence can still produce model
|
||||
checks requesting its missing work, exchange, lifeline or referenced scenario.</p>
|
||||
<table>
|
||||
<caption>Occurrence body constructors</caption>
|
||||
@@ -1569,29 +1569,40 @@ has no explicit default, and does not convey ownership or deletion of the type.<
|
||||
<thead>
|
||||
<tr><th class="head"><p>Constructor</p></th>
|
||||
<th class="head"><p>Required fields</p></th>
|
||||
<th class="head"><p>Optional fields (omission value)</p></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><p>Boolean</p></td>
|
||||
<td><p>None</p></td>
|
||||
<td><p>None</p></td>
|
||||
</tr>
|
||||
<tr><td><p>Integer</p></td>
|
||||
<td><p><span class="docutils literal">minimum</span>, <span class="docutils literal">maximum</span>: optional signed 64-bit integers</p></td>
|
||||
<td><p>None</p></td>
|
||||
<td><p><span class="docutils literal">minimum</span>, <span class="docutils literal">maximum</span>: signed 64-bit integers? (none)</p></td>
|
||||
</tr>
|
||||
<tr><td><p>Real</p></td>
|
||||
<td><p><span class="docutils literal">minimum</span>, <span class="docutils literal">maximum</span>: optional finite IEEE-754 binary64 values</p></td>
|
||||
<td><p>None</p></td>
|
||||
<td><p><span class="docutils literal">minimum</span>, <span class="docutils literal">maximum</span>: finite IEEE-754 binary64 values? (none)</p></td>
|
||||
</tr>
|
||||
<tr><td><p>String</p></td>
|
||||
<td><p><span class="docutils literal">minimum_length</span>: u32; <span class="docutils literal">maximum_length</span>: optional u32</p></td>
|
||||
<td><p><span class="docutils literal">minimum_length</span>: u32</p></td>
|
||||
<td><p><span class="docutils literal">maximum_length</span>: u32? (none)</p></td>
|
||||
</tr>
|
||||
<tr><td><p>Enumeration</p></td>
|
||||
<td><p><span class="docutils literal">literals</span>: unique nonempty, case-sensitive strings</p></td>
|
||||
<td><p>None</p></td>
|
||||
</tr>
|
||||
<tr><td><p>Record</p></td>
|
||||
<td><p><span class="docutils literal">supertype</span>: same-layer Record ref?</p></td>
|
||||
<td><p>None</p></td>
|
||||
<td><p><span class="docutils literal">supertype</span>: same-layer Record ref? (none)</p></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Optional fields may be absent or explicitly <span class="docutils literal">none</span>. Selected String and
|
||||
Enumeration constructors still require <span class="docutils literal">minimum_length</span> and <span class="docutils literal">literals</span>,
|
||||
respectively, including zero and an empty list. Selecting a constructor does
|
||||
not merge fields from the default String definition. Unknown fields reject.</p>
|
||||
<p>Bounds are inclusive and ordered; an absent bound means unbounded within the
|
||||
represented numeric domain. Integer comparisons MUST NOT convert to binary64.
|
||||
NaN, infinities, overflow and reversed bounds reject. String length counts Unicode
|
||||
@@ -2439,10 +2450,27 @@ references, placements, artifacts, trace baselines, and ordered occurrences.
|
||||
Map order is irrelevant. The writer preserves signed coordinate zero and
|
||||
emits decimals that recover the original binary32 value.</p>
|
||||
<p>Canonical output uses LF, four-space indentation, explicit identities,
|
||||
defaults, and JSON-escaped strings. Elements are emitted in UUID order, then
|
||||
and JSON-escaped strings. It omits fields equal to the omission defaults of
|
||||
their actual declaration or selected constructor, as specified in the field
|
||||
tables. This includes an empty project description. It MUST NOT omit a required
|
||||
field merely because its value is zero, false, empty, or <span class="docutils literal">none</span>. Elements are emitted in UUID order, then
|
||||
relations; diagrams and their nodes follow, then artifacts, then traces.
|
||||
Fields use deterministic name order. Output ends with LF. Alias values come
|
||||
from the model, never from regenerating display names.</p>
|
||||
<p>Omission is context-sensitive: Scenario uses the source default MixedLegacy;
|
||||
BoundaryExchange and CommunicationMean have different orientation defaults;
|
||||
state declarations determine their state-kind discriminator. A nonempty or
|
||||
whitespace-only description remains exact. Present-empty optional strings or
|
||||
collections, unbounded multiplicity versus a finite default, selected operation
|
||||
intent, and negative coordinate zero remain distinct values. A nondefault
|
||||
geometry record retains all required coordinates or dimensions.</p>
|
||||
<p>Only specified optional fields inside selected constructors may be omitted.
|
||||
For example, Integer bounds and LegacyPortContract.interface may be absent;
|
||||
String.minimum_length, Signal.items, port-contract payload lists, and historical
|
||||
snapshot name/description fields remain required. A LegacyPortContract record
|
||||
still requires direction. Historical semantic baseline payloads are retained
|
||||
without generic empty/default pruning. Compact output MUST preserve every
|
||||
persistent model field after parsing, including these distinctions.</p>
|
||||
<p>Comments and input whitespace are lexical trivia to the canonical writer.
|
||||
Canonical export does not preserve them. The source-project editor and compiler
|
||||
preserve authored comments and whitespace on Apply while inserting missing
|
||||
@@ -2484,11 +2512,19 @@ independent parent and child moves are resolved together. Leading whitespace
|
||||
adjusts to the destination scope; other token spelling remains authored.</p>
|
||||
<p>Updates MUST NOT canonicalize surviving declarations in full. Header, field and
|
||||
nested value positions are derived from the existing syntax parser. Unchanged
|
||||
fields retain their exact bytes, including field order, omitted defaults, number
|
||||
fields retain their exact bytes, including field order, explicit and omitted defaults, number
|
||||
and string spelling, comments, indentation and line endings. Changed literals and
|
||||
references replace only the required tokens; a reference with comments between
|
||||
<span class="docutils literal">@</span> and its alias retains those comments. A previously omitted default is
|
||||
inserted only if its semantic value changes. New fields use the existing record's
|
||||
<span class="docutils literal">@</span> and its alias retains those comments. A previously omitted field is
|
||||
inserted only when needed to express the requested value in its new declaration
|
||||
context. This includes an unchanged value whose omission default changes with
|
||||
the declaration kind. For example, changing BoundaryExchange to CommunicationMean
|
||||
while retaining orientation true requires <span class="docutils literal">oriented true;</span> if previously
|
||||
implicit. Unchanged explicit defaults MUST survive unrelated sibling edits,
|
||||
including persistence-serializer omissions such as <span class="docutils literal">operation none</span>,
|
||||
<span class="docutils literal">port_anchors []</span> and <span class="docutils literal">baseline.semantic none</span>. New fields and value fragments
|
||||
use compact generation while retaining required selected-constructor payloads.
|
||||
New fields use the existing record's
|
||||
indentation and line-ending style without reformatting its other fields.</p>
|
||||
<p>A constructor tag change retains shared nested fields in place. Removed fields,
|
||||
constructors and declarations remove syntax tokens, retaining their comments
|
||||
@@ -2618,6 +2654,26 @@ placement move are different transactions. The UI MUST identify which operation
|
||||
will occur. Reparenting work must not silently change its performer. A computed
|
||||
parent summary must remain distinguishable from direct allocation. A collapse
|
||||
operation hides presentation detail without deleting the children.</p>
|
||||
<p>In an Allocation view, dragging an Activity or Function body MAY combine a
|
||||
performer change with diagram placement. The preview MUST identify the
|
||||
destination and preserve readable work and destination identity. Destination
|
||||
identity is shown outside the canvas, independently of explanatory or refusal
|
||||
text. The outlined target's hit region remains fixed throughout the gesture;
|
||||
feedback MUST NOT obscure the work or change its pointer-relative placement.
|
||||
A clean click selects without applying unrelated drafts. A refused work press
|
||||
with an unapplied property draft MUST preserve its text, caret and focused
|
||||
editor. An accepted drag is one atomic model/source/history transaction:
|
||||
semantic ownership and work identities are
|
||||
preserved; only leaf direct Allocation relations, their own retired routes and
|
||||
the affected view geometry change. Existing Allocation identities are retained
|
||||
on reassignment. Role realizations and assignments MUST NOT be silently changed. Uniform work groups
|
||||
may move their leaf responsibilities together; ambiguous or distributed groups
|
||||
and changes requiring collateral record edits MUST be refused with an explanation.
|
||||
Moving outside performer regions removes direct assignment only when the
|
||||
effective role responsibility is also absent. Releasing outside the viewport
|
||||
cancels the gesture. Explicit Arrange next drag applies geometry alone for one
|
||||
gesture. Refused and snapped no-op drops MUST NOT create history entries or
|
||||
discard Redo. Undo restores the exact source bundle and identities together.</p>
|
||||
<p>Applying a reachable source bundle is one model transaction. Graphical edits
|
||||
update its owning declarations through the source-preserving updater. Both
|
||||
paths validate their candidate and model/source equality before
|
||||
@@ -2719,6 +2775,40 @@ 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="focused-diagram-removal">
|
||||
<h3>8.1. Focused diagram removal</h3>
|
||||
<p>Delete is scoped to the focused diagram and the exact visible selection.
|
||||
Text fields retain their own Delete behavior; diagram shortcuts MUST NOT apply
|
||||
pending property, source or behavior drafts. Keyboard traversal retires diagram
|
||||
input admission before subsequent keys in the same event batch are processed.
|
||||
Active placement, creation, connection, reorder and pan gestures do not authorize
|
||||
removal. A stale document, revision, owner or selection MUST NOT redirect removal.</p>
|
||||
<p>For an explicit architecture or state placement, Delete removes that placement
|
||||
only. The semantic element and its placements in other views remain. Shift+Delete
|
||||
requests confirmed deletion of the semantic element, including when selection
|
||||
originated from Placement properties. A selected architecture route bend takes
|
||||
priority: plain Delete removes the bend without removing its edge.</p>
|
||||
<p>A visible relation with no placement, including a state action or internal
|
||||
transition row, uses confirmed relation deletion for either key. States and
|
||||
referenced Activity/Function definitions remain. Hidden or collapsed rows are
|
||||
not keyboard deletion targets.</p>
|
||||
<p>Process and scenario projections have no independent occurrence placements.
|
||||
Delete and Shift+Delete request confirmed removal of the selected occurrence,
|
||||
participant or maintained link. A link's endpoints and outbound reusable
|
||||
definitions are preserved. Removing a fragment or branch removes its owned
|
||||
subtree. Removing a scenario participant preserves its represented element and
|
||||
retains occurrences with affected participant references cleared; resulting
|
||||
incomplete references remain visible for repair. The modeled scenario Delete
|
||||
message is a separate domain event and is not this editing shortcut.</p>
|
||||
<p>A qualified selection inside an expanded reusable process MUST NOT silently
|
||||
delete the outer reference or shared definition item. The editor directs the
|
||||
user to open the definition or explicitly select the outer reference.</p>
|
||||
<p>Confirmation identifies the captured object and removal effect. Cancel and
|
||||
Escape preserve model, source and history. A confirmation token belongs to one
|
||||
dialog; changes to its document, revision, owner or selected identity invalidate
|
||||
it. Accepted deletion uses one ordinary model/source transaction, and Undo
|
||||
restores the exact complete accepted source bundle and model.</p>
|
||||
</section>
|
||||
</section>
|
||||
<section id="persistence-and-explicit-migration">
|
||||
<h2>9. Persistence and explicit migration</h2>
|
||||
@@ -2793,8 +2883,9 @@ This compatibility applies equally to source and JSON. It adds explicit
|
||||
defaults without changing any prior identity, name, reference, order, geometry,
|
||||
artifact or trace field. The original V2 golden source/JSON in
|
||||
<span class="docutils literal"><span class="pre">crates/oa-core/tests/fixtures</span></span> MUST NOT be regenerated from the newer demo;
|
||||
they verify that every old persistent field survives. Canonical output writes
|
||||
the added defaults through explicit migration to the current schema 7.</p>
|
||||
they verify that every old persistent field survives. Canonical output uses
|
||||
the current schema 7 and omits values equal to its specified source defaults;
|
||||
parsing recovers the complete migrated model.</p>
|
||||
<p>The V2 adapter preserves every original identity, alias, prose, parent,
|
||||
placement, trace and artifact. Generic ports become Legacy with their original
|
||||
direction/interface record. Existing FE endpoint IDs remain unchanged. For
|
||||
|
||||
+56
-16
@@ -171,11 +171,11 @@ These omissions are accepted in newly authored source 7, not only in schema-2
|
||||
compatibility input. Earlier schema admission of constructors and fields is
|
||||
still governed by its version rules.
|
||||
|
||||
Canonical formatting writes the hydrated occurrence references and default
|
||||
``message_kind``, ``reply_to`` and ``values`` explicitly. An absent
|
||||
``operation`` remains omitted in canonical output. Lossless no-op updates and
|
||||
unrelated name edits retain authored omissions; canonical formatting is a
|
||||
different operation. An admitted incomplete occurrence can still produce model
|
||||
Canonical formatting omits optional occurrence references equal to ``none``,
|
||||
the default Asynchronous ``message_kind``, empty ``values``, and absent
|
||||
``operation``. Required fields of a selected constructor remain present.
|
||||
Lossless updates retain unchanged explicit defaults and authored omissions;
|
||||
canonical formatting is a different operation. An admitted incomplete occurrence can still produce model
|
||||
checks requesting its missing work, exchange, lifeline or referenced scenario.
|
||||
|
||||
.. list-table:: Occurrence body constructors
|
||||
@@ -933,18 +933,30 @@ DataTypeDefinition has exactly the following constructors:
|
||||
|
||||
* - Constructor
|
||||
- Required fields
|
||||
- Optional fields (omission value)
|
||||
* - Boolean
|
||||
- None
|
||||
- None
|
||||
* - Integer
|
||||
- ``minimum``, ``maximum``: optional signed 64-bit integers
|
||||
- None
|
||||
- ``minimum``, ``maximum``: signed 64-bit integers? (none)
|
||||
* - Real
|
||||
- ``minimum``, ``maximum``: optional finite IEEE-754 binary64 values
|
||||
- None
|
||||
- ``minimum``, ``maximum``: finite IEEE-754 binary64 values? (none)
|
||||
* - String
|
||||
- ``minimum_length``: u32; ``maximum_length``: optional u32
|
||||
- ``minimum_length``: u32
|
||||
- ``maximum_length``: u32? (none)
|
||||
* - Enumeration
|
||||
- ``literals``: unique nonempty, case-sensitive strings
|
||||
- None
|
||||
* - Record
|
||||
- ``supertype``: same-layer Record ref?
|
||||
- None
|
||||
- ``supertype``: same-layer Record ref? (none)
|
||||
|
||||
Optional fields may be absent or explicitly ``none``. Selected String and
|
||||
Enumeration constructors still require ``minimum_length`` and ``literals``,
|
||||
respectively, including zero and an empty list. Selecting a constructor does
|
||||
not merge fields from the default String definition. Unknown fields reject.
|
||||
|
||||
Bounds are inclusive and ordered; an absent bound means unbounded within the
|
||||
represented numeric domain. Integer comparisons MUST NOT convert to binary64.
|
||||
@@ -1840,11 +1852,30 @@ Map order is irrelevant. The writer preserves signed coordinate zero and
|
||||
emits decimals that recover the original binary32 value.
|
||||
|
||||
Canonical output uses LF, four-space indentation, explicit identities,
|
||||
defaults, and JSON-escaped strings. Elements are emitted in UUID order, then
|
||||
and JSON-escaped strings. It omits fields equal to the omission defaults of
|
||||
their actual declaration or selected constructor, as specified in the field
|
||||
tables. This includes an empty project description. It MUST NOT omit a required
|
||||
field merely because its value is zero, false, empty, or ``none``. Elements are emitted in UUID order, then
|
||||
relations; diagrams and their nodes follow, then artifacts, then traces.
|
||||
Fields use deterministic name order. Output ends with LF. Alias values come
|
||||
from the model, never from regenerating display names.
|
||||
|
||||
Omission is context-sensitive: Scenario uses the source default MixedLegacy;
|
||||
BoundaryExchange and CommunicationMean have different orientation defaults;
|
||||
state declarations determine their state-kind discriminator. A nonempty or
|
||||
whitespace-only description remains exact. Present-empty optional strings or
|
||||
collections, unbounded multiplicity versus a finite default, selected operation
|
||||
intent, and negative coordinate zero remain distinct values. A nondefault
|
||||
geometry record retains all required coordinates or dimensions.
|
||||
|
||||
Only specified optional fields inside selected constructors may be omitted.
|
||||
For example, Integer bounds and LegacyPortContract.interface may be absent;
|
||||
String.minimum_length, Signal.items, port-contract payload lists, and historical
|
||||
snapshot name/description fields remain required. A LegacyPortContract record
|
||||
still requires direction. Historical semantic baseline payloads are retained
|
||||
without generic empty/default pruning. Compact output MUST preserve every
|
||||
persistent model field after parsing, including these distinctions.
|
||||
|
||||
Comments and input whitespace are lexical trivia to the canonical writer.
|
||||
Canonical export does not preserve them. The source-project editor and compiler
|
||||
preserve authored comments and whitespace on Apply while inserting missing
|
||||
@@ -1890,11 +1921,19 @@ adjusts to the destination scope; other token spelling remains authored.
|
||||
|
||||
Updates MUST NOT canonicalize surviving declarations in full. Header, field and
|
||||
nested value positions are derived from the existing syntax parser. Unchanged
|
||||
fields retain their exact bytes, including field order, omitted defaults, number
|
||||
fields retain their exact bytes, including field order, explicit and omitted defaults, number
|
||||
and string spelling, comments, indentation and line endings. Changed literals and
|
||||
references replace only the required tokens; a reference with comments between
|
||||
``@`` and its alias retains those comments. A previously omitted default is
|
||||
inserted only if its semantic value changes. New fields use the existing record's
|
||||
``@`` and its alias retains those comments. A previously omitted field is
|
||||
inserted only when needed to express the requested value in its new declaration
|
||||
context. This includes an unchanged value whose omission default changes with
|
||||
the declaration kind. For example, changing BoundaryExchange to CommunicationMean
|
||||
while retaining orientation true requires ``oriented true;`` if previously
|
||||
implicit. Unchanged explicit defaults MUST survive unrelated sibling edits,
|
||||
including persistence-serializer omissions such as ``operation none``,
|
||||
``port_anchors []`` and ``baseline.semantic none``. New fields and value fragments
|
||||
use compact generation while retaining required selected-constructor payloads.
|
||||
New fields use the existing record's
|
||||
indentation and line-ending style without reformatting its other fields.
|
||||
|
||||
A constructor tag change retains shared nested fields in place. Removed fields,
|
||||
@@ -2173,7 +2212,7 @@ Text edits describe a complete candidate and must remove or repair dependencies
|
||||
explicitly before Apply succeeds.
|
||||
|
||||
8.1. Focused diagram removal
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Delete is scoped to the focused diagram and the exact visible selection.
|
||||
Text fields retain their own Delete behavior; diagram shortcuts MUST NOT apply
|
||||
@@ -2294,8 +2333,9 @@ This compatibility applies equally to source and JSON. It adds explicit
|
||||
defaults without changing any prior identity, name, reference, order, geometry,
|
||||
artifact or trace field. The original V2 golden source/JSON in
|
||||
``crates/oa-core/tests/fixtures`` MUST NOT be regenerated from the newer demo;
|
||||
they verify that every old persistent field survives. Canonical output writes
|
||||
the added defaults through explicit migration to the current schema 7.
|
||||
they verify that every old persistent field survives. Canonical output uses
|
||||
the current schema 7 and omits values equal to its specified source defaults;
|
||||
parsing recovers the complete migrated model.
|
||||
|
||||
The V2 adapter preserves every original identity, alias, prose, parent,
|
||||
placement, trace and artifact. Generic ports become Legacy with their original
|
||||
|
||||
@@ -49,6 +49,8 @@ P1 extension to frozen relation interface: FunctionalExchange now has `source_po
|
||||
|
||||
`transitions::counterpart_kinds(source: Kind, direction: TransitionDirection) -> Vec<Kind>` is the shared choice policy for UI and other clients. It returns supported explicit element counterpart kinds in stable `Kind::ALL` order using the same compatibility predicate as transition validation; an empty list means no automatic element mapping.
|
||||
|
||||
`transitions::counterpart_compatible(&Project, source: Id, target: Id, direction: TransitionDirection) -> bool` checks existing-record candidate eligibility: both IDs must exist in the direction's respective layers, belong to the same element/relation category, and have compatible transition kinds. It does not approve properties, dependencies or structural changes; clients still preview and apply a complete plan. Manual trace creation retains its separate policy. The desktop counterpart search uses this predicate, filters by name or `@alias`, and sorts by name then alias and ID. Search is presentation state: it does not replan, change mappings or reviewed field decisions, or enter workflow recovery data. A selected target remains visible when filtered out; invalid or missing recovered targets remain visible with their error and require correction. Multiple traces to the same target count as one inferred counterpart; distinct traced targets require an explicit choice.
|
||||
|
||||
`TransitionRequest {direction:OaToSa|SaToOa, sources:Vec<Id>, scope:SelectedOnly|Required|Subtree, mappings:Vec<TransitionMapping>, fields:Vec<TransitionField>, resolutions:Vec<FieldResolution>, destination_diagram:Option<Id>}`. `TransitionRequest::new(direction,sources)` defaults to selected-only and name/description fields. `TransitionMapping {source,target:Option<Id>,target_kind:Option<Kind>}` selects reuse or explicit creation kind. An explicit mapping with target=None means create a new counterpart even if another trace exists. Without mapping, one existing trace is reused; multiple traces require selection.
|
||||
|
||||
`FieldResolution {source,field:Name|Description,decision:TakeSource|KeepTarget|Value(String)}`. `TransitionPlan {base_fingerprint,request,changes,conflicts,warnings,commands}`. Changes carry source/target IDs, Create/Update/Reuse and before/after field changes. Conflicts have optional source/target/field and message. Conflicts prevent apply. The deterministic fingerprint guards stale plans, is not a security hash, and does not introduce persistent project revision fields. Planned IDs and aliases remain unchanged during apply. Server owns request idempotency/revision storage. Apply is one candidate transaction; Session adoption can use Command::Replace(candidate) for one Undo.
|
||||
|
||||
@@ -1,43 +1,34 @@
|
||||
oa_activity oa_restore_work "Restore safe pressure" id "1b91f8a7-0bdb-514e-b1cd-955658e09927" {
|
||||
description "";
|
||||
parent none;
|
||||
}
|
||||
|
||||
oa_activity oa_assess "Assess pressure alarm" id "61f7bbfb-74a4-5540-9182-2583dfb27a49" {
|
||||
description "";
|
||||
parent @oa_restore_work;
|
||||
}
|
||||
|
||||
oa_activity oa_confirm "Confirm safe pressure" id "727c0b5c-8db6-5eb7-83d6-735519c5949f" {
|
||||
description "";
|
||||
parent @oa_restore_work;
|
||||
}
|
||||
|
||||
oa_activity oa_repair "Repair valve in the field" id "a21465af-05f3-5fb5-8983-de26932422fb" {
|
||||
description "";
|
||||
parent @oa_restore_work;
|
||||
}
|
||||
|
||||
oa_activity oa_reset "Reset valve remotely" id "d0c8be87-d238-503a-a549-7558a210507f" {
|
||||
description "";
|
||||
parent @oa_restore_work;
|
||||
}
|
||||
|
||||
capability_involvement oa_assessment_support "Capability involvement: Assess an alarm" id "ffdbfcd0-d423-5707-a880-0dfbe1400f72" {
|
||||
description "";
|
||||
source @oa_assess;
|
||||
target @oa_triage;
|
||||
}
|
||||
|
||||
interaction oa_repair_request "Request field repair" id "cf423ea2-8b2e-5a27-93d4-5e8542d9db8d" {
|
||||
description "";
|
||||
items [@oa_repair_operation];
|
||||
source @oa_assess;
|
||||
target @oa_repair;
|
||||
}
|
||||
|
||||
interaction oa_repair_report "Report repair result" id "b436d0b3-c824-5a96-af6b-2c269816d1cc" {
|
||||
description "";
|
||||
items [@oa_repair_operation];
|
||||
source @oa_repair;
|
||||
target @oa_confirm;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
oa_process oa_assessment_procedure "Assess an alarm procedure" id "022d3cfa-bbaf-5e13-924a-8e55099b8745" {
|
||||
description "";
|
||||
parent none;
|
||||
steps [@oa_assessment_action];
|
||||
}
|
||||
|
||||
@@ -8,17 +6,12 @@ oa_occurrence oa_assessment_action "Assess pressure alarm" id "942975b8-0bef-5ae
|
||||
body {
|
||||
Action {
|
||||
action @oa_assess;
|
||||
lifeline none;
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @oa_assessment_procedure;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
chain_involvement oa_assessment_delivery "Chain involvement: Assess an alarm" id "628cb4fc-fe63-5f6b-b364-6268f26b5740" {
|
||||
description "";
|
||||
source @oa_assessment_procedure;
|
||||
target @oa_triage;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
oa_process oa_field_procedure "Field repair procedure" id "5d3af417-244c-54bb-9588-16daedda4e1f" {
|
||||
description "";
|
||||
parent none;
|
||||
steps [@oa_request_step, @oa_repair_step, @oa_report_step];
|
||||
}
|
||||
|
||||
@@ -8,59 +6,35 @@ oa_occurrence oa_repair_step "Repair the valve" id "c59432fe-6092-5ee0-908d-9dbf
|
||||
body {
|
||||
Action {
|
||||
action @oa_repair;
|
||||
lifeline none;
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @oa_field_procedure;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
oa_occurrence oa_report_step "Report the measured result" id "df2fd3f8-6bc2-5380-a13f-36643ed35ac7" {
|
||||
body {
|
||||
Exchange {
|
||||
exchange @oa_repair_report;
|
||||
message_kind Asynchronous;
|
||||
receiver none;
|
||||
reply_to none;
|
||||
sender none;
|
||||
values [];
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @oa_field_procedure;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
oa_occurrence oa_request_step "Request the field repair" id "5ba8079c-2dbe-5e98-ae82-2b60c6020a02" {
|
||||
body {
|
||||
Exchange {
|
||||
exchange @oa_repair_request;
|
||||
message_kind Asynchronous;
|
||||
receiver none;
|
||||
reply_to none;
|
||||
sender none;
|
||||
values [];
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @oa_field_procedure;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
sequence_link oa_repair_then_report "Sequence link: Report the measured result" id "c8196d35-2168-5fef-8b03-ba0d139bc00f" {
|
||||
description "";
|
||||
guard "";
|
||||
source @oa_repair_step;
|
||||
target @oa_report_step;
|
||||
}
|
||||
|
||||
sequence_link oa_request_then_repair "Sequence link: Repair the valve" id "3f54d890-4ca5-5e36-873b-abb5c92d578c" {
|
||||
description "";
|
||||
guard "";
|
||||
source @oa_request_step;
|
||||
target @oa_repair_step;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
oa_process oa_response "Handle a pressure alarm" id "58bae2b0-f560-50f2-818d-19091d22d18a" {
|
||||
description "";
|
||||
parent none;
|
||||
steps [@oa_assess_step, @oa_decision, @oa_reset_step, @oa_use_field, @oa_merge, @oa_confirm_step];
|
||||
}
|
||||
|
||||
@@ -10,23 +8,16 @@ oa_occurrence oa_assess_step "Assess the alarm" id "0b07b9ed-492d-57f6-b0b1-ce8a
|
||||
sequence @oa_assessment_procedure;
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @oa_response;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
oa_occurrence oa_confirm_step "Confirm safe pressure" id "e00594a3-6122-5a6b-b3c2-ecb5506c741d" {
|
||||
body {
|
||||
Action {
|
||||
action @oa_confirm;
|
||||
lifeline none;
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @oa_response;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
oa_occurrence oa_decision "Can a remote reset safely recover pressure?" id "b280c6ca-e5f9-5424-a5c1-5676e01c2371" {
|
||||
@@ -35,10 +26,7 @@ oa_occurrence oa_decision "Can a remote reset safely recover pressure?" id "b280
|
||||
kind Choice;
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @oa_response;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
oa_occurrence oa_merge "Recovery path completed" id "6b945a6d-6aa0-5dd3-96e6-2e8d757d167d" {
|
||||
@@ -47,23 +35,16 @@ oa_occurrence oa_merge "Recovery path completed" id "6b945a6d-6aa0-5dd3-96e6-2e8
|
||||
kind Merge;
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @oa_response;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
oa_occurrence oa_reset_step "Try the authorized remote reset" id "d2026fc2-74a8-5e22-9942-ff3eb68a3677" {
|
||||
body {
|
||||
Action {
|
||||
action @oa_reset;
|
||||
lifeline none;
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @oa_response;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
oa_occurrence oa_use_field "Use the field repair procedure" id "fc6763bf-a534-54bf-afcd-57ab285ff07d" {
|
||||
@@ -72,56 +53,42 @@ oa_occurrence oa_use_field "Use the field repair procedure" id "fc6763bf-a534-54
|
||||
sequence @oa_field_procedure;
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @oa_response;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
sequence_link oa_assess_then_decide "Continue" id "109e2d6b-7e86-5934-93a5-fab56eced84e" {
|
||||
description "";
|
||||
guard "";
|
||||
source @oa_assess_step;
|
||||
target @oa_decision;
|
||||
}
|
||||
|
||||
sequence_link oa_field_path "else" id "c1adbbd0-a762-5ee3-8b9a-5c18af377771" {
|
||||
description "";
|
||||
guard "else";
|
||||
source @oa_decision;
|
||||
target @oa_use_field;
|
||||
}
|
||||
|
||||
sequence_link oa_remote_path "remote reset is authorized" id "76218d89-68fd-531d-b35b-90c5b769a5ef" {
|
||||
description "";
|
||||
guard "remote reset is authorized";
|
||||
source @oa_decision;
|
||||
target @oa_reset_step;
|
||||
}
|
||||
|
||||
sequence_link oa_confirm_path "Continue" id "229ee94f-d147-50f6-9769-b5bd0c03a084" {
|
||||
description "";
|
||||
guard "";
|
||||
source @oa_merge;
|
||||
target @oa_confirm_step;
|
||||
}
|
||||
|
||||
sequence_link oa_remote_merge "Continue" id "bac43d3b-910b-5bdc-be9b-f78cf973e1ed" {
|
||||
description "";
|
||||
guard "";
|
||||
source @oa_reset_step;
|
||||
target @oa_merge;
|
||||
}
|
||||
|
||||
chain_involvement oa_response_delivery "Chain involvement: Restore a safe supply" id "c7c248f8-28b8-55be-9023-0991d44021e3" {
|
||||
description "";
|
||||
source @oa_response;
|
||||
target @oa_restore;
|
||||
}
|
||||
|
||||
sequence_link oa_field_merge "Continue" id "c8593670-8906-530f-b6a2-3ada77a9c219" {
|
||||
description "";
|
||||
guard "";
|
||||
source @oa_use_field;
|
||||
target @oa_merge;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
oa_scenario oa_conversation "Repair request and measured reply" id "20472233-5ce6-5ffe-b4ad-0bdbccf3e423" {
|
||||
context @oa_restore;
|
||||
description "";
|
||||
parent none;
|
||||
postcondition "The controller has received the measured pressure after repair";
|
||||
precondition "A pressure alarm has been assessed";
|
||||
steps [@oa_call, @oa_reply];
|
||||
@@ -14,23 +12,17 @@ oa_occurrence oa_call "Request valve repair" id "b97437c6-cd7a-5a05-b537-b30a17d
|
||||
exchange @oa_contact;
|
||||
message_kind Call;
|
||||
receiver @oa_responder;
|
||||
reply_to none;
|
||||
sender @oa_caller;
|
||||
values [{
|
||||
expression "420.0";
|
||||
field @oa_pressure_parameter;
|
||||
path [];
|
||||
}];
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @oa_conversation;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
oa_lifeline oa_caller "Controller" id "41859a65-ded2-547b-ab03-14e2fa740cf9" {
|
||||
description "";
|
||||
parent @oa_conversation;
|
||||
represented @oa_control;
|
||||
}
|
||||
@@ -46,18 +38,13 @@ oa_occurrence oa_reply "Return pressure after repair" id "cc2d497a-fd15-5455-9ee
|
||||
values [{
|
||||
expression "320.0";
|
||||
field @oa_pressure_parameter;
|
||||
path [];
|
||||
}];
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @oa_conversation;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
oa_lifeline oa_responder "Technician" id "fc81c5f0-0442-5b88-992b-e5b10f351c59" {
|
||||
description "";
|
||||
parent @oa_conversation;
|
||||
represented @oa_technician;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
oa_scenario oa_full_scenario "Restore supply through field repair" id "d6f881a4-95da-54e9-ad97-1b8965532629" {
|
||||
context @oa_restore;
|
||||
description "";
|
||||
parent none;
|
||||
postcondition "The controller has received the measured pressure after repair";
|
||||
precondition "A pressure alarm has been assessed";
|
||||
steps [@oa_use_conversation];
|
||||
@@ -9,13 +7,11 @@ oa_scenario oa_full_scenario "Restore supply through field repair" id "d6f881a4-
|
||||
}
|
||||
|
||||
oa_lifeline oa_outer_controller "Controller" id "2011b3ac-614b-5a47-8891-7ff81965f425" {
|
||||
description "";
|
||||
parent @oa_full_scenario;
|
||||
represented @oa_control;
|
||||
}
|
||||
|
||||
oa_lifeline oa_outer_technician "Technician" id "fffff882-78fd-5415-928d-c1ef7fac6576" {
|
||||
description "";
|
||||
parent @oa_full_scenario;
|
||||
represented @oa_technician;
|
||||
}
|
||||
@@ -33,9 +29,6 @@ oa_occurrence oa_use_conversation "Use the reviewed repair conversation" id "9ff
|
||||
scenario @oa_conversation;
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @oa_full_scenario;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,80 +1,43 @@
|
||||
oa_state_machine oa_monitor "Pressure supervision" id "1970608f-85d1-5234-ac86-5e2b0df27dc3" {
|
||||
description "";
|
||||
parent @oa_control;
|
||||
}
|
||||
|
||||
oa_region oa_monitor_region "Supervision cycle" id "8412e9c6-96fe-5ed6-a903-1d37ec3c3bf7" {
|
||||
description "";
|
||||
parent @oa_monitor;
|
||||
}
|
||||
|
||||
oa_initial oa_monitor_initial "Start" id "a7ffb869-9dde-5e76-b276-c719c5c143a9" {
|
||||
description "";
|
||||
do_activity "";
|
||||
entry "";
|
||||
exit "";
|
||||
kind Initial;
|
||||
parent @oa_monitor_region;
|
||||
}
|
||||
|
||||
oa_state oa_monitoring "Monitoring" id "6cbde0d8-9f5f-510a-a8c1-e29cb99ad4ae" {
|
||||
description "";
|
||||
do_activity "";
|
||||
entry "";
|
||||
exit "";
|
||||
kind State;
|
||||
parent @oa_monitor_region;
|
||||
}
|
||||
|
||||
oa_state oa_waiting "Awaiting repair" id "58c15833-6e75-523a-b57f-a88f19f6d672" {
|
||||
description "";
|
||||
do_activity "";
|
||||
entry "";
|
||||
exit "";
|
||||
kind State;
|
||||
parent @oa_monitor_region;
|
||||
}
|
||||
|
||||
state_transition oa_start_monitoring "State transition: Monitoring" id "2b00270c-79a5-52a5-af96-33346bea531b" {
|
||||
description "";
|
||||
effect "";
|
||||
effects [];
|
||||
guard "";
|
||||
source @oa_monitor_initial;
|
||||
target @oa_monitoring;
|
||||
transition_kind External;
|
||||
trigger "";
|
||||
triggers [];
|
||||
}
|
||||
|
||||
state_transition oa_alarm_transition "State transition: Awaiting repair" id "fbde9d5e-c7e9-55db-a2be-497345c491b3" {
|
||||
description "";
|
||||
effect "";
|
||||
effects [];
|
||||
guard "";
|
||||
source @oa_monitoring;
|
||||
target @oa_waiting;
|
||||
transition_kind External;
|
||||
trigger "";
|
||||
triggers [@oa_high_pressure];
|
||||
}
|
||||
|
||||
state_action oa_assess_on_entry "Assess when repair is needed" id "7fa37e63-db65-5aa7-88f2-31ff4347a787" {
|
||||
description "";
|
||||
phase Entry;
|
||||
source @oa_waiting;
|
||||
target @oa_assess;
|
||||
}
|
||||
|
||||
state_transition oa_normal_transition "State transition: Monitoring" id "21fe5e8e-cbc2-5dad-b3e8-1df46e167d97" {
|
||||
description "";
|
||||
effect "";
|
||||
effects [];
|
||||
guard "repair confirmed and pressure is safe";
|
||||
source @oa_waiting;
|
||||
target @oa_monitoring;
|
||||
transition_kind External;
|
||||
trigger "";
|
||||
triggers [];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
oa_mission oa_mission "Maintain safe water supply" id "c9c61f50-06e0-5d17-973d-dcc57cd5cc90" {
|
||||
description "";
|
||||
parent none;
|
||||
}
|
||||
|
||||
oa_capability oa_restore "Restore a safe supply" id "3dfee23a-148b-5cec-ad2d-b774e84ea5bb" {
|
||||
description "";
|
||||
parent none;
|
||||
}
|
||||
|
||||
oa_capability oa_triage "Assess an alarm" id "6a30b763-cc51-546d-907f-5c63657cfd03" {
|
||||
description "";
|
||||
parent none;
|
||||
}
|
||||
|
||||
mission_capability oa_mission_delivery "Mission capability: Restore a safe supply" id "1f8b1898-65f6-5360-99cf-b78a71bd50ec" {
|
||||
description "";
|
||||
source @oa_mission;
|
||||
target @oa_restore;
|
||||
}
|
||||
|
||||
capability_include oa_include_triage "Capability include: Assess an alarm" id "e21d9a54-ae8e-5618-a4f8-67ee22b92638" {
|
||||
description "";
|
||||
source @oa_restore;
|
||||
target @oa_triage;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
oa_exchange_category oa_contact_category "Field contact" id "19bd3a3a-4d0c-56d5-bf50-a6b43fd81771" {
|
||||
description "";
|
||||
exchanges [@oa_contact];
|
||||
family "Participant";
|
||||
parent none;
|
||||
}
|
||||
|
||||
oa_event oa_high_pressure "Pressure alarm detected" id "65d6b8d6-271a-5c84-8b42-abbe310c955e" {
|
||||
@@ -11,14 +9,9 @@ oa_event oa_high_pressure "Pressure alarm detected" id "65d6b8d6-271a-5c84-8b42-
|
||||
expression "measured_pressure > operating_limit";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent none;
|
||||
}
|
||||
|
||||
oa_exchange_category oa_repair_traffic "Repair work exchanges" id "9349b353-53f2-56f8-b571-a05b09335cdf" {
|
||||
description "";
|
||||
exchanges [@oa_repair_request, @oa_repair_report];
|
||||
family "Work";
|
||||
parent none;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,59 +1,46 @@
|
||||
oa_entity oa_control "Control room" id "32aa9940-df6e-5a12-b9bd-6337e2d2ed22" {
|
||||
description "";
|
||||
parent none;
|
||||
}
|
||||
|
||||
oa_actor oa_technician "Field technician" id "e07c3952-4a1a-5cc7-b7d5-03a01101ce49" {
|
||||
description "";
|
||||
parent none;
|
||||
}
|
||||
|
||||
allocation oa_assess_allocation "Allocation: Assess pressure alarm" id "594c61cd-94e0-5c07-890d-edfd520d7df9" {
|
||||
description "";
|
||||
source @oa_control;
|
||||
target @oa_assess;
|
||||
}
|
||||
|
||||
allocation oa_confirm_allocation "Allocation: Confirm safe pressure" id "bfd4326e-ea65-5269-b5e6-e063a355d1e9" {
|
||||
description "";
|
||||
source @oa_control;
|
||||
target @oa_confirm;
|
||||
}
|
||||
|
||||
communication_mean oa_contact "Technician service conversation" id "27b47887-baf0-518b-bf87-f5acbf7d0b93" {
|
||||
description "";
|
||||
items [@oa_repair_operation];
|
||||
oriented false;
|
||||
source @oa_control;
|
||||
target @oa_technician;
|
||||
}
|
||||
|
||||
exchange_allocation oa_report_carriage "Exchange allocation: Report repair result" id "4549d3fa-5ab9-52a9-b4f9-3a78fc9d625a" {
|
||||
description "";
|
||||
source @oa_contact;
|
||||
target @oa_repair_report;
|
||||
}
|
||||
|
||||
exchange_allocation oa_request_carriage "Exchange allocation: Request field repair" id "094b888c-450c-5c4f-971a-7d75db059574" {
|
||||
description "";
|
||||
source @oa_contact;
|
||||
target @oa_repair_request;
|
||||
}
|
||||
|
||||
mission_involvement oa_mission_participant "Mission involvement: Maintain safe water supply" id "0c4c9e58-ff32-5c00-b82a-d408a4be0183" {
|
||||
description "";
|
||||
source @oa_control;
|
||||
target @oa_mission;
|
||||
}
|
||||
|
||||
allocation oa_reset_allocation "Allocation: Reset valve remotely" id "1853df1f-30f4-5b1a-baac-2dcaf73c7b3a" {
|
||||
description "";
|
||||
source @oa_control;
|
||||
target @oa_reset;
|
||||
}
|
||||
|
||||
allocation oa_repair_allocation "Allocation: Repair valve in the field" id "ae53f363-0dec-5a7c-9afd-8fcd1b778ab2" {
|
||||
description "";
|
||||
source @oa_technician;
|
||||
target @oa_repair;
|
||||
}
|
||||
|
||||
@@ -1,35 +1,22 @@
|
||||
oa_data_type oa_pressure "Measured pressure" id "e7824764-5db9-5384-8198-2fbaea2b10b2" {
|
||||
default none;
|
||||
definition {
|
||||
Real {
|
||||
maximum 600.0;
|
||||
minimum 0.0;
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent none;
|
||||
unit "kPa";
|
||||
}
|
||||
|
||||
oa_exchange_item oa_repair_operation "Valve repair" id "d736c7d6-d369-550a-8ba4-dae223e40bb3" {
|
||||
description "";
|
||||
kind Information;
|
||||
mechanism Operation;
|
||||
parent none;
|
||||
supertype none;
|
||||
}
|
||||
|
||||
oa_data_field oa_pressure_parameter "Pressure before / after repair" id "8bc6d0a4-3e87-559b-8bd7-1b679a2db5b7" {
|
||||
default none;
|
||||
description "";
|
||||
direction InOut;
|
||||
maximum 1;
|
||||
minimum 1;
|
||||
parent @oa_repair_operation;
|
||||
storage Value;
|
||||
type_ref {
|
||||
Named @oa_pressure;
|
||||
};
|
||||
unit "";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
sa_functional_chain sa_assessment_procedure "Assess an alarm procedure" id "acd5a018-f559-5298-a40d-1cf6e1353e74" {
|
||||
description "";
|
||||
parent none;
|
||||
steps [@sa_assessment_action];
|
||||
}
|
||||
|
||||
@@ -8,17 +6,12 @@ sa_occurrence sa_assessment_action "Assess pressure alarm" id "bae5b192-5b8e-51c
|
||||
body {
|
||||
Action {
|
||||
action @sa_assess;
|
||||
lifeline none;
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @sa_assessment_procedure;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
chain_involvement sa_assessment_delivery "Chain involvement: Assess an alarm" id "a6d5a6be-fa6e-5fa0-a309-08e87ba40c57" {
|
||||
description "";
|
||||
source @sa_assessment_procedure;
|
||||
target @sa_triage;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
sa_functional_chain sa_field_procedure "Field repair procedure" id "93eb6479-54e9-59cb-ac7b-16912347bfc0" {
|
||||
description "";
|
||||
parent none;
|
||||
steps [@sa_request_step, @sa_repair_step, @sa_report_step];
|
||||
}
|
||||
|
||||
@@ -8,59 +6,35 @@ sa_occurrence sa_repair_step "Repair the valve" id "0f195fea-3493-5ebd-966d-da9e
|
||||
body {
|
||||
Action {
|
||||
action @sa_repair;
|
||||
lifeline none;
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @sa_field_procedure;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
sa_occurrence sa_report_step "Report the measured result" id "ea32e4ec-8bdc-57ea-8a90-7b09ba4f0eac" {
|
||||
body {
|
||||
Exchange {
|
||||
exchange @sa_repair_report;
|
||||
message_kind Asynchronous;
|
||||
receiver none;
|
||||
reply_to none;
|
||||
sender none;
|
||||
values [];
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @sa_field_procedure;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
sa_occurrence sa_request_step "Request the field repair" id "40d94322-baa2-5c81-b261-ff47cdc33782" {
|
||||
body {
|
||||
Exchange {
|
||||
exchange @sa_repair_request;
|
||||
message_kind Asynchronous;
|
||||
receiver none;
|
||||
reply_to none;
|
||||
sender none;
|
||||
values [];
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @sa_field_procedure;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
sequence_link sa_repair_then_report "Sequence link: Report the measured result" id "7fb30b1d-195c-5f0e-a19c-a536f519a899" {
|
||||
description "";
|
||||
guard "";
|
||||
source @sa_repair_step;
|
||||
target @sa_report_step;
|
||||
}
|
||||
|
||||
sequence_link sa_request_then_repair "Sequence link: Repair the valve" id "6a6d968f-26d6-5dbb-b721-5a27d362779a" {
|
||||
description "";
|
||||
guard "";
|
||||
source @sa_request_step;
|
||||
target @sa_repair_step;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
sa_functional_chain sa_response "Handle a pressure alarm" id "5139ce73-56fd-5875-ba3a-e569fc890231" {
|
||||
description "";
|
||||
parent none;
|
||||
steps [@sa_assess_step, @sa_decision, @sa_reset_step, @sa_use_field, @sa_merge, @sa_confirm_step];
|
||||
}
|
||||
|
||||
@@ -10,23 +8,16 @@ sa_occurrence sa_assess_step "Assess the alarm" id "9024243c-ccc1-5edb-8477-2aa1
|
||||
sequence @sa_assessment_procedure;
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @sa_response;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
sa_occurrence sa_confirm_step "Confirm safe pressure" id "47bec4f9-d6d0-5ed1-adca-fa0d960dc416" {
|
||||
body {
|
||||
Action {
|
||||
action @sa_confirm;
|
||||
lifeline none;
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @sa_response;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
sa_occurrence sa_decision "Can a remote reset safely recover pressure?" id "30c8fad9-a49b-5692-9542-3148e76afd7b" {
|
||||
@@ -35,10 +26,7 @@ sa_occurrence sa_decision "Can a remote reset safely recover pressure?" id "30c8
|
||||
kind Choice;
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @sa_response;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
sa_occurrence sa_merge "Recovery path completed" id "4ed0dd9d-c9e0-5652-aedf-5f0ab4328a07" {
|
||||
@@ -47,23 +35,16 @@ sa_occurrence sa_merge "Recovery path completed" id "4ed0dd9d-c9e0-5652-aedf-5f0
|
||||
kind Merge;
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @sa_response;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
sa_occurrence sa_reset_step "Try the authorized remote reset" id "bfa2ef0b-ce68-55c2-a9e5-31ea8a74fce6" {
|
||||
body {
|
||||
Action {
|
||||
action @sa_reset;
|
||||
lifeline none;
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @sa_response;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
sa_occurrence sa_use_field "Use the field repair procedure" id "d5e61c5d-510b-5091-a878-da46f3a9e152" {
|
||||
@@ -72,56 +53,42 @@ sa_occurrence sa_use_field "Use the field repair procedure" id "d5e61c5d-510b-50
|
||||
sequence @sa_field_procedure;
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @sa_response;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
sequence_link sa_assess_then_decide "Continue" id "9d4a131f-36ad-5125-b21f-c3dd033b8c82" {
|
||||
description "";
|
||||
guard "";
|
||||
source @sa_assess_step;
|
||||
target @sa_decision;
|
||||
}
|
||||
|
||||
sequence_link sa_field_path "else" id "1bbd5957-1173-51d8-97e3-ba4f084227ae" {
|
||||
description "";
|
||||
guard "else";
|
||||
source @sa_decision;
|
||||
target @sa_use_field;
|
||||
}
|
||||
|
||||
sequence_link sa_remote_path "remote reset is authorized" id "d8b6710a-e713-5979-934f-e5ca7d155bb4" {
|
||||
description "";
|
||||
guard "remote reset is authorized";
|
||||
source @sa_decision;
|
||||
target @sa_reset_step;
|
||||
}
|
||||
|
||||
sequence_link sa_confirm_path "Continue" id "1e738f27-a3a1-526d-b330-6384088b9689" {
|
||||
description "";
|
||||
guard "";
|
||||
source @sa_merge;
|
||||
target @sa_confirm_step;
|
||||
}
|
||||
|
||||
sequence_link sa_remote_merge "Continue" id "e5279265-8eac-52e8-98dc-571b20475e1f" {
|
||||
description "";
|
||||
guard "";
|
||||
source @sa_reset_step;
|
||||
target @sa_merge;
|
||||
}
|
||||
|
||||
chain_involvement sa_response_delivery "Chain involvement: Restore a safe supply" id "66efc484-53c7-51a3-89e9-422429344b38" {
|
||||
description "";
|
||||
source @sa_response;
|
||||
target @sa_restore;
|
||||
}
|
||||
|
||||
sequence_link sa_field_merge "Continue" id "0f6ac3fb-4ad6-581e-ad14-336472072d5e" {
|
||||
description "";
|
||||
guard "";
|
||||
source @sa_use_field;
|
||||
target @sa_merge;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
sa_scenario sa_conversation "Repair request and measured reply" id "730b27ca-b039-5a3b-8dc6-03418f9061b3" {
|
||||
context @sa_restore;
|
||||
description "";
|
||||
parent none;
|
||||
postcondition "The controller has received the measured pressure after repair";
|
||||
precondition "A pressure alarm has been assessed";
|
||||
steps [@sa_call, @sa_reply];
|
||||
@@ -14,23 +12,17 @@ sa_occurrence sa_call "Request valve repair" id "8bab4783-a098-5984-9e36-493957c
|
||||
exchange @sa_contact;
|
||||
message_kind Call;
|
||||
receiver @sa_responder;
|
||||
reply_to none;
|
||||
sender @sa_caller;
|
||||
values [{
|
||||
expression "420.0";
|
||||
field @sa_pressure_parameter;
|
||||
path [];
|
||||
}];
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @sa_conversation;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
sa_lifeline sa_caller "Controller" id "98c61788-2fee-51f4-8cf9-de48e8c70e9e" {
|
||||
description "";
|
||||
parent @sa_conversation;
|
||||
represented @sa_control;
|
||||
}
|
||||
@@ -46,18 +38,13 @@ sa_occurrence sa_reply "Return pressure after repair" id "4e1c0a56-ce80-5f13-a93
|
||||
values [{
|
||||
expression "320.0";
|
||||
field @sa_pressure_parameter;
|
||||
path [];
|
||||
}];
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @sa_conversation;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
sa_lifeline sa_responder "Technician" id "6249b54a-e396-5338-82b9-89266c55670d" {
|
||||
description "";
|
||||
parent @sa_conversation;
|
||||
represented @sa_technician;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
sa_scenario sa_full_scenario "Restore supply through field repair" id "f746a1cd-ff9f-55c0-b325-01c03d2f4fae" {
|
||||
context @sa_restore;
|
||||
description "";
|
||||
parent none;
|
||||
postcondition "The controller has received the measured pressure after repair";
|
||||
precondition "A pressure alarm has been assessed";
|
||||
steps [@sa_use_conversation];
|
||||
@@ -9,13 +7,11 @@ sa_scenario sa_full_scenario "Restore supply through field repair" id "f746a1cd-
|
||||
}
|
||||
|
||||
sa_lifeline sa_outer_controller "Controller" id "1edfcf9e-9bfa-51f1-80d5-62b8fd8bb023" {
|
||||
description "";
|
||||
parent @sa_full_scenario;
|
||||
represented @sa_control;
|
||||
}
|
||||
|
||||
sa_lifeline sa_outer_technician "Technician" id "155e4fe4-9e3b-5f4e-b886-28abcc3f086b" {
|
||||
description "";
|
||||
parent @sa_full_scenario;
|
||||
represented @sa_technician;
|
||||
}
|
||||
@@ -33,9 +29,6 @@ sa_occurrence sa_use_conversation "Use the reviewed repair conversation" id "1c9
|
||||
scenario @sa_conversation;
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent @sa_full_scenario;
|
||||
postcondition "";
|
||||
precondition "";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,80 +1,43 @@
|
||||
sa_state_machine sa_monitor "Pressure supervision" id "132b3f28-3f88-5327-9868-8c8b645ed412" {
|
||||
description "";
|
||||
parent @sa_control;
|
||||
}
|
||||
|
||||
sa_region sa_monitor_region "Supervision cycle" id "3ab11e24-4140-58fc-b3fa-da5f51c7fe2c" {
|
||||
description "";
|
||||
parent @sa_monitor;
|
||||
}
|
||||
|
||||
sa_initial sa_monitor_initial "Start" id "57eb5348-14d8-560e-996a-8dbf993243e2" {
|
||||
description "";
|
||||
do_activity "";
|
||||
entry "";
|
||||
exit "";
|
||||
kind Initial;
|
||||
parent @sa_monitor_region;
|
||||
}
|
||||
|
||||
sa_state sa_monitoring "Monitoring" id "8af21f9b-c21e-5aae-af0d-0958840f6655" {
|
||||
description "";
|
||||
do_activity "";
|
||||
entry "";
|
||||
exit "";
|
||||
kind State;
|
||||
parent @sa_monitor_region;
|
||||
}
|
||||
|
||||
sa_state sa_waiting "Awaiting repair" id "a6bb10b1-f1e5-5b31-8afd-5bad5a7b6c37" {
|
||||
description "";
|
||||
do_activity "";
|
||||
entry "";
|
||||
exit "";
|
||||
kind State;
|
||||
parent @sa_monitor_region;
|
||||
}
|
||||
|
||||
state_transition sa_start_monitoring "State transition: Monitoring" id "47730532-0372-5cc5-9d6d-0b8953aebdd8" {
|
||||
description "";
|
||||
effect "";
|
||||
effects [];
|
||||
guard "";
|
||||
source @sa_monitor_initial;
|
||||
target @sa_monitoring;
|
||||
transition_kind External;
|
||||
trigger "";
|
||||
triggers [];
|
||||
}
|
||||
|
||||
state_transition sa_alarm_transition "State transition: Awaiting repair" id "13c3bfa4-9254-5ab2-b96f-f65f402adf81" {
|
||||
description "";
|
||||
effect "";
|
||||
effects [];
|
||||
guard "";
|
||||
source @sa_monitoring;
|
||||
target @sa_waiting;
|
||||
transition_kind External;
|
||||
trigger "";
|
||||
triggers [@sa_high_pressure];
|
||||
}
|
||||
|
||||
state_action sa_assess_on_entry "Assess when repair is needed" id "a887f96a-37cb-5891-8070-011fa276e5b5" {
|
||||
description "";
|
||||
phase Entry;
|
||||
source @sa_waiting;
|
||||
target @sa_assess;
|
||||
}
|
||||
|
||||
state_transition sa_normal_transition "State transition: Monitoring" id "6e2c1b47-764c-577a-b325-ff66dd2a2768" {
|
||||
description "";
|
||||
effect "";
|
||||
effects [];
|
||||
guard "repair confirmed and pressure is safe";
|
||||
source @sa_waiting;
|
||||
target @sa_monitoring;
|
||||
transition_kind External;
|
||||
trigger "";
|
||||
triggers [];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
sa_mission sa_mission "Maintain safe water supply" id "5ad034b1-084d-5273-9e70-971671766132" {
|
||||
description "";
|
||||
parent none;
|
||||
}
|
||||
|
||||
sa_capability sa_restore "Restore a safe supply" id "81259051-370a-512d-a3b5-6420a6f933b6" {
|
||||
description "";
|
||||
parent none;
|
||||
}
|
||||
|
||||
sa_capability sa_triage "Assess an alarm" id "bad8fa7c-f634-55fd-98d3-e1eb2da05d8f" {
|
||||
description "";
|
||||
parent none;
|
||||
}
|
||||
|
||||
mission_capability sa_mission_delivery "Mission capability: Restore a safe supply" id "a0902e0f-3d18-576a-9aca-975a136dfdd7" {
|
||||
description "";
|
||||
source @sa_mission;
|
||||
target @sa_restore;
|
||||
}
|
||||
|
||||
capability_include sa_include_triage "Capability include: Assess an alarm" id "f3fe7690-734a-5877-8b57-a6599a4033c4" {
|
||||
description "";
|
||||
source @sa_restore;
|
||||
target @sa_triage;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
sa_function sa_audit_thresholds "Audit pressure thresholds" id "4149a872-3440-59de-9879-8981e78d27ab" {
|
||||
description "Deliberately unfinished: assigned to the service, but no behavior context yet. Model checks should expose this one work_without_behavior_context warning.";
|
||||
parent none;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
sa_function sa_restore_work "Restore safe pressure" id "58867e7d-916e-5ebc-9e4c-8dbed1314be3" {
|
||||
description "";
|
||||
parent none;
|
||||
}
|
||||
|
||||
sa_function sa_assess "Assess pressure alarm" id "dddae020-a444-5d48-acc8-139d2696a672" {
|
||||
description "";
|
||||
parent @sa_restore_work;
|
||||
}
|
||||
|
||||
@@ -14,13 +11,10 @@ sa_port sa_repair_request_out "Request field repair output" id "24a6850e-b819-57
|
||||
outgoing_items [@sa_repair_operation];
|
||||
};
|
||||
};
|
||||
description "";
|
||||
legacy none;
|
||||
parent @sa_assess;
|
||||
}
|
||||
|
||||
sa_function sa_confirm "Confirm safe pressure" id "d75eb3fa-a27d-52d5-8782-8b342b3865f6" {
|
||||
description "";
|
||||
parent @sa_restore_work;
|
||||
}
|
||||
|
||||
@@ -30,13 +24,10 @@ sa_port sa_repair_report_in "Report repair result input" id "391af7c4-b07d-53a7-
|
||||
incoming_items [@sa_repair_operation];
|
||||
};
|
||||
};
|
||||
description "";
|
||||
legacy none;
|
||||
parent @sa_confirm;
|
||||
}
|
||||
|
||||
sa_function sa_repair "Repair valve in the field" id "9497e10f-04c4-5586-8738-2464a6f00bd4" {
|
||||
description "";
|
||||
parent @sa_restore_work;
|
||||
}
|
||||
|
||||
@@ -46,8 +37,6 @@ sa_port sa_repair_report_out "Report repair result output" id "994faed7-dbc8-5b9
|
||||
outgoing_items [@sa_repair_operation];
|
||||
};
|
||||
};
|
||||
description "";
|
||||
legacy none;
|
||||
parent @sa_repair;
|
||||
}
|
||||
|
||||
@@ -57,24 +46,19 @@ sa_port sa_repair_request_in "Request field repair input" id "d2113fb0-a531-54ef
|
||||
incoming_items [@sa_repair_operation];
|
||||
};
|
||||
};
|
||||
description "";
|
||||
legacy none;
|
||||
parent @sa_repair;
|
||||
}
|
||||
|
||||
sa_function sa_reset "Reset valve remotely" id "09342ccb-044b-5483-ac58-87dc3cf7831c" {
|
||||
description "";
|
||||
parent @sa_restore_work;
|
||||
}
|
||||
|
||||
capability_involvement sa_assessment_support "Capability involvement: Assess an alarm" id "93eaad8d-83b7-5980-b9bb-86c9e3f31da4" {
|
||||
description "";
|
||||
source @sa_assess;
|
||||
target @sa_triage;
|
||||
}
|
||||
|
||||
functional_exchange sa_repair_request "Request field repair" id "93474111-f23e-56c1-b61b-b7c2396b91bd" {
|
||||
description "";
|
||||
items [@sa_repair_operation];
|
||||
source @sa_assess;
|
||||
source_port @sa_repair_request_out;
|
||||
@@ -83,7 +67,6 @@ functional_exchange sa_repair_request "Request field repair" id "93474111-f23e-5
|
||||
}
|
||||
|
||||
functional_exchange sa_repair_report "Report repair result" id "684eadc3-e670-5779-a60f-bbc9a5bfb062" {
|
||||
description "";
|
||||
items [@sa_repair_operation];
|
||||
source @sa_repair;
|
||||
source_port @sa_repair_report_out;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
sa_exchange_category sa_contact_category "Field contact" id "781cbe75-410f-5cc5-aeb8-a43491f77b99" {
|
||||
description "";
|
||||
exchanges [@sa_contact];
|
||||
family "Participant";
|
||||
parent none;
|
||||
}
|
||||
|
||||
sa_event sa_high_pressure "Pressure alarm detected" id "01490fa8-2f01-5ac8-9722-d55d45414379" {
|
||||
@@ -11,14 +9,9 @@ sa_event sa_high_pressure "Pressure alarm detected" id "01490fa8-2f01-5ac8-9722-
|
||||
expression "measured_pressure > operating_limit";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent none;
|
||||
}
|
||||
|
||||
sa_exchange_category sa_repair_traffic "Repair work exchanges" id "a9a47e48-100c-57a8-95f0-7fe24da94f94" {
|
||||
description "";
|
||||
exchanges [@sa_repair_request, @sa_repair_report];
|
||||
family "Work";
|
||||
parent none;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
sa_system sa_control "Pressure coordination service" id "435bf87c-73fd-5e4d-978a-4c471bc9eaf0" {
|
||||
description "";
|
||||
parent none;
|
||||
}
|
||||
|
||||
sa_port sa_control_port "Repair service client" id "98d411e7-7c65-5c3f-b543-8bf0f2522ea2" {
|
||||
@@ -11,13 +9,10 @@ sa_port sa_control_port "Repair service client" id "98d411e7-7c65-5c3f-b543-8bf0
|
||||
};
|
||||
};
|
||||
description "Explicitly chosen Standard component port for the repair service.";
|
||||
legacy none;
|
||||
parent @sa_control;
|
||||
}
|
||||
|
||||
sa_actor sa_technician "Field technician" id "c05b9422-b9da-5de5-b21d-07ad23bd2b8a" {
|
||||
description "";
|
||||
parent none;
|
||||
}
|
||||
|
||||
sa_port sa_technician_port "Repair service provider" id "e6d69cbd-99d7-596b-bbe3-dada6118a600" {
|
||||
@@ -28,36 +23,30 @@ sa_port sa_technician_port "Repair service provider" id "e6d69cbd-99d7-596b-bbe3
|
||||
};
|
||||
};
|
||||
description "Explicitly chosen Standard component port for the repair service.";
|
||||
legacy none;
|
||||
parent @sa_technician;
|
||||
}
|
||||
|
||||
allocation sa_assess_allocation "Allocation: Assess pressure alarm" id "9b9356d7-2db2-5a94-a47e-ea892cf02afe" {
|
||||
description "";
|
||||
source @sa_control;
|
||||
target @sa_assess;
|
||||
}
|
||||
|
||||
allocation sa_audit_allocation "Allocation: Audit pressure thresholds" id "1b245065-7242-5d81-8472-aaeb932054c9" {
|
||||
description "";
|
||||
source @sa_control;
|
||||
target @sa_audit_thresholds;
|
||||
}
|
||||
|
||||
allocation sa_confirm_allocation "Allocation: Confirm safe pressure" id "b76bf8de-2fa4-563f-a540-dd30f30e6c49" {
|
||||
description "";
|
||||
source @sa_control;
|
||||
target @sa_confirm;
|
||||
}
|
||||
|
||||
interface_requirement sa_client_requires "Interface requirement: Valve repair service" id "4f85337b-f6eb-533c-a45a-9a5d1d9567ca" {
|
||||
description "";
|
||||
source @sa_control_port;
|
||||
target @sa_repair_interface;
|
||||
}
|
||||
|
||||
boundary_exchange sa_contact "Technician service conversation" id "63009b5f-539e-5914-b630-ec6b7fefb4d5" {
|
||||
description "";
|
||||
items [@sa_repair_operation];
|
||||
oriented false;
|
||||
source @sa_control_port;
|
||||
@@ -65,61 +54,51 @@ boundary_exchange sa_contact "Technician service conversation" id "63009b5f-539e
|
||||
}
|
||||
|
||||
exchange_allocation sa_report_carriage "Exchange allocation: Report repair result" id "ff9897b3-a23a-5994-a5b3-99a5b4d34ceb" {
|
||||
description "";
|
||||
source @sa_contact;
|
||||
target @sa_repair_report;
|
||||
}
|
||||
|
||||
exchange_allocation sa_request_carriage "Exchange allocation: Request field repair" id "4c4c4851-5a3d-5bb9-94df-b19f9cf39819" {
|
||||
description "";
|
||||
source @sa_contact;
|
||||
target @sa_repair_request;
|
||||
}
|
||||
|
||||
port_allocation sa_port_report_in "Port allocation: Report repair result input" id "568c5c01-de21-52a2-83bd-afdddc158085" {
|
||||
description "";
|
||||
source @sa_control_port;
|
||||
target @sa_repair_report_in;
|
||||
}
|
||||
|
||||
port_allocation sa_port_request_out "Port allocation: Request field repair output" id "55ec4dbf-bd17-5b0c-b3a3-95b2194aa8b9" {
|
||||
description "";
|
||||
source @sa_control_port;
|
||||
target @sa_repair_request_out;
|
||||
}
|
||||
|
||||
mission_involvement sa_mission_participant "Mission involvement: Maintain safe water supply" id "1c1e5634-ba8c-591f-b143-90a27cdc6cd2" {
|
||||
description "";
|
||||
source @sa_control;
|
||||
target @sa_mission;
|
||||
}
|
||||
|
||||
allocation sa_reset_allocation "Allocation: Reset valve remotely" id "3cc9ab52-2b00-57fd-8709-aabc75e5d3e3" {
|
||||
description "";
|
||||
source @sa_control;
|
||||
target @sa_reset;
|
||||
}
|
||||
|
||||
allocation sa_repair_allocation "Allocation: Repair valve in the field" id "725f2e1b-aa2c-5615-9b1f-28da178168fa" {
|
||||
description "";
|
||||
source @sa_technician;
|
||||
target @sa_repair;
|
||||
}
|
||||
|
||||
port_allocation sa_port_report_out "Port allocation: Report repair result output" id "e53b7a40-e8a3-5b3f-826d-c2b5c164a84a" {
|
||||
description "";
|
||||
source @sa_technician_port;
|
||||
target @sa_repair_report_out;
|
||||
}
|
||||
|
||||
port_allocation sa_port_request_in "Port allocation: Request field repair input" id "09eaa992-4cdf-5ea3-b748-f10fd74b8cba" {
|
||||
description "";
|
||||
source @sa_technician_port;
|
||||
target @sa_repair_request_in;
|
||||
}
|
||||
|
||||
interface_provision sa_provider_provides "Interface provision: Valve repair service" id "55f89fbf-aa3c-5ae1-95ef-ac676f9c1ada" {
|
||||
description "";
|
||||
source @sa_technician_port;
|
||||
target @sa_repair_interface;
|
||||
}
|
||||
|
||||
@@ -1,48 +1,30 @@
|
||||
sa_data_type sa_pressure "Measured pressure" id "87d0a95b-af1b-5352-b753-e4c6533671a5" {
|
||||
default none;
|
||||
definition {
|
||||
Real {
|
||||
maximum 600.0;
|
||||
minimum 0.0;
|
||||
};
|
||||
};
|
||||
description "";
|
||||
parent none;
|
||||
unit "kPa";
|
||||
}
|
||||
|
||||
sa_interface sa_repair_interface "Valve repair service" id "a4b8942e-a975-5f88-ad14-8f38c345baac" {
|
||||
description "";
|
||||
parent none;
|
||||
supertype none;
|
||||
}
|
||||
|
||||
sa_exchange_item sa_repair_operation "Valve repair" id "a962fc5e-3ab0-5296-8b0d-fd84633f9218" {
|
||||
description "";
|
||||
kind Information;
|
||||
mechanism Operation;
|
||||
parent none;
|
||||
supertype none;
|
||||
}
|
||||
|
||||
sa_data_field sa_pressure_parameter "Pressure before / after repair" id "bb12ba5d-aab5-5a1b-99de-c767f5365b57" {
|
||||
default none;
|
||||
description "";
|
||||
direction InOut;
|
||||
maximum 1;
|
||||
minimum 1;
|
||||
parent @sa_repair_operation;
|
||||
storage Value;
|
||||
type_ref {
|
||||
Named @sa_pressure;
|
||||
};
|
||||
unit "";
|
||||
}
|
||||
|
||||
interface_item_allocation sa_repair_interface_item "Repair operation contract" id "f074e041-23cf-54ad-af9c-f171e30621e7" {
|
||||
acquisition Accept;
|
||||
description "";
|
||||
order 0;
|
||||
source @sa_repair_interface;
|
||||
target @sa_repair_operation;
|
||||
transmission Synchronous;
|
||||
|
||||
@@ -9,7 +9,6 @@ trace trace_oa_alarm_transition "State transition: Awaiting repair ↔ State tra
|
||||
name "State transition: Awaiting repair";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Manual;
|
||||
oa @oa_alarm_transition;
|
||||
sa @sa_alarm_transition;
|
||||
@@ -26,8 +25,6 @@ trace trace_oa_assess "Assess pressure alarm ↔ Assess pressure alarm" id "5309
|
||||
name "Assess pressure alarm";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Refinement;
|
||||
oa @oa_assess;
|
||||
sa @sa_assess;
|
||||
}
|
||||
@@ -43,7 +40,6 @@ trace trace_oa_assess_allocation "Allocation: Assess pressure alarm ↔ Allocati
|
||||
name "Allocation: Assess pressure alarm";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Manual;
|
||||
oa @oa_assess_allocation;
|
||||
sa @sa_assess_allocation;
|
||||
@@ -60,7 +56,6 @@ trace trace_oa_assess_on_entry "Assess when repair is needed ↔ Assess when rep
|
||||
name "Assess when repair is needed";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Manual;
|
||||
oa @oa_assess_on_entry;
|
||||
sa @sa_assess_on_entry;
|
||||
@@ -77,7 +72,6 @@ trace trace_oa_assess_step "Assess the alarm ↔ Assess the alarm" id "e848da41-
|
||||
name "Assess the alarm";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Sequence;
|
||||
oa @oa_assess_step;
|
||||
sa @sa_assess_step;
|
||||
@@ -94,7 +88,6 @@ trace trace_oa_assess_then_decide "Continue ↔ Continue" id "a5c1eb01-a95f-5f7a
|
||||
name "Continue";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Manual;
|
||||
oa @oa_assess_then_decide;
|
||||
sa @sa_assess_then_decide;
|
||||
@@ -111,7 +104,6 @@ trace trace_oa_assessment_action "Assess pressure alarm ↔ Assess pressure alar
|
||||
name "Assess pressure alarm";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Sequence;
|
||||
oa @oa_assessment_action;
|
||||
sa @sa_assessment_action;
|
||||
@@ -128,7 +120,6 @@ trace trace_oa_assessment_delivery "Chain involvement: Assess an alarm ↔ Chain
|
||||
name "Chain involvement: Assess an alarm";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Manual;
|
||||
oa @oa_assessment_delivery;
|
||||
sa @sa_assessment_delivery;
|
||||
@@ -145,7 +136,6 @@ trace trace_oa_assessment_procedure "Assess an alarm procedure ↔ Assess an ala
|
||||
name "Assess an alarm procedure";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Sequence;
|
||||
oa @oa_assessment_procedure;
|
||||
sa @sa_assessment_procedure;
|
||||
@@ -162,7 +152,6 @@ trace trace_oa_assessment_support "Capability involvement: Assess an alarm ↔ C
|
||||
name "Capability involvement: Assess an alarm";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Manual;
|
||||
oa @oa_assessment_support;
|
||||
sa @sa_assessment_support;
|
||||
@@ -179,7 +168,6 @@ trace trace_oa_call "Request valve repair ↔ Request valve repair" id "f0cc86e5
|
||||
name "Request valve repair";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Scenario;
|
||||
oa @oa_call;
|
||||
sa @sa_call;
|
||||
@@ -196,7 +184,6 @@ trace trace_oa_caller "Controller ↔ Controller" id "241dfdac-2450-5704-a4cb-9f
|
||||
name "Controller";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Scenario;
|
||||
oa @oa_caller;
|
||||
sa @sa_caller;
|
||||
@@ -213,8 +200,6 @@ trace trace_oa_confirm "Confirm safe pressure ↔ Confirm safe pressure" id "d77
|
||||
name "Confirm safe pressure";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Refinement;
|
||||
oa @oa_confirm;
|
||||
sa @sa_confirm;
|
||||
}
|
||||
@@ -230,7 +215,6 @@ trace trace_oa_confirm_allocation "Allocation: Confirm safe pressure ↔ Allocat
|
||||
name "Allocation: Confirm safe pressure";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Manual;
|
||||
oa @oa_confirm_allocation;
|
||||
sa @sa_confirm_allocation;
|
||||
@@ -247,7 +231,6 @@ trace trace_oa_confirm_path "Continue ↔ Continue" id "a106d183-c239-5c9f-b1f1-
|
||||
name "Continue";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Manual;
|
||||
oa @oa_confirm_path;
|
||||
sa @sa_confirm_path;
|
||||
@@ -264,7 +247,6 @@ trace trace_oa_confirm_step "Confirm safe pressure ↔ Confirm safe pressure" id
|
||||
name "Confirm safe pressure";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Sequence;
|
||||
oa @oa_confirm_step;
|
||||
sa @sa_confirm_step;
|
||||
@@ -281,7 +263,6 @@ trace trace_oa_contact "Technician service conversation ↔ Technician service c
|
||||
name "Technician service conversation";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Exchange;
|
||||
oa @oa_contact;
|
||||
sa @sa_contact;
|
||||
@@ -298,7 +279,6 @@ trace trace_oa_contact_category "Field contact ↔ Field contact" id "e96ff119-b
|
||||
name "Field contact";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Manual;
|
||||
oa @oa_contact_category;
|
||||
sa @sa_contact_category;
|
||||
@@ -315,7 +295,6 @@ trace trace_oa_control "Control room ↔ Pressure coordination service" id "aba5
|
||||
name "Pressure coordination service";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Participant;
|
||||
oa @oa_control;
|
||||
sa @sa_control;
|
||||
@@ -332,7 +311,6 @@ trace trace_oa_conversation "Repair request and measured reply ↔ Repair reques
|
||||
name "Repair request and measured reply";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Scenario;
|
||||
oa @oa_conversation;
|
||||
sa @sa_conversation;
|
||||
@@ -349,7 +327,6 @@ trace trace_oa_decision "Can a remote reset safely recover pressure? ↔ Can a r
|
||||
name "Can a remote reset safely recover pressure?";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Sequence;
|
||||
oa @oa_decision;
|
||||
sa @sa_decision;
|
||||
@@ -366,7 +343,6 @@ trace trace_oa_field_merge "Continue ↔ Continue" id "d32dd433-9c9e-5b57-9355-2
|
||||
name "Continue";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Manual;
|
||||
oa @oa_field_merge;
|
||||
sa @sa_field_merge;
|
||||
@@ -383,7 +359,6 @@ trace trace_oa_field_path "else ↔ else" id "2e849e31-35aa-5aef-a989-26948038e5
|
||||
name "else";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Manual;
|
||||
oa @oa_field_path;
|
||||
sa @sa_field_path;
|
||||
@@ -400,7 +375,6 @@ trace trace_oa_field_procedure "Field repair procedure ↔ Field repair procedur
|
||||
name "Field repair procedure";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Sequence;
|
||||
oa @oa_field_procedure;
|
||||
sa @sa_field_procedure;
|
||||
@@ -417,7 +391,6 @@ trace trace_oa_full_scenario "Restore supply through field repair ↔ Restore su
|
||||
name "Restore supply through field repair";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Scenario;
|
||||
oa @oa_full_scenario;
|
||||
sa @sa_full_scenario;
|
||||
@@ -434,7 +407,6 @@ trace trace_oa_high_pressure "Pressure alarm detected ↔ Pressure alarm detecte
|
||||
name "Pressure alarm detected";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind State;
|
||||
oa @oa_high_pressure;
|
||||
sa @sa_high_pressure;
|
||||
@@ -451,7 +423,6 @@ trace trace_oa_include_triage "Capability include: Assess an alarm ↔ Capabilit
|
||||
name "Capability include: Assess an alarm";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Manual;
|
||||
oa @oa_include_triage;
|
||||
sa @sa_include_triage;
|
||||
@@ -468,7 +439,6 @@ trace trace_oa_merge "Recovery path completed ↔ Recovery path completed" id "0
|
||||
name "Recovery path completed";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Sequence;
|
||||
oa @oa_merge;
|
||||
sa @sa_merge;
|
||||
@@ -485,7 +455,6 @@ trace trace_oa_mission "Maintain safe water supply ↔ Maintain safe water suppl
|
||||
name "Maintain safe water supply";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Mission;
|
||||
oa @oa_mission;
|
||||
sa @sa_mission;
|
||||
@@ -502,7 +471,6 @@ trace trace_oa_mission_delivery "Mission capability: Restore a safe supply ↔ M
|
||||
name "Mission capability: Restore a safe supply";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Manual;
|
||||
oa @oa_mission_delivery;
|
||||
sa @sa_mission_delivery;
|
||||
@@ -519,7 +487,6 @@ trace trace_oa_mission_participant "Mission involvement: Maintain safe water sup
|
||||
name "Mission involvement: Maintain safe water supply";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Manual;
|
||||
oa @oa_mission_participant;
|
||||
sa @sa_mission_participant;
|
||||
@@ -536,7 +503,6 @@ trace trace_oa_monitor "Pressure supervision ↔ Pressure supervision" id "b5386
|
||||
name "Pressure supervision";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind State;
|
||||
oa @oa_monitor;
|
||||
sa @sa_monitor;
|
||||
@@ -553,7 +519,6 @@ trace trace_oa_monitor_initial "Start ↔ Start" id "48d243f0-553f-507e-acb0-947
|
||||
name "Start";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind State;
|
||||
oa @oa_monitor_initial;
|
||||
sa @sa_monitor_initial;
|
||||
@@ -570,7 +535,6 @@ trace trace_oa_monitor_region "Supervision cycle ↔ Supervision cycle" id "2087
|
||||
name "Supervision cycle";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind State;
|
||||
oa @oa_monitor_region;
|
||||
sa @sa_monitor_region;
|
||||
@@ -587,7 +551,6 @@ trace trace_oa_monitoring "Monitoring ↔ Monitoring" id "12ec320f-2e14-5a5a-8c6
|
||||
name "Monitoring";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind State;
|
||||
oa @oa_monitoring;
|
||||
sa @sa_monitoring;
|
||||
@@ -604,7 +567,6 @@ trace trace_oa_normal_transition "State transition: Monitoring ↔ State transit
|
||||
name "State transition: Monitoring";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Manual;
|
||||
oa @oa_normal_transition;
|
||||
sa @sa_normal_transition;
|
||||
@@ -621,7 +583,6 @@ trace trace_oa_outer_controller "Controller ↔ Controller" id "fd195498-dbba-52
|
||||
name "Controller";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Scenario;
|
||||
oa @oa_outer_controller;
|
||||
sa @sa_outer_controller;
|
||||
@@ -638,7 +599,6 @@ trace trace_oa_outer_technician "Technician ↔ Technician" id "c132a0df-1270-55
|
||||
name "Technician";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Scenario;
|
||||
oa @oa_outer_technician;
|
||||
sa @sa_outer_technician;
|
||||
@@ -655,7 +615,6 @@ trace trace_oa_pressure "Measured pressure ↔ Measured pressure" id "d9d47371-e
|
||||
name "Measured pressure";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Item;
|
||||
oa @oa_pressure;
|
||||
sa @sa_pressure;
|
||||
@@ -672,7 +631,6 @@ trace trace_oa_pressure_parameter "Pressure before / after repair ↔ Pressure b
|
||||
name "Pressure before / after repair";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Item;
|
||||
oa @oa_pressure_parameter;
|
||||
sa @sa_pressure_parameter;
|
||||
@@ -689,7 +647,6 @@ trace trace_oa_remote_merge "Continue ↔ Continue" id "1b788e74-36b2-5c17-a936-
|
||||
name "Continue";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Manual;
|
||||
oa @oa_remote_merge;
|
||||
sa @sa_remote_merge;
|
||||
@@ -706,7 +663,6 @@ trace trace_oa_remote_path "remote reset is authorized ↔ remote reset is autho
|
||||
name "remote reset is authorized";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Manual;
|
||||
oa @oa_remote_path;
|
||||
sa @sa_remote_path;
|
||||
@@ -723,8 +679,6 @@ trace trace_oa_repair "Repair valve in the field ↔ Repair valve in the field"
|
||||
name "Repair valve in the field";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Refinement;
|
||||
oa @oa_repair;
|
||||
sa @sa_repair;
|
||||
}
|
||||
@@ -740,7 +694,6 @@ trace trace_oa_repair_allocation "Allocation: Repair valve in the field ↔ Allo
|
||||
name "Allocation: Repair valve in the field";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Manual;
|
||||
oa @oa_repair_allocation;
|
||||
sa @sa_repair_allocation;
|
||||
@@ -757,7 +710,6 @@ trace trace_oa_repair_operation "Valve repair ↔ Valve repair" id "cf92cedf-ea1
|
||||
name "Valve repair";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Item;
|
||||
oa @oa_repair_operation;
|
||||
sa @sa_repair_operation;
|
||||
@@ -774,7 +726,6 @@ trace trace_oa_repair_report "Report repair result ↔ Report repair result" id
|
||||
name "Report repair result";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Exchange;
|
||||
oa @oa_repair_report;
|
||||
sa @sa_repair_report;
|
||||
@@ -791,7 +742,6 @@ trace trace_oa_repair_request "Request field repair ↔ Request field repair" id
|
||||
name "Request field repair";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Exchange;
|
||||
oa @oa_repair_request;
|
||||
sa @sa_repair_request;
|
||||
@@ -808,7 +758,6 @@ trace trace_oa_repair_step "Repair the valve ↔ Repair the valve" id "ac3aa6b0-
|
||||
name "Repair the valve";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Sequence;
|
||||
oa @oa_repair_step;
|
||||
sa @sa_repair_step;
|
||||
@@ -825,7 +774,6 @@ trace trace_oa_repair_then_report "Sequence link: Report the measured result ↔
|
||||
name "Sequence link: Report the measured result";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Manual;
|
||||
oa @oa_repair_then_report;
|
||||
sa @sa_repair_then_report;
|
||||
@@ -842,7 +790,6 @@ trace trace_oa_repair_traffic "Repair work exchanges ↔ Repair work exchanges"
|
||||
name "Repair work exchanges";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Manual;
|
||||
oa @oa_repair_traffic;
|
||||
sa @sa_repair_traffic;
|
||||
@@ -859,7 +806,6 @@ trace trace_oa_reply "Return pressure after repair ↔ Return pressure after rep
|
||||
name "Return pressure after repair";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Scenario;
|
||||
oa @oa_reply;
|
||||
sa @sa_reply;
|
||||
@@ -876,7 +822,6 @@ trace trace_oa_report_step "Report the measured result ↔ Report the measured r
|
||||
name "Report the measured result";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Sequence;
|
||||
oa @oa_report_step;
|
||||
sa @sa_report_step;
|
||||
@@ -893,7 +838,6 @@ trace trace_oa_request_step "Request the field repair ↔ Request the field repa
|
||||
name "Request the field repair";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Sequence;
|
||||
oa @oa_request_step;
|
||||
sa @sa_request_step;
|
||||
@@ -910,7 +854,6 @@ trace trace_oa_request_then_repair "Sequence link: Repair the valve ↔ Sequence
|
||||
name "Sequence link: Repair the valve";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Manual;
|
||||
oa @oa_request_then_repair;
|
||||
sa @sa_request_then_repair;
|
||||
@@ -927,8 +870,6 @@ trace trace_oa_reset "Reset valve remotely ↔ Reset valve remotely" id "82a5be1
|
||||
name "Reset valve remotely";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Refinement;
|
||||
oa @oa_reset;
|
||||
sa @sa_reset;
|
||||
}
|
||||
@@ -944,7 +885,6 @@ trace trace_oa_reset_allocation "Allocation: Reset valve remotely ↔ Allocation
|
||||
name "Allocation: Reset valve remotely";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Manual;
|
||||
oa @oa_reset_allocation;
|
||||
sa @sa_reset_allocation;
|
||||
@@ -961,7 +901,6 @@ trace trace_oa_reset_step "Try the authorized remote reset ↔ Try the authorize
|
||||
name "Try the authorized remote reset";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Sequence;
|
||||
oa @oa_reset_step;
|
||||
sa @sa_reset_step;
|
||||
@@ -978,7 +917,6 @@ trace trace_oa_responder "Technician ↔ Technician" id "c5051628-b7af-5fb1-a014
|
||||
name "Technician";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Scenario;
|
||||
oa @oa_responder;
|
||||
sa @sa_responder;
|
||||
@@ -995,7 +933,6 @@ trace trace_oa_response "Handle a pressure alarm ↔ Handle a pressure alarm" id
|
||||
name "Handle a pressure alarm";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Sequence;
|
||||
oa @oa_response;
|
||||
sa @sa_response;
|
||||
@@ -1012,7 +949,6 @@ trace trace_oa_response_delivery "Chain involvement: Restore a safe supply ↔ C
|
||||
name "Chain involvement: Restore a safe supply";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Manual;
|
||||
oa @oa_response_delivery;
|
||||
sa @sa_response_delivery;
|
||||
@@ -1029,7 +965,6 @@ trace trace_oa_restore "Restore a safe supply ↔ Restore a safe supply" id "0fb
|
||||
name "Restore a safe supply";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Capability;
|
||||
oa @oa_restore;
|
||||
sa @sa_restore;
|
||||
@@ -1046,8 +981,6 @@ trace trace_oa_restore_work "Restore safe pressure ↔ Restore safe pressure" id
|
||||
name "Restore safe pressure";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Refinement;
|
||||
oa @oa_restore_work;
|
||||
sa @sa_restore_work;
|
||||
}
|
||||
@@ -1063,7 +996,6 @@ trace trace_oa_start_monitoring "State transition: Monitoring ↔ State transiti
|
||||
name "State transition: Monitoring";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Manual;
|
||||
oa @oa_start_monitoring;
|
||||
sa @sa_start_monitoring;
|
||||
@@ -1080,7 +1012,6 @@ trace trace_oa_technician "Field technician ↔ Field technician" id "d47d3c36-5
|
||||
name "Field technician";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Participant;
|
||||
oa @oa_technician;
|
||||
sa @sa_technician;
|
||||
@@ -1097,7 +1028,6 @@ trace trace_oa_triage "Assess an alarm ↔ Assess an alarm" id "6acd072d-431e-58
|
||||
name "Assess an alarm";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Capability;
|
||||
oa @oa_triage;
|
||||
sa @sa_triage;
|
||||
@@ -1114,7 +1044,6 @@ trace trace_oa_use_conversation "Use the reviewed repair conversation ↔ Use th
|
||||
name "Use the reviewed repair conversation";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Scenario;
|
||||
oa @oa_use_conversation;
|
||||
sa @sa_use_conversation;
|
||||
@@ -1131,7 +1060,6 @@ trace trace_oa_use_field "Use the field repair procedure ↔ Use the field repai
|
||||
name "Use the field repair procedure";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind Sequence;
|
||||
oa @oa_use_field;
|
||||
sa @sa_use_field;
|
||||
@@ -1148,7 +1076,6 @@ trace trace_oa_waiting "Awaiting repair ↔ Awaiting repair" id "0754be23-e4b8-5
|
||||
name "Awaiting repair";
|
||||
};
|
||||
};
|
||||
description "";
|
||||
kind State;
|
||||
oa @oa_waiting;
|
||||
sa @sa_waiting;
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
diagram oa_allocation_view "1 · OA responsibility and repair traffic" id "103ea62c-ac56-5827-94e7-cf09bca96ccc" {
|
||||
description "Teaching view: positions and filters do not change ownership or exchange identities.";
|
||||
layer Oa;
|
||||
projection Allocation;
|
||||
routes [];
|
||||
view_options {
|
||||
coverage "All";
|
||||
exchange_labels "Name";
|
||||
hidden_edges [];
|
||||
participant_categories "All";
|
||||
relation_kinds "All";
|
||||
work_categories "All";
|
||||
work_contexts "All";
|
||||
};
|
||||
}
|
||||
|
||||
node oa_allocation_view_oa_assess id "5ae1c46d-59a3-50fd-8cc3-b292df21f951" {
|
||||
collapsed false;
|
||||
diagram @oa_allocation_view;
|
||||
element @oa_assess;
|
||||
position {
|
||||
@@ -29,7 +17,6 @@ node oa_allocation_view_oa_assess id "5ae1c46d-59a3-50fd-8cc3-b292df21f951" {
|
||||
}
|
||||
|
||||
node oa_allocation_view_oa_confirm id "edd74060-ac61-5450-a57c-a6577daa5d50" {
|
||||
collapsed false;
|
||||
diagram @oa_allocation_view;
|
||||
element @oa_confirm;
|
||||
position {
|
||||
@@ -43,7 +30,6 @@ node oa_allocation_view_oa_confirm id "edd74060-ac61-5450-a57c-a6577daa5d50" {
|
||||
}
|
||||
|
||||
node oa_allocation_view_oa_control id "3191e05c-1c7f-5f0e-beb0-5f3bded54c48" {
|
||||
collapsed false;
|
||||
diagram @oa_allocation_view;
|
||||
element @oa_control;
|
||||
position {
|
||||
@@ -57,7 +43,6 @@ node oa_allocation_view_oa_control id "3191e05c-1c7f-5f0e-beb0-5f3bded54c48" {
|
||||
}
|
||||
|
||||
node oa_allocation_view_oa_repair id "202ebe37-ddb4-5099-8ce9-ec3e895ab9e9" {
|
||||
collapsed false;
|
||||
diagram @oa_allocation_view;
|
||||
element @oa_repair;
|
||||
position {
|
||||
@@ -71,7 +56,6 @@ node oa_allocation_view_oa_repair id "202ebe37-ddb4-5099-8ce9-ec3e895ab9e9" {
|
||||
}
|
||||
|
||||
node oa_allocation_view_oa_reset id "7066f78c-9736-5349-b94a-0587331b980e" {
|
||||
collapsed false;
|
||||
diagram @oa_allocation_view;
|
||||
element @oa_reset;
|
||||
position {
|
||||
@@ -85,7 +69,6 @@ node oa_allocation_view_oa_reset id "7066f78c-9736-5349-b94a-0587331b980e" {
|
||||
}
|
||||
|
||||
node oa_allocation_view_oa_technician id "d27f28ef-f10a-5355-9a05-6f2fd39b7de6" {
|
||||
collapsed false;
|
||||
diagram @oa_allocation_view;
|
||||
element @oa_technician;
|
||||
position {
|
||||
|
||||
@@ -2,20 +2,9 @@ diagram sa_allocation_view "2 · SA service boundary and functions" id "35d3c24b
|
||||
description "Teaching view: positions and filters do not change ownership or exchange identities.";
|
||||
layer Sa;
|
||||
projection Allocation;
|
||||
routes [];
|
||||
view_options {
|
||||
coverage "All";
|
||||
exchange_labels "Name";
|
||||
hidden_edges [];
|
||||
participant_categories "All";
|
||||
relation_kinds "All";
|
||||
work_categories "All";
|
||||
work_contexts "All";
|
||||
};
|
||||
}
|
||||
|
||||
node sa_allocation_view_sa_assess id "60f6ad74-b69e-5aef-b832-f1bd60b903ae" {
|
||||
collapsed false;
|
||||
diagram @sa_allocation_view;
|
||||
element @sa_assess;
|
||||
position {
|
||||
@@ -29,7 +18,6 @@ node sa_allocation_view_sa_assess id "60f6ad74-b69e-5aef-b832-f1bd60b903ae" {
|
||||
}
|
||||
|
||||
node sa_allocation_view_sa_confirm id "7e3ba36f-bc68-5520-bdb9-77083bb7bba2" {
|
||||
collapsed false;
|
||||
diagram @sa_allocation_view;
|
||||
element @sa_confirm;
|
||||
position {
|
||||
@@ -43,7 +31,6 @@ node sa_allocation_view_sa_confirm id "7e3ba36f-bc68-5520-bdb9-77083bb7bba2" {
|
||||
}
|
||||
|
||||
node sa_allocation_view_sa_control id "8345c76a-8d85-5c31-902e-ec1cd33ed4a3" {
|
||||
collapsed false;
|
||||
diagram @sa_allocation_view;
|
||||
element @sa_control;
|
||||
position {
|
||||
@@ -57,7 +44,6 @@ node sa_allocation_view_sa_control id "8345c76a-8d85-5c31-902e-ec1cd33ed4a3" {
|
||||
}
|
||||
|
||||
node sa_allocation_view_sa_repair id "34630a18-41b0-5fdd-bff4-ee86e93e8651" {
|
||||
collapsed false;
|
||||
diagram @sa_allocation_view;
|
||||
element @sa_repair;
|
||||
position {
|
||||
@@ -71,7 +57,6 @@ node sa_allocation_view_sa_repair id "34630a18-41b0-5fdd-bff4-ee86e93e8651" {
|
||||
}
|
||||
|
||||
node sa_allocation_view_sa_reset id "73406244-ebd7-5995-86c7-e17c73c50b32" {
|
||||
collapsed false;
|
||||
diagram @sa_allocation_view;
|
||||
element @sa_reset;
|
||||
position {
|
||||
@@ -85,7 +70,6 @@ node sa_allocation_view_sa_reset id "73406244-ebd7-5995-86c7-e17c73c50b32" {
|
||||
}
|
||||
|
||||
node sa_allocation_view_sa_technician id "f7225d1c-e506-505d-bb20-916950269d4b" {
|
||||
collapsed false;
|
||||
diagram @sa_allocation_view;
|
||||
element @sa_technician;
|
||||
position {
|
||||
|
||||
@@ -2,20 +2,9 @@ diagram sa_capability_view "4 · SA mission and included capability" id "3e6ad19
|
||||
description "Teaching view: positions and filters do not change ownership or exchange identities.";
|
||||
layer Sa;
|
||||
projection Capabilities;
|
||||
routes [];
|
||||
view_options {
|
||||
coverage "All";
|
||||
exchange_labels "Name";
|
||||
hidden_edges [];
|
||||
participant_categories "All";
|
||||
relation_kinds "All";
|
||||
work_categories "All";
|
||||
work_contexts "All";
|
||||
};
|
||||
}
|
||||
|
||||
node sa_capability_view_sa_control id "41ddb68b-bd2b-5f91-82eb-efde01157828" {
|
||||
collapsed false;
|
||||
diagram @sa_capability_view;
|
||||
element @sa_control;
|
||||
position {
|
||||
@@ -29,7 +18,6 @@ node sa_capability_view_sa_control id "41ddb68b-bd2b-5f91-82eb-efde01157828" {
|
||||
}
|
||||
|
||||
node sa_capability_view_sa_mission id "4b814335-bf98-5b68-85d5-ea549766517a" {
|
||||
collapsed false;
|
||||
diagram @sa_capability_view;
|
||||
element @sa_mission;
|
||||
position {
|
||||
@@ -43,7 +31,6 @@ node sa_capability_view_sa_mission id "4b814335-bf98-5b68-85d5-ea549766517a" {
|
||||
}
|
||||
|
||||
node sa_capability_view_sa_restore id "d3bc13f9-7726-5280-83fe-63f391c45afb" {
|
||||
collapsed false;
|
||||
diagram @sa_capability_view;
|
||||
element @sa_restore;
|
||||
position {
|
||||
@@ -57,7 +44,6 @@ node sa_capability_view_sa_restore id "d3bc13f9-7726-5280-83fe-63f391c45afb" {
|
||||
}
|
||||
|
||||
node sa_capability_view_sa_triage id "069a3fb3-077c-59d8-91da-27b0665143c7" {
|
||||
collapsed false;
|
||||
diagram @sa_capability_view;
|
||||
element @sa_triage;
|
||||
position {
|
||||
|
||||
@@ -2,24 +2,18 @@ diagram sa_service_view "3 · SA service conversation only" id "50af4c9b-93ce-5f
|
||||
description "Teaching view: positions and filters do not change ownership or exchange identities.";
|
||||
layer Sa;
|
||||
projection Allocation;
|
||||
routes [];
|
||||
view_options {
|
||||
coverage "All";
|
||||
exchange_labels "NameAndItems";
|
||||
hidden_edges [];
|
||||
participant_categories {
|
||||
Any [@sa_contact_category];
|
||||
};
|
||||
relation_kinds "All";
|
||||
work_categories {
|
||||
Any [];
|
||||
};
|
||||
work_contexts "All";
|
||||
};
|
||||
}
|
||||
|
||||
node sa_service_view_sa_assess id "439f1e45-e5ed-567c-978b-4c93ef267db1" {
|
||||
collapsed false;
|
||||
diagram @sa_service_view;
|
||||
element @sa_assess;
|
||||
position {
|
||||
@@ -33,7 +27,6 @@ node sa_service_view_sa_assess id "439f1e45-e5ed-567c-978b-4c93ef267db1" {
|
||||
}
|
||||
|
||||
node sa_service_view_sa_confirm id "6f138672-149b-5f88-ab07-7fc413a42262" {
|
||||
collapsed false;
|
||||
diagram @sa_service_view;
|
||||
element @sa_confirm;
|
||||
position {
|
||||
@@ -47,7 +40,6 @@ node sa_service_view_sa_confirm id "6f138672-149b-5f88-ab07-7fc413a42262" {
|
||||
}
|
||||
|
||||
node sa_service_view_sa_control id "f53d4352-b042-56a6-8b3e-c51cd27254f9" {
|
||||
collapsed false;
|
||||
diagram @sa_service_view;
|
||||
element @sa_control;
|
||||
position {
|
||||
@@ -61,7 +53,6 @@ node sa_service_view_sa_control id "f53d4352-b042-56a6-8b3e-c51cd27254f9" {
|
||||
}
|
||||
|
||||
node sa_service_view_sa_repair id "ae36489d-6ec8-5f1c-b603-8b497227efba" {
|
||||
collapsed false;
|
||||
diagram @sa_service_view;
|
||||
element @sa_repair;
|
||||
position {
|
||||
@@ -75,7 +66,6 @@ node sa_service_view_sa_repair id "ae36489d-6ec8-5f1c-b603-8b497227efba" {
|
||||
}
|
||||
|
||||
node sa_service_view_sa_reset id "d52da4e6-40b7-5dbc-84fb-80c443711c69" {
|
||||
collapsed false;
|
||||
diagram @sa_service_view;
|
||||
element @sa_reset;
|
||||
position {
|
||||
@@ -89,7 +79,6 @@ node sa_service_view_sa_reset id "d52da4e6-40b7-5dbc-84fb-80c443711c69" {
|
||||
}
|
||||
|
||||
node sa_service_view_sa_technician id "40210589-69f7-5209-b329-10bb83cf615c" {
|
||||
collapsed false;
|
||||
diagram @sa_service_view;
|
||||
element @sa_technician;
|
||||
position {
|
||||
|
||||
@@ -6,20 +6,9 @@ diagram sa_state_view "5 · SA pressure supervision" id "30d5baf9-6581-5cb6-846a
|
||||
machine @sa_monitor;
|
||||
};
|
||||
};
|
||||
routes [];
|
||||
view_options {
|
||||
coverage "All";
|
||||
exchange_labels "Name";
|
||||
hidden_edges [];
|
||||
participant_categories "All";
|
||||
relation_kinds "All";
|
||||
work_categories "All";
|
||||
work_contexts "All";
|
||||
};
|
||||
}
|
||||
|
||||
node sa_state_view_sa_monitor id "a676fbb9-0833-59a8-98cf-a10d8f4b7434" {
|
||||
collapsed false;
|
||||
diagram @sa_state_view;
|
||||
element @sa_monitor;
|
||||
position {
|
||||
@@ -33,7 +22,6 @@ node sa_state_view_sa_monitor id "a676fbb9-0833-59a8-98cf-a10d8f4b7434" {
|
||||
}
|
||||
|
||||
node sa_state_view_sa_monitor_initial id "5e2326a0-0f6c-5f8d-8a21-fdded18a9281" {
|
||||
collapsed false;
|
||||
diagram @sa_state_view;
|
||||
element @sa_monitor_initial;
|
||||
position {
|
||||
@@ -47,7 +35,6 @@ node sa_state_view_sa_monitor_initial id "5e2326a0-0f6c-5f8d-8a21-fdded18a9281"
|
||||
}
|
||||
|
||||
node sa_state_view_sa_monitor_region id "55a74f91-2025-5001-8db8-9566b1dbe783" {
|
||||
collapsed false;
|
||||
diagram @sa_state_view;
|
||||
element @sa_monitor_region;
|
||||
position {
|
||||
@@ -61,7 +48,6 @@ node sa_state_view_sa_monitor_region id "55a74f91-2025-5001-8db8-9566b1dbe783" {
|
||||
}
|
||||
|
||||
node sa_state_view_sa_monitoring id "b3af7b04-5a45-5b82-ab69-7eada4b876f0" {
|
||||
collapsed false;
|
||||
diagram @sa_state_view;
|
||||
element @sa_monitoring;
|
||||
position {
|
||||
@@ -75,7 +61,6 @@ node sa_state_view_sa_monitoring id "b3af7b04-5a45-5b82-ab69-7eada4b876f0" {
|
||||
}
|
||||
|
||||
node sa_state_view_sa_waiting id "57acbb71-ca86-57fc-ae90-42dad195f0e0" {
|
||||
collapsed false;
|
||||
diagram @sa_state_view;
|
||||
element @sa_waiting;
|
||||
position {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Ordinary-text union: subsequent gates
|
||||
|
||||
Closed automated-gates checkpoint. Preserve the separately published initial evidence package
|
||||
at `../ordinary-text-integration-20260913/`.
|
||||
|
||||
Run `python verify.py` with Python 3.10 or newer. Verification uses only these
|
||||
five files and the standard library: no archived code execution, extraction,
|
||||
network, Git, Cargo, binaries or original-machine paths. All outer text files
|
||||
are UTF-8 with LF before hashing. Raw ZIP contents retain original bytes.
|
||||
`files.json` pins the archive, manifest, verifier and README; the manifest pins
|
||||
each member and maps each capture to exact source inventory/archive objects.
|
||||
Only byte-identical objects are deduplicated, including ZIP timestamps.
|
||||
|
||||
| Capture | Outcome |
|
||||
| --- | --- |
|
||||
| desktop-list-1 | 1,185 actual listed test names |
|
||||
| core-2 | 537 passed, 0 failed, 1 ignored |
|
||||
| desktop-1 | 1,179 passed, 0 failed, 6 ignored |
|
||||
| clippy-1 | Failed items_after_test_module lint; raw logs retained |
|
||||
| clippy-2 | Failed unused SymbolEvent::Close lint; raw logs retained |
|
||||
| clippy-3 | Passed strict workspace/all-target lint |
|
||||
| desktop-affected-2 | 83 passed, 0 failed, 1 known ignored, 1,101 filtered |
|
||||
| windows-build-1 | Workspace binaries built successfully; 3 binary artifact receipts |
|
||||
|
||||
The full suites use the e624d5b9c4f848eff3fba2ed83f9decf959e14d8 input
|
||||
state. The final lint inputs include the reviewed four-file fix committed as
|
||||
f714a25bbcd435941404104ad0cbb0fea3b89149. That change moves test-only
|
||||
helpers, removes the unconstructed Close variant and its handler, and changes
|
||||
existing tests to send the captured HideDrawer event already used by production
|
||||
UI. `fullSuiteToFinalInputChanges` records the exact four file/digest pairs.
|
||||
Per-run source inventories are authoritative; receipt HEADs can predate dirty
|
||||
changes. Full-suite outcomes are scoped to their recorded inputs; they are not
|
||||
a full-suite rerun of the later fix. The final affected-test recheck uses the
|
||||
same 707 input hashes as clippy-3. Its 84 names equal the OR-filter selection
|
||||
from the prior 1,185-name listing: `source_symbols::tests`, `text_workspace::`,
|
||||
`views::tests`, `palette_union`. All 83 enabled selected tests passed; the
|
||||
known ignored case is the source-symbol 100-module performance probe. This
|
||||
check introduces no test names and leaves 1,101 previous tests unexecuted on
|
||||
the final source state.
|
||||
The final Windows build uses those same 707 input hashes. Its three binary
|
||||
artifact records are preserved; the binaries themselves are not in this package.
|
||||
|
||||
Verification checks archive membership/hashes, nested source manifests and all
|
||||
707 inputs including 71 vendored iced_widget files, receipt relationships,
|
||||
helper identity, commands, outcomes, exact test summary rows, and artifact
|
||||
receipt records. It independently recomputes actual desktop names/outcomes,
|
||||
listing/execution equality, all 150 required matches (144 passed, 6 ignored),
|
||||
removed/retained names, and 24 original checklist Git blob bindings from archived
|
||||
source bytes. It checks the full-suite-to-final four-file input delta. The
|
||||
checklist's original pending-listing wording is preserved as raw historical
|
||||
input; desktop-admission.json records the completed admission.
|
||||
|
||||
`final-input-commit-readback.json` is a secondary parent-produced summary of
|
||||
comparison between all 707 final lint inputs and f714a25 committed blobs. Its
|
||||
helper and Git stream were not archived. Verification checks its relationship
|
||||
to the captured input manifest; it does not independently reproduce the Git
|
||||
comparison or authenticate that commit.
|
||||
|
||||
Integrity and internal consistency do not establish independent reproduction or
|
||||
authenticated provenance. Expected summary rows are frozen from raw logs.
|
||||
Ignored probes did not execute. Test membership/pass outcomes do not establish
|
||||
complete requirement coverage or native GUI acceptance. Compiler artifact hashes
|
||||
are retained as observations; binaries are intentionally absent and cannot be
|
||||
verified here. Source archives are not a hermetic compiler/dependency bundle.
|
||||
This package makes no native interaction, release publication or packaged-build
|
||||
claim. No credentials, dependency caches or whole user profile are included.
|
||||
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"evidence.zip": {
|
||||
"bytes": 8624040,
|
||||
"sha256": "b0da6611dd86e83c8f779c025786e473834298f9b9990b19a76b250b61137780"
|
||||
},
|
||||
"manifest.json": {
|
||||
"bytes": 19056,
|
||||
"sha256": "752d4c8e703cdbac361f959716a8a752096280727f9f0c966854d479aa68750d"
|
||||
},
|
||||
"verify.py": {
|
||||
"bytes": 12815,
|
||||
"sha256": "5157b5cadf38e59bd59693fe1d14235d2007788bb6c8bd9cbac938cb048b14f4"
|
||||
},
|
||||
"README.md": {
|
||||
"bytes": 4177,
|
||||
"sha256": "9bfafd6506d1eb322e6efb8070a1599766d6d09532a678251a8b2414387edc6e"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,819 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fullSuiteInputsCommit": "e624d5b9c4f848eff3fba2ed83f9decf959e14d8",
|
||||
"reviewedFixCommit": "f714a25bbcd435941404104ad0cbb0fea3b89149",
|
||||
"runs": {
|
||||
"desktop-list-1": {
|
||||
"kind": "cargo",
|
||||
"inputs": "inputs/bbe8290d9cb11a7ee5dba250350451c5b7ec88069aa65f6be786a67922408c48.json",
|
||||
"archive": "inputs/86ac50e9cec8b39db91bbc98e5388e8c0f8466e6c217cf2f4eca5f1db087dde1.zip",
|
||||
"command": [
|
||||
"cargo",
|
||||
"test",
|
||||
"--locked",
|
||||
"--offline",
|
||||
"-p",
|
||||
"oa-desktop",
|
||||
"--bin",
|
||||
"arcadia-studio",
|
||||
"--message-format=json",
|
||||
"--",
|
||||
"--list"
|
||||
],
|
||||
"head": "e624d5b9c4f848eff3fba2ed83f9decf959e14d8",
|
||||
"exitCode": 0,
|
||||
"inputCount": 707,
|
||||
"testRows": [],
|
||||
"testTotals": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"core-2": {
|
||||
"kind": "cargo",
|
||||
"inputs": "inputs/bbe8290d9cb11a7ee5dba250350451c5b7ec88069aa65f6be786a67922408c48.json",
|
||||
"archive": "inputs/86ac50e9cec8b39db91bbc98e5388e8c0f8466e6c217cf2f4eca5f1db087dde1.zip",
|
||||
"command": [
|
||||
"cargo",
|
||||
"test",
|
||||
"--locked",
|
||||
"--offline",
|
||||
"-p",
|
||||
"oa-core",
|
||||
"--no-fail-fast",
|
||||
"--message-format=json",
|
||||
"--",
|
||||
"--nocapture"
|
||||
],
|
||||
"head": "cb941367d7748f7bfe7dda53641d395ad60a499f",
|
||||
"exitCode": 0,
|
||||
"inputCount": 707,
|
||||
"testRows": [
|
||||
[
|
||||
102,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
9,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
6,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
10,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
9,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
6,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
11,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
6,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
16,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
8,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
7,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
8,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
5,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
35,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
7,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
15,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
7,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
5,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
7,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
14,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
11,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
6,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
15,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
5,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
18,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
8,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
16,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
10,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
12,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
33,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
28,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
],
|
||||
"testTotals": [
|
||||
537,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"desktop-1": {
|
||||
"kind": "cargo",
|
||||
"inputs": "inputs/bbe8290d9cb11a7ee5dba250350451c5b7ec88069aa65f6be786a67922408c48.json",
|
||||
"archive": "inputs/86ac50e9cec8b39db91bbc98e5388e8c0f8466e6c217cf2f4eca5f1db087dde1.zip",
|
||||
"command": [
|
||||
"cargo",
|
||||
"test",
|
||||
"--locked",
|
||||
"--offline",
|
||||
"-p",
|
||||
"oa-desktop",
|
||||
"--bin",
|
||||
"arcadia-studio",
|
||||
"--no-fail-fast",
|
||||
"--message-format=json",
|
||||
"--",
|
||||
"--nocapture"
|
||||
],
|
||||
"head": "cb941367d7748f7bfe7dda53641d395ad60a499f",
|
||||
"exitCode": 0,
|
||||
"inputCount": 707,
|
||||
"testRows": [
|
||||
[
|
||||
1179,
|
||||
0,
|
||||
6,
|
||||
0,
|
||||
0
|
||||
]
|
||||
],
|
||||
"testTotals": [
|
||||
1179,
|
||||
0,
|
||||
6,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"clippy-1": {
|
||||
"kind": "cargo",
|
||||
"inputs": "inputs/bbe8290d9cb11a7ee5dba250350451c5b7ec88069aa65f6be786a67922408c48.json",
|
||||
"archive": "inputs/86ac50e9cec8b39db91bbc98e5388e8c0f8466e6c217cf2f4eca5f1db087dde1.zip",
|
||||
"command": [
|
||||
"cargo",
|
||||
"clippy",
|
||||
"--locked",
|
||||
"--offline",
|
||||
"--workspace",
|
||||
"--all-targets",
|
||||
"--message-format=json",
|
||||
"--",
|
||||
"-D",
|
||||
"warnings"
|
||||
],
|
||||
"head": "cb941367d7748f7bfe7dda53641d395ad60a499f",
|
||||
"exitCode": 101,
|
||||
"inputCount": 707,
|
||||
"testRows": [],
|
||||
"testTotals": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"clippy-2": {
|
||||
"kind": "cargo",
|
||||
"inputs": "inputs/6b2c2925c732b79150b1d3c2f9dc215f9221b89796bb1d8668d3ecc138095f82.json",
|
||||
"archive": "inputs/c5080fa78cb5cac2c6bdbf53f00d9d19313fa08de351153b9c5babd3a55b2f7d.zip",
|
||||
"command": [
|
||||
"cargo",
|
||||
"clippy",
|
||||
"--locked",
|
||||
"--offline",
|
||||
"--workspace",
|
||||
"--all-targets",
|
||||
"--message-format=json",
|
||||
"--",
|
||||
"-D",
|
||||
"warnings"
|
||||
],
|
||||
"head": "cb941367d7748f7bfe7dda53641d395ad60a499f",
|
||||
"exitCode": 101,
|
||||
"inputCount": 707,
|
||||
"testRows": [],
|
||||
"testTotals": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"clippy-3": {
|
||||
"kind": "cargo",
|
||||
"inputs": "inputs/a36e2cbc29eb7a9217774464cda9da8aa1d3c39caae9b9aacd6cdb28df5bad95.json",
|
||||
"archive": "inputs/9346fba1be9bf4dd2225fe9965fd76a30429f086fd6a101f92d0963d3822f71c.zip",
|
||||
"command": [
|
||||
"cargo",
|
||||
"clippy",
|
||||
"--locked",
|
||||
"--offline",
|
||||
"--workspace",
|
||||
"--all-targets",
|
||||
"--message-format=json",
|
||||
"--",
|
||||
"-D",
|
||||
"warnings"
|
||||
],
|
||||
"head": "cb941367d7748f7bfe7dda53641d395ad60a499f",
|
||||
"exitCode": 0,
|
||||
"inputCount": 707,
|
||||
"testRows": [],
|
||||
"testTotals": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"desktop-affected-2": {
|
||||
"kind": "cargo",
|
||||
"inputs": "inputs/a36e2cbc29eb7a9217774464cda9da8aa1d3c39caae9b9aacd6cdb28df5bad95.json",
|
||||
"archive": "inputs/9346fba1be9bf4dd2225fe9965fd76a30429f086fd6a101f92d0963d3822f71c.zip",
|
||||
"command": [
|
||||
"cargo",
|
||||
"test",
|
||||
"--locked",
|
||||
"--offline",
|
||||
"-p",
|
||||
"oa-desktop",
|
||||
"--bin",
|
||||
"arcadia-studio",
|
||||
"--message-format=json",
|
||||
"--",
|
||||
"source_symbols::tests",
|
||||
"text_workspace::",
|
||||
"views::tests",
|
||||
"palette_union",
|
||||
"--nocapture"
|
||||
],
|
||||
"head": "f714a25bbcd435941404104ad0cbb0fea3b89149",
|
||||
"exitCode": 0,
|
||||
"inputCount": 707,
|
||||
"testRows": [
|
||||
[
|
||||
83,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1101
|
||||
]
|
||||
],
|
||||
"testTotals": [
|
||||
83,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1101
|
||||
]
|
||||
},
|
||||
"windows-build-1": {
|
||||
"kind": "cargo",
|
||||
"inputs": "inputs/a36e2cbc29eb7a9217774464cda9da8aa1d3c39caae9b9aacd6cdb28df5bad95.json",
|
||||
"archive": "inputs/9346fba1be9bf4dd2225fe9965fd76a30429f086fd6a101f92d0963d3822f71c.zip",
|
||||
"command": [
|
||||
"cargo",
|
||||
"build",
|
||||
"--locked",
|
||||
"--offline",
|
||||
"--workspace",
|
||||
"--bins",
|
||||
"--message-format=json"
|
||||
],
|
||||
"head": "f714a25bbcd435941404104ad0cbb0fea3b89149",
|
||||
"exitCode": 0,
|
||||
"inputCount": 707,
|
||||
"testRows": [],
|
||||
"testTotals": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"members": {
|
||||
"helpers/desktop-admission.json": {
|
||||
"bytes": 289720,
|
||||
"sha256": "af2d601032f1f98217d841d181255dda5e123f48078f8d09815b498ee8975eb9"
|
||||
},
|
||||
"helpers/final-input-commit-readback.json": {
|
||||
"bytes": 260,
|
||||
"sha256": "9670f7400e358c5033b09ae4388cf577fb3db3f288a51d7ffe7b47c87bc377c0"
|
||||
},
|
||||
"helpers/gate-union.py": {
|
||||
"bytes": 3495,
|
||||
"sha256": "9cca2646b1bcf09961a9ad1a276d633d8e4296ff2c6f2f356f028afaf789f503"
|
||||
},
|
||||
"helpers/required-desktop-tests.json": {
|
||||
"bytes": 125261,
|
||||
"sha256": "6672e3f7ccfb92b7710310836af6933a8e58ef113353505ec4fc0c3d6908c136"
|
||||
},
|
||||
"inputs/6b2c2925c732b79150b1d3c2f9dc215f9221b89796bb1d8668d3ecc138095f82.json": {
|
||||
"bytes": 118403,
|
||||
"sha256": "6b2c2925c732b79150b1d3c2f9dc215f9221b89796bb1d8668d3ecc138095f82"
|
||||
},
|
||||
"inputs/86ac50e9cec8b39db91bbc98e5388e8c0f8466e6c217cf2f4eca5f1db087dde1.zip": {
|
||||
"bytes": 2831327,
|
||||
"sha256": "86ac50e9cec8b39db91bbc98e5388e8c0f8466e6c217cf2f4eca5f1db087dde1"
|
||||
},
|
||||
"inputs/9346fba1be9bf4dd2225fe9965fd76a30429f086fd6a101f92d0963d3822f71c.zip": {
|
||||
"bytes": 2831326,
|
||||
"sha256": "9346fba1be9bf4dd2225fe9965fd76a30429f086fd6a101f92d0963d3822f71c"
|
||||
},
|
||||
"inputs/a36e2cbc29eb7a9217774464cda9da8aa1d3c39caae9b9aacd6cdb28df5bad95.json": {
|
||||
"bytes": 118403,
|
||||
"sha256": "a36e2cbc29eb7a9217774464cda9da8aa1d3c39caae9b9aacd6cdb28df5bad95"
|
||||
},
|
||||
"inputs/bbe8290d9cb11a7ee5dba250350451c5b7ec88069aa65f6be786a67922408c48.json": {
|
||||
"bytes": 118403,
|
||||
"sha256": "bbe8290d9cb11a7ee5dba250350451c5b7ec88069aa65f6be786a67922408c48"
|
||||
},
|
||||
"inputs/c5080fa78cb5cac2c6bdbf53f00d9d19313fa08de351153b9c5babd3a55b2f7d.zip": {
|
||||
"bytes": 2831320,
|
||||
"sha256": "c5080fa78cb5cac2c6bdbf53f00d9d19313fa08de351153b9c5babd3a55b2f7d"
|
||||
},
|
||||
"runs/clippy-1/process.json": {
|
||||
"bytes": 199,
|
||||
"sha256": "1af72ef6a92af65144a1faea8cae4f5c868799894bf539385b8f40912b0b50ae"
|
||||
},
|
||||
"runs/clippy-1/result.json": {
|
||||
"bytes": 1799,
|
||||
"sha256": "355929311a614bfd682e9dc47fa1afef98e14b22f76c856771564b9eb282af57"
|
||||
},
|
||||
"runs/clippy-1/started.json": {
|
||||
"bytes": 1400,
|
||||
"sha256": "4671c110e5e26a34e48470b0002322fc450b3844ee849f90ae6523091528ece1"
|
||||
},
|
||||
"runs/clippy-1/stderr.log": {
|
||||
"bytes": 1368,
|
||||
"sha256": "741e0cd2a2418cdb748703cf02cc72064e5b88b123a6fdacb5c6f87a2a6959c6"
|
||||
},
|
||||
"runs/clippy-1/stdout.log": {
|
||||
"bytes": 272957,
|
||||
"sha256": "37c21ea32cba74fbb54bc9e9f962fc7dfd5c018ac7f3750c2ae905e7cb51a542"
|
||||
},
|
||||
"runs/clippy-2/process.json": {
|
||||
"bytes": 199,
|
||||
"sha256": "6d11bf41b9461e19f6a06b64841ef1fa30c5129de4a4dad215866fc14c8f29d1"
|
||||
},
|
||||
"runs/clippy-2/result.json": {
|
||||
"bytes": 1797,
|
||||
"sha256": "0583fa653d2b2be928250b1845d2eec36886a5ea1dc12013ea853b68a79fe4be"
|
||||
},
|
||||
"runs/clippy-2/started.json": {
|
||||
"bytes": 1400,
|
||||
"sha256": "9d290135f4125ac119601b2704f1ac9b03ec5800a80624155ead21a35e7a4ec6"
|
||||
},
|
||||
"runs/clippy-2/stderr.log": {
|
||||
"bytes": 155,
|
||||
"sha256": "93a6965e0904eb30abe0657f4a9c30b5ba9e148c9d5274b3c3fb5e78d898c562"
|
||||
},
|
||||
"runs/clippy-2/stdout.log": {
|
||||
"bytes": 270250,
|
||||
"sha256": "bdc2338aab9fc18fb4e29fbb9e9192ccf9b67e1e9fb2cca828ec50acc90c1437"
|
||||
},
|
||||
"runs/clippy-3/process.json": {
|
||||
"bytes": 199,
|
||||
"sha256": "0d3a92e71ae9b33b703479a54fd1ca2491b67c1374b1a1e259816eebea9bb3b0"
|
||||
},
|
||||
"runs/clippy-3/result.json": {
|
||||
"bytes": 1796,
|
||||
"sha256": "cf49e16fa5cbecacf24edbf054036b1190a02771c61f1178c12c6f3d7d9477b0"
|
||||
},
|
||||
"runs/clippy-3/started.json": {
|
||||
"bytes": 1400,
|
||||
"sha256": "5499895565380e2efb1ccf6a9acc825318dfe09135815adf9512bf50530524a5"
|
||||
},
|
||||
"runs/clippy-3/stderr.log": {
|
||||
"bytes": 325,
|
||||
"sha256": "d2811192fb769c1a0f2e4258dd31c76f6a475e43bc6214e49db7c0757e7dad00"
|
||||
},
|
||||
"runs/clippy-3/stdout.log": {
|
||||
"bytes": 307989,
|
||||
"sha256": "319cad11af00c742a1436734b5daee273a112859c7775b06fada1948d54e93d6"
|
||||
},
|
||||
"runs/core-2/process.json": {
|
||||
"bytes": 197,
|
||||
"sha256": "50d3c62701417b7cf49de72139a71d5b26ede0ebec36eac6ecc254cf45f1594a"
|
||||
},
|
||||
"runs/core-2/result.json": {
|
||||
"bytes": 67027,
|
||||
"sha256": "2eb69042c0c6eecb89b11e739b9598005b8a26479564d299275817310011b5bb"
|
||||
},
|
||||
"runs/core-2/started.json": {
|
||||
"bytes": 1398,
|
||||
"sha256": "eadc9d64a5fedc1bea614c2ccae4d19616e2f7b0538bc5e08e84e9ee92f532e0"
|
||||
},
|
||||
"runs/core-2/stderr.log": {
|
||||
"bytes": 8930,
|
||||
"sha256": "0d5a5d2a4e64bd38c89fffafd8089de7b74a52f1dbbccb8cc59dae640bebba3b"
|
||||
},
|
||||
"runs/core-2/stdout.log": {
|
||||
"bytes": 131042,
|
||||
"sha256": "4185a25e52e52f3c6ee1dcd942a45e1a3ef57b3961ec7e7c4ca0aacc86967cfc"
|
||||
},
|
||||
"runs/desktop-1/process.json": {
|
||||
"bytes": 227,
|
||||
"sha256": "38ff4960fbcdf8282c8fc323a337b25f19b1ab424d7ed590917e6765f8a643c1"
|
||||
},
|
||||
"runs/desktop-1/result.json": {
|
||||
"bytes": 3154,
|
||||
"sha256": "f254ec40b83763f1398ab96fff16d96c9a0ff8a3113c2d16556c973eb5eaf93e"
|
||||
},
|
||||
"runs/desktop-1/started.json": {
|
||||
"bytes": 1438,
|
||||
"sha256": "ba43fbd949da1d6e453217c2f534d661b81dbe4b42fc1fe198c811234dc48aaf"
|
||||
},
|
||||
"runs/desktop-1/stderr.log": {
|
||||
"bytes": 552,
|
||||
"sha256": "c975782e7df7ce13b9f298d164fd4cc8e8161b6da7fd77c4607920fd8b3bf900"
|
||||
},
|
||||
"runs/desktop-1/stdout.log": {
|
||||
"bytes": 430091,
|
||||
"sha256": "1bfcc88362ef725cb3e4b9af7967d4bd18e65db3eccd342fe3264ea6ef95ec19"
|
||||
},
|
||||
"runs/desktop-affected-2/process.json": {
|
||||
"bytes": 287,
|
||||
"sha256": "b72fb9ac1c7400ef7af2ed80f26a1e13fc718342779c868e4cbad4a5152b131f"
|
||||
},
|
||||
"runs/desktop-affected-2/result.json": {
|
||||
"bytes": 3228,
|
||||
"sha256": "cfb3f9563f35977efe26d69d6a12040278f725bf09fb30ec812a984cab72dc02"
|
||||
},
|
||||
"runs/desktop-affected-2/started.json": {
|
||||
"bytes": 1513,
|
||||
"sha256": "cca30f43f1d79c10a996422f9a506a08fc91eba7fc15a02615105f7fed89a20a"
|
||||
},
|
||||
"runs/desktop-affected-2/stderr.log": {
|
||||
"bytes": 281,
|
||||
"sha256": "90504f37b7256fd61af78a6c636560b3646ec9bdb2bc954c6e5bc77f69f21306"
|
||||
},
|
||||
"runs/desktop-affected-2/stdout.log": {
|
||||
"bytes": 304036,
|
||||
"sha256": "adf50044a9f6947e7384dbd0193fe069276c2e78da1d6c45a55150d2b1defa26"
|
||||
},
|
||||
"runs/desktop-list-1/process.json": {
|
||||
"bytes": 204,
|
||||
"sha256": "139e8f5d11f45dd2b6d2151e0b0628d892f3ad228134032ab8c9bfb1989059db"
|
||||
},
|
||||
"runs/desktop-list-1/result.json": {
|
||||
"bytes": 3128,
|
||||
"sha256": "24eacbb644a6b82be7439e4f8895562356eb63485e82c30292b2951cb090d29d"
|
||||
},
|
||||
"runs/desktop-list-1/started.json": {
|
||||
"bytes": 1410,
|
||||
"sha256": "33adc56c5fad5bd53dd425f5c6ccf8faf550bd382867b848d04ebaafcb3d5c66"
|
||||
},
|
||||
"runs/desktop-list-1/stderr.log": {
|
||||
"bytes": 1427,
|
||||
"sha256": "97885faf304979ca43898f88d3ca7118100fd1c2358f9d4729984f7369788704"
|
||||
},
|
||||
"runs/desktop-list-1/stdout.log": {
|
||||
"bytes": 421428,
|
||||
"sha256": "32f0025d9205288e92c4031dc1b53298b8e79f1ccb8ce990a40d7c3d66f4305f"
|
||||
},
|
||||
"runs/windows-build-1/process.json": {
|
||||
"bytes": 167,
|
||||
"sha256": "b400c1f8076bcddb583424fe34d155655a41d25b5ccf0db8af26bda6b9b2685e"
|
||||
},
|
||||
"runs/windows-build-1/result.json": {
|
||||
"bytes": 5532,
|
||||
"sha256": "5e9e227c66a4b71ade734b8ebeb12070b8a5c64a9a6cf9d8d0b6d70379d4ffc2"
|
||||
},
|
||||
"runs/windows-build-1/started.json": {
|
||||
"bytes": 1353,
|
||||
"sha256": "15c54be09a881e6860735395569b22a7a9114789a1f8276cf79bc6fb095c8163"
|
||||
},
|
||||
"runs/windows-build-1/stderr.log": {
|
||||
"bytes": 261,
|
||||
"sha256": "480dff999944c756fff581db0f3fac088717e0d73cae20af9a28829446df746f"
|
||||
},
|
||||
"runs/windows-build-1/stdout.log": {
|
||||
"bytes": 295092,
|
||||
"sha256": "5a824ca91877328acd332585b39194b27fd2cb167279b99c4c78a89bf13bd19b"
|
||||
}
|
||||
},
|
||||
"fullSuiteToFinalInputChanges": {
|
||||
"crates/oa-desktop/src/app/source_symbols.rs": {
|
||||
"before": {
|
||||
"bytes": 26476,
|
||||
"sha256": "230b2d19b0aeb767a311bdc0eddfd69986ebc5f65b8892382cd033a69f86cb2d"
|
||||
},
|
||||
"after": {
|
||||
"bytes": 26369,
|
||||
"sha256": "9dbcb70a7eaa0b2f7ec011da1fcc808ba1e53ec1c29a3c9a09cf7517e058c7b7"
|
||||
}
|
||||
},
|
||||
"crates/oa-desktop/src/app/source_symbols/tests.rs": {
|
||||
"before": {
|
||||
"bytes": 85389,
|
||||
"sha256": "0e3ae6861ded52e7d137736f4ffe6bf55a00291bf685d3bbe6141174210eee9c"
|
||||
},
|
||||
"after": {
|
||||
"bytes": 85506,
|
||||
"sha256": "aebd05f3fa9f1e9c2e744948d4478d9f267365d063f01f8b8f4e67560bc23f32"
|
||||
}
|
||||
},
|
||||
"crates/oa-desktop/src/source_navigation/controller.rs": {
|
||||
"before": {
|
||||
"bytes": 3691,
|
||||
"sha256": "2042d608a4e9698c614c0d308d02896983480087908ecc353369b424eca3d124"
|
||||
},
|
||||
"after": {
|
||||
"bytes": 3680,
|
||||
"sha256": "938b8264c2d5166bd979439723d6fbec34c4cc5ed7f872f58c2cb9ba06a4a93d"
|
||||
}
|
||||
},
|
||||
"crates/oa-desktop/src/views.rs": {
|
||||
"before": {
|
||||
"bytes": 58364,
|
||||
"sha256": "46bb9410567d47e3043723fc3b8c7342c04e85b1b4a4c06a84e8497757cc04dc"
|
||||
},
|
||||
"after": {
|
||||
"bytes": 58364,
|
||||
"sha256": "e74da124914a54a3b757eb4728a5093157f172cd7f5e90b7744e526e3ec3f8d2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
"""Read-only, Python-standard-library verification. Never executes archived code."""
|
||||
from pathlib import Path, PurePosixPath
|
||||
from datetime import datetime
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
|
||||
def require(ok, message):
|
||||
if not ok:
|
||||
raise ValueError(message)
|
||||
|
||||
def digest(data):
|
||||
return {'bytes': len(data), 'sha256': hashlib.sha256(data).hexdigest()}
|
||||
|
||||
def members(z, expected):
|
||||
names = z.namelist()
|
||||
require(len(names) == len(set(names)), 'duplicate ZIP members')
|
||||
require(set(names) == set(expected), 'archive membership mismatch')
|
||||
require(all(not PurePosixPath(n).is_absolute() and '..' not in PurePosixPath(n).parts
|
||||
and '\\' not in n and ':' not in n for n in names), 'unsafe member')
|
||||
|
||||
def main():
|
||||
inventory = json.loads((ROOT / 'files.json').read_text())
|
||||
for name, expected in inventory.items():
|
||||
require(digest((ROOT / name).read_bytes()) == expected, 'external file hash: ' + name)
|
||||
manifest = json.loads((ROOT / 'manifest.json').read_text())
|
||||
with zipfile.ZipFile(ROOT / 'evidence.zip') as z:
|
||||
members(z, manifest['members'])
|
||||
for name, expected in manifest['members'].items():
|
||||
require(digest(z.read(name)) == expected, 'member hash: ' + name)
|
||||
def raw(name): return z.read(name)
|
||||
def obj(name): return json.loads(raw(name))
|
||||
checked = set()
|
||||
for run, spec in manifest['runs'].items():
|
||||
if spec['kind'] != 'cargo':
|
||||
continue
|
||||
prefix = 'runs/' + run + '/'
|
||||
start, result, process = (obj(prefix + n + '.json') for n in ('started', 'result', 'process'))
|
||||
require(all(result[k] == v for k, v in start.items()), run + ': start/result relationship')
|
||||
require(result['command'] == spec['command'] == process['command'], run + ': command')
|
||||
require(result['head'] == spec['head'], run + ': HEAD')
|
||||
require(result['exitCode'] == spec['exitCode'] and result['inputsUnchanged'] is True, run + ': outcome')
|
||||
require(datetime.fromisoformat(result['finish']) >= datetime.fromisoformat(start['start']), run + ': times')
|
||||
require(result['seconds'] >= 0, run + ': duration')
|
||||
require(result['helper'] == digest(raw('helpers/gate-union.py')), run + ': helper')
|
||||
for field, file in [('inputs', spec['inputs']), ('archive', spec['archive']), ('stdout', prefix+'stdout.log'), ('stderr', prefix+'stderr.log')]:
|
||||
require(result[field] == digest(raw(file)), run + ': receipt ' + field)
|
||||
inputs = obj(spec['inputs'])
|
||||
require(len(inputs) == result['inputCount'] == spec['inputCount'] == 707, run + ': input count')
|
||||
require(sum(p.startswith('crates/vendor/iced_widget/') for p in inputs) == 71, run + ': vendor count')
|
||||
pair = (spec['inputs'], spec['archive'])
|
||||
if pair not in checked:
|
||||
with zipfile.ZipFile(io.BytesIO(raw(spec['archive']))) as source:
|
||||
members(source, inputs)
|
||||
for name, expected in inputs.items():
|
||||
require(digest(source.read(name)) == expected, run + ': source hash ' + name)
|
||||
checked.add(pair)
|
||||
stdout = raw(prefix+'stdout.log').decode('utf-8', errors='replace')
|
||||
rows = [[int(x) for x in row] for row in re.findall(r'test result: (?:ok|FAILED)\. (\d+) passed; (\d+) failed; (\d+) ignored; (\d+) measured; (\d+) filtered out;', stdout)]
|
||||
require(rows == spec['testRows'], run + ': exact test summaries')
|
||||
require([sum(row[i] for row in rows) for i in range(5)] == spec['testTotals'], run + ': test totals')
|
||||
artifacts = []
|
||||
for line in stdout.splitlines():
|
||||
try: item = json.loads(line)
|
||||
except json.JSONDecodeError: continue
|
||||
if isinstance(item, dict) and item.get('reason') == 'compiler-artifact' and item.get('executable'):
|
||||
artifacts.append(item)
|
||||
require(artifacts == [a['cargo'] for a in result['artifacts']], run + ': artifact receipt relationship')
|
||||
print(run, 'exit', result['exitCode'], 'test totals', spec['testTotals'])
|
||||
admission(z, manifest)
|
||||
print('PASS:', len(manifest['runs']), 'closed captures;', len(manifest['members']), 'members;', len(checked), 'input manifest/archive pairs')
|
||||
print('Integrity and recorded outcomes only; no rerun, binary verification, or native GUI claim.')
|
||||
|
||||
def admission(z, manifest):
|
||||
def obj(p): return json.loads(z.read(p))
|
||||
report = obj('helpers/desktop-admission.json')
|
||||
checklist = obj('helpers/required-desktop-tests.json')
|
||||
require(report['status'] == 'PASS', 'admission status')
|
||||
require({k:report['checklist'][k] for k in ('bytes','sha256')} == digest(z.read('helpers/required-desktop-tests.json')), 'checklist receipt')
|
||||
require(report['checklist']['sourceCommit'] == checklist['sourceCommit'], 'checklist source pin')
|
||||
for run, receipt in report['runs'].items():
|
||||
result = obj('runs/'+run+'/result.json')
|
||||
for field in ('head','command','exitCode','inputsUnchanged','inputCount'):
|
||||
require(receipt[field] == result[field], 'admission run relationship '+field)
|
||||
require(receipt['inputManifest'] == result['inputs'] and receipt['stdout'] == result['stdout'], 'admission inputs/log')
|
||||
require(receipt['resultReceipt'] == digest(z.read('runs/'+run+'/result.json')), 'admission result hash')
|
||||
listing = z.read('runs/desktop-list-1/stdout.log').decode('utf-8')
|
||||
execution = z.read('runs/desktop-1/stdout.log').decode('utf-8')
|
||||
names = re.findall(r'^([A-Za-z0-9_:]+): test$', listing, re.M)
|
||||
outcomes = re.findall(r'^test ([A-Za-z0-9_:]+) \.\.\. (ok|FAILED|ignored)(?:, (.*))?$', execution, re.M)
|
||||
require(len(names) == len(set(names)) == 1185, 'listing names/count/uniqueness')
|
||||
require(len(outcomes) == len({r[0] for r in outcomes}) == 1185, 'execution names/count/uniqueness')
|
||||
require(set(names) == {r[0] for r in outcomes}, 'listing/execution set equality')
|
||||
actual = {n:(status, reason or None) for n,status,reason in outcomes}
|
||||
require(actual == {r['name']:(r['status'],r['reason']) for r in report['allActualNamesAndOutcomes']}, 'admission raw outcomes')
|
||||
require(len(report['allActualNamesAndOutcomes']) == len(actual), 'admission duplicate outcomes')
|
||||
required = []
|
||||
for spec in checklist['required']:
|
||||
matches = [n for n in names if n.endswith(spec['match']['suffix'])]
|
||||
require(len(matches) == spec['match']['expectedOccurrences'] == 1, 'required unique match '+spec['name'])
|
||||
n = matches[0]
|
||||
require(actual[n][0] == ('ignored' if spec['expectedIgnored'] else 'ok'), 'required outcome '+n)
|
||||
required.append({'sourcePath':spec['sourcePath'],'expectedBasename':spec['name'],'actualName':n,'expectedIgnored':spec['expectedIgnored'],'status':actual[n][0],'reason':actual[n][1]})
|
||||
require(required == report['requiredOutcomes'], 'required admission relationships')
|
||||
removed=[]
|
||||
for spec in checklist['removedOrReplaced']:
|
||||
require(not any(n.endswith('::'+spec['name']) for n in names), 'removed test present')
|
||||
replacements=[n for replacement in spec.get('replacementNames',[]) for n in names if n.endswith('::'+replacement)]
|
||||
removed.append({'name':spec['name'],'absent':True,'replacementActualNames':replacements})
|
||||
require(removed == report['removedOrReplaced'], 'removed admission relationship')
|
||||
retained=[]
|
||||
for spec in checklist['duplicateDeparture']:
|
||||
require(actual[spec['requiredFullName']][0] == 'ok' and spec['removedFullName'] not in names, 'retained duplicate admission')
|
||||
require(sum(n.endswith('::'+spec['name']) for n in names) == spec['expectedTotalOccurrences'] == 1, 'retained duplicate count')
|
||||
retained.append({'name':spec['name'],'retainedName':spec['requiredFullName'],'retainedStatus':'ok','removedParentNameAbsent':True})
|
||||
require(retained == report['retainedDepartureCases'], 'retained admission relationship')
|
||||
before = obj(manifest['runs']['desktop-1']['inputs'])
|
||||
require(before == obj(manifest['runs']['desktop-list-1']['inputs']), 'listing/execution source equality')
|
||||
require(before == obj(manifest['runs']['core-2']['inputs']), 'core/desktop full-suite source equality')
|
||||
with zipfile.ZipFile(io.BytesIO(z.read(manifest['runs']['desktop-1']['archive']))) as source:
|
||||
bindings=[]
|
||||
for spec in checklist['sourceBindings']:
|
||||
b=source.read(spec['path'])
|
||||
blob=hashlib.sha1(b'blob '+str(len(b)).encode()+b'\0'+b).hexdigest()
|
||||
require(blob == spec['gitBlob'], 'original checklist source blob '+spec['path'])
|
||||
bindings.append({**spec,'capturedAndCurrentDigest':digest(b),'matchesOriginalChecklistBlob':True})
|
||||
require(bindings == report['sourceBindings'], 'source binding admission')
|
||||
summary={'listed':len(names),'executed':len(actual),'exactNameSetEquality':True,'duplicateListedNames':0,'duplicateExecutedNames':0,
|
||||
'passed':sum(v[0]=='ok' for v in actual.values()),'failed':sum(v[0]=='FAILED' for v in actual.values()),'ignored':sum(v[0]=='ignored' for v in actual.values()),
|
||||
'required':len(required),'requiredPassed':sum(r['status']=='ok' for r in required),'requiredIgnored':sum(r['status']=='ignored' for r in required),
|
||||
'removedNamesAbsent':len(checklist['removedOrReplaced']),'retainedDuplicateCasesPassExactlyOnce':len(checklist['duplicateDeparture']),
|
||||
'originalChecklistSourceFilesVerified':len(checklist['sourceBindings']),'listingAndExecutionInputManifestsEqual':True}
|
||||
require(summary == report['summary'], 'admission summary')
|
||||
after = obj(manifest['runs']['clippy-3']['inputs'])
|
||||
changed={p:{'before':before.get(p),'after':after.get(p)} for p in sorted(set(before)|set(after)) if before.get(p)!=after.get(p)}
|
||||
require(changed == manifest['fullSuiteToFinalInputChanges'] and len(changed)==4, 'four-file source delta')
|
||||
require(set(changed) == {'crates/oa-desktop/src/source_navigation/controller.rs','crates/oa-desktop/src/app/source_symbols.rs','crates/oa-desktop/src/app/source_symbols/tests.rs','crates/oa-desktop/src/views.rs'}, 'reviewed four-file scope')
|
||||
readback=obj('helpers/final-input-commit-readback.json')
|
||||
require(readback['status']=='PASS' and readback['commit']==manifest['reviewedFixCommit'], 'secondary readback summary')
|
||||
require(readback['inputCount']==len(after) and readback['inputManifestSha256']==digest(z.read(manifest['runs']['clippy-3']['inputs']))['sha256'], 'secondary readback input relationship')
|
||||
for run in ('desktop-affected-2','windows-build-1'):
|
||||
if run in manifest['runs']:
|
||||
require(obj(manifest['runs'][run]['inputs']) == after, run+': final source equality')
|
||||
if 'windows-build-1' in manifest['runs']:
|
||||
result=obj('runs/windows-build-1/result.json')
|
||||
require(result['exitCode']==0 and len(result['artifacts'])==3, 'final build outcome/artifacts')
|
||||
require({a['cargo']['target']['name'] for a in result['artifacts']}=={'arcadia','arcadia-server','arcadia-studio'}, 'final build target membership')
|
||||
if 'desktop-affected-2' in manifest['runs']:
|
||||
spec=manifest['runs']['desktop-affected-2']
|
||||
args=spec['command'][spec['command'].index('--')+1:]
|
||||
filters=[arg for arg in args if not arg.startswith('--')]
|
||||
require(filters==['source_symbols::tests','text_workspace::','views::tests','palette_union'], 'affected OR filters')
|
||||
selected={n for n in names if any(f in n for f in filters)}
|
||||
log=z.read('runs/desktop-affected-2/stdout.log').decode('utf-8')
|
||||
affected=re.findall(r'^test ([A-Za-z0-9_:]+) \.\.\. (ok|FAILED|ignored)(?:, (.*))?$',log,re.M)
|
||||
require(len(affected)==len({r[0] for r in affected})==len(selected)==84, 'affected count/unique names')
|
||||
require({n for n,s,r in affected}==selected, 'affected exact OR-filter selection from prior listing')
|
||||
require(sum(s=='ok' for n,s,r in affected)==83 and sum(s=='FAILED' for n,s,r in affected)==0, 'affected enabled passes')
|
||||
require([n for n,s,r in affected if s=='ignored']==['app::source_symbols::tests::source_symbols_100_modules_1000_definitions_10000_uses'], 'affected known ignored probe')
|
||||
require(spec['testTotals']==[83,0,1,0,len(names)-len(selected)], 'affected summary and filtered count')
|
||||
print('Affected recheck: exact prior-list OR selection; 83 passed, 1 known ignored, 1101 filtered')
|
||||
print('Desktop admission:', summary['passed'], 'passed,', summary['ignored'], 'ignored;',len(required),'required; four final changed input files')
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,52 @@
|
||||
# Ordinary-text union: closed-run checkpoint
|
||||
|
||||
Run `python verify.py` with Python 3.10 or newer in this directory. The verifier
|
||||
uses only the standard library and these five files. It reads archives in memory,
|
||||
does not extract or execute source/binaries, and does not consult Git, Cargo,
|
||||
network services, or paths recorded from the original machine.
|
||||
|
||||
`files.json` hashes the ZIP, manifest, verifier and this README. `manifest.json`
|
||||
lists every ZIP member and maps each run to its exact input manifest and ZIP.
|
||||
Only byte-identical input files are deduplicated; ZIPs with different timestamps
|
||||
remain separate even when their source inventories match. Receipts and raw logs
|
||||
are retained verbatim, including failures and original local path strings.
|
||||
|
||||
The production composition is d7c065991d3f63cc41341d23a546739de8837ac3.
|
||||
The checkpoint is e624d5b9c4f848eff3fba2ed83f9decf959e14d8, including generated
|
||||
teaching fixtures and normative language documentation. Individual receipt HEADs
|
||||
and input inventories are authoritative for each run; a HEAD alone does not
|
||||
describe dirty fixture/document changes during capture.
|
||||
|
||||
| Closed capture | Recorded outcome |
|
||||
| --- | --- |
|
||||
| core-consumers-1 | Offline dependency resolution failed: num-bigint missing |
|
||||
| windows-dependencies-1 | Dependency preparation completed |
|
||||
| core-1 | 536 passed, 1 failed, 1 ignored; stale generated teaching fixture failure preserved |
|
||||
| teaching-generate-1 | Teaching generator completed; raw generated-release1 outputs included |
|
||||
| teaching-check-2 | 4 passed after fixture regeneration |
|
||||
| language-doc-1 | Warning log from failed strict render preserved |
|
||||
| language-doc-2 | Strict docutils 0.23 render succeeded; empty warning log |
|
||||
| consumers-1 | 63 passed |
|
||||
|
||||
The verifier checks exact archive membership, hashes and lengths, nested input
|
||||
archive membership/content, input counts (including 71 vendored iced_widget
|
||||
files), start/result/process command relationships, unchanged-input assertions,
|
||||
helper hashes, exit codes, exact test summary rows and aggregate counts,
|
||||
compiler-artifact receipt relationships, composition patch hash, generated output
|
||||
inventory, and successful document source/output/warning receipts.
|
||||
|
||||
Limitations: integrity and internal consistency are not independent reproduction
|
||||
or authenticated provenance. Test expectations in the manifest are frozen from
|
||||
the raw capture. No runtime binaries, dependency cache, credentials or user
|
||||
profile are packaged. Recorded artifact readback hashes cannot be independently
|
||||
checked without the excluded binaries. Input archives contain project sources,
|
||||
not a hermetic toolchain or complete dependency distribution. The gate helper is
|
||||
receipt-pinned; the included render helper is its current successful-render
|
||||
version, with no historical helper hash receipt. language-doc-1 contains only
|
||||
warnings.log: its original helper, exact failed input and process/exit receipt
|
||||
were not captured. The successful render inputs and outputs are hash-pinned by
|
||||
its result receipt. Generated outputs are archived as observed, without a
|
||||
separate generation-output hash receipt. This package does not claim a full
|
||||
core rerun after regeneration, complete desktop/GUI validation, lint success,
|
||||
release packaging, or publication. Active desktop-list-1 and any later runs are
|
||||
excluded from this initial checkpoint.
|
||||
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"evidence.zip": {
|
||||
"bytes": 8584848,
|
||||
"sha256": "f220433f8f6fe99cea762aca98ee471c14411b5f6762745cf91432e6bfaca440"
|
||||
},
|
||||
"manifest.json": {
|
||||
"bytes": 24274,
|
||||
"sha256": "ad731f45fda8eb8678160315de00a4c77037d49ee40f0774496e7a2a970342aa"
|
||||
},
|
||||
"README.md": {
|
||||
"bytes": 3328,
|
||||
"sha256": "98c900e5c83e13da6c6edc11521df8edb87f4e1ebebb004fd1d6cc8acc91683b"
|
||||
},
|
||||
"verify.py": {
|
||||
"bytes": 5526,
|
||||
"sha256": "65136bbf0074c63a1478a56e0a66523d785081c368a50cd698d1231f6b0618ba"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,901 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"productionCommit": "d7c065991d3f63cc41341d23a546739de8837ac3",
|
||||
"checkpointCommit": "e624d5b9c4f848eff3fba2ed83f9decf959e14d8",
|
||||
"runs": {
|
||||
"core-consumers-1": {
|
||||
"kind": "cargo",
|
||||
"inputs": "inputs/e3d2d504b18bb1f7c4c3fa1f26abff425b957f18b418d8556888151723ba67b4.json",
|
||||
"archive": "inputs/12ce1ca9c1185d7a91a1dea322cdc0c7fc4372db5d879386595acdbfd412d880.zip",
|
||||
"command": [
|
||||
"cargo",
|
||||
"test",
|
||||
"--locked",
|
||||
"--offline",
|
||||
"-p",
|
||||
"oa-core",
|
||||
"-p",
|
||||
"oa-server",
|
||||
"--test",
|
||||
"http",
|
||||
"-p",
|
||||
"oa-cli",
|
||||
"--test",
|
||||
"source_cli",
|
||||
"--no-fail-fast",
|
||||
"--message-format=json",
|
||||
"--",
|
||||
"--nocapture"
|
||||
],
|
||||
"head": "d7c065991d3f63cc41341d23a546739de8837ac3",
|
||||
"exitCode": 101,
|
||||
"inputCount": 707,
|
||||
"testRows": [],
|
||||
"testTotals": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"windows-dependencies-1": {
|
||||
"kind": "cargo",
|
||||
"inputs": "inputs/e3d2d504b18bb1f7c4c3fa1f26abff425b957f18b418d8556888151723ba67b4.json",
|
||||
"archive": "inputs/12ce1ca9c1185d7a91a1dea322cdc0c7fc4372db5d879386595acdbfd412d880.zip",
|
||||
"command": [
|
||||
"cargo",
|
||||
"fetch",
|
||||
"--locked",
|
||||
"--target",
|
||||
"x86_64-pc-windows-gnullvm"
|
||||
],
|
||||
"head": "d7c065991d3f63cc41341d23a546739de8837ac3",
|
||||
"exitCode": 0,
|
||||
"inputCount": 707,
|
||||
"testRows": [],
|
||||
"testTotals": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"core-1": {
|
||||
"kind": "cargo",
|
||||
"inputs": "inputs/e3d2d504b18bb1f7c4c3fa1f26abff425b957f18b418d8556888151723ba67b4.json",
|
||||
"archive": "inputs/12ce1ca9c1185d7a91a1dea322cdc0c7fc4372db5d879386595acdbfd412d880.zip",
|
||||
"command": [
|
||||
"cargo",
|
||||
"test",
|
||||
"--locked",
|
||||
"--offline",
|
||||
"-p",
|
||||
"oa-core",
|
||||
"--no-fail-fast",
|
||||
"--message-format=json",
|
||||
"--",
|
||||
"--nocapture"
|
||||
],
|
||||
"head": "d7c065991d3f63cc41341d23a546739de8837ac3",
|
||||
"exitCode": 101,
|
||||
"inputCount": 707,
|
||||
"testRows": [
|
||||
[
|
||||
102,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
9,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
6,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
10,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
9,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
6,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
11,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
6,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
16,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
8,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
7,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
8,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
5,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
35,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
7,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
15,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
7,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
5,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
3,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
7,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
14,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
11,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
6,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
15,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
5,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
18,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
8,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
16,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
10,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
12,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
33,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
13,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
28,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
],
|
||||
"testTotals": [
|
||||
536,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"teaching-generate-1": {
|
||||
"kind": "cargo",
|
||||
"inputs": "inputs/e3d2d504b18bb1f7c4c3fa1f26abff425b957f18b418d8556888151723ba67b4.json",
|
||||
"archive": "inputs/12ce1ca9c1185d7a91a1dea322cdc0c7fc4372db5d879386595acdbfd412d880.zip",
|
||||
"command": [
|
||||
"cargo",
|
||||
"run",
|
||||
"--locked",
|
||||
"--offline",
|
||||
"-p",
|
||||
"oa-core",
|
||||
"--example",
|
||||
"generate_release1",
|
||||
"--message-format=json",
|
||||
"--",
|
||||
".local/ordinary-text-integration/generated-release1"
|
||||
],
|
||||
"head": "d7c065991d3f63cc41341d23a546739de8837ac3",
|
||||
"exitCode": 0,
|
||||
"inputCount": 707,
|
||||
"testRows": [],
|
||||
"testTotals": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"teaching-check-2": {
|
||||
"kind": "cargo",
|
||||
"inputs": "inputs/0fa1da63d71c984cd99b079de661e88154b5a31818b384f557deefd40b8ead3a.json",
|
||||
"archive": "inputs/81bd006f10034bc0c203187af179548a549fda6e58bc7cde0702cc26994fd28d.zip",
|
||||
"command": [
|
||||
"cargo",
|
||||
"test",
|
||||
"--locked",
|
||||
"--offline",
|
||||
"-p",
|
||||
"oa-core",
|
||||
"--test",
|
||||
"release1_teaching",
|
||||
"--message-format=json",
|
||||
"--",
|
||||
"--nocapture"
|
||||
],
|
||||
"head": "d7c065991d3f63cc41341d23a546739de8837ac3",
|
||||
"exitCode": 0,
|
||||
"inputCount": 707,
|
||||
"testRows": [
|
||||
[
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
],
|
||||
"testTotals": [
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"language-doc-1": {
|
||||
"kind": "render"
|
||||
},
|
||||
"language-doc-2": {
|
||||
"kind": "render"
|
||||
},
|
||||
"consumers-1": {
|
||||
"kind": "cargo",
|
||||
"inputs": "inputs/bbe8290d9cb11a7ee5dba250350451c5b7ec88069aa65f6be786a67922408c48.json",
|
||||
"archive": "inputs/86ac50e9cec8b39db91bbc98e5388e8c0f8466e6c217cf2f4eca5f1db087dde1.zip",
|
||||
"command": [
|
||||
"cargo",
|
||||
"test",
|
||||
"--locked",
|
||||
"--offline",
|
||||
"-p",
|
||||
"oa-server",
|
||||
"--test",
|
||||
"http",
|
||||
"-p",
|
||||
"oa-cli",
|
||||
"--test",
|
||||
"source_cli",
|
||||
"--no-fail-fast",
|
||||
"--message-format=json",
|
||||
"--",
|
||||
"--nocapture"
|
||||
],
|
||||
"head": "d7c065991d3f63cc41341d23a546739de8837ac3",
|
||||
"exitCode": 0,
|
||||
"inputCount": 707,
|
||||
"testRows": [
|
||||
[
|
||||
12,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
51,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
],
|
||||
"testTotals": [
|
||||
63,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"members": {
|
||||
"generated-release1/expected-diagnostics.json": {
|
||||
"bytes": 288,
|
||||
"sha256": "3700393d76b940cfc076bf52861bd7b1f43370f8eabce3594531bd0bf1fc851e"
|
||||
},
|
||||
"generated-release1/model.json": {
|
||||
"bytes": 134520,
|
||||
"sha256": "ef0ef4006c3e09d5db6e1b621256b3f7d6642f3a7b09cc34607cfbcca830c727"
|
||||
},
|
||||
"generated-release1/oa/activities/activity_oa_restore_work.arcadia": {
|
||||
"bytes": 1130,
|
||||
"sha256": "21cbd7176af8fad68453306f2371ff03e127a994b8796cdf044567332de9686f"
|
||||
},
|
||||
"generated-release1/oa/behavior/process_oa_assessment_procedure.arcadia": {
|
||||
"bytes": 550,
|
||||
"sha256": "23cabc213c264a16c8595414db6acb09248c1cef9cd699f1b0316e5bb82c062a"
|
||||
},
|
||||
"generated-release1/oa/behavior/process_oa_field_procedure.arcadia": {
|
||||
"bytes": 1178,
|
||||
"sha256": "9c52086759b7344b4809ceb8d372f65647b16fc24153a03b887faa378c37fe96"
|
||||
},
|
||||
"generated-release1/oa/behavior/process_oa_response.arcadia": {
|
||||
"bytes": 2560,
|
||||
"sha256": "675798bd19e6b40d03e7076b0e59b110cdaaab7b6d16c47c3ac80fa960072e84"
|
||||
},
|
||||
"generated-release1/oa/behavior/scenario_oa_conversation.arcadia": {
|
||||
"bytes": 1515,
|
||||
"sha256": "e217e5394a0771dc16206ed9437a231da27905d2f19be64b3f48281c0a32fb6b"
|
||||
},
|
||||
"generated-release1/oa/behavior/scenario_oa_full_scenario.arcadia": {
|
||||
"bytes": 1121,
|
||||
"sha256": "c5a285f2d7c61bd2a136107f267fbf9cddb1cba3f63ff36a6f53f7ab98f33df5"
|
||||
},
|
||||
"generated-release1/oa/behavior/state_machine_oa_monitor.arcadia": {
|
||||
"bytes": 1358,
|
||||
"sha256": "e7349e1e5336282eed3f5a103129db33d3ae4ed500788ee7129e8fc193ac57f6"
|
||||
},
|
||||
"generated-release1/oa/capabilities.arcadia": {
|
||||
"bytes": 630,
|
||||
"sha256": "26a3d9b615a3251ff010e0b47127f2d16bc2dcd45ed95967208b0076d725843a"
|
||||
},
|
||||
"generated-release1/oa/model.arcadia": {
|
||||
"bytes": 531,
|
||||
"sha256": "069a5eaaf525089935cc9d73ae7b916866ed78c6534705d75739650ecdea7ada"
|
||||
},
|
||||
"generated-release1/oa/participants.arcadia": {
|
||||
"bytes": 1580,
|
||||
"sha256": "591a75a9ecad280302be4d4ae0171ad616377ed11ead5e233848ca2dc0a71819"
|
||||
},
|
||||
"generated-release1/oa/types.arcadia": {
|
||||
"bytes": 554,
|
||||
"sha256": "f02c391d584674c879f662a418abba3a220f1fee88471ca49322ac9873de628d"
|
||||
},
|
||||
"generated-release1/project.arcadia": {
|
||||
"bytes": 1958,
|
||||
"sha256": "b2674e6bf77cfe0f6f0ba79c81d2e96d92f39c02ca2c6266d80e91723bfb9fda"
|
||||
},
|
||||
"generated-release1/sa/behavior/process_sa_assessment_procedure.arcadia": {
|
||||
"bytes": 559,
|
||||
"sha256": "5c414fbdaabf9f9e402b0441fcacf1cacf4dbbc6c822162f028de35abf59be2d"
|
||||
},
|
||||
"generated-release1/sa/behavior/process_sa_field_procedure.arcadia": {
|
||||
"bytes": 1187,
|
||||
"sha256": "ece846fdf93857e71a9ab0efcbdf64a5c41c71180f62afa3f79077fffb53b8f2"
|
||||
},
|
||||
"generated-release1/sa/behavior/process_sa_response.arcadia": {
|
||||
"bytes": 2569,
|
||||
"sha256": "af7fdda280a917cf626f6e077162d481f9aaad280f8e81330c07d7fe384b1c52"
|
||||
},
|
||||
"generated-release1/sa/behavior/scenario_sa_conversation.arcadia": {
|
||||
"bytes": 1847,
|
||||
"sha256": "01339699ec3ee409418d9bb232542f2bcd9419f1e67b183e77700c56bb9f71d2"
|
||||
},
|
||||
"generated-release1/sa/behavior/scenario_sa_full_scenario.arcadia": {
|
||||
"bytes": 1116,
|
||||
"sha256": "5dce6b00a3a441162e8dee3e23d9ca5ab7bed4a24de669b1fba03664a3086dda"
|
||||
},
|
||||
"generated-release1/sa/behavior/state_machine_sa_monitor.arcadia": {
|
||||
"bytes": 1358,
|
||||
"sha256": "17dbcfa5bbfd10fc8e23a325fc950ce853c219b4aa89c74f96278cbb3f34acc8"
|
||||
},
|
||||
"generated-release1/sa/capabilities.arcadia": {
|
||||
"bytes": 630,
|
||||
"sha256": "81706a0ca9d7db2aa0350888b9c00d853885185db2e6b90557d3d51a11c3d645"
|
||||
},
|
||||
"generated-release1/sa/functions/function_sa_audit_thresholds.arcadia": {
|
||||
"bytes": 280,
|
||||
"sha256": "27f798f78f36e72b38edab3725af2feb0eaa010fd52a60b5c6b6bef57ad3b686"
|
||||
},
|
||||
"generated-release1/sa/functions/function_sa_restore_work.arcadia": {
|
||||
"bytes": 2251,
|
||||
"sha256": "357c6acb80f86b4e8d8f5c98f9f79d140f85fbe09f3406b41b7ac6eba464ec8c"
|
||||
},
|
||||
"generated-release1/sa/model.arcadia": {
|
||||
"bytes": 531,
|
||||
"sha256": "805b90262e27090ae7da81a51bcefa49c97fb0b3d3220b4a72323b4c4351c395"
|
||||
},
|
||||
"generated-release1/sa/participants.arcadia": {
|
||||
"bytes": 3604,
|
||||
"sha256": "ca144d6a8f45fe720b5d18c7b59c7671444baa8c37e5ce9117bcba34b3a6aa84"
|
||||
},
|
||||
"generated-release1/sa/types.arcadia": {
|
||||
"bytes": 903,
|
||||
"sha256": "540ddbc2bcd0ccf45298e3ea5660637341aff63ff65fd87ac2f26469d1adc064"
|
||||
},
|
||||
"generated-release1/traceability.arcadia": {
|
||||
"bytes": 28041,
|
||||
"sha256": "474bd28d955ff7af7722f6e9a23220d816879e3f46202e79a233544b9c593116"
|
||||
},
|
||||
"generated-release1/transition-before.json": {
|
||||
"bytes": 35801,
|
||||
"sha256": "2253e6434f741687e22327c37d520ed15c043bc36025498650bc2366bb6a874d"
|
||||
},
|
||||
"generated-release1/transition-request.json": {
|
||||
"bytes": 4276,
|
||||
"sha256": "43b9a22e5f95ad3bc0ebd2478ead0e14406854abc989ae94ded9367b013d812a"
|
||||
},
|
||||
"generated-release1/views/diagram_oa_allocation_view.arcadia": {
|
||||
"bytes": 1786,
|
||||
"sha256": "c45d7467839219d894804c1ef2d22ff4d4a5a84a6a273c526bafd4c1facde624"
|
||||
},
|
||||
"generated-release1/views/diagram_sa_allocation_view.arcadia": {
|
||||
"bytes": 1797,
|
||||
"sha256": "352a3ca745517592b5c9b1239b5770f62f0466a329a91fd03457ef2b53c06ed6"
|
||||
},
|
||||
"generated-release1/views/diagram_sa_capability_view.arcadia": {
|
||||
"bytes": 1286,
|
||||
"sha256": "8f18fcccbcddcf3c1ea0ffe727b5b23b77fc148c5da4fde953abe681e4c20cd2"
|
||||
},
|
||||
"generated-release1/views/diagram_sa_service_view.arcadia": {
|
||||
"bytes": 1960,
|
||||
"sha256": "4c72cb7593d9c4be8d2600b0656a7a3a6ce84c45bb2f3e58a4ee5ae3bf91913c"
|
||||
},
|
||||
"generated-release1/views/diagram_sa_state_view.arcadia": {
|
||||
"bytes": 1576,
|
||||
"sha256": "dd1bea81ac7dc549a53f9b190be6f13052e00ede345bd180444cc31edb892230"
|
||||
},
|
||||
"helpers/composition.json": {
|
||||
"bytes": 1190,
|
||||
"sha256": "78a98f3a034cda4d0cc1abfe33efb785e847bb6278b49c2071d70ee5f1dc23f3"
|
||||
},
|
||||
"helpers/gate-union.py": {
|
||||
"bytes": 3495,
|
||||
"sha256": "9cca2646b1bcf09961a9ad1a276d633d8e4296ff2c6f2f356f028afaf789f503"
|
||||
},
|
||||
"helpers/ordinary-contract.patch": {
|
||||
"bytes": 7301,
|
||||
"sha256": "931a5822dbfa5ad34a0dc961035e060ccdc7aed15712c4a9b5dc15a852675f71"
|
||||
},
|
||||
"helpers/ordinary.patch": {
|
||||
"bytes": 145928,
|
||||
"sha256": "e319a5151b7e6713880eaaab632f3f1c01aee6df3cd534d85a1c099fb501dcb7"
|
||||
},
|
||||
"helpers/render-language.py": {
|
||||
"bytes": 1009,
|
||||
"sha256": "82909cf11b0bdb0594b424cba51e71ec0abdead78d878f101b7ae8ff6c4281f8"
|
||||
},
|
||||
"helpers/required-desktop-tests.json": {
|
||||
"bytes": 125261,
|
||||
"sha256": "6672e3f7ccfb92b7710310836af6933a8e58ef113353505ec4fc0c3d6908c136"
|
||||
},
|
||||
"inputs/0fa1da63d71c984cd99b079de661e88154b5a31818b384f557deefd40b8ead3a.json": {
|
||||
"bytes": 118403,
|
||||
"sha256": "0fa1da63d71c984cd99b079de661e88154b5a31818b384f557deefd40b8ead3a"
|
||||
},
|
||||
"inputs/12ce1ca9c1185d7a91a1dea322cdc0c7fc4372db5d879386595acdbfd412d880.zip": {
|
||||
"bytes": 2830239,
|
||||
"sha256": "12ce1ca9c1185d7a91a1dea322cdc0c7fc4372db5d879386595acdbfd412d880"
|
||||
},
|
||||
"inputs/81bd006f10034bc0c203187af179548a549fda6e58bc7cde0702cc26994fd28d.zip": {
|
||||
"bytes": 2828619,
|
||||
"sha256": "81bd006f10034bc0c203187af179548a549fda6e58bc7cde0702cc26994fd28d"
|
||||
},
|
||||
"inputs/86ac50e9cec8b39db91bbc98e5388e8c0f8466e6c217cf2f4eca5f1db087dde1.zip": {
|
||||
"bytes": 2831327,
|
||||
"sha256": "86ac50e9cec8b39db91bbc98e5388e8c0f8466e6c217cf2f4eca5f1db087dde1"
|
||||
},
|
||||
"inputs/bbe8290d9cb11a7ee5dba250350451c5b7ec88069aa65f6be786a67922408c48.json": {
|
||||
"bytes": 118403,
|
||||
"sha256": "bbe8290d9cb11a7ee5dba250350451c5b7ec88069aa65f6be786a67922408c48"
|
||||
},
|
||||
"inputs/e3d2d504b18bb1f7c4c3fa1f26abff425b957f18b418d8556888151723ba67b4.json": {
|
||||
"bytes": 118404,
|
||||
"sha256": "e3d2d504b18bb1f7c4c3fa1f26abff425b957f18b418d8556888151723ba67b4"
|
||||
},
|
||||
"render/language.html": {
|
||||
"bytes": 188141,
|
||||
"sha256": "57e71090f56ae27aa50f757886d6cadf63d49f5641fe4a365547cbd9e59685b6"
|
||||
},
|
||||
"render/language.rst": {
|
||||
"bytes": 142931,
|
||||
"sha256": "5deafe3f75a503ab5fb3d05c5ad8f126d82668f89bef02067ce5d56c3045a2bb"
|
||||
},
|
||||
"runs/consumers-1/process.json": {
|
||||
"bytes": 257,
|
||||
"sha256": "85b18634abd4ccf90144b44b01cbcade2521ef6706b2e46abc2e1d60a4dfc346"
|
||||
},
|
||||
"runs/consumers-1/result.json": {
|
||||
"bytes": 6986,
|
||||
"sha256": "5501663ed1f845b8d18487c90fb827faab2a1c2319cf8ea8c127324fdd9add15"
|
||||
},
|
||||
"runs/consumers-1/started.json": {
|
||||
"bytes": 1488,
|
||||
"sha256": "908dda6bce0013b3c5db9bc5cf678a9df3b221845e9c29d29409bf440bab52d3"
|
||||
},
|
||||
"runs/consumers-1/stderr.log": {
|
||||
"bytes": 652,
|
||||
"sha256": "6dca8394973a95c080a0b8858926efe7779512adf0fd5e64b47d1239f9446618"
|
||||
},
|
||||
"runs/consumers-1/stdout.log": {
|
||||
"bytes": 144498,
|
||||
"sha256": "f1ff82c06e0e1b0f0c3eadb5533402bfea42983c89fa3581340768f3d953de3e"
|
||||
},
|
||||
"runs/core-1/process.json": {
|
||||
"bytes": 197,
|
||||
"sha256": "1bf0f358d94b6e95eab5cf3823a4230a72f0363b77c75b8222c73f1122739f21"
|
||||
},
|
||||
"runs/core-1/result.json": {
|
||||
"bytes": 67077,
|
||||
"sha256": "a9f111627d5a2a7fa33e525cbd43150b803505e3e201b8465f94d2060fce143e"
|
||||
},
|
||||
"runs/core-1/started.json": {
|
||||
"bytes": 1398,
|
||||
"sha256": "56fb8a8824c031ed8f831561241a3bf321158f75cc0a2acdca6b0a77d9b648f6"
|
||||
},
|
||||
"runs/core-1/stderr.log": {
|
||||
"bytes": 12121,
|
||||
"sha256": "ca7ae2dfd0b6eecec811fce5831ea0dc7fb3b6209ffcad88d45eb6c5afd65103"
|
||||
},
|
||||
"runs/core-1/stdout.log": {
|
||||
"bytes": 131218,
|
||||
"sha256": "0d683632d1046f102e6b70af56d67ea6841134d99c923f0648261b4832a860fa"
|
||||
},
|
||||
"runs/core-consumers-1/process.json": {
|
||||
"bytes": 274,
|
||||
"sha256": "16bba7d4e4e3eb39c0ae89ecdbbfe38818c2ed329a32ac52a8db3cea69ed7fd4"
|
||||
},
|
||||
"runs/core-consumers-1/result.json": {
|
||||
"bytes": 1907,
|
||||
"sha256": "4bb8e79cd647aacebe2c4790a18ac37d07bbc29b0f7c8eca1933f1c5020474f8"
|
||||
},
|
||||
"runs/core-consumers-1/started.json": {
|
||||
"bytes": 1515,
|
||||
"sha256": "2ff77ca7601800340bf458d0707aaeef742a010ec7f6a55cf1577a1c33f5c543"
|
||||
},
|
||||
"runs/core-consumers-1/stderr.log": {
|
||||
"bytes": 327,
|
||||
"sha256": "5ded46d388f7ecc597c5e468621e84a297b1231f4eab8c2aa413697dd005e286"
|
||||
},
|
||||
"runs/core-consumers-1/stdout.log": {
|
||||
"bytes": 0,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"runs/language-doc-1/warnings.log": {
|
||||
"bytes": 144,
|
||||
"sha256": "82cf5ddae4d21aa3730437a7962e6582507aa8032cc75fb8164ff7cf9003b7ae"
|
||||
},
|
||||
"runs/language-doc-2/result.json": {
|
||||
"bytes": 415,
|
||||
"sha256": "620a449b2995fbbb27aa769cadf603dd01d9f7036c595d012e66b74469102bde"
|
||||
},
|
||||
"runs/language-doc-2/warnings.log": {
|
||||
"bytes": 0,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"runs/teaching-check-2/process.json": {
|
||||
"bytes": 210,
|
||||
"sha256": "18e9d3ee3e9c0d2d21c196c811ae543c899cc4580ad3288eca3a544f20f7a9cc"
|
||||
},
|
||||
"runs/teaching-check-2/result.json": {
|
||||
"bytes": 3147,
|
||||
"sha256": "cfc8dad31b1d8ba45face31f7986a30066c3bc464d19bd3b8495c9a013b45c0e"
|
||||
},
|
||||
"runs/teaching-check-2/started.json": {
|
||||
"bytes": 1416,
|
||||
"sha256": "259944f97150c87b2c95e9f53ae5cbb09264b03284510c9ac44272a70fdc9c87"
|
||||
},
|
||||
"runs/teaching-check-2/stderr.log": {
|
||||
"bytes": 218,
|
||||
"sha256": "9c90adb28961f087f33b905352c50c5d2aaa46785f47f3061dbe7dc161bc95d9"
|
||||
},
|
||||
"runs/teaching-check-2/stdout.log": {
|
||||
"bytes": 42153,
|
||||
"sha256": "d53a7d2d3d726302111aab71208814f487489080d250c1f555ad18312e80d86c"
|
||||
},
|
||||
"runs/teaching-generate-1/process.json": {
|
||||
"bytes": 252,
|
||||
"sha256": "c97f65ec53807bca963754b767ab21a88c3476c3639f0b54b38880281aff8a22"
|
||||
},
|
||||
"runs/teaching-generate-1/result.json": {
|
||||
"bytes": 3171,
|
||||
"sha256": "bd0095f78b4ff3cbaf7c1c3183bc280dbb1eec1cb68a528507af27ac35f34f83"
|
||||
},
|
||||
"runs/teaching-generate-1/started.json": {
|
||||
"bytes": 1458,
|
||||
"sha256": "e0526c09c8baa67bdb753ed1b0f15e237d3db2b1c60f097a145a141b9d174169"
|
||||
},
|
||||
"runs/teaching-generate-1/stderr.log": {
|
||||
"bytes": 229,
|
||||
"sha256": "464f44f1eaa5cabe25b7f66bcd689912977482c3eaad0e65328e0210749491fa"
|
||||
},
|
||||
"runs/teaching-generate-1/stdout.log": {
|
||||
"bytes": 41964,
|
||||
"sha256": "c7b195917f9b5b15aaf654703349b713b788f4cfbf00dce966539ff0764f892b"
|
||||
},
|
||||
"runs/windows-dependencies-1/process.json": {
|
||||
"bytes": 146,
|
||||
"sha256": "3901471653ef98f1abe326244dc639c6a2f0f53d3fff72db82acf068d8afabf9"
|
||||
},
|
||||
"runs/windows-dependencies-1/result.json": {
|
||||
"bytes": 1712,
|
||||
"sha256": "f1aa540938e09c5f4b92d13b293861a9e2cd08681e3f6b285b9e85f848a44935"
|
||||
},
|
||||
"runs/windows-dependencies-1/started.json": {
|
||||
"bytes": 1321,
|
||||
"sha256": "a54a7f78b455414d592b7ca3daf3d824487583a844097db76265e564bfb531d7"
|
||||
},
|
||||
"runs/windows-dependencies-1/stderr.log": {
|
||||
"bytes": 117,
|
||||
"sha256": "a82eeeb2d2fffd4d9e5d6e20ef76ed29e9e365923e2cc33bac0ab8f21a7a88d8"
|
||||
},
|
||||
"runs/windows-dependencies-1/stdout.log": {
|
||||
"bytes": 0,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
}
|
||||
},
|
||||
"generatedFiles": [
|
||||
"generated-release1/expected-diagnostics.json",
|
||||
"generated-release1/model.json",
|
||||
"generated-release1/oa/activities/activity_oa_restore_work.arcadia",
|
||||
"generated-release1/oa/behavior/process_oa_assessment_procedure.arcadia",
|
||||
"generated-release1/oa/behavior/process_oa_field_procedure.arcadia",
|
||||
"generated-release1/oa/behavior/process_oa_response.arcadia",
|
||||
"generated-release1/oa/behavior/scenario_oa_conversation.arcadia",
|
||||
"generated-release1/oa/behavior/scenario_oa_full_scenario.arcadia",
|
||||
"generated-release1/oa/behavior/state_machine_oa_monitor.arcadia",
|
||||
"generated-release1/oa/capabilities.arcadia",
|
||||
"generated-release1/oa/model.arcadia",
|
||||
"generated-release1/oa/participants.arcadia",
|
||||
"generated-release1/oa/types.arcadia",
|
||||
"generated-release1/project.arcadia",
|
||||
"generated-release1/sa/behavior/process_sa_assessment_procedure.arcadia",
|
||||
"generated-release1/sa/behavior/process_sa_field_procedure.arcadia",
|
||||
"generated-release1/sa/behavior/process_sa_response.arcadia",
|
||||
"generated-release1/sa/behavior/scenario_sa_conversation.arcadia",
|
||||
"generated-release1/sa/behavior/scenario_sa_full_scenario.arcadia",
|
||||
"generated-release1/sa/behavior/state_machine_sa_monitor.arcadia",
|
||||
"generated-release1/sa/capabilities.arcadia",
|
||||
"generated-release1/sa/functions/function_sa_audit_thresholds.arcadia",
|
||||
"generated-release1/sa/functions/function_sa_restore_work.arcadia",
|
||||
"generated-release1/sa/model.arcadia",
|
||||
"generated-release1/sa/participants.arcadia",
|
||||
"generated-release1/sa/types.arcadia",
|
||||
"generated-release1/traceability.arcadia",
|
||||
"generated-release1/transition-before.json",
|
||||
"generated-release1/transition-request.json",
|
||||
"generated-release1/views/diagram_oa_allocation_view.arcadia",
|
||||
"generated-release1/views/diagram_sa_allocation_view.arcadia",
|
||||
"generated-release1/views/diagram_sa_capability_view.arcadia",
|
||||
"generated-release1/views/diagram_sa_service_view.arcadia",
|
||||
"generated-release1/views/diagram_sa_state_view.arcadia"
|
||||
],
|
||||
"generatedCount": 34
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
"""Read-only, Python-standard-library verification. Never executes archived code."""
|
||||
from pathlib import Path, PurePosixPath
|
||||
from datetime import datetime
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
|
||||
def require(ok, message):
|
||||
if not ok:
|
||||
raise ValueError(message)
|
||||
|
||||
def digest(data):
|
||||
return {'bytes': len(data), 'sha256': hashlib.sha256(data).hexdigest()}
|
||||
|
||||
def members(z, expected):
|
||||
names = z.namelist()
|
||||
require(len(names) == len(set(names)), 'duplicate ZIP members')
|
||||
require(set(names) == set(expected), 'archive membership mismatch')
|
||||
require(all(not PurePosixPath(n).is_absolute() and '..' not in PurePosixPath(n).parts
|
||||
and '\\' not in n and ':' not in n for n in names), 'unsafe member')
|
||||
|
||||
def main():
|
||||
inventory = json.loads((ROOT / 'files.json').read_text())
|
||||
for name, expected in inventory.items():
|
||||
require(digest((ROOT / name).read_bytes()) == expected, 'external file hash: ' + name)
|
||||
manifest = json.loads((ROOT / 'manifest.json').read_text())
|
||||
with zipfile.ZipFile(ROOT / 'evidence.zip') as z:
|
||||
members(z, manifest['members'])
|
||||
for name, expected in manifest['members'].items():
|
||||
require(digest(z.read(name)) == expected, 'member hash: ' + name)
|
||||
def raw(name): return z.read(name)
|
||||
def obj(name): return json.loads(raw(name))
|
||||
checked = set()
|
||||
for run, spec in manifest['runs'].items():
|
||||
if spec['kind'] != 'cargo':
|
||||
continue
|
||||
prefix = 'runs/' + run + '/'
|
||||
start, result, process = (obj(prefix + n + '.json') for n in ('started', 'result', 'process'))
|
||||
require(all(result[k] == v for k, v in start.items()), run + ': start/result relationship')
|
||||
require(result['command'] == spec['command'] == process['command'], run + ': command')
|
||||
require(result['head'] == spec['head'], run + ': HEAD')
|
||||
require(result['exitCode'] == spec['exitCode'] and result['inputsUnchanged'] is True, run + ': outcome')
|
||||
require(datetime.fromisoformat(result['finish']) >= datetime.fromisoformat(start['start']), run + ': times')
|
||||
require(result['seconds'] >= 0, run + ': duration')
|
||||
require(result['helper'] == digest(raw('helpers/gate-union.py')), run + ': helper')
|
||||
for field, file in [('inputs', spec['inputs']), ('archive', spec['archive']), ('stdout', prefix+'stdout.log'), ('stderr', prefix+'stderr.log')]:
|
||||
require(result[field] == digest(raw(file)), run + ': receipt ' + field)
|
||||
inputs = obj(spec['inputs'])
|
||||
require(len(inputs) == result['inputCount'] == spec['inputCount'], run + ': input count')
|
||||
require(sum(p.startswith('crates/vendor/iced_widget/') for p in inputs) == 71, run + ': vendor count')
|
||||
pair = (spec['inputs'], spec['archive'])
|
||||
if pair not in checked:
|
||||
with zipfile.ZipFile(io.BytesIO(raw(spec['archive']))) as source:
|
||||
members(source, inputs)
|
||||
for name, expected in inputs.items():
|
||||
require(digest(source.read(name)) == expected, run + ': source hash ' + name)
|
||||
checked.add(pair)
|
||||
stdout = raw(prefix+'stdout.log').decode('utf-8', errors='replace')
|
||||
rows = [[int(x) for x in row] for row in re.findall(r'test result: (?:ok|FAILED)\. (\d+) passed; (\d+) failed; (\d+) ignored; (\d+) measured; (\d+) filtered out;', stdout)]
|
||||
require(rows == spec['testRows'], run + ': exact test summaries')
|
||||
require([sum(row[i] for row in rows) for i in range(5)] == spec['testTotals'], run + ': test totals')
|
||||
artifacts = []
|
||||
for line in stdout.splitlines():
|
||||
try: item = json.loads(line)
|
||||
except json.JSONDecodeError: continue
|
||||
if isinstance(item, dict) and item.get('reason') == 'compiler-artifact' and item.get('executable'):
|
||||
artifacts.append(item)
|
||||
require(artifacts == [a['cargo'] for a in result['artifacts']], run + ': artifact receipt relationship')
|
||||
print(run, 'exit', result['exitCode'], 'test totals', spec['testTotals'])
|
||||
composition = obj('helpers/composition.json')
|
||||
require(composition['patch'] == digest(raw('helpers/ordinary.patch')), 'composition patch')
|
||||
doc = obj('runs/language-doc-2/result.json')
|
||||
require(doc['strict'] is True and doc['docutils'] == '0.23', 'render settings')
|
||||
for field, path in [('source','render/language.rst'), ('html','render/language.html'), ('warnings','runs/language-doc-2/warnings.log')]:
|
||||
require(doc[field] == digest(raw(path)), 'render receipt ' + field)
|
||||
require(raw('runs/language-doc-2/warnings.log') == b'', 'successful render warnings')
|
||||
require(bool(raw('runs/language-doc-1/warnings.log')), 'failed render warning missing')
|
||||
for path in manifest['generatedFiles']:
|
||||
require(path.startswith('generated-release1/') and path in manifest['members'], 'generated mapping')
|
||||
require(len(manifest['generatedFiles']) == manifest['generatedCount'], 'generated count')
|
||||
print('PASS:', len(manifest['runs']), 'closed captures;', len(manifest['members']), 'members;', len(checked), 'input manifest/archive pairs')
|
||||
print('Integrity and receipt consistency only; no rerun, signature, binary verification, or GUI claim.')
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,45 @@
|
||||
# Ordinary-text union: native trial evidence
|
||||
|
||||
Run `python verify.py` with Python 3.10 or newer. These five files form a portable
|
||||
read-only package. The verifier uses only the Python standard library, reads
|
||||
ZIPs without extraction, and executes neither archived helpers nor binaries.
|
||||
It requires no Git checkout, original machine, network, UI or dependency cache.
|
||||
Outer files are UTF-8 LF before hash pins; all archived raw bytes are unchanged.
|
||||
|
||||
The complete closed trial-1 capture set is preserved, including small auxiliary
|
||||
picker frames, extra captures, resumed-state capture, and repeated save-path
|
||||
attempts. Filename numbering labels recorded observations; it is not a complete
|
||||
action log and does not assert an unrecorded intermediate transition. Screenshots
|
||||
and accessibility snapshots support human review; their interpretation is not
|
||||
automatically proven by this verifier.
|
||||
|
||||
`native/independent-review.md` preserves the separate review of screenshots,
|
||||
source/CLI evidence and claim boundaries. Its judgments are human-readable review
|
||||
evidence, distinct from machine-verified byte and receipt relationships. Dialog
|
||||
states are archived; operator/tool calls are not a complete archived action log.
|
||||
|
||||
Archived material includes original input, both saved bundles, all trial receipts,
|
||||
logs and frames, capture/launch helpers, saved-bundle check helper and raw results.
|
||||
The exact successful windows-build-1 receipts, logs, gate helper, input manifest
|
||||
and 707-file input archive are included for a self-contained receipt chain.
|
||||
Those build captures also appear in the gates package committed at
|
||||
7836a93edb98d2f9dcbf9c0290352e38f9acbb39. The production build source is
|
||||
f714a25bbcd435941404104ad0cbb0fea3b89149. Binaries are intentionally excluded;
|
||||
hashes establish receipt consistency, not independent binary reproduction.
|
||||
|
||||
Verification checks every archive member/hash, nested source membership and
|
||||
content, build and native start/result/helper relationships, GUI/CLI binary
|
||||
receipt links, original input archive, capture timestamps/image references,
|
||||
normal GUI exit and empty logs. It recomputes exact saved-source membership and
|
||||
bytes: compact added activity and include, stable recorded IDs, and only the
|
||||
intended existing diagram x coordinate changing from 120 to 190 between saved
|
||||
bundles. Both CLI captures report checked status, four warnings, no writes or
|
||||
changes, and zero identities to stabilize.
|
||||
|
||||
These are saved-byte and recorded CLI parsing/validation oracles, not a comparison
|
||||
of the complete parsed models. Undo/Redo, reopening, diagnostic freshness and
|
||||
navigation observations require inspecting the actual frames and review. Empty
|
||||
logs and exit zero alone do not establish usability or complete acceptance.
|
||||
The hashes are integrity checks, not authenticated provenance. Raw captures
|
||||
retain original local path strings and timestamps; no credentials, user profile,
|
||||
runtime binaries or dependency cache are packaged.
|
||||
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"evidence.zip": {
|
||||
"bytes": 5704887,
|
||||
"sha256": "b827d0bdaec76b7b86d8a169530dcf98767c9883187ecdda02b4ea87171a42b5"
|
||||
},
|
||||
"manifest.json": {
|
||||
"bytes": 24911,
|
||||
"sha256": "49d0bf1c4c4732c9db8a44ae525fb4516cca4883e1a209aa736b913550396037"
|
||||
},
|
||||
"README.md": {
|
||||
"bytes": 2931,
|
||||
"sha256": "2422dd76bfcd3e81d57d1db48db27b36e7505ff6b17a68c3b393518f623de256"
|
||||
},
|
||||
"verify.py": {
|
||||
"bytes": 9043,
|
||||
"sha256": "7242e9ecc68f21349f0073379391d7890367e0b4f5f32697af306cc2346e50d0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,624 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"productionCommit": "f714a25bbcd435941404104ad0cbb0fea3b89149",
|
||||
"relatedGatesCommit": "7836a93edb98d2f9dcbf9c0290352e38f9acbb39",
|
||||
"captures": [
|
||||
"native/trial-1/01-start.json",
|
||||
"native/trial-1/02-open-dialog.json",
|
||||
"native/trial-1/03-open-dialog-ready.json",
|
||||
"native/trial-1/04-open-picker.json",
|
||||
"native/trial-1/05-open-filename-focus.json",
|
||||
"native/trial-1/06-open-fixture-path.json",
|
||||
"native/trial-1/07-opened-fixture.json",
|
||||
"native/trial-1/08-model-explorer.json",
|
||||
"native/trial-1/09-created-activity.json",
|
||||
"native/trial-1/10-text-created-source.json",
|
||||
"native/trial-1/11-source-folder.json",
|
||||
"native/trial-1/12-activity-folder.json",
|
||||
"native/trial-1/13-compact-activity.json",
|
||||
"native/trial-1/14-save-before-dialog.json",
|
||||
"native/trial-1/15-save-before-picker.json",
|
||||
"native/trial-1/16-save-before-focus.json",
|
||||
"native/trial-1/17-save-before-selected.json",
|
||||
"native/trial-1/18-save-before-selection-confirmed.json",
|
||||
"native/trial-1/19-save-before-path.json",
|
||||
"native/trial-1/20-baseline-saved.json",
|
||||
"native/trial-1/21-before-drag.json",
|
||||
"native/trial-1/22-after-drag.json",
|
||||
"native/trial-1/23-text-after-drag.json",
|
||||
"native/trial-1/24-root-after-drag.json",
|
||||
"native/trial-1/25-text-actions.json",
|
||||
"native/trial-1/25a-resumed-state.json",
|
||||
"native/trial-1/26-model-undo.json",
|
||||
"native/trial-1/27-redo-menu.json",
|
||||
"native/trial-1/28-model-redo.json",
|
||||
"native/trial-1/29-save-after-pending.json",
|
||||
"native/trial-1/30-save-after-dialog.json",
|
||||
"native/trial-1/31a-save-after-field.json",
|
||||
"native/trial-1/32-save-after-path.json",
|
||||
"native/trial-1/33-save-after-path-ready.json",
|
||||
"native/trial-1/34-save-after-path.json",
|
||||
"native/trial-1/35-saved-after.json",
|
||||
"native/trial-1/36-new-cleared.json",
|
||||
"native/trial-1/37-home-reopen.json",
|
||||
"native/trial-1/38-reopened-after.json",
|
||||
"native/trial-1/39-reopened-source.json",
|
||||
"native/trial-1/40-problems-menu.json",
|
||||
"native/trial-1/41-fresh-diagnostics.json",
|
||||
"native/trial-1/42-fresh-warning-navigation.json",
|
||||
"native/trial-1/43-comment-insertion-point.json",
|
||||
"native/trial-1/44-edited-diagnostics-stale.json",
|
||||
"native/trial-1/45-refreshed-diagnostics.json",
|
||||
"native/trial-1/46-refreshed-warning-navigation.json",
|
||||
"native/trial-1/47-restore-menu.json",
|
||||
"native/trial-1/48-diagnostic-edit-undone.json"
|
||||
],
|
||||
"captureCount": 49,
|
||||
"imageCount": 62,
|
||||
"members": {
|
||||
"build/gate-union.py": {
|
||||
"bytes": 3495,
|
||||
"sha256": "9cca2646b1bcf09961a9ad1a276d633d8e4296ff2c6f2f356f028afaf789f503"
|
||||
},
|
||||
"build/inputs.json": {
|
||||
"bytes": 118403,
|
||||
"sha256": "a36e2cbc29eb7a9217774464cda9da8aa1d3c39caae9b9aacd6cdb28df5bad95"
|
||||
},
|
||||
"build/inputs.zip": {
|
||||
"bytes": 2831326,
|
||||
"sha256": "9346fba1be9bf4dd2225fe9965fd76a30429f086fd6a101f92d0963d3822f71c"
|
||||
},
|
||||
"build/process.json": {
|
||||
"bytes": 167,
|
||||
"sha256": "b400c1f8076bcddb583424fe34d155655a41d25b5ccf0db8af26bda6b9b2685e"
|
||||
},
|
||||
"build/result.json": {
|
||||
"bytes": 5532,
|
||||
"sha256": "5e9e227c66a4b71ade734b8ebeb12070b8a5c64a9a6cf9d8d0b6d70379d4ffc2"
|
||||
},
|
||||
"build/started.json": {
|
||||
"bytes": 1353,
|
||||
"sha256": "15c54be09a881e6860735395569b22a7a9114789a1f8276cf79bc6fb095c8163"
|
||||
},
|
||||
"build/stderr.log": {
|
||||
"bytes": 261,
|
||||
"sha256": "480dff999944c756fff581db0f3fac088717e0d73cae20af9a28829446df746f"
|
||||
},
|
||||
"build/stdout.log": {
|
||||
"bytes": 295092,
|
||||
"sha256": "5a824ca91877328acd332585b39194b27fd2cb167279b99c4c78a89bf13bd19b"
|
||||
},
|
||||
"native/bundle-checks/saved-after.result.json": {
|
||||
"bytes": 1930,
|
||||
"sha256": "34e2e8754026f35f4d57b24f8bde39f5b354aea703cd9c6650c673f674c2d167"
|
||||
},
|
||||
"native/bundle-checks/saved-after.stderr.log": {
|
||||
"bytes": 0,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"native/bundle-checks/saved-after.stdout.json": {
|
||||
"bytes": 2592,
|
||||
"sha256": "095f83e38f6c59b69fd72d0d7d0825b7849d34b963ff35e7f9a4ab7434bae5f2"
|
||||
},
|
||||
"native/bundle-checks/saved-before.result.json": {
|
||||
"bytes": 1931,
|
||||
"sha256": "52d3f54ce37412fe6f0bf23484d7ada549ed26f8b16501e0ddff1261df673f6c"
|
||||
},
|
||||
"native/bundle-checks/saved-before.stderr.log": {
|
||||
"bytes": 0,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"native/bundle-checks/saved-before.stdout.json": {
|
||||
"bytes": 2593,
|
||||
"sha256": "966ee3fac987156a8b9e5e1ebf9d10dd7f7a89c84d0f42645874d153a404517e"
|
||||
},
|
||||
"native/bundle-checks/summary.json": {
|
||||
"bytes": 484,
|
||||
"sha256": "7f9f6ddd98b303caf0edebef19aa507257c176168f94218ac86f3ac180d13631"
|
||||
},
|
||||
"native/capture-snippet.js": {
|
||||
"bytes": 1579,
|
||||
"sha256": "55279ed080289ccf5ba482cb24b5a807669a9360012e2d87f794d4465227cb2c"
|
||||
},
|
||||
"native/check-saved-bundles.py": {
|
||||
"bytes": 4216,
|
||||
"sha256": "beaf19c8d07e63fbc462c555f5e486fc6d035f0175ae9567ac1217414a63038c"
|
||||
},
|
||||
"native/independent-review.md": {
|
||||
"bytes": 5534,
|
||||
"sha256": "7e952270478fb0f57b0ed44d54bba6c2b09b5c315cebe11ac7ab613155ed299f"
|
||||
},
|
||||
"native/input/project.arcadia": {
|
||||
"bytes": 451,
|
||||
"sha256": "375c80aaf2b26bb7a638e79d76e4374454e0a86b9698b888d39dc5cad0206818"
|
||||
},
|
||||
"native/run-native.py": {
|
||||
"bytes": 2609,
|
||||
"sha256": "2d2a63bcfccad32c2b5db09cd723dcff9877d3cbe813853e278cb3df49ed7b88"
|
||||
},
|
||||
"native/saved-after/oa/activities/activity_new_activity.arcadia": {
|
||||
"bytes": 89,
|
||||
"sha256": "7d2a6a27fecb94eaf42a13b330fed5a269d0e80cad75688a9f26d4e08db0dfe7"
|
||||
},
|
||||
"native/saved-after/project.arcadia": {
|
||||
"bytes": 511,
|
||||
"sha256": "c93bd35eaaf3a19bc07b5c6a4ee5839ab565e9fd90a2605261b7618fc8a7ecef"
|
||||
},
|
||||
"native/saved-before/oa/activities/activity_new_activity.arcadia": {
|
||||
"bytes": 89,
|
||||
"sha256": "7d2a6a27fecb94eaf42a13b330fed5a269d0e80cad75688a9f26d4e08db0dfe7"
|
||||
},
|
||||
"native/saved-before/project.arcadia": {
|
||||
"bytes": 511,
|
||||
"sha256": "dad153f45038ed9ae79ea7263364e6cdedc40737fe7f63fdad7fb198ee7f65c8"
|
||||
},
|
||||
"native/trial-1/01-start-0.jpg": {
|
||||
"bytes": 117627,
|
||||
"sha256": "0778c21336a401d30316ed3ef5c93abe9ed349082a64cf78e8643edc0b29ee9e"
|
||||
},
|
||||
"native/trial-1/01-start.json": {
|
||||
"bytes": 919,
|
||||
"sha256": "986aab266f45d9871bd5b134d318a38ce7d8a7c992e2a78a38c54dcfa4a86baa"
|
||||
},
|
||||
"native/trial-1/02-open-dialog-0.jpg": {
|
||||
"bytes": 116197,
|
||||
"sha256": "e6248082f0dbe81d8d387906686538061ce1cf0f1be4b68aa70156238ea991c0"
|
||||
},
|
||||
"native/trial-1/02-open-dialog.json": {
|
||||
"bytes": 925,
|
||||
"sha256": "e9852679994314e8ede462a414787841ed8254b8ea660f83543b1bf4e4ab3979"
|
||||
},
|
||||
"native/trial-1/03-open-dialog-ready-0.jpg": {
|
||||
"bytes": 90928,
|
||||
"sha256": "2a931629a94f94660af8b189f919ab3e41400ca860ae8eac486f4b90184083ce"
|
||||
},
|
||||
"native/trial-1/03-open-dialog-ready.json": {
|
||||
"bytes": 735,
|
||||
"sha256": "5aea33223bfdb19456c182f9c7cd55eaedaa8c62a6af63c33b5d334d6b7a4af6"
|
||||
},
|
||||
"native/trial-1/04-open-picker-0.jpg": {
|
||||
"bytes": 674,
|
||||
"sha256": "5a2387a1a593a32ea169b56b9a3e59333936ee1da95164564e4d9be61bf5a24b"
|
||||
},
|
||||
"native/trial-1/04-open-picker-1.jpg": {
|
||||
"bytes": 53413,
|
||||
"sha256": "61176947f83f35fe13aa1e31b0aa61ea478d0537b9a08fbf45ab6442ab4e2296"
|
||||
},
|
||||
"native/trial-1/04-open-picker.json": {
|
||||
"bytes": 4951,
|
||||
"sha256": "c8f356251dafcf9465555c629d32e68d6d905d5c221e9580853f6f378123dbe4"
|
||||
},
|
||||
"native/trial-1/05-open-filename-focus-0.jpg": {
|
||||
"bytes": 674,
|
||||
"sha256": "5a2387a1a593a32ea169b56b9a3e59333936ee1da95164564e4d9be61bf5a24b"
|
||||
},
|
||||
"native/trial-1/05-open-filename-focus-1.jpg": {
|
||||
"bytes": 53809,
|
||||
"sha256": "fc29b6c470522c8d8c024a9ce43477b507525b4c5ad6482f812f69b526a321eb"
|
||||
},
|
||||
"native/trial-1/05-open-filename-focus.json": {
|
||||
"bytes": 4967,
|
||||
"sha256": "4ad084f9d123899129c9c7cb4faa53f4727b14bc901fc1cc1dce45e0a15702b4"
|
||||
},
|
||||
"native/trial-1/06-open-fixture-path-0.jpg": {
|
||||
"bytes": 674,
|
||||
"sha256": "5a2387a1a593a32ea169b56b9a3e59333936ee1da95164564e4d9be61bf5a24b"
|
||||
},
|
||||
"native/trial-1/06-open-fixture-path-1.jpg": {
|
||||
"bytes": 56049,
|
||||
"sha256": "fc7d2cd6b12111e9bca60cc786b82f1f8f42ee37654a29bd058345ce10b77eaf"
|
||||
},
|
||||
"native/trial-1/06-open-fixture-path.json": {
|
||||
"bytes": 4963,
|
||||
"sha256": "1eb51f68dc204ad419d54cc3d7bf1e5335b41bd65a083d23bed53dfc90f9a374"
|
||||
},
|
||||
"native/trial-1/07-opened-fixture-0.jpg": {
|
||||
"bytes": 98357,
|
||||
"sha256": "eddb3b2e85487bc4e53da399a2062c6bd138b6022216dc0b01c3cffc811c2c99"
|
||||
},
|
||||
"native/trial-1/07-opened-fixture.json": {
|
||||
"bytes": 956,
|
||||
"sha256": "d384ab6efa1fbf49e1b2afd93f933c64d865aa2b6baf6c7a69e2ac372da50449"
|
||||
},
|
||||
"native/trial-1/08-model-explorer-0.jpg": {
|
||||
"bytes": 90072,
|
||||
"sha256": "65a6e4f0545f6a5e104b9a2896e9b1f82851528a9e91651a31915d9a4f7a0058"
|
||||
},
|
||||
"native/trial-1/08-model-explorer.json": {
|
||||
"bytes": 963,
|
||||
"sha256": "3a772406fe219f21d954590f6c96f47adc9901d130c2d7bb087acc086cf6079b"
|
||||
},
|
||||
"native/trial-1/09-created-activity-0.jpg": {
|
||||
"bytes": 100572,
|
||||
"sha256": "115443b3ac12e32fc5a9d0736c9a475a431ed7423a84275a17fa1b4b10496185"
|
||||
},
|
||||
"native/trial-1/09-created-activity.json": {
|
||||
"bytes": 975,
|
||||
"sha256": "49a134749f520039a5390fc4b4110e5411822c6fb7573e31e1ffb1bff428a4d0"
|
||||
},
|
||||
"native/trial-1/10-text-created-source-0.jpg": {
|
||||
"bytes": 103031,
|
||||
"sha256": "6999c782d6b38719757898cd7d1575e71239fab882b040ce3125e823e734d995"
|
||||
},
|
||||
"native/trial-1/10-text-created-source.json": {
|
||||
"bytes": 978,
|
||||
"sha256": "56af19e09aa7d9bd4ef315a80d5060bfd52bd8e08d111c3647ead2bfff9f8b12"
|
||||
},
|
||||
"native/trial-1/11-source-folder-0.jpg": {
|
||||
"bytes": 103956,
|
||||
"sha256": "72656e598bfef0ad4355ca53d85b88a0d62ad186b3189b0a3e3f5900461e16b1"
|
||||
},
|
||||
"native/trial-1/11-source-folder.json": {
|
||||
"bytes": 972,
|
||||
"sha256": "e59af1c50e4f374cd840bd62fd0b8676e18e47a45ad820dd8b6a8dd835315c27"
|
||||
},
|
||||
"native/trial-1/12-activity-folder-0.jpg": {
|
||||
"bytes": 105653,
|
||||
"sha256": "2aff6c99a3845c334b94ba3e7b27fbdb3c31056784ce3ec4e76ef45ebb32911f"
|
||||
},
|
||||
"native/trial-1/12-activity-folder.json": {
|
||||
"bytes": 974,
|
||||
"sha256": "383be400c6317b571546e51d0c40128e7381b5ce1686ab3aaa20ece1a5e801ee"
|
||||
},
|
||||
"native/trial-1/13-compact-activity-0.jpg": {
|
||||
"bytes": 88781,
|
||||
"sha256": "87b367c8fe3017d221228ec63aa5192aaee19b4f80db6a07007b32f10f41d242"
|
||||
},
|
||||
"native/trial-1/13-compact-activity.json": {
|
||||
"bytes": 975,
|
||||
"sha256": "ba4daa10aeb0d9cd555d03cfbcb6bf5a7c6777d9247a46859f35d6fe740fa347"
|
||||
},
|
||||
"native/trial-1/14-save-before-dialog-0.jpg": {
|
||||
"bytes": 83376,
|
||||
"sha256": "9fd9bd5067f57dc0f297f5d02442e41e3395bd3f6964d726e852746c54a0cff7"
|
||||
},
|
||||
"native/trial-1/14-save-before-dialog.json": {
|
||||
"bytes": 977,
|
||||
"sha256": "91884e38ade4bd4fa93474a1076d84a4228b4d65a3d4c70afe296166150335b5"
|
||||
},
|
||||
"native/trial-1/15-save-before-picker-0.jpg": {
|
||||
"bytes": 674,
|
||||
"sha256": "5a2387a1a593a32ea169b56b9a3e59333936ee1da95164564e4d9be61bf5a24b"
|
||||
},
|
||||
"native/trial-1/15-save-before-picker-1.jpg": {
|
||||
"bytes": 55054,
|
||||
"sha256": "3529090d92bf9ea0375a8e95cc6f1b67d9159872367c89e3bf308e3d024a7ff7"
|
||||
},
|
||||
"native/trial-1/15-save-before-picker.json": {
|
||||
"bytes": 4764,
|
||||
"sha256": "febecff84014510968d802845126b9935a1c5b40a92dc2795d7bd05d4b11ecbf"
|
||||
},
|
||||
"native/trial-1/16-save-before-focus-0.jpg": {
|
||||
"bytes": 674,
|
||||
"sha256": "5a2387a1a593a32ea169b56b9a3e59333936ee1da95164564e4d9be61bf5a24b"
|
||||
},
|
||||
"native/trial-1/16-save-before-focus-1.jpg": {
|
||||
"bytes": 55425,
|
||||
"sha256": "2544f1ccc8ed5a21e682f6e3c836daa684a60f5ee0e4eb39de8ec4f05aca5daa"
|
||||
},
|
||||
"native/trial-1/16-save-before-focus.json": {
|
||||
"bytes": 4762,
|
||||
"sha256": "e07187e7179555f84a8b3ad7c87e707ba686945bf60dc89b2bfe86ce34d4fb42"
|
||||
},
|
||||
"native/trial-1/17-save-before-selected-0.jpg": {
|
||||
"bytes": 674,
|
||||
"sha256": "5a2387a1a593a32ea169b56b9a3e59333936ee1da95164564e4d9be61bf5a24b"
|
||||
},
|
||||
"native/trial-1/17-save-before-selected-1.jpg": {
|
||||
"bytes": 55437,
|
||||
"sha256": "7e12139b558f7427de23cf4a611c6a0a8b20be25e6a0b0799832ddc715ba1fd5"
|
||||
},
|
||||
"native/trial-1/17-save-before-selected.json": {
|
||||
"bytes": 4768,
|
||||
"sha256": "96d7d8468d87e24808617aea0cd42b52acdf4126c601b10e6f98b614beaf5f75"
|
||||
},
|
||||
"native/trial-1/18-save-before-selection-confirmed-0.jpg": {
|
||||
"bytes": 674,
|
||||
"sha256": "5a2387a1a593a32ea169b56b9a3e59333936ee1da95164564e4d9be61bf5a24b"
|
||||
},
|
||||
"native/trial-1/18-save-before-selection-confirmed-1.jpg": {
|
||||
"bytes": 54676,
|
||||
"sha256": "4cc51ff7fa2af6325879b442aea5eee1992f0fd41c4b6455fab3540601691a49"
|
||||
},
|
||||
"native/trial-1/18-save-before-selection-confirmed.json": {
|
||||
"bytes": 4790,
|
||||
"sha256": "a24fb0576c3339fa281c96d82055428d1e947ea8e0fbda3d9304fbec2410ea62"
|
||||
},
|
||||
"native/trial-1/19-save-before-path-0.jpg": {
|
||||
"bytes": 674,
|
||||
"sha256": "5a2387a1a593a32ea169b56b9a3e59333936ee1da95164564e4d9be61bf5a24b"
|
||||
},
|
||||
"native/trial-1/19-save-before-path-1.jpg": {
|
||||
"bytes": 57797,
|
||||
"sha256": "3e5f67d8125353318557c1fffc3aa3184e7870b79f57fb2b272f784a55438854"
|
||||
},
|
||||
"native/trial-1/19-save-before-path.json": {
|
||||
"bytes": 4760,
|
||||
"sha256": "0af8bed5fa5a3d93b59d046b48623992321d2436275d835ba39550df0edf3a89"
|
||||
},
|
||||
"native/trial-1/20-baseline-saved-0.jpg": {
|
||||
"bytes": 85738,
|
||||
"sha256": "2a1cb7c65ce2ae68c6c8e1b60c052fec4ccad5574da8e8900d452b41324150c0"
|
||||
},
|
||||
"native/trial-1/20-baseline-saved.json": {
|
||||
"bytes": 965,
|
||||
"sha256": "5990919d8cdb16e9f5445bb858b0d014015ef02d9157cbc2dc84f75a74f02b00"
|
||||
},
|
||||
"native/trial-1/21-before-drag-0.jpg": {
|
||||
"bytes": 101486,
|
||||
"sha256": "81f4bb4b0cb7314011b8acd829e9905993b596175139c482cef6030ec542e078"
|
||||
},
|
||||
"native/trial-1/21-before-drag.json": {
|
||||
"bytes": 960,
|
||||
"sha256": "e35a0357f5b334c8e519cb83116b2d516e9f19bb1ac794cfde0aaf3f6c98d62d"
|
||||
},
|
||||
"native/trial-1/22-after-drag-0.jpg": {
|
||||
"bytes": 103060,
|
||||
"sha256": "6a8f8c1bd011bd6224050236736e90a982c2102ec2a4700d09e7fc0ecd6ee9a6"
|
||||
},
|
||||
"native/trial-1/22-after-drag.json": {
|
||||
"bytes": 967,
|
||||
"sha256": "68e018c9d5b9e28b9a138f8085539bc9d87b0e0e48761049fd57d67334313715"
|
||||
},
|
||||
"native/trial-1/23-text-after-drag-0.jpg": {
|
||||
"bytes": 83460,
|
||||
"sha256": "dfb676eca325444b0383f47d056621d62c9cba3a522739a0015eb1d4a04a36f3"
|
||||
},
|
||||
"native/trial-1/23-text-after-drag.json": {
|
||||
"bytes": 974,
|
||||
"sha256": "100687443402acff33f2c9eba03a57d1146475e2225e6e90baa9d36624fec96f"
|
||||
},
|
||||
"native/trial-1/24-root-after-drag-0.jpg": {
|
||||
"bytes": 104490,
|
||||
"sha256": "25a2302b69bd367ea04c4b1ca28030c34e4a5b32a7c88c7c21c5a721fa69c60d"
|
||||
},
|
||||
"native/trial-1/24-root-after-drag.json": {
|
||||
"bytes": 974,
|
||||
"sha256": "ebbd8e200f431ad99760ed94895a74bc0b01d33f1fb67a6ab1464a1cf008c40a"
|
||||
},
|
||||
"native/trial-1/25-text-actions-0.jpg": {
|
||||
"bytes": 117335,
|
||||
"sha256": "4627be733b9c84798ff8324fe8bb8bba98dd7c8f3c7be5563d1a24c99e66b59c"
|
||||
},
|
||||
"native/trial-1/25-text-actions.json": {
|
||||
"bytes": 971,
|
||||
"sha256": "4d0a137244adf721a04dc17af1ae0ef380db737c3ab7478fbf6661bab24ce835"
|
||||
},
|
||||
"native/trial-1/25a-resumed-state-0.jpg": {
|
||||
"bytes": 117221,
|
||||
"sha256": "f509ed4e37b675d4cbcf1c075a983654b016d5459905a68b45514e24891ced3f"
|
||||
},
|
||||
"native/trial-1/25a-resumed-state.json": {
|
||||
"bytes": 973,
|
||||
"sha256": "304de4fa36d0b97b891455b26a14850b43bfd0811eb7e7be22bdcae9814da703"
|
||||
},
|
||||
"native/trial-1/26-model-undo-0.jpg": {
|
||||
"bytes": 103052,
|
||||
"sha256": "a8966f1244e9bbe9be4ad75278f3a492e748029c3fff3174ce38250997250a11"
|
||||
},
|
||||
"native/trial-1/26-model-undo.json": {
|
||||
"bytes": 959,
|
||||
"sha256": "9cd25b0f2d954f7772ae139c8d5afcb04ab1bf4c965aae6c6e22af37c352618b"
|
||||
},
|
||||
"native/trial-1/27-redo-menu-0.jpg": {
|
||||
"bytes": 117521,
|
||||
"sha256": "13634900648a485f38fd5183aa097ae544688b5a6112b821362b7128709d06c1"
|
||||
},
|
||||
"native/trial-1/27-redo-menu.json": {
|
||||
"bytes": 958,
|
||||
"sha256": "4cb0672342dd468ce4a403993e22613abe7498cdceb6111eb39747e7a24a8c86"
|
||||
},
|
||||
"native/trial-1/28-model-redo-0.jpg": {
|
||||
"bytes": 103295,
|
||||
"sha256": "8202074761aca9c067f5851a17c0586cc6698426473c1621defd3739cbbb817b"
|
||||
},
|
||||
"native/trial-1/28-model-redo.json": {
|
||||
"bytes": 969,
|
||||
"sha256": "252817aafe641c994ddba7fa244435371b4e75f6b9bbd16d355f02e1b35f0d9d"
|
||||
},
|
||||
"native/trial-1/29-save-after-pending-0.jpg": {
|
||||
"bytes": 102718,
|
||||
"sha256": "57a731a2b96cd9685ed0dc206a49b7804231d83d763503308fe73e6e326e725d"
|
||||
},
|
||||
"native/trial-1/29-save-after-pending.json": {
|
||||
"bytes": 977,
|
||||
"sha256": "32490225d11e793b31168cbd00ce184d38190c6e7c4420dba0e4c20424b19533"
|
||||
},
|
||||
"native/trial-1/30-save-after-dialog-0.jpg": {
|
||||
"bytes": 674,
|
||||
"sha256": "5a2387a1a593a32ea169b56b9a3e59333936ee1da95164564e4d9be61bf5a24b"
|
||||
},
|
||||
"native/trial-1/30-save-after-dialog-1.jpg": {
|
||||
"bytes": 56737,
|
||||
"sha256": "58dab12435a2f7b258e0c83ab0edd4a95379b0d6ec95f16184d63f8d76b94daf"
|
||||
},
|
||||
"native/trial-1/30-save-after-dialog.json": {
|
||||
"bytes": 5057,
|
||||
"sha256": "c9515daa0a573ee5510a421a205d1697835dfeaa3d2d69a64d93e4d66c94310e"
|
||||
},
|
||||
"native/trial-1/31a-save-after-field-0.jpg": {
|
||||
"bytes": 674,
|
||||
"sha256": "5a2387a1a593a32ea169b56b9a3e59333936ee1da95164564e4d9be61bf5a24b"
|
||||
},
|
||||
"native/trial-1/31a-save-after-field-1.jpg": {
|
||||
"bytes": 56737,
|
||||
"sha256": "58dab12435a2f7b258e0c83ab0edd4a95379b0d6ec95f16184d63f8d76b94daf"
|
||||
},
|
||||
"native/trial-1/31a-save-after-field.json": {
|
||||
"bytes": 5057,
|
||||
"sha256": "39e59a6120ad31528e55d379eeb2a463af05b5bea8710020c659687b414208fc"
|
||||
},
|
||||
"native/trial-1/32-save-after-path-0.jpg": {
|
||||
"bytes": 674,
|
||||
"sha256": "5a2387a1a593a32ea169b56b9a3e59333936ee1da95164564e4d9be61bf5a24b"
|
||||
},
|
||||
"native/trial-1/32-save-after-path-1.jpg": {
|
||||
"bytes": 55986,
|
||||
"sha256": "cb789f536317fff99d6f55133297a8d8511ef280a6b82c5e6e2960da6d0ceafe"
|
||||
},
|
||||
"native/trial-1/32-save-after-path.json": {
|
||||
"bytes": 5053,
|
||||
"sha256": "9167fd1352cfe5bb1b70631d1413cb7dac3a49311f459c39df374cdfe2d453d8"
|
||||
},
|
||||
"native/trial-1/33-save-after-path-ready-0.jpg": {
|
||||
"bytes": 674,
|
||||
"sha256": "5a2387a1a593a32ea169b56b9a3e59333936ee1da95164564e4d9be61bf5a24b"
|
||||
},
|
||||
"native/trial-1/33-save-after-path-ready-1.jpg": {
|
||||
"bytes": 56034,
|
||||
"sha256": "b41240172f775f45e4f19c1240784595004134baace690ccc5388097c9776104"
|
||||
},
|
||||
"native/trial-1/33-save-after-path-ready.json": {
|
||||
"bytes": 5065,
|
||||
"sha256": "00f3dc075eb6e446a6e7b85790a6d7e913151de53ff8041d9a63c6acd2597c47"
|
||||
},
|
||||
"native/trial-1/34-save-after-path-0.jpg": {
|
||||
"bytes": 674,
|
||||
"sha256": "5a2387a1a593a32ea169b56b9a3e59333936ee1da95164564e4d9be61bf5a24b"
|
||||
},
|
||||
"native/trial-1/34-save-after-path-1.jpg": {
|
||||
"bytes": 59217,
|
||||
"sha256": "182c41675727c07555b126f0e7876b32e7c8f141b0d7254e1511299318e08c9d"
|
||||
},
|
||||
"native/trial-1/34-save-after-path.json": {
|
||||
"bytes": 5053,
|
||||
"sha256": "2c356eeff45b49e9ce17919042929382027445ee07917a80eb8432581ede66e3"
|
||||
},
|
||||
"native/trial-1/35-saved-after-0.jpg": {
|
||||
"bytes": 102789,
|
||||
"sha256": "cb90a19d855faf3bab39cdb9468c788d72cf840366ef41b98e99d2cd4d9160fc"
|
||||
},
|
||||
"native/trial-1/35-saved-after.json": {
|
||||
"bytes": 962,
|
||||
"sha256": "f96d3e0c7d0c83603ae14ec4231250e7be9f12113b465050c101852de1755548"
|
||||
},
|
||||
"native/trial-1/36-new-cleared-0.jpg": {
|
||||
"bytes": 90719,
|
||||
"sha256": "dfc505fa7c2ce48fea5a04aa90b2745d11b144704c16cec0b2431a92291a8966"
|
||||
},
|
||||
"native/trial-1/36-new-cleared.json": {
|
||||
"bytes": 925,
|
||||
"sha256": "04c4e7a5de7af7974b5ecff290582854cb07979ffca6ca3e468eda09af403b09"
|
||||
},
|
||||
"native/trial-1/37-home-reopen-0.jpg": {
|
||||
"bytes": 115448,
|
||||
"sha256": "79680f4fbe8ea41a8318d175959d9bf80b8f28ae161b81818f6daa1a97ff294f"
|
||||
},
|
||||
"native/trial-1/37-home-reopen.json": {
|
||||
"bytes": 925,
|
||||
"sha256": "93b4c5dedb87e64bfd4ebead90b9fb6e6cc89a96983312b70260d8b1cd2ea430"
|
||||
},
|
||||
"native/trial-1/38-reopened-after-0.jpg": {
|
||||
"bytes": 98471,
|
||||
"sha256": "1a463750e7eeca04b36db7d1ebd3e093f44c92cff40bc68ce603e2e788c23fe7"
|
||||
},
|
||||
"native/trial-1/38-reopened-after.json": {
|
||||
"bytes": 956,
|
||||
"sha256": "154ba579c620a7d4adaf6c303e045b73223e69996ebaa5841d751dc9cf3f090f"
|
||||
},
|
||||
"native/trial-1/39-reopened-source-0.jpg": {
|
||||
"bytes": 103219,
|
||||
"sha256": "2bf7bc9dc32c265c303453c054a032af780cb091fb4cc9e8830b0c8d1019c4cd"
|
||||
},
|
||||
"native/trial-1/39-reopened-source.json": {
|
||||
"bytes": 964,
|
||||
"sha256": "5faf1b0d7810c67d17c77d914824b111b945915a1cf97020e544d3df5c2378d8"
|
||||
},
|
||||
"native/trial-1/40-problems-menu-0.jpg": {
|
||||
"bytes": 117324,
|
||||
"sha256": "ffdbb858d382d5eb7f5c02769fb6ca0fea64a223280f27366c95087ee5a1f618"
|
||||
},
|
||||
"native/trial-1/40-problems-menu.json": {
|
||||
"bytes": 962,
|
||||
"sha256": "a2f4a64ce2619952ce1983af22400c13f28f496ba69cc2c9e09eb3b19d0d045b"
|
||||
},
|
||||
"native/trial-1/41-fresh-diagnostics-0.jpg": {
|
||||
"bytes": 124773,
|
||||
"sha256": "25f26374a247b7188a8458950ded42eeb1635919a54b4bfd33cedc17446f7506"
|
||||
},
|
||||
"native/trial-1/41-fresh-diagnostics.json": {
|
||||
"bytes": 966,
|
||||
"sha256": "3c227b0e93aa63082536324498c01a47df905fe7677bda072bd8309dc369e78f"
|
||||
},
|
||||
"native/trial-1/42-fresh-warning-navigation-0.jpg": {
|
||||
"bytes": 107742,
|
||||
"sha256": "2d5d6b4dc722d63313f8aaa2bb6943d3bec11a1bcf9087cdc56ad287e5b5936d"
|
||||
},
|
||||
"native/trial-1/42-fresh-warning-navigation.json": {
|
||||
"bytes": 973,
|
||||
"sha256": "0a3fd08f2e994530dd41b6fdc6d28bcb0d665e5d9c16a2e89f8bd54dcb797048"
|
||||
},
|
||||
"native/trial-1/43-comment-insertion-point-0.jpg": {
|
||||
"bytes": 108879,
|
||||
"sha256": "4267f2294d6ceb9ffdb1ea5e72cfbd9aa17ef14df72ea410c0e80128ef854520"
|
||||
},
|
||||
"native/trial-1/43-comment-insertion-point.json": {
|
||||
"bytes": 972,
|
||||
"sha256": "182b10cdcaf379defeb0fc448cf1e933aab4f783aa383a97bdbf73a3338af69d"
|
||||
},
|
||||
"native/trial-1/44-edited-diagnostics-stale-0.jpg": {
|
||||
"bytes": 91431,
|
||||
"sha256": "206a1ea8360ca766a17d12594265edc5c54abeda976456259315569879aa4165"
|
||||
},
|
||||
"native/trial-1/44-edited-diagnostics-stale.json": {
|
||||
"bytes": 983,
|
||||
"sha256": "222c7915eb406f618e8e10caf8b02bb57545a3668fe32f20ef4381eb3780d67a"
|
||||
},
|
||||
"native/trial-1/45-refreshed-diagnostics-0.jpg": {
|
||||
"bytes": 107187,
|
||||
"sha256": "0c9309378f1bac39941836312b926eddf52a78c164329955f677df1000c8c4c1"
|
||||
},
|
||||
"native/trial-1/45-refreshed-diagnostics.json": {
|
||||
"bytes": 980,
|
||||
"sha256": "42a51a6eccce6b983368981c194a54c2e3172cee61302bc75b9ee555ace9c367"
|
||||
},
|
||||
"native/trial-1/46-refreshed-warning-navigation-0.jpg": {
|
||||
"bytes": 109007,
|
||||
"sha256": "e2e23d83f8e5ad9ddbc98f53858276990e27e59550de94dcf6e96054ee0f6a00"
|
||||
},
|
||||
"native/trial-1/46-refreshed-warning-navigation.json": {
|
||||
"bytes": 987,
|
||||
"sha256": "39d8e7cc945cb444b5f664c243e9ad373ccc1d18b6c04e8cc6dd37a8ce46c643"
|
||||
},
|
||||
"native/trial-1/47-restore-menu-0.jpg": {
|
||||
"bytes": 120506,
|
||||
"sha256": "a5f11f64e091b20caa628c3253ad92b8b98b2647c19e798f150f3a2d7a3f87e8"
|
||||
},
|
||||
"native/trial-1/47-restore-menu.json": {
|
||||
"bytes": 971,
|
||||
"sha256": "4a7e3bc9c23f95df538a488d0186f37dc54a5bb1dd184d06d2d6268bf1ccf256"
|
||||
},
|
||||
"native/trial-1/48-diagnostic-edit-undone-0.jpg": {
|
||||
"bytes": 105205,
|
||||
"sha256": "cf550b51b74b6fef8e21cd2d50f41f1797e22b4b7f38214d8311062bee3e0483"
|
||||
},
|
||||
"native/trial-1/48-diagnostic-edit-undone.json": {
|
||||
"bytes": 971,
|
||||
"sha256": "43c970d94c53c31a570ab92c6855566a6e2ce690768f6ed7836d64c5a6120f81"
|
||||
},
|
||||
"native/trial-1/input.zip": {
|
||||
"bytes": 343,
|
||||
"sha256": "f73fb93e2ee2eae45d1df2f63cdbb838a60cdda3f09b8977af6ded6e4ab424f0"
|
||||
},
|
||||
"native/trial-1/process.json": {
|
||||
"bytes": 61,
|
||||
"sha256": "8a60a20728ea02517d7c22812d27f9814bc3aa98fee597820e8351076f32e7c2"
|
||||
},
|
||||
"native/trial-1/result.json": {
|
||||
"bytes": 1665,
|
||||
"sha256": "6e615456856f30b8c25dedcd881d7dff6918535916a62b39b4de15c591960dd2"
|
||||
},
|
||||
"native/trial-1/started.json": {
|
||||
"bytes": 1198,
|
||||
"sha256": "39425278d31c29db4f293b4be7f3e9b2616a85e09c2f6a124dbab2b7ea0ac48c"
|
||||
},
|
||||
"native/trial-1/stderr.log": {
|
||||
"bytes": 0,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"native/trial-1/stdout.log": {
|
||||
"bytes": 0,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Portable read-only evidence verification; no archived helper or binary runs."""
|
||||
from pathlib import Path, PurePosixPath
|
||||
from datetime import datetime
|
||||
import hashlib, io, json, zipfile
|
||||
|
||||
ROOT=Path(__file__).resolve().parent
|
||||
def require(ok,message):
|
||||
if not ok: raise ValueError(message)
|
||||
def pin(b): return {'bytes':len(b),'sha256':hashlib.sha256(b).hexdigest()}
|
||||
def membership(z,expected):
|
||||
names=z.namelist()
|
||||
require(len(names)==len(set(names)) and set(names)==set(expected),'ZIP membership')
|
||||
require(all(not PurePosixPath(n).is_absolute() and '..' not in PurePosixPath(n).parts and '\\' not in n and ':' not in n for n in names),'unsafe ZIP path')
|
||||
def main():
|
||||
files=json.loads((ROOT/'files.json').read_bytes())
|
||||
for name,p in files.items(): require(pin((ROOT/name).read_bytes())==p,'outer hash '+name)
|
||||
manifest=json.loads((ROOT/'manifest.json').read_bytes())
|
||||
with zipfile.ZipFile(ROOT/'evidence.zip') as z:
|
||||
membership(z,manifest['members'])
|
||||
for name,p in manifest['members'].items():require(pin(z.read(name))==p,'member hash '+name)
|
||||
def raw(p):return z.read(p)
|
||||
def obj(p):return json.loads(raw(p))
|
||||
build=obj('build/result.json'); start=obj('build/started.json')
|
||||
require(all(build[k]==v for k,v in start.items()),'build start/result')
|
||||
require(build['exitCode']==0 and build['inputsUnchanged'] is True,'build success')
|
||||
require(build['head']==manifest['productionCommit'],'build commit')
|
||||
require(datetime.fromisoformat(build['start'])<=datetime.fromisoformat(build['finish']),'build time order')
|
||||
require(build['command']==obj('build/process.json')['command'],'build process command')
|
||||
for field,path in [('inputs','build/inputs.json'),('archive','build/inputs.zip'),('helper','build/gate-union.py'),('stdout','build/stdout.log'),('stderr','build/stderr.log')]:
|
||||
require(build[field]==pin(raw(path)),'build receipt '+field)
|
||||
inputs=obj('build/inputs.json')
|
||||
require(len(inputs)==build['inputCount']==707,'707 build inputs')
|
||||
require(sum(p.startswith('crates/vendor/iced_widget/') for p in inputs)==71,'vendor inventory')
|
||||
with zipfile.ZipFile(io.BytesIO(raw('build/inputs.zip'))) as source:
|
||||
membership(source,inputs)
|
||||
for name,p in inputs.items():require(pin(source.read(name))==p,'source input '+name)
|
||||
artifacts=[]
|
||||
for line in raw('build/stdout.log').decode('utf-8').splitlines():
|
||||
try:a=json.loads(line)
|
||||
except json.JSONDecodeError:continue
|
||||
if a.get('reason')=='compiler-artifact' and a.get('executable'):artifacts.append(a)
|
||||
require(artifacts==[a['cargo'] for a in build['artifacts']],'build artifact receipts')
|
||||
binaries={a['cargo']['target']['name']:a['readback'] for a in build['artifacts']}
|
||||
executables={a['cargo']['target']['name']:a['cargo']['executable'] for a in build['artifacts']}
|
||||
require(set(binaries)=={'arcadia','arcadia-server','arcadia-studio'},'build artifact targets')
|
||||
result=obj('native/trial-1/result.json');start=obj('native/trial-1/started.json')
|
||||
require(all(result[k]==v for k,v in start.items()),'native start/result')
|
||||
require(result['exitCode']==0 and result['inputUnchanged'] is True,'native closure/input')
|
||||
require(result['command']==[executables['arcadia-studio']],'native exact GUI command')
|
||||
process=obj('native/trial-1/process.json')
|
||||
require(isinstance(process['pid'],int) and process['pid']>0,'native positive process ID')
|
||||
require(datetime.fromisoformat(result['start'])<=datetime.fromisoformat(process['observed'])<=datetime.fromisoformat(result['finish']),'native process observation time')
|
||||
require(result['binary']==result['binaryAfter']==binaries['arcadia-studio'],'GUI binary receipt chain')
|
||||
require(result['buildReceipt']==pin(raw('build/result.json')) and result['buildInputs']==pin(raw('build/inputs.json')) and result['buildHead']==build['head'],'native build receipt chain')
|
||||
for field,path in [('helper','native/run-native.py'),('captureHelper','native/capture-snippet.js'),('inputArchive','native/trial-1/input.zip'),('stdout','native/trial-1/stdout.log'),('stderr','native/trial-1/stderr.log')]:
|
||||
require(result[field]==pin(raw(path)),'native receipt '+field)
|
||||
require(not raw('native/trial-1/stdout.log') and not raw('native/trial-1/stderr.log'),'native empty streams')
|
||||
with zipfile.ZipFile(io.BytesIO(raw('native/trial-1/input.zip'))) as fixture:
|
||||
membership(fixture,result['input'])
|
||||
for name,p in result['input'].items():require(pin(fixture.read(name))==p and fixture.read(name)==raw('native/input/'+name),'fixture bytes')
|
||||
captures=manifest['captures']; seen=[]
|
||||
for name in captures:
|
||||
capture=obj(name)
|
||||
require(datetime.fromisoformat(result['start'])<=datetime.fromisoformat(capture['time'])<=datetime.fromisoformat(result['finish']),'capture timestamp')
|
||||
for image in capture['images']:
|
||||
path='native/trial-1/'+image['file']
|
||||
require(path in manifest['members'] and image['width']>0 and image['height']>0,'capture image reference')
|
||||
require(raw(path).startswith(b'\xff\xd8'),'JPEG signature')
|
||||
seen.append(path)
|
||||
images=[n for n in manifest['members'] if n.startswith('native/trial-1/') and n.endswith('.jpg')]
|
||||
require(len(seen)==len(set(seen)) and set(seen)==set(images),'all raw frames referenced exactly once')
|
||||
require(len(captures)==manifest['captureCount'] and len(images)==manifest['imageCount'],'capture counts')
|
||||
original=raw('native/input/project.arcadia')
|
||||
expected={'project.arcadia':original[:-2]+b'\n include "oa/activities/activity_new_activity.arcadia";\n}\n',
|
||||
'oa/activities/activity_new_activity.arcadia':b'\noa_activity new_activity "New activity" id "d2b6d784-dea5-433d-aea9-214c604744e5" {\n}\n\n\n'}
|
||||
require(original.endswith(b'}\n') and expected['project.arcadia'].count(b'position { x 120.0; y 80.0; };')==1,'fixture oracle preconditions')
|
||||
for bundle in ['saved-before','saved-after']:
|
||||
if bundle=='saved-after':expected['project.arcadia']=expected['project.arcadia'].replace(b'position { x 120.0; y 80.0; };',b'position { x 190.0; y 80.0; };',1)
|
||||
prefix='native/'+bundle+'/'
|
||||
actual={n[len(prefix):]:raw(n) for n in manifest['members'] if n.startswith(prefix)}
|
||||
require(actual==expected,bundle+' exact source-byte oracle')
|
||||
receipt=obj('native/bundle-checks/'+bundle+'.result.json')
|
||||
report=obj('native/bundle-checks/'+bundle+'.stdout.json')
|
||||
expected_entry=result['cwd'].replace('\\','/').rstrip('/')+'/.local/union-native/'+bundle+'/project.arcadia'
|
||||
require(len(receipt['command'])==4 and receipt['command'][0]==executables['arcadia'] and receipt['command'][1]=='check' and receipt['command'][2].replace('\\','/')==expected_entry and receipt['command'][3]=='--json',bundle+' exact CLI command')
|
||||
require(datetime.fromisoformat(receipt['start'])<=datetime.fromisoformat(receipt['finish']),bundle+' CLI time order')
|
||||
require(receipt['files']=={n:pin(b) for n,b in actual.items()} and receipt['filesUnchanged'] is True,bundle+' file receipts')
|
||||
require(receipt['binary']==binaries['arcadia'] and receipt['buildReceipt']==pin(raw('build/result.json')),bundle+' CLI build receipt')
|
||||
require(receipt['producer']==pin(raw('native/check-saved-bundles.py')),bundle+' producer')
|
||||
require(receipt['exitCode']==0 and report['status']=='checked',bundle+' CLI status')
|
||||
for field,suffix in [('stdout','.stdout.json'),('stderr','.stderr.log')]:require(receipt[field]==pin(raw('native/bundle-checks/'+bundle+suffix)),bundle+' '+field)
|
||||
require(not raw('native/bundle-checks/'+bundle+'.stderr.log'),bundle+' empty stderr')
|
||||
require(len(report['diagnostics'])==4 and all(d['severity']=='warning' for d in report['diagnostics']),bundle+' warnings')
|
||||
require(receipt['diagnostics']==[{k:d[k] for k in ('severity','code','object_id')} for d in report['diagnostics']],bundle+' diagnostic receipt')
|
||||
require(not report['changes'] and not report['written'] and report['identities_to_stabilize']==0,bundle+' nonmutating stable-ID check')
|
||||
summary=obj('native/bundle-checks/summary.json')
|
||||
require(summary['status']=='PASS' and summary['runs']==['saved-before','saved-after'] and summary['producer']==pin(raw('native/check-saved-bundles.py')),'bundle summary')
|
||||
print('PASS:',len(manifest['members']),'members;',len(captures),'captures;',len(images),'unaltered frames; 707 build inputs; native closure and both saved-byte/CLI receipt oracles.')
|
||||
print('No archived code executed. Screenshot interpretation and full parsed-model equality are not machine-verified.')
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,51 @@
|
||||
# Transition lifecycle: closed-gate evidence
|
||||
|
||||
Closed Cargo-gate checkpoint with eight runs. Native work remains separate.
|
||||
Run `python verify.py` using
|
||||
Python 3.10 or newer. The five package files are sufficient; no archived code,
|
||||
binary, Git command or network operation is executed by verification.
|
||||
|
||||
Raw logs/receipts preserve the initial lifecycle-1 fixture admission failure as
|
||||
a distinct input state. Successful core-transition-2, desktop-workflow-1 and
|
||||
core-workflows-1 inputs are bound to
|
||||
acbfb6cc929cb08b599b2e8ccf4d6752e63b84a8 by an independent local comparison of
|
||||
all 710 captured input hashes with exact committed bytes using Git cat-file.
|
||||
The readback summary records Git blob identities; its raw Git stream is not
|
||||
archived. Offline verification checks those blob hashes and input relationships,
|
||||
not authenticated membership of a remote commit. Receipt HEADs may precede dirty
|
||||
changes, so per-run input manifests are authoritative for tested source bytes.
|
||||
|
||||
Strict all-target workspace Clippy passed on those tested inputs. The first
|
||||
format check failed only on an inherited tuple layout in diagram/focus.rs;
|
||||
the raw failure is retained. Commit d46854c2dc719bb8b44a0978102de44ecc3a6b39
|
||||
reformats that tuple and the second format check passes. Its separate 710-input
|
||||
inventory is independently compared with that commit; per-run inputCommit
|
||||
bindings and formattingInputChanges distinguish these source states. Earlier
|
||||
execution and lint results are not represented as reruns on the formatted
|
||||
commit. No application behavior change is claimed from formatting evidence.
|
||||
The Windows workspace build completed on d46854c with the same 710 unchanged
|
||||
inputs as format-2 and three executable artifact receipts. Those recorded
|
||||
artifacts identify CLI, server and desktop binaries; binaries are not packaged.
|
||||
|
||||
| Gate | Recorded outcome |
|
||||
| --- | --- |
|
||||
| lifecycle-1 | Initial fixture admission failed: 6 failures |
|
||||
| core-transition-2 | 10 passed |
|
||||
| desktop-workflow-1 | 25 passed, 1,164 filtered out |
|
||||
| core-workflows-1 | 28 passed |
|
||||
| clippy-1 | Strict workspace/all-target lint passed |
|
||||
| format-1 | Initial tuple-format check failed |
|
||||
| format-2 | Format check passed with empty streams |
|
||||
| windows-build-1 | Build passed; three executable artifact records |
|
||||
|
||||
The manifest maps each run to exact manifests and archives; only byte-identical
|
||||
input objects are deduplicated. Verification checks safe exact ZIP membership,
|
||||
hashes/lengths, source inventory and contents, 71 vendored iced_widget files,
|
||||
helper/start/process/result relationships, time order, commands, exit outcomes,
|
||||
unchanged input assertions, exact test summary rows and artifact receipt records.
|
||||
Outer text uses UTF-8 LF before pinning; inner bytes remain raw.
|
||||
|
||||
This is process evidence and internal consistency, not a test rerun, proof of
|
||||
complete coverage, native acceptance or release readiness. Binaries, dependency
|
||||
caches and credentials are excluded; artifact readback hashes are historical
|
||||
observations, not independently verified binaries.
|
||||
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"evidence.zip": {
|
||||
"bytes": 8496343,
|
||||
"sha256": "5f44ad5d83024a04a873142e77c5b8961c278a19d76492618c3ef2db63801070"
|
||||
},
|
||||
"manifest.json": {
|
||||
"bytes": 14323,
|
||||
"sha256": "7d48cb7e0788fb409b196f8be94c5947b30c698219bd016141fec136dbce3148"
|
||||
},
|
||||
"README.md": {
|
||||
"bytes": 3017,
|
||||
"sha256": "7946dd1317e326a6319c8e6399a6177ce251c6c8bc6c6c9cc7eac1517130d2db"
|
||||
},
|
||||
"verify.py": {
|
||||
"bytes": 8239,
|
||||
"sha256": "13cd285e8c5331bc9b1b7ada1ee7d3ebce4135570e45c047fb56c2f66b558700"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"testedSourceCommit": "acbfb6cc929cb08b599b2e8ccf4d6752e63b84a8",
|
||||
"formattedSourceCommit": "d46854c2dc719bb8b44a0978102de44ecc3a6b39",
|
||||
"runs": {
|
||||
"lifecycle-1": {
|
||||
"command": [
|
||||
"cargo",
|
||||
"test",
|
||||
"--locked",
|
||||
"--offline",
|
||||
"-p",
|
||||
"oa-core",
|
||||
"--test",
|
||||
"transition_lifecycle",
|
||||
"--message-format=json",
|
||||
"--",
|
||||
"--nocapture"
|
||||
],
|
||||
"head": "3f9e6a18af96b5cd10940da4093be8da28b9c835",
|
||||
"exitCode": 101,
|
||||
"inputCount": 708,
|
||||
"kind": "cargo",
|
||||
"inputs": "inputs/c637fea08bdfad17a86c335b58f79666a22b677174a84e817300451a11260a7a.json",
|
||||
"archive": "inputs/6f54f538a71dfcd81cfe1309e6fa338f27f6e844f2f315983ef3556134e3eaf6.zip",
|
||||
"testRows": [
|
||||
[
|
||||
0,
|
||||
6,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
],
|
||||
"testTotals": [
|
||||
0,
|
||||
6,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"core-transition-2": {
|
||||
"command": [
|
||||
"cargo",
|
||||
"test",
|
||||
"--locked",
|
||||
"--offline",
|
||||
"-p",
|
||||
"oa-core",
|
||||
"--test",
|
||||
"transition_lifecycle",
|
||||
"--test",
|
||||
"transition_counterpart_compatibility",
|
||||
"--message-format=json",
|
||||
"--",
|
||||
"--nocapture"
|
||||
],
|
||||
"head": "3f9e6a18af96b5cd10940da4093be8da28b9c835",
|
||||
"exitCode": 0,
|
||||
"inputCount": 710,
|
||||
"kind": "cargo",
|
||||
"inputs": "inputs/65138ba4ac5843e411a068a8309dfe62c0c4186579aec9e755f6decbed8ecc26.json",
|
||||
"archive": "inputs/e68321a18e2c1951263744ac9a4e505b18d4bdd520992360f652937fd0abf88b.zip",
|
||||
"inputCommit": "acbfb6cc929cb08b599b2e8ccf4d6752e63b84a8",
|
||||
"testRows": [
|
||||
[
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
6,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
],
|
||||
"testTotals": [
|
||||
10,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"desktop-workflow-1": {
|
||||
"command": [
|
||||
"cargo",
|
||||
"test",
|
||||
"--locked",
|
||||
"--offline",
|
||||
"-p",
|
||||
"oa-desktop",
|
||||
"--bin",
|
||||
"arcadia-studio",
|
||||
"--message-format=json",
|
||||
"workflow::",
|
||||
"--",
|
||||
"--nocapture"
|
||||
],
|
||||
"head": "3f9e6a18af96b5cd10940da4093be8da28b9c835",
|
||||
"exitCode": 0,
|
||||
"inputCount": 710,
|
||||
"kind": "cargo",
|
||||
"inputs": "inputs/65138ba4ac5843e411a068a8309dfe62c0c4186579aec9e755f6decbed8ecc26.json",
|
||||
"archive": "inputs/e68321a18e2c1951263744ac9a4e505b18d4bdd520992360f652937fd0abf88b.zip",
|
||||
"inputCommit": "acbfb6cc929cb08b599b2e8ccf4d6752e63b84a8",
|
||||
"testRows": [
|
||||
[
|
||||
25,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1164
|
||||
]
|
||||
],
|
||||
"testTotals": [
|
||||
25,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1164
|
||||
]
|
||||
},
|
||||
"core-workflows-1": {
|
||||
"command": [
|
||||
"cargo",
|
||||
"test",
|
||||
"--locked",
|
||||
"--offline",
|
||||
"-p",
|
||||
"oa-core",
|
||||
"--test",
|
||||
"workflows",
|
||||
"--message-format=json",
|
||||
"--",
|
||||
"--nocapture"
|
||||
],
|
||||
"head": "3f9e6a18af96b5cd10940da4093be8da28b9c835",
|
||||
"exitCode": 0,
|
||||
"inputCount": 710,
|
||||
"kind": "cargo",
|
||||
"inputs": "inputs/65138ba4ac5843e411a068a8309dfe62c0c4186579aec9e755f6decbed8ecc26.json",
|
||||
"archive": "inputs/e68321a18e2c1951263744ac9a4e505b18d4bdd520992360f652937fd0abf88b.zip",
|
||||
"inputCommit": "acbfb6cc929cb08b599b2e8ccf4d6752e63b84a8",
|
||||
"testRows": [
|
||||
[
|
||||
28,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
],
|
||||
"testTotals": [
|
||||
28,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"clippy-1": {
|
||||
"command": [
|
||||
"cargo",
|
||||
"clippy",
|
||||
"--locked",
|
||||
"--offline",
|
||||
"--workspace",
|
||||
"--all-targets",
|
||||
"--message-format=json",
|
||||
"--",
|
||||
"-D",
|
||||
"warnings"
|
||||
],
|
||||
"head": "acbfb6cc929cb08b599b2e8ccf4d6752e63b84a8",
|
||||
"exitCode": 0,
|
||||
"inputCount": 710,
|
||||
"kind": "cargo",
|
||||
"inputs": "inputs/65138ba4ac5843e411a068a8309dfe62c0c4186579aec9e755f6decbed8ecc26.json",
|
||||
"archive": "inputs/e68321a18e2c1951263744ac9a4e505b18d4bdd520992360f652937fd0abf88b.zip",
|
||||
"inputCommit": "acbfb6cc929cb08b599b2e8ccf4d6752e63b84a8",
|
||||
"testRows": [],
|
||||
"testTotals": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"format-1": {
|
||||
"command": [
|
||||
"cargo",
|
||||
"fmt",
|
||||
"--all",
|
||||
"--",
|
||||
"--check"
|
||||
],
|
||||
"head": "acbfb6cc929cb08b599b2e8ccf4d6752e63b84a8",
|
||||
"exitCode": 1,
|
||||
"inputCount": 710,
|
||||
"kind": "cargo",
|
||||
"inputs": "inputs/65138ba4ac5843e411a068a8309dfe62c0c4186579aec9e755f6decbed8ecc26.json",
|
||||
"archive": "inputs/e68321a18e2c1951263744ac9a4e505b18d4bdd520992360f652937fd0abf88b.zip",
|
||||
"inputCommit": "acbfb6cc929cb08b599b2e8ccf4d6752e63b84a8",
|
||||
"testRows": [],
|
||||
"testTotals": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"format-2": {
|
||||
"command": [
|
||||
"cargo",
|
||||
"fmt",
|
||||
"--all",
|
||||
"--",
|
||||
"--check"
|
||||
],
|
||||
"head": "d46854c2dc719bb8b44a0978102de44ecc3a6b39",
|
||||
"exitCode": 0,
|
||||
"inputCount": 710,
|
||||
"kind": "cargo",
|
||||
"inputs": "inputs/feb3e7ff11a74571fcfe290016131165d7be82e83b8e4a4daae1667e8a9feb3c.json",
|
||||
"archive": "inputs/00a630ff5d3aa838a3d6ba582f3e54ce12fe45ab52569f4da2cc583be1b1f802.zip",
|
||||
"inputCommit": "d46854c2dc719bb8b44a0978102de44ecc3a6b39",
|
||||
"testRows": [],
|
||||
"testTotals": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"windows-build-1": {
|
||||
"command": [
|
||||
"cargo",
|
||||
"build",
|
||||
"--locked",
|
||||
"--offline",
|
||||
"--workspace",
|
||||
"--bins",
|
||||
"--message-format=json"
|
||||
],
|
||||
"head": "d46854c2dc719bb8b44a0978102de44ecc3a6b39",
|
||||
"exitCode": 0,
|
||||
"inputCount": 710,
|
||||
"kind": "cargo",
|
||||
"inputs": "inputs/feb3e7ff11a74571fcfe290016131165d7be82e83b8e4a4daae1667e8a9feb3c.json",
|
||||
"archive": "inputs/00a630ff5d3aa838a3d6ba582f3e54ce12fe45ab52569f4da2cc583be1b1f802.zip",
|
||||
"inputCommit": "d46854c2dc719bb8b44a0978102de44ecc3a6b39",
|
||||
"testRows": [],
|
||||
"testTotals": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"members": {
|
||||
"helpers/commit-readbacks.json": {
|
||||
"bytes": 149015,
|
||||
"sha256": "d78b2c4182f2b1db7413cc3eaa36614ade32686e520d6e565bab959b36067838"
|
||||
},
|
||||
"helpers/gate-transition.py": {
|
||||
"bytes": 3500,
|
||||
"sha256": "065a752452e297ba8a000ee69cfae3996b4e6df9b4edbe002f233a66c32e1c04"
|
||||
},
|
||||
"inputs/00a630ff5d3aa838a3d6ba582f3e54ce12fe45ab52569f4da2cc583be1b1f802.zip": {
|
||||
"bytes": 2843714,
|
||||
"sha256": "00a630ff5d3aa838a3d6ba582f3e54ce12fe45ab52569f4da2cc583be1b1f802"
|
||||
},
|
||||
"inputs/65138ba4ac5843e411a068a8309dfe62c0c4186579aec9e755f6decbed8ecc26.json": {
|
||||
"bytes": 118907,
|
||||
"sha256": "65138ba4ac5843e411a068a8309dfe62c0c4186579aec9e755f6decbed8ecc26"
|
||||
},
|
||||
"inputs/6f54f538a71dfcd81cfe1309e6fa338f27f6e844f2f315983ef3556134e3eaf6.zip": {
|
||||
"bytes": 2836501,
|
||||
"sha256": "6f54f538a71dfcd81cfe1309e6fa338f27f6e844f2f315983ef3556134e3eaf6"
|
||||
},
|
||||
"inputs/c637fea08bdfad17a86c335b58f79666a22b677174a84e817300451a11260a7a.json": {
|
||||
"bytes": 118565,
|
||||
"sha256": "c637fea08bdfad17a86c335b58f79666a22b677174a84e817300451a11260a7a"
|
||||
},
|
||||
"inputs/e68321a18e2c1951263744ac9a4e505b18d4bdd520992360f652937fd0abf88b.zip": {
|
||||
"bytes": 2843705,
|
||||
"sha256": "e68321a18e2c1951263744ac9a4e505b18d4bdd520992360f652937fd0abf88b"
|
||||
},
|
||||
"inputs/feb3e7ff11a74571fcfe290016131165d7be82e83b8e4a4daae1667e8a9feb3c.json": {
|
||||
"bytes": 118907,
|
||||
"sha256": "feb3e7ff11a74571fcfe290016131165d7be82e83b8e4a4daae1667e8a9feb3c"
|
||||
},
|
||||
"runs/clippy-1/process.json": {
|
||||
"bytes": 199,
|
||||
"sha256": "e8aa78d9ea3dd2b623d990a9481e70354707e6aa5a591cb7b93dba5180a90aa9"
|
||||
},
|
||||
"runs/clippy-1/result.json": {
|
||||
"bytes": 1801,
|
||||
"sha256": "b6b63d0069a0194d4a1fa91f17d2f6a69555087ec6a9224c322891a3b3047db2"
|
||||
},
|
||||
"runs/clippy-1/started.json": {
|
||||
"bytes": 1405,
|
||||
"sha256": "165e5bb3b433703336dff190ad594ff3438da491d3396398e616dc5bc700e376"
|
||||
},
|
||||
"runs/clippy-1/stderr.log": {
|
||||
"bytes": 456,
|
||||
"sha256": "e3de1e791b11f30e85b682df843625d4ca6556f7eb298d6a1bf310aa5b38aa30"
|
||||
},
|
||||
"runs/clippy-1/stdout.log": {
|
||||
"bytes": 310455,
|
||||
"sha256": "e2692121e9c3536fe9f673de7388c393f23e6fe22dc08bb191a37610392dc0f9"
|
||||
},
|
||||
"runs/core-transition-2/process.json": {
|
||||
"bytes": 263,
|
||||
"sha256": "39b8ea38cfb392a251a3279cb7d4904545702ef10a1fb870b5e866af843b8b3b"
|
||||
},
|
||||
"runs/core-transition-2/result.json": {
|
||||
"bytes": 4667,
|
||||
"sha256": "3ed748bc46d4e7ebf21881ab6ffea85da6f94a36dde0778068b7a0282212a49d"
|
||||
},
|
||||
"runs/core-transition-2/started.json": {
|
||||
"bytes": 1484,
|
||||
"sha256": "3cca432872797dddf8de33e0cf9f931274851acdc8002d5d3599a4fd43ca853d"
|
||||
},
|
||||
"runs/core-transition-2/stderr.log": {
|
||||
"bytes": 489,
|
||||
"sha256": "92fd897c8f9608b06cdca0033a5008a6f00c5c28c4a88b3cba4bcbb3aeb4f986"
|
||||
},
|
||||
"runs/core-transition-2/stdout.log": {
|
||||
"bytes": 43653,
|
||||
"sha256": "1c35f25e869d6151cbb6de224282a05395caa41d27cd8d2452872c651ba08dbf"
|
||||
},
|
||||
"runs/core-workflows-1/process.json": {
|
||||
"bytes": 202,
|
||||
"sha256": "b6a5f0a3c4887888198f10d8e93b2422a1559c4b1214162f6e0954a8f0826828"
|
||||
},
|
||||
"runs/core-workflows-1/result.json": {
|
||||
"bytes": 3128,
|
||||
"sha256": "89dccdd6655ffb2cd792d25ce76e7da258efbd884dc4ab47d0ba48f8aa23ec6e"
|
||||
},
|
||||
"runs/core-workflows-1/started.json": {
|
||||
"bytes": 1413,
|
||||
"sha256": "a33e662e9743d650f57401b7276b696f97d97b8c5a916c0c783465b02eb3fc3c"
|
||||
},
|
||||
"runs/core-workflows-1/stderr.log": {
|
||||
"bytes": 271,
|
||||
"sha256": "bb006c1bb74a6ce4d938f3eb0db36bb9179395fbc3a1b42a7dfb3edad691ca46"
|
||||
},
|
||||
"runs/core-workflows-1/stdout.log": {
|
||||
"bytes": 43869,
|
||||
"sha256": "a51bf33e8e8767c73efcdc176f615801c63ff3ec9e369586f640fc634d60c528"
|
||||
},
|
||||
"runs/desktop-workflow-1/process.json": {
|
||||
"bytes": 223,
|
||||
"sha256": "3360e8316f62834c74da7c08742e9f2e62856b7a43974f3fd311d95074bfe4cd"
|
||||
},
|
||||
"runs/desktop-workflow-1/result.json": {
|
||||
"bytes": 3170,
|
||||
"sha256": "a9626e778ea9626fe76080c95faeac312455c7fd6b7511f1b5887da07bb6676a"
|
||||
},
|
||||
"runs/desktop-workflow-1/started.json": {
|
||||
"bytes": 1439,
|
||||
"sha256": "cf9cee18e2a6cd06d1ed12306eecb79a056f2a85ca3913557a219302f9b650c5"
|
||||
},
|
||||
"runs/desktop-workflow-1/stderr.log": {
|
||||
"bytes": 539,
|
||||
"sha256": "84734875bfd88145ebb582986937ccaf5fad3fc32ca0b50d03f57554195742e5"
|
||||
},
|
||||
"runs/desktop-workflow-1/stdout.log": {
|
||||
"bytes": 296022,
|
||||
"sha256": "0ec72412bc1b3976e0c7470853a06efdf99f051aace8dad540fb3afc3a99bae2"
|
||||
},
|
||||
"runs/format-1/process.json": {
|
||||
"bytes": 116,
|
||||
"sha256": "3ed4ed5a510a0b116a3c0fd2baf3e4db7ff7de66b7df0a0464cc098841fff115"
|
||||
},
|
||||
"runs/format-1/result.json": {
|
||||
"bytes": 1688,
|
||||
"sha256": "1d5eac33f2174bcb464d89bc858483affe3c73d5016d19db7d0a32412d9ea475"
|
||||
},
|
||||
"runs/format-1/started.json": {
|
||||
"bytes": 1297,
|
||||
"sha256": "60fcb6b008530a620534fba89c0cb8e143d423fab2dd2fe08a19a72d21eab904"
|
||||
},
|
||||
"runs/format-1/stderr.log": {
|
||||
"bytes": 0,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"runs/format-1/stdout.log": {
|
||||
"bytes": 538,
|
||||
"sha256": "e6eac9f78ecb50fdaa288974ac26df928f6aa437d293d5aaf9ac60190394b88f"
|
||||
},
|
||||
"runs/format-2/process.json": {
|
||||
"bytes": 116,
|
||||
"sha256": "20c7c2177f85bb157b2cfff08b26c447bebed42eb11e0e7f6b43c6664c4a494d"
|
||||
},
|
||||
"runs/format-2/result.json": {
|
||||
"bytes": 1686,
|
||||
"sha256": "b220a0401f478cde47f2e1386a311de58e11bf7937a722490ac630ab223ab279"
|
||||
},
|
||||
"runs/format-2/started.json": {
|
||||
"bytes": 1297,
|
||||
"sha256": "6d87fb899116fed854f2c2f2c358e0810103f462aa2f3f225feaf89b0855af6d"
|
||||
},
|
||||
"runs/format-2/stderr.log": {
|
||||
"bytes": 0,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"runs/format-2/stdout.log": {
|
||||
"bytes": 0,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"runs/lifecycle-1/process.json": {
|
||||
"bytes": 213,
|
||||
"sha256": "ef6976e24b19e881d5d435f5dd2d7a54fd228f8013be6cd4a97110a5078f4494"
|
||||
},
|
||||
"runs/lifecycle-1/result.json": {
|
||||
"bytes": 3188,
|
||||
"sha256": "149a0f3c8747b3c5227916aa88e4b94b77415b0c8d765445e92bfdd201069fb0"
|
||||
},
|
||||
"runs/lifecycle-1/started.json": {
|
||||
"bytes": 1425,
|
||||
"sha256": "3e3458c4227da8bfbc4d4b6244fe0510687b8c98c2167a4802b2b691431565fa"
|
||||
},
|
||||
"runs/lifecycle-1/stderr.log": {
|
||||
"bytes": 21458,
|
||||
"sha256": "2e13355db62b04032baa7007f7d7d691eadaad974f4fde8e0bf0ddd4a8b12027"
|
||||
},
|
||||
"runs/lifecycle-1/stdout.log": {
|
||||
"bytes": 42779,
|
||||
"sha256": "9d545eaba4c279233a344b1e8bc845d0df4dbce5de5c4af9de14d288d3d8d7d8"
|
||||
},
|
||||
"runs/windows-build-1/process.json": {
|
||||
"bytes": 167,
|
||||
"sha256": "5292a76450540df9e145cd21cc20118b310ed306126da85e893d0c9c221a8564"
|
||||
},
|
||||
"runs/windows-build-1/result.json": {
|
||||
"bytes": 5583,
|
||||
"sha256": "91b38045319adc1fb9aa0e51d45d79a4be99d2de9aec08a47f73915e3ac9dd79"
|
||||
},
|
||||
"runs/windows-build-1/started.json": {
|
||||
"bytes": 1358,
|
||||
"sha256": "826453a44cd915065caf1db9c0cc547dca0fa5a2b56594e4a72146b19ffe9e8c"
|
||||
},
|
||||
"runs/windows-build-1/stderr.log": {
|
||||
"bytes": 276,
|
||||
"sha256": "0a035e700f490f73889da441e363052b34f33b3de5690c30eb1e6c2666c75bc6"
|
||||
},
|
||||
"runs/windows-build-1/stdout.log": {
|
||||
"bytes": 295257,
|
||||
"sha256": "6ecb53aef4a76dda4174213dd5ba9b03aabc1434577c3141c4ed3e71adf068fa"
|
||||
}
|
||||
},
|
||||
"formattingInputChanges": {
|
||||
"crates/oa-desktop/src/diagram/focus.rs": {
|
||||
"before": {
|
||||
"bytes": 9197,
|
||||
"sha256": "b88a7c43123b08cf6228d8e32257f6ed7fe361f7278f427ec138e233e1ea6ccf"
|
||||
},
|
||||
"after": {
|
||||
"bytes": 9276,
|
||||
"sha256": "44e7c13f8f56deb21c62cb7757a412ec8b790344387cebdee983c3a28b6b0160"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
"""Read-only, Python-standard-library verification. Never executes archived code."""
|
||||
from pathlib import Path, PurePosixPath
|
||||
from datetime import datetime
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
|
||||
def require(ok, message):
|
||||
if not ok:
|
||||
raise ValueError(message)
|
||||
|
||||
def digest(data):
|
||||
return {'bytes': len(data), 'sha256': hashlib.sha256(data).hexdigest()}
|
||||
|
||||
def members(z, expected):
|
||||
names = z.namelist()
|
||||
require(len(names) == len(set(names)), 'duplicate ZIP members')
|
||||
require(set(names) == set(expected), 'archive membership mismatch')
|
||||
require(all(not PurePosixPath(n).is_absolute() and '..' not in PurePosixPath(n).parts
|
||||
and '\\' not in n and ':' not in n for n in names), 'unsafe member')
|
||||
|
||||
def main():
|
||||
inventory = json.loads((ROOT / 'files.json').read_text())
|
||||
for name, expected in inventory.items():
|
||||
require(digest((ROOT / name).read_bytes()) == expected, 'external file hash: ' + name)
|
||||
manifest = json.loads((ROOT / 'manifest.json').read_text())
|
||||
with zipfile.ZipFile(ROOT / 'evidence.zip') as z:
|
||||
members(z, manifest['members'])
|
||||
for name, expected in manifest['members'].items():
|
||||
require(digest(z.read(name)) == expected, 'member hash: ' + name)
|
||||
def raw(name): return z.read(name)
|
||||
def obj(name): return json.loads(raw(name))
|
||||
checked = set()
|
||||
for run, spec in manifest['runs'].items():
|
||||
if spec['kind'] != 'cargo':
|
||||
continue
|
||||
prefix = 'runs/' + run + '/'
|
||||
start, result, process = (obj(prefix + n + '.json') for n in ('started', 'result', 'process'))
|
||||
require(all(result[k] == v for k, v in start.items()), run + ': start/result relationship')
|
||||
require(result['command'] == spec['command'] == process['command'], run + ': command')
|
||||
require(result['head'] == spec['head'], run + ': HEAD')
|
||||
require(result['exitCode'] == spec['exitCode'] and result['inputsUnchanged'] is True, run + ': outcome')
|
||||
require(datetime.fromisoformat(result['finish']) >= datetime.fromisoformat(start['start']), run + ': times')
|
||||
require(result['seconds'] >= 0, run + ': duration')
|
||||
require(process['pid']>0 and datetime.fromisoformat(start['start'])<=datetime.fromisoformat(process['observed'])<=datetime.fromisoformat(result['finish']), run+': process observation')
|
||||
require(result['helper'] == digest(raw('helpers/gate-transition.py')), run + ': helper')
|
||||
for field, file in [('inputs', spec['inputs']), ('archive', spec['archive']), ('stdout', prefix+'stdout.log'), ('stderr', prefix+'stderr.log')]:
|
||||
require(result[field] == digest(raw(file)), run + ': receipt ' + field)
|
||||
inputs = obj(spec['inputs'])
|
||||
require(len(inputs) == result['inputCount'] == spec['inputCount'], run + ': input count')
|
||||
require(sum(p.startswith('crates/vendor/iced_widget/') for p in inputs) == 71, run + ': vendor count')
|
||||
pair = (spec['inputs'], spec['archive'])
|
||||
if pair not in checked:
|
||||
with zipfile.ZipFile(io.BytesIO(raw(spec['archive']))) as source:
|
||||
members(source, inputs)
|
||||
for name, expected in inputs.items():
|
||||
require(digest(source.read(name)) == expected, run + ': source hash ' + name)
|
||||
checked.add(pair)
|
||||
stdout = raw(prefix+'stdout.log').decode('utf-8', errors='replace')
|
||||
rows = [[int(x) for x in row] for row in re.findall(r'test result: (?:ok|FAILED)\. (\d+) passed; (\d+) failed; (\d+) ignored; (\d+) measured; (\d+) filtered out;', stdout)]
|
||||
require(rows == spec['testRows'], run + ': exact test summaries')
|
||||
require([sum(row[i] for row in rows) for i in range(5)] == spec['testTotals'], run + ': test totals')
|
||||
artifacts = []
|
||||
for line in stdout.splitlines():
|
||||
try: item = json.loads(line)
|
||||
except json.JSONDecodeError: continue
|
||||
if isinstance(item, dict) and item.get('reason') == 'compiler-artifact' and item.get('executable'):
|
||||
artifacts.append(item)
|
||||
require(artifacts == [a['cargo'] for a in result['artifacts']], run + ': artifact receipt relationship')
|
||||
print(run, 'exit', result['exitCode'], 'test totals', spec['testTotals'])
|
||||
readbacks=obj('helpers/commit-readbacks.json')
|
||||
require(set(readbacks)=={spec['inputCommit'] for spec in manifest['runs'].values() if 'inputCommit' in spec}, 'readback commit coverage')
|
||||
base_inputs=obj(manifest['runs']['core-transition-2']['inputs'])
|
||||
for commit,readback in readbacks.items():
|
||||
require(readback['status']=='PASS' and readback['commit']==commit, 'commit readback status')
|
||||
specs=[spec for spec in manifest['runs'].values() if spec.get('inputCommit')==commit]
|
||||
success=specs[0];inputs=obj(success['inputs'])
|
||||
require(all(obj(spec['inputs'])==inputs for spec in specs), 'per-commit input equality')
|
||||
require(readback['inputs']==digest(raw(success['inputs'])) and len(inputs)==readback['inputCount']==710, '710 readback')
|
||||
require(set(readback['gitBlobs'])==set(inputs), 'readback blob inventory')
|
||||
with zipfile.ZipFile(io.BytesIO(raw(success['archive']))) as source:
|
||||
for name,oid in readback['gitBlobs'].items():
|
||||
b=source.read(name)
|
||||
require(hashlib.sha1(b'blob '+str(len(b)).encode()+b'\0'+b).hexdigest()==oid, 'readback blob '+name)
|
||||
if commit==manifest['formattedSourceCommit']:
|
||||
changed={p:{'before':base_inputs.get(p),'after':inputs.get(p)} for p in sorted(set(base_inputs)|set(inputs)) if base_inputs.get(p)!=inputs.get(p)}
|
||||
require(changed==manifest['formattingInputChanges'] and set(changed)=={'crates/oa-desktop/src/diagram/focus.rs'}, 'one-file formatting input difference')
|
||||
with zipfile.ZipFile(io.BytesIO(raw(manifest['runs']['core-transition-2']['archive']))) as old, zipfile.ZipFile(io.BytesIO(raw(success['archive']))) as new:
|
||||
path='crates/oa-desktop/src/diagram/focus.rs'
|
||||
before=old.read(path);after=new.read(path)
|
||||
compact=b' != (self.context.document, self.context.diagram, self.context.epoch)'
|
||||
expanded=b' != (\n self.context.document,\n self.context.diagram,\n self.context.epoch,\n )'
|
||||
require(before.count(compact)==1 and before.replace(compact,expanded,1)==after, 'exact tuple-format-only delta')
|
||||
for run in ('core-transition-2','desktop-workflow-1','core-workflows-1','clippy-1','format-1'):
|
||||
if run in manifest['runs']:require(manifest['runs'][run]['inputCommit']==manifest['testedSourceCommit'], 'earlier execution source scope '+run)
|
||||
for run in ('format-2','windows-build-1'):
|
||||
if run in manifest['runs']:require(manifest['runs'][run]['inputCommit']==manifest['formattedSourceCommit'], 'formatted source scope '+run)
|
||||
if 'format-2' in manifest['runs']:
|
||||
require(not raw('runs/format-2/stdout.log') and not raw('runs/format-2/stderr.log'), 'format2 empty success streams')
|
||||
if 'windows-build-1' in manifest['runs']:
|
||||
build=obj('runs/windows-build-1/result.json')
|
||||
require(build['exitCode']==0 and len(build['artifacts'])==3, 'Windows build outcome')
|
||||
require({a['cargo']['target']['name'] for a in build['artifacts']}=={'arcadia','arcadia-server','arcadia-studio'}, 'Windows build executable membership')
|
||||
require(obj(manifest['runs']['lifecycle-1']['inputs'])!=base_inputs, 'initial failure remains distinct')
|
||||
print('PASS:',len(manifest['runs']),'closed runs;',len(manifest['members']),'members;',len(checked),'input pairs;710 successful input blob bindings.')
|
||||
print('No archived code executed. Local commit-readback summary is not authenticated commit membership.')
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,55 @@
|
||||
# Closed lifecycle checker and footer gates
|
||||
|
||||
Run `python verify.py` with Python 3.10 or newer. Verification uses only these
|
||||
package files and the standard library; it does not execute archived source,
|
||||
binaries, Cargo, Git or network requests. `prepare.py [CHECKOUT]` reproducibly
|
||||
packages the explicitly named raw captures if those local originals are present.
|
||||
The input capsules are source snapshots, not a hermetic toolchain distribution.
|
||||
|
||||
Ten closed Cargo runs are preserved: footer-format-1, footer-windows-build-1,
|
||||
footer-windows-build-2, footer-clippy-1, lifecycle-producer-build-1,
|
||||
lifecycle-producer-generate-1, lifecycle-producer-build-2, footer-windows-build-3,
|
||||
footer-format-2 and lifecycle-producer-format-1. Each remains bound
|
||||
to its original command, source inventory, helper, logs and receipts. The first
|
||||
footer build has no JSON executable artifact records; the second captures them.
|
||||
Recorded HEAD d0b7c28 does not identify uncommitted footer/producer source bytes;
|
||||
the archived per-run input manifests do.
|
||||
|
||||
The three final gates use the same 715-file manifest, SHA-256
|
||||
`b5a62817a1b5df936b9d759241bdfc4e8283901c1d4b540b9e494995e1544d8d`.
|
||||
Compared with producer build2's captured inputs, only the footer literal changes
|
||||
from "changes" to "reviewed items". The verifier checks that exact byte delta.
|
||||
The final GUI build and both format checks pass. Earlier lint and oracle captures
|
||||
retain their original input scope; they are not presented as reruns of the final
|
||||
wording. The checker source is unchanged by that label refinement.
|
||||
|
||||
Producer build1 and its generation succeeded, but that older checker incorrectly
|
||||
rejected expected engineering warnings during saved-file admission. Build2
|
||||
contains the corrected admission logic. The 40 PASS falsifier trials are bound
|
||||
to build2's exact checker artifact receipt and producer source, not to build1.
|
||||
Build success alone does not demonstrate checker correctness. The earlier native
|
||||
admission failure is outside these selected captures, not fabricated as a Cargo
|
||||
failure or silently converted into successful native evidence.
|
||||
|
||||
The oracle archive retains all original fixtures, repeated generation outputs,
|
||||
mutated specimens, identity receipts, copied build evidence, runner/source,
|
||||
stdout/stderr and per-trial results. The 40 expected outcomes include accepted
|
||||
original/deleted/recreated/relinked states and deliberate rejection cases for
|
||||
retained prose/geometry corruption, source/trace identity drift, missing persisted
|
||||
IDs, absent identity receipts and receipt overwrite. These are synthetic checker
|
||||
tests, not native interaction. A PASS summary includes expected negative exits.
|
||||
|
||||
`manifest.json` maps every logical raw path to a stored member. Only byte-identical
|
||||
inputs.json/inputs.zip files are deduplicated; exact run mapping is retained.
|
||||
`files.json` pins outer files including the reproducible packager. Outer text is
|
||||
UTF-8 LF; all raw archived bytes are preserved. The verifier checks exact safe ZIP
|
||||
membership, hashes, nested input contents/counts, helpers, timestamps, receipt and
|
||||
binary-pin relationships, repeated fixture equality, watched specimen identities,
|
||||
all 40 outcomes and their recorded rejection text. It rereads evidence rather
|
||||
than rerunning the model checker, and does not authenticate historical execution.
|
||||
|
||||
Native trial1 lacks a closing exit receipt and is excluded. No successful native
|
||||
deletion, Undo, relinking, save/reopen, or full lifecycle acceptance is claimed.
|
||||
No runtime binaries, credentials or whole user profile are included. Recorded
|
||||
absolute paths are historical labels; verification maps them to archive members
|
||||
without accessing the original machine. Binary hashes remain receipt observations.
|
||||
Binary file not shown.
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"evidence.zip": {
|
||||
"bytes": 14254708,
|
||||
"sha256": "663bf83db4ead276063b321fd5e9857e29d2305e6046c2e87dbbac87a366b19c"
|
||||
},
|
||||
"manifest.json": {
|
||||
"bytes": 79071,
|
||||
"sha256": "5d2be3d68206871a560dd1a83064ff779e8aab6e11302d09aaf1a655502dd38e"
|
||||
},
|
||||
"README.md": {
|
||||
"bytes": 3696,
|
||||
"sha256": "0e33fe1a85a1ee17ebde57ed7994280dcb95d3a7c4338b78c1f7e0ae9fdcbb7d"
|
||||
},
|
||||
"verify.py": {
|
||||
"bytes": 10908,
|
||||
"sha256": "1e8ba1cc2759fbe76e14636a37925c9dfce9a6f14bf63569acb76498bd8380ad"
|
||||
},
|
||||
"prepare.py": {
|
||||
"bytes": 3572,
|
||||
"sha256": "147a63f26b7131076df52057f1cc263dfefdf99d937b2c2a819217a46c56105f"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,56 @@
|
||||
"""Repackage the explicitly closed captures; reads raw evidence, never runs Cargo."""
|
||||
from pathlib import Path
|
||||
import hashlib, json, sys, zipfile
|
||||
|
||||
OUT=Path(__file__).resolve().parent
|
||||
ROOT=Path(sys.argv[1]).resolve() if len(sys.argv)>1 else OUT.parents[3]
|
||||
GATES=ROOT/'.local/transition-gates'
|
||||
ORACLE=ROOT/'.local/transition-native/oracle-falsifiers-1'
|
||||
RUNS=['footer-format-1','footer-windows-build-1','footer-windows-build-2','footer-clippy-1',
|
||||
'lifecycle-producer-build-1','lifecycle-producer-generate-1','lifecycle-producer-build-2',
|
||||
'footer-windows-build-3','footer-format-2','lifecycle-producer-format-1']
|
||||
def pin(b):return {'bytes':len(b),'sha256':hashlib.sha256(b).hexdigest()}
|
||||
def main():
|
||||
payload={};logical={};run_specs={}
|
||||
def add(name,b):
|
||||
# Deduplicate only byte-identical input manifests and source archives.
|
||||
stored='capsules/'+pin(b)['sha256']+Path(name).suffix if Path(name).name in ('inputs.json','inputs.zip') else name
|
||||
if stored in payload:assert payload[stored]==b
|
||||
payload[stored]=b;logical[name]=stored
|
||||
for run in RUNS:
|
||||
result=json.loads((GATES/run/'result.json').read_bytes())
|
||||
assert 'finish' in result and result['inputsUnchanged'] is True,run
|
||||
for f in sorted((GATES/run).iterdir()):
|
||||
assert f.is_file(),f
|
||||
add('cargo/'+run+'/'+f.name,f.read_bytes())
|
||||
helpers=[f for f in GATES.glob('*.py') if pin(f.read_bytes())==result['helper']]
|
||||
assert helpers,run+' helper unavailable'
|
||||
helper='helpers/'+result['helper']['sha256']+'.py'
|
||||
add(helper,helpers[0].read_bytes())
|
||||
run_specs[run]={k:result[k] for k in ('command','head','exitCode','inputCount')}
|
||||
run_specs[run]['helper']=helper
|
||||
for f in sorted(ORACLE.rglob('*')):
|
||||
if f.is_file():add('oracle/'+f.relative_to(ORACLE).as_posix(),f.read_bytes())
|
||||
summary=json.loads((ORACLE/'summary.json').read_bytes())
|
||||
assert summary['status']=='PASS' and summary['trialCount']==40
|
||||
manifest={'schema':1,'captureCheckout':'d0b7c285018a516c6dc34c196d1359a861ddaeb3',
|
||||
'cargo':run_specs,'oracleExpectedTrials':summary['trials'],
|
||||
'oracleOriginalDirectory':str(ORACLE).replace('\\','/'),
|
||||
'logicalFiles':dict(sorted(logical.items())),
|
||||
'members':{n:pin(b) for n,b in sorted(payload.items())}}
|
||||
with zipfile.ZipFile(OUT/'evidence.zip','w',compression=zipfile.ZIP_DEFLATED,compresslevel=9) as z:
|
||||
for name,b in sorted(payload.items()):
|
||||
info=zipfile.ZipInfo(name,date_time=(2026,9,13,0,0,0));info.compress_type=zipfile.ZIP_DEFLATED
|
||||
z.writestr(info,b)
|
||||
(OUT/'manifest.json').write_bytes((json.dumps(manifest,indent=2)+'\n').encode())
|
||||
for name in ('README.md','verify.py','prepare.py'):
|
||||
(OUT/name).write_bytes((OUT/name).read_bytes().replace(b'\r\n',b'\n'))
|
||||
files={name:pin((OUT/name).read_bytes()) for name in ('evidence.zip','manifest.json','README.md','verify.py','prepare.py')}
|
||||
(OUT/'files.json').write_bytes((json.dumps(files,indent=2)+'\n').encode())
|
||||
with zipfile.ZipFile(OUT/'evidence.zip') as z:
|
||||
assert all(z.read(logical[n])==b for n,b in (
|
||||
[('cargo/'+r+'/'+f.name,f.read_bytes()) for r in RUNS for f in sorted((GATES/r).iterdir())]+
|
||||
[('oracle/'+f.relative_to(ORACLE).as_posix(),f.read_bytes()) for f in sorted(ORACLE.rglob('*')) if f.is_file()]))
|
||||
print(json.dumps(files,indent=2))
|
||||
print('Exact raw fidelity:',len(logical),'logical files;',len(payload),'stored members')
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,132 @@
|
||||
"""Read-only standard-library verification; no archived source/binary execution."""
|
||||
from pathlib import Path, PurePosixPath
|
||||
from datetime import datetime
|
||||
import hashlib, io, json, zipfile
|
||||
ROOT=Path(__file__).resolve().parent
|
||||
def pin(b):return {'bytes':len(b),'sha256':hashlib.sha256(b).hexdigest()}
|
||||
def need(ok,msg):
|
||||
if not ok:raise ValueError(msg)
|
||||
def safe(n):return not PurePosixPath(n).is_absolute() and '..' not in PurePosixPath(n).parts and '\\' not in n and ':' not in n
|
||||
def members(z,expected):
|
||||
names=z.namelist()
|
||||
need(len(names)==len(set(names)) and set(names)==set(expected),'archive membership')
|
||||
need(all(safe(n) for n in names),'unsafe archive member')
|
||||
def time_order(*values):return list(map(datetime.fromisoformat,values))==sorted(map(datetime.fromisoformat,values))
|
||||
def main():
|
||||
files=json.loads((ROOT/'files.json').read_bytes())
|
||||
need(set(files)=={'evidence.zip','manifest.json','README.md','verify.py','prepare.py'},'outer inventory')
|
||||
for n,p in files.items():need(pin((ROOT/n).read_bytes())==p,'outer hash '+n)
|
||||
m=json.loads((ROOT/'manifest.json').read_bytes())
|
||||
with zipfile.ZipFile(ROOT/'evidence.zip') as z:
|
||||
members(z,m['members'])
|
||||
for n,p in m['members'].items():need(pin(z.read(n))==p,'member hash '+n)
|
||||
logical=m['logicalFiles']
|
||||
need(all(safe(n) for n in logical) and set(logical.values())==set(m['members']),'logical mapping')
|
||||
def raw(n):return z.read(logical[n])
|
||||
def obj(n):return json.loads(raw(n))
|
||||
def directory(prefix):return {n[len(prefix):]:pin(raw(n)) for n in logical if n.startswith(prefix)}
|
||||
pairs=set()
|
||||
for run,spec in m['cargo'].items():
|
||||
p='cargo/'+run+'/'
|
||||
start=obj(p+'started.json');result=obj(p+'result.json');process=obj(p+'process.json')
|
||||
need(all(result[k]==v for k,v in start.items()),run+' start/result')
|
||||
need(result['command']==process['command']==spec['command'],run+' commands')
|
||||
need(result['head']==spec['head'] and result['exitCode']==spec['exitCode'],run+' recorded outcome')
|
||||
need(result['inputsUnchanged'] is True and result['seconds']>=0,run+' source/duration')
|
||||
need(process['pid']>0 and time_order(start['start'],process['observed'],result['finish']),run+' process times')
|
||||
for field,n in [('inputs','inputs.json'),('archive','inputs.zip'),('stdout','stdout.log'),('stderr','stderr.log')]:need(result[field]==pin(raw(p+n)),run+' '+field)
|
||||
need(result['helper']==pin(raw(spec['helper'])),run+' helper')
|
||||
inputs=obj(p+'inputs.json')
|
||||
need(len(inputs)==result['inputCount']==spec['inputCount'],run+' input count')
|
||||
need(sum(n.startswith('crates/vendor/iced_widget/') for n in inputs)==71,run+' vendor inventory')
|
||||
pair=(logical[p+'inputs.json'],logical[p+'inputs.zip'])
|
||||
if pair not in pairs:
|
||||
with zipfile.ZipFile(io.BytesIO(raw(p+'inputs.zip'))) as source:
|
||||
members(source,inputs)
|
||||
for n,expected in inputs.items():need(pin(source.read(n))==expected,run+' source '+n)
|
||||
pairs.add(pair)
|
||||
artifacts=[]
|
||||
for line in raw(p+'stdout.log').decode('utf-8',errors='replace').splitlines():
|
||||
try:item=json.loads(line)
|
||||
except json.JSONDecodeError:continue
|
||||
if isinstance(item,dict) and item.get('reason')=='compiler-artifact' and item.get('executable'):artifacts.append(item)
|
||||
need(artifacts==[a['cargo'] for a in result['artifacts']],run+' artifact relationships')
|
||||
print(run,'exit',result['exitCode'],'inputs',len(inputs),'artifact records',len(artifacts))
|
||||
# Successful build1/generation are not the corrected checker admission.
|
||||
older=obj('cargo/lifecycle-producer-build-1/inputs.json')
|
||||
newer=obj('cargo/lifecycle-producer-build-2/inputs.json')
|
||||
need(older==obj('cargo/lifecycle-producer-generate-1/inputs.json'),'generation uses older producer inputs')
|
||||
source_path='harness/experiments/transition-native-lifecycle-20260913/producer/src/main.rs'
|
||||
need(older[source_path]!=newer[source_path],'distinct producer revisions')
|
||||
final_runs=('footer-windows-build-3','footer-format-2','lifecycle-producer-format-1')
|
||||
final_inputs=obj('cargo/footer-windows-build-3/inputs.json')
|
||||
final_pin='b5a62817a1b5df936b9d759241bdfc4e8283901c1d4b540b9e494995e1544d8d'
|
||||
for run in final_runs:
|
||||
p='cargo/'+run+'/'
|
||||
need(pin(raw(p+'inputs.json'))['sha256']==final_pin and obj(p+'inputs.json')==final_inputs,run+' final source manifest')
|
||||
need(obj(p+'result.json')['exitCode']==0 and len(final_inputs)==715,run+' final successful gate')
|
||||
for run in final_runs[1:]:
|
||||
need(not raw('cargo/'+run+'/stdout.log') and not raw('cargo/'+run+'/stderr.log'),run+' empty format streams')
|
||||
final_build=obj('cargo/footer-windows-build-3/result.json')
|
||||
need(len(final_build['artifacts'])==1 and final_build['artifacts'][0]['cargo']['target']['name']=='arcadia-studio','final GUI artifact')
|
||||
changed={n for n in set(newer)|set(final_inputs) if newer.get(n)!=final_inputs.get(n)}
|
||||
need(changed=={'crates/oa-desktop/src/workflow.rs'},'final wording-only file scope')
|
||||
with zipfile.ZipFile(io.BytesIO(raw('cargo/lifecycle-producer-build-2/inputs.zip'))) as before, zipfile.ZipFile(io.BytesIO(raw('cargo/footer-windows-build-3/inputs.zip'))) as after:
|
||||
path='crates/oa-desktop/src/workflow.rs';a=before.read(path);b=after.read(path)
|
||||
old='{} changes · {} boundary choices · {} conflicts · {} warnings'.encode()
|
||||
new='{} reviewed items · {} boundary choices · {} conflicts · {} warnings'.encode()
|
||||
need(a.count(old)==1 and a.replace(old,new,1)==b,'exact final footer label delta')
|
||||
build=obj('cargo/lifecycle-producer-build-2/result.json')
|
||||
artifact=[a for a in build['artifacts'] if a['cargo']['target']['name']=='transition-native-lifecycle']
|
||||
need(len(artifact)==1,'corrected checker artifact')
|
||||
# Oracle copied build capsule must match the independently included gate.
|
||||
for name in ('result.json','started.json','process.json','inputs.json','inputs.zip','stdout.log','stderr.log'):
|
||||
need(raw('oracle/build-capture/'+name)==raw('cargo/lifecycle-producer-build-2/'+name),'oracle build capsule '+name)
|
||||
need(pin(raw('oracle/build-capture/build-helper.py'))==build['helper'],'oracle build helper')
|
||||
start=obj('oracle/started.json');summary=obj('oracle/summary.json')
|
||||
need(summary['status']=='PASS' and summary['trialCount']==40,'oracle completion')
|
||||
need(all(summary['context'][k]==v for k,v in start.items() if k!='start'),'oracle context')
|
||||
need(start['checker']==artifact[0]['readback'] and start['buildReceipt']==pin(raw('cargo/lifecycle-producer-build-2/result.json')),'oracle checker chain')
|
||||
need(start['runner']==pin(raw('oracle/oracle-falsifiers.py')),'oracle runner pin')
|
||||
for path,p in directory('oracle/producer/').items():need(p==newer['harness/experiments/transition-native-lifecycle-20260913/producer/'+path],'oracle producer source '+path)
|
||||
fixtures=directory('oracle/fixtures/')
|
||||
need(fixtures==start['fixtureInput']==directory('oracle/repeat-generation-a/')==directory('oracle/repeat-generation-b/'),'deterministic repeated fixtures')
|
||||
need(directory('oracle/specimens/')==summary['specimens'],'mutation inventory')
|
||||
rows=summary['trials']
|
||||
need(rows==m['oracleExpectedTrials'] and len(rows)==len({r['label'] for r in rows})==40,'forty distinct trials')
|
||||
actual_labels={n.split('/')[2] for n in logical if n.startswith('oracle/trials/')}
|
||||
need(actual_labels=={r['label'] for r in rows},'exact trial membership')
|
||||
# Original absolute names are mapped to archive members, never opened.
|
||||
original=m['oracleOriginalDirectory'].rstrip('/')+'/'
|
||||
def mapped(path):
|
||||
norm=path.replace('\\','/')
|
||||
need(norm.startswith(original),'unmapped trial input path')
|
||||
rel=norm[len(original):];need(safe(rel),'unsafe trial input mapping')
|
||||
return 'oracle/'+rel
|
||||
for row in rows:
|
||||
label=row['label'];p='oracle/trials/'+label+'/'
|
||||
r=obj(p+'result.json')
|
||||
need(r['label']==label and r['exitCode']==r['expectedExitCode']==row['exitCode']==row['expectedExitCode'],label+' outcome')
|
||||
need(time_order(start['start'],r['start'],r['finish'],summary['finish']),label+' time order')
|
||||
need(r['checkerBefore']==r['checkerAfter']==start['checker'] and r['runner']==start['runner'] and r['buildReceipt']==start['buildReceipt'],label+' provenance')
|
||||
for stream in ('stdout','stderr'):need(r[stream]==pin(raw(p+stream+'.log')),label+' '+stream)
|
||||
need(r['watchedBefore']==r['watchedAfter'],label+' unchanged inputs')
|
||||
for path,expected in r['watchedBefore'].items():need(pin(raw(mapped(path)))==expected,label+' watched input')
|
||||
cmd=r['command'];need(cmd[0]==artifact[0]['cargo']['executable'],label+' executable command')
|
||||
if cmd[1]=='check':
|
||||
need(mapped(cmd[3]) in logical,label+' check entry')
|
||||
opts=cmd[4:];need(len(opts)%2==0,label+' option arity')
|
||||
for flag,path in zip(opts[::2],opts[1::2]):
|
||||
need(flag in ('--identities','--write-identities') and mapped(path) in logical,label+' identity option')
|
||||
if r['exitCode']==0:
|
||||
report=obj(p+'stdout.log')
|
||||
need(report['status']=='PASS' and report['comparison']=='complete Project equality' and report['state']==cmd[2] and report['entry']==cmd[3],label+' positive report')
|
||||
if cmd[2].endswith(('-reopened','-noop')):need(report['trace_identity_pinned'] is True and '--identities' in opts,label+' continuity receipt')
|
||||
if '--write-identities' in opts:need(obj(p+'written-identities.json')==report['new_ids']==obj(mapped(opts[opts.index('--write-identities')+1])),label+' identity write')
|
||||
else:need(cmd[1]=='generate' and len(cmd)==3,label+' generation command')
|
||||
stderr=raw(p+'stderr.log').decode('utf-8',errors='replace')
|
||||
if r['expectedStderrFragment']:need(r['expectedStderrFragment'] in stderr,label+' rejection reason')
|
||||
elif r['exitCode']==0:need(not stderr,label+' successful stderr')
|
||||
print('PASS:',len(m['cargo']),'closed Cargo runs; 40 recorded oracle trials;',len(logical),'logical raw files;',len(m['members']),'stored members;',len(pairs),'source capsule pairs.')
|
||||
print('Offline consistency only: no binaries/source executed; no native lifecycle completion claim.')
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,45 @@
|
||||
# Closed native transition lifecycle captures
|
||||
|
||||
Keep this directory beside `../checks/`. Run `python verify.py` with Python 3.10
|
||||
or newer. Verification requires the exact pinned `../checks/evidence.zip` and
|
||||
these native package files, uses only the standard library, and executes no
|
||||
archived helper, application, checker, Cargo command or network request.
|
||||
`prepare.py [CHECKOUT]` rebuilds this archive from the named closed originals.
|
||||
|
||||
Trials lifecycle-2/3 capture OA deletion, recreation, first relink, a separate
|
||||
reopening and no-op check. Trials lifecycle-4/5 capture reverse SA deletion,
|
||||
Undo/Redo, recreation, first relink, reopening and no-op. All four GUI receipts
|
||||
close successfully with empty stdout/stderr and unchanged fixture/binary pins.
|
||||
Trial1 is excluded entirely because it lacked a closing process receipt. This
|
||||
package does not turn that interrupted history into successful evidence.
|
||||
|
||||
Trials 2/3 use footer-windows-build-2; trials 4/5 use footer-windows-build-3 with
|
||||
the refined "reviewed items" footer. Snapshot checks use the corrected producer
|
||||
build2. Exact build receipts, helpers, artifact records and complete source
|
||||
capsules are read from the pinned sibling checks archive; source proof is not
|
||||
self-contained in the native archive alone. Runtime binaries are excluded.
|
||||
|
||||
Every original frame, adjacent metadata, trial file, accepted identity receipt,
|
||||
saved source snapshot and helper is retained without editing. The launch-pinned
|
||||
captureHelper refers to the corresponding capture-lifecycle-N.js source. Each
|
||||
trial's separately recorded capture-function.js is the serialized function used
|
||||
in the persistent UI session; it is different bytes and was not that launch pin.
|
||||
The files document the distinction rather than asserting byte equivalence or
|
||||
authenticated execution. Frame labels/timestamps/application paths record states,
|
||||
not a complete action invocation log or independent PID attestation. Image/window
|
||||
dimensions vary; these captures are not all minimum-window checks.
|
||||
|
||||
The verifier checks exact safe archive membership, hashes, linked build/source
|
||||
capsules, trial/helper/stream relationships, frame membership and timestamps,
|
||||
snapshot helper and corrected-checker provenance, and the recorded complete
|
||||
Project comparison results. Accepted first-relink IDs match later reopen/no-op
|
||||
receipts in each direction, and both later saved source bundles are byte-identical
|
||||
to their first-relink bundle. The producer's full-model result is recorded evidence;
|
||||
the portable verifier does not rerun its Rust comparisons or derive model equality
|
||||
from screenshots. Snapshot receipts record start time but no completion timestamp;
|
||||
only their start can be located in the GUI process interval.
|
||||
|
||||
These finite workflows do not establish exhaustive usability/accessibility,
|
||||
performance, deployment readiness, or behavior on another platform. No credentials,
|
||||
whole user profile, or runtime binaries are packaged. Outer text is UTF-8 LF;
|
||||
archived raw bytes and historical local-path strings remain unchanged.
|
||||
Binary file not shown.
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"evidence.zip": {
|
||||
"bytes": 6888298,
|
||||
"sha256": "b53253fbe9b98db6a0028ed3629426460e7086c8943be0d13ab5ec540e7e4b68"
|
||||
},
|
||||
"manifest.json": {
|
||||
"bytes": 56348,
|
||||
"sha256": "f6bd0bb5e3d29d5e47f1dfb8f1cb7747a3c2e12aa62b6e4e81b68e68ce5ccf95"
|
||||
},
|
||||
"README.md": {
|
||||
"bytes": 3019,
|
||||
"sha256": "01aed34547156bede9c2fea75162c8fecda18203fa575c4cf81e28e440130a95"
|
||||
},
|
||||
"verify.py": {
|
||||
"bytes": 10474,
|
||||
"sha256": "5f93ff8b266c0c7c3e64630f64a522ff5f8f8bd41d7d0a14cc4856c16a529306"
|
||||
},
|
||||
"prepare.py": {
|
||||
"bytes": 2861,
|
||||
"sha256": "c1884b10724cc618b58abd84b93c03623ea9e80dbda4e5a8b866fff499e93fca"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
"""Package only closed lifecycle trials 2-5; never runs a GUI or checker."""
|
||||
from pathlib import Path
|
||||
import hashlib,json,sys,zipfile
|
||||
OUT=Path(__file__).resolve().parent
|
||||
ROOT=Path(sys.argv[1]).resolve() if len(sys.argv)>1 else OUT.parents[3]
|
||||
SRC=ROOT/'.local/transition-native'
|
||||
def pin(b):return {'bytes':len(b),'sha256':hashlib.sha256(b).hexdigest()}
|
||||
def main():
|
||||
payload={};runs={}
|
||||
for number in (2,3,4,5):
|
||||
run=f'lifecycle-{number}';folder=SRC/run
|
||||
result=json.loads((folder/'result.json').read_bytes())
|
||||
assert result['exitCode']==0 and 'finish' in result,run+' not closed successfully'
|
||||
for f in sorted(folder.rglob('*')):
|
||||
if f.is_file():payload['native/'+f.relative_to(SRC).as_posix()]=f.read_bytes()
|
||||
helpers={}
|
||||
for field,name in [('runner',f'run-lifecycle-{number}.py'),('launchCapture',f'capture-lifecycle-{number}.js'),('snapshot',f'check-lifecycle-snapshot-{number}.py')]:
|
||||
helpers[field]='helpers/'+name;payload[helpers[field]]=(SRC/name).read_bytes()
|
||||
captures=[n for n,b in payload.items() if n.startswith('native/'+run+'/') and n.endswith('.json') and isinstance(json.loads(b),dict) and 'images' in json.loads(b)]
|
||||
snapshots={f.name:json.loads((f/'result.json').read_bytes())['command'][2] for f in sorted((folder/'snapshots').iterdir()) if f.is_dir()}
|
||||
runs[run]={'helpers':helpers,'guiBuild':'footer-windows-build-2' if number<4 else 'footer-windows-build-3','captures':captures,'snapshots':snapshots}
|
||||
for f in sorted((SRC/'lifecycle-input-1').rglob('*')):
|
||||
if f.is_file():payload['fixtures/'+f.relative_to(SRC/'lifecycle-input-1').as_posix()]=f.read_bytes()
|
||||
checks=OUT.parent/'checks'
|
||||
payload['references/checks-manifest.json']=(checks/'manifest.json').read_bytes()
|
||||
manifest={'schema':1,'checksEvidence':pin((checks/'evidence.zip').read_bytes()),
|
||||
'checksRelativePath':'../checks/evidence.zip','originalNativeDirectory':str(SRC).replace('\\','/'),
|
||||
'runs':runs,'members':{n:pin(b) for n,b in sorted(payload.items())}}
|
||||
with zipfile.ZipFile(OUT/'evidence.zip','w',compression=zipfile.ZIP_DEFLATED,compresslevel=9) as z:
|
||||
for n,b in sorted(payload.items()):
|
||||
info=zipfile.ZipInfo(n,date_time=(2026,9,22,0,0,0));info.compress_type=zipfile.ZIP_DEFLATED;z.writestr(info,b)
|
||||
(OUT/'manifest.json').write_bytes((json.dumps(manifest,indent=2)+'\n').encode())
|
||||
for n in ('README.md','verify.py','prepare.py'):(OUT/n).write_bytes((OUT/n).read_bytes().replace(b'\r\n',b'\n'))
|
||||
files={n:pin((OUT/n).read_bytes()) for n in ('evidence.zip','manifest.json','README.md','verify.py','prepare.py')}
|
||||
(OUT/'files.json').write_bytes((json.dumps(files,indent=2)+'\n').encode())
|
||||
print(json.dumps(files,indent=2));print('Native raw members:',len(payload))
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,115 @@
|
||||
"""Read-only offline verification. Requires the pinned sibling checks/evidence.zip."""
|
||||
from pathlib import Path,PurePosixPath
|
||||
from datetime import datetime
|
||||
import hashlib,io,json,zipfile
|
||||
ROOT=Path(__file__).resolve().parent
|
||||
def pin(b):return {'bytes':len(b),'sha256':hashlib.sha256(b).hexdigest()}
|
||||
def need(ok,msg):
|
||||
if not ok:raise ValueError(msg)
|
||||
def safe(n):return not PurePosixPath(n).is_absolute() and '..' not in PurePosixPath(n).parts and '\\' not in n and ':' not in n
|
||||
def members(z,expected):
|
||||
names=z.namelist();need(len(names)==len(set(names)) and set(names)==set(expected),'exact ZIP membership');need(all(safe(n) for n in names),'safe ZIP members')
|
||||
def ordered(*times):return list(map(datetime.fromisoformat,times))==sorted(map(datetime.fromisoformat,times))
|
||||
def main():
|
||||
files=json.loads((ROOT/'files.json').read_bytes())
|
||||
need(set(files)=={'evidence.zip','manifest.json','README.md','verify.py','prepare.py'},'outer inventory')
|
||||
for n,p in files.items():need(pin((ROOT/n).read_bytes())==p,'outer hash '+n)
|
||||
m=json.loads((ROOT/'manifest.json').read_bytes())
|
||||
need(m['checksRelativePath']=='../checks/evidence.zip','fixed sibling evidence location')
|
||||
checks_bytes=(ROOT/'../checks/evidence.zip').read_bytes()
|
||||
need(pin(checks_bytes)==m['checksEvidence'],'pinned sibling checks archive')
|
||||
with zipfile.ZipFile(ROOT/'evidence.zip') as z,zipfile.ZipFile(io.BytesIO(checks_bytes)) as checks:
|
||||
members(z,m['members'])
|
||||
for n,p in m['members'].items():need(pin(z.read(n))==p,'native member hash '+n)
|
||||
def raw(n):return z.read(n)
|
||||
def obj(n):return json.loads(raw(n))
|
||||
def directory(prefix):return {n[len(prefix):]:pin(raw(n)) for n in m['members'] if n.startswith(prefix)}
|
||||
cm=obj('references/checks-manifest.json');members(checks,cm['members'])
|
||||
def craw(logical):
|
||||
n=cm['logicalFiles'][logical];b=checks.read(n);need(pin(b)==cm['members'][n],'checks member '+n);return b
|
||||
def cobj(n):return json.loads(craw(n))
|
||||
# Source capsules and receipt helper chains are independently checked here.
|
||||
builds={};artifacts={};pairs=set()
|
||||
for run in ('footer-windows-build-2','footer-windows-build-3','lifecycle-producer-build-2'):
|
||||
p='cargo/'+run+'/';r=cobj(p+'result.json');start=cobj(p+'started.json');process=cobj(p+'process.json')
|
||||
need(all(r[k]==v for k,v in start.items()) and r['exitCode']==0 and r['inputsUnchanged'] is True,run+' build closure')
|
||||
need(r['command']==process['command'] and ordered(r['start'],process['observed'],r['finish']),run+' build process')
|
||||
for field,n in [('inputs','inputs.json'),('archive','inputs.zip'),('stdout','stdout.log'),('stderr','stderr.log')]:need(r[field]==pin(craw(p+n)),run+' receipt '+field)
|
||||
need(r['helper']==pin(craw(cm['cargo'][run]['helper'])),run+' build helper')
|
||||
inputs=cobj(p+'inputs.json');need(len(inputs)==r['inputCount'] and sum(n.startswith('crates/vendor/iced_widget/') for n in inputs)==71,run+' input inventory')
|
||||
pair=(r['inputs']['sha256'],r['archive']['sha256'])
|
||||
if pair not in pairs:
|
||||
with zipfile.ZipFile(io.BytesIO(craw(p+'inputs.zip'))) as source:
|
||||
members(source,inputs)
|
||||
for n,expected in inputs.items():need(pin(source.read(n))==expected,run+' source '+n)
|
||||
pairs.add(pair)
|
||||
records=[]
|
||||
for line in craw(p+'stdout.log').decode('utf-8',errors='replace').splitlines():
|
||||
try:item=json.loads(line)
|
||||
except json.JSONDecodeError:continue
|
||||
if isinstance(item,dict) and item.get('reason')=='compiler-artifact' and item.get('executable'):records.append(item)
|
||||
need(records==[a['cargo'] for a in r['artifacts']],run+' artifact records')
|
||||
expected='transition-native-lifecycle' if run.startswith('lifecycle') else 'arcadia-studio'
|
||||
selected=[a for a in r['artifacts'] if a['cargo']['target']['name']==expected];need(len(selected)==1,run+' unique artifact')
|
||||
builds[run]=r;artifacts[run]=selected[0]
|
||||
original=m['originalNativeDirectory'].rstrip('/')+'/'
|
||||
def mapped(path):
|
||||
norm=path.replace('\\','/');need(norm.startswith(original),'snapshot path outside captured native root')
|
||||
rel=norm[len(original):];need(safe(rel),'unsafe snapshot path');return 'native/'+rel
|
||||
fixtures=directory('fixtures/');snapshots={};reports={};capture_count=image_count=0
|
||||
need(set(m['runs'])=={'lifecycle-2','lifecycle-3','lifecycle-4','lifecycle-5'},'closed trial membership')
|
||||
for run,spec in m['runs'].items():
|
||||
p='native/'+run+'/';r=obj(p+'result.json');start=obj(p+'started.json');process=obj(p+'process.json')
|
||||
build=builds[spec['guiBuild']];artifact=artifacts[spec['guiBuild']]
|
||||
need(all(r[k]==v for k,v in start.items()),run+' start/result relationship')
|
||||
need(r['exitCode']==0 and r['inputUnchanged'] is True and r['binary']==r['binaryAfter']==artifact['readback'],run+' native closure and binary pin')
|
||||
need(r['buildReceipt']==pin(craw('cargo/'+spec['guiBuild']+'/result.json')) and r['buildHead']==build['head'],run+' GUI build relationship')
|
||||
need(r['command']==[artifact['cargo']['executable']] and process['pid']>0,run+' native command/PID')
|
||||
need(ordered(build['finish'],r['start'],process['observed'],r['finish']),run+' native times')
|
||||
need(r['helper']==pin(raw(spec['helpers']['runner'])) and r['captureHelper']==pin(raw(spec['helpers']['launchCapture'])),run+' launch helper pins')
|
||||
need(p+'capture-function.js' in m['members'],run+' actual capture-function record')
|
||||
need(r['inputs']==pin(raw(p+'inputs.json')) and obj(p+'inputs.json')==fixtures,run+' unchanged fixture bytes')
|
||||
for stream in ('stdout','stderr'):need(not raw(p+stream+'.log') and r[stream]==pin(raw(p+stream+'.log')),run+' empty '+stream)
|
||||
images=[]
|
||||
actual_captures=[n for n in m['members'] if n.startswith(p) and n.endswith('.json') and isinstance(obj(n),dict) and 'images' in obj(n)]
|
||||
need(set(actual_captures)==set(spec['captures']) and len(actual_captures)==len(spec['captures']),run+' exact metadata coverage')
|
||||
for n in spec['captures']:
|
||||
frame=obj(n);need(ordered(r['start'],frame['time'],r['finish']),n+' frame timestamp')
|
||||
need(frame['window']['app'].replace('\\','/')=='process:'+r['command'][0].replace('\\','/'),n+' window executable path')
|
||||
for image in frame['images']:
|
||||
name=p+image['file'];b=raw(name)
|
||||
need(image['width']>0 and image['height']>0 and (b.startswith(b'\xff\xd8') or b.startswith(b'\x89PNG')),n+' image record')
|
||||
images.append(name)
|
||||
actual_images={n for n in m['members'] if n.startswith(p) and n.endswith(('.jpg','.png'))}
|
||||
need(len(images)==len(set(images)) and set(images)==actual_images,run+' all original images')
|
||||
capture_count+=len(spec['captures']);image_count+=len(images)
|
||||
actual_snapshots={n.split('/')[3] for n in m['members'] if n.startswith(p+'snapshots/')}
|
||||
need(actual_snapshots==set(spec['snapshots']),run+' snapshot inventory')
|
||||
for label,state in spec['snapshots'].items():
|
||||
q=p+'snapshots/'+label+'/';s=obj(q+'result.json');ss=obj(q+'started.json');report=obj(q+'stdout.log')
|
||||
checker=artifacts['lifecycle-producer-build-2']
|
||||
need(all(s[k]==v for k,v in ss.items()) and s['exitCode']==0 and s['binaryUnchanged'] is True and s['workingSourceUnchanged'] is True,label+' snapshot closure')
|
||||
need(ordered(r['start'],s['time'],r['finish']),label+' snapshot start during trial')
|
||||
need(s['binary']==checker['readback'] and s['buildReceipt']==pin(craw('cargo/lifecycle-producer-build-2/result.json')),label+' corrected checker build')
|
||||
need(s['helper']==pin(raw(spec['helpers']['snapshot'])),label+' snapshot helper pin')
|
||||
need(s['source']==directory(q+'source/'),label+' exact saved source bytes')
|
||||
for stream in ('stdout','stderr'):need(s[stream]==pin(raw(q+stream+'.log')),label+' '+stream)
|
||||
need(not raw(q+'stderr.log'),label+' empty snapshot stderr')
|
||||
cmd=s['command'];need(cmd[:3]==[checker['cargo']['executable'],'check',state] and mapped(cmd[3])==q+'source/original.arcadia',label+' checker command')
|
||||
need(report['status']=='PASS' and report['comparison']=='complete Project equality' and report['state']==state and report['entry']==cmd[3],label+' accepted model oracle')
|
||||
opts=cmd[4:];need(len(opts)%2==0,label+' option pairs')
|
||||
for flag,path in zip(opts[::2],opts[1::2]):
|
||||
need(flag in ('--identities','--write-identities'),label+' identity-only options')
|
||||
need(obj(mapped(path))==report['new_ids'],label+' exact identity receipt')
|
||||
if state.endswith(('-reopened','-noop')):need('--identities' in opts and report['trace_identity_pinned'] is True,label+' continuity identity pin')
|
||||
if state.endswith('-relinked'):need('--write-identities' in opts and mapped(report['identity_receipt'])==mapped(opts[opts.index('--write-identities')+1]),label+' initial accepted identities')
|
||||
snapshots[(run,label)]=s['source'];reports[(run,label)]=report
|
||||
print(run,'captures',len(spec['captures']),'images',len(images),'accepted snapshots',len(spec['snapshots']))
|
||||
for side,first,later in [('oa','lifecycle-2','lifecycle-3'),('sa','lifecycle-4','lifecycle-5')]:
|
||||
base=snapshots[(first,side+'-relinked')];ids=reports[(first,side+'-relinked')]['new_ids']
|
||||
for phase in ('reopened','noop'):
|
||||
key=(later,side+'-'+phase)
|
||||
need(snapshots[key]==base and reports[key]['new_ids']==ids,side+' saved bytes and identity continuity '+phase)
|
||||
print('PASS: 4 closed trials;',len(snapshots),'accepted full-model receipts;',capture_count,'capture records;',image_count,'original images;',len(pairs),'linked source capsules.')
|
||||
print('No UI history authentication or checker re-execution; snapshot receipts lack completion timestamps. Frame dimensions vary.')
|
||||
if __name__=='__main__':main()
|
||||
+434
@@ -0,0 +1,434 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oa-core"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"num-bigint",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"same-file",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1_smol"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "transition-native-lifecycle"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"oa-core",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"js-sys",
|
||||
"serde_core",
|
||||
"sha1_smol",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "transition-native-lifecycle"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[workspace]
|
||||
|
||||
[dependencies]
|
||||
oa-core = { path = "../../../../crates/oa-core" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -0,0 +1,154 @@
|
||||
# Native transition lifecycle fixture and oracle
|
||||
|
||||
This isolated producer/checker prepares native OA↔SA acceptance work. It does
|
||||
not automate the application. The project owns no production code. It has its
|
||||
own Cargo workspace and depends on this checkout's `oa-core` by relative path.
|
||||
|
||||
## Commands
|
||||
|
||||
From the repository root, with the configured Rust toolchain and compiler gate:
|
||||
|
||||
```powershell
|
||||
cargo run --manifest-path harness/experiments/transition-native-lifecycle-20260913/producer/Cargo.toml -- generate .local/transition-native/lifecycle-input-1
|
||||
cargo run --manifest-path harness/experiments/transition-native-lifecycle-20260913/producer/Cargo.toml -- check original .local/transition-native/lifecycle-input-1/original.arcadia
|
||||
```
|
||||
|
||||
The parent owns compilation, execution and native verification. These commands
|
||||
are instructions, not evidence that they have run. Generation refuses to
|
||||
overwrite an existing deliverable; use a new output directory for a new run.
|
||||
All fixture identities are reproducible UUIDv5 values of fixed aliases in one
|
||||
fixed namespace. Generating into a second directory must reproduce file bytes.
|
||||
|
||||
`generate OUTDIR` validates and round-trips each expected project, then emits:
|
||||
|
||||
- `original.arcadia`, `original.json`, and `manifest.json` with owned removal IDs.
|
||||
- `oa-deleted.arcadia` / `.json` and `sa-deleted.arcadia` / `.json`.
|
||||
- `oa-recreated.arcadia` / `.json` and `sa-recreated.arcadia` / `.json`.
|
||||
- `oa-recreate-root.snippet` and `sa-recreate-root.snippet`.
|
||||
|
||||
The deleted/recreated files are independent expected artifacts. Opening them
|
||||
does not establish that the native deletion, Undo or editing operation works.
|
||||
Each snippet contains only its replacement root declaration, with the original
|
||||
alias, name and description, a deterministic new UUID, no children, and no
|
||||
placement. Generation verifies that inserting it immediately before the deleted
|
||||
project's final closing brace compiles to the exact expected recreated Project.
|
||||
|
||||
`check STATE SAVED_ENTRY [--placements PATH] [--identities PATH]
|
||||
[--write-identities PATH]` loads the actual saved entry and its reachable
|
||||
includes through the public disk loader/compiler. It rejects any source bundle
|
||||
changed by compilation, including declarations with missing UUIDs. An identity
|
||||
inserted only into the compiler's memory cannot prove a saved identity. States:
|
||||
|
||||
| State | Required saved model |
|
||||
| --- | --- |
|
||||
| `original` | Entire original Project, including every original UUID |
|
||||
| `oa-deleted`, `sa-deleted` | Explicit source-owned removals; opposite design and shared definitions retained exactly |
|
||||
| `oa-recreated`, `sa-recreated` | Deleted state plus only the original source root under its pinned replacement UUID |
|
||||
| `oa-relinked`, `sa-relinked` | First relink: recreated state plus exactly one new refinement trace, with both existing prose values acknowledged |
|
||||
| `oa-reopened`, `sa-reopened` | Same relinked model; requires `--identities` with its previously accepted trace UUID |
|
||||
| `oa-noop`, `sa-noop` | Same relinked model after unchanged retry; requires `--identities` with its previously accepted trace UUID |
|
||||
|
||||
The checker compares every Project field, including IDs, aliases, prose, typed
|
||||
bodies, ownership, item/port contracts, relations, trace baselines, diagram
|
||||
geometry/routes and artifacts. A mismatch reports its first differing JSON path.
|
||||
No deletion impact helper, transition planner, or observed post-deletion state
|
||||
constructs the expected removal set. Relinking expectations construct a new
|
||||
Trace directly. The source UUID is always the deterministic snippet UUID unless
|
||||
an explicit `--identities` file supplies another source UUID. Only the first
|
||||
`*-relinked` check can observe a fresh trace UUID. Use `--write-identities PATH`
|
||||
on that successful check to write a receipt, then pass that exact receipt using
|
||||
`--identities PATH` for reopening and no-op checks. Those stages refuse to run
|
||||
without a pinned trace UUID and reject any replacement of source, trace or
|
||||
declared placement identities. Receipt writes refuse overwrite and happen only
|
||||
after complete-model comparison succeeds.
|
||||
|
||||
An identity receipt is a JSON object with `source` and, after relinking, `trace`
|
||||
UUID strings. Optional declared placements add `placement:ALIAS` UUID entries.
|
||||
An explicitly authored source-only identity file can establish a different
|
||||
GUI-created root UUID for initial recreation/relinking. Do not re-extract it
|
||||
from later saves. The checker rejects unused identity entries and reports whether
|
||||
trace identity was pinned; the first relink establishes identity evidence but
|
||||
does not itself prove preservation across save/reopen.
|
||||
|
||||
Example using the already compiled producer executable:
|
||||
|
||||
```powershell
|
||||
transition-native-lifecycle check oa-relinked saved-first-relink.arcadia --write-identities first-relink.identities.json
|
||||
transition-native-lifecycle check oa-reopened saved-after-reopen.arcadia --identities first-relink.identities.json
|
||||
transition-native-lifecycle check oa-noop saved-after-noop.arcadia --identities first-relink.identities.json
|
||||
```
|
||||
|
||||
## Native workflow
|
||||
|
||||
| Layer | Root to select | Alias | Diagrams |
|
||||
| --- | --- | --- | --- |
|
||||
| OA | Coordinate distress response | `oa_response` | `oa_work`, `oa_overview` |
|
||||
| SA | Deliver coordinated response | `sa_response` | `sa_work`, `sa_overview` |
|
||||
|
||||
Both roots own two work children with an internal report exchange. The SA
|
||||
children own typed report input/output ports. Shared report definitions have
|
||||
Flow mechanisms and String payload fields. OA intentionally has no ports.
|
||||
Both layers have an expanded decomposition and overview diagram, authored
|
||||
placements, exchange routing/labels, and artifacts on roots, children,
|
||||
exchanges, placements and shared items. The initial correspondence and its
|
||||
artifact must disappear when either root is deleted. Shared item definitions,
|
||||
their fields/evidence, diagrams and the independent System remain.
|
||||
|
||||
1. Open `original.arcadia`. Save a native snapshot and check `original`.
|
||||
2. Graphically delete **Coordinate distress response** (`oa_response`). Save a
|
||||
snapshot and check `oa-deleted`. Inspect the complete retained SA design.
|
||||
3. Native Undo, save another snapshot and check `original`. This checks the
|
||||
actual saved result, including original IDs and all owned records.
|
||||
4. Delete OA again. In the native Textual model editor, insert the inspected
|
||||
`oa-recreate-root.snippet` immediately before the project's final `}`. Apply,
|
||||
save, and check `oa-recreated`. Do not open the generated recreated fixture
|
||||
as a substitute for this editing step.
|
||||
5. Select the newly recreated `oa_response` and start OA→SA transition. Choose
|
||||
existing `sa_response` explicitly. Select **Keep target** for both Name and
|
||||
Description; do not request child/required dependency creation. The existing
|
||||
target already has placements. Apply, save, and check `oa-relinked` with
|
||||
`--write-identities first-relink.identities.json`.
|
||||
6. Reopen the saved result and save a fresh snapshot; check `oa-reopened` with
|
||||
`--identities first-relink.identities.json`.
|
||||
Re-preview unchanged and inspect reuse/no changes. Applying any no-op and
|
||||
saving must pass `oa-noop` with the same original identity receipt. Do not
|
||||
substitute another unpinned `oa-relinked` observation for these later checks.
|
||||
7. For the reverse workflow, start again from `original.arcadia`, delete
|
||||
`sa_response`, use the SA snippet, and explicitly relink to retained
|
||||
`oa_response`, using the corresponding `sa-*` checker states.
|
||||
|
||||
These instructions separate source recreation in the text editor from relinking
|
||||
through the graphical transition dialog, avoiding unpredictable canvas placement
|
||||
side effects. The newly recreated root deliberately has no children or placement.
|
||||
|
||||
## Optional explicitly authored source placement
|
||||
|
||||
If a separate GUI recreation task intentionally creates a placement, pass
|
||||
`--placements` with an expectation file shaped like `oa-placement.example.json`. Set its diagram, alias,
|
||||
position, size and collapsed state to the intended interaction inputs. Only the
|
||||
new placement UUID is observed during the first check; all its other fields are
|
||||
compared with the authored expectation. It must reference the new source root
|
||||
and use the existing source-layer work/overview diagram. Undeclared placements,
|
||||
changed target geometry and extra artifacts fail comparison. The snippet
|
||||
workflow requires no placement expectation file. Later checks must supply the
|
||||
same placement specifications and the saved identity receipt; the placement UUID
|
||||
is then pinned rather than re-learned from the new saved model.
|
||||
|
||||
## Explicit incompleteness and limits
|
||||
|
||||
The architecture fixture deliberately omits performer allocations and
|
||||
process/chain/scenario contexts. Admission requires exactly `unallocated_leaf`
|
||||
(`allocation`) and `work_without_behavior_context` (no field) on each surviving
|
||||
leaf work UUID, and `exchange_without_behavior_context` (no field) on each
|
||||
surviving work exchange UUID. Errors, extra warnings, wrong warning owners and
|
||||
duplicate warnings fail. System and data contracts are modeled explicitly.
|
||||
The disk loader and source compiler may report these same engineering warnings.
|
||||
Their diagnostic multisets must exactly match the validated model warnings by
|
||||
severity, code, object UUID and field. Additional loading warnings, missing or
|
||||
duplicate warnings, and all source-loading errors remain failures.
|
||||
|
||||
Whole-model comparison and source/JSON round trips do not prove usability,
|
||||
authored comment/whitespace preservation, native Undo without saving, or HTTP
|
||||
retry behavior. The parent must record actual native interactions, executable
|
||||
provenance, saved snapshots and checker results. This tool does not perform
|
||||
native interaction or publish an acceptance result itself.
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"diagram": "oa_work",
|
||||
"alias": "view_coordinate_distress_response",
|
||||
"position": { "x": 60.0, "y": 60.0 },
|
||||
"size": { "width": 224.0, "height": 94.0 },
|
||||
"collapsed": false
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,914 @@
|
||||
use oa_core::{
|
||||
language, source_files,
|
||||
source_project::{ProjectLimits, SourceBundle, compile_bundle},
|
||||
validation::{self, Severity},
|
||||
*,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::BTreeMap, fs, io::Write, path::Path};
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
fn id(alias: &str) -> Id {
|
||||
Id::new_v5(
|
||||
&Id::from_u128(0xc1192026_0913_5000_8000_000000000028),
|
||||
alias.as_bytes(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Side {
|
||||
prefix: String,
|
||||
root: Id,
|
||||
elements: Vec<Id>,
|
||||
exchange: Id,
|
||||
placements: Vec<(Id, Id)>,
|
||||
artifacts: Vec<Id>,
|
||||
}
|
||||
|
||||
struct Fixture {
|
||||
project: Project,
|
||||
oa: Side,
|
||||
sa: Side,
|
||||
}
|
||||
|
||||
fn add(
|
||||
p: &mut Project,
|
||||
kind: Kind,
|
||||
layer: Layer,
|
||||
alias: &str,
|
||||
name: &str,
|
||||
parent: Option<Id>,
|
||||
) -> Id {
|
||||
let key = id(alias);
|
||||
assert!(
|
||||
p.elements
|
||||
.insert(
|
||||
key,
|
||||
Element {
|
||||
id: key,
|
||||
alias: alias.into(),
|
||||
name: name.into(),
|
||||
description: String::new(),
|
||||
parent,
|
||||
body: ElementBody::new(kind, layer).unwrap(),
|
||||
}
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
key
|
||||
}
|
||||
|
||||
fn artifact(p: &mut Project, owner: Id, alias: &str, name: &str) -> Id {
|
||||
let key = id(alias);
|
||||
p.artifact_links.insert(
|
||||
key,
|
||||
ArtifactLink {
|
||||
id: key,
|
||||
alias: alias.into(),
|
||||
name: name.into(),
|
||||
description: "Reviewed evidence must survive opposite-layer deletion.".into(),
|
||||
owner,
|
||||
relation: ArtifactRelation::References,
|
||||
uri: "git:response/design.md".into(),
|
||||
revision: Some("review-17".into()),
|
||||
fragment: Some("response-handling".into()),
|
||||
},
|
||||
);
|
||||
key
|
||||
}
|
||||
|
||||
fn port(p: &mut Project, alias: &str, owner: Id, item: Id, output: bool) -> Id {
|
||||
let key = id(alias);
|
||||
p.elements.insert(
|
||||
key,
|
||||
Element {
|
||||
id: key,
|
||||
alias: alias.into(),
|
||||
name: if output {
|
||||
"Report output"
|
||||
} else {
|
||||
"Report input"
|
||||
}
|
||||
.into(),
|
||||
description: "Retain the typed report contract.".into(),
|
||||
parent: Some(owner),
|
||||
body: ElementBody::shared_in(
|
||||
Layer::Sa,
|
||||
SharedBody::Port {
|
||||
contract: if output {
|
||||
PortContract::FunctionOutput {
|
||||
outgoing_items: vec![item],
|
||||
}
|
||||
} else {
|
||||
PortContract::FunctionInput {
|
||||
incoming_items: vec![item],
|
||||
}
|
||||
},
|
||||
legacy: None,
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
key
|
||||
}
|
||||
|
||||
fn side(p: &mut Project, layer: Layer) -> Side {
|
||||
let (prefix, kind, root_name, send_name, receive_name, description) = match layer {
|
||||
Layer::Oa => (
|
||||
"oa",
|
||||
Kind::Activity,
|
||||
"Coordinate distress response",
|
||||
"Report distress",
|
||||
"Assess incident",
|
||||
"Coordinate responders while preserving operational responsibility.",
|
||||
),
|
||||
Layer::Sa => (
|
||||
"sa",
|
||||
Kind::Function,
|
||||
"Deliver coordinated response",
|
||||
"Transmit report",
|
||||
"Classify incident",
|
||||
"Technical design: preserve independent processing and report contracts.",
|
||||
),
|
||||
};
|
||||
let root = add(
|
||||
p,
|
||||
kind,
|
||||
layer,
|
||||
&format!("{prefix}_response"),
|
||||
root_name,
|
||||
None,
|
||||
);
|
||||
p.elements.get_mut(&root).unwrap().description = description.into();
|
||||
let send = add(
|
||||
p,
|
||||
kind,
|
||||
layer,
|
||||
&format!("{prefix}_send"),
|
||||
send_name,
|
||||
Some(root),
|
||||
);
|
||||
let receive = add(
|
||||
p,
|
||||
kind,
|
||||
layer,
|
||||
&format!("{prefix}_receive"),
|
||||
receive_name,
|
||||
Some(root),
|
||||
);
|
||||
p.elements.get_mut(&send).unwrap().description =
|
||||
"Keep the detailed sending responsibility.".into();
|
||||
p.elements.get_mut(&receive).unwrap().description =
|
||||
"Keep the detailed receiving responsibility.".into();
|
||||
let item = add(
|
||||
p,
|
||||
Kind::ExchangeItem,
|
||||
layer,
|
||||
&format!("{prefix}_report"),
|
||||
"Incident report",
|
||||
None,
|
||||
);
|
||||
*p.elements.get_mut(&item).unwrap().shared_mut().unwrap() = SharedBody::ExchangeItem {
|
||||
kind: ExchangeItemKind::Information,
|
||||
mechanism: ExchangeMechanism::Flow,
|
||||
supertype: None,
|
||||
};
|
||||
add(
|
||||
p,
|
||||
Kind::DataField,
|
||||
layer,
|
||||
&format!("{prefix}_report_text"),
|
||||
"Report text",
|
||||
Some(item),
|
||||
);
|
||||
let mut elements = vec![root, send, receive];
|
||||
let body = if layer == Layer::Sa {
|
||||
let output = port(p, "sa_send_output", send, item, true);
|
||||
let input = port(p, "sa_receive_input", receive, item, false);
|
||||
elements.extend([output, input]);
|
||||
RelationBody::FunctionalExchange {
|
||||
source: send,
|
||||
target: receive,
|
||||
source_port: output,
|
||||
target_port: input,
|
||||
items: vec![item],
|
||||
}
|
||||
} else {
|
||||
RelationBody::Interaction {
|
||||
source: send,
|
||||
target: receive,
|
||||
items: vec![item],
|
||||
}
|
||||
};
|
||||
let exchange_alias = format!("{prefix}_report_delivery");
|
||||
let exchange = id(&exchange_alias);
|
||||
p.relations.insert(
|
||||
exchange,
|
||||
Relation {
|
||||
id: exchange,
|
||||
alias: exchange_alias,
|
||||
name: "Deliver incident report".into(),
|
||||
description: "Preserve direction, item identity and endpoint contracts.".into(),
|
||||
body,
|
||||
},
|
||||
);
|
||||
let mut placements = vec![];
|
||||
let mut artifacts = vec![
|
||||
artifact(
|
||||
p,
|
||||
root,
|
||||
&format!("{prefix}_root_evidence"),
|
||||
"Response rationale",
|
||||
),
|
||||
artifact(
|
||||
p,
|
||||
send,
|
||||
&format!("{prefix}_send_evidence"),
|
||||
"Sending requirement",
|
||||
),
|
||||
artifact(
|
||||
p,
|
||||
exchange,
|
||||
&format!("{prefix}_exchange_evidence"),
|
||||
"Report contract evidence",
|
||||
),
|
||||
];
|
||||
for (suffix, title, projection) in [
|
||||
(
|
||||
"work",
|
||||
"Response decomposition",
|
||||
ProjectionKind::Decomposition,
|
||||
),
|
||||
("overview", "Response overview", ProjectionKind::Overview),
|
||||
] {
|
||||
let alias = format!("{prefix}_{suffix}");
|
||||
let diagram = id(&alias);
|
||||
let mut nodes = BTreeMap::new();
|
||||
for (index, (element, short)) in [(root, "root"), (send, "send"), (receive, "receive")]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let node_alias = format!("{prefix}_{suffix}_{short}");
|
||||
let node_id = id(&node_alias);
|
||||
nodes.insert(
|
||||
node_id,
|
||||
NodePlacement {
|
||||
id: node_id,
|
||||
alias: node_alias,
|
||||
element,
|
||||
position: match index {
|
||||
0 => Position { x: 40., y: 40. },
|
||||
1 => Position { x: 80., y: 120. },
|
||||
_ => Position { x: 400., y: 120. },
|
||||
},
|
||||
size: if index == 0 {
|
||||
Size {
|
||||
width: 660.,
|
||||
height: 240.,
|
||||
}
|
||||
} else {
|
||||
Size {
|
||||
width: 224.,
|
||||
height: 94.,
|
||||
}
|
||||
},
|
||||
collapsed: false,
|
||||
port_anchors: vec![],
|
||||
},
|
||||
);
|
||||
placements.push((diagram, node_id));
|
||||
if index == 0 {
|
||||
artifacts.push(artifact(
|
||||
p,
|
||||
node_id,
|
||||
&format!("{prefix}_{suffix}_layout_evidence"),
|
||||
"Reviewed layout",
|
||||
));
|
||||
}
|
||||
}
|
||||
p.diagrams.insert(
|
||||
diagram,
|
||||
Diagram {
|
||||
id: diagram,
|
||||
alias,
|
||||
name: format!("{} {title}", layer),
|
||||
description: "Inspect the response root, its children and their original exchange."
|
||||
.into(),
|
||||
layer,
|
||||
projection,
|
||||
view_options: Default::default(),
|
||||
nodes,
|
||||
routes: vec![EdgeRoute {
|
||||
edge: exchange,
|
||||
waypoints: vec![Position { x: 360., y: 167. }],
|
||||
label: Some(EdgeLabel {
|
||||
fraction: 0.55,
|
||||
offset: Position { x: 0., y: -18. },
|
||||
}),
|
||||
}],
|
||||
},
|
||||
);
|
||||
}
|
||||
artifact(
|
||||
p,
|
||||
item,
|
||||
&format!("{prefix}_item_evidence"),
|
||||
"Shared report definition",
|
||||
);
|
||||
Side {
|
||||
prefix: prefix.into(),
|
||||
root,
|
||||
elements,
|
||||
exchange,
|
||||
placements,
|
||||
artifacts,
|
||||
}
|
||||
}
|
||||
|
||||
fn fixture() -> Fixture {
|
||||
let mut project = Project::empty();
|
||||
project.id = id("response_lifecycle");
|
||||
project.alias = "response_lifecycle".into();
|
||||
project.name = "Distress response: transition lifecycle".into();
|
||||
project.description =
|
||||
"Native acceptance fixture for preserving opposite-layer engineering work.".into();
|
||||
add(
|
||||
&mut project,
|
||||
Kind::System,
|
||||
Layer::Sa,
|
||||
"response_system",
|
||||
"Response support system",
|
||||
None,
|
||||
);
|
||||
let oa = side(&mut project, Layer::Oa);
|
||||
let sa = side(&mut project, Layer::Sa);
|
||||
let key = id("response_refinement");
|
||||
project.traces.insert(
|
||||
key,
|
||||
Trace {
|
||||
id: key,
|
||||
alias: "response_refinement".into(),
|
||||
name: "Reviewed response refinement".into(),
|
||||
description: "Operational and technical prose are intentionally different.".into(),
|
||||
oa: oa.root,
|
||||
sa: sa.root,
|
||||
kind: TraceKind::Refinement,
|
||||
baseline: Some(TraceBaseline {
|
||||
oa: project.properties(oa.root).unwrap(),
|
||||
sa: project.properties(sa.root).unwrap(),
|
||||
semantic: None,
|
||||
}),
|
||||
},
|
||||
);
|
||||
artifact(
|
||||
&mut project,
|
||||
key,
|
||||
"response_trace_evidence",
|
||||
"Review decision",
|
||||
);
|
||||
Fixture { project, oa, sa }
|
||||
}
|
||||
|
||||
// The removal oracle enumerates fixture-owned records. No deletion command,
|
||||
// impact helper, transition planner or observed post-deletion model is used.
|
||||
fn deleted(f: &Fixture, side: &Side) -> Project {
|
||||
let mut p = f.project.clone();
|
||||
for key in &side.elements {
|
||||
assert!(p.elements.remove(key).is_some());
|
||||
}
|
||||
assert!(p.relations.remove(&side.exchange).is_some());
|
||||
assert!(p.traces.remove(&id("response_refinement")).is_some());
|
||||
for key in side
|
||||
.artifacts
|
||||
.iter()
|
||||
.copied()
|
||||
.chain([id("response_trace_evidence")])
|
||||
{
|
||||
assert!(p.artifact_links.remove(&key).is_some());
|
||||
}
|
||||
for (diagram, node) in &side.placements {
|
||||
assert!(
|
||||
p.diagrams
|
||||
.get_mut(diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.remove(node)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
for suffix in ["work", "overview"] {
|
||||
let diagram = p
|
||||
.diagrams
|
||||
.get_mut(&id(&format!("{}_{suffix}", side.prefix)))
|
||||
.unwrap();
|
||||
assert_eq!(diagram.routes.len(), 1);
|
||||
assert_eq!(diagram.routes[0].edge, side.exchange);
|
||||
diagram.routes.clear();
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
fn accepted(p: &Project) -> Result<Vec<validation::Issue>> {
|
||||
// Intentional isolated architecture: no allocations, processes or scenarios.
|
||||
// Require the exact warning multiset by record UUID, code and field.
|
||||
let issues = validation::validate(p);
|
||||
if issues.iter().any(|i| i.severity != Severity::Warning) {
|
||||
return Err(format!("Structural validation failed: {issues:?}").into());
|
||||
}
|
||||
let mut expected = vec![];
|
||||
for e in p.elements.values().filter(|e| {
|
||||
matches!(e.kind(), Kind::Activity | Kind::Function)
|
||||
&& !p
|
||||
.elements
|
||||
.values()
|
||||
.any(|child| child.parent == Some(e.id) && child.kind() == e.kind())
|
||||
}) {
|
||||
expected.push(("unallocated_leaf", Some(e.id), Some("allocation")));
|
||||
expected.push(("work_without_behavior_context", Some(e.id), None));
|
||||
}
|
||||
for r in p.relations.values().filter(|r| {
|
||||
matches!(
|
||||
r.kind(),
|
||||
RelationKind::Interaction | RelationKind::FunctionalExchange
|
||||
)
|
||||
}) {
|
||||
expected.push(("exchange_without_behavior_context", Some(r.id), None));
|
||||
}
|
||||
let mut actual = issues
|
||||
.iter()
|
||||
.map(|i| (i.code.as_str(), i.element, i.field.as_deref()))
|
||||
.collect::<Vec<_>>();
|
||||
expected.sort();
|
||||
actual.sort();
|
||||
if expected != actual {
|
||||
return Err(
|
||||
format!("Unexpected warnings: expected {expected:?}; actual {actual:?}").into(),
|
||||
);
|
||||
}
|
||||
Ok(issues)
|
||||
}
|
||||
|
||||
fn compile(source: String) -> Result<Project> {
|
||||
let bundle = SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([("project.arcadia".into(), source)]),
|
||||
};
|
||||
let result = compile_bundle(&bundle, ProjectLimits::default())
|
||||
.map_err(|e| format!("Source compile: {e:?}"))?;
|
||||
Ok(result.decoded.project)
|
||||
}
|
||||
|
||||
fn round_trip(p: &Project) -> Result<String> {
|
||||
accepted(p)?;
|
||||
let bytes = serde_json::to_vec_pretty(p)?;
|
||||
compare(p, &persistence::decode(&bytes)?)?;
|
||||
let source = language::format(p);
|
||||
compare(p, &compile(source.clone())?)?;
|
||||
Ok(source)
|
||||
}
|
||||
|
||||
fn recreated(f: &Fixture, side: &Side, new_id: Id) -> Project {
|
||||
assert!(!f.project.contains(new_id));
|
||||
let mut p = deleted(f, side);
|
||||
let mut root = f.project.elements[&side.root].clone();
|
||||
root.id = new_id;
|
||||
p.elements.insert(new_id, root);
|
||||
p
|
||||
}
|
||||
|
||||
fn snippet(f: &Fixture, side: &Side) -> String {
|
||||
let e = &f.project.elements[&side.root];
|
||||
let keyword = if side.prefix == "oa" {
|
||||
"oa_activity"
|
||||
} else {
|
||||
"sa_function"
|
||||
};
|
||||
format!(
|
||||
" {keyword} {} {} id \"{}\" {{\n description {};\n }}\n",
|
||||
e.alias,
|
||||
serde_json::to_string(&e.name).unwrap(),
|
||||
id(&format!("{}_recreated_response", side.prefix)),
|
||||
serde_json::to_string(&e.description).unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
fn generate(out: &Path) -> Result<()> {
|
||||
let f = fixture();
|
||||
let mut files: Vec<(String, Vec<u8>)> = vec![
|
||||
(
|
||||
"original.arcadia".into(),
|
||||
round_trip(&f.project)?.into_bytes(),
|
||||
),
|
||||
(
|
||||
"original.json".into(),
|
||||
serde_json::to_vec_pretty(&f.project)?,
|
||||
),
|
||||
(
|
||||
"manifest.json".into(),
|
||||
serde_json::to_vec_pretty(&serde_json::json!({
|
||||
"fixture": "transition-native-lifecycle-20260913", "oa": &f.oa, "sa": &f.sa,
|
||||
"trace": id("response_refinement"), "trace_artifact": id("response_trace_evidence"),
|
||||
"recreated_oa_id": id("oa_recreated_response"), "recreated_sa_id": id("sa_recreated_response")
|
||||
}))?,
|
||||
),
|
||||
];
|
||||
for side in [&f.oa, &f.sa] {
|
||||
let removal = deleted(&f, side);
|
||||
let removal_source = round_trip(&removal)?;
|
||||
let replacement = recreated(&f, side, id(&format!("{}_recreated_response", side.prefix)));
|
||||
let replacement_source = round_trip(&replacement)?;
|
||||
let fragment = snippet(&f, side);
|
||||
let (body, tail) = removal_source
|
||||
.rsplit_once('}')
|
||||
.ok_or("Missing project closing brace")?;
|
||||
compare(&replacement, &compile(format!("{body}{fragment}}}{tail}"))?)?;
|
||||
files.extend([
|
||||
(
|
||||
format!("{}-deleted.json", side.prefix),
|
||||
serde_json::to_vec_pretty(&removal)?,
|
||||
),
|
||||
(
|
||||
format!("{}-deleted.arcadia", side.prefix),
|
||||
removal_source.into_bytes(),
|
||||
),
|
||||
(
|
||||
format!("{}-recreated.json", side.prefix),
|
||||
serde_json::to_vec_pretty(&replacement)?,
|
||||
),
|
||||
(
|
||||
format!("{}-recreated.arcadia", side.prefix),
|
||||
replacement_source.into_bytes(),
|
||||
),
|
||||
(
|
||||
format!("{}-recreate-root.snippet", side.prefix),
|
||||
fragment.into_bytes(),
|
||||
),
|
||||
]);
|
||||
}
|
||||
if files.iter().any(|(name, _)| out.join(name).exists()) {
|
||||
return Err(
|
||||
"Generation refuses to overwrite fixture files; use a fresh output directory".into(),
|
||||
);
|
||||
}
|
||||
fs::create_dir_all(out)?;
|
||||
for (name, bytes) in files {
|
||||
fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(out.join(name))?
|
||||
.write_all(&bytes)?;
|
||||
}
|
||||
println!(
|
||||
"Generated and round-trip verified both directions in {}",
|
||||
out.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct PlacementExpectation {
|
||||
diagram: String,
|
||||
alias: String,
|
||||
position: Position,
|
||||
size: Size,
|
||||
collapsed: bool,
|
||||
}
|
||||
|
||||
fn first_difference(
|
||||
expected: &serde_json::Value,
|
||||
actual: &serde_json::Value,
|
||||
path: &str,
|
||||
) -> Option<String> {
|
||||
use serde_json::Value;
|
||||
if expected == actual {
|
||||
return None;
|
||||
}
|
||||
match (expected, actual) {
|
||||
(Value::Object(a), Value::Object(b)) => {
|
||||
for key in a.keys().chain(b.keys()) {
|
||||
match (a.get(key), b.get(key)) {
|
||||
(Some(x), Some(y)) => {
|
||||
if let Some(diff) = first_difference(x, y, &format!("{path}.{key}")) {
|
||||
return Some(diff);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Some(format!(
|
||||
"{path}.{key}: expected presence {}, actual presence {}",
|
||||
a.contains_key(key),
|
||||
b.contains_key(key)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(Value::Array(a), Value::Array(b)) if a.len() == b.len() => {
|
||||
for (index, (x, y)) in a.iter().zip(b).enumerate() {
|
||||
if let Some(diff) = first_difference(x, y, &format!("{path}[{index}]")) {
|
||||
return Some(diff);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => return Some(format!("{path}: expected {expected}, actual {actual}")),
|
||||
}
|
||||
Some(format!("{path}: values differ"))
|
||||
}
|
||||
|
||||
fn compare(expected: &Project, actual: &Project) -> Result<()> {
|
||||
if expected != actual {
|
||||
return Err(first_difference(
|
||||
&serde_json::to_value(expected)?,
|
||||
&serde_json::to_value(actual)?,
|
||||
"Project",
|
||||
)
|
||||
.unwrap_or_else(|| "Project differs".into())
|
||||
.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check(
|
||||
state: &str,
|
||||
entry: &Path,
|
||||
placement_path: Option<&Path>,
|
||||
identity_path: Option<&Path>,
|
||||
receipt_path: Option<&Path>,
|
||||
) -> Result<()> {
|
||||
let f = fixture();
|
||||
let identity_expectations: Option<BTreeMap<String, Id>> = identity_path
|
||||
.map(|path| -> Result<_> { Ok(serde_json::from_slice(&fs::read(path)?)?) })
|
||||
.transpose()?;
|
||||
let disk = source_files::load_project(entry, ProjectLimits::default())?;
|
||||
if disk
|
||||
.diagnostics
|
||||
.iter()
|
||||
.any(|located| located.diagnostic.severity != Severity::Warning)
|
||||
{
|
||||
return Err(format!("Source loading errors: {:?}", disk.diagnostics).into());
|
||||
}
|
||||
let disk_bundle = disk.reachable_bundle();
|
||||
let compiled = compile_bundle(&disk_bundle, ProjectLimits::default())
|
||||
.map_err(|e| format!("Saved source compile: {e:?}"))?;
|
||||
if compiled.bundle != disk_bundle {
|
||||
return Err("Saved source is not identity-stable: compilation inserted UUIDs or changed source; save explicit identities before checking".into());
|
||||
}
|
||||
let actual = &compiled.decoded.project;
|
||||
let validated_warnings = accepted(actual)?;
|
||||
let mut expected_diagnostics = validated_warnings
|
||||
.iter()
|
||||
.map(|issue| {
|
||||
(
|
||||
issue.severity == Severity::Warning,
|
||||
issue.code.as_str(),
|
||||
issue.element,
|
||||
issue.field.as_deref(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
expected_diagnostics.sort();
|
||||
// Loading/compilation may return the fixture's intentional engineering
|
||||
// warnings. Admit exactly the independently validated warning multiset,
|
||||
// retaining owner, field and severity checks; no extra discovery warning or
|
||||
// error is silently discarded. Source location spelling is not semantic.
|
||||
for (stage, diagnostics) in [
|
||||
("loading", &disk.diagnostics),
|
||||
("compilation", &compiled.diagnostics),
|
||||
] {
|
||||
let mut actual_diagnostics = diagnostics
|
||||
.iter()
|
||||
.map(|located| {
|
||||
let diagnostic = &located.diagnostic;
|
||||
(
|
||||
diagnostic.severity == Severity::Warning,
|
||||
diagnostic.code.as_str(),
|
||||
diagnostic.object_id,
|
||||
diagnostic.field.as_deref(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
actual_diagnostics.sort();
|
||||
if actual_diagnostics != expected_diagnostics {
|
||||
return Err(format!("Unexpected source {stage} diagnostics: expected {expected_diagnostics:?}; actual {actual_diagnostics:?}").into());
|
||||
}
|
||||
}
|
||||
let mut generated_ids = BTreeMap::<String, Id>::new();
|
||||
let expected = if state == "original" {
|
||||
if placement_path.is_some() || identity_path.is_some() || receipt_path.is_some() {
|
||||
return Err("original does not accept placement/identity options".into());
|
||||
}
|
||||
f.project.clone()
|
||||
} else {
|
||||
let (prefix, phase) = state
|
||||
.split_once('-')
|
||||
.ok_or("State must be original or oa-/sa-deleted/recreated/relinked/reopened/noop")?;
|
||||
let side = match prefix {
|
||||
"oa" => &f.oa,
|
||||
"sa" => &f.sa,
|
||||
_ => return Err("Unknown source layer".into()),
|
||||
};
|
||||
if phase == "deleted" {
|
||||
if placement_path.is_some() || identity_path.is_some() || receipt_path.is_some() {
|
||||
return Err("deleted does not accept placement/identity options".into());
|
||||
}
|
||||
deleted(&f, side)
|
||||
} else if matches!(phase, "recreated" | "relinked" | "reopened" | "noop") {
|
||||
if matches!(phase, "reopened" | "noop")
|
||||
&& identity_expectations
|
||||
.as_ref()
|
||||
.and_then(|ids| ids.get("trace"))
|
||||
.is_none()
|
||||
{
|
||||
return Err("Reopened/noop checks require --identities with the first accepted relink trace UUID".into());
|
||||
}
|
||||
let original_root = &f.project.elements[&side.root];
|
||||
let new_source = match &identity_expectations {
|
||||
Some(ids) => *ids
|
||||
.get("source")
|
||||
.ok_or("Identity expectations must explicitly include source")?,
|
||||
None => id(&format!("{prefix}_recreated_response")),
|
||||
};
|
||||
let roots = actual
|
||||
.elements
|
||||
.values()
|
||||
.filter(|e| e.alias == original_root.alias)
|
||||
.collect::<Vec<_>>();
|
||||
if roots.len() != 1 || roots[0].id != new_source || f.project.contains(new_source) {
|
||||
return Err(
|
||||
"Expected exactly one recreated root with the original alias and the pinned new UUID (snippet UUID unless --identities supplied)"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
generated_ids.insert("source".into(), new_source);
|
||||
let mut candidate = recreated(&f, side, new_source);
|
||||
if let Some(path) = placement_path {
|
||||
let specs: Vec<PlacementExpectation> = serde_json::from_slice(&fs::read(path)?)?;
|
||||
for spec in specs {
|
||||
if ![format!("{prefix}_work"), format!("{prefix}_overview")]
|
||||
.contains(&spec.diagram)
|
||||
{
|
||||
return Err("A new source placement must use its existing source-layer work/overview diagram".into());
|
||||
}
|
||||
let diagram = id(&spec.diagram);
|
||||
let nodes = actual
|
||||
.diagrams
|
||||
.get(&diagram)
|
||||
.ok_or("Expected diagram is missing")?
|
||||
.nodes
|
||||
.values()
|
||||
.filter(|node| node.alias == spec.alias)
|
||||
.collect::<Vec<_>>();
|
||||
if nodes.len() != 1 || f.project.contains(nodes[0].id) {
|
||||
return Err(
|
||||
"Expected one newly identified declared source placement".into()
|
||||
);
|
||||
}
|
||||
let receipt_key = format!("placement:{}", spec.alias);
|
||||
let key = match &identity_expectations {
|
||||
Some(ids) => *ids.get(&receipt_key).ok_or(
|
||||
"Identity receipt lacks an explicitly declared placement UUID",
|
||||
)?,
|
||||
None => nodes[0].id,
|
||||
};
|
||||
if nodes[0].id != key {
|
||||
return Err(
|
||||
"Source placement UUID changed from its identity expectation".into(),
|
||||
);
|
||||
}
|
||||
if candidate
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.insert(
|
||||
key,
|
||||
NodePlacement {
|
||||
id: key,
|
||||
alias: spec.alias.clone(),
|
||||
element: new_source,
|
||||
position: spec.position,
|
||||
size: spec.size,
|
||||
collapsed: spec.collapsed,
|
||||
port_anchors: vec![],
|
||||
},
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
return Err("Duplicate placement expectation".into());
|
||||
}
|
||||
generated_ids.insert(receipt_key, key);
|
||||
}
|
||||
}
|
||||
if phase != "recreated" {
|
||||
if actual.traces.len() != 1 {
|
||||
return Err("Relinking must create exactly one trace".into());
|
||||
}
|
||||
let actual_key = actual.traces.values().next().unwrap().id;
|
||||
let key = identity_expectations
|
||||
.as_ref()
|
||||
.and_then(|ids| ids.get("trace"))
|
||||
.copied()
|
||||
.unwrap_or(actual_key);
|
||||
if actual_key != key {
|
||||
return Err(
|
||||
"Refinement trace UUID changed from its accepted identity receipt".into(),
|
||||
);
|
||||
}
|
||||
if f.project.contains(key) || candidate.contains(key) {
|
||||
return Err("Expected a fresh trace UUID".into());
|
||||
}
|
||||
let (oa, sa) = if prefix == "oa" {
|
||||
(new_source, f.sa.root)
|
||||
} else {
|
||||
(f.oa.root, new_source)
|
||||
};
|
||||
candidate.traces.insert(
|
||||
key,
|
||||
Trace {
|
||||
id: key,
|
||||
alias: "trace_oa_response_sa_response".into(),
|
||||
name: format!(
|
||||
"{} ↔ {}",
|
||||
candidate.elements[&oa].name, candidate.elements[&sa].name
|
||||
),
|
||||
description: String::new(),
|
||||
oa,
|
||||
sa,
|
||||
kind: TraceKind::Refinement,
|
||||
baseline: Some(TraceBaseline {
|
||||
oa: candidate.properties(oa).unwrap(),
|
||||
sa: candidate.properties(sa).unwrap(),
|
||||
semantic: None,
|
||||
}),
|
||||
},
|
||||
);
|
||||
generated_ids.insert("trace".into(), key);
|
||||
}
|
||||
candidate
|
||||
} else {
|
||||
return Err("Unknown lifecycle phase".into());
|
||||
}
|
||||
};
|
||||
// No actual body, prose, ownership, geometry, alias or baseline is copied
|
||||
// into the expected model: only explicitly permitted fresh identities.
|
||||
compare(&expected, actual)?;
|
||||
round_trip(&expected)?;
|
||||
if let Some(ids) = &identity_expectations {
|
||||
for (label, expected_id) in ids {
|
||||
if generated_ids.get(label) != Some(expected_id) {
|
||||
return Err(format!("Unused or mismatching identity expectation: {label}").into());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(path) = receipt_path {
|
||||
// Publish a receipt only after complete-model validation succeeds. Never
|
||||
// replace earlier accepted identity evidence with a new observation.
|
||||
fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(path)?
|
||||
.write_all(&serde_json::to_vec_pretty(&generated_ids)?)?;
|
||||
}
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&serde_json::json!({
|
||||
"status": "PASS", "state": state, "entry": entry,
|
||||
"comparison": "complete Project equality", "new_ids": generated_ids,
|
||||
"trace_identity_pinned": identity_expectations.as_ref().is_some_and(|ids| ids.contains_key("trace")),
|
||||
"identity_receipt": receipt_path,
|
||||
"recreated_id_matches_snippet": generated_ids.get("source").map(|key|
|
||||
*key == id(if state.starts_with("oa-") { "oa_recreated_response" } else { "sa_recreated_response" }))
|
||||
}))?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run() -> Result<()> {
|
||||
let args = std::env::args().skip(1).collect::<Vec<_>>();
|
||||
match args.as_slice() {
|
||||
[command, out] if command == "generate" => generate(Path::new(out)),
|
||||
[command, state, entry, options @ ..] if command == "check" => {
|
||||
if options.len() % 2 != 0 { return Err("Every check option requires a path".into()); }
|
||||
let (mut placements, mut identities, mut receipt) = (None, None, None);
|
||||
for pair in options.chunks_exact(2) {
|
||||
let slot = match pair[0].as_str() {
|
||||
"--placements" => &mut placements,
|
||||
"--identities" => &mut identities,
|
||||
"--write-identities" => &mut receipt,
|
||||
_ => return Err(format!("Unknown check option: {}", pair[0]).into()),
|
||||
};
|
||||
if slot.replace(Path::new(&pair[1])).is_some() { return Err("Duplicate check option".into()); }
|
||||
}
|
||||
check(state, Path::new(entry), placements, identities, receipt)
|
||||
},
|
||||
_ => Err("Usage: transition-native-lifecycle generate OUTDIR | check STATE SAVED_ENTRY [--placements PATH] [--identities PATH] [--write-identities PATH]".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(error) = run() {
|
||||
eprintln!("FAIL: {error}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
# Independent bounded transition review
|
||||
|
||||
Reviewer: `transition_picker_review`, 2026-09-22. This review inspected code,
|
||||
original native screenshots and closed-run/snapshot receipts. The reviewer did
|
||||
not operate the application, build it, rerun the checker, or alter the specimens.
|
||||
Only this review note was written. The parent owns execution and publication.
|
||||
|
||||
The reviewed production baseline is `d0b7c285018a516c6dc34c196d1359a861ddaeb3`
|
||||
plus the captured, initially uncommitted footer changes in
|
||||
`crates/oa-desktop/src/workflow.rs`. HEAD alone does not identify those changes.
|
||||
Lifecycle trials 2 and 3 identify GUI SHA-256
|
||||
`3813e79d84cf4a83f2290639b93c3daaa8de427b0268b288f87e4f535fa1aea2`;
|
||||
trials 4 and 5 identify final-footer GUI SHA-256
|
||||
`f0bb021e350bcc55435d29e3daf6a4b3dd7389620df99f5d169f9cb669fd53d0`.
|
||||
Exact build/source inventories belong to the accompanying evidence packages.
|
||||
|
||||
## Code and evaluator findings
|
||||
|
||||
The picker shares the core layer/category/kind predicate, preserves selected
|
||||
identity independently of search results, and keeps query state outside the
|
||||
request and recovery schema. Query events return before replanning. Compatibility
|
||||
does not imply that dependencies or the complete transition plan are valid.
|
||||
Distinct traced IDs, rather than trace-record count, determine inferred reuse;
|
||||
ambiguous traces remain an explicit decision. Manual tracing remains separate.
|
||||
|
||||
The desktop tests compare the whole request, plan and recovery snapshot during
|
||||
search, including generated preview identities. The initial second-query test
|
||||
used a target ID rather than a second rendered source card; native corrected
|
||||
picker frame 17 separately demonstrates two visible source cards with independent
|
||||
queries. These checks do not prove keyboard access or every focus transition.
|
||||
|
||||
The core lifecycle tests and [native producer/checker](producer/src/main.rs)
|
||||
construct expected deletions from fixture-owned identities, not a deletion
|
||||
impact helper or observed deletion result. Expected recreation adds only the
|
||||
replacement root. Expected relinking adds a directly constructed refinement
|
||||
trace and reviewed baseline while preserving target prose, bodies, ownership,
|
||||
geometry, routes and artifacts. Whole-Project equality detects incidental changes.
|
||||
First-relink admission may observe the fresh trace UUID; reopen/no-op admission
|
||||
requires the previously written identity receipt and rejects replacement IDs.
|
||||
Source loading rejects compiler-inserted identities. Round trips compare the
|
||||
decoded model; they do not promise original comments or formatting preservation.
|
||||
No blocking conservation-oracle defect was found in the inspected paths.
|
||||
|
||||
## Native observations
|
||||
|
||||
Trial/frame labels below refer to original `*-0.jpg` files and matching JSON
|
||||
under `.local/transition-native/` at capture time. Receipt evidence is distinct
|
||||
from what screenshots visibly establish. Capture helpers and checksums provide
|
||||
provenance context, not authenticated historical action logs.
|
||||
|
||||
| Originals inspected | Visible observation |
|
||||
| --- | --- |
|
||||
| `picker-baseline-1` 10–11 | Activity menu includes incompatible ports; selecting one yields a kind conflict. |
|
||||
| `picker-corrected-2` 06–07, 10–12, 15, 17–19, 22–23 | Six compatible Functions are sorted; alias filtering preserves current selection until explicit selection; zero results preserve the selected target; two cards retain independent queries; field review remains required; clearing search retains Keep target; Cancel returns to the saved-status view. |
|
||||
| `lifecycle-2` 05, 11–12 | OA deletion is visible; a replacement root declaration is entered and applied through Textual model. |
|
||||
| `lifecycle-2` 17, 21, 27–30 | Recreation initially offers Create rather than silently relinking. Selecting the retained SA target requires Name and Description decisions. Sticky guidance advances from Name/two conflicts to Description/one, then clears after both Keep target decisions. Seven warnings do not block Apply. |
|
||||
| `lifecycle-2` 31–32 | Apply displays the retained SA root, children and exchange; subsequent frame reports all changes saved. |
|
||||
| `lifecycle-3` 02, 06–08 | A fresh process opens the saved model; unchanged transition infers reuse without field conflicts; Apply and save return to the retained SA design. |
|
||||
| `lifecycle-4` 12, 14, 16, 22–23 | SA deletion, Undo restoration, Redo deletion and replacement-root entry/application are visible. |
|
||||
| `lifecycle-4` 28, 30–38 | Reverse transition defaults to Create; alias search offers the retained OA Activity; explicit selection requires both prose decisions. Persistent guidance follows two/one/zero conflicts. Apply displays the retained OA decomposition and save reports completion. The final `reviewed items` wording is visible. |
|
||||
| `lifecycle-5` 02, 07–09 | A fresh process opens the retained OA design; unchanged SA-to-OA review infers the accepted counterpart without conflicts; Apply and save retain the visible OA design. |
|
||||
|
||||
Closed GUI receipts for lifecycle trials 2–5 record exit 0, empty stdout/stderr,
|
||||
and unchanged executable hashes. Snapshot receipts record exit 0 for OA deletion,
|
||||
recreation and relink; OA reopen/no-op; and SA deletion, original-state Undo,
|
||||
deletion-state Redo, recreation, relink and subsequent reopen/no-op. Checks operate on saved source copies
|
||||
and report the working source unchanged. SA Redo's source hash equals deletion's.
|
||||
|
||||
OA relink, reopen and no-op snapshots have identical hashes for both
|
||||
`original.arcadia` and `traceability.arcadia`. Reopen/no-op use source
|
||||
`8eb83649-2810-58e1-94cd-71e25422658e` and accepted trace
|
||||
`856eb758-33fc-4ebf-9a87-22ad40a89952`. SA first relink establishes source
|
||||
`074eab66-7092-5e7e-8b83-b31b95afdbcb` and trace
|
||||
`acc7969b-08c3-43f4-a31d-9140ca873ffe`. Trial 5 passes that exact first-relink
|
||||
receipt to both saved-source checks. Its reopen and no-op source hashes match the
|
||||
first relink for both files (`original.arcadia`: `e75b4cb55e5b205bd5784a1f35573931f1e64491b646f4ffb241f90288447b5d`;
|
||||
`traceability.arcadia`: `5046e2d2731c5d8fcbcb7fe2deede0677ec61de03b85890852fa20f778c57552`).
|
||||
This supports conservation and persisted identity across these finite reopen and
|
||||
unchanged-review workflows in both directions. No new blocking finding arose
|
||||
from the reviewed lifecycle evidence.
|
||||
|
||||
## Feedback correction and remaining limits
|
||||
|
||||
Corrected picker frames 12/15 initially left Apply disabled while its explanation
|
||||
was below the fold. The reviewed footer change moves counts outside the scrolling
|
||||
body and derives guidance from the same conflicts and destination validity used
|
||||
by Apply. It adds no persisted state and weakens no validation. Generic guidance
|
||||
was corrected from “adjust mapping or scope” because operation-omission rationale
|
||||
and component-port decisions require other controls. Final native trial 4 shows
|
||||
the Name/Description guidance and `reviewed items` count. Destination, boundary
|
||||
and general-conflict guidance were checked against code, not exercised in these
|
||||
native lifecycle frames.
|
||||
|
||||
All inspected originals are 1442×932. Resize attempts did not establish a smaller
|
||||
viewport, regardless of frame filenames; minimum-window behavior is unverified.
|
||||
The inherited picker is pointer-dependent. This review does not establish full
|
||||
keyboard/screen-reader accessibility, packaged launch, other platforms, arbitrary
|
||||
models, implicit creation applied natively, or relink Undo/Redo. Recreated roots
|
||||
were authored through Textual model and intentionally have no new placement;
|
||||
the empty source canvas is consistent with that fixture. Screenshots alone do
|
||||
not establish complete model conservation: the independent saved-source comparisons
|
||||
provide that bounded evidence.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Transition picker: native comparison evidence
|
||||
|
||||
Run `python verify.py` using Python 3.10 or newer. This five-file package supports
|
||||
read-only verification using the standard library. It does not execute archived
|
||||
helpers, binaries, Git commands or network calls. Outer text is UTF-8 LF before
|
||||
hashing; archived files retain exact original bytes.
|
||||
|
||||
All three trials, launch helpers, copied release1 inputs and original frames are
|
||||
preserved. Baseline picker-baseline-1 used the f714a25 GUI and closed with exit 0.
|
||||
Corrected picker-corrected-1 exited immediately with 3221225781 and empty logs;
|
||||
the operator attributes this to omitted runtime environment setup. That cause
|
||||
is not independently proven by empty logs. Corrected picker-corrected-2 used
|
||||
the d46854c GUI, ran from 15:22:22 to 15:27:13 UTC, and closed with exit 0 and
|
||||
empty logs. Its frame 02 is a loading state, not proof that loading finished.
|
||||
|
||||
The capture-snippet.js source was retained from a previous persistent session.
|
||||
It was not pinned in these trials' launch receipts. Archiving it now does not
|
||||
establish the exact capture function executed during the trials. Frame JSON
|
||||
records timestamps, a window/application path and image references; it is not
|
||||
an action invocation log or independent PID attestation. No frame is fabricated,
|
||||
renumbered, cropped or omitted, including auxiliary dialog images.
|
||||
|
||||
Both exact GUI build result receipts are included. Full build source evidence
|
||||
remains external: the sibling transition-lifecycle-20260913 Cargo package at
|
||||
commit fe43730bec196cc6ebbc06dcf827169f04c120f0 covers the corrected d46854c
|
||||
build, and the ordinary-text-native-20260913 package at
|
||||
commit 3f9e6a18af96b5cd10940da4093be8da28b9c835 includes the baseline f714a25
|
||||
build/source evidence. The manifest pins those related evidence ZIPs. This is
|
||||
not a self-contained source or runtime distribution; the verifier does not
|
||||
require those external packages for its local receipt/frame checks.
|
||||
|
||||
Verification checks exact safe ZIP membership/hashes, launch helper pins,
|
||||
start/process/result timing, expected exit outcomes, empty streams, GUI binary
|
||||
and build receipt relationships, identical copied input membership/bytes across
|
||||
all trials, and complete frame/image references within process intervals. Binary
|
||||
hashes are receipt observations; binaries are excluded. Screenshots require
|
||||
separate interpretation and do not prove action execution, complete model
|
||||
equality, persistence or full usability. The included independent-review.md is
|
||||
a parent-consolidated record of separate human-readable review observations.
|
||||
No credentials or whole user profile are
|
||||
packaged; raw receipts retain original runtime path strings.
|
||||
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"evidence.zip": {
|
||||
"bytes": 3412321,
|
||||
"sha256": "c801eda9e33cd796a3594e73c34d6d29691597552a68307d2dc5a5e113c1e470"
|
||||
},
|
||||
"manifest.json": {
|
||||
"bytes": 26082,
|
||||
"sha256": "f80092a483a00e17c3ffcdbf2dd71480ed6ead4d003248de2f9ef708104cfba1"
|
||||
},
|
||||
"README.md": {
|
||||
"bytes": 2673,
|
||||
"sha256": "86251ca15d4950cd0b037ba05b63433877608c1d0d6424fedef8ff4e34340141"
|
||||
},
|
||||
"verify.py": {
|
||||
"bytes": 5143,
|
||||
"sha256": "23c5040f91f29e243e65ad31b4f8f82f7ad1d692efd862b1bc87450c31e5ed5f"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,614 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"runs": {
|
||||
"picker-baseline-1": {
|
||||
"helper": "native/run-picker-baseline.py",
|
||||
"build": "build/baseline-result.json",
|
||||
"exitCode": 0,
|
||||
"captures": [
|
||||
"native/picker-baseline-1/01-start.json",
|
||||
"native/picker-baseline-1/02-open-pending.json",
|
||||
"native/picker-baseline-1/03-open-dialog.json",
|
||||
"native/picker-baseline-1/04-teaching-path.json",
|
||||
"native/picker-baseline-1/05-teaching-opened.json",
|
||||
"native/picker-baseline-1/06-teaching-ready.json",
|
||||
"native/picker-baseline-1/07-source-selected.json",
|
||||
"native/picker-baseline-1/08-source-actions.json",
|
||||
"native/picker-baseline-1/09-transition-dialog.json",
|
||||
"native/picker-baseline-1/10-unfiltered-counterparts.json",
|
||||
"native/picker-baseline-1/11-incompatible-selection.json",
|
||||
"native/picker-baseline-1/12-cancel-preserved.json"
|
||||
]
|
||||
},
|
||||
"picker-corrected-1": {
|
||||
"helper": "native/run-picker-corrected.py",
|
||||
"build": "build/corrected-result.json",
|
||||
"exitCode": 3221225781,
|
||||
"captures": []
|
||||
},
|
||||
"picker-corrected-2": {
|
||||
"helper": "native/run-picker-corrected-2.py",
|
||||
"build": "build/corrected-result.json",
|
||||
"exitCode": 0,
|
||||
"captures": [
|
||||
"native/picker-corrected-2/01-opened.json",
|
||||
"native/picker-corrected-2/02-teaching-reopened.json",
|
||||
"native/picker-corrected-2/03-loaded.json",
|
||||
"native/picker-corrected-2/04-source-selected.json",
|
||||
"native/picker-corrected-2/05-actions.json",
|
||||
"native/picker-corrected-2/06-transition-dialog.json",
|
||||
"native/picker-corrected-2/07-compatible-menu.json",
|
||||
"native/picker-corrected-2/08-menu-dismissed.json",
|
||||
"native/picker-corrected-2/09-search-focus.json",
|
||||
"native/picker-corrected-2/10-alias-query.json",
|
||||
"native/picker-corrected-2/11-filtered-menu.json",
|
||||
"native/picker-corrected-2/12-existing-selection.json",
|
||||
"native/picker-corrected-2/13-refocus-search.json",
|
||||
"native/picker-corrected-2/14-query-selected.json",
|
||||
"native/picker-corrected-2/15-no-results-selection-retained.json",
|
||||
"native/picker-corrected-2/16-parent-search-focus.json",
|
||||
"native/picker-corrected-2/17-independent-card-queries.json",
|
||||
"native/picker-corrected-2/18-field-review.json",
|
||||
"native/picker-corrected-2/19-keep-target-reviewed.json",
|
||||
"native/picker-corrected-2/20-reviewed-search-focus.json",
|
||||
"native/picker-corrected-2/21-reviewed-query-selected.json",
|
||||
"native/picker-corrected-2/22-cleared-search-review-retained.json",
|
||||
"native/picker-corrected-2/23-cancel-restores-saved.json"
|
||||
]
|
||||
}
|
||||
},
|
||||
"members": {
|
||||
"build/baseline-result.json": {
|
||||
"bytes": 5532,
|
||||
"sha256": "5e9e227c66a4b71ade734b8ebeb12070b8a5c64a9a6cf9d8d0b6d70379d4ffc2"
|
||||
},
|
||||
"build/corrected-result.json": {
|
||||
"bytes": 5583,
|
||||
"sha256": "91b38045319adc1fb9aa0e51d45d79a4be99d2de9aec08a47f73915e3ac9dd79"
|
||||
},
|
||||
"native/capture-snippet.js": {
|
||||
"bytes": 1579,
|
||||
"sha256": "55279ed080289ccf5ba482cb24b5a807669a9360012e2d87f794d4465227cb2c"
|
||||
},
|
||||
"native/independent-review.md": {
|
||||
"bytes": 1898,
|
||||
"sha256": "c59d2ff78635f94f3f225c27999b69d24c11977c89026fc8a7331ddbd160c8c6"
|
||||
},
|
||||
"native/input/release1/README.md": {
|
||||
"bytes": 11137,
|
||||
"sha256": "113cfb1cf38a98b51bff60442d86a35d636a3d80ef206bece7d53d0aae67f2d5"
|
||||
},
|
||||
"native/input/release1/evidence/repair-check.md": {
|
||||
"bytes": 1419,
|
||||
"sha256": "f4db17bdca4ecbb1bf0caef01b2da5f1f7f798ab3152b648ce6dd8696a6a68e5"
|
||||
},
|
||||
"native/input/release1/expected-diagnostics.json": {
|
||||
"bytes": 288,
|
||||
"sha256": "3700393d76b940cfc076bf52861bd7b1f43370f8eabce3594531bd0bf1fc851e"
|
||||
},
|
||||
"native/input/release1/model.json": {
|
||||
"bytes": 134520,
|
||||
"sha256": "ef0ef4006c3e09d5db6e1b621256b3f7d6642f3a7b09cc34607cfbcca830c727"
|
||||
},
|
||||
"native/input/release1/model.rs": {
|
||||
"bytes": 28762,
|
||||
"sha256": "15f30f6bde655808fe754665c26d560e36f2a9bad340f346b09137d0d05045f5"
|
||||
},
|
||||
"native/input/release1/oa/activities/activity_oa_restore_work.arcadia": {
|
||||
"bytes": 1130,
|
||||
"sha256": "21cbd7176af8fad68453306f2371ff03e127a994b8796cdf044567332de9686f"
|
||||
},
|
||||
"native/input/release1/oa/behavior/process_oa_assessment_procedure.arcadia": {
|
||||
"bytes": 550,
|
||||
"sha256": "23cabc213c264a16c8595414db6acb09248c1cef9cd699f1b0316e5bb82c062a"
|
||||
},
|
||||
"native/input/release1/oa/behavior/process_oa_field_procedure.arcadia": {
|
||||
"bytes": 1178,
|
||||
"sha256": "9c52086759b7344b4809ceb8d372f65647b16fc24153a03b887faa378c37fe96"
|
||||
},
|
||||
"native/input/release1/oa/behavior/process_oa_response.arcadia": {
|
||||
"bytes": 2560,
|
||||
"sha256": "675798bd19e6b40d03e7076b0e59b110cdaaab7b6d16c47c3ac80fa960072e84"
|
||||
},
|
||||
"native/input/release1/oa/behavior/scenario_oa_conversation.arcadia": {
|
||||
"bytes": 1515,
|
||||
"sha256": "e217e5394a0771dc16206ed9437a231da27905d2f19be64b3f48281c0a32fb6b"
|
||||
},
|
||||
"native/input/release1/oa/behavior/scenario_oa_full_scenario.arcadia": {
|
||||
"bytes": 1121,
|
||||
"sha256": "c5a285f2d7c61bd2a136107f267fbf9cddb1cba3f63ff36a6f53f7ab98f33df5"
|
||||
},
|
||||
"native/input/release1/oa/behavior/state_machine_oa_monitor.arcadia": {
|
||||
"bytes": 1358,
|
||||
"sha256": "e7349e1e5336282eed3f5a103129db33d3ae4ed500788ee7129e8fc193ac57f6"
|
||||
},
|
||||
"native/input/release1/oa/capabilities.arcadia": {
|
||||
"bytes": 630,
|
||||
"sha256": "26a3d9b615a3251ff010e0b47127f2d16bc2dcd45ed95967208b0076d725843a"
|
||||
},
|
||||
"native/input/release1/oa/model.arcadia": {
|
||||
"bytes": 531,
|
||||
"sha256": "069a5eaaf525089935cc9d73ae7b916866ed78c6534705d75739650ecdea7ada"
|
||||
},
|
||||
"native/input/release1/oa/participants.arcadia": {
|
||||
"bytes": 1580,
|
||||
"sha256": "591a75a9ecad280302be4d4ae0171ad616377ed11ead5e233848ca2dc0a71819"
|
||||
},
|
||||
"native/input/release1/oa/types.arcadia": {
|
||||
"bytes": 554,
|
||||
"sha256": "f02c391d584674c879f662a418abba3a220f1fee88471ca49322ac9873de628d"
|
||||
},
|
||||
"native/input/release1/project.arcadia": {
|
||||
"bytes": 1958,
|
||||
"sha256": "b2674e6bf77cfe0f6f0ba79c81d2e96d92f39c02ca2c6266d80e91723bfb9fda"
|
||||
},
|
||||
"native/input/release1/sa/behavior/process_sa_assessment_procedure.arcadia": {
|
||||
"bytes": 559,
|
||||
"sha256": "5c414fbdaabf9f9e402b0441fcacf1cacf4dbbc6c822162f028de35abf59be2d"
|
||||
},
|
||||
"native/input/release1/sa/behavior/process_sa_field_procedure.arcadia": {
|
||||
"bytes": 1187,
|
||||
"sha256": "ece846fdf93857e71a9ab0efcbdf64a5c41c71180f62afa3f79077fffb53b8f2"
|
||||
},
|
||||
"native/input/release1/sa/behavior/process_sa_response.arcadia": {
|
||||
"bytes": 2569,
|
||||
"sha256": "af7fdda280a917cf626f6e077162d481f9aaad280f8e81330c07d7fe384b1c52"
|
||||
},
|
||||
"native/input/release1/sa/behavior/scenario_sa_conversation.arcadia": {
|
||||
"bytes": 1847,
|
||||
"sha256": "01339699ec3ee409418d9bb232542f2bcd9419f1e67b183e77700c56bb9f71d2"
|
||||
},
|
||||
"native/input/release1/sa/behavior/scenario_sa_full_scenario.arcadia": {
|
||||
"bytes": 1116,
|
||||
"sha256": "5dce6b00a3a441162e8dee3e23d9ca5ab7bed4a24de669b1fba03664a3086dda"
|
||||
},
|
||||
"native/input/release1/sa/behavior/state_machine_sa_monitor.arcadia": {
|
||||
"bytes": 1358,
|
||||
"sha256": "17dbcfa5bbfd10fc8e23a325fc950ce853c219b4aa89c74f96278cbb3f34acc8"
|
||||
},
|
||||
"native/input/release1/sa/capabilities.arcadia": {
|
||||
"bytes": 630,
|
||||
"sha256": "81706a0ca9d7db2aa0350888b9c00d853885185db2e6b90557d3d51a11c3d645"
|
||||
},
|
||||
"native/input/release1/sa/functions/function_sa_audit_thresholds.arcadia": {
|
||||
"bytes": 280,
|
||||
"sha256": "27f798f78f36e72b38edab3725af2feb0eaa010fd52a60b5c6b6bef57ad3b686"
|
||||
},
|
||||
"native/input/release1/sa/functions/function_sa_restore_work.arcadia": {
|
||||
"bytes": 2251,
|
||||
"sha256": "357c6acb80f86b4e8d8f5c98f9f79d140f85fbe09f3406b41b7ac6eba464ec8c"
|
||||
},
|
||||
"native/input/release1/sa/model.arcadia": {
|
||||
"bytes": 531,
|
||||
"sha256": "805b90262e27090ae7da81a51bcefa49c97fb0b3d3220b4a72323b4c4351c395"
|
||||
},
|
||||
"native/input/release1/sa/participants.arcadia": {
|
||||
"bytes": 3604,
|
||||
"sha256": "ca144d6a8f45fe720b5d18c7b59c7671444baa8c37e5ce9117bcba34b3a6aa84"
|
||||
},
|
||||
"native/input/release1/sa/types.arcadia": {
|
||||
"bytes": 903,
|
||||
"sha256": "540ddbc2bcd0ccf45298e3ea5660637341aff63ff65fd87ac2f26469d1adc064"
|
||||
},
|
||||
"native/input/release1/traceability.arcadia": {
|
||||
"bytes": 28041,
|
||||
"sha256": "474bd28d955ff7af7722f6e9a23220d816879e3f46202e79a233544b9c593116"
|
||||
},
|
||||
"native/input/release1/transition-before.json": {
|
||||
"bytes": 35801,
|
||||
"sha256": "2253e6434f741687e22327c37d520ed15c043bc36025498650bc2366bb6a874d"
|
||||
},
|
||||
"native/input/release1/transition-request.json": {
|
||||
"bytes": 4276,
|
||||
"sha256": "43b9a22e5f95ad3bc0ebd2478ead0e14406854abc989ae94ded9367b013d812a"
|
||||
},
|
||||
"native/input/release1/views/diagram_oa_allocation_view.arcadia": {
|
||||
"bytes": 1786,
|
||||
"sha256": "c45d7467839219d894804c1ef2d22ff4d4a5a84a6a273c526bafd4c1facde624"
|
||||
},
|
||||
"native/input/release1/views/diagram_sa_allocation_view.arcadia": {
|
||||
"bytes": 1797,
|
||||
"sha256": "352a3ca745517592b5c9b1239b5770f62f0466a329a91fd03457ef2b53c06ed6"
|
||||
},
|
||||
"native/input/release1/views/diagram_sa_capability_view.arcadia": {
|
||||
"bytes": 1286,
|
||||
"sha256": "8f18fcccbcddcf3c1ea0ffe727b5b23b77fc148c5da4fde953abe681e4c20cd2"
|
||||
},
|
||||
"native/input/release1/views/diagram_sa_service_view.arcadia": {
|
||||
"bytes": 1960,
|
||||
"sha256": "4c72cb7593d9c4be8d2600b0656a7a3a6ce84c45bb2f3e58a4ee5ae3bf91913c"
|
||||
},
|
||||
"native/input/release1/views/diagram_sa_state_view.arcadia": {
|
||||
"bytes": 1576,
|
||||
"sha256": "dd1bea81ac7dc549a53f9b190be6f13052e00ede345bd180444cc31edb892230"
|
||||
},
|
||||
"native/picker-baseline-1/01-start-0.jpg": {
|
||||
"bytes": 116509,
|
||||
"sha256": "d5c669575ffa4f8c0fe2027d39ad0b655bf6557a6315a9f27ef4cc81e3e4a9d8"
|
||||
},
|
||||
"native/picker-baseline-1/01-start.json": {
|
||||
"bytes": 920,
|
||||
"sha256": "f8ac2638390d3dc2a4bf7d64a9c6b8988c59ef377b53d7067fc41c1d30b5f091"
|
||||
},
|
||||
"native/picker-baseline-1/02-open-pending-0.jpg": {
|
||||
"bytes": 115140,
|
||||
"sha256": "d06c8963b5b752f6248ecead582c317cadfcf699955f7ecd629d6052ef157f70"
|
||||
},
|
||||
"native/picker-baseline-1/02-open-pending.json": {
|
||||
"bytes": 927,
|
||||
"sha256": "f326b85978dfa21cc2fd2e4454ebfee6a6d856e439de5ab41a8cec7c2998fb23"
|
||||
},
|
||||
"native/picker-baseline-1/03-open-dialog-0.jpg": {
|
||||
"bytes": 674,
|
||||
"sha256": "5a2387a1a593a32ea169b56b9a3e59333936ee1da95164564e4d9be61bf5a24b"
|
||||
},
|
||||
"native/picker-baseline-1/03-open-dialog-1.jpg": {
|
||||
"bytes": 52573,
|
||||
"sha256": "23ab19c5286f26ead42aec7e3690fda96cad512518243593f37e26579eb08ef6"
|
||||
},
|
||||
"native/picker-baseline-1/03-open-dialog.json": {
|
||||
"bytes": 4915,
|
||||
"sha256": "b1d4e6c59104b3d4d36210e855750cc7fc445a8d05e12f3c63cc43196df9df95"
|
||||
},
|
||||
"native/picker-baseline-1/04-teaching-path-0.jpg": {
|
||||
"bytes": 674,
|
||||
"sha256": "5a2387a1a593a32ea169b56b9a3e59333936ee1da95164564e4d9be61bf5a24b"
|
||||
},
|
||||
"native/picker-baseline-1/04-teaching-path-1.jpg": {
|
||||
"bytes": 55565,
|
||||
"sha256": "569f70974c0347c3accecb412de468751fc2294ba5f577b572765335a1cbca7c"
|
||||
},
|
||||
"native/picker-baseline-1/04-teaching-path.json": {
|
||||
"bytes": 4919,
|
||||
"sha256": "e7ef7212f637bff8125fa86892cc0b610a0c29a64c3715baeab12a6b36992620"
|
||||
},
|
||||
"native/picker-baseline-1/05-teaching-opened-0.jpg": {
|
||||
"bytes": 115272,
|
||||
"sha256": "c74bba329bca67a25c83352467842dadb8d393b17785b87929a4b7f10a8a2ae2"
|
||||
},
|
||||
"native/picker-baseline-1/05-teaching-opened.json": {
|
||||
"bytes": 930,
|
||||
"sha256": "dacbdbc29a7a61de78eb987069ed8e31f5818cdf6269b34836927109badd87ec"
|
||||
},
|
||||
"native/picker-baseline-1/06-teaching-ready-0.jpg": {
|
||||
"bytes": 148650,
|
||||
"sha256": "89957f78f5744c35f600b09f55dfbe52fddc4aac1b76ea1e820d1063b15c2833"
|
||||
},
|
||||
"native/picker-baseline-1/06-teaching-ready.json": {
|
||||
"bytes": 1114,
|
||||
"sha256": "4a708684d4469e4b5612ffe10216ad81d492eaf28730f0e11a4f5c4dcd30c903"
|
||||
},
|
||||
"native/picker-baseline-1/07-source-selected-0.jpg": {
|
||||
"bytes": 146555,
|
||||
"sha256": "ca53af5035524280f3144309a12c931193115e025bec6fe5b6b1799de3c8c949"
|
||||
},
|
||||
"native/picker-baseline-1/07-source-selected.json": {
|
||||
"bytes": 1115,
|
||||
"sha256": "4a725e4db48f2f8db7bc41a37ce69d49b2cd01a9b046791fee57b1bb580183a0"
|
||||
},
|
||||
"native/picker-baseline-1/08-source-actions-0.jpg": {
|
||||
"bytes": 153686,
|
||||
"sha256": "367876958aa58ed08c913d33530ba8da663d247ee3b0b9503625e7d3f945480c"
|
||||
},
|
||||
"native/picker-baseline-1/08-source-actions.json": {
|
||||
"bytes": 1114,
|
||||
"sha256": "4d244ccced055bdff280bbf99269554173fe85c816d15f9dae03aca40ad64ece"
|
||||
},
|
||||
"native/picker-baseline-1/09-transition-dialog-0.jpg": {
|
||||
"bytes": 147596,
|
||||
"sha256": "f197b031669070f731e354bca993ffc77ff43966349f4699d69e361287f2a704"
|
||||
},
|
||||
"native/picker-baseline-1/09-transition-dialog.json": {
|
||||
"bytes": 1127,
|
||||
"sha256": "3de33b54e5b15a598a901103acfa48650170d94724c5f5708b90d8fac6bcf6dd"
|
||||
},
|
||||
"native/picker-baseline-1/10-unfiltered-counterparts-0.jpg": {
|
||||
"bytes": 138703,
|
||||
"sha256": "a3c8464125657ea37550eb46a269f9a0c0646001ba387a8188b987ba74c6a270"
|
||||
},
|
||||
"native/picker-baseline-1/10-unfiltered-counterparts.json": {
|
||||
"bytes": 1133,
|
||||
"sha256": "977c0bbc366937ce2242b871b12f350cb56af4661f09c214e4945296195f4c55"
|
||||
},
|
||||
"native/picker-baseline-1/11-incompatible-selection-0.jpg": {
|
||||
"bytes": 144348,
|
||||
"sha256": "53884020ceb9c0b416386ceaa1e06801614ac132760e58c7c3320c820c251d04"
|
||||
},
|
||||
"native/picker-baseline-1/11-incompatible-selection.json": {
|
||||
"bytes": 1132,
|
||||
"sha256": "94f6814f61b75dc4ad9f9f14babed492865079c45fa5de17e6fc9c8a4fabc349"
|
||||
},
|
||||
"native/picker-baseline-1/12-cancel-preserved-0.jpg": {
|
||||
"bytes": 145507,
|
||||
"sha256": "d3db7643267cdc532f630c972d6c53823679326910e482179d447efad6289a61"
|
||||
},
|
||||
"native/picker-baseline-1/12-cancel-preserved.json": {
|
||||
"bytes": 1116,
|
||||
"sha256": "259769191f79cd381e2c8be3230b0da8912d35ef0ce5c362b2dd4f1189773647"
|
||||
},
|
||||
"native/picker-baseline-1/inputs.json": {
|
||||
"bytes": 5843,
|
||||
"sha256": "c1e7219e30171f3854c97cfe6b2fa2411d7b3f39c60a12ad41b57ca61a8f4dff"
|
||||
},
|
||||
"native/picker-baseline-1/process.json": {
|
||||
"bytes": 61,
|
||||
"sha256": "a99574647fda39a1529b9733794064c5214005df5a5ea821f7e0d0e61e4136a9"
|
||||
},
|
||||
"native/picker-baseline-1/result.json": {
|
||||
"bytes": 1247,
|
||||
"sha256": "a4cfe1faa8edc3ae8a7eaef9bb0714b357ecbaf91ad5a5bc6e787fea7fe013d3"
|
||||
},
|
||||
"native/picker-baseline-1/started.json": {
|
||||
"bytes": 780,
|
||||
"sha256": "b059824ca0e7b1a528ec8ececbe1b8d37335bcd5a34de921c3c9b6fda448acbe"
|
||||
},
|
||||
"native/picker-baseline-1/stderr.log": {
|
||||
"bytes": 0,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"native/picker-baseline-1/stdout.log": {
|
||||
"bytes": 0,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"native/picker-corrected-1/inputs.json": {
|
||||
"bytes": 5843,
|
||||
"sha256": "c1e7219e30171f3854c97cfe6b2fa2411d7b3f39c60a12ad41b57ca61a8f4dff"
|
||||
},
|
||||
"native/picker-corrected-1/process.json": {
|
||||
"bytes": 61,
|
||||
"sha256": "749d13d70bb8c9d7b9ad35a11e63980562b5e2140a1325454d61c6fd2cc5ef7d"
|
||||
},
|
||||
"native/picker-corrected-1/result.json": {
|
||||
"bytes": 1256,
|
||||
"sha256": "98d9e77ed93d56f2fb79f21c70ba5dceb012605f491fa6d704bd4f2f3bb2f8ea"
|
||||
},
|
||||
"native/picker-corrected-1/started.json": {
|
||||
"bytes": 780,
|
||||
"sha256": "9260aaee5e9e733eeb38a334a96881a1ed796149d9ece8438e0682f1990a8404"
|
||||
},
|
||||
"native/picker-corrected-1/stderr.log": {
|
||||
"bytes": 0,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"native/picker-corrected-1/stdout.log": {
|
||||
"bytes": 0,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"native/picker-corrected-2/01-opened-0.jpg": {
|
||||
"bytes": 118158,
|
||||
"sha256": "84db89be21fabc31958b4521587ff708619a4a4cbf0ded6fe8b2c58e94a26a68"
|
||||
},
|
||||
"native/picker-corrected-2/01-opened.json": {
|
||||
"bytes": 922,
|
||||
"sha256": "44cb8a61c47648bdcdfdc278ebfd43e45942eb5211e8f77624b9982186c89c25"
|
||||
},
|
||||
"native/picker-corrected-2/02-teaching-reopened-0.jpg": {
|
||||
"bytes": 117821,
|
||||
"sha256": "aa6fd6a99c2f808c6b55c734c83b0aa8b2fbd1ce03a14b02f609e5c84275bc66"
|
||||
},
|
||||
"native/picker-corrected-2/02-teaching-reopened.json": {
|
||||
"bytes": 933,
|
||||
"sha256": "b8a9dd0a0462f9b2703bfd9d88823b9ca00efba304f0f1cd923d637e8a01b6da"
|
||||
},
|
||||
"native/picker-corrected-2/03-loaded-0.jpg": {
|
||||
"bytes": 148546,
|
||||
"sha256": "dfe2700568a0e2f688c8c197224d75d7c15dfde5f060dfe2aba09631837687ef"
|
||||
},
|
||||
"native/picker-corrected-2/03-loaded.json": {
|
||||
"bytes": 1107,
|
||||
"sha256": "e8b18a5e75b52649dd87118f70c55be4b0e9c7e2ee00c2a3db730376a6c36aa4"
|
||||
},
|
||||
"native/picker-corrected-2/04-source-selected-0.jpg": {
|
||||
"bytes": 146472,
|
||||
"sha256": "74baf9f9059b4f0741d500a1f6d84ccb91474ff48de7466a03736e5decc868ca"
|
||||
},
|
||||
"native/picker-corrected-2/04-source-selected.json": {
|
||||
"bytes": 1116,
|
||||
"sha256": "45f7cfeab95e2ca2ca2ed2cd45bea9a269c1c6a1515dbe9cbcb3087fb2917c55"
|
||||
},
|
||||
"native/picker-corrected-2/05-actions-0.jpg": {
|
||||
"bytes": 153725,
|
||||
"sha256": "c00b412af778af51cb2a6658bae4f4e11bb024a0770c3ea5aa4a54383be381c4"
|
||||
},
|
||||
"native/picker-corrected-2/05-actions.json": {
|
||||
"bytes": 1108,
|
||||
"sha256": "1132f230694873a7e9536fd59f06b976e3b52041b119e2e9632b7c66de53d400"
|
||||
},
|
||||
"native/picker-corrected-2/06-transition-dialog-0.jpg": {
|
||||
"bytes": 149653,
|
||||
"sha256": "a4736781ac296164ddffe12005f9f1ad29d73ddd2bc752d14fb2b63ac2579153"
|
||||
},
|
||||
"native/picker-corrected-2/06-transition-dialog.json": {
|
||||
"bytes": 1126,
|
||||
"sha256": "2a2b8e8d9ac75a7c354e79bd5ba764c4024b045db009d5feb2b7f3909423f753"
|
||||
},
|
||||
"native/picker-corrected-2/07-compatible-menu-0.jpg": {
|
||||
"bytes": 148938,
|
||||
"sha256": "80621a49998cc7fc2a643e7338a6867c722b4365fabc713fac0b39f7812703f9"
|
||||
},
|
||||
"native/picker-corrected-2/07-compatible-menu.json": {
|
||||
"bytes": 1126,
|
||||
"sha256": "0f3c6a3d512e163953b9773de45df81d4129c0ded48a13c4d79de32dd8ab2d9e"
|
||||
},
|
||||
"native/picker-corrected-2/08-menu-dismissed-0.jpg": {
|
||||
"bytes": 149416,
|
||||
"sha256": "867236aeac2dcf839104692f73cb63fb45d7dcc38d95eaabd5cba8d0a7ff862a"
|
||||
},
|
||||
"native/picker-corrected-2/08-menu-dismissed.json": {
|
||||
"bytes": 1125,
|
||||
"sha256": "65ce375a1918a848cc57739f90eb15fbd55e9173c2343701791d0cf9740b818e"
|
||||
},
|
||||
"native/picker-corrected-2/09-search-focus-0.jpg": {
|
||||
"bytes": 149668,
|
||||
"sha256": "4a08a7955982956d518397832a0befdda951347f04da9af75af787fe5a163332"
|
||||
},
|
||||
"native/picker-corrected-2/09-search-focus.json": {
|
||||
"bytes": 1123,
|
||||
"sha256": "de505bdab419c6c60a3ad260479036d373f915333eb6fc62cd68c57ce2974af9"
|
||||
},
|
||||
"native/picker-corrected-2/10-alias-query-0.jpg": {
|
||||
"bytes": 149373,
|
||||
"sha256": "dceaa7f1e0e0959aa6e654d0b9f84cbe20badec8d99315e3fd2ebc3cb47f6f5f"
|
||||
},
|
||||
"native/picker-corrected-2/10-alias-query.json": {
|
||||
"bytes": 1122,
|
||||
"sha256": "c080f52656f6f603065e763cefb4a6ef790f4cfad96a3ddd3b6d58c5053cd62d"
|
||||
},
|
||||
"native/picker-corrected-2/11-filtered-menu-0.jpg": {
|
||||
"bytes": 149367,
|
||||
"sha256": "6893983b46da3a6f3da1ee3eac90dc50f34dbb257671835d7e893244d18b1b03"
|
||||
},
|
||||
"native/picker-corrected-2/11-filtered-menu.json": {
|
||||
"bytes": 1124,
|
||||
"sha256": "f8c5c8573840faef1337a7b429645e46652d29340104c2d6b15a9e1fbc14f152"
|
||||
},
|
||||
"native/picker-corrected-2/12-existing-selection-0.jpg": {
|
||||
"bytes": 147991,
|
||||
"sha256": "839dee6d22448d12d90758ca76f5503fae69a099af0e055e20cf814d7dd498ec"
|
||||
},
|
||||
"native/picker-corrected-2/12-existing-selection.json": {
|
||||
"bytes": 1129,
|
||||
"sha256": "84bc484447049dbafa1cda1b4c1ce0ade93c7ddcbf1c0489f2b18167deabed00"
|
||||
},
|
||||
"native/picker-corrected-2/13-refocus-search-0.jpg": {
|
||||
"bytes": 148017,
|
||||
"sha256": "70f5a9d3ea5d2b82b68e19d5682659ba0bba1abaeac0f56aca4a88ffeec74b76"
|
||||
},
|
||||
"native/picker-corrected-2/13-refocus-search.json": {
|
||||
"bytes": 1125,
|
||||
"sha256": "c2e42830b2d2364699210f716c375f2e63a447f910e729be0aa60c157f8341f6"
|
||||
},
|
||||
"native/picker-corrected-2/14-query-selected-0.jpg": {
|
||||
"bytes": 148447,
|
||||
"sha256": "e0e5807ed5f0f535110ceecc6c6aabb9d077ea67fa1d83367d2c50141e8cdc53"
|
||||
},
|
||||
"native/picker-corrected-2/14-query-selected.json": {
|
||||
"bytes": 1125,
|
||||
"sha256": "5d97ec91b3fd752d9f5e896c6fe97abc97ca40a91b6a708209663d4c198cc2e0"
|
||||
},
|
||||
"native/picker-corrected-2/15-no-results-selection-retained-0.jpg": {
|
||||
"bytes": 150165,
|
||||
"sha256": "25be189c30acb4c8dab03d458b2fc73bd09d0766513d5c9ee4e0e0bffcee2457"
|
||||
},
|
||||
"native/picker-corrected-2/15-no-results-selection-retained.json": {
|
||||
"bytes": 1140,
|
||||
"sha256": "a4f3c3e357aed8f66cad8b3c89f0e89d9eaf0662e3d740a06ba8eee6e7a33748"
|
||||
},
|
||||
"native/picker-corrected-2/16-parent-search-focus-0.jpg": {
|
||||
"bytes": 149957,
|
||||
"sha256": "84bdd2c3ed40178a16452a680932d27d42142220f09f79dc747906732aafb97d"
|
||||
},
|
||||
"native/picker-corrected-2/16-parent-search-focus.json": {
|
||||
"bytes": 1130,
|
||||
"sha256": "c3346906c51cf087a1fa850b46c867491630c2ec9a1565671e5674f2600ee261"
|
||||
},
|
||||
"native/picker-corrected-2/17-independent-card-queries-0.jpg": {
|
||||
"bytes": 149082,
|
||||
"sha256": "a48e99ca120c7e34ce2823bc0140a99778b88849eb7a6b245867f129a0e46aff"
|
||||
},
|
||||
"native/picker-corrected-2/17-independent-card-queries.json": {
|
||||
"bytes": 1135,
|
||||
"sha256": "ced4961b7bffb3d5f82757036ac77dab3cffba705bdd0a596183e686b3a59691"
|
||||
},
|
||||
"native/picker-corrected-2/18-field-review-0.jpg": {
|
||||
"bytes": 144509,
|
||||
"sha256": "734e83058cc66d1af13098b5b69c7e2cbad8daf2a3afd2a25780e65faa38bf71"
|
||||
},
|
||||
"native/picker-corrected-2/18-field-review.json": {
|
||||
"bytes": 1123,
|
||||
"sha256": "7489d77a9b1d4718f43aa10edca688e42a76a05507198538bd714ac8de638432"
|
||||
},
|
||||
"native/picker-corrected-2/19-keep-target-reviewed-0.jpg": {
|
||||
"bytes": 144547,
|
||||
"sha256": "8eafc33857650748a059a2c1dc345112b300a6d16d0395b33ff8f7e596d18d04"
|
||||
},
|
||||
"native/picker-corrected-2/19-keep-target-reviewed.json": {
|
||||
"bytes": 1131,
|
||||
"sha256": "509c9c3f0237a860c61e2413f05ab7bd9c51e5ffcbf97caede5c7298de0d479a"
|
||||
},
|
||||
"native/picker-corrected-2/20-reviewed-search-focus-0.jpg": {
|
||||
"bytes": 144893,
|
||||
"sha256": "0e46bb4787aba3c19cb27461cf70a22fdb579f3a63839a91ee68d37d7f9dd1d9"
|
||||
},
|
||||
"native/picker-corrected-2/20-reviewed-search-focus.json": {
|
||||
"bytes": 1132,
|
||||
"sha256": "33325c0cdac00fd49f25d4606c88a241dc6957f91f38d97b3ebafd4f5c42eaf2"
|
||||
},
|
||||
"native/picker-corrected-2/21-reviewed-query-selected-0.jpg": {
|
||||
"bytes": 144916,
|
||||
"sha256": "bec9c198292a5d4f6883c003112da1634fc7b8b8694a2910e51ed29079eaeb4b"
|
||||
},
|
||||
"native/picker-corrected-2/21-reviewed-query-selected.json": {
|
||||
"bytes": 1134,
|
||||
"sha256": "cf01d193ba011ed141e98236dff49fc06f789bbb26bb92b5a848a3f059c4319a"
|
||||
},
|
||||
"native/picker-corrected-2/22-cleared-search-review-retained-0.jpg": {
|
||||
"bytes": 145507,
|
||||
"sha256": "ff8670ba72a6b516e01bfcb9a8036940682e8f2caec92342b0902d9c0bb26b87"
|
||||
},
|
||||
"native/picker-corrected-2/22-cleared-search-review-retained.json": {
|
||||
"bytes": 1141,
|
||||
"sha256": "9a6a51578618a6c8aaf238329624102f3f37fda306be1dadffbc97dca178c221"
|
||||
},
|
||||
"native/picker-corrected-2/23-cancel-restores-saved-0.jpg": {
|
||||
"bytes": 145951,
|
||||
"sha256": "c79b52574c7ad9ea3a442679fbccb7e2683be6c0c8d015260398bc31fe9c705c"
|
||||
},
|
||||
"native/picker-corrected-2/23-cancel-restores-saved.json": {
|
||||
"bytes": 1122,
|
||||
"sha256": "6524ed5fd1cadc41de60d60818ec69e47a406189d03ede0822ea70ed502798b9"
|
||||
},
|
||||
"native/picker-corrected-2/inputs.json": {
|
||||
"bytes": 5843,
|
||||
"sha256": "c1e7219e30171f3854c97cfe6b2fa2411d7b3f39c60a12ad41b57ca61a8f4dff"
|
||||
},
|
||||
"native/picker-corrected-2/process.json": {
|
||||
"bytes": 61,
|
||||
"sha256": "6c4ea33570bd26271c5ec70c884dd4aeeab0642c33f46896ecd8832cb7ff5361"
|
||||
},
|
||||
"native/picker-corrected-2/result.json": {
|
||||
"bytes": 2837,
|
||||
"sha256": "b483791514deb4c1e16272dfabdf78f5a8092ddc8deeefc01a23baaed2913c2d"
|
||||
},
|
||||
"native/picker-corrected-2/started.json": {
|
||||
"bytes": 2370,
|
||||
"sha256": "cd97a270ae7e4eae8c6777ec311eb2437c4dd55e8b5882cbd8011c82fca2f7af"
|
||||
},
|
||||
"native/picker-corrected-2/stderr.log": {
|
||||
"bytes": 0,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"native/picker-corrected-2/stdout.log": {
|
||||
"bytes": 0,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"native/run-picker-baseline.py": {
|
||||
"bytes": 2106,
|
||||
"sha256": "86c88e94ccb5fcfd8f81db429db9991170839bc4a20dab408ab1d284c7e234f1"
|
||||
},
|
||||
"native/run-picker-corrected-2.py": {
|
||||
"bytes": 2162,
|
||||
"sha256": "e61347fa440337bc9629c0d3b532c39e16cfb8d1966132992676bfa1c8f546f5"
|
||||
},
|
||||
"native/run-picker-corrected.py": {
|
||||
"bytes": 2124,
|
||||
"sha256": "0e82acfca234bade68a6263f1c5a2222e1eeaff932fa7c92eb88063f254f6701"
|
||||
}
|
||||
},
|
||||
"externalEvidence": {
|
||||
"correctedCargo": {
|
||||
"repositoryPath": "harness/experiments/transition-lifecycle-20260913/evidence.zip",
|
||||
"commit": "fe43730bec196cc6ebbc06dcf827169f04c120f0",
|
||||
"bytes": 8496343,
|
||||
"sha256": "5f44ad5d83024a04a873142e77c5b8961c278a19d76492618c3ef2db63801070"
|
||||
},
|
||||
"baselineNative": {
|
||||
"repositoryPath": "harness/experiments/ordinary-text-native-20260913/evidence.zip",
|
||||
"commit": "3f9e6a18af96b5cd10940da4093be8da28b9c835",
|
||||
"bytes": 5704887,
|
||||
"sha256": "b827d0bdaec76b7b86d8a169530dcf98767c9883187ecdda02b4ea87171a42b5"
|
||||
}
|
||||
},
|
||||
"captureCount": 35,
|
||||
"imageCount": 37
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Read-only portable receipt and frame verification, without archived execution."""
|
||||
from pathlib import Path, PurePosixPath
|
||||
from datetime import datetime
|
||||
import hashlib,json,zipfile
|
||||
ROOT=Path(__file__).resolve().parent
|
||||
def pin(b):return {'bytes':len(b),'sha256':hashlib.sha256(b).hexdigest()}
|
||||
def need(ok,msg):
|
||||
if not ok:raise ValueError(msg)
|
||||
def main():
|
||||
files=json.loads((ROOT/'files.json').read_bytes())
|
||||
for n,p in files.items():need(pin((ROOT/n).read_bytes())==p,'outer hash '+n)
|
||||
manifest=json.loads((ROOT/'manifest.json').read_bytes())
|
||||
with zipfile.ZipFile(ROOT/'evidence.zip') as z:
|
||||
names=z.namelist()
|
||||
need(len(names)==len(set(names)) and set(names)==set(manifest['members']),'exact ZIP membership')
|
||||
need(all(not PurePosixPath(n).is_absolute() and '..' not in PurePosixPath(n).parts and '\\' not in n and ':' not in n for n in names),'safe members')
|
||||
for n,p in manifest['members'].items():need(pin(z.read(n))==p,'member hash '+n)
|
||||
def obj(n):return json.loads(z.read(n))
|
||||
input_names={n[len('native/input/'):]:n for n in names if n.startswith('native/input/')}
|
||||
reference=None;frame_count=0;image_count=0
|
||||
for run,spec in manifest['runs'].items():
|
||||
prefix='native/'+run+'/'
|
||||
start=obj(prefix+'started.json');result=obj(prefix+'result.json');process=obj(prefix+'process.json')
|
||||
need(all(result[k]==v for k,v in start.items()),run+' start/result relationship')
|
||||
need(result['exitCode']==spec['exitCode'],run+' exit code')
|
||||
need(isinstance(process['pid'],int) and process['pid']>0,run+' process ID')
|
||||
need(datetime.fromisoformat(start['start'])<=datetime.fromisoformat(process['observed'])<=datetime.fromisoformat(result['finish']),run+' process time order')
|
||||
need(result['helper']==pin(z.read(spec['helper'])),run+' launch helper')
|
||||
need(result['inputs']==pin(z.read(prefix+'inputs.json')) and result['inputUnchanged'] is True,run+' input receipt')
|
||||
inputs=obj(prefix+'inputs.json')
|
||||
need(set(inputs)==set(input_names),run+' input inventory')
|
||||
for n,p in inputs.items():need(pin(z.read(input_names[n]))==p,run+' input bytes '+n)
|
||||
if reference is None:reference=inputs
|
||||
need(inputs==reference,run+' all trials identical fixture')
|
||||
for stream in ['stdout','stderr']:
|
||||
b=z.read(prefix+stream+'.log');need(not b and result[stream]==pin(b),run+' empty stream '+stream)
|
||||
build=obj(spec['build'])
|
||||
need(result['buildReceipt']==pin(z.read(spec['build'])) and result['buildHead']==build['head'],run+' build receipt')
|
||||
need(build['exitCode']==0 and build['inputsUnchanged'] is True,run+' build status')
|
||||
need(datetime.fromisoformat(build['start'])<=datetime.fromisoformat(build['finish'])<=datetime.fromisoformat(start['start']),run+' build before launch')
|
||||
gui=[a for a in build['artifacts'] if a['cargo']['target']['name']=='arcadia-studio']
|
||||
need(len(gui)==1,run+' unique GUI artifact')
|
||||
need(result['binary']==result['binaryAfter']==gui[0]['readback'],run+' unchanged GUI pin')
|
||||
need(result['command']==[gui[0]['cargo']['executable']],run+' exact launch command')
|
||||
images=[]
|
||||
for metadata in spec['captures']:
|
||||
frame=obj(metadata)
|
||||
need(datetime.fromisoformat(start['start'])<=datetime.fromisoformat(frame['time'])<=datetime.fromisoformat(result['finish']),metadata+' capture interval')
|
||||
need(frame['window']['app'].replace('\\','/')=='process:'+result['command'][0].replace('\\','/'),metadata+' window application path')
|
||||
for image in frame['images']:
|
||||
n=prefix+image['file'];need(n in manifest['members'] and z.read(n).startswith(b'\xff\xd8'),metadata+' JPEG reference')
|
||||
need(image['width']>0 and image['height']>0,metadata+' frame dimensions')
|
||||
images.append(n)
|
||||
actual_images=[n for n in names if n.startswith(prefix) and n.endswith('.jpg')]
|
||||
need(len(images)==len(set(images)) and set(images)==set(actual_images),run+' all images referenced once')
|
||||
actual_metadata=[n for n in names if n.startswith(prefix) and n.endswith('.json') and 'images' in obj(n)]
|
||||
need(set(actual_metadata)==set(spec['captures']) and len(actual_metadata)==len(spec['captures']),run+' all capture metadata')
|
||||
frame_count+=len(actual_metadata);image_count+=len(images)
|
||||
print(run,'exit',result['exitCode'],'captures',len(actual_metadata),'images',len(images))
|
||||
need(frame_count==manifest['captureCount'] and image_count==manifest['imageCount'],'aggregate frame counts')
|
||||
print('PASS:',len(names),'raw members;',len(reference),'unchanged fixture files;',frame_count,'capture records;',image_count,'original JPEGs.')
|
||||
print('Build source inventories are external. Frames prove recorded states, not action execution or PID attestation. Capture helper was not start-pinned.')
|
||||
if __name__=='__main__':main()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user