Integrate release recovery, semantic review, coverage and Windows packaging

This commit is contained in:
Codex
2026-09-06 16:31:55 -07:00
parent c091f80e6e
commit 2b2d2bc18b
174 changed files with 27635 additions and 127 deletions
+206
View File
@@ -0,0 +1,206 @@
//! Typed authored workflow inputs; no stored command is executable on recovery.
use super::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
enum Saved {
Semantic(serde_json::Value),
Ports(serde_json::Value),
Relation {
base: String,
id: Id,
name: String,
kind: RelationKind,
source: Option<Id>,
target: Option<Id>,
condition: String,
layer: Layer,
items: Vec<Id>,
source_port: PortChoice,
target_port: PortChoice,
oriented: bool,
},
ManualTrace {
base: String,
name: String,
source: Id,
target: Option<Id>,
},
Transition {
base: String,
request: TransitionRequest,
},
}
pub(crate) fn endpoint(p: &Project, port: PortChoice) -> EndpointChoice {
let label = match port {
PortChoice::Auto => "Automatic · reuse only if unique".into(),
PortChoice::Create => "Create a new port".into(),
PortChoice::Reuse(id) => format!("Reuse {}", choice(p, id).label),
};
EndpointChoice {
choice: port,
label,
}
}
fn check(p: &Project, base: &str) -> Result<(), String> {
if project_fingerprint(p) != base {
return Err(
"Workflow recovery does not match the accepted model identity and revision.".into(),
);
}
Ok(())
}
impl Workflow {
pub fn recovery_snapshot(&self) -> Result<serde_json::Value, String> {
let saved = match self {
Self::Semantic(state) => Saved::Semantic(state.recovery_snapshot()?),
Self::Ports(state) => Saved::Ports(state.recovery_snapshot()?),
Self::Relation(d) => Saved::Relation {
base: d.base_fingerprint.clone(),
id: d.id,
name: d.name.clone(),
kind: d.kind,
source: d.source.as_ref().map(|c| c.id),
target: d.target.as_ref().map(|c| c.id),
condition: d.condition.text(),
layer: d.layer,
items: d.items.clone(),
source_port: d.source_port.choice,
target_port: d.target_port.choice,
oriented: d.oriented,
},
Self::ManualTrace {
base_fingerprint,
name,
source,
target,
} => Saved::ManualTrace {
base: base_fingerprint.clone(),
name: name.clone(),
source: source.id,
target: target.as_ref().map(|c| c.id),
},
Self::Transition(d) => Saved::Transition {
base: d.plan.base_fingerprint.clone(),
request: d.request.clone(),
},
};
serde_json::to_value(saved).map_err(|e| e.to_string())
}
pub fn from_recovery(p: &Project, value: serde_json::Value) -> Result<Self, String> {
match serde_json::from_value::<Saved>(value).map_err(|e| e.to_string())? {
Saved::Semantic(value) => crate::semantic_review::State::from_recovery(p, value)
.map(|state| Self::Semantic(Box::new(state))),
Saved::Ports(value) => crate::port_workflow::State::from_recovery(p, value)
.map(|state| Self::Ports(Box::new(state))),
Saved::Relation {
base,
id,
name,
kind,
source,
target,
condition,
layer,
items,
source_port,
target_port,
oriented,
} => {
check(p, &base)?;
if items.len() > crate::source_workspace::LIMITS.model.max_records {
return Err("Recovered relationship exceeds item limits.".into());
}
let mut d = Relationship {
base_fingerprint: base,
id,
name,
kind,
source: source.map(|id| choice(p, id)),
target: target.map(|id| choice(p, id)),
condition: text_editor::Content::with_text(&condition),
layer,
items,
source_port: endpoint(p, source_port),
target_port: endpoint(p, target_port),
oriented,
sources: vec![],
targets: vec![],
};
d.refresh_choices(p);
Ok(Self::Relation(Box::new(d)))
}
Saved::ManualTrace {
base,
name,
source,
target,
} => {
check(p, &base)?;
let mut workflow = Self::manual_trace(p, source)?;
if let Self::ManualTrace {
name: n, target: t, ..
} = &mut workflow
{
*n = name;
*t = target.map(|id| choice(p, id));
}
Ok(workflow)
}
Saved::Transition { base, request } => {
check(p, &base)?;
let plan = plan_transition(p, &request).map_err(|e| e.to_string())?;
Ok(Self::Transition(Box::new(Transition { request, plan })))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn incomplete_relationship_and_trace_recover_raw_fields_and_reject_changed_model() {
let mut p = Project::empty();
let a = p
.add_element(Kind::Activity, Layer::Oa, "Source", None)
.unwrap();
let mut relation = Workflow::relationship(&p, a, Layer::Oa);
if let Workflow::Relation(d) = &mut relation {
d.name = " unfinished name ".into();
d.condition = text_editor::Content::with_text("raw (\n unfinished");
d.target = None;
}
let saved = relation.recovery_snapshot().unwrap();
let restored = Workflow::from_recovery(&p, saved.clone()).unwrap();
assert_eq!(restored.recovery_snapshot().unwrap(), saved);
let trace = Workflow::manual_trace(&p, a).unwrap();
let saved_trace = trace.recovery_snapshot().unwrap();
assert_eq!(
Workflow::from_recovery(&p, saved_trace.clone())
.unwrap()
.recovery_snapshot()
.unwrap(),
saved_trace
);
p.name.push('!');
assert!(Workflow::from_recovery(&p, saved).is_err());
assert!(Workflow::from_recovery(&p, saved_trace).is_err());
}
#[test]
fn transition_recovery_replans_typed_request_without_persisted_commands() {
let mut p = Project::empty();
let a = p
.add_element(Kind::Activity, Layer::Oa, "Source", None)
.unwrap();
let workflow = Workflow::transition(&p, a).unwrap();
let saved = workflow.recovery_snapshot().unwrap();
assert!(!serde_json::to_string(&saved).unwrap().contains("commands"));
let Workflow::Transition(restored) = Workflow::from_recovery(&p, saved).unwrap() else {
panic!()
};
assert_eq!(restored.plan.base_fingerprint, project_fingerprint(&p));
assert_eq!(restored.request.sources, vec![a]);
}
}