test: verify compact default policy against real model round trips

This commit is contained in:
2026-09-13 03:36:42 -07:00
parent 6f2aae0f76
commit 2a64344b92
21 changed files with 6226 additions and 0 deletions
@@ -0,0 +1,349 @@
//! Test-only fidelity bridge. Reuse the production canonical renderer and remove
//! only policy-selected field spans; never regenerate a value or reference.
//! This is not a source editor, production formatter, or general syntax parser.
#[path = "source-defaults-20260913.rs"]
mod policy;
use oa_core::{Id, Project, language};
use policy::{Context, Mode};
use serde_json::{Map, Value};
use std::collections::{BTreeMap, BTreeSet};
pub struct Probe {
pub canonical: String,
pub compact: String,
pub removed_fields: usize,
}
pub fn compact(project: &Project) -> Probe {
assert_eq!(project.schema_version, 7, "this probe is source7 only");
let canonical = language::format(project);
assert_eq!(
language::parse(&canonical).unwrap(),
*project,
"original renderer must admit the entire model first"
);
let records = extracted_fields(project);
let outline = language::outline(
&canonical,
language::OutlineLimits {
max_source_bytes: usize::MAX,
max_tokens: usize::MAX,
max_declarations: usize::MAX,
},
)
.unwrap();
assert!(!outline.has_lexical_errors && !outline.has_unmatched_braces);
let mut edits = Vec::new();
let mut seen = BTreeSet::new();
for entry in outline.declarations {
let declaration = entry.declaration;
let id = declaration.id.expect("canonical explicit identity");
let start = declaration.span.start;
let end = declaration.span.end;
let mut reader = Reader::new(&canonical[start..end]);
reader.seek_body();
if id == project.id {
// Project headers are outside the field policy. Its canonical first
// field is description; leave all following declarations to outline.
let description = reader.field();
assert_eq!(description.name, "description");
if project.description.is_empty() {
edits.push(line_span(
&canonical,
start + description.start,
start + description.end,
));
}
continue;
}
assert!(seen.insert(id), "duplicate canonical identity");
let (context, input) = records
.get(&id)
.expect("every canonical record maps to a typed model record");
let complete = policy::fields(*context, input, Mode::CompleteForEdit);
let reduced = policy::fields(*context, &complete, Mode::Compact);
assert_subset(&Value::Object(complete), &Value::Object(reduced.clone()));
let fields = reader.fields();
reader.expect("}");
assert_eq!(
reader.cursor,
reader.tokens.len(),
"consume exactly one canonical declaration"
);
prune_record(&canonical, start, &fields, &reduced, &mut edits);
}
assert_eq!(
seen,
records.keys().copied().collect(),
"every model record must be exercised"
);
edits.sort_unstable();
for pair in edits.windows(2) {
assert!(pair[0].1 <= pair[1].0, "field deletions must not overlap");
}
let removed_fields = edits.len();
let mut result = canonical.clone();
for (start, end) in edits.into_iter().rev() {
result.replace_range(start..end, "");
}
Probe {
canonical,
compact: result,
removed_fields,
}
}
fn extracted_fields(project: &Project) -> BTreeMap<Id, (Context, Map<String, Value>)> {
let mut records = BTreeMap::new();
for element in project.elements.values() {
let mut fields = payload(serde_json::to_value(&element.body).unwrap());
fields.insert(
"description".into(),
Value::String(element.description.clone()),
);
fields.insert(
"parent".into(),
serde_json::to_value(element.parent).unwrap(),
);
records.insert(element.id, (Context::Element(element.kind()), fields));
}
for relation in project.relations.values() {
let mut fields = payload(serde_json::to_value(&relation.body).unwrap());
fields.insert(
"description".into(),
Value::String(relation.description.clone()),
);
records.insert(relation.id, (Context::Relation(relation.kind()), fields));
}
for diagram in project.diagrams.values() {
let mut fields = record_fields(diagram);
fields.remove("nodes");
records.insert(diagram.id, (Context::Diagram, fields));
for node in diagram.nodes.values() {
let mut fields = record_fields(node);
fields.insert("diagram".into(), Value::String(diagram.id.to_string()));
records.insert(node.id, (Context::Node, fields));
}
}
for artifact in project.artifact_links.values() {
records.insert(artifact.id, (Context::Artifact, record_fields(artifact)));
}
for trace in project.traces.values() {
records.insert(trace.id, (Context::Trace, record_fields(trace)));
}
records
}
fn record_fields(value: &impl serde::Serialize) -> Map<String, Value> {
let mut fields = serde_json::to_value(value)
.unwrap()
.as_object()
.unwrap()
.clone();
for key in ["id", "alias", "name"] {
fields.remove(key);
}
fields
}
fn payload(value: Value) -> Map<String, Value> {
match value {
Value::Object(fields)
if fields.len() == 1
&& fields
.keys()
.next()
.unwrap()
.starts_with(char::is_uppercase) =>
{
payload(fields.into_values().next().unwrap())
}
Value::Object(fields) => fields,
Value::String(_) => Map::new(),
_ => panic!("typed declaration payload"),
}
}
// The policy may only delete fields. It must never silently replace a value,
// change a number's sign, shorten an ordered array, or add an invented field.
fn assert_subset(before: &Value, after: &Value) {
match (before, after) {
(Value::Object(before), Value::Object(after)) => {
for (key, value) in after {
assert_subset(before.get(key).expect("no added field"), value);
}
}
(Value::Array(before), Value::Array(after)) => {
assert_eq!(before.len(), after.len());
for (a, b) in before.iter().zip(after) {
assert_subset(a, b);
}
}
(Value::Number(a), Value::Number(b)) => {
assert_eq!(a, b);
if a.as_f64() == Some(0.0) {
assert_eq!(a.as_f64().unwrap().to_bits(), b.as_f64().unwrap().to_bits());
}
}
_ => assert_eq!(before, after),
}
}
enum SyntaxValue {
Record(Vec<Field>),
Array(Vec<SyntaxValue>),
Scalar,
}
struct Field {
name: String,
start: usize,
end: usize,
value: SyntaxValue,
}
struct Reader<'a> {
source: &'a str,
tokens: Vec<language::TokenSpan>,
cursor: usize,
}
impl<'a> Reader<'a> {
fn new(source: &'a str) -> Self {
let tokens = language::tokenize(source)
.into_iter()
.filter(|token| {
!matches!(
token.class,
language::TokenClass::Whitespace | language::TokenClass::Comment
)
})
.collect();
Self {
source,
tokens,
cursor: 0,
}
}
fn text(&self) -> &str {
let span = self.tokens[self.cursor].span;
&self.source[span.start..span.end]
}
fn expect(&mut self, text: &str) {
assert_eq!(self.text(), text);
self.cursor += 1;
}
fn seek_body(&mut self) {
while self.text() != "{" {
self.cursor += 1;
}
self.expect("{");
}
fn fields(&mut self) -> Vec<Field> {
let mut fields = Vec::new();
while self.text() != "}" {
fields.push(self.field());
}
fields
}
fn field(&mut self) -> Field {
let start = self.tokens[self.cursor].span.start;
let name = self.text().to_owned();
self.cursor += 1;
let value = self.value();
let end = self.tokens[self.cursor].span.end;
self.expect(";");
Field {
name,
start,
end,
value,
}
}
fn value(&mut self) -> SyntaxValue {
match self.text() {
"{" => {
self.expect("{");
let fields = self.fields();
self.expect("}");
SyntaxValue::Record(fields)
}
"[" => {
self.expect("[");
let mut values = Vec::new();
while self.text() != "]" {
values.push(self.value());
if self.text() != "]" {
self.expect(",");
}
}
self.expect("]");
SyntaxValue::Array(values)
}
"@" | "uuid" => {
self.cursor += 2;
SyntaxValue::Scalar
}
_ => {
self.cursor += 1;
SyntaxValue::Scalar
}
}
}
}
fn line_span(source: &str, start: usize, end: usize) -> (usize, usize) {
let line = source[..start].rfind('\n').map_or(0, |offset| offset + 1);
assert!(
source[line..start].chars().all(|c| c == ' '),
"canonical field starts on its own indented line"
);
assert_eq!(source.as_bytes().get(end), Some(&b'\n'));
(line, end + 1)
}
fn prune_record(
source: &str,
base: usize,
fields: &[Field],
kept: &Map<String, Value>,
edits: &mut Vec<(usize, usize)>,
) {
for key in kept.keys() {
assert!(
fields.iter().any(|field| &field.name == key),
"retained field missing from rendered source: {key}"
);
}
for field in fields {
if let Some(value) = kept.get(&field.name) {
prune_value(source, base, &field.value, value, edits);
} else {
edits.push(line_span(source, base + field.start, base + field.end));
}
}
}
fn prune_value(
source: &str,
base: usize,
syntax: &SyntaxValue,
kept: &Value,
edits: &mut Vec<(usize, usize)>,
) {
match (syntax, kept) {
(SyntaxValue::Record(fields), Value::Object(values)) => {
prune_record(source, base, fields, values, edits)
}
(SyntaxValue::Array(fields), Value::Array(values)) => {
assert_eq!(fields.len(), values.len());
for (field, value) in fields.iter().zip(values) {
prune_value(source, base, field, value, edits);
}
}
(
SyntaxValue::Scalar,
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_),
) => {}
_ => panic!("canonical syntax must match the typed value shape"),
}
}