2130 lines
79 KiB
Rust
2130 lines
79 KiB
Rust
//! Isolated finite-model experiment. Not Studio syntax or production admission.
|
|
use std::collections::{BTreeMap, BTreeSet};
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
type Result<T> = std::result::Result<T, String>;
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
struct Definition {
|
|
member: String,
|
|
class: String,
|
|
layer: String,
|
|
fields: Vec<String>,
|
|
refs: Vec<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
struct Package {
|
|
identity: String,
|
|
version: String,
|
|
raw: String,
|
|
digest: String,
|
|
definitions: BTreeMap<String, Definition>,
|
|
description: String,
|
|
prototype: BTreeMap<String, Record>,
|
|
selected: BTreeSet<String>,
|
|
role_member: Option<String>,
|
|
}
|
|
|
|
impl Package {
|
|
fn parse(raw: String) -> Result<Self> {
|
|
let mut identity = None;
|
|
let mut version = None;
|
|
let mut definitions = BTreeMap::new();
|
|
let mut description = None;
|
|
let mut prototype = BTreeMap::new();
|
|
let mut selected = BTreeSet::new();
|
|
let mut role_member = None;
|
|
for line in raw.lines() {
|
|
let row: Vec<_> = line.split('|').collect();
|
|
match row.as_slice() {
|
|
["package", p, v] if identity.is_none() => {
|
|
identity = Some((*p).into());
|
|
version = Some((*v).into());
|
|
}
|
|
["description", text] if description.is_none() => {
|
|
description = Some((*text).into())
|
|
}
|
|
["def", member, class, layer, fields, refs] => {
|
|
let def = Definition {
|
|
member: (*member).into(),
|
|
class: (*class).into(),
|
|
layer: (*layer).into(),
|
|
fields: split(fields),
|
|
refs: split(refs),
|
|
};
|
|
if definitions.insert((*member).into(), def).is_some() {
|
|
return Err("duplicate member".into());
|
|
}
|
|
}
|
|
["prototype", member, ..] => {
|
|
let record = Record::parse(line.splitn(3, '|').nth(2).ok_or("bad prototype")?)?;
|
|
if record.id != *member || prototype.insert((*member).into(), record).is_some()
|
|
{
|
|
return Err("duplicate or mismatched prototype member".into());
|
|
}
|
|
}
|
|
["pattern", "monitor", members] if selected.is_empty() => {
|
|
selected = split(members).into_iter().collect()
|
|
}
|
|
["role", "sink", member, "Activity", "OA"] if role_member.is_none() => {
|
|
role_member = Some((*member).into())
|
|
}
|
|
["note", _] | ["analysis", "P", "K", "C"] => {}
|
|
_ => return Err(format!("unrecognized package row: {line}")),
|
|
}
|
|
}
|
|
let value = Self {
|
|
identity: identity.ok_or("missing package identity")?,
|
|
version: version.ok_or("missing version")?,
|
|
digest: sha256(raw.as_bytes()),
|
|
raw,
|
|
definitions,
|
|
description: description.ok_or("missing description")?,
|
|
prototype,
|
|
selected,
|
|
role_member,
|
|
};
|
|
for def in value.definitions.values() {
|
|
if !matches!(
|
|
def.class.as_str(),
|
|
"Kind" | "Unit" | "Scalar" | "Item" | "Field" | "Interface" | "Allocation"
|
|
) {
|
|
return Err("unsupported contract class".into());
|
|
}
|
|
if def.refs.iter().any(|r| !value.definitions.contains_key(r)) {
|
|
return Err("unresolved package reference".into());
|
|
}
|
|
}
|
|
if value
|
|
.selected
|
|
.iter()
|
|
.any(|member| !value.prototype.contains_key(member))
|
|
|| value
|
|
.role_member
|
|
.as_ref()
|
|
.is_some_and(|m| !value.prototype.contains_key(m) || value.selected.contains(m))
|
|
{
|
|
return Err("incomplete prototype/export/role".into());
|
|
}
|
|
Ok(value)
|
|
}
|
|
fn contract(&self, root: &str) -> Result<Vec<Definition>> {
|
|
let mut pending = vec![root.to_string()];
|
|
let mut seen = BTreeSet::new();
|
|
while let Some(id) = pending.pop() {
|
|
if seen.insert(id.clone()) {
|
|
pending.extend(
|
|
self.definitions
|
|
.get(&id)
|
|
.ok_or("missing contract root")?
|
|
.refs
|
|
.clone(),
|
|
);
|
|
}
|
|
}
|
|
Ok(seen.iter().map(|id| self.definitions[id].clone()).collect())
|
|
}
|
|
}
|
|
|
|
fn equivalent(a: &Package, am: &str, b: &Package, bm: &str) -> bool {
|
|
a.identity == b.identity
|
|
&& am == bm
|
|
&& a.contract(am)
|
|
.ok()
|
|
.zip(b.contract(bm).ok())
|
|
.is_some_and(|(a, b)| a == b)
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
enum Policy {
|
|
Strict,
|
|
SameOrigin,
|
|
Bridges(BTreeSet<(String, String)>),
|
|
}
|
|
fn pair(a: &str, b: &str) -> (String, String) {
|
|
if a <= b {
|
|
(a.into(), b.into())
|
|
} else {
|
|
(b.into(), a.into())
|
|
}
|
|
}
|
|
fn policy_matches(a: &Package, am: &str, b: &Package, bm: &str, policy: &Policy) -> bool {
|
|
if a.identity == b.identity && a.digest == b.digest && am == bm {
|
|
return true;
|
|
}
|
|
match policy {
|
|
Policy::Strict => false,
|
|
Policy::SameOrigin => equivalent(a, am, b, bm),
|
|
Policy::Bridges(witnesses) => {
|
|
equivalent(a, am, b, bm)
|
|
&& witnesses.contains(&pair(
|
|
&format!("{}:{am}", a.digest),
|
|
&format!("{}:{bm}", b.digest),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
struct DRef {
|
|
import: String,
|
|
member: String,
|
|
}
|
|
impl DRef {
|
|
fn new(import: &str, member: &str) -> Self {
|
|
Self {
|
|
import: import.into(),
|
|
member: member.into(),
|
|
}
|
|
}
|
|
fn text(&self) -> String {
|
|
format!("{}:{}", self.import, self.member)
|
|
}
|
|
fn parse(s: &str) -> Result<Self> {
|
|
let (i, m) = s.split_once(':').ok_or("bad definition reference")?;
|
|
Ok(Self::new(i, m))
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
struct Literal {
|
|
magnitude: i64,
|
|
unit: DRef,
|
|
}
|
|
impl Literal {
|
|
fn new(magnitude: i64, import: &str, unit: &str) -> Self {
|
|
Self {
|
|
magnitude,
|
|
unit: DRef::new(import, unit),
|
|
}
|
|
}
|
|
fn text(&self) -> String {
|
|
format!("{}@{}", self.magnitude, self.unit.text())
|
|
}
|
|
fn parse(s: &str) -> Result<Self> {
|
|
let (n, u) = s.split_once('@').ok_or("bad literal")?;
|
|
Ok(Self {
|
|
magnitude: n.parse().map_err(|_| "bad integer")?,
|
|
unit: DRef::parse(u)?,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
enum Body {
|
|
Activity {
|
|
description: String,
|
|
},
|
|
Interaction {
|
|
source: String,
|
|
target: String,
|
|
item: String,
|
|
},
|
|
Property {
|
|
subject: String,
|
|
kind: DRef,
|
|
base: Literal,
|
|
},
|
|
Case {
|
|
subject: Option<String>,
|
|
overrides: BTreeMap<String, Literal>,
|
|
},
|
|
Check {
|
|
property: String,
|
|
limit: Literal,
|
|
},
|
|
Budget {
|
|
left: String,
|
|
right: String,
|
|
kind: DRef,
|
|
limit: Literal,
|
|
},
|
|
Consumer {
|
|
exchange: String,
|
|
interface: String,
|
|
allocation: String,
|
|
field: String,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
struct Record {
|
|
id: String,
|
|
layer: String,
|
|
name: String,
|
|
body: Body,
|
|
}
|
|
|
|
impl Record {
|
|
fn line(&self) -> String {
|
|
let tail = match &self.body {
|
|
Body::Activity { description } => format!("Activity|{description}"),
|
|
Body::Interaction {
|
|
source,
|
|
target,
|
|
item,
|
|
} => format!("Interaction|{source}|{target}|{item}"),
|
|
Body::Property {
|
|
subject,
|
|
kind,
|
|
base,
|
|
} => format!("Property|{subject}|{}|{}", kind.text(), base.text()),
|
|
Body::Case { subject, overrides } => format!(
|
|
"Case|{}|{}",
|
|
subject.as_deref().unwrap_or("-"),
|
|
overrides
|
|
.iter()
|
|
.map(|(p, e)| format!("{p}={}", e.text()))
|
|
.collect::<Vec<_>>()
|
|
.join(",")
|
|
),
|
|
Body::Check { property, limit } => format!("Check|{property}|{}", limit.text()),
|
|
Body::Budget {
|
|
left,
|
|
right,
|
|
kind,
|
|
limit,
|
|
} => format!("Budget|{left}|{right}|{}|{}", kind.text(), limit.text()),
|
|
Body::Consumer {
|
|
exchange,
|
|
interface,
|
|
allocation,
|
|
field,
|
|
} => format!("Consumer|{exchange}|{interface}|{allocation}|{field}"),
|
|
};
|
|
format!("record|{}|{}|{}|{tail}", self.id, self.layer, self.name)
|
|
}
|
|
fn parse(line: &str) -> Result<Self> {
|
|
let r: Vec<_> = line.split('|').collect();
|
|
if r.len() < 5 || r[0] != "record" {
|
|
return Err("bad record".into());
|
|
}
|
|
let body = match &r[4..] {
|
|
["Activity", description] => Body::Activity {
|
|
description: (*description).into(),
|
|
},
|
|
["Interaction", source, target, item] => Body::Interaction {
|
|
source: (*source).into(),
|
|
target: (*target).into(),
|
|
item: (*item).into(),
|
|
},
|
|
["Property", subject, kind, value] => Body::Property {
|
|
subject: (*subject).into(),
|
|
kind: DRef::parse(kind)?,
|
|
base: Literal::parse(value)?,
|
|
},
|
|
["Case", subject, rows] => {
|
|
let mut overrides = BTreeMap::new();
|
|
for row in split(rows) {
|
|
let (id, value) = row.split_once('=').ok_or("bad override")?;
|
|
if overrides
|
|
.insert(id.into(), Literal::parse(value)?)
|
|
.is_some()
|
|
{
|
|
return Err("duplicate override".into());
|
|
}
|
|
}
|
|
Body::Case {
|
|
subject: (*subject != "-").then(|| (*subject).into()),
|
|
overrides,
|
|
}
|
|
}
|
|
["Check", property, limit] => Body::Check {
|
|
property: (*property).into(),
|
|
limit: Literal::parse(limit)?,
|
|
},
|
|
["Budget", left, right, kind, limit] => Body::Budget {
|
|
left: (*left).into(),
|
|
right: (*right).into(),
|
|
kind: DRef::parse(kind)?,
|
|
limit: Literal::parse(limit)?,
|
|
},
|
|
["Consumer", exchange, interface, allocation, field] => Body::Consumer {
|
|
exchange: (*exchange).into(),
|
|
interface: (*interface).into(),
|
|
allocation: (*allocation).into(),
|
|
field: (*field).into(),
|
|
},
|
|
_ => return Err("bad constructor".into()),
|
|
};
|
|
Ok(Self {
|
|
id: r[1].into(),
|
|
layer: r[2].into(),
|
|
name: r[3].into(),
|
|
body,
|
|
})
|
|
}
|
|
fn remap_live(&mut self, old: &str, new: &str) {
|
|
fn remap(id: &mut String, old: &str, new: &str) {
|
|
if id == old {
|
|
*id = new.into();
|
|
}
|
|
}
|
|
remap(&mut self.id, old, new);
|
|
match &mut self.body {
|
|
Body::Activity { .. } => {}
|
|
Body::Interaction {
|
|
source,
|
|
target,
|
|
item,
|
|
} => {
|
|
remap(source, old, new);
|
|
remap(target, old, new);
|
|
remap(item, old, new);
|
|
}
|
|
Body::Property { subject, .. } => remap(subject, old, new),
|
|
Body::Case { subject, overrides } => {
|
|
if let Some(subject) = subject {
|
|
remap(subject, old, new);
|
|
}
|
|
if let Some(v) = overrides.remove(old) {
|
|
overrides.insert(new.into(), v);
|
|
}
|
|
}
|
|
Body::Check { property, .. } => remap(property, old, new),
|
|
Body::Budget { left, right, .. } => {
|
|
remap(left, old, new);
|
|
remap(right, old, new);
|
|
}
|
|
Body::Consumer {
|
|
exchange,
|
|
interface,
|
|
allocation,
|
|
field,
|
|
} => {
|
|
remap(exchange, old, new);
|
|
remap(interface, old, new);
|
|
remap(allocation, old, new);
|
|
remap(field, old, new);
|
|
}
|
|
}
|
|
}
|
|
fn rebind_self_definitions(&mut self, import: &str) {
|
|
fn reference(r: &mut DRef, import: &str) {
|
|
if r.import == "SELF" {
|
|
r.import = import.into();
|
|
}
|
|
}
|
|
fn literal(l: &mut Literal, import: &str) {
|
|
reference(&mut l.unit, import);
|
|
}
|
|
match &mut self.body {
|
|
Body::Property { kind, base, .. } => {
|
|
reference(kind, import);
|
|
literal(base, import);
|
|
}
|
|
Body::Case { overrides, .. } => {
|
|
for value in overrides.values_mut() {
|
|
literal(value, import);
|
|
}
|
|
}
|
|
Body::Check { limit, .. } => literal(limit, import),
|
|
Body::Budget { kind, limit, .. } => {
|
|
reference(kind, import);
|
|
literal(limit, import);
|
|
}
|
|
Body::Activity { .. } | Body::Interaction { .. } | Body::Consumer { .. } => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
struct Mount {
|
|
import: String,
|
|
members: BTreeMap<String, String>,
|
|
}
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
struct Instance {
|
|
import: String,
|
|
mount: String,
|
|
sink: String,
|
|
prefix: String,
|
|
original: BTreeMap<String, String>,
|
|
current: BTreeMap<String, Option<String>>,
|
|
lineage: BTreeMap<String, String>,
|
|
base: BTreeMap<String, Record>,
|
|
}
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
struct Document {
|
|
identity: String,
|
|
revision: u32,
|
|
imports: BTreeMap<String, Package>,
|
|
mounts: BTreeMap<String, Mount>,
|
|
instances: BTreeMap<String, Instance>,
|
|
live: BTreeMap<String, Record>,
|
|
receipts: Vec<String>,
|
|
}
|
|
|
|
fn materialize(instance: &Instance, package: &Package, mount: &Mount) -> BTreeMap<String, Record> {
|
|
package
|
|
.selected
|
|
.iter()
|
|
.map(|member| {
|
|
let mut record = package.prototype[member].clone();
|
|
for (foreign, local) in &instance.original {
|
|
record.remap_live(foreign, local);
|
|
}
|
|
if let Some(role_member) = &package.role_member {
|
|
record.remap_live(role_member, &instance.sink);
|
|
}
|
|
for (foreign, local) in &mount.members {
|
|
record.remap_live(&format!("MOUNT/{foreign}"), local);
|
|
}
|
|
record.rebind_self_definitions(&instance.import);
|
|
record.name = format!("{} {}", instance.prefix, record.name);
|
|
(member.clone(), record)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn split(s: &str) -> Vec<String> {
|
|
if s.is_empty() || s == "-" {
|
|
vec![]
|
|
} else {
|
|
s.split(',').map(String::from).collect()
|
|
}
|
|
}
|
|
fn read(path: &Path) -> Result<String> {
|
|
fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))
|
|
}
|
|
fn write(path: &Path, text: &str) -> Result<()> {
|
|
if let Some(parent) = path.parent() {
|
|
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
|
|
}
|
|
fs::write(path, text).map_err(|e| e.to_string())
|
|
}
|
|
|
|
fn prepare_new(request: &str, packages: &BTreeMap<String, Package>) -> Result<Document> {
|
|
let mut doc = Document {
|
|
identity: String::new(),
|
|
revision: 1,
|
|
imports: BTreeMap::new(),
|
|
mounts: BTreeMap::new(),
|
|
instances: BTreeMap::new(),
|
|
live: BTreeMap::new(),
|
|
receipts: vec![],
|
|
};
|
|
for line in request.lines() {
|
|
let r: Vec<_> = line.split('|').collect();
|
|
match r.as_slice() {
|
|
["new", id] if doc.identity.is_empty() => doc.identity = (*id).into(),
|
|
["import", alias, package] => {
|
|
if doc
|
|
.imports
|
|
.insert(
|
|
(*alias).into(),
|
|
packages
|
|
.get(*package)
|
|
.ok_or("missing supplied package")?
|
|
.clone(),
|
|
)
|
|
.is_some()
|
|
{
|
|
return Err("duplicate import".into());
|
|
}
|
|
}
|
|
["mount", id, import] => {
|
|
let p = doc.imports.get(*import).ok_or("missing import")?;
|
|
let members = p
|
|
.definitions
|
|
.values()
|
|
.filter(|d| d.layer == "OA")
|
|
.map(|d| (d.member.clone(), format!("{id}/{}", d.member)))
|
|
.collect();
|
|
if doc
|
|
.mounts
|
|
.insert(
|
|
(*id).into(),
|
|
Mount {
|
|
import: (*import).into(),
|
|
members,
|
|
},
|
|
)
|
|
.is_some()
|
|
{
|
|
return Err("duplicate mount".into());
|
|
}
|
|
}
|
|
["activity", id, name] => {
|
|
doc.live.insert(
|
|
(*id).into(),
|
|
Record {
|
|
id: (*id).into(),
|
|
layer: "OA".into(),
|
|
name: (*name).into(),
|
|
body: Body::Activity {
|
|
description: String::new(),
|
|
},
|
|
},
|
|
);
|
|
}
|
|
["instantiate", id, import, mount, sink] => {
|
|
if !doc.live.contains_key(*sink) {
|
|
return Err("missing role binding".into());
|
|
}
|
|
let original: BTreeMap<String, String> = ["A", "E", "P", "C", "K"]
|
|
.into_iter()
|
|
.map(|m| (m.into(), format!("{id}/{m}")))
|
|
.collect();
|
|
let mut instance = Instance {
|
|
import: (*import).into(),
|
|
mount: (*mount).into(),
|
|
sink: (*sink).into(),
|
|
prefix: (*id).into(),
|
|
current: original
|
|
.iter()
|
|
.map(|(m, id)| (m.clone(), Some(id.clone())))
|
|
.collect(),
|
|
lineage: original
|
|
.values()
|
|
.map(|id| (id.clone(), id.clone()))
|
|
.collect(),
|
|
original,
|
|
base: BTreeMap::new(),
|
|
};
|
|
let mount = doc.mounts.get(*mount).ok_or("missing mount")?;
|
|
if mount.import != *import {
|
|
return Err("initial mount pin mismatch".into());
|
|
}
|
|
instance.base = materialize(
|
|
&instance,
|
|
doc.imports.get(*import).ok_or("missing import")?,
|
|
mount,
|
|
);
|
|
for r in instance.base.values() {
|
|
if doc.live.insert(r.id.clone(), r.clone()).is_some() {
|
|
return Err("identity collision".into());
|
|
}
|
|
}
|
|
if doc.instances.insert((*id).into(), instance).is_some() {
|
|
return Err("duplicate instance".into());
|
|
}
|
|
}
|
|
["budget", id, left, right, import] => {
|
|
doc.live.insert(
|
|
(*id).into(),
|
|
Record {
|
|
id: (*id).into(),
|
|
layer: "OA".into(),
|
|
name: "Combined duration".into(),
|
|
body: Body::Budget {
|
|
left: (*left).into(),
|
|
right: (*right).into(),
|
|
kind: DRef::new(import, "Duration"),
|
|
limit: Literal::new(2000, import, "ms"),
|
|
},
|
|
},
|
|
);
|
|
}
|
|
["consumer", id, exchange, mount] => {
|
|
let m = doc.mounts.get(*mount).ok_or("missing consumer mount")?;
|
|
doc.live.insert(
|
|
(*id).into(),
|
|
Record {
|
|
id: (*id).into(),
|
|
layer: "OA".into(),
|
|
name: "Outside typed receiver".into(),
|
|
body: Body::Consumer {
|
|
exchange: (*exchange).into(),
|
|
interface: m.members["Interface"].clone(),
|
|
allocation: m.members["Operation"].clone(),
|
|
field: m.members["Code"].clone(),
|
|
},
|
|
},
|
|
);
|
|
}
|
|
["global_case", id, property] => {
|
|
doc.live.insert(
|
|
(*id).into(),
|
|
Record {
|
|
id: (*id).into(),
|
|
layer: "OA".into(),
|
|
name: "Unrelated project-wide study".into(),
|
|
body: Body::Case {
|
|
subject: None,
|
|
overrides: BTreeMap::from([(
|
|
(*property).into(),
|
|
Literal::new(3000, "controls1", "ms"),
|
|
)]),
|
|
},
|
|
},
|
|
);
|
|
}
|
|
["receipt", id] => doc.receipts.push((*id).into()),
|
|
_ => return Err(format!("unsupported request row: {line}")),
|
|
}
|
|
}
|
|
admit(&doc)?;
|
|
Ok(doc)
|
|
}
|
|
|
|
fn admit(doc: &Document) -> Result<()> {
|
|
let mut identities: BTreeSet<String> = doc.live.keys().cloned().collect();
|
|
let mut labels = BTreeMap::new();
|
|
for (alias, p) in &doc.imports {
|
|
if sha256(p.raw.as_bytes()) != p.digest {
|
|
return Err(format!("wrong package bytes: {alias}"));
|
|
}
|
|
if Package::parse(p.raw.clone())? != *p {
|
|
return Err("forged parsed package".into());
|
|
}
|
|
if let Some(prior) =
|
|
labels.insert((p.identity.clone(), p.version.clone()), p.digest.clone())
|
|
&& prior != p.digest
|
|
{
|
|
return Err("same package/version has conflicting bytes".into());
|
|
}
|
|
}
|
|
for mount in doc.mounts.values() {
|
|
let p = doc
|
|
.imports
|
|
.get(&mount.import)
|
|
.ok_or("missing mount package")?;
|
|
let required: BTreeSet<_> = p
|
|
.definitions
|
|
.values()
|
|
.filter(|d| d.layer == "OA")
|
|
.map(|d| d.member.clone())
|
|
.collect();
|
|
if required != mount.members.keys().cloned().collect() {
|
|
return Err("mount not complete".into());
|
|
}
|
|
for id in mount.members.values() {
|
|
if !identities.insert(id.clone()) {
|
|
return Err("mount identity collision".into());
|
|
}
|
|
}
|
|
}
|
|
let mut tracked = BTreeSet::new();
|
|
for instance in doc.instances.values() {
|
|
let p = doc
|
|
.imports
|
|
.get(&instance.import)
|
|
.ok_or("missing base package")?;
|
|
let m = doc
|
|
.mounts
|
|
.get(&instance.mount)
|
|
.ok_or("missing base mount")?;
|
|
if m.import != instance.import {
|
|
return Err("base mount pin mismatch".into());
|
|
}
|
|
if instance.original.keys().cloned().collect::<BTreeSet<_>>()
|
|
!= BTreeSet::from(["A", "E", "P", "C", "K"].map(String::from))
|
|
{
|
|
return Err("incomplete original map".into());
|
|
}
|
|
if materialize(instance, p, m) != instance.base {
|
|
return Err("forged historical base".into());
|
|
}
|
|
if instance.current.keys().ne(instance.original.keys()) {
|
|
return Err("incomplete correspondence".into());
|
|
}
|
|
for (member, live) in &instance.current {
|
|
let original = &instance.original[member];
|
|
if let Some(id) = live {
|
|
if id != original && identities.contains(original) {
|
|
return Err("historical local identity was reused".into());
|
|
}
|
|
if !tracked.insert(id.clone()) || !doc.live.contains_key(id) {
|
|
return Err("bad current map".into());
|
|
}
|
|
if instance.lineage.get(original) != Some(id) {
|
|
return Err("missing typed lineage".into());
|
|
}
|
|
if std::mem::discriminant(&doc.live[id].body)
|
|
!= std::mem::discriminant(&instance.base[member].body)
|
|
{
|
|
return Err("record class changed".into());
|
|
}
|
|
} else if identities.contains(original) {
|
|
return Err("deleted identity was reused".into());
|
|
}
|
|
}
|
|
}
|
|
for r in doc.live.values() {
|
|
let required = match &r.body {
|
|
Body::Activity { .. } => vec![],
|
|
Body::Interaction {
|
|
source,
|
|
target,
|
|
item,
|
|
} => vec![source, target, item],
|
|
Body::Property {
|
|
subject,
|
|
kind,
|
|
base,
|
|
} => {
|
|
kind_compatible(doc, kind, &unit_kind(doc, &base.unit)?)?
|
|
.then_some(())
|
|
.ok_or("property unit kind mismatch")?;
|
|
vec![subject]
|
|
}
|
|
Body::Case { subject, overrides } => {
|
|
let mut ids: Vec<_> = overrides.keys().collect();
|
|
if let Some(s) = subject {
|
|
ids.push(s);
|
|
}
|
|
for (id, literal) in overrides {
|
|
let Body::Property { kind, .. } =
|
|
&doc.live.get(id).ok_or("missing override property")?.body
|
|
else {
|
|
return Err("wrong override kind".into());
|
|
};
|
|
if !kind_compatible(doc, kind, &unit_kind(doc, &literal.unit)?)? {
|
|
return Err("override unit kind mismatch".into());
|
|
}
|
|
}
|
|
ids
|
|
}
|
|
Body::Check { property, .. } => vec![property],
|
|
Body::Budget { left, right, .. } => vec![left, right],
|
|
Body::Consumer {
|
|
exchange,
|
|
interface,
|
|
allocation,
|
|
field,
|
|
} => vec![exchange, interface, allocation, field],
|
|
};
|
|
if required.iter().any(|id| !identities.contains(*id)) {
|
|
return Err(format!("dangling live reference in {}", r.id));
|
|
}
|
|
}
|
|
evaluate(doc, "budget")?;
|
|
consumer_match(doc, "outside")?;
|
|
Ok(())
|
|
}
|
|
|
|
fn kind_compatible(doc: &Document, a: &DRef, b: &DRef) -> Result<bool> {
|
|
let pa = doc.imports.get(&a.import).ok_or("missing kind import")?;
|
|
let pb = doc.imports.get(&b.import).ok_or("missing kind import")?;
|
|
Ok(pa
|
|
.definitions
|
|
.get(&a.member)
|
|
.is_some_and(|d| d.class == "Kind")
|
|
&& pb
|
|
.definitions
|
|
.get(&b.member)
|
|
.is_some_and(|d| d.class == "Kind")
|
|
&& equivalent(pa, &a.member, pb, &b.member))
|
|
}
|
|
fn unit_kind(doc: &Document, unit: &DRef) -> Result<DRef> {
|
|
let p = doc.imports.get(&unit.import).ok_or("missing unit import")?;
|
|
let d = p.definitions.get(&unit.member).ok_or("missing unit")?;
|
|
if d.class != "Unit" || d.refs.len() != 1 {
|
|
return Err("wrong unit kind".into());
|
|
}
|
|
Ok(DRef::new(&unit.import, &d.refs[0]))
|
|
}
|
|
fn millis(doc: &Document, literal: &Literal) -> Result<i64> {
|
|
let p = doc
|
|
.imports
|
|
.get(&literal.unit.import)
|
|
.ok_or("missing unit import")?;
|
|
let d = p
|
|
.definitions
|
|
.get(&literal.unit.member)
|
|
.ok_or("missing unit")?;
|
|
let scale = d
|
|
.fields
|
|
.first()
|
|
.ok_or("missing scale")?
|
|
.strip_prefix("millis=")
|
|
.ok_or("bad scale")?
|
|
.parse::<i64>()
|
|
.map_err(|_| "bad scale")?;
|
|
literal
|
|
.magnitude
|
|
.checked_mul(scale)
|
|
.ok_or("overflow".into())
|
|
}
|
|
fn evaluate(doc: &Document, id: &str) -> Result<(i64, bool)> {
|
|
evaluate_policy(doc, id, &Policy::SameOrigin)
|
|
}
|
|
fn evaluate_check(doc: &Document, check: &str, context: Option<&str>) -> Result<(i64, bool)> {
|
|
let Body::Check { property, limit } = &doc.live.get(check).ok_or("missing check")?.body else {
|
|
return Err("not check".into());
|
|
};
|
|
let Body::Property { kind, base, .. } = &doc.live.get(property).ok_or("missing property")?.body
|
|
else {
|
|
return Err("not property".into());
|
|
};
|
|
let effective = if let Some(context) = context {
|
|
let Body::Case { overrides, .. } = &doc.live.get(context).ok_or("missing case")?.body
|
|
else {
|
|
return Err("not case".into());
|
|
};
|
|
overrides.get(property).unwrap_or(base)
|
|
} else {
|
|
base
|
|
};
|
|
if !kind_compatible(doc, kind, &unit_kind(doc, &effective.unit)?)?
|
|
|| !kind_compatible(doc, kind, &unit_kind(doc, &limit.unit)?)?
|
|
{
|
|
return Err("check kind mismatch".into());
|
|
}
|
|
let value = millis(doc, effective)?;
|
|
Ok((value, value <= millis(doc, limit)?))
|
|
}
|
|
fn evaluate_policy(doc: &Document, id: &str, policy: &Policy) -> Result<(i64, bool)> {
|
|
let Body::Budget {
|
|
left,
|
|
right,
|
|
kind,
|
|
limit,
|
|
} = &doc.live.get(id).ok_or("missing budget")?.body
|
|
else {
|
|
return Err("not budget".into());
|
|
};
|
|
let mut total = 0i64;
|
|
for id in [left, right] {
|
|
let Body::Property {
|
|
kind: input_kind,
|
|
base,
|
|
..
|
|
} = &doc.live.get(id).ok_or("missing property")?.body
|
|
else {
|
|
return Err("not property".into());
|
|
};
|
|
let unit_kind = unit_kind(doc, &base.unit)?;
|
|
if !policy_matches(
|
|
&doc.imports[&kind.import],
|
|
&kind.member,
|
|
&doc.imports[&input_kind.import],
|
|
&input_kind.member,
|
|
policy,
|
|
) || !policy_matches(
|
|
&doc.imports[&input_kind.import],
|
|
&input_kind.member,
|
|
&doc.imports[&unit_kind.import],
|
|
&unit_kind.member,
|
|
policy,
|
|
) {
|
|
return Err("incompatible budget kind".into());
|
|
}
|
|
total += millis(doc, base)?;
|
|
}
|
|
Ok((total, total <= millis(doc, limit)?))
|
|
}
|
|
fn mounted<'a>(doc: &'a Document, id: &str) -> Result<(&'a Mount, String)> {
|
|
for mount in doc.mounts.values() {
|
|
for (member, local) in &mount.members {
|
|
if id == local {
|
|
return Ok((mount, member.clone()));
|
|
}
|
|
}
|
|
}
|
|
Err("not a mounted member".into())
|
|
}
|
|
fn consumer_match(doc: &Document, id: &str) -> Result<BTreeMap<String, String>> {
|
|
consumer_match_policy(doc, id, &Policy::SameOrigin)
|
|
}
|
|
fn consumer_match_policy(
|
|
doc: &Document,
|
|
id: &str,
|
|
policy: &Policy,
|
|
) -> Result<BTreeMap<String, String>> {
|
|
let Body::Consumer {
|
|
exchange,
|
|
interface,
|
|
allocation,
|
|
field,
|
|
} = &doc.live.get(id).ok_or("missing consumer")?.body
|
|
else {
|
|
return Err("not consumer".into());
|
|
};
|
|
let Body::Interaction { item, .. } = &doc.live.get(exchange).ok_or("missing exchange")?.body
|
|
else {
|
|
return Err("not exchange".into());
|
|
};
|
|
let (actual, member) = mounted(doc, item)?;
|
|
let (expected, expected_interface) = mounted(doc, interface)?;
|
|
if member != "Alarm" || expected_interface != "Interface" {
|
|
return Err("wrong contracted root".into());
|
|
}
|
|
let a = &doc.imports[&actual.import];
|
|
let e = &doc.imports[&expected.import];
|
|
if !policy_matches(a, "Interface", e, "Interface", policy) {
|
|
return Err("item/interface not compatible".into());
|
|
}
|
|
if expected.members.get("Operation") != Some(allocation)
|
|
|| expected.members.get("Code") != Some(field)
|
|
{
|
|
return Err("operation or field outside declared root".into());
|
|
}
|
|
Ok(expected
|
|
.members
|
|
.iter()
|
|
.map(|(member, local)| (local.clone(), actual.members[member].clone()))
|
|
.collect())
|
|
}
|
|
|
|
fn apply_edit(doc: &Document, request: &str) -> Result<Document> {
|
|
let r: Vec<_> = request.trim().split('|').collect();
|
|
let ["edit_base", id, literal] = r.as_slice() else {
|
|
return Err("bad edit request".into());
|
|
};
|
|
let mut candidate = doc.clone();
|
|
let Body::Property { base, .. } = &mut candidate
|
|
.live
|
|
.get_mut(*id)
|
|
.ok_or("missing edit target")?
|
|
.body
|
|
else {
|
|
return Err("not property".into());
|
|
};
|
|
*base = Literal::parse(literal)?;
|
|
candidate.revision += 1;
|
|
candidate.receipts.push(request.trim().into());
|
|
admit(&candidate)?;
|
|
Ok(candidate)
|
|
}
|
|
|
|
fn merge_value<T: PartialEq + Clone>(base: &T, local: &T, next: &T, keep_local: bool) -> Result<T> {
|
|
if local == base {
|
|
Ok(next.clone())
|
|
} else if next == base || local == next || keep_local {
|
|
Ok(local.clone())
|
|
} else {
|
|
Err("unresolved typed value conflict".into())
|
|
}
|
|
}
|
|
fn update(doc: &Document, request: &str, package: Package) -> Result<Document> {
|
|
let r: Vec<_> = request.trim().split('|').collect();
|
|
let [
|
|
"update",
|
|
id,
|
|
old_digest,
|
|
new_import,
|
|
new_mount,
|
|
expected_revision,
|
|
choice,
|
|
] = r.as_slice()
|
|
else {
|
|
return Err("bad update request".into());
|
|
};
|
|
if expected_revision
|
|
.parse::<u32>()
|
|
.map_err(|_| "bad revision")?
|
|
!= doc.revision
|
|
{
|
|
return Err("stale revision".into());
|
|
}
|
|
let old = doc.instances.get(*id).ok_or("missing instance")?;
|
|
if doc.imports[&old.import].digest != *old_digest {
|
|
return Err("stale base pin".into());
|
|
}
|
|
let mut candidate = doc.clone();
|
|
if candidate.imports.contains_key(*new_import) || candidate.mounts.contains_key(*new_mount) {
|
|
return Err("immutable import/mount collision".into());
|
|
}
|
|
let mount = Mount {
|
|
import: (*new_import).into(),
|
|
members: package
|
|
.definitions
|
|
.values()
|
|
.filter(|d| d.layer == "OA")
|
|
.map(|d| (d.member.clone(), format!("{new_mount}/{}", d.member)))
|
|
.collect(),
|
|
};
|
|
let mut next = old.clone();
|
|
next.import = (*new_import).into();
|
|
next.mount = (*new_mount).into();
|
|
next.original = old
|
|
.current
|
|
.iter()
|
|
.map(|(m, id)| {
|
|
id.clone()
|
|
.map(|id| (m.clone(), id))
|
|
.ok_or("deleted member needs explicit policy")
|
|
})
|
|
.collect::<std::result::Result<_, _>>()?;
|
|
next.base = materialize(&next, &package, &mount);
|
|
for (member, incoming) in &next.base {
|
|
let local_id = old.current[member]
|
|
.as_ref()
|
|
.ok_or("missing current member")?;
|
|
let mut baseline = old.base[member].clone();
|
|
for (original, current) in &old.lineage {
|
|
baseline.remap_live(original, current);
|
|
}
|
|
let local = &doc.live[local_id];
|
|
let value = if local == &baseline {
|
|
incoming.clone()
|
|
} else if incoming == &baseline || incoming == local {
|
|
local.clone()
|
|
} else if member == "P" && *choice == "P.base=keep_local" {
|
|
let (
|
|
Body::Property {
|
|
subject: bs,
|
|
kind: bk,
|
|
base: bb,
|
|
},
|
|
Body::Property {
|
|
subject: ls,
|
|
kind: lk,
|
|
base: lb,
|
|
},
|
|
Body::Property {
|
|
subject: ns,
|
|
kind: nk,
|
|
base: nb,
|
|
},
|
|
) = (&baseline.body, &local.body, &incoming.body)
|
|
else {
|
|
return Err("constructor mismatch".into());
|
|
};
|
|
Record {
|
|
id: merge_value(&baseline.id, &local.id, &incoming.id, false)?,
|
|
layer: merge_value(&baseline.layer, &local.layer, &incoming.layer, false)?,
|
|
name: merge_value(&baseline.name, &local.name, &incoming.name, false)?,
|
|
body: Body::Property {
|
|
subject: merge_value(bs, ls, ns, false)?,
|
|
kind: merge_value(bk, lk, nk, false)?,
|
|
base: merge_value(bb, lb, nb, true)?,
|
|
},
|
|
}
|
|
} else {
|
|
return Err(format!("unresolved typed facet conflict: {member}"));
|
|
};
|
|
candidate.live.insert(value.id.clone(), value);
|
|
}
|
|
for id in next.original.values() {
|
|
next.lineage.insert(id.clone(), id.clone());
|
|
}
|
|
candidate.imports.insert((*new_import).into(), package);
|
|
candidate.mounts.insert((*new_mount).into(), mount);
|
|
candidate.instances.insert((*id).into(), next);
|
|
candidate.revision += 1;
|
|
candidate.receipts.push(request.trim().into());
|
|
admit(&candidate)?;
|
|
Ok(candidate)
|
|
}
|
|
|
|
fn save(doc: &Document, dir: &Path) -> Result<()> {
|
|
let mut source = format!("document|{}|{}\n", doc.identity, doc.revision);
|
|
for (alias, p) in &doc.imports {
|
|
let path = format!("packages/{}.library", p.digest);
|
|
source += &format!(
|
|
"import|{alias}|{}|{}|{}|{path}\n",
|
|
p.identity, p.version, p.digest
|
|
);
|
|
write(&dir.join(path), &p.raw)?;
|
|
}
|
|
for (id, m) in &doc.mounts {
|
|
source += &format!("mount|{id}|{}|management/{id}.mount\n", m.import);
|
|
let body = m
|
|
.members
|
|
.iter()
|
|
.map(|(foreign, local)| format!("member|{foreign}|{local}\n"))
|
|
.collect::<String>();
|
|
write(&dir.join(format!("management/{id}.mount")), &body)?;
|
|
}
|
|
for (id, i) in &doc.instances {
|
|
source += &format!("instance|{id}|management/{id}\n");
|
|
let mut base = format!("base|{}|{}|{}|{}\n", i.import, i.mount, i.sink, i.prefix);
|
|
for (m, original) in &i.original {
|
|
base += &format!("original|{m}|{original}\n");
|
|
}
|
|
for (m, r) in &i.base {
|
|
base += &format!("member|{m}|{}\n", r.line());
|
|
}
|
|
write(&dir.join(format!("management/{id}/base.model")), &base)?;
|
|
let map = i
|
|
.current
|
|
.iter()
|
|
.map(|(m, current)| {
|
|
format!("current|{m}|{}\n", current.as_deref().unwrap_or("DELETED"))
|
|
})
|
|
.collect::<String>();
|
|
write(&dir.join(format!("management/{id}/map.model")), &map)?;
|
|
let lineage = i
|
|
.lineage
|
|
.iter()
|
|
.map(|(original, current)| format!("lineage|{original}|{current}\n"))
|
|
.collect::<String>();
|
|
write(
|
|
&dir.join(format!("management/{id}/lineage.model")),
|
|
&lineage,
|
|
)?;
|
|
}
|
|
for r in doc.live.values() {
|
|
source += &format!("{}\n", r.line());
|
|
}
|
|
for receipt in &doc.receipts {
|
|
source += &format!("applied|{receipt}\n");
|
|
}
|
|
write(&dir.join("project.model"), &source)
|
|
}
|
|
|
|
fn declared(dir: &Path, logical: &str) -> Result<PathBuf> {
|
|
if logical.starts_with('/')
|
|
|| logical.contains('\\')
|
|
|| logical
|
|
.split('/')
|
|
.any(|p| p.is_empty() || p == "." || p == ".." || p.contains(':'))
|
|
{
|
|
return Err("nonportable logical path".into());
|
|
}
|
|
Ok(dir.join(logical))
|
|
}
|
|
fn load(dir: &Path) -> Result<Document> {
|
|
let mut doc = Document {
|
|
identity: String::new(),
|
|
revision: 0,
|
|
imports: BTreeMap::new(),
|
|
mounts: BTreeMap::new(),
|
|
instances: BTreeMap::new(),
|
|
live: BTreeMap::new(),
|
|
receipts: vec![],
|
|
};
|
|
for line in read(&dir.join("project.model"))?.lines() {
|
|
let r: Vec<_> = line.split('|').collect();
|
|
match r.as_slice() {
|
|
["document", id, rev] => {
|
|
doc.identity = (*id).into();
|
|
doc.revision = rev.parse().map_err(|_| "bad revision")?;
|
|
}
|
|
["import", alias, identity, version, digest, path] => {
|
|
let p = Package::parse(read(&declared(dir, path)?)?)?;
|
|
if p.digest != *digest || p.identity != *identity || p.version != *version {
|
|
return Err("package pin mismatch".into());
|
|
}
|
|
if doc.imports.insert((*alias).into(), p).is_some() {
|
|
return Err("duplicate import".into());
|
|
}
|
|
}
|
|
["mount", id, import, path] => {
|
|
let mut members = BTreeMap::new();
|
|
for line in read(&declared(dir, path)?)?.lines() {
|
|
let row: Vec<_> = line.split('|').collect();
|
|
let ["member", member, local] = row.as_slice() else {
|
|
return Err("bad mount member".into());
|
|
};
|
|
if members.insert((*member).into(), (*local).into()).is_some() {
|
|
return Err("duplicate mount member".into());
|
|
}
|
|
}
|
|
doc.mounts.insert(
|
|
(*id).into(),
|
|
Mount {
|
|
import: (*import).into(),
|
|
members,
|
|
},
|
|
);
|
|
}
|
|
["instance", id, path] => {
|
|
let mut i = Instance {
|
|
import: String::new(),
|
|
mount: String::new(),
|
|
sink: String::new(),
|
|
prefix: String::new(),
|
|
original: BTreeMap::new(),
|
|
current: BTreeMap::new(),
|
|
lineage: BTreeMap::new(),
|
|
base: BTreeMap::new(),
|
|
};
|
|
for line in read(&declared(dir, &format!("{path}/base.model"))?)?.lines() {
|
|
let row: Vec<_> = line.split('|').collect();
|
|
match row.as_slice() {
|
|
["base", import, mount, sink, prefix] => {
|
|
i.import = (*import).into();
|
|
i.mount = (*mount).into();
|
|
i.sink = (*sink).into();
|
|
i.prefix = (*prefix).into();
|
|
}
|
|
["original", m, local] => {
|
|
if i.original.insert((*m).into(), (*local).into()).is_some() {
|
|
return Err("duplicate original".into());
|
|
}
|
|
}
|
|
["member", m, ..] => {
|
|
let body = line.splitn(3, '|').nth(2).ok_or("bad member record")?;
|
|
if i.base.insert((*m).into(), Record::parse(body)?).is_some() {
|
|
return Err("duplicate base member".into());
|
|
}
|
|
}
|
|
_ => return Err("bad base row".into()),
|
|
}
|
|
}
|
|
for line in read(&declared(dir, &format!("{path}/map.model"))?)?.lines() {
|
|
let row: Vec<_> = line.split('|').collect();
|
|
let ["current", m, local] = row.as_slice() else {
|
|
return Err("bad current row".into());
|
|
};
|
|
if i.current
|
|
.insert((*m).into(), (*local != "DELETED").then(|| (*local).into()))
|
|
.is_some()
|
|
{
|
|
return Err("duplicate current row".into());
|
|
}
|
|
}
|
|
for line in read(&declared(dir, &format!("{path}/lineage.model"))?)?.lines() {
|
|
let row: Vec<_> = line.split('|').collect();
|
|
let ["lineage", old, new] = row.as_slice() else {
|
|
return Err("bad lineage row".into());
|
|
};
|
|
if i.lineage.insert((*old).into(), (*new).into()).is_some() {
|
|
return Err("duplicate lineage row".into());
|
|
}
|
|
}
|
|
doc.instances.insert((*id).into(), i);
|
|
}
|
|
["record", ..] => {
|
|
let record = Record::parse(line)?;
|
|
if doc.live.insert(record.id.clone(), record).is_some() {
|
|
return Err("duplicate live record".into());
|
|
}
|
|
}
|
|
["applied", ..] => doc.receipts.push(line[8..].into()),
|
|
_ => return Err("unknown normalized row".into()),
|
|
}
|
|
}
|
|
admit(&doc)?;
|
|
Ok(doc)
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
struct Transition {
|
|
layer: String,
|
|
records: BTreeMap<String, Definition>,
|
|
origin_pin: String,
|
|
target_pin: Option<String>,
|
|
origin_map: BTreeMap<String, String>,
|
|
mount: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
enum TargetRecord {
|
|
Function {
|
|
name: String,
|
|
},
|
|
Port {
|
|
owner: String,
|
|
direction: String,
|
|
accepted_item: String,
|
|
},
|
|
FunctionalExchange {
|
|
source: String,
|
|
target: String,
|
|
source_port: String,
|
|
target_port: String,
|
|
item: String,
|
|
},
|
|
}
|
|
fn target_architecture(transition: &Transition) -> Result<BTreeMap<String, TargetRecord>> {
|
|
let member = transition
|
|
.origin_map
|
|
.get("Alarm")
|
|
.ok_or("missing item correspondence")?;
|
|
let item = transition
|
|
.records
|
|
.get(member)
|
|
.ok_or("missing mapped item")?;
|
|
if transition.layer != "SA" || item.layer != "SA" || item.class != "Item" {
|
|
return Err("invalid SA item disposition".into());
|
|
}
|
|
let item = if transition.mount {
|
|
format!("sa_mount/{member}")
|
|
} else {
|
|
member.clone()
|
|
};
|
|
Ok(BTreeMap::from([
|
|
(
|
|
"sa/watch".into(),
|
|
TargetRecord::Function {
|
|
name: "Watch".into(),
|
|
},
|
|
),
|
|
(
|
|
"sa/receive".into(),
|
|
TargetRecord::Function {
|
|
name: "Receive left".into(),
|
|
},
|
|
),
|
|
(
|
|
"sa/watch_out".into(),
|
|
TargetRecord::Port {
|
|
owner: "sa/watch".into(),
|
|
direction: "Out".into(),
|
|
accepted_item: item.clone(),
|
|
},
|
|
),
|
|
(
|
|
"sa/receive_in".into(),
|
|
TargetRecord::Port {
|
|
owner: "sa/receive".into(),
|
|
direction: "In".into(),
|
|
accepted_item: item.clone(),
|
|
},
|
|
),
|
|
(
|
|
"sa/alarm".into(),
|
|
TargetRecord::FunctionalExchange {
|
|
source: "sa/watch".into(),
|
|
target: "sa/receive".into(),
|
|
source_port: "sa/watch_out".into(),
|
|
target_port: "sa/receive_in".into(),
|
|
item,
|
|
},
|
|
),
|
|
]))
|
|
}
|
|
fn derivative(package: &Package) -> Transition {
|
|
let members = package.definitions.values().filter(|d| d.layer == "OA");
|
|
let records = members
|
|
.map(|d| {
|
|
let mut copied = d.clone();
|
|
copied.layer = "SA".into();
|
|
copied.member = format!("SA/{}", d.member);
|
|
copied.refs = d
|
|
.refs
|
|
.iter()
|
|
.map(|r| {
|
|
if package.definitions[r].layer == "OA" {
|
|
format!("SA/{r}")
|
|
} else {
|
|
r.clone()
|
|
}
|
|
})
|
|
.collect();
|
|
(copied.member.clone(), copied)
|
|
})
|
|
.collect();
|
|
Transition {
|
|
layer: "SA".into(),
|
|
records,
|
|
origin_pin: package.digest.clone(),
|
|
target_pin: None,
|
|
origin_map: package
|
|
.definitions
|
|
.values()
|
|
.filter(|d| d.layer == "OA")
|
|
.map(|d| (d.member.clone(), format!("SA/{}", d.member)))
|
|
.collect(),
|
|
mount: false,
|
|
}
|
|
}
|
|
fn pinned_counterpart(
|
|
package: &Package,
|
|
target: &Package,
|
|
correspondence: &BTreeMap<String, String>,
|
|
) -> Result<Transition> {
|
|
let expected = derivative(package);
|
|
let records = target
|
|
.definitions
|
|
.values()
|
|
.filter(|d| d.layer == "SA")
|
|
.map(|d| (d.member.clone(), d.clone()))
|
|
.collect();
|
|
if expected.records != records || &expected.origin_map != correspondence {
|
|
return Err("incomplete/incompatible layer correspondence".into());
|
|
}
|
|
Ok(Transition {
|
|
layer: "SA".into(),
|
|
records,
|
|
origin_pin: package.digest.clone(),
|
|
target_pin: Some(target.digest.clone()),
|
|
origin_map: correspondence.clone(),
|
|
mount: true,
|
|
})
|
|
}
|
|
|
|
fn analysis_scope(
|
|
properties: &[&str],
|
|
checks: &[&str],
|
|
cases: &[&str],
|
|
external: &[&str],
|
|
) -> Result<BTreeSet<String>> {
|
|
let mut selected: BTreeSet<String> = properties
|
|
.iter()
|
|
.chain(checks)
|
|
.chain(cases)
|
|
.map(|s| (*s).into())
|
|
.collect();
|
|
let boundary: BTreeSet<_> = external.iter().copied().collect();
|
|
loop {
|
|
let old = selected.clone();
|
|
for id in &old {
|
|
let dependencies = match id.as_str() {
|
|
"P" => vec![],
|
|
"K" | "C" => vec!["P"],
|
|
"G" => vec!["P", "Q"],
|
|
"Q" => vec![],
|
|
_ => return Err("unknown analysis root".into()),
|
|
};
|
|
selected.extend(
|
|
dependencies
|
|
.into_iter()
|
|
.filter(|id| !boundary.contains(id))
|
|
.map(String::from),
|
|
);
|
|
}
|
|
if selected == old {
|
|
break;
|
|
}
|
|
}
|
|
if selected.contains("Q") {
|
|
return Err("outside property Q requires explicit subject/property boundary choice".into());
|
|
}
|
|
Ok(selected)
|
|
}
|
|
|
|
fn sha256(bytes: &[u8]) -> String {
|
|
const K: [u32; 64] = [
|
|
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
|
|
0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
|
|
0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
|
|
0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
|
|
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
|
|
0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
|
|
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
|
|
0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
|
|
0xc67178f2,
|
|
];
|
|
let mut h: [u32; 8] = [
|
|
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
|
|
0x5be0cd19,
|
|
];
|
|
let mut data = bytes.to_vec();
|
|
let bit_len = (data.len() as u64) * 8;
|
|
data.push(0x80);
|
|
while data.len() % 64 != 56 {
|
|
data.push(0);
|
|
}
|
|
data.extend(bit_len.to_be_bytes());
|
|
for chunk in data.chunks_exact(64) {
|
|
let mut w = [0u32; 64];
|
|
for (i, word) in chunk.chunks_exact(4).enumerate() {
|
|
w[i] = u32::from_be_bytes(word.try_into().expect("four bytes"));
|
|
}
|
|
for i in 16..64 {
|
|
let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
|
|
let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
|
|
w[i] = w[i - 16]
|
|
.wrapping_add(s0)
|
|
.wrapping_add(w[i - 7])
|
|
.wrapping_add(s1);
|
|
}
|
|
let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh] = h;
|
|
for i in 0..64 {
|
|
let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
|
|
let ch = (e & f) ^ (!e & g);
|
|
let t1 = hh
|
|
.wrapping_add(s1)
|
|
.wrapping_add(ch)
|
|
.wrapping_add(K[i])
|
|
.wrapping_add(w[i]);
|
|
let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
|
|
let maj = (a & b) ^ (a & c) ^ (b & c);
|
|
let t2 = s0.wrapping_add(maj);
|
|
hh = g;
|
|
g = f;
|
|
f = e;
|
|
e = d.wrapping_add(t1);
|
|
d = c;
|
|
c = b;
|
|
b = a;
|
|
a = t1.wrapping_add(t2);
|
|
}
|
|
for (state, value) in h.iter_mut().zip([a, b, c, d, e, f, g, hh]) {
|
|
*state = state.wrapping_add(value);
|
|
}
|
|
}
|
|
h.iter().map(|x| format!("{x:08x}")).collect()
|
|
}
|
|
|
|
fn main() -> Result<()> {
|
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
|
if args.first().is_some_and(|a| a == "--reopen") {
|
|
if args.len() != 3 {
|
|
return Err("usage: --reopen INPUT_DIRECTORY OUTPUT_DIRECTORY".into());
|
|
}
|
|
let doc = load(Path::new(&args[1]))?;
|
|
let budget = evaluate(&doc, "budget")?;
|
|
save(&doc, Path::new(&args[2]))?;
|
|
println!(
|
|
"Fresh process reopened {} records at revision {}; budget={} ms satisfied={}",
|
|
doc.live.len(),
|
|
doc.revision,
|
|
budget.0,
|
|
budget.1
|
|
);
|
|
return Ok(());
|
|
}
|
|
let root = PathBuf::from(
|
|
args.first()
|
|
.ok_or("provide the owned experiment output directory")?,
|
|
);
|
|
fs::create_dir_all(&root).map_err(|e| e.to_string())?;
|
|
let mut checks = 0usize;
|
|
macro_rules! check {
|
|
($condition:expr) => {{
|
|
checks += 1;
|
|
assert!($condition, "check {}: {}", checks, stringify!($condition));
|
|
}};
|
|
}
|
|
check!(sha256(b"") == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
|
|
check!(sha256(b"abc") == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
|
|
let raw = concat!(
|
|
"package|ControlPatterns|1.0.0\n",
|
|
"description|Monitor alarm\n",
|
|
"note|Initial documentation\n",
|
|
"def|Duration|Kind|Neutral|dimension=0,0,1,0,0,0,0;linear|-\n",
|
|
"def|Energy|Kind|Neutral|dimension=2,1,-2,0,0,0,0;linear|-\n",
|
|
"def|Torque|Kind|Neutral|dimension=2,1,-2,0,0,0,0;linear|-\n",
|
|
"def|ms|Unit|Neutral|millis=1|Duration\n",
|
|
"def|s|Unit|Neutral|millis=1000|Duration\n",
|
|
"def|Flag|Scalar|OA|Boolean|-\n",
|
|
"def|Alarm|Item|OA|Flow|Code\n",
|
|
"def|Code|Field|OA|minimum=1;maximum=1|Alarm,Flag\n",
|
|
"def|Interface|Interface|OA|-|Operation\n",
|
|
"def|Operation|Allocation|OA|order=0|Interface,Alarm\n",
|
|
"prototype|A|record|A|OA|Watch|Activity|Monitor alarm\n",
|
|
"prototype|E|record|E|OA|Alarm|Interaction|A|H|MOUNT/Alarm\n",
|
|
"prototype|P|record|P|OA|Elapsed|Property|A|SELF:Duration|750@SELF:ms\n",
|
|
"prototype|C|record|C|OA|Stress|Case|-|P=1250@SELF:ms\n",
|
|
"prototype|K|record|K|OA|Elapsed limit|Check|P|1000@SELF:ms\n",
|
|
"prototype|H|record|H|OA|Prototype receiver|Activity|Context role only\n",
|
|
"prototype|Q|record|Q|OA|Outside delay|Property|H|SELF:Duration|1000@SELF:ms\n",
|
|
"prototype|G|record|G|OA|Project-wide study|Case|-|P=2500@SELF:ms,Q=4000@SELF:ms\n",
|
|
"pattern|monitor|A,E,P,C,K\n",
|
|
"analysis|P|K|C\n",
|
|
"role|sink|H|Activity|OA\n"
|
|
);
|
|
let p1 = Package::parse(raw.into())?;
|
|
let p2 = Package::parse(
|
|
raw.replace("1.0.0", "1.0.1")
|
|
.replace("Initial documentation", "Revised documentation only"),
|
|
)?;
|
|
let p3 = Package::parse(
|
|
raw.replace("1.0.0", "2.0.0")
|
|
.replace("maximum=1", "maximum=2"),
|
|
)?;
|
|
let changed_kind =
|
|
Package::parse(raw.replace("dimension=0,0,1,0,0,0,0", "dimension=1,0,0,0,0,0,0"))?;
|
|
let other = Package::parse(raw.replace("ControlPatterns", "OtherVocabulary"))?;
|
|
check!(p1.digest != p2.digest && p1.raw != p2.raw);
|
|
for member in p1.definitions.keys() {
|
|
check!(equivalent(&p1, member, &p2, member));
|
|
}
|
|
check!(!equivalent(&p1, "Duration", &p1, "Energy"));
|
|
check!(!equivalent(&p1, "Energy", &p1, "Torque"));
|
|
check!(!equivalent(&p1, "Duration", &other, "Duration"));
|
|
check!(!equivalent(&p1, "Duration", &changed_kind, "Duration"));
|
|
check!(!equivalent(&p1, "Interface", &p3, "Interface"));
|
|
check!(equivalent(&p1, "Duration", &p3, "Duration"));
|
|
let packages = BTreeMap::from([("D1".into(), p1.clone()), ("D2".into(), p2.clone())]);
|
|
let request = "new|Alpha\nimport|controls1|D1\nmount|data1|controls1\nactivity|sink_left|Receive left\nactivity|sink_right|Receive right\ninstantiate|left|controls1|data1|sink_left\ninstantiate|right|controls1|data1|sink_right\nbudget|budget|left/P|right/P|controls1\nconsumer|outside|left/E|data1\nglobal_case|global|left/P\nreceipt|create-two-instances\n";
|
|
write(&root.join("initial.request"), request)?;
|
|
let initial = prepare_new(&read(&root.join("initial.request"))?, &packages)?;
|
|
let expected_initial_rows = [
|
|
"record|sink_left|OA|Receive left|Activity|",
|
|
"record|sink_right|OA|Receive right|Activity|",
|
|
"record|left/A|OA|left Watch|Activity|Monitor alarm",
|
|
"record|left/E|OA|left Alarm|Interaction|left/A|sink_left|data1/Alarm",
|
|
"record|left/P|OA|left Elapsed|Property|left/A|controls1:Duration|750@controls1:ms",
|
|
"record|left/C|OA|left Stress|Case|-|left/P=1250@controls1:ms",
|
|
"record|left/K|OA|left Elapsed limit|Check|left/P|1000@controls1:ms",
|
|
"record|right/A|OA|right Watch|Activity|Monitor alarm",
|
|
"record|right/E|OA|right Alarm|Interaction|right/A|sink_right|data1/Alarm",
|
|
"record|right/P|OA|right Elapsed|Property|right/A|controls1:Duration|750@controls1:ms",
|
|
"record|right/C|OA|right Stress|Case|-|right/P=1250@controls1:ms",
|
|
"record|right/K|OA|right Elapsed limit|Check|right/P|1000@controls1:ms",
|
|
"record|budget|OA|Combined duration|Budget|left/P|right/P|controls1:Duration|2000@controls1:ms",
|
|
"record|outside|OA|Outside typed receiver|Consumer|left/E|data1/Interface|data1/Operation|data1/Code",
|
|
"record|global|OA|Unrelated project-wide study|Case|-|left/P=3000@controls1:ms",
|
|
];
|
|
let expected_initial_live: BTreeMap<_, _> = expected_initial_rows
|
|
.iter()
|
|
.map(|line| Record::parse(line).map(|r| (r.id.clone(), r)))
|
|
.collect::<Result<_>>()?;
|
|
check!(initial.live == expected_initial_live);
|
|
write(
|
|
&root.join("expected-initial.model"),
|
|
&(expected_initial_rows.join("\n") + "\n"),
|
|
)?;
|
|
check!(initial.live.len() == 15);
|
|
check!(initial.instances["left"].current.values().all(|id| {
|
|
!initial.instances["right"]
|
|
.current
|
|
.values()
|
|
.any(|other| id == other)
|
|
}));
|
|
check!(evaluate(&initial, "budget")? == (1500, true));
|
|
save(&initial, &root.join("01-initial"))?;
|
|
check!(load(&root.join("01-initial"))? == initial);
|
|
save(
|
|
&load(&root.join("01-initial"))?,
|
|
&root.join("01-recompiled"),
|
|
)?;
|
|
check!(
|
|
read(&root.join("01-initial/project.model"))?
|
|
== read(&root.join("01-recompiled/project.model"))?
|
|
);
|
|
write(
|
|
&root.join("local-edit.request"),
|
|
"edit_base|left/P|900@controls1:ms\n",
|
|
)?;
|
|
let edited = apply_edit(&initial, &read(&root.join("local-edit.request"))?)?;
|
|
check!(edited.instances == initial.instances);
|
|
check!(evaluate(&edited, "budget")? == (1650, true));
|
|
save(&edited, &root.join("02-edited"))?;
|
|
let request = format!(
|
|
"update|left|{}|controls2|data2|2|P.base=keep_local\n",
|
|
p1.digest
|
|
);
|
|
write(&root.join("selective-update.request"), &request)?;
|
|
check!(
|
|
update(
|
|
&edited,
|
|
&request.replace("P.base=keep_local", "unresolved"),
|
|
p2.clone()
|
|
)
|
|
.is_err()
|
|
);
|
|
check!(update(&edited, &request.replace("|2|", "|1|"), p2.clone()).is_err());
|
|
let updated = update(
|
|
&edited,
|
|
&read(&root.join("selective-update.request"))?,
|
|
p2.clone(),
|
|
)?;
|
|
check!(evaluate_policy(&initial, "budget", &Policy::Strict)? == (1500, true));
|
|
check!(consumer_match_policy(&initial, "outside", &Policy::Strict).is_ok());
|
|
check!(evaluate_policy(&updated, "budget", &Policy::Strict).is_err());
|
|
check!(consumer_match_policy(&updated, "outside", &Policy::Strict).is_err());
|
|
check!(evaluate(&updated, "budget")? == (1650, true));
|
|
check!(evaluate_check(&updated, "left/K", None)? == (900, true));
|
|
check!(evaluate_check(&updated, "left/K", Some("left/C"))? == (1250, false));
|
|
check!(evaluate_check(&updated, "left/K", Some("global"))? == (3000, false));
|
|
check!(evaluate_check(&updated, "right/K", Some("right/C"))? == (1250, false));
|
|
check!(updated.instances["right"] == edited.instances["right"]);
|
|
check!(updated.live["right/P"] == edited.live["right/P"]);
|
|
for id in ["outside", "budget", "global", "sink_left", "sink_right"] {
|
|
check!(updated.live[id] == edited.live[id]);
|
|
}
|
|
for (member, current) in &edited.instances["left"].current {
|
|
check!(updated.instances["left"].current[member] == *current);
|
|
}
|
|
let Body::Property { kind, base, .. } = &updated.live["left/P"].body else {
|
|
unreachable!()
|
|
};
|
|
check!(kind == &DRef::new("controls2", "Duration"));
|
|
check!(base == &Literal::new(900, "controls1", "ms"));
|
|
let Body::Property { base, .. } = &updated.instances["left"].base["P"].body else {
|
|
unreachable!()
|
|
};
|
|
check!(base == &Literal::new(750, "controls2", "ms"));
|
|
check!(updated.instances["left"].import == "controls2");
|
|
check!(
|
|
consumer_match(&updated, "outside")?
|
|
== BTreeMap::from([
|
|
("data1/Alarm".into(), "data2/Alarm".into()),
|
|
("data1/Code".into(), "data2/Code".into()),
|
|
("data1/Flag".into(), "data2/Flag".into()),
|
|
("data1/Interface".into(), "data2/Interface".into()),
|
|
("data1/Operation".into(), "data2/Operation".into())
|
|
])
|
|
);
|
|
save(&updated, &root.join("03-updated"))?;
|
|
check!(load(&root.join("03-updated"))? == updated);
|
|
// Independently authored expectations: no materialize()/update() call supplies these rows.
|
|
let expected_rows = [
|
|
"record|left/P|OA|left Elapsed|Property|left/A|controls2:Duration|900@controls1:ms",
|
|
"record|left/E|OA|left Alarm|Interaction|left/A|sink_left|data2/Alarm",
|
|
"record|left/C|OA|left Stress|Case|-|left/P=1250@controls2:ms",
|
|
"record|left/K|OA|left Elapsed limit|Check|left/P|1000@controls2:ms",
|
|
"record|right/P|OA|right Elapsed|Property|right/A|controls1:Duration|750@controls1:ms",
|
|
"record|budget|OA|Combined duration|Budget|left/P|right/P|controls1:Duration|2000@controls1:ms",
|
|
"record|outside|OA|Outside typed receiver|Consumer|left/E|data1/Interface|data1/Operation|data1/Code",
|
|
];
|
|
for line in expected_rows {
|
|
let expected = Record::parse(line)?;
|
|
check!(updated.live[&expected.id] == expected);
|
|
}
|
|
write(
|
|
&root.join("expected-key-records.model"),
|
|
&(expected_rows.join("\n") + "\n"),
|
|
)?;
|
|
// Full independent graph oracle, including every accepted baseline and management map.
|
|
let mut expected_live = expected_initial_live;
|
|
for line in expected_rows {
|
|
let r = Record::parse(line)?;
|
|
expected_live.insert(r.id.clone(), r);
|
|
}
|
|
let expected_mounts = BTreeMap::from([
|
|
(
|
|
"data1".into(),
|
|
Mount {
|
|
import: "controls1".into(),
|
|
members: BTreeMap::from([
|
|
("Alarm".into(), "data1/Alarm".into()),
|
|
("Code".into(), "data1/Code".into()),
|
|
("Flag".into(), "data1/Flag".into()),
|
|
("Interface".into(), "data1/Interface".into()),
|
|
("Operation".into(), "data1/Operation".into()),
|
|
]),
|
|
},
|
|
),
|
|
(
|
|
"data2".into(),
|
|
Mount {
|
|
import: "controls2".into(),
|
|
members: BTreeMap::from([
|
|
("Alarm".into(), "data2/Alarm".into()),
|
|
("Code".into(), "data2/Code".into()),
|
|
("Flag".into(), "data2/Flag".into()),
|
|
("Interface".into(), "data2/Interface".into()),
|
|
("Operation".into(), "data2/Operation".into()),
|
|
]),
|
|
},
|
|
),
|
|
]);
|
|
let mut expected_instances = BTreeMap::new();
|
|
for (prefix, import, mount, sink) in [
|
|
("left", "controls2", "data2", "sink_left"),
|
|
("right", "controls1", "data1", "sink_right"),
|
|
] {
|
|
let original: BTreeMap<String, String> = ["A", "C", "E", "K", "P"]
|
|
.into_iter()
|
|
.map(|m| (m.into(), format!("{prefix}/{m}")))
|
|
.collect();
|
|
let mut base: BTreeMap<String, Record> = original
|
|
.iter()
|
|
.map(|(m, id)| (m.clone(), expected_live[id].clone()))
|
|
.collect();
|
|
if prefix == "left" {
|
|
base.insert("P".into(), Record::parse("record|left/P|OA|left Elapsed|Property|left/A|controls2:Duration|750@controls2:ms")?);
|
|
}
|
|
expected_instances.insert(
|
|
prefix.into(),
|
|
Instance {
|
|
import: import.into(),
|
|
mount: mount.into(),
|
|
sink: sink.into(),
|
|
prefix: prefix.into(),
|
|
current: original
|
|
.iter()
|
|
.map(|(m, id)| (m.clone(), Some(id.clone())))
|
|
.collect(),
|
|
lineage: original
|
|
.values()
|
|
.map(|id| (id.clone(), id.clone()))
|
|
.collect(),
|
|
original,
|
|
base,
|
|
},
|
|
);
|
|
}
|
|
let expected_full = Document {
|
|
identity: "Alpha".into(),
|
|
revision: 3,
|
|
imports: BTreeMap::from([
|
|
("controls1".into(), p1.clone()),
|
|
("controls2".into(), p2.clone()),
|
|
]),
|
|
mounts: expected_mounts,
|
|
instances: expected_instances,
|
|
live: expected_live,
|
|
receipts: vec![
|
|
"create-two-instances".into(),
|
|
"edit_base|left/P|900@controls1:ms".into(),
|
|
request.trim().into(),
|
|
],
|
|
};
|
|
check!(updated == expected_full);
|
|
save(&expected_full, &root.join("expected-full-updated"))?;
|
|
// Unchanged exact identity remains a stricter relation than compatibility.
|
|
check!(updated.imports["controls1"].digest != updated.imports["controls2"].digest);
|
|
check!(DRef::new("controls1", "Duration") != DRef::new("controls2", "Duration"));
|
|
check!(updated.mounts["data1"].members["Alarm"] != updated.mounts["data2"].members["Alarm"]);
|
|
check!(update(&edited, &request, p3.clone()).is_err());
|
|
let mut forged = updated.clone();
|
|
forged
|
|
.instances
|
|
.get_mut("left")
|
|
.unwrap()
|
|
.base
|
|
.get_mut("P")
|
|
.unwrap()
|
|
.name = "Forged history".into();
|
|
check!(admit(&forged).is_err());
|
|
let mut forged = updated.clone();
|
|
forged.imports.get_mut("controls1").unwrap().raw.push(' ');
|
|
check!(admit(&forged).is_err());
|
|
let mut forged_parsed = updated.clone();
|
|
forged_parsed
|
|
.imports
|
|
.get_mut("controls1")
|
|
.unwrap()
|
|
.definitions
|
|
.get_mut("Alarm")
|
|
.unwrap()
|
|
.layer = "SA".into();
|
|
check!(admit(&forged_parsed).is_err());
|
|
let mut label_collision = updated.clone();
|
|
label_collision.imports.insert(
|
|
"conflicting_version".into(),
|
|
Package::parse(
|
|
p1.raw
|
|
.replace("Initial documentation", "Another revision claiming 1.0.0"),
|
|
)?,
|
|
);
|
|
check!(admit(&label_collision).is_err());
|
|
let mut collision = updated.clone();
|
|
collision
|
|
.instances
|
|
.get_mut("left")
|
|
.unwrap()
|
|
.current
|
|
.insert("P".into(), Some("right/P".into()));
|
|
check!(admit(&collision).is_err());
|
|
let mut missing = updated.clone();
|
|
missing.imports.remove("controls1");
|
|
check!(admit(&missing).is_err());
|
|
let mut other_local_edits = edited.clone();
|
|
let property = other_local_edits.live.get_mut("left/P").unwrap();
|
|
property.name = "Local measurement name".into();
|
|
let Body::Property { subject, .. } = &mut property.body else {
|
|
unreachable!()
|
|
};
|
|
*subject = "sink_left".into();
|
|
let twice_edited = update(&other_local_edits, &request, p2.clone())?;
|
|
check!(twice_edited.live["left/P"].name == "Local measurement name");
|
|
let Body::Property { subject, .. } = &twice_edited.live["left/P"].body else {
|
|
unreachable!()
|
|
};
|
|
check!(subject == "sink_left");
|
|
let mut extended = edited.clone();
|
|
extended.live.insert(
|
|
"local_extra".into(),
|
|
Record {
|
|
id: "local_extra".into(),
|
|
layer: "OA".into(),
|
|
name: "left Watch".into(),
|
|
body: Body::Activity {
|
|
description: "Independent extension".into(),
|
|
},
|
|
},
|
|
);
|
|
let extended_update = update(&extended, &request, p2.clone())?;
|
|
check!(extended_update.live["local_extra"] == extended.live["local_extra"]);
|
|
let mut dangling = edited.clone();
|
|
dangling.live.remove("left/E");
|
|
dangling
|
|
.instances
|
|
.get_mut("left")
|
|
.unwrap()
|
|
.current
|
|
.insert("E".into(), None);
|
|
check!(admit(&dangling).is_err());
|
|
// Explicit reidentify changes live references/current map/lineage, never base or foreign origins.
|
|
let mut reidentified = edited.clone();
|
|
let mut moved = reidentified.live.remove("left/A").unwrap();
|
|
moved.remap_live("left/A", "left/A2");
|
|
reidentified.live.insert(moved.id.clone(), moved);
|
|
for record in reidentified.live.values_mut() {
|
|
record.remap_live("left/A", "left/A2");
|
|
}
|
|
let i = reidentified.instances.get_mut("left").unwrap();
|
|
i.current.insert("A".into(), Some("left/A2".into()));
|
|
i.lineage.insert("left/A".into(), "left/A2".into());
|
|
check!(admit(&reidentified).is_ok());
|
|
check!(reidentified.instances["left"].base == edited.instances["left"].base);
|
|
check!(reidentified.imports == edited.imports);
|
|
let mut reused = reidentified.clone();
|
|
reused.live.insert(
|
|
"left/A".into(),
|
|
Record {
|
|
id: "left/A".into(),
|
|
layer: "OA".into(),
|
|
name: "New object".into(),
|
|
body: Body::Activity {
|
|
description: String::new(),
|
|
},
|
|
},
|
|
);
|
|
check!(admit(&reused).is_err());
|
|
save(&reidentified, &root.join("04-reidentified"))?;
|
|
check!(load(&root.join("04-reidentified"))? == reidentified);
|
|
let reupdated = update(&reidentified, &request, p2.clone())?;
|
|
check!(reupdated.live.contains_key("left/A2") && !reupdated.live.contains_key("left/A"));
|
|
// A deleted member remains reserved; no implicit resurrection or name adoption.
|
|
let mut deleted = edited.clone();
|
|
deleted.live.remove("left/K");
|
|
deleted
|
|
.instances
|
|
.get_mut("left")
|
|
.unwrap()
|
|
.current
|
|
.insert("K".into(), None);
|
|
check!(admit(&deleted).is_ok());
|
|
check!(update(&deleted, &request, p2.clone()).is_err());
|
|
deleted.live.insert(
|
|
"left/K".into(),
|
|
Record {
|
|
id: "left/K".into(),
|
|
layer: "OA".into(),
|
|
name: "Same-looking new check".into(),
|
|
body: Body::Check {
|
|
property: "right/P".into(),
|
|
limit: Literal::new(1000, "controls1", "ms"),
|
|
},
|
|
},
|
|
);
|
|
check!(admit(&deleted).is_err());
|
|
// Subjectless C/K are included only by explicit selection; G is never inverse-pulled.
|
|
check!(analysis_scope(&["P"], &[], &[], &[])? == BTreeSet::from(["P".into()]));
|
|
check!(
|
|
analysis_scope(&["P"], &["K"], &["C"], &[])?
|
|
== BTreeSet::from(["P".into(), "K".into(), "C".into()])
|
|
);
|
|
check!(analysis_scope(&[], &[], &["G"], &[]).is_err());
|
|
check!(analysis_scope(&[], &[], &["G"], &["Q"])? == BTreeSet::from(["G".into(), "P".into()]));
|
|
// Layer transformation has independent provenance and mutability, not implicit compatibility.
|
|
let sa = derivative(&p2);
|
|
check!(!sa.mount && sa.layer == "SA" && sa.origin_pin == p2.digest);
|
|
check!(sa.records.len() == 5 && sa.origin_map.len() == 5);
|
|
let mut sa_raw =
|
|
String::from("package|ControlPatternsSA|1.0.1\ndescription|Explicit SA counterpart\n");
|
|
for d in sa.records.values() {
|
|
sa_raw += &format!(
|
|
"def|{}|{}|{}|{}|{}\n",
|
|
d.member,
|
|
d.class,
|
|
d.layer,
|
|
d.fields.join(","),
|
|
d.refs.join(",")
|
|
);
|
|
}
|
|
let sa_package = Package::parse(sa_raw)?;
|
|
let pinned = pinned_counterpart(&p2, &sa_package, &sa.origin_map)?;
|
|
check!(
|
|
pinned.mount
|
|
&& pinned.records == sa.records
|
|
&& pinned.target_pin == Some(sa_package.digest.clone())
|
|
);
|
|
check!(!equivalent(&p2, "Alarm", &sa_package, "SA/Alarm"));
|
|
let mut incomplete = sa.origin_map.clone();
|
|
incomplete.remove("Code");
|
|
check!(pinned_counterpart(&p2, &sa_package, &incomplete).is_err());
|
|
let mut refined = sa.clone();
|
|
refined.records.get_mut("SA/Code").unwrap().fields = vec!["minimum=1;maximum=2".into()];
|
|
let refined_package = Package::parse(sa_package.raw.replace("maximum=1", "maximum=2"))?;
|
|
check!(pinned_counterpart(&p2, &refined_package, &sa.origin_map).is_err());
|
|
check!(derivative(&p2).records == sa.records); // Explicit reverse BindPinned may reuse the original unchanged OA contract.
|
|
check!(derivative(&p2).records != refined.records); // Reverse transition of an edited derivative must not mutate the original mount.
|
|
let pinned_architecture = target_architecture(&pinned)?;
|
|
let local_architecture = target_architecture(&sa)?;
|
|
check!(pinned_architecture.len() == 5 && local_architecture.len() == 5);
|
|
check!(
|
|
pinned_architecture["sa/alarm"]
|
|
== TargetRecord::FunctionalExchange {
|
|
source: "sa/watch".into(),
|
|
target: "sa/receive".into(),
|
|
source_port: "sa/watch_out".into(),
|
|
target_port: "sa/receive_in".into(),
|
|
item: "sa_mount/SA/Alarm".into()
|
|
}
|
|
);
|
|
check!(
|
|
local_architecture["sa/alarm"]
|
|
== TargetRecord::FunctionalExchange {
|
|
source: "sa/watch".into(),
|
|
target: "sa/receive".into(),
|
|
source_port: "sa/watch_out".into(),
|
|
target_port: "sa/receive_in".into(),
|
|
item: "SA/Alarm".into()
|
|
}
|
|
);
|
|
let mut wrong_layer = sa.clone();
|
|
wrong_layer.records.get_mut("SA/Alarm").unwrap().layer = "OA".into();
|
|
check!(target_architecture(&wrong_layer).is_err());
|
|
check!(sa == derivative(&p2)); // Original source/package revisions have not mutated the captured derivative.
|
|
check!(derivative(&p3).records != sa.records);
|
|
let mut illegal = p2.clone();
|
|
illegal.definitions.get_mut("Alarm").unwrap().layer = "SA".into();
|
|
check!(!equivalent(&p2, "Alarm", &illegal, "Alarm"));
|
|
write(
|
|
&root.join("oa-sa-transition.model"),
|
|
&format!(
|
|
"pinned-counterpart={pinned:#?}\npinned-architecture={pinned_architecture:#?}\nlocal-derivative={sa:#?}\nlocal-architecture={local_architecture:#?}\nrefined-derivative={refined:#?}\n"
|
|
),
|
|
)?;
|
|
// Checked identity bridges are scoped direct witnesses, not unverified flags or dimension casts.
|
|
let bridge = |a: &Package, am: &str, b: &Package, bm: &str| -> Result<String> {
|
|
if equivalent(a, am, b, bm) {
|
|
Ok(format!(
|
|
"checked-same-origin|{}|{am}|{}|{bm}",
|
|
a.digest, b.digest
|
|
))
|
|
} else {
|
|
Err("bridge proof failed".into())
|
|
}
|
|
};
|
|
check!(bridge(&p1, "Duration", &p2, "Duration").is_ok());
|
|
check!(bridge(&p1, "Interface", &p2, "Interface").is_ok());
|
|
check!(bridge(&p1, "Energy", &p1, "Torque").is_err());
|
|
check!(bridge(&p1, "Interface", &p3, "Interface").is_err());
|
|
let witnesses = BTreeSet::from([
|
|
pair(
|
|
&format!("{}:Duration", p1.digest),
|
|
&format!("{}:Duration", p2.digest),
|
|
),
|
|
pair(
|
|
&format!("{}:Interface", p1.digest),
|
|
&format!("{}:Interface", p2.digest),
|
|
),
|
|
]);
|
|
let explicit_policy = Policy::Bridges(witnesses);
|
|
check!(evaluate_policy(&updated, "budget", &explicit_policy)? == (1650, true));
|
|
check!(consumer_match_policy(&updated, "outside", &explicit_policy).is_ok());
|
|
check!(evaluate_policy(&updated, "budget", &Policy::Bridges(BTreeSet::new())).is_err());
|
|
check!(consumer_match_policy(&updated, "outside", &Policy::Bridges(BTreeSet::new())).is_err());
|
|
// Qualified layered definitions have the same compatibility/layer obligations without materialized IDs.
|
|
check!(policy_matches(
|
|
&p1,
|
|
"Interface",
|
|
&p2,
|
|
"Interface",
|
|
&Policy::SameOrigin
|
|
));
|
|
check!(!policy_matches(
|
|
&p1,
|
|
"Interface",
|
|
&p2,
|
|
"Interface",
|
|
&Policy::Strict
|
|
));
|
|
check!(p2.definitions["Alarm"].layer != "SA");
|
|
check!(sa_package.definitions["SA/Alarm"].layer == "SA");
|
|
let bridge_text = format!(
|
|
"{}\n{}\n",
|
|
bridge(&p1, "Duration", &p2, "Duration")?,
|
|
bridge(&p1, "Interface", &p2, "Interface")?
|
|
);
|
|
write(&root.join("explicit-bridges.model"), &bridge_text)?;
|
|
// Stable vocabulary separates pattern bytes from definition provenance, but vocabulary changes still need the same decision.
|
|
let vocabulary_pin = p1.digest.clone();
|
|
let pattern1 = ("PatternRevision1", vocabulary_pin.clone());
|
|
let pattern2 = ("PatternRevision2", vocabulary_pin.clone());
|
|
check!(pattern1.0 != pattern2.0 && pattern1.1 == pattern2.1);
|
|
check!(pattern1.1 != p3.digest);
|
|
let results = format!(
|
|
"# Finite-model results\n\nAssertions passed: {checks}\n\nInitial budget: 1500 ms, satisfied.\nAfter one local base edit: 1650 ms, satisfied.\nAfter selected cosmetic update: 1650 ms, satisfied; exact pins differ.\nOutside budget/consumer/global case: exact records unchanged.\nUpdated left baseline: 750 controls2:ms; current: 900 controls1:ms; declared kind controls2:Duration.\nRight instance/base/map: exact original.\nContract path projection: data1/Code -> data2/Code and data1/Operation -> data2/Operation.\nChanged field cardinality: selective candidate rejected.\nStrict revision identity: old/new refs unequal.\nSame-origin typed compatibility: cosmetic revision accepted; Energy/Torque, other origin, changed contract rejected.\nBridge alternative: 2 explicit validated direct witnesses; changed contracts rejected.\nStable vocabulary alternative: exact common pin survives pattern change; vocabulary change remains unresolved without compatibility/migration.\nSubjectless explicit C+K selected; unrelated G omitted; selected G requires Q boundary.\nOA/SA: explicit pinned counterpart or independent local derivative; refined reverse counterpart fails exact transformed contract.\n\nD1: {}\nD2: {}\nD3: {}\n\nThis dialect uses opaque symbolic identities, a restricted AST, one package per import and scalar integer-millisecond arithmetic. It is not Studio source, a full package dependency solver, production UUID allocation, complete data/operation/transition admission, REST or native acceptance.\n",
|
|
p1.digest, p2.digest, p3.digest
|
|
);
|
|
write(&root.join("RESULTS.md"), &results)?;
|
|
write(&root.join("package-D1.library"), &p1.raw)?;
|
|
write(&root.join("package-D2.library"), &p2.raw)?;
|
|
write(&root.join("package-D3.library"), &p3.raw)?;
|
|
write(&root.join("package-SA.library"), &sa_package.raw)?;
|
|
println!(
|
|
"{checks} assertions passed\nD1={}\nD2={}\nD3={}",
|
|
p1.digest, p2.digest, p3.digest
|
|
);
|
|
Ok(())
|
|
}
|