feat(source): persist typed semantic reviews in schema five
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
//! Typed OA/SA language, tolerant source analysis, and explicit V1/V2/V3 migration.
|
||||
//! Typed OA/SA language, tolerant source analysis, and explicit V1/V2/V3/V4 migration.
|
||||
#[path = "language/semantic_source.rs"]
|
||||
mod semantic_source;
|
||||
#[path = "migration_v2/source.rs"]
|
||||
mod v2_source;
|
||||
use crate::*;
|
||||
@@ -387,7 +389,7 @@ fn enum_field(key: &str) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
/// Parse V4, or explicitly migrate a valid V1/V2/V3 source candidate. Parsing never
|
||||
/// Parse V5, or explicitly migrate a valid V1/V2/V3/V4 source candidate. Parsing never
|
||||
/// adopts a model and never overwrites an editor draft.
|
||||
pub fn parse(source: &str) -> Result<Project, Vec<Diagnostic>> {
|
||||
parse_limited(source, ParseLimits::UNLIMITED)
|
||||
@@ -552,7 +554,7 @@ fn offset_for(source: &str, line: usize, column: usize) -> usize {
|
||||
/// nested records are ordinary language fields; JSON documents are not embedded.
|
||||
pub fn format(project: &Project) -> String {
|
||||
let mut out = std::format!(
|
||||
"arcadia 4;\nproject {} {} id \"{}\" {{\n description {};\n\n",
|
||||
"arcadia {SCHEMA_VERSION};\nproject {} {} id \"{}\" {{\n description {};\n\n",
|
||||
project.alias,
|
||||
quote(&project.name),
|
||||
project.id,
|
||||
@@ -743,6 +745,10 @@ fn write_value(
|
||||
indent: usize,
|
||||
aliases: &BTreeMap<Id, String>,
|
||||
) {
|
||||
if key == "semantic" {
|
||||
semantic_source::write(out, value, indent);
|
||||
return;
|
||||
}
|
||||
match value {
|
||||
Value::Null => out.push_str("none"),
|
||||
Value::Bool(v) => out.push_str(if *v { "true" } else { "false" }),
|
||||
@@ -786,6 +792,7 @@ enum Ast {
|
||||
Literal(Value, usize),
|
||||
Word(String, usize),
|
||||
Reference(String, usize),
|
||||
HistoricalUuid(Id, usize),
|
||||
Array(Vec<Ast>, usize),
|
||||
Object(BTreeMap<String, Ast>, usize),
|
||||
}
|
||||
@@ -795,12 +802,20 @@ impl Ast {
|
||||
Self::Literal(_, p)
|
||||
| Self::Word(_, p)
|
||||
| Self::Reference(_, p)
|
||||
| Self::HistoricalUuid(_, p)
|
||||
| Self::Array(_, p)
|
||||
| Self::Object(_, p) => *p,
|
||||
}
|
||||
}
|
||||
fn resolve(&self, key: &str, aliases: &BTreeMap<String, Id>) -> Result<Value, Error> {
|
||||
match self {
|
||||
Self::HistoricalUuid(_, _) => Err(Error {
|
||||
code: "invalid_source",
|
||||
offset: self.offset(),
|
||||
message:
|
||||
"A uuid literal is allowed only in a typed historical semantic reference slot."
|
||||
.into(),
|
||||
}),
|
||||
Self::Word(word, offset) => {
|
||||
if !enum_field(key) {
|
||||
return Err(Error {
|
||||
@@ -968,6 +983,7 @@ pub(crate) struct EditValue {
|
||||
pub(crate) enum EditKind {
|
||||
Atom,
|
||||
Reference(String),
|
||||
HistoricalUuid(Id),
|
||||
Record(BTreeMap<String, EditField>),
|
||||
List(Vec<EditValue>),
|
||||
}
|
||||
@@ -1080,7 +1096,7 @@ fn edit_positions(
|
||||
let span = Span {
|
||||
start: ast.offset(),
|
||||
end: match ast {
|
||||
Ast::Reference(..) => self.tokens[index + 1].span.end,
|
||||
Ast::Reference(..) | Ast::HistoricalUuid(..) => self.tokens[index + 1].span.end,
|
||||
_ => self.ends[index],
|
||||
},
|
||||
};
|
||||
@@ -1090,6 +1106,7 @@ fn edit_positions(
|
||||
EditKind::List(items.iter().map(|item| self.value(item)).collect())
|
||||
}
|
||||
Ast::Reference(alias, _) => EditKind::Reference(alias.clone()),
|
||||
Ast::HistoricalUuid(id, _) => EditKind::HistoricalUuid(*id),
|
||||
_ => EditKind::Atom,
|
||||
};
|
||||
EditValue { span, kind }
|
||||
@@ -1270,6 +1287,16 @@ impl Parser<'_> {
|
||||
let token = self.current();
|
||||
let offset = token.span.start;
|
||||
match self.text() {
|
||||
"uuid" => {
|
||||
self.cursor += 1;
|
||||
let value = self.quoted()?;
|
||||
let id = Id::parse_str(&value).map_err(|_| Error {
|
||||
code: "invalid_source",
|
||||
offset,
|
||||
message: "Expected a UUID after uuid.".into(),
|
||||
})?;
|
||||
Ok(Ast::HistoricalUuid(id, offset))
|
||||
}
|
||||
"@" => {
|
||||
self.cursor += 1;
|
||||
Ok(Ast::Reference(self.identifier()?, offset))
|
||||
@@ -1356,11 +1383,11 @@ impl Parser<'_> {
|
||||
fn unit(&mut self, entry: bool, includes_allowed: bool) -> Result<SourceUnit, Error> {
|
||||
let mut header = if entry {
|
||||
self.take("arcadia")?;
|
||||
if !matches!(self.text(), "3" | "4") {
|
||||
if !matches!(self.text(), "3" | "4" | "5") {
|
||||
return Err(self
|
||||
.error("Unsupported schema version: expected 'arcadia 3;' or 'arcadia 4;'."));
|
||||
.error("Unsupported schema version: expected 'arcadia 3;', 'arcadia 4;' or 'arcadia 5;'."));
|
||||
}
|
||||
let schema_version = if self.text() == "3" { 3 } else { 4 };
|
||||
let schema_version = self.text().parse().expect("checked source version");
|
||||
self.cursor += 1;
|
||||
self.take(";")?;
|
||||
let start = self.current().span.start;
|
||||
@@ -1557,7 +1584,12 @@ pub(crate) fn resolve_declarations(
|
||||
let mut fields = declaration
|
||||
.fields
|
||||
.iter()
|
||||
.map(|(k, v)| Ok((k.clone(), v.resolve(k, aliases)?)))
|
||||
.map(|(k, v)| {
|
||||
let value = if matches!(declaration.kind, DeclKind::Trace) && k == "baseline" {
|
||||
semantic_source::resolve_baseline(v, aliases, project.schema_version)?
|
||||
} else { v.resolve(k, aliases)? };
|
||||
Ok((k.clone(), value))
|
||||
})
|
||||
.collect::<Result<Map<_, _>, Error>>()?;
|
||||
let d = declaration;
|
||||
let conversion_error = |message: String| Error {
|
||||
@@ -1729,8 +1761,12 @@ pub(crate) fn resolve_declarations(
|
||||
let mut record = merge_fields(defaults.as_object().unwrap().clone(), fields)
|
||||
.map_err(&conversion_error)?;
|
||||
header(&mut record, d, true);
|
||||
let trace = serde_json::from_value(Value::Object(record))
|
||||
.map_err(|e| conversion_error(e.to_string()))?;
|
||||
let trace = if project.schema_version < 5 {
|
||||
let old: crate::persistence::v4::Trace4 = serde_json::from_value(Value::Object(record)).map_err(|e| conversion_error(e.to_string()))?;
|
||||
Trace::from(old)
|
||||
} else {
|
||||
serde_json::from_value(Value::Object(record)).map_err(|e| conversion_error(e.to_string()))?
|
||||
};
|
||||
project.traces.insert(d.id, trace);
|
||||
}
|
||||
}
|
||||
@@ -1801,6 +1837,22 @@ pub(crate) fn resolve_declarations(
|
||||
}
|
||||
return Ok(migrated);
|
||||
}
|
||||
if project.schema_version == 4 {
|
||||
return crate::persistence::v4::finish(project, limits).map_err(|error| {
|
||||
(
|
||||
usize::MAX,
|
||||
Error {
|
||||
code: if matches!(error, crate::persistence::FileError::Limit(_)) {
|
||||
"model_limit"
|
||||
} else {
|
||||
"migration_error"
|
||||
},
|
||||
offset: 0,
|
||||
message: error.to_string(),
|
||||
},
|
||||
)
|
||||
});
|
||||
}
|
||||
limits.check_model(&project).map_err(|message| {
|
||||
(
|
||||
usize::MAX,
|
||||
@@ -2595,3 +2647,8 @@ pub(crate) mod legacy {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Closed historical facet identity for the targeted source writer.
|
||||
pub(crate) fn semantic_row_key(source: &str, origins: &BTreeMap<Id, Id>) -> Option<String> {
|
||||
semantic_source::row_key(source, origins)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
//! Typed historical source leaves; live alias resolution remains in the parent.
|
||||
use super::*;
|
||||
use crate::semantic::{HistoricalSourceKind, SemanticBaseline, historical_source_slots};
|
||||
|
||||
fn error(ast: &Ast, message: impl Into<String>) -> Error {
|
||||
Error {
|
||||
code: "invalid_source",
|
||||
offset: ast.offset(),
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn raw(ast: &Ast) -> Result<Value, Error> {
|
||||
Ok(match ast {
|
||||
Ast::Literal(value, _) => value.clone(),
|
||||
Ast::Word(value, _) => Value::String(value.clone()),
|
||||
Ast::HistoricalUuid(value, _) => Value::String(value.to_string()),
|
||||
Ast::Reference(_, _) => {
|
||||
return Err(error(
|
||||
ast,
|
||||
"Historical semantic references require uuid \"...\", not @alias.",
|
||||
));
|
||||
}
|
||||
Ast::Array(values, _) => Value::Array(values.iter().map(raw).collect::<Result<_, _>>()?),
|
||||
Ast::Object(fields, _) => Value::Object(
|
||||
fields
|
||||
.iter()
|
||||
.map(|(key, value)| Ok((key.clone(), raw(value)?)))
|
||||
.collect::<Result<_, Error>>()?,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_tokens(
|
||||
ast: &Ast,
|
||||
path: &mut Vec<String>,
|
||||
slots: &BTreeMap<Vec<String>, HistoricalSourceKind>,
|
||||
) -> Result<(), Error> {
|
||||
match ast {
|
||||
Ast::Object(fields, _) => {
|
||||
for (key, value) in fields {
|
||||
path.push(key.clone());
|
||||
validate_tokens(value, path, slots)?;
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
Ast::Array(values, _) => {
|
||||
for (index, value) in values.iter().enumerate() {
|
||||
path.push(index.to_string());
|
||||
validate_tokens(value, path, slots)?;
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
_ => match (slots.get(path), ast) {
|
||||
(Some(HistoricalSourceKind::Reference), Ast::HistoricalUuid(..))
|
||||
| (Some(HistoricalSourceKind::Enum), Ast::Word(..)) => {}
|
||||
(Some(HistoricalSourceKind::Reference), _) => {
|
||||
return Err(error(
|
||||
ast,
|
||||
"This historical reference requires uuid \"...\".",
|
||||
));
|
||||
}
|
||||
(Some(HistoricalSourceKind::Enum), _) => {
|
||||
return Err(error(
|
||||
ast,
|
||||
"This typed historical enum requires an unquoted enum word.",
|
||||
));
|
||||
}
|
||||
(None, Ast::HistoricalUuid(..)) => {
|
||||
return Err(error(
|
||||
ast,
|
||||
"A uuid literal is allowed only in a typed historical reference slot.",
|
||||
));
|
||||
}
|
||||
(None, Ast::Word(..)) => {
|
||||
return Err(error(
|
||||
ast,
|
||||
"This historical text value requires a quoted string.",
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn resolve_baseline(
|
||||
ast: &Ast,
|
||||
aliases: &BTreeMap<String, Id>,
|
||||
version: u32,
|
||||
) -> Result<Value, Error> {
|
||||
let Ast::Object(fields, _) = ast else {
|
||||
return ast.resolve("baseline", aliases);
|
||||
};
|
||||
let mut resolved = Map::new();
|
||||
for (key, value) in fields {
|
||||
if key != "semantic" {
|
||||
resolved.insert(key.clone(), value.resolve(key, aliases)?);
|
||||
continue;
|
||||
}
|
||||
if version < 5 {
|
||||
return Err(error(
|
||||
value,
|
||||
"Semantic trace baselines require schema/source5; the older baseline contains prose only.",
|
||||
));
|
||||
}
|
||||
if matches!(value, Ast::Literal(Value::Null, _)) {
|
||||
resolved.insert(key.clone(), Value::Null);
|
||||
continue;
|
||||
}
|
||||
let json = raw(value)?;
|
||||
let baseline: SemanticBaseline = serde_json::from_value(json.clone())
|
||||
.map_err(|cause| error(value, format!("Invalid semantic baseline: {cause}")))?;
|
||||
let slots = historical_source_slots(&baseline).into_iter().collect();
|
||||
validate_tokens(value, &mut vec![], &slots)?;
|
||||
resolved.insert(key.clone(), json);
|
||||
}
|
||||
Ok(Value::Object(resolved))
|
||||
}
|
||||
|
||||
pub(super) fn write(out: &mut String, value: &Value, indent: usize) {
|
||||
if value.is_null() {
|
||||
out.push_str("none");
|
||||
return;
|
||||
}
|
||||
let baseline: SemanticBaseline =
|
||||
serde_json::from_value(value.clone()).expect("typed semantic baseline");
|
||||
let slots = historical_source_slots(&baseline).into_iter().collect();
|
||||
write_at(out, value, indent, &mut vec![], &slots);
|
||||
}
|
||||
|
||||
fn write_at(
|
||||
out: &mut String,
|
||||
value: &Value,
|
||||
indent: usize,
|
||||
path: &mut Vec<String>,
|
||||
slots: &BTreeMap<Vec<String>, HistoricalSourceKind>,
|
||||
) {
|
||||
match value {
|
||||
Value::Null => out.push_str("none"),
|
||||
Value::Bool(value) => out.push_str(if *value { "true" } else { "false" }),
|
||||
Value::Number(value) => out.push_str(&value.to_string()),
|
||||
Value::String(value) => match slots.get(path) {
|
||||
Some(HistoricalSourceKind::Reference) => {
|
||||
out.push_str("uuid ");
|
||||
out.push_str("e(value));
|
||||
}
|
||||
Some(HistoricalSourceKind::Enum) => out.push_str(value),
|
||||
None => out.push_str("e(value)),
|
||||
},
|
||||
Value::Array(values) => {
|
||||
out.push('[');
|
||||
for (index, value) in values.iter().enumerate() {
|
||||
if index != 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
path.push(index.to_string());
|
||||
write_at(out, value, indent, path, slots);
|
||||
path.pop();
|
||||
}
|
||||
out.push(']');
|
||||
}
|
||||
Value::Object(fields) => {
|
||||
out.push_str("{\n");
|
||||
for (key, value) in fields {
|
||||
out.push_str(&" ".repeat(indent + 1));
|
||||
out.push_str(key);
|
||||
out.push(' ');
|
||||
path.push(key.clone());
|
||||
write_at(out, value, indent + 1, path, slots);
|
||||
path.pop();
|
||||
out.push_str(";\n");
|
||||
}
|
||||
out.push_str(&" ".repeat(indent));
|
||||
out.push('}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Recognize only complete typed facet rows. Their closed selector, with real
|
||||
/// identity provenance applied, owns comments when review rows reorder.
|
||||
pub(super) fn row_key(source: &str, origins: &BTreeMap<Id, Id>) -> Option<String> {
|
||||
let mut parser = Parser {
|
||||
source,
|
||||
tokens: strict_tokens(source).ok()?,
|
||||
cursor: 0,
|
||||
value_depth: 0,
|
||||
limits: ParseLimits::UNLIMITED,
|
||||
};
|
||||
let ast = parser.value().ok()?;
|
||||
let facet = serde_json::from_value(raw(&ast).ok()?).ok()?;
|
||||
let mut baseline = SemanticBaseline {
|
||||
facets: vec![facet],
|
||||
};
|
||||
crate::semantic::visit_semantic_references(&mut baseline, |id| {
|
||||
if let Some(original) = origins.get(id) {
|
||||
*id = *original;
|
||||
}
|
||||
});
|
||||
serde_json::to_string(&baseline.facets[0].selector())
|
||||
.ok()
|
||||
.map(|key| format!("semantic:{key}"))
|
||||
}
|
||||
@@ -3,7 +3,7 @@ use crate::source_project::{ProjectLimits, SourceBundle, compile_bundle, lint_bu
|
||||
|
||||
fn project_source(body: &str) -> String {
|
||||
format!(
|
||||
"arcadia 4; project model \"Model\" id \"00000000-0000-0000-0000-000000000001\" {{\n{body}\n}}"
|
||||
"arcadia 5; project model \"Model\" id \"00000000-0000-0000-0000-000000000001\" {{\n{body}\n}}"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ fn nested_fragment_id_stabilization_preserves_comments_and_indexes_real_bytes()
|
||||
files: BTreeMap::from([
|
||||
(
|
||||
"project.arcadia".into(),
|
||||
"arcadia 4; project model \"Model\" { include \"children.arcadia\"; }".into(),
|
||||
"arcadia 5; project model \"Model\" { include \"children.arcadia\"; }".into(),
|
||||
),
|
||||
(
|
||||
"children.arcadia".into(),
|
||||
@@ -300,7 +300,7 @@ fn deep_declaration_parsing_and_positions_use_a_bounded_rust_stack() {
|
||||
|
||||
#[test]
|
||||
fn tolerant_nested_analysis_retains_closed_children_before_an_unfinished_sibling() {
|
||||
let source = "arcadia 4; project model \"航海\" {\noa_activity outer_one \"Outer\" {\n oa_activity ready \"Ready\" { description \"} is text\"; }\n oa_activity unfinished \"Unfinished\" { description \"unterminated";
|
||||
let source = "arcadia 5; project model \"航海\" {\noa_activity outer_one \"Outer\" {\n oa_activity ready \"Ready\" { description \"} is text\"; }\n oa_activity unfinished \"Unfinished\" { description \"unterminated";
|
||||
let analysis = analyze(source);
|
||||
let ready = analysis
|
||||
.declarations
|
||||
|
||||
@@ -13,7 +13,9 @@ generic port/optional FE endpoint contracts therefore remain explicit.
|
||||
|
||||
`adapter.rs` budgets generated data, validates V2 and transforms only the changed
|
||||
port/endpoint payloads into an explicit schema 3 candidate. The schema 3 reader
|
||||
then performs the data-contract upgrade and validates schema 4. Serialization
|
||||
then performs the data-contract upgrade into the strict schema 4 envelope. The
|
||||
schema 4 review adapter preserves its prose-only trace baselines and returns
|
||||
schema 5 with semantic review absent. Serialization
|
||||
between compatibility DTOs copies unchanged fields; original input bytes are
|
||||
never deserialized into the new `Project`. V1 normalization also constructs this
|
||||
V2 DTO before the shared stages. Combined generation stays within one budget.
|
||||
|
||||
@@ -24,7 +24,7 @@ pub(crate) struct Project3 {
|
||||
#[serde(deserialize_with = "crate::unique_id_map")]
|
||||
pub artifact_links: BTreeMap<Id, ArtifactLink>,
|
||||
#[serde(deserialize_with = "crate::unique_id_map")]
|
||||
pub traces: BTreeMap<Id, Trace>,
|
||||
pub traces: BTreeMap<Id, crate::persistence::v4::Trace4>,
|
||||
}
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -486,8 +486,10 @@ pub(crate) fn migrate(old: Project3, limits: ParseLimits) -> Result<DecodeResult
|
||||
));
|
||||
}
|
||||
}
|
||||
value["schema_version"] = SCHEMA_VERSION.into();
|
||||
let mut project: Project = serde_json::from_value(value)?;
|
||||
value["schema_version"] = 4.into();
|
||||
let normalized: crate::persistence::v4::Project4 = serde_json::from_value(value)?;
|
||||
let mut project = normalized.into_current()?;
|
||||
crate::persistence::v4::review_defaults(&project, &mut report);
|
||||
let mut references = project
|
||||
.elements
|
||||
.values()
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
//! Frozen schema4 review envelopes. Other model records are unchanged in schema5.
|
||||
use crate::language::ParseLimits;
|
||||
use crate::persistence::{DecodeResult, FileError, MigrationReport, SemanticDefault};
|
||||
use crate::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct TraceBaseline4 {
|
||||
pub oa: TracedProperties,
|
||||
pub sa: TracedProperties,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct Trace4 {
|
||||
pub id: Id,
|
||||
pub alias: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub oa: Id,
|
||||
pub sa: Id,
|
||||
pub kind: TraceKind,
|
||||
pub baseline: Option<TraceBaseline4>,
|
||||
}
|
||||
impl From<Trace4> for Trace {
|
||||
fn from(old: Trace4) -> Self {
|
||||
Self {
|
||||
id: old.id,
|
||||
alias: old.alias,
|
||||
name: old.name,
|
||||
description: old.description,
|
||||
oa: old.oa,
|
||||
sa: old.sa,
|
||||
kind: old.kind,
|
||||
baseline: old.baseline.map(|baseline| TraceBaseline {
|
||||
oa: baseline.oa,
|
||||
sa: baseline.sa,
|
||||
semantic: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct Project4 {
|
||||
pub schema_version: u32,
|
||||
pub id: Id,
|
||||
pub alias: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
#[serde(deserialize_with = "crate::unique_id_map")]
|
||||
pub elements: BTreeMap<Id, Element>,
|
||||
#[serde(deserialize_with = "crate::unique_id_map")]
|
||||
pub relations: BTreeMap<Id, Relation>,
|
||||
#[serde(deserialize_with = "crate::unique_id_map")]
|
||||
pub diagrams: BTreeMap<Id, Diagram>,
|
||||
#[serde(deserialize_with = "crate::unique_id_map")]
|
||||
pub artifact_links: BTreeMap<Id, ArtifactLink>,
|
||||
#[serde(deserialize_with = "crate::unique_id_map")]
|
||||
pub traces: BTreeMap<Id, Trace4>,
|
||||
}
|
||||
impl Project4 {
|
||||
pub(crate) fn into_current(self) -> Result<Project, FileError> {
|
||||
if self.schema_version != 4 {
|
||||
return Err(FileError::Invalid("Expected schema_version 4.".into()));
|
||||
}
|
||||
Ok(Project {
|
||||
schema_version: SCHEMA_VERSION,
|
||||
id: self.id,
|
||||
alias: self.alias,
|
||||
name: self.name,
|
||||
description: self.description,
|
||||
elements: self.elements,
|
||||
relations: self.relations,
|
||||
diagrams: self.diagrams,
|
||||
artifact_links: self.artifact_links,
|
||||
traces: self
|
||||
.traces
|
||||
.into_iter()
|
||||
.map(|(id, trace)| (id, trace.into()))
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn review_defaults(project: &Project, report: &mut MigrationReport) {
|
||||
report.semantic_defaults.extend(
|
||||
project
|
||||
.traces
|
||||
.values()
|
||||
.filter(|trace| trace.baseline.is_some())
|
||||
.map(|trace| SemanticDefault {
|
||||
object: trace.id,
|
||||
field: "baseline.semantic".into(),
|
||||
description: "Original prose review preserved; semantic facets remain unreviewed."
|
||||
.into(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn finish(mut project: Project, limits: ParseLimits) -> Result<DecodeResult, FileError> {
|
||||
project.schema_version = SCHEMA_VERSION;
|
||||
limits.check_model(&project).map_err(FileError::Limit)?;
|
||||
let mut report = MigrationReport::data_upgrade(4);
|
||||
review_defaults(&project, &mut report);
|
||||
Ok(DecodeResult {
|
||||
project,
|
||||
migration: Some(report),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn migrate(old: Project4, limits: ParseLimits) -> Result<DecodeResult, FileError> {
|
||||
finish(old.into_current()?, limits)
|
||||
}
|
||||
@@ -7,6 +7,8 @@ pub(crate) mod v2;
|
||||
pub(crate) use v2::adapter::migrate as migrate_v2_limited;
|
||||
#[path = "migration_v3/mod.rs"]
|
||||
pub(crate) mod v3;
|
||||
#[path = "migration_v4.rs"]
|
||||
pub(crate) mod v4;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -148,13 +150,14 @@ pub fn decode_with_report_limited(
|
||||
}
|
||||
Some(2) => migrate_v2_limited(serde_json::from_slice(bytes)?, limits)?,
|
||||
Some(3) => v3::migrate(serde_json::from_slice(bytes)?, limits)?,
|
||||
Some(4) => DecodeResult {
|
||||
Some(4) => v4::migrate(serde_json::from_slice(bytes)?, limits)?,
|
||||
Some(5) => DecodeResult {
|
||||
project: serde_json::from_slice(bytes)?,
|
||||
migration: None,
|
||||
},
|
||||
_ => {
|
||||
return Err(FileError::Invalid(
|
||||
"Unsupported or missing schema_version; expected 1, 2, 3 or 4.".into(),
|
||||
"Unsupported or missing schema_version; expected 1, 2, 3, 4 or 5.".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -999,7 +999,7 @@ mod transaction_tests {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
fs::write(
|
||||
directory.path().join("project.arcadia"),
|
||||
"arcadia 4; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
|
||||
"arcadia 5; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(directory.path().join("a.arcadia"), "oa_activity a \"A\" {}").unwrap();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Pure, bounded source projects. All includes resolve against supplied UTF-8
|
||||
//! documents; this module never reads a filesystem, URL, or process directory.
|
||||
use crate::{Id, Kind, Project, language, persistence::DecodeResult, validation};
|
||||
use crate::{Id, Kind, Project, SCHEMA_VERSION, language, persistence::DecodeResult, validation};
|
||||
use language::{Diagnostic, ParseLimits, Span};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
@@ -280,7 +280,7 @@ pub fn lint_bundle(bundle: &SourceBundle, limits: ProjectLimits) -> Vec<SourceDi
|
||||
}
|
||||
}
|
||||
|
||||
/// Compile reachable source3/4 files, explicitly upgrade data contracts, and stabilize UUIDs.
|
||||
/// Compile reachable source3/4/5 files, explicitly upgrade contracts, and stabilize UUIDs.
|
||||
/// Missing/unreachable files and every admission or engineering error leave the
|
||||
/// input untouched. V1/V2 single-file migration remains in `language::parse`.
|
||||
pub fn compile_bundle(
|
||||
@@ -720,7 +720,7 @@ fn apply_edits(bundle: &mut SourceBundle, edits: Edits) -> Result<(), Vec<Source
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Canonical, readable source4 export. Like `language::format`, the caller is
|
||||
/// Canonical, readable current-version export. Like `language::format`, the caller is
|
||||
/// responsible for supplying a valid model; compilation checks the result.
|
||||
/// Existing source layout is preserved by `update_bundle`, not this formatter.
|
||||
pub fn format_bundle(project: &Project) -> SourceBundle {
|
||||
@@ -736,7 +736,7 @@ pub fn format_bundle(project: &Project) -> SourceBundle {
|
||||
.push_str(&unindent(&declaration));
|
||||
}
|
||||
let mut entry = format!(
|
||||
"arcadia 4;\n{}{{\n description {};\n",
|
||||
"arcadia {SCHEMA_VERSION};\n{}{{\n description {};\n",
|
||||
project_header(project),
|
||||
quote(&project.description)
|
||||
);
|
||||
@@ -926,6 +926,7 @@ pub fn update_bundle_with_identity_map(
|
||||
old_aliases: &old_aliases,
|
||||
new_aliases: &new_aliases,
|
||||
legacy_value_paths: false,
|
||||
identity_origins: Some(&origins),
|
||||
};
|
||||
let local = edits.entry(path.clone()).or_default();
|
||||
update.header(&actual.header, &previous.header, &next.header, local);
|
||||
@@ -955,6 +956,7 @@ pub fn update_bundle_with_identity_map(
|
||||
old_aliases: &old_aliases,
|
||||
new_aliases: &new_aliases,
|
||||
legacy_value_paths: false,
|
||||
identity_origins: Some(&origins),
|
||||
};
|
||||
let mut local = Vec::new();
|
||||
update.header(
|
||||
@@ -1126,6 +1128,7 @@ struct SyntaxUpdate<'a> {
|
||||
// Source3 migration only adds path qualification; the existing field identity
|
||||
// must keep its authored argument trivia during that one-time transformation.
|
||||
legacy_value_paths: bool,
|
||||
identity_origins: Option<&'a BTreeMap<Id, Id>>,
|
||||
}
|
||||
impl SyntaxUpdate<'_> {
|
||||
fn header(
|
||||
@@ -1221,6 +1224,18 @@ impl SyntaxUpdate<'_> {
|
||||
alias.clone(),
|
||||
));
|
||||
}
|
||||
(EditKind::HistoricalUuid(_), EditKind::HistoricalUuid(id)) => {
|
||||
let token = significant(&self.source[actual.span.start..actual.span.end])
|
||||
.pop()
|
||||
.expect("historical UUID string");
|
||||
edits.push((
|
||||
Span {
|
||||
start: actual.span.start + token.span.start,
|
||||
end: actual.span.start + token.span.end,
|
||||
},
|
||||
quote(&id.to_string()),
|
||||
));
|
||||
}
|
||||
(EditKind::Atom, EditKind::Atom) => edits.push((
|
||||
actual.span,
|
||||
self.after[after.span.start..after.span.end].into(),
|
||||
@@ -1345,11 +1360,27 @@ impl SyntaxUpdate<'_> {
|
||||
let previous = before.unwrap_or(&[]);
|
||||
let old_keys: Option<Vec<_>> = previous
|
||||
.iter()
|
||||
.map(|item| item_key(item, self.before, self.old_aliases, self.legacy_value_paths))
|
||||
.map(|item| {
|
||||
item_key(
|
||||
item,
|
||||
self.before,
|
||||
self.old_aliases,
|
||||
self.legacy_value_paths,
|
||||
None,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let new_keys: Option<Vec<_>> = after
|
||||
.iter()
|
||||
.map(|item| item_key(item, self.after, self.new_aliases, self.legacy_value_paths))
|
||||
.map(|item| {
|
||||
item_key(
|
||||
item,
|
||||
self.after,
|
||||
self.new_aliases,
|
||||
self.legacy_value_paths,
|
||||
self.identity_origins,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let keyed = old_keys.zip(new_keys).filter(|(old, new)| {
|
||||
old.iter().collect::<BTreeSet<_>>().len() == old.len()
|
||||
@@ -1457,9 +1488,22 @@ fn item_key(
|
||||
source: &str,
|
||||
aliases: &BTreeMap<String, String>,
|
||||
legacy_value_paths: bool,
|
||||
origins: Option<&BTreeMap<Id, Id>>,
|
||||
) -> Option<String> {
|
||||
use language::EditKind;
|
||||
if matches!(value.kind, EditKind::Record(_))
|
||||
&& let Some(key) = language::semantic_row_key(
|
||||
&source[value.span.start..value.span.end],
|
||||
origins.unwrap_or(&BTreeMap::new()),
|
||||
)
|
||||
{
|
||||
return Some(key);
|
||||
}
|
||||
match &value.kind {
|
||||
EditKind::HistoricalUuid(id) => Some(format!(
|
||||
"uuid:{}",
|
||||
origins.and_then(|map| map.get(id)).unwrap_or(id)
|
||||
)),
|
||||
EditKind::Reference(alias) => aliases.get(alias).map(|id| format!("ref:{id}")),
|
||||
EditKind::Atom => Some(format!(
|
||||
"literal:{}",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Source3 data upgrade through token edits; authored modules, nesting and trivia survive.
|
||||
//! Source3/4 upgrade through token edits; authored modules, nesting and trivia survive.
|
||||
use super::*;
|
||||
|
||||
pub(super) fn upgrade(
|
||||
@@ -15,6 +15,7 @@ pub(super) fn upgrade(
|
||||
let changed: BTreeSet<_> = report
|
||||
.semantic_defaults
|
||||
.iter()
|
||||
.filter(|default| default.field != "baseline.semantic")
|
||||
.map(|default| default.object)
|
||||
.collect();
|
||||
let aliases: BTreeMap<_, _> = project
|
||||
@@ -61,6 +62,7 @@ pub(super) fn upgrade(
|
||||
old_aliases: &aliases,
|
||||
new_aliases: &aliases,
|
||||
legacy_value_paths: true,
|
||||
identity_origins: None,
|
||||
};
|
||||
update.value(
|
||||
&actual.fields,
|
||||
@@ -78,7 +80,7 @@ pub(super) fn upgrade(
|
||||
edits
|
||||
.entry(current.bundle.entry.clone())
|
||||
.or_default()
|
||||
.push((version, "4".into()));
|
||||
.push((version, crate::SCHEMA_VERSION.to_string()));
|
||||
// New allocation relations are emitted into the allocating interface's
|
||||
// existing module, outside its lexical element body. Existing source never moves.
|
||||
let mut added: BTreeMap<String, String> = BTreeMap::new();
|
||||
@@ -117,7 +119,7 @@ pub(super) fn upgrade(
|
||||
&bundle.entry,
|
||||
Span::default(),
|
||||
"migration_source_mismatch",
|
||||
"Targeted source3 upgrade did not reproduce the migrated project.",
|
||||
"Targeted source upgrade did not reproduce the migrated project.",
|
||||
)]);
|
||||
}
|
||||
result.decoded.migration = Some(report.clone());
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Frozen schema4 input: reviewed prose is deliberately different from live prose.
|
||||
arcadia 4;
|
||||
project reviewed_prose "Reviewed dimensions" id "40000000-0000-4000-8000-000000000001" {
|
||||
description "Preserve the original review without inventing semantic history.";
|
||||
oa_data_type oa_dimension "OA dimension now" id "40000000-0000-4000-8000-000000000002" {
|
||||
description "Unreviewed OA description";
|
||||
definition { Integer { minimum 0; maximum 100; }; };
|
||||
default { Integer 20; };
|
||||
unit "m";
|
||||
}
|
||||
sa_data_type sa_dimension "SA dimension now" id "40000000-0000-4000-8000-000000000003" {
|
||||
description "Unreviewed SA description";
|
||||
definition { Real { minimum 0.0; maximum 100.0; }; };
|
||||
default { Real 20.0; };
|
||||
unit "ft";
|
||||
}
|
||||
trace dimension_review "Dimension rationale" id "40000000-0000-4000-8000-000000000004" {
|
||||
description "The target unit and representation are intentional.";
|
||||
oa @oa_dimension;
|
||||
sa @sa_dimension;
|
||||
kind Item;
|
||||
baseline {
|
||||
oa { name "Approved OA name"; description "Approved OA\nprose"; };
|
||||
sa { name "Approved SA name"; description "Approved SA prose"; };
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"schema_version": 4,
|
||||
"id": "40000000-0000-4000-8000-000000000001",
|
||||
"alias": "reviewed_prose",
|
||||
"name": "Reviewed dimensions",
|
||||
"description": "Preserve the original review without inventing semantic history.",
|
||||
"elements": {
|
||||
"40000000-0000-4000-8000-000000000002": {
|
||||
"id": "40000000-0000-4000-8000-000000000002",
|
||||
"alias": "oa_dimension",
|
||||
"name": "OA dimension now",
|
||||
"description": "Unreviewed OA description",
|
||||
"parent": null,
|
||||
"body": {"Oa": {"Shared": {"DataType": {
|
||||
"definition": {"Integer": {"minimum": 0, "maximum": 100}},
|
||||
"default": {"Integer": 20},
|
||||
"unit": "m"
|
||||
}}}}
|
||||
},
|
||||
"40000000-0000-4000-8000-000000000003": {
|
||||
"id": "40000000-0000-4000-8000-000000000003",
|
||||
"alias": "sa_dimension",
|
||||
"name": "SA dimension now",
|
||||
"description": "Unreviewed SA description",
|
||||
"parent": null,
|
||||
"body": {"Sa": {"Shared": {"DataType": {
|
||||
"definition": {"Real": {"minimum": 0.0, "maximum": 100.0}},
|
||||
"default": {"Real": 20.0},
|
||||
"unit": "ft"
|
||||
}}}}
|
||||
}
|
||||
},
|
||||
"relations": {},
|
||||
"diagrams": {},
|
||||
"artifact_links": {},
|
||||
"traces": {
|
||||
"40000000-0000-4000-8000-000000000004": {
|
||||
"id": "40000000-0000-4000-8000-000000000004",
|
||||
"alias": "dimension_review",
|
||||
"name": "Dimension rationale",
|
||||
"description": "The target unit and representation are intentional.",
|
||||
"oa": "40000000-0000-4000-8000-000000000002",
|
||||
"sa": "40000000-0000-4000-8000-000000000003",
|
||||
"kind": "Item",
|
||||
"baseline": {
|
||||
"oa": {"name": "Approved OA name", "description": "Approved OA\nprose"},
|
||||
"sa": {"name": "Approved SA name", "description": "Approved SA prose"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -491,7 +491,7 @@ fn authoring_defaults_and_forward_references_remain_strict() {
|
||||
#[test]
|
||||
fn typed_unknown_fields_enums_duplicates_and_reference_spelling_are_rejected() {
|
||||
for (source, expected) in [
|
||||
("arcadia 5; project mission \"x\" {}", "Unsupported schema"),
|
||||
("arcadia 6; project mission \"x\" {}", "Unsupported schema"),
|
||||
(
|
||||
"arcadia 2; project mission \"x\" { oa_activity a \"A\" { nonsense 1; } }",
|
||||
"Unknown field",
|
||||
@@ -795,7 +795,7 @@ fn original_v2_text_and_json_preserve_identity_and_mixed_scenarios_after_extensi
|
||||
assert_eq!(from_source, from_json);
|
||||
let original: serde_json::Value = serde_json::from_slice(old_json).unwrap();
|
||||
let mut retained = serde_json::to_value(&from_json).unwrap();
|
||||
assert_eq!(retained["schema_version"], 4);
|
||||
assert_eq!(retained["schema_version"], 5);
|
||||
retained["schema_version"] = 2.into();
|
||||
let allocations: Vec<_> = from_json
|
||||
.relations
|
||||
@@ -991,7 +991,7 @@ fn v1_text_and_json_migrate_deterministically_without_losing_legacy_semantics()
|
||||
assert_eq!(from_text, from_json);
|
||||
assert_eq!(read(text), from_text);
|
||||
assert_eq!(persistence::decode(json).unwrap(), from_json);
|
||||
assert_eq!(from_text.schema_version, 4);
|
||||
assert_eq!(from_text.schema_version, 5);
|
||||
assert!(!from_text.elements.values().any(|e| e.layer() == Layer::Sa));
|
||||
let raw: serde_json::Value = serde_json::from_slice(json).unwrap();
|
||||
for (id, record) in raw["elements"].as_object().unwrap() {
|
||||
@@ -1193,6 +1193,7 @@ fn complete_normative_examples_are_accepted_by_the_real_parser() {
|
||||
if code.trim_start().starts_with("arcadia 2;")
|
||||
|| code.trim_start().starts_with("arcadia 3;")
|
||||
|| code.trim_start().starts_with("arcadia 4;")
|
||||
|| code.trim_start().starts_with("arcadia 5;")
|
||||
{
|
||||
read(&code);
|
||||
examples += 1;
|
||||
|
||||
@@ -39,7 +39,7 @@ fn frozen_original_v2_reader_preserves_every_original_record() {
|
||||
assert_eq!(decoded.project, from_source.project);
|
||||
assert_eq!(decoded.migration, from_source.migration);
|
||||
let report = decoded.migration.unwrap();
|
||||
assert_eq!((report.from_schema, report.to_schema), (2, 4));
|
||||
assert_eq!((report.from_schema, report.to_schema), (2, 5));
|
||||
assert_eq!(report.preserved_ports.len(), 4);
|
||||
assert!(report.generated_endpoints.is_empty());
|
||||
assert_eq!(report.orientation_defaults.len(), 1);
|
||||
@@ -144,7 +144,7 @@ fn missing_v2_endpoints_are_deterministic_reported_and_independently_typed() {
|
||||
}
|
||||
}
|
||||
let canonical = language::format(&first.project);
|
||||
assert!(canonical.starts_with("arcadia 4;"));
|
||||
assert!(canonical.starts_with("arcadia 5;"));
|
||||
let reopened = persistence::decode_with_report(canonical.as_bytes()).unwrap();
|
||||
assert!(reopened.migration.is_none());
|
||||
assert_eq!(reopened.project, first.project);
|
||||
@@ -274,7 +274,7 @@ fn old_contract_is_validated_before_new_shapes_and_v1_flows_through_v2() {
|
||||
assert!(error.contains("V2") && error.contains(&fe), "{error}");
|
||||
let v1 = include_bytes!("../../../examples/legacy-v1-coastal-rescue.oa.json");
|
||||
let result = persistence::decode_with_report(v1).unwrap();
|
||||
assert_eq!(result.project.schema_version, 4);
|
||||
assert_eq!(result.project.schema_version, 5);
|
||||
assert_eq!(result.migration.unwrap().from_schema, 1);
|
||||
assert_eq!(persistence::decode(v1).unwrap(), result.project);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ project typed_ports "Typed ports" {
|
||||
#[test]
|
||||
fn source4_covers_typed_contracts_roles_allocations_and_nested_legacy_references() {
|
||||
let decoded = language::parse_with_report(SOURCE).unwrap();
|
||||
assert!(decoded.migration.is_none());
|
||||
let migration = decoded.migration.as_ref().unwrap();
|
||||
assert_eq!((migration.from_schema, migration.to_schema), (4, 5));
|
||||
let canonical = language::format(&decoded.project);
|
||||
for field in [
|
||||
"incoming_items [@data]",
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
use oa_core::{language, persistence, source_project::*, *};
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
const JSON4: &[u8] = include_bytes!("fixtures/v4-reviewed-prose.json");
|
||||
const SOURCE4: &str = include_str!("fixtures/v4-reviewed-prose.arcadia");
|
||||
|
||||
#[test]
|
||||
fn frozen_v4_json_and_source_preserve_the_old_review_without_fabricating_semantics() {
|
||||
let json = persistence::decode_with_report(JSON4).unwrap();
|
||||
let source = language::parse_with_report(SOURCE4).unwrap();
|
||||
assert_eq!(json.project, source.project);
|
||||
for decoded in [json, source] {
|
||||
assert_eq!(decoded.project.schema_version, 5);
|
||||
let report = decoded.migration.unwrap();
|
||||
assert_eq!((report.from_schema, report.to_schema), (4, 5));
|
||||
let trace = decoded.project.traces.values().next().unwrap();
|
||||
let baseline = trace.baseline.as_ref().unwrap();
|
||||
assert!(baseline.semantic.is_none());
|
||||
assert_eq!(baseline.oa.name, "Approved OA name");
|
||||
assert_eq!(baseline.oa.description, "Approved OA\nprose");
|
||||
assert_eq!(baseline.sa.name, "Approved SA name");
|
||||
assert_ne!(baseline.oa.name, decoded.project.named(trace.oa));
|
||||
let mut expected: Value = serde_json::from_slice(JSON4).unwrap();
|
||||
expected["schema_version"] = 5.into();
|
||||
assert_eq!(serde_json::to_value(&decoded.project).unwrap(), expected);
|
||||
let reopened =
|
||||
persistence::decode_with_report(&serde_json::to_vec(&decoded.project).unwrap())
|
||||
.unwrap();
|
||||
assert!(reopened.migration.is_none());
|
||||
assert_eq!(reopened.project, decoded.project);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn older_headers_cannot_admit_new_semantic_baselines() {
|
||||
let mut value: Value = serde_json::from_slice(JSON4).unwrap();
|
||||
let trace = value["traces"]
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.values_mut()
|
||||
.next()
|
||||
.unwrap();
|
||||
trace["baseline"]["semantic"] = serde_json::json!({"facets": []});
|
||||
for version in [3, 4] {
|
||||
value["schema_version"] = version.into();
|
||||
let error = persistence::decode(&serde_json::to_vec(&value).unwrap())
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(error.contains("semantic"), "{error}");
|
||||
}
|
||||
let source = SOURCE4.replace("baseline {", "baseline { semantic { facets []; };");
|
||||
assert!(
|
||||
language::parse(&source)
|
||||
.unwrap_err()
|
||||
.iter()
|
||||
.any(|d| d.message.contains("require schema/source5"))
|
||||
);
|
||||
let bundle = SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([("project.arcadia".into(), source)]),
|
||||
};
|
||||
assert!(
|
||||
compile_bundle(&bundle, ProjectLimits::default())
|
||||
.unwrap_err()
|
||||
.iter()
|
||||
.any(|d| d.diagnostic.message.contains("require schema/source5"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_v4_trace_envelope_keeps_duplicate_and_unknown_field_rejection() {
|
||||
let json = std::str::from_utf8(JSON4).unwrap();
|
||||
let duplicate = json.replace("\"baseline\": {", "\"baseline\": null, \"baseline\": {");
|
||||
assert!(
|
||||
persistence::decode(duplicate.as_bytes())
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("duplicate field")
|
||||
);
|
||||
let unknown = json.replace("\"baseline\": {", "\"baseline\": { \"reviewed\": true,");
|
||||
assert!(
|
||||
persistence::decode(unknown.as_bytes())
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("unknown field")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authored_v4_modules_upgrade_only_the_version_and_preserve_nested_comments() {
|
||||
let start = SOURCE4.find(" oa_data_type").unwrap();
|
||||
let end = SOURCE4.rfind('}').unwrap();
|
||||
let entry = format!(
|
||||
"{} include \"contracts.arcadia\";\n}}\n",
|
||||
&SOURCE4[..start]
|
||||
);
|
||||
let mut module =
|
||||
SOURCE4[start..end].replace("baseline {", "baseline { // original review rationale");
|
||||
module.push_str(
|
||||
r#"
|
||||
oa_data_type record "Independent record" id "40000000-0000-4000-8000-000000000011" {
|
||||
definition { Record { supertype none; }; };
|
||||
// This field remains inside its authored record.
|
||||
oa_data_field flag "Flag" id "40000000-0000-4000-8000-000000000012" { type_ref Boolean; }
|
||||
}
|
||||
"#,
|
||||
);
|
||||
let bundle = SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("project.arcadia".into(), entry.clone()),
|
||||
("contracts.arcadia".into(), module.clone()),
|
||||
]),
|
||||
};
|
||||
let compiled = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(compiled.bundle.files["contracts.arcadia"], module);
|
||||
assert_eq!(
|
||||
compiled.bundle.files["project.arcadia"],
|
||||
entry.replacen("arcadia 4;", "arcadia 5;", 1)
|
||||
);
|
||||
assert_eq!(
|
||||
compiled.index.lexical_owners
|
||||
[&Id::parse_str("40000000-0000-4000-8000-000000000012").unwrap()],
|
||||
Id::parse_str("40000000-0000-4000-8000-000000000011").unwrap()
|
||||
);
|
||||
let reopened = compile_bundle(&compiled.bundle, ProjectLimits::default()).unwrap();
|
||||
assert!(reopened.decoded.migration.is_none());
|
||||
assert_eq!(reopened.bundle, compiled.bundle);
|
||||
assert_eq!(reopened.decoded.project, compiled.decoded.project);
|
||||
assert_eq!(
|
||||
bundle.files["project.arcadia"], entry,
|
||||
"caller draft is unchanged"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn original_v1_v2_v3_inputs_complete_the_explicit_upgrade_to_five() {
|
||||
for (version, bytes) in [
|
||||
(
|
||||
1,
|
||||
include_bytes!("../../../examples/legacy-v1-coastal-rescue.oa.json").as_slice(),
|
||||
),
|
||||
(
|
||||
2,
|
||||
include_bytes!("fixtures/v2-coastal-rescue.json").as_slice(),
|
||||
),
|
||||
(
|
||||
3,
|
||||
include_bytes!("fixtures/v3-data-contract.json").as_slice(),
|
||||
),
|
||||
] {
|
||||
let decoded = persistence::decode_with_report(bytes).unwrap();
|
||||
let report = decoded.migration.unwrap();
|
||||
assert_eq!((report.from_schema, report.to_schema), (version, 5));
|
||||
assert_eq!(decoded.project.schema_version, 5);
|
||||
assert!(
|
||||
decoded
|
||||
.project
|
||||
.traces
|
||||
.values()
|
||||
.all(|t| t.baseline.as_ref().is_none_or(|b| b.semantic.is_none()))
|
||||
);
|
||||
assert_eq!(
|
||||
language::parse(&language::format(&decoded.project)).unwrap(),
|
||||
decoded.project
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
use oa_core::{language, persistence, semantic::*, source_project::*, *};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn id(n: u128) -> Id {
|
||||
Id::from_u128(0x40000000_0000_4000_8000_000000000000 + n)
|
||||
}
|
||||
|
||||
fn reviewed<T>(oa: T, sa: T) -> Reviewed<T> {
|
||||
Reviewed {
|
||||
oa: Observed::Value(oa),
|
||||
sa: Observed::Value(sa),
|
||||
}
|
||||
}
|
||||
|
||||
fn project(facets: Vec<FacetReview>) -> Project {
|
||||
let mut project =
|
||||
persistence::decode(include_bytes!("fixtures/v4-reviewed-prose.json")).unwrap();
|
||||
project
|
||||
.traces
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic = Some(SemanticBaseline { facets });
|
||||
project
|
||||
}
|
||||
|
||||
fn facets(project: &mut Project) -> &mut Vec<FacetReview> {
|
||||
&mut project
|
||||
.traces
|
||||
.get_mut(&id(4))
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.facets
|
||||
}
|
||||
|
||||
fn compile(source: String) -> CompiledSourceProject {
|
||||
compile_bundle(
|
||||
&SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([("project.arcadia".into(), source)]),
|
||||
},
|
||||
ProjectLimits::default(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_historical_facets_roundtrip_deleted_references_and_all_leaf_categories() {
|
||||
let row = MessageValue {
|
||||
path: vec![id(91), id(92)],
|
||||
field: id(93),
|
||||
expression: "x + 1".into(),
|
||||
};
|
||||
let model = project(vec![
|
||||
FacetReview::Data(DataReview::Constructor(reviewed(
|
||||
DataConstructor::Integer,
|
||||
DataConstructor::Real,
|
||||
))),
|
||||
FacetReview::Data(DataReview::IntegerMinimum(Reviewed {
|
||||
oa: Observed::Value(None),
|
||||
sa: Observed::NotApplicable,
|
||||
})),
|
||||
FacetReview::Data(DataReview::RealMaximum(Reviewed {
|
||||
oa: Observed::NotApplicable,
|
||||
sa: Observed::Value(Some(1.5)),
|
||||
})),
|
||||
FacetReview::Field(FieldReview::Type(reviewed(
|
||||
TypeRef::Named(id(90)),
|
||||
TypeRef::Boolean,
|
||||
))),
|
||||
FacetReview::Field(FieldReview::Default(reviewed(
|
||||
Some(vec![DataValue::String(id(90).to_string())]),
|
||||
None,
|
||||
))),
|
||||
FacetReview::Member {
|
||||
collection: ReferenceCollection::Steps,
|
||||
keys: KeyPair {
|
||||
oa: Some(id(91)),
|
||||
sa: None,
|
||||
},
|
||||
review: reviewed(true, false),
|
||||
},
|
||||
FacetReview::Order {
|
||||
collection: OrderedCollection::Steps,
|
||||
review: reviewed(vec![id(91), id(92)], vec![]),
|
||||
},
|
||||
FacetReview::Annotation {
|
||||
keys: KeyPair {
|
||||
oa: Some(AnnotationKey::from(&row)),
|
||||
sa: None,
|
||||
},
|
||||
review: reviewed(Some(row), None),
|
||||
},
|
||||
FacetReview::Binding {
|
||||
keys: KeyPair {
|
||||
oa: Some(id(94)),
|
||||
sa: None,
|
||||
},
|
||||
review: reviewed(
|
||||
Some(LifelineBinding {
|
||||
referenced: id(94),
|
||||
local: id(95),
|
||||
}),
|
||||
None,
|
||||
),
|
||||
},
|
||||
FacetReview::Literal {
|
||||
keys: KeyPair {
|
||||
oa: Some(id(90).to_string()),
|
||||
sa: None,
|
||||
},
|
||||
review: reviewed(true, false),
|
||||
},
|
||||
FacetReview::Artifact {
|
||||
keys: KeyPair {
|
||||
oa: Some(id(96)),
|
||||
sa: None,
|
||||
},
|
||||
review: ArtifactReview::Relation(reviewed(
|
||||
ArtifactRelation::References,
|
||||
ArtifactRelation::References,
|
||||
)),
|
||||
},
|
||||
]);
|
||||
assert!(!model.contains(id(90)));
|
||||
let source = language::format(&model);
|
||||
assert!(source.starts_with("arcadia 5;"));
|
||||
assert!(
|
||||
source.contains(&format!("Named uuid \"{}\"", id(90))),
|
||||
"{source}"
|
||||
);
|
||||
assert!(source.contains("sa NotApplicable;"));
|
||||
assert!(source.contains("Value none;"));
|
||||
assert!(source.contains(&format!("String \"{}\"", id(90))));
|
||||
assert_eq!(language::parse(&source).unwrap(), model);
|
||||
assert_eq!(compile(source).decoded.project, model);
|
||||
assert_eq!(
|
||||
persistence::decode(&serde_json::to_vec(&model).unwrap()).unwrap(),
|
||||
model
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn historical_uuid_and_enum_tokens_are_contextual_and_text_remains_quoted() {
|
||||
let model = project(vec![
|
||||
FacetReview::Field(FieldReview::Type(reviewed(
|
||||
TypeRef::Named(id(2)),
|
||||
TypeRef::Boolean,
|
||||
))),
|
||||
FacetReview::Data(DataReview::Unit(reviewed(id(2).to_string(), "m".into()))),
|
||||
]);
|
||||
let source = language::format(&model);
|
||||
let reference = format!("Named uuid \"{}\"", id(2));
|
||||
for replacement in [format!("Named \"{}\"", id(2)), "Named @oa_dimension".into()] {
|
||||
let invalid = source.replace(&reference, &replacement);
|
||||
assert!(language::parse(&invalid).is_err(), "{invalid}");
|
||||
assert!(
|
||||
compile_bundle(
|
||||
&SourceBundle {
|
||||
entry: "p.arcadia".into(),
|
||||
files: BTreeMap::from([("p.arcadia".into(), invalid)])
|
||||
},
|
||||
ProjectLimits::default()
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
let invalid_text = source.replace("Value \"m\";", &format!("Value uuid \"{}\";", id(2)));
|
||||
assert!(language::parse(&invalid_text).is_err());
|
||||
let invalid_enum = source.replace("Value Boolean;", "Value \"Boolean\";");
|
||||
assert!(language::parse(&invalid_enum).is_err());
|
||||
let invalid_live = source.replace("oa @oa_dimension;", &format!("oa uuid \"{}\";", id(2)));
|
||||
assert!(language::parse(&invalid_live).is_err());
|
||||
let invalid_prose = source.replace(
|
||||
"name \"Approved OA name\";",
|
||||
&format!("name uuid \"{}\";", id(2)),
|
||||
);
|
||||
assert!(language::parse(&invalid_prose).is_err());
|
||||
// `uuid` remains an ordinary legal declaration alias after @.
|
||||
assert!(language::parse(&source.replace("oa_dimension", "uuid")).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn semantic_values_remain_closed_typed_records_in_both_formats() {
|
||||
let model = project(vec![FacetReview::Data(DataReview::IntegerMinimum(
|
||||
reviewed(Some(0), None),
|
||||
))]);
|
||||
let source = language::format(&model);
|
||||
for invalid in [
|
||||
source.replace("IntegerMinimum", "UnknownFacet"),
|
||||
source.replace("Value 0;", "Value \"0\";"),
|
||||
source.replace("facets [", "unexpected true; facets ["),
|
||||
] {
|
||||
assert!(language::parse(&invalid).is_err(), "{invalid}");
|
||||
}
|
||||
let mut json = serde_json::to_value(&model).unwrap();
|
||||
json["traces"][id(4).to_string()]["baseline"]["semantic"] = serde_json::json!({"facets": [{"Data": {"IntegerMinimum": {"oa": {"Value": "0"}, "sa": {"Value": null}}}}]});
|
||||
assert!(persistence::decode(&serde_json::to_vec(&json).unwrap()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn facet_reordering_and_reidentification_keep_comments_with_typed_row_identity() {
|
||||
let row = MessageValue {
|
||||
path: vec![id(2)],
|
||||
field: id(90),
|
||||
expression: id(2).to_string(),
|
||||
};
|
||||
let model = project(vec![
|
||||
FacetReview::Annotation {
|
||||
keys: KeyPair {
|
||||
oa: Some(AnnotationKey::from(&row)),
|
||||
sa: None,
|
||||
},
|
||||
review: reviewed(Some(row), None),
|
||||
},
|
||||
FacetReview::Literal {
|
||||
keys: KeyPair {
|
||||
oa: Some(id(2).to_string()),
|
||||
sa: None,
|
||||
},
|
||||
review: reviewed(true, false),
|
||||
},
|
||||
FacetReview::Field(FieldReview::Type(reviewed(
|
||||
TypeRef::Named(id(2)),
|
||||
TypeRef::Boolean,
|
||||
))),
|
||||
]);
|
||||
let source = language::format(&model)
|
||||
.replace("Annotation {", "Annotation { // annotation rationale")
|
||||
.replace("Literal {", "Literal { // literal rationale")
|
||||
.replace("Named uuid ", "Named uuid // historical type rationale\n");
|
||||
let current = compile(source);
|
||||
let mut prepared = prepare_command(
|
||||
¤t.decoded.project,
|
||||
&Command::Reidentify(id(2), id(20)),
|
||||
)
|
||||
.unwrap();
|
||||
facets(&mut prepared.project).reverse();
|
||||
let updated = update_bundle_with_identity_map(
|
||||
¤t,
|
||||
&prepared.project,
|
||||
&prepared.identity_map,
|
||||
ProjectLimits::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let text = &updated.bundle.files["project.arcadia"];
|
||||
assert_eq!(updated.decoded.project, prepared.project);
|
||||
assert!(
|
||||
text.contains(&format!(
|
||||
"Named uuid // historical type rationale\n\"{}\"",
|
||||
id(20)
|
||||
)),
|
||||
"{text}"
|
||||
);
|
||||
let type_note = text.find("// historical type rationale").unwrap();
|
||||
let literal_note = text.find("// literal rationale").unwrap();
|
||||
let annotation_note = text.find("// annotation rationale").unwrap();
|
||||
assert!(
|
||||
type_note < literal_note && literal_note < annotation_note,
|
||||
"{text}"
|
||||
);
|
||||
assert!(text.contains(&format!("expression \"{}\"", id(2))));
|
||||
let FacetReview::Literal { keys, .. } = &facets(&mut prepared.project)[1] else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(
|
||||
keys.oa,
|
||||
Some(id(2).to_string()),
|
||||
"UUID-shaped literal keys are text"
|
||||
);
|
||||
assert_eq!(
|
||||
compile_bundle(&updated.bundle, ProjectLimits::default())
|
||||
.unwrap()
|
||||
.bundle,
|
||||
updated.bundle
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn historical_reference_deletion_does_not_remove_the_review_or_create_a_live_alias() {
|
||||
let mut model = project(vec![FacetReview::Field(FieldReview::Type(reviewed(
|
||||
TypeRef::Named(id(90)),
|
||||
TypeRef::Boolean,
|
||||
)))]);
|
||||
let mut former_type = model.elements[&id(2)].clone();
|
||||
former_type.id = id(90);
|
||||
former_type.alias = "former_type".into();
|
||||
model.elements.insert(id(90), former_type);
|
||||
let current = compile(language::format(&model));
|
||||
let prepared = prepare_command(¤t.decoded.project, &Command::Delete(id(90))).unwrap();
|
||||
let updated = update_bundle_with_identity_map(
|
||||
¤t,
|
||||
&prepared.project,
|
||||
&prepared.identity_map,
|
||||
ProjectLimits::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let source = &updated.bundle.files["project.arcadia"];
|
||||
assert!(!updated.decoded.project.contains(id(90)));
|
||||
assert!(!source.contains("@former_type"));
|
||||
assert!(source.contains(&format!("Named uuid \"{}\"", id(90))));
|
||||
assert_eq!(updated.decoded.project.traces, model.traces);
|
||||
assert_eq!(language::parse(source).unwrap(), prepared.project);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn included_historical_review_edits_save_and_reopen_without_rewriting_other_modules() {
|
||||
use oa_core::source_files::{load_project, save_project};
|
||||
let model = project(vec![FacetReview::Field(FieldReview::Type(reviewed(
|
||||
TypeRef::Named(id(90)),
|
||||
TypeRef::Boolean,
|
||||
)))]);
|
||||
let source = language::format(&model);
|
||||
let trace_at = source.find(" trace ").unwrap();
|
||||
let end = source.rfind('}').unwrap();
|
||||
let entry = format!(
|
||||
"{} include \"reviews.arcadia\";\n}}\n",
|
||||
&source[..trace_at]
|
||||
);
|
||||
let review =
|
||||
source[trace_at..end].replace("Named uuid ", "Named uuid // deleted type retained\n");
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let entry_path = directory.path().join("project.arcadia");
|
||||
std::fs::write(&entry_path, &entry).unwrap();
|
||||
std::fs::write(directory.path().join("reviews.arcadia"), &review).unwrap();
|
||||
let snapshot = load_project(&entry_path, ProjectLimits::default()).unwrap();
|
||||
let compiled = compile_bundle(&snapshot.reachable_bundle(), ProjectLimits::default()).unwrap();
|
||||
assert_eq!(compiled.index.declarations[&id(4)].path, "reviews.arcadia");
|
||||
let mut changed = compiled.decoded.project.clone();
|
||||
let FacetReview::Field(FieldReview::Type(value)) = &mut facets(&mut changed)[0] else {
|
||||
panic!()
|
||||
};
|
||||
value.sa = Observed::Value(TypeRef::String);
|
||||
let updated = update_bundle(&compiled, &changed, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(updated.bundle.files["project.arcadia"], entry);
|
||||
let saved = save_project(&snapshot, &updated, ProjectLimits::default()).unwrap();
|
||||
let reopened = load_project(&entry_path, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(saved.bundle, reopened.bundle);
|
||||
assert_eq!(
|
||||
compile_bundle(&reopened.reachable_bundle(), ProjectLimits::default())
|
||||
.unwrap()
|
||||
.decoded
|
||||
.project,
|
||||
changed
|
||||
);
|
||||
assert_eq!(std::fs::read_to_string(&entry_path).unwrap(), entry);
|
||||
assert!(reopened.bundle.files["reviews.arcadia"].contains("uuid // deleted type retained"));
|
||||
assert!(!changed.contains(id(90)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepared_semantic_review_writes_back_and_preserves_the_accepted_target_difference() {
|
||||
use oa_core::transitions::{TransitionDirection, TransitionMapping, TransitionRequest};
|
||||
let model = project(vec![]);
|
||||
let current = compile(language::format(&model).replace(
|
||||
"baseline {",
|
||||
"baseline { // prior prose review is independent",
|
||||
));
|
||||
let mut transition = TransitionRequest::new(TransitionDirection::OaToSa, vec![id(2)]);
|
||||
transition.mappings.push(TransitionMapping {
|
||||
source: id(2),
|
||||
target: Some(id(3)),
|
||||
target_kind: None,
|
||||
});
|
||||
let mut request = SemanticTransitionRequest::new(transition);
|
||||
request.reviews.push(SemanticReviewRequest {
|
||||
source: id(2),
|
||||
resolutions: vec![
|
||||
FacetResolution {
|
||||
facet: FacetSelector::Data(DataFacet::Unit),
|
||||
decision: SemanticDecision::KeepTarget,
|
||||
},
|
||||
FacetResolution {
|
||||
facet: FacetSelector::Header(HeaderFacet::Name),
|
||||
decision: SemanticDecision::TakeSource,
|
||||
},
|
||||
],
|
||||
reset: vec![],
|
||||
});
|
||||
let plan = plan_semantic_transition(¤t.decoded.project, request).unwrap();
|
||||
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
||||
let prepared = prepare_semantic_transition(¤t.decoded.project, &plan).unwrap();
|
||||
let updated = update_bundle_with_identity_map(
|
||||
¤t,
|
||||
&prepared.project,
|
||||
&prepared.identity_map,
|
||||
ProjectLimits::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
updated.decoded.project.elements[&id(3)].name,
|
||||
"OA dimension now"
|
||||
);
|
||||
assert!(
|
||||
matches!(updated.decoded.project.elements[&id(3)].shared(), Some(SharedBody::DataType { unit, .. }) if unit == "ft")
|
||||
);
|
||||
assert!(
|
||||
updated.bundle.files["project.arcadia"].contains("// prior prose review is independent")
|
||||
);
|
||||
let baseline = updated.decoded.project.traces[&id(4)]
|
||||
.baseline
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
assert_eq!(baseline.oa.name, "Approved OA name");
|
||||
assert!(
|
||||
baseline
|
||||
.semantic
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.facets
|
||||
.contains(&FacetReview::Data(DataReview::Unit(reviewed(
|
||||
"m".into(),
|
||||
"ft".into()
|
||||
))))
|
||||
);
|
||||
assert_eq!(
|
||||
language::parse(&language::format(&updated.decoded.project)).unwrap(),
|
||||
prepared.project
|
||||
);
|
||||
assert_eq!(
|
||||
persistence::decode(&serde_json::to_vec(&updated.decoded.project).unwrap()).unwrap(),
|
||||
prepared.project
|
||||
);
|
||||
}
|
||||
@@ -21,7 +21,7 @@ fn genuine_v3_preserves_typed_ports_and_allocates_deterministic_relationships()
|
||||
assert_eq!(decoded.migration, source.migration);
|
||||
let project = &decoded.project;
|
||||
let report = decoded.migration.as_ref().unwrap();
|
||||
assert_eq!((report.from_schema, report.to_schema), (3, 4));
|
||||
assert_eq!((report.from_schema, report.to_schema), (3, 5));
|
||||
assert_eq!(project.identities().len(), 19);
|
||||
assert!(report.preserved_ports.is_empty());
|
||||
assert!(report.generated_endpoints.is_empty());
|
||||
@@ -71,7 +71,7 @@ fn genuine_v3_preserves_typed_ports_and_allocates_deterministic_relationships()
|
||||
.any(|issue| issue.code == "message_value_ambiguous_path")
|
||||
);
|
||||
let canonical = language::format(project);
|
||||
assert!(canonical.starts_with("arcadia 4;"));
|
||||
assert!(canonical.starts_with("arcadia 5;"));
|
||||
let reopened = language::parse_with_report(&canonical).unwrap();
|
||||
assert_eq!(&reopened.project, project);
|
||||
assert!(reopened.migration.is_none());
|
||||
@@ -154,12 +154,12 @@ fn v1_and_frozen_v2_inputs_reach_four_through_the_shared_data_upgrade() {
|
||||
language::parse_with_report(include_str!("fixtures/v2-coastal-rescue.arcadia")).unwrap();
|
||||
assert_eq!(v2.project, text.project);
|
||||
assert_eq!(v2.migration, text.migration);
|
||||
assert_eq!(v2.migration.unwrap().to_schema, 4);
|
||||
assert_eq!(v2.migration.unwrap().to_schema, 5);
|
||||
let v1 = language::parse_with_report(
|
||||
"arcadia 1; project \"Old\" id \"00000000-0000-0000-0000-000000000001\" {}",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(v1.project.schema_version, 4);
|
||||
assert_eq!(v1.project.schema_version, 5);
|
||||
assert_eq!(v1.migration.unwrap().from_schema, 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ fn authored_v3_bundle_upgrade_preserves_includes_nested_ownership_comments_and_i
|
||||
};
|
||||
let compiled = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(compiled.bundle.files.len(), 2);
|
||||
assert!(compiled.bundle.files["project.arcadia"].contains("arcadia 4;"));
|
||||
assert!(compiled.bundle.files["project.arcadia"].contains("arcadia 5;"));
|
||||
assert!(compiled.bundle.files["project.arcadia"].contains("include \"contracts.arcadia\";"));
|
||||
for text in [
|
||||
"// Contracts stay in their original module.",
|
||||
|
||||
@@ -5,7 +5,7 @@ fn setup() -> tempfile::TempDir {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
fs::write(
|
||||
directory.path().join("project.arcadia"),
|
||||
"arcadia 4; project p \"P\" { include \"parts/a.arcadia\"; }\n",
|
||||
"arcadia 5; project p \"P\" { include \"parts/a.arcadia\"; }\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::create_dir(directory.path().join("parts")).unwrap();
|
||||
@@ -59,7 +59,7 @@ fn loading_keeps_original_no_uuid_bytes_and_saving_stabilizes_exactly_once() {
|
||||
fn invalid_and_missing_includes_keep_editable_text_and_located_diagnostics() {
|
||||
let directory = setup();
|
||||
let entry = directory.path().join("project.arcadia");
|
||||
fs::write(&entry, "arcadia 4; project p \"P\" {\n include \"missing.arcadia\";\n include \"parts/a.arcadia\";\n}\n").unwrap();
|
||||
fs::write(&entry, "arcadia 5; project p \"P\" {\n include \"missing.arcadia\";\n include \"parts/a.arcadia\";\n}\n").unwrap();
|
||||
fs::write(
|
||||
directory.path().join("parts/a.arcadia"),
|
||||
"oa_activity a \"A\" { invalid",
|
||||
@@ -102,7 +102,7 @@ fn discovery_never_refreshes_drafts_and_preserves_detached_buffers() {
|
||||
let overlays = BTreeMap::from([
|
||||
(
|
||||
"project.arcadia".into(),
|
||||
"arcadia 4; project p \"P\" { include \"new.arcadia\"; }".into(),
|
||||
"arcadia 5; project p \"P\" { include \"new.arcadia\"; }".into(),
|
||||
),
|
||||
(
|
||||
"parts/a.arcadia".into(),
|
||||
@@ -176,7 +176,7 @@ fn new_destinations_require_explicit_missing_expectations_and_reject_collisions(
|
||||
files: BTreeMap::from([
|
||||
(
|
||||
"project.arcadia".into(),
|
||||
"arcadia 4; project p \"P\" { include \"child.arcadia\"; }".into(),
|
||||
"arcadia 5; project p \"P\" { include \"child.arcadia\"; }".into(),
|
||||
),
|
||||
("child.arcadia".into(), "oa_activity a \"A\" {}".into()),
|
||||
]),
|
||||
@@ -256,7 +256,7 @@ fn physical_hard_link_aliases_are_rejected() {
|
||||
.unwrap();
|
||||
fs::write(
|
||||
directory.path().join("project.arcadia"),
|
||||
"arcadia 4; project p \"P\" { include \"parts/a.arcadia\"; include \"alias.arcadia\"; }",
|
||||
"arcadia 5; project p \"P\" { include \"parts/a.arcadia\"; include \"alias.arcadia\"; }",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
@@ -286,7 +286,7 @@ fn symlinks_to_outside_or_inside_project_are_rejected() {
|
||||
std::os::unix::fs::symlink(&destination, &link).unwrap();
|
||||
fs::write(
|
||||
directory.path().join("project.arcadia"),
|
||||
"arcadia 4; project p \"P\" { include \"alias.arcadia\"; }",
|
||||
"arcadia 5; project p \"P\" { include \"alias.arcadia\"; }",
|
||||
)
|
||||
.unwrap();
|
||||
let loaded = load_project(
|
||||
|
||||
@@ -6,7 +6,7 @@ fn source() -> CompiledSourceProject {
|
||||
&SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("project.arcadia".into(), "arcadia 4; project p \"Identity review\" { include \"team/first.arcadia\"; include \"team/second.arcadia\"; include \"contracts.arcadia\"; }".into()),
|
||||
("project.arcadia".into(), "arcadia 5; project p \"Identity review\" { include \"team/first.arcadia\"; include \"team/second.arcadia\"; include \"contracts.arcadia\"; }".into()),
|
||||
("team/first.arcadia".into(), "// Rationale for first\nsa_exchange_item first \"First\" { description \"First rationale\"; }\n".into()),
|
||||
("team/second.arcadia".into(), "// Rationale for second\nsa_exchange_item second \"Second\" {}\n".into()),
|
||||
("contracts.arcadia".into(), "sa_event contract \"Contract\" { body { Signal { items [\n @first // first item rationale\n ,\n @second // second item rationale\n ]; }; }; }\n".into()),
|
||||
|
||||
@@ -5,7 +5,7 @@ fn compile(a: &str, b: &str) -> CompiledSourceProject {
|
||||
compile_bundle(&SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("project.arcadia".into(), "arcadia 4; project p \"Nested writeback\" { include \"a.arcadia\"; include \"b.arcadia\"; }\n".into()),
|
||||
("project.arcadia".into(), "arcadia 5; project p \"Nested writeback\" { include \"a.arcadia\"; include \"b.arcadia\"; }\n".into()),
|
||||
("a.arcadia".into(), a.into()), ("b.arcadia".into(), b.into()),
|
||||
]),
|
||||
}, ProjectLimits::default()).unwrap()
|
||||
|
||||
@@ -30,7 +30,7 @@ fn id(project: &Project, alias: &str) -> Id {
|
||||
#[test]
|
||||
fn route_edits_keep_diagram_file_ownership_and_comments_with_edge_identity() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"Routing\" { include \"views.arcadia\"; include \"work.arcadia\"; }",
|
||||
"arcadia 5; project p \"Routing\" { include \"views.arcadia\"; include \"work.arcadia\"; }",
|
||||
&[
|
||||
(
|
||||
"work.arcadia",
|
||||
@@ -95,7 +95,7 @@ diagram review "Review" {
|
||||
#[test]
|
||||
fn invalid_cross_file_route_references_and_geometry_locate_the_authored_view() {
|
||||
let mut input = bundle(
|
||||
"arcadia 4; project p \"Routing\" { include \"views.arcadia\"; include \"work.arcadia\"; }",
|
||||
"arcadia 5; project p \"Routing\" { include \"views.arcadia\"; include \"work.arcadia\"; }",
|
||||
&[
|
||||
(
|
||||
"work.arcadia",
|
||||
@@ -127,7 +127,7 @@ fn invalid_cross_file_route_references_and_geometry_locate_the_authored_view() {
|
||||
|
||||
fn typed() -> SourceBundle {
|
||||
bundle(
|
||||
"arcadia 4; project test_model \"航海\" {\n include \"exchanges.arcadia\";\n include \"work/send.arcadia\";\n include \"work/receive.arcadia\";\n include \"types.arcadia\";\n}",
|
||||
"arcadia 5; project test_model \"航海\" {\n include \"exchanges.arcadia\";\n include \"work/send.arcadia\";\n include \"work/receive.arcadia\";\n include \"types.arcadia\";\n}",
|
||||
&[
|
||||
(
|
||||
"exchanges.arcadia",
|
||||
@@ -194,7 +194,7 @@ fn cross_file_typed_ports_global_forward_references_and_stable_ids() {
|
||||
#[test]
|
||||
fn advisory_lint_locations_refer_to_raw_drafts_before_identity_stabilization() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"work.arcadia\"; }",
|
||||
"arcadia 5; project p \"P\" { include \"work.arcadia\"; }",
|
||||
&[(
|
||||
"work.arcadia",
|
||||
"sa_function first \"First\" {}\nsa_function second \"Second\" {}\n",
|
||||
@@ -228,7 +228,7 @@ fn advisory_lint_locations_refer_to_raw_drafts_before_identity_stabilization() {
|
||||
#[test]
|
||||
fn source_errors_keep_exact_fragment_unicode_positions() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"child.arcadia\"; }",
|
||||
"arcadia 5; project p \"P\" { include \"child.arcadia\"; }",
|
||||
&[(
|
||||
"child.arcadia",
|
||||
"// Пример\nsa_function child \"航海\" { parent @missing; }",
|
||||
@@ -274,7 +274,7 @@ fn duplicate_alias_and_identity_report_both_files_including_project_identity() {
|
||||
),
|
||||
] {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
|
||||
"arcadia 5; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
|
||||
&[("a.arcadia", left), ("b.arcadia", right)],
|
||||
);
|
||||
let error = compile_bundle(&input, ProjectLimits::default())
|
||||
@@ -285,7 +285,7 @@ fn duplicate_alias_and_identity_report_both_files_including_project_identity() {
|
||||
assert_eq!(error.related[0].path, "a.arcadia");
|
||||
}
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"child.arcadia\"; }",
|
||||
"arcadia 5; project p \"P\" { include \"child.arcadia\"; }",
|
||||
&[("child.arcadia", "oa_activity p \"Collision\" {}")],
|
||||
);
|
||||
let error = compile_bundle(&input, ProjectLimits::default())
|
||||
@@ -297,7 +297,7 @@ fn duplicate_alias_and_identity_report_both_files_including_project_identity() {
|
||||
#[test]
|
||||
fn includes_are_once_only_and_cycles_missing_and_unreachable_files_are_located() {
|
||||
let mut input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"sub/a.arcadia\"; include \"sub/b.arcadia\"; }",
|
||||
"arcadia 5; project p \"P\" { include \"sub/a.arcadia\"; include \"sub/b.arcadia\"; }",
|
||||
&[
|
||||
("sub/a.arcadia", "include \"../shared.arcadia\";"),
|
||||
("sub/b.arcadia", "include \"../shared.arcadia\";"),
|
||||
@@ -356,7 +356,7 @@ fn portable_paths_reject_escapes_devices_urls_and_case_collisions() {
|
||||
);
|
||||
assert!(resolve_include("../bad.arcadia", "a.arcadia").is_err());
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" {}",
|
||||
"arcadia 5; project p \"P\" {}",
|
||||
&[("A.arcadia", ""), ("a.arcadia", "")],
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -370,7 +370,7 @@ fn portable_paths_reject_escapes_devices_urls_and_case_collisions() {
|
||||
#[test]
|
||||
fn project_admission_is_aggregate_and_precedes_global_resolution() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
|
||||
"arcadia 5; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
|
||||
&[
|
||||
("a.arcadia", "oa_activity one \"One\" { parent @missing; }"),
|
||||
("b.arcadia", "oa_activity two \"Two\" {}"),
|
||||
@@ -591,7 +591,7 @@ fn failed_update_preserves_candidate_and_rejects_stale_index() {
|
||||
|
||||
#[test]
|
||||
fn single_string_and_legacy_apis_remain_explicit_and_filesystem_free() {
|
||||
let source = "arcadia 4; project p \"P\" { include \"missing.arcadia\"; }";
|
||||
let source = "arcadia 5; project p \"P\" { include \"missing.arcadia\"; }";
|
||||
assert!(
|
||||
language::parse(source).unwrap_err()[0]
|
||||
.message
|
||||
@@ -681,7 +681,7 @@ fn json_bundle_rejects_duplicate_file_keys_and_unknown_fields() {
|
||||
#[test]
|
||||
fn generated_id_insertion_handles_adjacent_header_tokens_and_remains_exactly_indexed() {
|
||||
let input = bundle(
|
||||
"arcadia 4;project p\"P\"{include \"child.arcadia\";}",
|
||||
"arcadia 5;project p\"P\"{include \"child.arcadia\";}",
|
||||
&[(
|
||||
"child.arcadia",
|
||||
"oa_activity a\"A\"{}diagram d\"D\"{}node n{diagram @d;element @a;}",
|
||||
@@ -703,7 +703,7 @@ fn generated_id_insertion_handles_adjacent_header_tokens_and_remains_exactly_ind
|
||||
#[test]
|
||||
fn duplicate_include_edges_count_toward_admission_and_ordered_references_are_aggregate() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"a.arcadia\"; include \"a.arcadia\"; }",
|
||||
"arcadia 5; project p \"P\" { include \"a.arcadia\"; include \"a.arcadia\"; }",
|
||||
&[("a.arcadia", "")],
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -720,7 +720,7 @@ fn duplicate_include_edges_count_toward_admission_and_ordered_references_are_agg
|
||||
"project_limit"
|
||||
);
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
|
||||
"arcadia 5; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
|
||||
&[
|
||||
(
|
||||
"a.arcadia",
|
||||
@@ -763,7 +763,7 @@ fn include_inspection_reuses_strict_entry_fragment_syntax_and_real_spans() {
|
||||
&source[span.start..span.end],
|
||||
"include \"../shared.arcadia\";"
|
||||
);
|
||||
let entry = "arcadia 4; project p \"P\" { include \"sub/work.arcadia\"; }";
|
||||
let entry = "arcadia 5; project p \"P\" { include \"sub/work.arcadia\"; }";
|
||||
assert_eq!(
|
||||
inspect_includes("project.arcadia", entry, true, ProjectLimits::default()).unwrap()[0]
|
||||
.target,
|
||||
@@ -829,7 +829,7 @@ fn comments(source: &str) -> Vec<String> {
|
||||
|
||||
#[test]
|
||||
fn scalar_header_guard_and_project_edits_preserve_exact_crlf_trivia_and_escape_spelling() {
|
||||
let entry = "arcadia 4;\r\nproject p // project title\r\n \"Original project\" {\r\n description // project reason\r\n \"Before\";\r\n include \"behavior.arcadia\";\r\n}\r\n";
|
||||
let entry = "arcadia 5;\r\nproject p // project title\r\n \"Original project\" {\r\n description // project reason\r\n \"Before\";\r\n include \"behavior.arcadia\";\r\n}\r\n";
|
||||
let source = "oa_state_machine machine \"Machine\" {}\r\noa_region region_one \"Region\" { parent @machine; }\r\noa_state first \"First\" { parent @region_one; }\r\noa_state second \"Second\" { parent @region_one; }\r\nstate_transition route // route title\r\n \"Original transition\" {\r\n\tguard // feasibility\r\n\t \"old guard\"; // retain explanation\r\n\tdescription \"Keep\\u0020spelling\";\r\n\tsource @ // endpoint proof\r\n first; target @second;\r\n}\r\n";
|
||||
let current = compile(&bundle(entry, &[("behavior.arcadia", source)]));
|
||||
let mut project = current.decoded.project.clone();
|
||||
@@ -864,7 +864,7 @@ fn scalar_header_guard_and_project_edits_preserve_exact_crlf_trivia_and_escape_s
|
||||
#[test]
|
||||
fn alias_updates_touch_only_header_and_reference_tokens_even_with_comments_after_at() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"work.arcadia\"; }",
|
||||
"arcadia 5; project p \"P\" { include \"work.arcadia\"; }",
|
||||
&[(
|
||||
"work.arcadia",
|
||||
"oa_activity root_work // root rationale\n \"Root\\u0020name\" { description \"root_work is prose\"; }\noa_activity child \"Child\" { parent @ // parent proof\n root_work; }\n",
|
||||
@@ -887,7 +887,7 @@ fn alias_updates_touch_only_header_and_reference_tokens_even_with_comments_after
|
||||
#[test]
|
||||
fn omitted_defaults_stay_omitted_and_new_fields_do_not_reformat_surviving_fields() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"work.arcadia\"; }",
|
||||
"arcadia 5; project p \"P\" { include \"work.arcadia\"; }",
|
||||
&[(
|
||||
"work.arcadia",
|
||||
"oa_activity work \"Work\" {\r\n\t// owner rationale\r\n\tparent none ; // preserve spacing\r\n}\r\n",
|
||||
@@ -913,7 +913,7 @@ fn omitted_defaults_stay_omitted_and_new_fields_do_not_reformat_surviving_fields
|
||||
#[test]
|
||||
fn kind_change_removes_field_syntax_but_keeps_its_comments_and_other_fields() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"item.arcadia\"; }",
|
||||
"arcadia 5; project p \"P\" { include \"item.arcadia\"; }",
|
||||
&[(
|
||||
"item.arcadia",
|
||||
"oa_exchange_item item \"Item\" {\n description \"Keep\\u0020this\" ; // description note\n kind // classification rationale\n Information ;\n parent none;\n}\n",
|
||||
@@ -953,7 +953,7 @@ fn significant_words(source: &str) -> Vec<String> {
|
||||
#[test]
|
||||
fn constructor_tag_changes_retain_shared_nested_fields_and_removed_field_comments() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"event.arcadia\"; }",
|
||||
"arcadia 5; project p \"P\" { include \"event.arcadia\"; }",
|
||||
&[(
|
||||
"event.arcadia",
|
||||
"oa_event timer \"Timer\" { body { Time // time rationale\n { expression // expression rationale\n \"before\" ; kind After; // clock rationale\n }; }; }\n",
|
||||
@@ -986,7 +986,7 @@ fn constructor_tag_changes_retain_shared_nested_fields_and_removed_field_comment
|
||||
#[test]
|
||||
fn scalar_record_constructor_changes_keep_comments_inside_removed_values() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"ports.arcadia\"; }",
|
||||
"arcadia 5; project p \"P\" { include \"ports.arcadia\"; }",
|
||||
&[(
|
||||
"ports.arcadia",
|
||||
"sa_function work \"Work\" {}\nsa_port input \"Input\" { parent @work; contract Legacy; // contract rationale\n legacy { direction In; // direction rationale\n interface none; // interface rationale\n }; }\n",
|
||||
@@ -1017,7 +1017,7 @@ fn scalar_record_constructor_changes_keep_comments_inside_removed_values() {
|
||||
#[test]
|
||||
fn reference_list_reordering_carries_item_comments_and_alias_spelling_with_identity() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"types.arcadia\"; }",
|
||||
"arcadia 5; project p \"P\" { include \"types.arcadia\"; }",
|
||||
&[(
|
||||
"types.arcadia",
|
||||
"sa_exchange_item a \"A\" {} sa_exchange_item b \"B\" {}\nsa_event service \"Service\" { body { Signal { items [\n // before A\n @ // A reference rationale\n a // after A\n ,\n // before B\n @b // after B\n ]; }; }; }\n",
|
||||
@@ -1052,7 +1052,7 @@ fn reference_list_reordering_carries_item_comments_and_alias_spelling_with_ident
|
||||
#[test]
|
||||
fn removed_list_items_and_declarations_keep_comments_as_enclosing_annotations() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"types.arcadia\"; }",
|
||||
"arcadia 5; project p \"P\" { include \"types.arcadia\"; }",
|
||||
&[(
|
||||
"types.arcadia",
|
||||
"sa_exchange_item a \"A\" { // declaration explanation\n } sa_exchange_item b \"B\" {}\nsa_event service \"Service\" { body { Signal { items [\n // explanation for removed A\n @a,\n // explanation for retained B\n @b ]; }; }; }\n",
|
||||
@@ -1104,7 +1104,7 @@ oa_occurrence message "Message" { parent @conversation; body { Exchange {
|
||||
}; }; }
|
||||
"#;
|
||||
let current = compile(&bundle(
|
||||
"arcadia 4; project p \"P\" { include \"conversation.arcadia\"; }",
|
||||
"arcadia 5; project p \"P\" { include \"conversation.arcadia\"; }",
|
||||
&[("conversation.arcadia", source)],
|
||||
));
|
||||
let mut project = current.decoded.project.clone();
|
||||
@@ -1153,7 +1153,7 @@ oa_occurrence message "Message" { parent @conversation; body { Exchange {
|
||||
#[test]
|
||||
fn added_list_items_empty_lists_and_literal_changes_preserve_existing_annotations() {
|
||||
let input = bundle(
|
||||
"arcadia 4; project p \"P\" { include \"types.arcadia\"; }",
|
||||
"arcadia 5; project p \"P\" { include \"types.arcadia\"; }",
|
||||
&[(
|
||||
"types.arcadia",
|
||||
"oa_data_type values \"Values\" { definition { Enumeration { literals [ // enumeration rationale\n ]; }; }; }\n",
|
||||
|
||||
Reference in New Issue
Block a user