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
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,666 @@
|
||||
//! Typed editing of stored observations, independently of the live counterparts.
|
||||
//! The checked-in, contract-tested OpenAPI descriptor supplies only field shapes.
|
||||
//! Serde and core historical validation remain the admission authority.
|
||||
use super::*;
|
||||
use iced::widget::column;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub(super) const TOGGLE: &str = "/$stored_history";
|
||||
pub(super) const PAGE: &str = "/$stored_history_page";
|
||||
pub(super) const VARIANT: &str = "/$stored_history_variant";
|
||||
const ROOT: &str = "/baseline";
|
||||
const FACETS: &str = "/baseline/semantic/facets";
|
||||
const PAGE_SIZE: usize = 12;
|
||||
const MAX_DEPTH: usize = 32;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct State {
|
||||
pub open: bool,
|
||||
pub pages: BTreeMap<String, usize>,
|
||||
}
|
||||
|
||||
pub(super) fn is_path(path: &str) -> bool {
|
||||
path == ROOT || path.starts_with("/baseline/")
|
||||
}
|
||||
|
||||
fn api() -> &'static Value {
|
||||
static API: OnceLock<Value> = OnceLock::new();
|
||||
API.get_or_init(|| {
|
||||
serde_json::from_str(include_str!("../../../../docs/openapi.json"))
|
||||
.expect("The bundled OpenAPI descriptor is valid JSON")
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve(mut schema: &'static Value) -> Result<&'static Value, &'static str> {
|
||||
for _ in 0..MAX_DEPTH {
|
||||
let Some(reference) = schema.get("$ref").and_then(Value::as_str) else {
|
||||
return Ok(schema);
|
||||
};
|
||||
schema = reference
|
||||
.strip_prefix('#')
|
||||
.and_then(|path| api().pointer(path))
|
||||
.ok_or("Stored-history descriptor has an unresolved local reference.")?;
|
||||
}
|
||||
Err("Stored-history descriptor reference depth exceeded.")
|
||||
}
|
||||
|
||||
fn named(name: &str) -> Result<&'static Value, &'static str> {
|
||||
api()["components"]["schemas"]
|
||||
.get(name)
|
||||
.ok_or("Stored-history descriptor is missing a required type.")
|
||||
}
|
||||
|
||||
fn alternatives(schema: &'static Value) -> Option<&'static Vec<Value>> {
|
||||
schema
|
||||
.get("oneOf")
|
||||
.or_else(|| schema.get("anyOf"))
|
||||
.and_then(Value::as_array)
|
||||
}
|
||||
|
||||
fn at_schema(schema: &'static Value, parts: &[&str], depth: usize) -> Option<&'static Value> {
|
||||
if depth > MAX_DEPTH {
|
||||
return None;
|
||||
}
|
||||
let schema = resolve(schema).ok()?;
|
||||
let Some((part, rest)) = parts.split_first() else {
|
||||
return Some(schema);
|
||||
};
|
||||
if let Some(options) = alternatives(schema) {
|
||||
return options.iter().find_map(|s| at_schema(s, parts, depth + 1));
|
||||
}
|
||||
match schema.get("type").and_then(Value::as_str) {
|
||||
Some("object") => at_schema(schema.get("properties")?.get(*part)?, rest, depth + 1),
|
||||
Some("array") if part.parse::<usize>().is_ok() => {
|
||||
at_schema(schema.get("items")?, rest, depth + 1)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn schema_at(path: &str) -> Option<&'static Value> {
|
||||
let tail = path.strip_prefix(ROOT)?;
|
||||
let parts: Vec<_> = tail
|
||||
.trim_start_matches('/')
|
||||
.split('/')
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
at_schema(named("TraceBaseline").ok()?, &parts, 0)
|
||||
}
|
||||
|
||||
pub(super) fn has_field(path: &str) -> bool {
|
||||
schema_at(path).is_some()
|
||||
}
|
||||
|
||||
pub(super) fn free_string(path: &str) -> bool {
|
||||
fn text_shape(schema: &'static Value, depth: usize) -> bool {
|
||||
if depth > MAX_DEPTH {
|
||||
return false;
|
||||
}
|
||||
let Ok(schema) = resolve(schema) else {
|
||||
return false;
|
||||
};
|
||||
if let Some(options) = alternatives(schema) {
|
||||
return options.iter().any(|s| text_shape(s, depth + 1));
|
||||
}
|
||||
schema.get("type").and_then(Value::as_str) == Some("string")
|
||||
&& schema.get("enum").is_none()
|
||||
&& schema.get("format").and_then(Value::as_str) != Some("uuid")
|
||||
}
|
||||
schema_at(path).is_some_and(|s| text_shape(s, 0))
|
||||
}
|
||||
|
||||
/// A shape seed is a draft, never an acknowledged historical fact. UUIDs start
|
||||
/// empty. Observed starts NA on both sides, which core rejects. Typed defaults
|
||||
/// inside Value are introduced only after the engineer chooses that branch.
|
||||
fn seed(schema: &'static Value, depth: usize) -> Result<Value, &'static str> {
|
||||
if depth > MAX_DEPTH {
|
||||
return Err("Stored-history constructor depth exceeded.");
|
||||
}
|
||||
let schema = resolve(schema)?;
|
||||
if let Some(options) = alternatives(schema) {
|
||||
if options
|
||||
.iter()
|
||||
.any(|s| matches_value(s, &json!("NotApplicable"), 0))
|
||||
{
|
||||
return Ok(json!("NotApplicable"));
|
||||
}
|
||||
if options.iter().any(|s| matches_value(s, &Value::Null, 0)) {
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
return seed(
|
||||
options
|
||||
.first()
|
||||
.ok_or("Stored-history union has no constructors.")?,
|
||||
depth + 1,
|
||||
);
|
||||
}
|
||||
if let Some(values) = schema.get("enum").and_then(Value::as_array) {
|
||||
return values
|
||||
.first()
|
||||
.cloned()
|
||||
.ok_or("Stored-history enumeration has no values.");
|
||||
}
|
||||
Ok(match schema.get("type").and_then(Value::as_str) {
|
||||
Some("object") => {
|
||||
let fields = schema
|
||||
.get("properties")
|
||||
.and_then(Value::as_object)
|
||||
.ok_or("Stored-history object has no closed field descriptor.")?;
|
||||
let mut result = serde_json::Map::new();
|
||||
for (name, schema) in fields {
|
||||
result.insert(name.clone(), seed(schema, depth + 1)?);
|
||||
}
|
||||
Value::Object(result)
|
||||
}
|
||||
Some("array") => json!([]),
|
||||
Some("boolean") => json!(false),
|
||||
Some("integer") => json!(0),
|
||||
Some("number") => json!(0.0),
|
||||
Some("string") => json!(""),
|
||||
Some("null") => Value::Null,
|
||||
_ => return Err("Unsupported stored-history field descriptor."),
|
||||
})
|
||||
}
|
||||
|
||||
/// Shape matching selects the displayed constructor; it is not validation.
|
||||
/// Required values may be incomplete while their chosen branch stays editable.
|
||||
fn matches_value(schema: &'static Value, value: &Value, depth: usize) -> bool {
|
||||
if depth > MAX_DEPTH {
|
||||
return false;
|
||||
}
|
||||
let Ok(schema) = resolve(schema) else {
|
||||
return false;
|
||||
};
|
||||
if let Some(options) = alternatives(schema) {
|
||||
return options.iter().any(|s| matches_value(s, value, depth + 1));
|
||||
}
|
||||
if let Some(choices) = schema.get("enum").and_then(Value::as_array) {
|
||||
return choices.contains(value);
|
||||
}
|
||||
match schema.get("type").and_then(Value::as_str) {
|
||||
Some("object") => value.as_object().is_some_and(|fields| {
|
||||
schema
|
||||
.get("properties")
|
||||
.and_then(Value::as_object)
|
||||
.is_some_and(|properties| {
|
||||
fields.keys().all(|key| properties.contains_key(key))
|
||||
&& schema
|
||||
.get("required")
|
||||
.and_then(Value::as_array)
|
||||
.is_none_or(|required| {
|
||||
required
|
||||
.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.all(|key| fields.contains_key(key))
|
||||
})
|
||||
})
|
||||
}),
|
||||
Some("array") => value.is_array(),
|
||||
Some("boolean") => value.is_boolean(),
|
||||
Some("integer" | "number") => value.is_number(),
|
||||
Some("string") => value.is_string(),
|
||||
Some("null") => value.is_null(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn label(name: &str) -> String {
|
||||
match name {
|
||||
"oa" => return "Operational observation".into(),
|
||||
"sa" => return "System observation".into(),
|
||||
"NotApplicable" => return "Not applicable".into(),
|
||||
"Value" => return "Observed value".into(),
|
||||
"keys" => return "Exact historical keys".into(),
|
||||
"review" => return "Stored observations".into(),
|
||||
_ => {}
|
||||
}
|
||||
let mut out = String::new();
|
||||
let mut previous_lower = false;
|
||||
for c in name.chars() {
|
||||
if c == '_' {
|
||||
out.push(' ');
|
||||
} else {
|
||||
if c.is_uppercase() && previous_lower {
|
||||
out.push(' ');
|
||||
}
|
||||
out.push(c);
|
||||
}
|
||||
previous_lower = c.is_lowercase();
|
||||
}
|
||||
let mut chars = out.chars();
|
||||
chars
|
||||
.next()
|
||||
.map(|c| c.to_uppercase().collect::<String>() + chars.as_str())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn branch_label(schema: &'static Value) -> String {
|
||||
let Ok(schema) = resolve(schema) else {
|
||||
return "Unavailable type".into();
|
||||
};
|
||||
if let Some(word) = schema
|
||||
.get("enum")
|
||||
.and_then(Value::as_array)
|
||||
.filter(|v| v.len() == 1)
|
||||
.and_then(|v| v[0].as_str())
|
||||
{
|
||||
return label(word);
|
||||
}
|
||||
if schema.get("enum").is_some() {
|
||||
return "Enumeration · choose below".into();
|
||||
}
|
||||
if let Some(fields) = schema.get("properties").and_then(Value::as_object)
|
||||
&& fields.len() == 1
|
||||
{
|
||||
return label(fields.keys().next().unwrap());
|
||||
}
|
||||
match schema.get("type").and_then(Value::as_str) {
|
||||
Some("null") => "Absent (none)".into(),
|
||||
Some("object") => "Set record".into(),
|
||||
Some("array") => "Set list".into(),
|
||||
Some("string") if schema.get("format").and_then(Value::as_str) == Some("uuid") => {
|
||||
"Set historical UUID".into()
|
||||
}
|
||||
Some("string") => "Set text".into(),
|
||||
Some("integer" | "number") => "Set number".into(),
|
||||
_ => "Set value / choose constructor".into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
struct FactChoice {
|
||||
label: String,
|
||||
value: Value,
|
||||
}
|
||||
impl std::fmt::Display for FactChoice {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.label)
|
||||
}
|
||||
}
|
||||
|
||||
/// Flatten only the closed FacetReview selector levels, not observation values.
|
||||
/// Collection and incident-end choices are selectors too, so none is guessed.
|
||||
fn fact_choices() -> Result<&'static Vec<FactChoice>, &'static str> {
|
||||
static CHOICES: OnceLock<Result<Vec<FactChoice>, &'static str>> = OnceLock::new();
|
||||
CHOICES
|
||||
.get_or_init(|| {
|
||||
let root = resolve(named("FacetReview")?)?;
|
||||
let mut result = vec![];
|
||||
for branch in alternatives(root).ok_or("FacetReview must be a closed union.")? {
|
||||
let outer = resolve(branch)?;
|
||||
let (family, payload) = outer["properties"]
|
||||
.as_object()
|
||||
.and_then(|v| v.iter().next())
|
||||
.ok_or("Facet family is missing.")?;
|
||||
let payload = resolve(payload)?;
|
||||
if let Some(leaves) = alternatives(payload) {
|
||||
for leaf in leaves {
|
||||
let mut value = serde_json::Map::new();
|
||||
value.insert(family.clone(), seed(leaf, 0)?);
|
||||
result.push(FactChoice {
|
||||
label: format!("{} / {}", label(family), branch_label(leaf)),
|
||||
value: Value::Object(value),
|
||||
});
|
||||
}
|
||||
} else if family == "Artifact" {
|
||||
let leaves = alternatives(resolve(&payload["properties"]["review"])?)
|
||||
.ok_or("Artifact facts must be a closed union.")?;
|
||||
for leaf in leaves {
|
||||
let mut value = seed(branch, 0)?;
|
||||
value[family]["review"] = seed(leaf, 0)?;
|
||||
result.push(FactChoice {
|
||||
label: format!("Artifact / {}", branch_label(leaf)),
|
||||
value,
|
||||
});
|
||||
}
|
||||
} else if matches!(family.as_str(), "Member" | "Order" | "Incident") {
|
||||
let field = if family == "Incident" {
|
||||
"end"
|
||||
} else {
|
||||
"collection"
|
||||
};
|
||||
let choices = resolve(&payload["properties"][field])?["enum"]
|
||||
.as_array()
|
||||
.ok_or("Historical selector choices are missing.")?;
|
||||
for choice in choices {
|
||||
let mut value = seed(branch, 0)?;
|
||||
value[family][field] = choice.clone();
|
||||
result.push(FactChoice {
|
||||
label: format!(
|
||||
"{} / {}",
|
||||
label(family),
|
||||
label(choice.as_str().unwrap_or(""))
|
||||
),
|
||||
value,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
result.push(FactChoice {
|
||||
label: label(family),
|
||||
value: seed(branch, 0)?,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
})
|
||||
.as_ref()
|
||||
.map_err(|error| *error)
|
||||
}
|
||||
|
||||
fn variant_message(path: &str, value: Value, present: bool) -> Message {
|
||||
Message::FieldShape(
|
||||
if present {
|
||||
format!("{VARIANT}{path}")
|
||||
} else {
|
||||
path.into()
|
||||
},
|
||||
value,
|
||||
)
|
||||
}
|
||||
|
||||
fn branch_seed(schema: &'static Value) -> Result<Value, &'static str> {
|
||||
let schema = resolve(schema)?;
|
||||
if let Some(values) = schema
|
||||
.get("enum")
|
||||
.and_then(Value::as_array)
|
||||
.filter(|v| v.len() == 1)
|
||||
{
|
||||
Ok(values[0].clone())
|
||||
} else {
|
||||
seed(schema, 0)
|
||||
}
|
||||
}
|
||||
|
||||
fn branch_has_fields(schema: &'static Value) -> bool {
|
||||
let Ok(schema) = resolve(schema) else {
|
||||
return false;
|
||||
};
|
||||
schema.get("type").and_then(Value::as_str) != Some("null")
|
||||
&& schema
|
||||
.get("enum")
|
||||
.and_then(Value::as_array)
|
||||
.is_none_or(|values| values.len() > 1)
|
||||
}
|
||||
|
||||
fn paging<'a>(draft: &'a Draft, path: &str, count: usize) -> (usize, View<'a, Message>) {
|
||||
let last = count.saturating_sub(1) / PAGE_SIZE;
|
||||
let page = draft
|
||||
.history
|
||||
.pages
|
||||
.get(path)
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
.min(last);
|
||||
let event = |page| Message::FieldShape(format!("{PAGE}{path}"), json!(page));
|
||||
let view = row![
|
||||
button("Previous").on_press_maybe((page > 0).then(|| event(page - 1))),
|
||||
text(format!(
|
||||
"{}–{} of {count}",
|
||||
if count == 0 { 0 } else { page * PAGE_SIZE + 1 },
|
||||
((page + 1) * PAGE_SIZE).min(count)
|
||||
))
|
||||
.size(11),
|
||||
button("Next").on_press_maybe((page < last).then(|| event(page + 1)))
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center)
|
||||
.into();
|
||||
(page, view)
|
||||
}
|
||||
|
||||
fn render<'a>(
|
||||
draft: &'a Draft,
|
||||
project: &'a Project,
|
||||
schema: &'static Value,
|
||||
value: Option<&'a Value>,
|
||||
path: &str,
|
||||
title: &str,
|
||||
depth: usize,
|
||||
) -> View<'a, Message> {
|
||||
if depth > MAX_DEPTH {
|
||||
return text("Stored-history field nesting exceeds the supported editor depth.")
|
||||
.size(11)
|
||||
.into();
|
||||
}
|
||||
let schema = match resolve(schema) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return text(e).size(11).into(),
|
||||
};
|
||||
let present = value.is_some();
|
||||
let value = value.unwrap_or(&Value::Null);
|
||||
if let Some(options) = alternatives(schema) {
|
||||
let initial: Vec<_> = match options.iter().map(branch_seed).collect::<Result<_, _>>() {
|
||||
Ok(values) => values,
|
||||
Err(error) => {
|
||||
return text(format!(
|
||||
"Stored-history constructor is unavailable: {error}"
|
||||
))
|
||||
.size(11)
|
||||
.into();
|
||||
}
|
||||
};
|
||||
let labels: Vec<_> = options.iter().map(branch_label).collect();
|
||||
let selected = options.iter().position(|s| matches_value(s, value, 0));
|
||||
let path_owned = path.to_string();
|
||||
let labels_owned = labels.clone();
|
||||
let mut out = column![
|
||||
text(title.to_owned()).size(11).color(style::MUTED),
|
||||
pick_list(
|
||||
labels,
|
||||
selected.map(|i| branch_label(&options[i])),
|
||||
move |choice| {
|
||||
let index = labels_owned.iter().position(|v| v == &choice).unwrap();
|
||||
variant_message(&path_owned, initial[index].clone(), present)
|
||||
}
|
||||
)
|
||||
.placeholder("Choose stored value / constructor…")
|
||||
.text_size(12)
|
||||
.padding(8)
|
||||
.width(Fill)
|
||||
]
|
||||
.spacing(6);
|
||||
if let Some(index) = selected {
|
||||
let branch = resolve(&options[index]).unwrap_or(&options[index]);
|
||||
if branch_has_fields(branch) {
|
||||
out = out.push(render(
|
||||
draft,
|
||||
project,
|
||||
branch,
|
||||
Some(value),
|
||||
path,
|
||||
"",
|
||||
depth + 1,
|
||||
));
|
||||
}
|
||||
}
|
||||
return out.into();
|
||||
}
|
||||
if let Some(words) = schema.get("enum").and_then(Value::as_array) {
|
||||
let choices: Vec<_> = words
|
||||
.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.map(str::to_owned)
|
||||
.collect();
|
||||
let selected = value
|
||||
.as_str()
|
||||
.map(str::to_owned)
|
||||
.filter(|v| choices.contains(v));
|
||||
let path = path.to_string();
|
||||
return column![
|
||||
text(title.to_owned()).size(11).color(style::MUTED),
|
||||
pick_list(choices, selected, move |word| Message::FieldShape(
|
||||
path.clone(),
|
||||
json!(word)
|
||||
))
|
||||
.placeholder("Choose an explicit value…")
|
||||
.text_size(12)
|
||||
.padding(8)
|
||||
.width(Fill)
|
||||
]
|
||||
.spacing(6)
|
||||
.into();
|
||||
}
|
||||
match schema.get("type").and_then(Value::as_str) {
|
||||
Some("object") => {
|
||||
let Some(fields) = schema.get("properties").and_then(Value::as_object) else {
|
||||
return text("Unsupported stored-history object shape.").size(11).into();
|
||||
};
|
||||
let mut out = column![].spacing(8);
|
||||
if !title.is_empty() { out = out.push(text(title.to_owned()).size(12).color(style::INK)); }
|
||||
for (key, child_schema) in fields {
|
||||
out = out.push(render(draft, project, child_schema, value.get(key), &format!("{path}/{key}"), &label(key), depth + 1));
|
||||
}
|
||||
out.into()
|
||||
}
|
||||
Some("array") => {
|
||||
let Some(items) = value.as_array() else {
|
||||
return button("Initialize empty list").on_press(Message::FieldShape(path.into(), json!([]))).into();
|
||||
};
|
||||
let Some(item_schema) = schema.get("items") else { return text("Missing history list item type.").into(); };
|
||||
let mut out = column![text(title.to_owned()).size(12)].spacing(8);
|
||||
if path == FACETS {
|
||||
match fact_choices() {
|
||||
Ok(choices) => {
|
||||
let path = path.to_owned();
|
||||
out = out.push(pick_list(choices.clone(), None::<FactChoice>, move |choice| Message::FieldAppend(path.clone(), choice.value))
|
||||
.placeholder("Add: choose family and fact…").text_size(12).padding(8).width(Fill));
|
||||
}
|
||||
Err(error) => { out = out.push(text(error).size(11)); }
|
||||
}
|
||||
} else {
|
||||
match seed(item_schema, 0) {
|
||||
Ok(initial) => out = out.push(button("Add list value").on_press(Message::FieldAppend(path.into(), initial))),
|
||||
Err(error) => out = out.push(text(error).size(11)),
|
||||
}
|
||||
}
|
||||
let (page, pager) = paging(draft, path, items.len());
|
||||
if items.len() > PAGE_SIZE { out = out.push(pager); }
|
||||
for (index, item) in items.iter().enumerate().skip(page * PAGE_SIZE).take(PAGE_SIZE) {
|
||||
let item_path = format!("{path}/{index}");
|
||||
let mut card = column![render(draft, project, item_schema, Some(item), &item_path, &format!("{} {}", title, index + 1), depth + 1)].spacing(8);
|
||||
if path == FACETS {
|
||||
if let Some(current) = current_observation(draft, project, &item_path) {
|
||||
card = card.push(button("Use current counterpart observations")
|
||||
.on_press(Message::FieldShape(item_path.clone(), json!(current))).style(style::nav));
|
||||
}
|
||||
}
|
||||
card = card.push(row![
|
||||
button("↑").on_press_maybe((index > 0).then(|| Message::FieldArray(path.into(), Some(index), -1))),
|
||||
button("↓").on_press_maybe((index + 1 < items.len()).then(|| Message::FieldArray(path.into(), Some(index), 1))),
|
||||
button("Remove").on_press(Message::FieldArray(path.into(), Some(index), 0))
|
||||
].spacing(6));
|
||||
out = out.push(container(card).padding(10).style(style::subtle));
|
||||
}
|
||||
if items.is_empty() { out = out.push(text("Empty list · no stored entries").size(11).color(style::MUTED)); }
|
||||
out.into()
|
||||
}
|
||||
Some("boolean") => {
|
||||
let path = path.to_owned();
|
||||
checkbox(value.as_bool().unwrap_or(false)).label(title.to_owned())
|
||||
.on_toggle(move |v| Message::FieldShape(path.clone(), json!(v))).text_size(12).into()
|
||||
}
|
||||
Some("integer" | "number" | "string") => {
|
||||
let is_uuid = schema.get("format").and_then(Value::as_str) == Some("uuid");
|
||||
let numeric = schema.get("type").and_then(Value::as_str) != Some("string");
|
||||
let mut out = column![text(title.to_owned()).size(11).color(style::MUTED)].spacing(5);
|
||||
if let Some(input) = draft.inputs.get(path) {
|
||||
let owned = path.to_owned();
|
||||
if !numeric && !is_uuid && !draft.exact_strings {
|
||||
out = out.push(text_editor(&input.editor).on_action(move |a| Message::FieldText(owned.clone(), a)).size(12).height(72).padding(8));
|
||||
} else {
|
||||
out = out.push(text_input(if is_uuid { "Historical UUID, including deleted identities" } else { "Stored value" }, &input.raw)
|
||||
.on_input(move |raw| Message::Field(owned.clone(), raw)).size(12).padding(8));
|
||||
}
|
||||
} else {
|
||||
match seed(schema, 0) {
|
||||
Ok(value) => out = out.push(button("Set stored value").on_press(Message::FieldShape(path.into(), value))),
|
||||
Err(error) => out = out.push(text(error).size(11)),
|
||||
}
|
||||
}
|
||||
if is_uuid { out = out.push(text("Historical identity · existence in the current model is not required.").size(10).color(style::MUTED)); }
|
||||
out.into()
|
||||
}
|
||||
Some("null") => text("Absent (none)").size(11).into(),
|
||||
_ => text("Unsupported stored-history descriptor. The draft is retained; this field cannot be edited with these controls.").size(11).into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn current_observation(draft: &Draft, project: &Project, item_path: &str) -> Option<FacetReview> {
|
||||
let raw = draft
|
||||
.materialize_scopes(&[item_path.to_owned(), "/oa".into(), "/sa".into()])
|
||||
.ok()?;
|
||||
let fact: FacetReview = serde_json::from_value(raw.pointer(item_path)?.clone()).ok()?;
|
||||
let oa = raw.get("oa")?.as_str()?.parse().ok()?;
|
||||
let sa = raw.get("sa")?.as_str()?.parse().ok()?;
|
||||
semantic::capture_facet(project, oa, sa, &fact.selector()).ok()
|
||||
}
|
||||
|
||||
pub(super) fn view<'a>(
|
||||
draft: &'a Draft,
|
||||
project: &'a Project,
|
||||
value: &'a Value,
|
||||
) -> View<'a, Message> {
|
||||
let count = value
|
||||
.pointer("/semantic/facets")
|
||||
.and_then(Value::as_array)
|
||||
.map_or(0, Vec::len);
|
||||
let mut out = column![
|
||||
text("Stored review history").size(12).color(style::INK),
|
||||
text(if value.is_null() {
|
||||
"No stored baseline".into()
|
||||
} else {
|
||||
format!("Prose baseline · {count} semantic observations")
|
||||
})
|
||||
.size(11)
|
||||
.color(style::MUTED),
|
||||
button(if draft.history.open {
|
||||
"Close stored-history editor"
|
||||
} else {
|
||||
"Edit stored history (advanced)…"
|
||||
})
|
||||
.on_press(Message::FieldShape(
|
||||
TOGGLE.into(),
|
||||
json!(!draft.history.open)
|
||||
))
|
||||
.style(style::nav)
|
||||
]
|
||||
.spacing(8);
|
||||
if !draft.history.open {
|
||||
return out.into();
|
||||
}
|
||||
out = out.push(text("These edits change the trace's stored observations only. They do not change either counterpart. Ordinary Keep/Take review remains available from Traceability.").size(11));
|
||||
if value.is_null() {
|
||||
return match named("TraceBaseline").and_then(|s| seed(s, 0)) {
|
||||
Ok(initial) => out
|
||||
.push(
|
||||
button("Start empty stored baseline")
|
||||
.on_press(Message::FieldShape(ROOT.into(), initial)),
|
||||
)
|
||||
.into(),
|
||||
Err(error) => out.push(text(error).size(11)).into(),
|
||||
};
|
||||
}
|
||||
out = out.push(
|
||||
button("Clear whole stored baseline")
|
||||
.on_press(Message::FieldShape(ROOT.into(), Value::Null))
|
||||
.style(style::nav),
|
||||
);
|
||||
match named("TraceBaseline") {
|
||||
Ok(schema) => out = out.push(render(draft, project, schema, Some(value), ROOT, "", 0)),
|
||||
Err(error) => out = out.push(text(error).size(11)),
|
||||
}
|
||||
let checked = draft.materialize_scopes(&[ROOT.into()]).and_then(|raw| {
|
||||
let baseline: TraceBaseline =
|
||||
serde_json::from_value(raw["baseline"].clone()).map_err(|e| e.to_string())?;
|
||||
baseline.semantic.as_ref().map_or(Ok(()), |b| {
|
||||
semantic::validate_semantic_baseline(b).map_err(|e| e.join("; "))
|
||||
})
|
||||
});
|
||||
if let Err(error) = checked {
|
||||
out = out.push(text(format!("Stored history is incomplete or invalid: {error}")).size(11));
|
||||
}
|
||||
out.into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,733 @@
|
||||
use super::*;
|
||||
use crate::document::Document;
|
||||
|
||||
fn fixture(baseline: Option<TraceBaseline>) -> (Project, Id) {
|
||||
let mut project = Project::empty();
|
||||
let oa = project
|
||||
.add_element(Kind::Activity, Layer::Oa, "Assess need", None)
|
||||
.unwrap();
|
||||
let sa = project
|
||||
.add_element(Kind::Function, Layer::Sa, "Assess request", None)
|
||||
.unwrap();
|
||||
let id = Id::from_u128(9001);
|
||||
project.traces.insert(
|
||||
id,
|
||||
Trace {
|
||||
id,
|
||||
alias: "assessment_review".into(),
|
||||
name: "Assessment correspondence".into(),
|
||||
description: "Engineering review history".into(),
|
||||
oa,
|
||||
sa,
|
||||
kind: TraceKind::Refinement,
|
||||
baseline,
|
||||
},
|
||||
);
|
||||
validate_candidate(&project).unwrap();
|
||||
(project, id)
|
||||
}
|
||||
|
||||
fn baseline(facets: Vec<FacetReview>) -> TraceBaseline {
|
||||
TraceBaseline {
|
||||
oa: TracedProperties {
|
||||
name: "Earlier OA name".into(),
|
||||
description: "OA note".into(),
|
||||
},
|
||||
sa: TracedProperties {
|
||||
name: "Earlier SA name".into(),
|
||||
description: "SA note".into(),
|
||||
},
|
||||
semantic: Some(SemanticBaseline { facets }),
|
||||
}
|
||||
}
|
||||
|
||||
fn document(project: &Project) -> Document {
|
||||
let mut bundle = source_project::format_bundle(project);
|
||||
bundle
|
||||
.files
|
||||
.get_mut(&bundle.entry)
|
||||
.unwrap()
|
||||
.insert_str(0, "// Retain this authored project note.\n\n");
|
||||
Document::from_bundle(bundle, Default::default()).unwrap()
|
||||
}
|
||||
|
||||
fn choice(selector: FacetSelector) -> Value {
|
||||
fact_choices()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|choice| {
|
||||
serde_json::from_value::<FacetReview>(choice.value.clone())
|
||||
.unwrap()
|
||||
.selector()
|
||||
== selector
|
||||
})
|
||||
.unwrap()
|
||||
.value
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn add_header(draft: &mut Draft) {
|
||||
draft
|
||||
.append(FACETS, choice(FacetSelector::Header(HeaderFacet::Name)))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn assert_history_transaction(document: &mut Document, draft: &Draft, expected: &Project) {
|
||||
let before = document.project().clone();
|
||||
let source_before = document.bundle().clone();
|
||||
document.execute(draft.command().unwrap()).unwrap();
|
||||
assert_eq!(document.project(), expected);
|
||||
let accepted_source = document.bundle().clone();
|
||||
assert!(
|
||||
accepted_source.files[&accepted_source.entry]
|
||||
.starts_with("// Retain this authored project note.\n\n")
|
||||
);
|
||||
assert_eq!(
|
||||
Document::from_bundle(accepted_source.clone(), Default::default())
|
||||
.unwrap()
|
||||
.project(),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_slice::<Project>(&serde_json::to_vec(expected).unwrap()).unwrap(),
|
||||
*expected
|
||||
);
|
||||
assert!(document.undo());
|
||||
assert_eq!(document.project(), &before);
|
||||
assert_eq!(document.bundle(), &source_before);
|
||||
assert!(
|
||||
!document.undo(),
|
||||
"one stored-history Apply must create one history entry"
|
||||
);
|
||||
assert!(document.redo());
|
||||
assert_eq!(document.project(), expected);
|
||||
assert_eq!(document.bundle(), &accepted_source);
|
||||
assert!(!document.redo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_absent_baseline_and_empty_history_use_explicit_facts_and_exact_document_history() {
|
||||
for initial in [None, Some(baseline(vec![]))] {
|
||||
let (project, id) = fixture(initial);
|
||||
let mut doc = document(&project);
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
assert!(!draft.history.open);
|
||||
draft.shape(TOGGLE, json!(true)).unwrap();
|
||||
assert!(draft.history.open);
|
||||
assert!(
|
||||
!draft.dirty(),
|
||||
"opening the advanced editor cannot acknowledge history"
|
||||
);
|
||||
if draft.value["baseline"].is_null() {
|
||||
draft
|
||||
.shape(ROOT, seed(named("TraceBaseline").unwrap(), 0).unwrap())
|
||||
.unwrap();
|
||||
draft
|
||||
.shape("/baseline/semantic", json!({"facets":[]}))
|
||||
.unwrap();
|
||||
}
|
||||
let untouched = draft.value.clone();
|
||||
assert!(
|
||||
draft
|
||||
.array(FACETS, None, 0)
|
||||
.unwrap_err()
|
||||
.contains("Choose a typed")
|
||||
);
|
||||
assert_eq!(
|
||||
draft.value, untouched,
|
||||
"the old nil-UUID Add must not mutate the draft"
|
||||
);
|
||||
add_header(&mut draft);
|
||||
let before = doc.snapshot();
|
||||
assert!(
|
||||
doc.execute(draft.command().unwrap()).is_err(),
|
||||
"NA/NA is incomplete, not a reviewed default"
|
||||
);
|
||||
assert!(std::sync::Arc::ptr_eq(&before, &doc.snapshot()));
|
||||
let current = current_observation(&draft, &project, &format!("{FACETS}/0")).unwrap();
|
||||
assert_eq!(
|
||||
current,
|
||||
semantic::capture_facet(
|
||||
&project,
|
||||
project.traces[&id].oa,
|
||||
project.traces[&id].sa,
|
||||
&FacetSelector::Header(HeaderFacet::Name)
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
draft.shape(&format!("{FACETS}/0"), json!(current)).unwrap();
|
||||
let mut expected = project.clone();
|
||||
expected.traces.get_mut(&id).unwrap().baseline =
|
||||
Some(serde_json::from_value(draft.materialize().unwrap()["baseline"].clone()).unwrap());
|
||||
assert_history_transaction(&mut doc, &draft, &expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_missing_semantic_field_can_be_added_and_removed_without_touching_prose_or_live_objects()
|
||||
{
|
||||
let mut prose = baseline(vec![]);
|
||||
prose.semantic = None;
|
||||
let (project, id) = fixture(Some(prose));
|
||||
let mut doc = document(&project);
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
assert!(draft.value.pointer("/baseline/semantic").is_none());
|
||||
draft
|
||||
.shape("/baseline/semantic", json!({"facets":[]}))
|
||||
.unwrap();
|
||||
let mut expected = project.clone();
|
||||
expected
|
||||
.traces
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic = Some(SemanticBaseline::default());
|
||||
assert_history_transaction(&mut doc, &draft, &expected);
|
||||
let mut restored = Draft::from_project(&expected, id).unwrap();
|
||||
restored.shape("/baseline/semantic", Value::Null).unwrap();
|
||||
assert_eq!(
|
||||
execute_batch(&expected, &[restored.command().unwrap()]).unwrap(),
|
||||
project
|
||||
);
|
||||
}
|
||||
|
||||
/// Generate type witnesses independently of the editor's deliberately incomplete
|
||||
/// seeds, then ask the real closed Rust types and historical validator to admit them.
|
||||
fn witness(schema: &'static Value, active: &Value, depth: usize) -> Value {
|
||||
assert!(
|
||||
depth <= MAX_DEPTH,
|
||||
"recursive/unsupported historical descriptor"
|
||||
);
|
||||
let schema = resolve(schema).unwrap();
|
||||
if let Some(options) = alternatives(schema) {
|
||||
let selected = if !active.is_null() && active != "NotApplicable" {
|
||||
options.iter().find(|s| matches_value(s, active, 0))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let selected = selected
|
||||
.or_else(|| {
|
||||
options.iter().find(|s| {
|
||||
!matches_value(s, &Value::Null, 0)
|
||||
&& !matches_value(s, &json!("NotApplicable"), 0)
|
||||
})
|
||||
})
|
||||
.unwrap_or(&options[0]);
|
||||
return witness(selected, active, depth + 1);
|
||||
}
|
||||
if let Some(values) = schema.get("enum").and_then(Value::as_array) {
|
||||
return if values.contains(active) {
|
||||
active.clone()
|
||||
} else {
|
||||
values[0].clone()
|
||||
};
|
||||
}
|
||||
match schema["type"].as_str().unwrap() {
|
||||
"object" => schema["properties"]
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|(key, s)| {
|
||||
(
|
||||
key.clone(),
|
||||
witness(s, active.get(key).unwrap_or(&Value::Null), depth + 1),
|
||||
)
|
||||
})
|
||||
.collect::<serde_json::Map<_, _>>()
|
||||
.into(),
|
||||
"array" => json!([witness(&schema["items"], &Value::Null, depth + 1)]),
|
||||
"string" if schema["format"] == "uuid" => json!(Id::from_u128(777)),
|
||||
"string" => json!("Earlier Ω value\nwith provenance"),
|
||||
"integer" => json!(1),
|
||||
"number" => json!(1.25),
|
||||
"boolean" => json!(true),
|
||||
"null" => Value::Null,
|
||||
other => panic!("unsupported historical schema {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_every_descriptor_fact_choice_matches_actual_closed_types_and_source_contract() {
|
||||
let (project, id) = fixture(Some(baseline(vec![])));
|
||||
let mut selectors = BTreeSet::new();
|
||||
let choices = fact_choices().unwrap();
|
||||
assert!(
|
||||
choices.len() > 100,
|
||||
"the full scalar and keyed catalog must be reachable"
|
||||
);
|
||||
for entry in choices {
|
||||
let incomplete: FacetReview = serde_json::from_value(entry.value.clone()).unwrap();
|
||||
assert!(
|
||||
selectors.insert(incomplete.selector()),
|
||||
"duplicate Add selector: {}",
|
||||
entry.label
|
||||
);
|
||||
assert!(
|
||||
semantic::validate_semantic_baseline(&SemanticBaseline {
|
||||
facets: vec![incomplete]
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
let complete = witness(named("FacetReview").unwrap(), &entry.value, 0);
|
||||
let typed: FacetReview = serde_json::from_value(complete.clone())
|
||||
.unwrap_or_else(|e| panic!("{}: {e}", entry.label));
|
||||
semantic::validate_semantic_baseline(&SemanticBaseline {
|
||||
facets: vec![typed.clone()],
|
||||
})
|
||||
.unwrap_or_else(|e| panic!("{}: {e:?}", entry.label));
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
draft.append(FACETS, complete).unwrap();
|
||||
draft.history.open = true;
|
||||
let _controls = view(&draft, &project, &draft.value["baseline"]);
|
||||
let result = execute_batch(&project, &[draft.command().unwrap()]).unwrap();
|
||||
let parsed = source_project::compile_bundle(
|
||||
&source_project::format_bundle(&result),
|
||||
Default::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
parsed.decoded.project, result,
|
||||
"source parity for {}",
|
||||
entry.label
|
||||
);
|
||||
assert_eq!(
|
||||
result.traces[&id]
|
||||
.baseline
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.semantic
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.facets,
|
||||
vec![typed]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_deleted_keys_and_observation_uuids_remain_exact_and_duplicate_history_rejects() {
|
||||
let old_link = Id::from_u128(401);
|
||||
let old_owner = Id::from_u128(402);
|
||||
let fact = FacetReview::Artifact {
|
||||
keys: KeyPair {
|
||||
oa: Some(old_link),
|
||||
sa: None,
|
||||
},
|
||||
review: ArtifactReview::Owner(Reviewed {
|
||||
oa: Observed::Value(old_owner),
|
||||
sa: Observed::NotApplicable,
|
||||
}),
|
||||
};
|
||||
let (project, id) = fixture(Some(baseline(vec![fact])));
|
||||
assert!(!project.contains(old_link) && !project.contains(old_owner));
|
||||
let mut doc = document(&project);
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
let key_path = format!("{FACETS}/0/Artifact/keys/oa");
|
||||
let owner_path = format!("{FACETS}/0/Artifact/review/Owner/oa/Value");
|
||||
assert!(!free_string(&key_path) && !free_string(&owner_path));
|
||||
let edited_key = Id::from_u128(403);
|
||||
let edited_owner = Id::from_u128(404);
|
||||
draft.input(&key_path, edited_key.to_string());
|
||||
draft.input(&owner_path, "bad uuid".into());
|
||||
assert!(draft.command().is_err());
|
||||
draft.input(&owner_path, edited_owner.to_string());
|
||||
let accepted = draft
|
||||
.materialize()
|
||||
.unwrap()
|
||||
.pointer(&format!("{FACETS}/0"))
|
||||
.unwrap()
|
||||
.clone();
|
||||
draft.append(FACETS, accepted.clone()).unwrap();
|
||||
let before = doc.snapshot();
|
||||
assert!(
|
||||
doc.execute(draft.command().unwrap()).is_err(),
|
||||
"overlapping historic addresses must reject"
|
||||
);
|
||||
assert!(std::sync::Arc::ptr_eq(&before, &doc.snapshot()));
|
||||
draft.array(FACETS, Some(1), 0).unwrap();
|
||||
let mut expected = project.clone();
|
||||
expected
|
||||
.traces
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.facets = vec![serde_json::from_value(accepted).unwrap()];
|
||||
assert_history_transaction(&mut doc, &draft, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_invalid_numbers_survive_constructor_switch_reorder_removal_and_recovery() {
|
||||
let (project, id) = fixture(Some(baseline(vec![])));
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
draft
|
||||
.append(
|
||||
FACETS,
|
||||
choice(FacetSelector::Data(DataFacet::IntegerMinimum)),
|
||||
)
|
||||
.unwrap();
|
||||
let side = format!("{FACETS}/0/Data/IntegerMinimum/oa");
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{side}"), json!({"Value": 7}))
|
||||
.unwrap();
|
||||
draft.input(&format!("{side}/Value"), "-".into());
|
||||
add_header(&mut draft);
|
||||
assert_eq!(draft.inputs[&format!("{side}/Value")].raw, "-");
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{side}"), json!("NotApplicable"))
|
||||
.unwrap();
|
||||
draft.array(FACETS, Some(0), 1).unwrap();
|
||||
let moved_side = format!("{FACETS}/1/Data/IntegerMinimum/oa");
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{moved_side}"), json!({"Value": 0}))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
draft.inputs[&format!("{moved_side}/Value")].raw,
|
||||
"-",
|
||||
"inactive constructor drafts follow the historical row"
|
||||
);
|
||||
let saved = draft.recovery_snapshot();
|
||||
let mut recovered = Draft::from_recovery(&project, saved).unwrap();
|
||||
assert!(recovered.history.open);
|
||||
assert_eq!(recovered.inputs[&format!("{moved_side}/Value")].raw, "-");
|
||||
assert!(recovered.command().is_err());
|
||||
recovered.array(FACETS, Some(0), 0).unwrap();
|
||||
assert_eq!(recovered.inputs[&format!("{side}/Value")].raw, "-");
|
||||
recovered.input(&format!("{side}/Value"), i64::MIN.to_string());
|
||||
let mut doc = document(&project);
|
||||
let mut expected = project.clone();
|
||||
expected
|
||||
.traces
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic = Some(SemanticBaseline {
|
||||
facets: vec![FacetReview::Data(DataReview::IntegerMinimum(Reviewed {
|
||||
oa: Observed::Value(Some(i64::MIN)),
|
||||
sa: Observed::NotApplicable,
|
||||
}))],
|
||||
});
|
||||
assert_history_transaction(&mut doc, &recovered, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_compound_value_and_constructor_variants_are_editable_without_live_context() {
|
||||
let (project, id) = fixture(Some(baseline(vec![])));
|
||||
let old = Id::from_u128(700);
|
||||
let values = vec![
|
||||
FacetReview::Data(DataReview::Default(Reviewed {
|
||||
oa: Observed::Value(Some(DataValue::Boolean(true))),
|
||||
sa: Observed::NotApplicable,
|
||||
})),
|
||||
FacetReview::Field(FieldReview::Type(Reviewed {
|
||||
oa: Observed::Value(TypeRef::Named(old)),
|
||||
sa: Observed::Value(TypeRef::Real),
|
||||
})),
|
||||
FacetReview::Occurrence(OccurrenceReview::Operation(Reviewed {
|
||||
oa: Observed::NotApplicable,
|
||||
sa: Observed::Value(Some(OperationUse {
|
||||
allocation: Some(old),
|
||||
})),
|
||||
})),
|
||||
FacetReview::Relation(RelationReview::ContextSource(Reviewed {
|
||||
oa: Observed::Value(OccurrenceEndpoint {
|
||||
references: vec![old, Id::from_u128(701)],
|
||||
occurrence: Id::from_u128(702),
|
||||
}),
|
||||
sa: Observed::NotApplicable,
|
||||
})),
|
||||
FacetReview::Relation(RelationReview::ContextPurpose(Reviewed {
|
||||
oa: Observed::Value(ContextualLinkPurpose::CommonWork { work: old }),
|
||||
sa: Observed::NotApplicable,
|
||||
})),
|
||||
FacetReview::Annotation {
|
||||
keys: KeyPair {
|
||||
oa: Some(AnnotationKey {
|
||||
path: vec![old],
|
||||
field: old,
|
||||
}),
|
||||
sa: None,
|
||||
},
|
||||
review: Reviewed {
|
||||
oa: Observed::Value(Some(MessageValue {
|
||||
path: vec![old],
|
||||
field: old,
|
||||
expression: "verbatim\tΩ".into(),
|
||||
})),
|
||||
sa: Observed::NotApplicable,
|
||||
},
|
||||
},
|
||||
FacetReview::Binding {
|
||||
keys: KeyPair {
|
||||
oa: Some(old),
|
||||
sa: None,
|
||||
},
|
||||
review: Reviewed {
|
||||
oa: Observed::Value(Some(LifelineBinding {
|
||||
referenced: old,
|
||||
local: Id::from_u128(702),
|
||||
})),
|
||||
sa: Observed::NotApplicable,
|
||||
},
|
||||
},
|
||||
];
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
for value in &values {
|
||||
draft.append(FACETS, json!(value)).unwrap();
|
||||
}
|
||||
let literal = format!("{FACETS}/0/Data/Default/oa/Value");
|
||||
for value in [
|
||||
DataValue::Integer(i64::MAX),
|
||||
DataValue::Real(-0.25),
|
||||
DataValue::String("quoted\r\nΩ".into()),
|
||||
DataValue::Enumeration("Literal".into()),
|
||||
] {
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{literal}"), json!(value))
|
||||
.unwrap();
|
||||
let model = execute_batch(&project, &[draft.command().unwrap()]).unwrap();
|
||||
assert!(
|
||||
matches!(&model.traces[&id].baseline.as_ref().unwrap().semantic.as_ref().unwrap().facets[0], FacetReview::Data(DataReview::Default(Reviewed { oa: Observed::Value(Some(actual)), .. })) if actual == &value)
|
||||
);
|
||||
}
|
||||
let purpose = format!("{FACETS}/4/Relation/ContextPurpose/oa/Value");
|
||||
draft
|
||||
.shape(
|
||||
&format!("{VARIANT}{purpose}"),
|
||||
json!({"Precedence":{"guard":"reviewed before dispatch"}}),
|
||||
)
|
||||
.unwrap();
|
||||
let mut doc = document(&project);
|
||||
let mut expected = project.clone();
|
||||
expected.traces.get_mut(&id).unwrap().baseline =
|
||||
Some(serde_json::from_value(draft.materialize().unwrap()["baseline"].clone()).unwrap());
|
||||
assert_history_transaction(&mut doc, &draft, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_exact_strings_do_not_confuse_uuid_shaped_prose_or_literal_keys_with_references() {
|
||||
let (project, id) = fixture(Some(baseline(vec![])));
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
add_header(&mut draft);
|
||||
let side = format!("{FACETS}/0/Header/Name/oa");
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{side}"), json!({"Value":""}))
|
||||
.unwrap();
|
||||
let path = format!("{side}/Value");
|
||||
assert!(free_string(&path));
|
||||
draft.set_exact(true).unwrap();
|
||||
let value = "00000000-0000-0000-0000-000000000077\tΩ\r\nname";
|
||||
draft.input(&path, serde_json::to_string(value).unwrap());
|
||||
draft
|
||||
.append(
|
||||
FACETS,
|
||||
choice(FacetSelector::Literal(KeyPair { oa: None, sa: None })),
|
||||
)
|
||||
.unwrap();
|
||||
let key = format!("{FACETS}/1/Literal/keys/oa");
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{key}"), json!("literal"))
|
||||
.unwrap();
|
||||
draft.input(&key, serde_json::to_string("literal\tΩ").unwrap());
|
||||
draft
|
||||
.shape(
|
||||
&format!("{VARIANT}{FACETS}/1/Literal/review/oa"),
|
||||
json!({"Value":true}),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(free_string(&key));
|
||||
let mut expected = project.clone();
|
||||
expected
|
||||
.traces
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic = Some(SemanticBaseline {
|
||||
facets: vec![
|
||||
FacetReview::Header(HeaderReview::Name(Reviewed {
|
||||
oa: Observed::Value(value.into()),
|
||||
sa: Observed::NotApplicable,
|
||||
})),
|
||||
FacetReview::Literal {
|
||||
keys: KeyPair {
|
||||
oa: Some("literal\tΩ".into()),
|
||||
sa: None,
|
||||
},
|
||||
review: Reviewed {
|
||||
oa: Observed::Value(true),
|
||||
sa: Observed::NotApplicable,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
assert_history_transaction(&mut document(&project), &draft, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_primitive_type_choices_remain_visible_inside_the_tagged_union() {
|
||||
let schema = resolve(named("TypeRef").unwrap()).unwrap();
|
||||
let branches = alternatives(schema).unwrap();
|
||||
let primitives = branches
|
||||
.iter()
|
||||
.find(|branch| {
|
||||
resolve(branch)
|
||||
.unwrap()
|
||||
.get("enum")
|
||||
.and_then(Value::as_array)
|
||||
.is_some_and(|values| values.len() > 1)
|
||||
})
|
||||
.unwrap();
|
||||
assert!(
|
||||
branch_has_fields(primitives),
|
||||
"the primitive branch needs an inner picker, not only its outer union label"
|
||||
);
|
||||
let (project, id) = fixture(Some(baseline(vec![])));
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
draft
|
||||
.append(
|
||||
FACETS,
|
||||
json!(FacetReview::Field(FieldReview::Type(Reviewed {
|
||||
oa: Observed::Value(TypeRef::Named(Id::from_u128(888))),
|
||||
sa: Observed::NotApplicable,
|
||||
}))),
|
||||
)
|
||||
.unwrap();
|
||||
let path = format!("{FACETS}/0/Field/Type/oa/Value");
|
||||
draft
|
||||
.shape(
|
||||
&format!("{VARIANT}{path}"),
|
||||
branch_seed(primitives).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
for value in [
|
||||
TypeRef::Boolean,
|
||||
TypeRef::Integer,
|
||||
TypeRef::Real,
|
||||
TypeRef::String,
|
||||
] {
|
||||
assert!(matches_value(primitives, &json!(value), 0));
|
||||
draft.shape(&path, json!(value)).unwrap();
|
||||
let mut expected = project.clone();
|
||||
expected
|
||||
.traces
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic = Some(SemanticBaseline {
|
||||
facets: vec![FacetReview::Field(FieldReview::Type(Reviewed {
|
||||
oa: Observed::Value(value),
|
||||
sa: Observed::NotApplicable,
|
||||
}))],
|
||||
});
|
||||
assert_history_transaction(&mut document(&project), &draft, &expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_retained_text_constructors_follow_exact_mode_without_changing_the_observation() {
|
||||
let (project, id) = fixture(Some(baseline(vec![])));
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
add_header(&mut draft);
|
||||
let side = format!("{FACETS}/0/Header/Name/oa");
|
||||
let path = format!("{side}/Value");
|
||||
let stored = "Earlier \"quoted\" name\tΩ\r\nsecond line";
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{side}"), json!({"Value":""}))
|
||||
.unwrap();
|
||||
draft.input(&path, stored.into());
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{side}"), json!("NotApplicable"))
|
||||
.unwrap();
|
||||
draft.set_exact(true).unwrap();
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{side}"), json!({"Value":""}))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
draft.inputs[&path].raw,
|
||||
serde_json::to_string(stored).unwrap()
|
||||
);
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{side}"), json!("NotApplicable"))
|
||||
.unwrap();
|
||||
draft.set_exact(false).unwrap();
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{side}"), json!({"Value":""}))
|
||||
.unwrap();
|
||||
assert_eq!(draft.inputs[&path].raw, stored);
|
||||
let mut expected = project.clone();
|
||||
expected
|
||||
.traces
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic = Some(SemanticBaseline {
|
||||
facets: vec![FacetReview::Header(HeaderReview::Name(Reviewed {
|
||||
oa: Observed::Value(stored.into()),
|
||||
sa: Observed::NotApplicable,
|
||||
}))],
|
||||
});
|
||||
assert_history_transaction(&mut document(&project), &draft, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_clear_discards_inactive_invalid_constructors_before_reusing_the_same_row() {
|
||||
let (project, id) = fixture(Some(baseline(vec![])));
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
add_header(&mut draft);
|
||||
let side = format!("{FACETS}/0/Header/Name/oa");
|
||||
let path = format!("{side}/Value");
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{side}"), json!({"Value":""}))
|
||||
.unwrap();
|
||||
draft.set_exact(true).unwrap();
|
||||
draft.input(&path, "\"unfinished".into());
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{side}"), json!("NotApplicable"))
|
||||
.unwrap();
|
||||
assert!(
|
||||
draft.set_exact(false).is_err(),
|
||||
"invalid retained quoted text cannot silently change modes"
|
||||
);
|
||||
assert!(draft.exact_strings);
|
||||
draft.shape(ROOT, Value::Null).unwrap();
|
||||
assert!(draft.constructor_drafts.is_empty());
|
||||
draft.set_exact(false).unwrap();
|
||||
draft.shape(ROOT, json!(baseline(vec![]))).unwrap();
|
||||
add_header(&mut draft);
|
||||
draft
|
||||
.shape(
|
||||
&format!("{VARIANT}{side}"),
|
||||
json!({"Value":"Fresh observation"}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(draft.inputs[&path].raw, "Fresh observation");
|
||||
let mut expected = project.clone();
|
||||
expected
|
||||
.traces
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic = Some(SemanticBaseline {
|
||||
facets: vec![FacetReview::Header(HeaderReview::Name(Reviewed {
|
||||
oa: Observed::Value("Fresh observation".into()),
|
||||
sa: Observed::NotApplicable,
|
||||
}))],
|
||||
});
|
||||
assert_history_transaction(&mut document(&project), &draft, &expected);
|
||||
}
|
||||
Reference in New Issue
Block a user