438 lines
13 KiB
Rust
438 lines
13 KiB
Rust
//! Design probe only: no production schema changes, no execution semantics.
|
|
//! Compile directly against an existing oa_core rlib; see the adjacent design.
|
|
use oa_core::*;
|
|
use std::collections::{BTreeMap, BTreeSet};
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
struct Endpoint {
|
|
references: Vec<Id>,
|
|
occurrence: Id,
|
|
}
|
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
struct Address {
|
|
owner: Id,
|
|
endpoint: Endpoint,
|
|
}
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
struct Resolved {
|
|
address: Address,
|
|
work: Option<Id>,
|
|
}
|
|
#[derive(Clone, Debug)]
|
|
enum Purpose {
|
|
Precedence { guard: String },
|
|
Exchange { exchange: Id },
|
|
CommonWork { work: Id },
|
|
}
|
|
|
|
struct Record {
|
|
parent: Option<Id>,
|
|
layer: Layer,
|
|
kind: Kind,
|
|
reference: Option<Id>,
|
|
work: Option<Id>,
|
|
contribution: bool,
|
|
}
|
|
struct Index<'a> {
|
|
project: &'a Project,
|
|
records: BTreeMap<Id, Record>,
|
|
cache: BTreeMap<Address, Resolved>,
|
|
max_depth: usize,
|
|
cache_capacity: usize,
|
|
}
|
|
impl<'a> Index<'a> {
|
|
fn new(project: &'a Project, max_depth: usize, cache_capacity: usize) -> Self {
|
|
let records = project
|
|
.elements
|
|
.values()
|
|
.map(|e| {
|
|
let (reference, work, contribution) = match e.shared() {
|
|
Some(SharedBody::Occurrence { body, .. }) => match body {
|
|
OccurrenceBody::SequenceReference { sequence } => (*sequence, None, true),
|
|
OccurrenceBody::Action { action, .. } => (None, *action, true),
|
|
OccurrenceBody::Exchange { .. } | OccurrenceBody::Control { .. } => {
|
|
(None, None, true)
|
|
}
|
|
_ => (None, None, false),
|
|
},
|
|
_ => (None, None, false),
|
|
};
|
|
(
|
|
e.id,
|
|
Record {
|
|
parent: e.parent,
|
|
layer: e.layer(),
|
|
kind: e.kind(),
|
|
reference,
|
|
work,
|
|
contribution,
|
|
},
|
|
)
|
|
})
|
|
.collect();
|
|
Self {
|
|
project,
|
|
records,
|
|
cache: BTreeMap::new(),
|
|
max_depth,
|
|
cache_capacity,
|
|
}
|
|
}
|
|
fn resolve(&mut self, owner: Id, endpoint: &Endpoint) -> Result<Resolved, &'static str> {
|
|
if endpoint.references.len() > self.max_depth {
|
|
return Err("reference depth limit");
|
|
}
|
|
let address = Address {
|
|
owner,
|
|
endpoint: endpoint.clone(),
|
|
};
|
|
if let Some(resolved) = self.cache.get(&address) {
|
|
return Ok(resolved.clone());
|
|
}
|
|
let root = self.records.get(&owner).ok_or("missing owner")?;
|
|
if !matches!(root.kind, Kind::Process | Kind::FunctionalChain) {
|
|
return Err("wrong owner kind");
|
|
}
|
|
let mut current = owner;
|
|
let mut visited = BTreeSet::from([owner]);
|
|
for step in &endpoint.references {
|
|
let reference = self.records.get(step).ok_or("missing path component")?;
|
|
if reference.parent != Some(current) || reference.layer != root.layer {
|
|
return Err("path is not direct in this context");
|
|
}
|
|
let next = reference
|
|
.reference
|
|
.ok_or("path component is not a resolved sequence reference")?;
|
|
let target = self
|
|
.records
|
|
.get(&next)
|
|
.ok_or("missing referenced sequence")?;
|
|
if target.layer != root.layer || target.kind != root.kind {
|
|
return Err("incompatible referenced sequence");
|
|
}
|
|
if !visited.insert(next) {
|
|
return Err("recursive sequence reuse");
|
|
}
|
|
current = next;
|
|
}
|
|
let leaf = self
|
|
.records
|
|
.get(&endpoint.occurrence)
|
|
.ok_or("missing leaf")?;
|
|
if leaf.parent != Some(current) || leaf.layer != root.layer || !leaf.contribution {
|
|
return Err("leaf is not a direct contribution of the resolved owner");
|
|
}
|
|
let resolved = Resolved {
|
|
address: address.clone(),
|
|
work: leaf.work,
|
|
};
|
|
// Cache residency cannot change the validity of an authored address.
|
|
// The production implementation can use LRU; deterministic eviction is
|
|
// enough to falsify the earlier history-dependent admission mistake.
|
|
if self.cache_capacity > 0 {
|
|
if self.cache.len() >= self.cache_capacity {
|
|
self.cache.pop_first();
|
|
}
|
|
self.cache.insert(address, resolved.clone());
|
|
}
|
|
Ok(resolved)
|
|
}
|
|
fn resolve_many(
|
|
&mut self,
|
|
owner: Id,
|
|
endpoints: &[Endpoint],
|
|
max_unique: usize,
|
|
) -> Result<Vec<Resolved>, &'static str> {
|
|
let unique: BTreeSet<_> = endpoints.iter().collect();
|
|
if unique.len() > max_unique {
|
|
return Err("analysis address limit");
|
|
}
|
|
endpoints
|
|
.iter()
|
|
.map(|endpoint| self.resolve(owner, endpoint))
|
|
.collect()
|
|
}
|
|
fn connection(
|
|
&mut self,
|
|
owner: Id,
|
|
source: &Endpoint,
|
|
target: &Endpoint,
|
|
purpose: &Purpose,
|
|
) -> Result<(), &'static str> {
|
|
let a = self.resolve(owner, source)?;
|
|
let b = self.resolve(owner, target)?;
|
|
if a.address == b.address {
|
|
return Err("same contextual occurrence");
|
|
}
|
|
match purpose {
|
|
Purpose::Precedence { guard } => {
|
|
let _opaque_guard = guard;
|
|
Ok(())
|
|
}
|
|
Purpose::CommonWork { work } if a.work == Some(*work) && b.work == Some(*work) => {
|
|
Ok(())
|
|
}
|
|
Purpose::CommonWork { .. } => Err("common work does not match both contributions"),
|
|
Purpose::Exchange { exchange } => {
|
|
let relation = self
|
|
.project
|
|
.relations
|
|
.get(exchange)
|
|
.ok_or("missing exchange")?;
|
|
if !matches!(
|
|
relation.kind(),
|
|
RelationKind::Interaction | RelationKind::FunctionalExchange
|
|
) || a.work != Some(relation.source())
|
|
|| b.work != Some(relation.target())
|
|
{
|
|
return Err("exchange endpoints do not match contextual work");
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
fn id(value: u128) -> Id {
|
|
Id::from_u128(value)
|
|
}
|
|
fn element(p: &mut Project, n: u128, kind: Kind, layer: Layer, parent: Option<Id>) -> Id {
|
|
let mut e = p
|
|
.make_element(kind, layer, &format!("item_{n}"), parent)
|
|
.unwrap();
|
|
e.id = id(n);
|
|
p.elements.insert(e.id, e);
|
|
id(n)
|
|
}
|
|
fn occurrence(p: &mut Project, n: u128, layer: Layer, owner: Id, body: OccurrenceBody) -> Id {
|
|
let key = element(p, n, Kind::Occurrence, layer, Some(owner));
|
|
*p.elements.get_mut(&key).unwrap().shared_mut().unwrap() = SharedBody::Occurrence {
|
|
body,
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
};
|
|
p.elements
|
|
.get_mut(&owner)
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.push(key);
|
|
key
|
|
}
|
|
fn endpoint(path: &[u128], leaf: u128) -> Endpoint {
|
|
Endpoint {
|
|
references: path.iter().copied().map(id).collect(),
|
|
occurrence: id(leaf),
|
|
}
|
|
}
|
|
fn run(layer: Layer) {
|
|
let mut p = Project::empty();
|
|
p.id = id(1000);
|
|
let chain = if layer == Layer::Oa {
|
|
Kind::Process
|
|
} else {
|
|
Kind::FunctionalChain
|
|
};
|
|
let work_kind = if layer == Layer::Oa {
|
|
Kind::Activity
|
|
} else {
|
|
Kind::Function
|
|
};
|
|
for n in 1..=4 {
|
|
element(&mut p, n, chain, layer, None);
|
|
}
|
|
for n in 10..=13 {
|
|
element(&mut p, n, work_kind, layer, None);
|
|
}
|
|
for (n, owner, work) in [
|
|
(20, 2, 10),
|
|
(21, 2, 11),
|
|
(22, 2, 12),
|
|
(23, 3, 11),
|
|
(24, 1, 13),
|
|
] {
|
|
occurrence(
|
|
&mut p,
|
|
n,
|
|
layer,
|
|
id(owner),
|
|
OccurrenceBody::Action {
|
|
action: Some(id(work)),
|
|
lifeline: None,
|
|
},
|
|
);
|
|
}
|
|
for (n, owner, target) in [(30, 1, 2), (31, 1, 2), (32, 1, 4), (33, 4, 2), (34, 1, 3)] {
|
|
occurrence(
|
|
&mut p,
|
|
n,
|
|
layer,
|
|
id(owner),
|
|
OccurrenceBody::SequenceReference {
|
|
sequence: Some(id(target)),
|
|
},
|
|
);
|
|
}
|
|
for (n, source, target) in [(40, 20, 21), (41, 21, 22)] {
|
|
p.relations.insert(
|
|
id(n),
|
|
Relation {
|
|
id: id(n),
|
|
alias: format!("link_{n}"),
|
|
name: format!("link_{n}"),
|
|
description: String::new(),
|
|
body: RelationBody::SequenceLink {
|
|
source: id(source),
|
|
target: id(target),
|
|
guard: String::new(),
|
|
},
|
|
},
|
|
);
|
|
}
|
|
let exchange = p.relate(
|
|
if layer == Layer::Oa {
|
|
RelationKind::Interaction
|
|
} else {
|
|
RelationKind::FunctionalExchange
|
|
},
|
|
"X supplies B",
|
|
id(13),
|
|
id(11),
|
|
);
|
|
assert!(
|
|
execute_batch(&p, &[]).is_ok(),
|
|
"Probe fixture must be admitted by the existing core"
|
|
);
|
|
let before = p.clone();
|
|
assert!(!validation::can_connect(
|
|
&p,
|
|
RelationKind::SequenceLink,
|
|
id(24),
|
|
id(21)
|
|
));
|
|
assert!(
|
|
execute_batch(
|
|
&p,
|
|
&[Command::Connect(
|
|
RelationKind::SequenceLink,
|
|
"unqualified".into(),
|
|
id(24),
|
|
id(21)
|
|
)]
|
|
)
|
|
.is_err()
|
|
);
|
|
let (x, r1b, r2b, nested_b, qb) = (
|
|
endpoint(&[], 24),
|
|
endpoint(&[30], 21),
|
|
endpoint(&[31], 21),
|
|
endpoint(&[32, 33], 21),
|
|
endpoint(&[34], 23),
|
|
);
|
|
let mut index = Index::new(&p, 32, 128);
|
|
let first = index.resolve(id(1), &r1b).unwrap();
|
|
let second = index.resolve(id(1), &r2b).unwrap();
|
|
let nested = index.resolve(id(1), &nested_b).unwrap();
|
|
assert_ne!(first.address, second.address);
|
|
assert_ne!(first.address, nested.address);
|
|
assert_eq!(first.work, second.work);
|
|
assert_eq!(first.work, nested.work);
|
|
for purpose in [
|
|
Purpose::Precedence {
|
|
guard: "pressure > 400".into(),
|
|
},
|
|
Purpose::Exchange { exchange },
|
|
] {
|
|
index.connection(id(1), &x, &r1b, &purpose).unwrap();
|
|
}
|
|
index
|
|
.connection(id(1), &r1b, &qb, &Purpose::CommonWork { work: id(11) })
|
|
.unwrap();
|
|
assert!(
|
|
index
|
|
.connection(id(1), &x, &qb, &Purpose::CommonWork { work: id(11) })
|
|
.is_err()
|
|
);
|
|
assert!(
|
|
index
|
|
.connection(id(1), &r1b, &x, &Purpose::Exchange { exchange })
|
|
.is_err()
|
|
);
|
|
assert!(
|
|
index
|
|
.connection(
|
|
id(1),
|
|
&r1b,
|
|
&r1b,
|
|
&Purpose::Precedence {
|
|
guard: String::new()
|
|
}
|
|
)
|
|
.is_err()
|
|
);
|
|
for bad in [
|
|
endpoint(&[], 21),
|
|
endpoint(&[30], 23),
|
|
endpoint(&[33], 21),
|
|
endpoint(&[30, 33], 21),
|
|
endpoint(&[24], 21),
|
|
endpoint(&[999], 21),
|
|
] {
|
|
assert!(
|
|
index.resolve(id(1), &bad).is_err(),
|
|
"accepted invalid path {bad:?}"
|
|
);
|
|
}
|
|
let mut depth_limited = Index::new(&p, 1, 128);
|
|
assert_eq!(
|
|
depth_limited.resolve(id(1), &nested_b),
|
|
Err("reference depth limit")
|
|
);
|
|
let mut count_limited = Index::new(&p, 32, 1);
|
|
count_limited.resolve(id(1), &r1b).unwrap();
|
|
count_limited.resolve(id(1), &r1b).unwrap();
|
|
count_limited.resolve(id(1), &r2b).unwrap();
|
|
count_limited.resolve(id(1), &r1b).unwrap();
|
|
assert_eq!(count_limited.cache.len(), 1);
|
|
assert_eq!(
|
|
count_limited.resolve_many(id(1), &[r1b.clone(), r2b.clone()], 1),
|
|
Err("analysis address limit")
|
|
);
|
|
assert_eq!(
|
|
count_limited.resolve_many(id(1), &[r2b.clone(), r1b.clone()], 1),
|
|
Err("analysis address limit")
|
|
);
|
|
assert_eq!(
|
|
count_limited
|
|
.resolve_many(id(1), &[r1b.clone(), r2b.clone()], 2)
|
|
.unwrap()
|
|
.len(),
|
|
2
|
|
);
|
|
let mut changed = p.clone();
|
|
if let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::SequenceReference { sequence },
|
|
..
|
|
}) = changed.elements.get_mut(&id(30)).unwrap().shared_mut()
|
|
{
|
|
*sequence = Some(id(3));
|
|
}
|
|
assert!(Index::new(&changed, 32, 128).resolve(id(1), &r1b).is_err());
|
|
let mut deleted = p.clone();
|
|
deleted.elements.remove(&id(30));
|
|
assert!(Index::new(&deleted, 32, 128).resolve(id(1), &r1b).is_err());
|
|
assert_eq!(
|
|
p, before,
|
|
"Address resolution may not mutate shared procedures"
|
|
);
|
|
println!(
|
|
"{layer:?}: existing foreign link rejected; R1/R2/nested distinct; 3 purposes checked; invalid paths/retarget/delete/limits refused; shared model unchanged"
|
|
);
|
|
}
|
|
fn main() {
|
|
run(Layer::Oa);
|
|
run(Layer::Sa);
|
|
println!(
|
|
"PASS: bounded contextual resolver design probe (not production schema/API acceptance)"
|
|
);
|
|
}
|