Improve behavior authoring and typed trace history (untested checkpoint)
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s

This commit is contained in:
Codex
2026-09-10 11:07:07 -07:00
parent 1797516f46
commit 01bb52a76d
25 changed files with 4078 additions and 171 deletions
+101 -4
View File
@@ -16,6 +16,7 @@ use serde_json::{Value, json};
use std::collections::{BTreeMap, BTreeSet};
mod context_fields;
mod data_fields;
mod history_fields;
mod port_fields;
mod view_fields;
@@ -50,6 +51,14 @@ pub(crate) fn set_shape_value(record: &mut Value, path: &str, value: Value) -> R
*target = value;
return Ok(());
}
if history_fields::is_path(path)
&& history_fields::has_field(path)
&& let Some((parent, field)) = path.rsplit_once('/')
&& let Some(fields) = record.pointer_mut(parent).and_then(Value::as_object_mut)
{
fields.insert(field.into(), value);
return Ok(());
}
// Ordinary messages omit operation=None in source/JSON. Authoring that one
// optional field must not require changing the historic record encoding.
if let Some(parent) = path.strip_suffix("/operation")
@@ -75,6 +84,7 @@ pub struct Draft {
port_contract_drafts: BTreeMap<String, Value>,
pub value_browsers: BTreeMap<String, ValueBrowser>,
pub reference_browsers: BTreeMap<String, ReferenceBrowser>,
history: history_fields::State,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
@@ -156,6 +166,7 @@ impl Draft {
})
.collect();
draft.port_contract_drafts = saved.ports;
draft.history.open = draft.kind == RecordKind::Trace;
Ok(draft)
}
pub fn from_project(project: &Project, selected: Id) -> Option<Self> {
@@ -197,6 +208,7 @@ impl Draft {
port_contract_drafts: BTreeMap::new(),
value_browsers: BTreeMap::new(),
reference_browsers: BTreeMap::new(),
history: Default::default(),
};
draft.rebuild();
Some(draft)
@@ -311,9 +323,38 @@ impl Draft {
.map_or(true, |value| value != self.baseline)
}
pub fn set_exact(&mut self, enabled: bool) -> Result<(), String> {
self.value = self.materialize()?;
let value = self.materialize()?;
let mut history_inputs = Vec::new();
if enabled != self.exact_strings {
for (key, (_, inputs)) in &self.constructor_drafts {
if self.kind != RecordKind::Trace || !history_fields::is_path(&key.0) {
continue;
}
for (path, input) in inputs {
if free_string(path) {
let raw = if enabled {
serde_json::to_string(&input.raw).expect("String serialization")
} else {
serde_json::from_str::<String>(&input.raw).map_err(|_| "Repair the quoted text in the retained history constructor before changing escaped-string mode.".to_owned())?
};
history_inputs.push((key.clone(), path.clone(), raw));
}
}
}
}
self.value = value;
self.exact_strings = enabled;
self.rebuild();
for (key, path, raw) in history_inputs {
if let Some(input) = self
.constructor_drafts
.get_mut(&key)
.and_then(|(_, inputs)| inputs.get_mut(&path))
{
input.editor = text_editor::Content::with_text(&raw);
input.raw = raw;
}
}
Ok(())
}
pub fn input(&mut self, path: &str, value: String) {
@@ -329,6 +370,29 @@ impl Draft {
}
}
pub fn shape(&mut self, path: &str, value: Value) -> Result<(), String> {
if self.kind == RecordKind::Trace {
if path == history_fields::TOGGLE {
self.history.open = value
.as_bool()
.ok_or("Choose whether to show stored history.")?;
return Ok(());
}
if let Some(path) = path.strip_prefix(history_fields::PAGE) {
self.history.pages.insert(
path.into(),
value.as_u64().ok_or("Choose a history page.")? as usize,
);
return Ok(());
}
if let Some(path) = path.strip_prefix(history_fields::VARIANT) {
return self.variant_scoped(path, value);
}
if history_fields::is_path(path) {
self.constructor_drafts
.retain(|(root, _), _| !within(root, path));
return self.shape_scoped(path, value);
}
}
let mut candidate = self.materialize()?;
let value = if path.ends_with("/Port/contract") {
port_fields::switch_contract(
@@ -369,7 +433,8 @@ impl Draft {
|| path.ends_with("/Occurrence/body")
|| path.ends_with("/Event/body")
|| path.ends_with("/message_kind")
|| path.ends_with("/ContextualLink/purpose");
|| path.ends_with("/ContextualLink/purpose")
|| (self.kind == RecordKind::Trace && history_fields::is_path(path));
if !constructor {
return self.shape_scoped(path, proposed);
}
@@ -448,7 +513,7 @@ impl Draft {
.len();
edit_array(&mut candidate, path, index, direction)?;
self.value = candidate;
self.rebuild_retaining(|old| {
let relocate = |old: &str| {
let Some(tail) = old.strip_prefix(&format!("{path}/")) else {
return Some(old.to_owned());
};
@@ -474,7 +539,24 @@ impl Draft {
} else {
format!("{path}/{position}/{rest}")
})
});
};
self.rebuild_retaining(relocate);
if self.kind == RecordKind::Trace && history_fields::is_path(path) {
let previous = std::mem::take(&mut self.constructor_drafts);
for ((root, tag), (value, inputs)) in previous {
if !root.starts_with(&format!("{path}/")) {
self.constructor_drafts.insert((root, tag), (value, inputs));
continue;
}
if let Some(root) = relocate(&root) {
let inputs = inputs
.into_iter()
.filter_map(|(path, input)| relocate(&path).map(|path| (path, input)))
.collect();
self.constructor_drafts.insert((root, tag), (value, inputs));
}
}
}
Ok(())
}
pub fn append_scoped(&mut self, path: &str, value: Value) -> Result<(), String> {
@@ -510,6 +592,9 @@ impl Draft {
index: Option<usize>,
direction: i32,
) -> Result<(), String> {
if self.kind == RecordKind::Trace && history_fields::is_path(path) {
return self.array_scoped(path, index, direction);
}
let mut candidate = self.materialize()?;
edit_array(&mut candidate, path, index, direction)?;
self.value = candidate;
@@ -517,6 +602,9 @@ impl Draft {
Ok(())
}
pub fn append(&mut self, path: &str, value: Value) -> Result<(), String> {
if self.kind == RecordKind::Trace && history_fields::is_path(path) {
return self.append_scoped(path, value);
}
let mut candidate = self.materialize()?;
candidate
.pointer_mut(path)
@@ -625,6 +713,9 @@ fn edit_array(
index: Option<usize>,
direction: i32,
) -> Result<(), String> {
if history_fields::is_path(path) && index.is_none() {
return Err("Choose a typed stored fact or list value before adding it.".into());
}
let items = value
.pointer_mut(path)
.and_then(Value::as_array_mut)
@@ -807,6 +898,9 @@ fn multiline(path: &str) -> bool {
)
}
fn free_string(path: &str) -> bool {
if history_fields::is_path(path) {
return history_fields::free_string(path);
}
multiline(path)
|| matches!(
path.rsplit('/').next(),
@@ -1728,6 +1822,9 @@ fn property<'a>(
label: &str,
skip_variant: bool,
) -> View<'a, Message> {
if draft.kind == RecordKind::Trace && path == "/baseline" {
return history_fields::view(draft, project, value);
}
if let Some(view) = context_fields::property(draft, project, value, path) {
return view;
}