///|
/// Exact caller-declared dependency set. Copies prevent a caller from adding a
/// duplicate or changing the set after construction. Lists retain authored order.
pub struct RevisionContext {
model : Snapshot
priv entity_revisions : Array[Snapshot]
priv source_revisions : Array[Snapshot]
priv implementation_revisions : Array[Snapshot]
}
///|
pub fn RevisionContext::new(
model : Snapshot,
entities~ : Array[Snapshot],
sources~ : Array[Snapshot],
implementations~ : Array[Snapshot],
) -> RevisionContext raise InvalidInput {
for entries in [entities, sources, implementations] {
let ids : Array[Id] = []
for entry in entries {
if ids.contains(entry.id) {
raise InvalidInput(
"Duplicate revision-context identity: " + entry.id.value(),
)
}
ids.push(entry.id)
}
}
{
model,
entity_revisions: entities.copy(),
source_revisions: sources.copy(),
implementation_revisions: implementations.copy(),
}
}
///|
pub fn RevisionContext::entities(self : RevisionContext) -> Array[Snapshot] {
self.entity_revisions.copy()
}
///|
pub fn RevisionContext::sources(self : RevisionContext) -> Array[Snapshot] {
self.source_revisions.copy()
}
///|
pub fn RevisionContext::implementations(
self : RevisionContext,
) -> Array[Snapshot] {
self.implementation_revisions.copy()
}
///|
#derive(Eq, Debug)
pub(all) enum Freshness {
Current
Stale
Inconclusive
}
///|
pub extend Freshness with Eq::{equal, not_equal}
///|
pub extend Freshness with @debug.Debug::{to_repr}
///|
fn includes_ids(entries : Array[Snapshot], required : Array[Id]) -> Bool {
required.all(id => entries.any(entry => entry.id == id))
}
///|
fn same_set(a : Array[Snapshot], b : Array[Snapshot]) -> Bool {
a.length() == b.length() && a.all(entry => b.contains(entry))
}
///|
priv struct RequiredContext {
entities : Array[Id]
sources : Array[Id]
implementations : Array[Id]
actual_model : Snapshot
actual_entities : Array[Snapshot]
actual_sources : Array[Snapshot]
actual_implementations : Array[Snapshot]
selected_entities : Array[Id]
selected_sources : Array[Id]
selected_implementations : Array[Id]
mut unavailable : Bool
}
///|
fn compare_context(
recorded : RevisionContext,
requested : RevisionContext,
required : RequiredContext,
) -> Freshness {
if required.unavailable ||
!includes_ids(recorded.entity_revisions, required.entities) ||
!includes_ids(requested.entity_revisions, required.entities) ||
!includes_ids(recorded.source_revisions, required.sources) ||
!includes_ids(requested.source_revisions, required.sources) ||
!includes_ids(recorded.implementation_revisions, required.implementations) ||
!includes_ids(requested.implementation_revisions, required.implementations) {
return Inconclusive
}
if recorded.model == requested.model &&
requested.model == required.actual_model &&
required.actual_entities.all(s => requested.entity_revisions.contains(s)) &&
required.actual_sources.all(s => requested.source_revisions.contains(s)) &&
required.actual_implementations.all(s => {
requested.implementation_revisions.contains(s)
}) &&
same_set(recorded.entity_revisions, requested.entity_revisions) &&
same_set(recorded.source_revisions, requested.source_revisions) &&
same_set(
recorded.implementation_revisions,
requested.implementation_revisions,
) {
Current
} else {
Stale
}
}
///|
fn add_id(ids : Array[Id], id : Id) -> Unit {
if !ids.contains(id) {
ids.push(id)
}
}
///|
fn add_actual(
required : RequiredContext,
entries : Array[Snapshot],
snapshot : Snapshot,
) -> Unit {
if entries.any(entry => entry.id == snapshot.id && entry != snapshot) {
required.unavailable = true
}
if !entries.contains(snapshot) {
entries.push(snapshot)
}
}
///|
fn require_source(
required : RequiredContext,
owner : Model,
reference : SourceRef,
) -> Unit {
guard owner.sources.filter(s => s.id == reference.source_id).get(0)
is Some(found) else {
required.unavailable = true
return
}
add_id(required.sources, found.document.id)
add_id(required.selected_sources, found.document.id)
match found.document.revision {
Some(revision) =>
add_actual(required, required.actual_sources, {
id: found.document.id,
revision,
})
None => required.unavailable = true
}
if found.resolution is Unresolved(_) {
required.unavailable = true
}
}
///|
/// A declared dependency is relevant even without a relationship. Exact linked
/// references already select its snapshot; otherwise inspect every candidate in
/// the owner's supplied closure. Conflicting revisions remain inconclusive.
fn include_declared_context(
required : RequiredContext,
owner : Model,
declared : RevisionContext,
) -> Unit {
for entry in declared.entity_revisions {
add_id(required.entities, entry.id)
}
for entry in declared.source_revisions {
add_id(required.sources, entry.id)
}
for entry in declared.implementation_revisions {
add_id(required.implementations, entry.id)
}
let entities = declared.entity_revisions
.filter(entry => !required.selected_entities.contains(entry.id))
.map(entry => entry.id)
let sources = declared.source_revisions
.filter(entry => !required.selected_sources.contains(entry.id))
.map(entry => entry.id)
let implementations = declared.implementation_revisions
.filter(entry => !required.selected_implementations.contains(entry.id))
.map(entry => entry.id)
for (candidate, _) in model_closure(owner).0 {
for entity in candidate.entities {
if entities.contains(entity.identity.id) {
add_actual(required, required.actual_entities, entity.identity)
}
if entity.content is Implementation(artifact) &&
implementations.contains(artifact.id) {
add_actual(required, required.actual_implementations, artifact)
}
}
for source in candidate.sources {
if sources.contains(source.document.id) {
match source.document.revision {
Some(revision) =>
add_actual(required, required.actual_sources, {
id: source.document.id,
revision,
})
None => required.unavailable = true
}
if source.resolution is Unresolved(_) {
required.unavailable = true
}
}
}
}
}
///|
/// Collect a selected entity's structured dependencies within the supplied scope.
fn require_entity_context(
model : Model,
required : RequiredContext,
selected : ResolvedRef,
) -> Unit {
let visited : Array[ResolvedRef] = []
fn visit(reference : ResolvedRef) -> Unit {
if visited.contains(reference) {
return
}
visited.push(reference)
add_id(required.entities, reference.entity.id)
let entity = match resolve(model, reference) {
Ok(e) => e
Err(_) => {
required.unavailable = true
return
}
}
add_actual(required, required.actual_entities, entity.identity)
add_id(required.selected_entities, entity.identity.id)
guard find_model(model, reference.model) is Some(owner) else {
required.unavailable = true
return
}
for source_reference in entity.source_refs {
require_source(required, owner, source_reference)
}
match entity.content {
SourceFragment(source_reference) =>
require_source(required, owner, source_reference)
Contract(subject, _, _) => visit(subject)
Declaration(_, Some(binding)) => visit(binding)
Implementation(artifact) => {
add_id(required.implementations, artifact.id)
add_id(required.selected_implementations, artifact.id)
add_actual(required, required.actual_implementations, artifact)
}
_ => ()
}
for (link_owner, _) in model_closure(model).0 {
for link in link_owner.relationships {
let follows = link.subject == reference &&
(
link.predicate == Refines ||
link.predicate == DerivedFrom ||
link.predicate == GovernedBy
)
let reverses = link.target is Resolved(target) &&
target == reference &&
(
link.predicate == Formalizes ||
link.predicate == Exercises ||
link.predicate == Implements
)
if follows || reverses {
for source_reference in link.source_refs {
require_source(required, link_owner, source_reference)
}
if relationship_error(link_owner, link) is Some(_) {
required.unavailable = true
}
if follows {
match link.target {
Resolved(target) => visit(target)
Unresolved(_, _, _) => required.unavailable = true
}
}
if reverses {
visit(link.subject)
}
}
}
}
}
visit(selected)
}
///|
/// Discover structural assertions throughout the supplied root closure while
/// keeping the claim's model revision and local provenance in its owning scope.
fn required_context(
root : Model,
model : Model,
claim : Claim,
source_refs : Array[SourceRef],
) -> RequiredContext {
let required : RequiredContext = {
entities: claim.context.entity_revisions.map(s => s.id),
sources: claim.context.source_revisions.map(s => s.id),
implementations: claim.context.implementation_revisions.map(s => s.id),
actual_model: model.identity,
actual_entities: [],
actual_sources: [],
actual_implementations: [],
selected_entities: [],
selected_sources: [],
selected_implementations: [],
unavailable: false,
}
require_entity_context(root, required, claim.subject)
for reference in source_refs {
require_source(required, model, reference)
}
include_declared_context(required, model, claim.context)
required
}
///|
fn observation_context(
model : Model,
base : RequiredContext,
link_owner : Model,
link : Relationship,
entity : Entity,
evidence : Evidence,
) -> RequiredContext {
let required : RequiredContext = {
entities: base.entities.copy(),
sources: base.sources.copy(),
implementations: base.implementations.copy(),
actual_model: base.actual_model,
actual_entities: base.actual_entities.copy(),
actual_sources: base.actual_sources.copy(),
actual_implementations: base.actual_implementations.copy(),
selected_entities: base.selected_entities.copy(),
selected_sources: base.selected_sources.copy(),
selected_implementations: base.selected_implementations.copy(),
unavailable: base.unavailable,
}
if find_model(model, link.subject.model) is Some(owner) {
for reference in entity.source_refs {
require_source(required, owner, reference)
}
}
for reference in link.source_refs {
require_source(required, link_owner, reference)
}
if evidence.observation is Run(run) {
for clause in run.clauses {
add_id(required.entities, clause.entity.id)
if !evidence.context.entity_revisions.contains(clause.entity) {
required.unavailable = true
}
// M1 clause results carry an entity snapshot, not a model reference.
// Resolve only inside the evidence owner's closure and require a unique
// supplied owner; equal entity snapshots in distinct models are ambiguous.
let candidates : Array[ResolvedRef] = []
if find_model(model, link.subject.model) is Some(owner) {
for (candidate, _) in model_closure(owner).0 {
for entity in candidate.entities {
if entity.identity == clause.entity && entity.kind() == ContractKind {
candidates.push({
model: candidate.identity,
entity: entity.identity,
})
}
}
}
if candidates.length() == 1 {
require_entity_context(model, required, candidates[0])
} else {
required.unavailable = true
}
} else {
required.unavailable = true
}
}
}
if find_model(model, link.subject.model) is Some(owner) {
include_declared_context(required, owner, evidence.context)
}
required
}