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());
|
||||
|
||||
Reference in New Issue
Block a user