Add source-first CLI with guarded writes and reviewed evidence
This commit is contained in:
@@ -32,6 +32,8 @@ harness/experiments/analysis-context-20260911/** -text whitespace=cr-at-eol
|
||||
harness/experiments/analysis-baseline-20260911/** -text whitespace=cr-at-eol
|
||||
harness/experiments/library-compatibility-20260911/** -text whitespace=cr-at-eol
|
||||
harness/experiments/refined-contracts-review-20260911/** -text whitespace=cr-at-eol
|
||||
harness/behavior-creation-contract.rs -text whitespace=cr-at-eol
|
||||
harness/experiments/source-cli-independent-20260911/** -text whitespace=cr-at-eol
|
||||
|
||||
# Preserve checksum-bound font notices verbatim through Git round trips.
|
||||
scripts/windows-notices/GNOME-Cantarell-61362600e104-COPYING -text
|
||||
|
||||
Generated
+9
@@ -2139,6 +2139,15 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oa-cli"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"oa-core",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oa-core"
|
||||
version = "0.2.0"
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
[workspace]
|
||||
members = ["crates/oa-core", "crates/oa-desktop", "crates/oa-server"]
|
||||
members = ["crates/oa-core", "crates/oa-desktop", "crates/oa-server", "crates/oa-cli"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
|
||||
@@ -18,7 +18,7 @@ From the repository root:
|
||||
cargo run --locked -p oa-desktop --bin arcadia-studio
|
||||
```
|
||||
|
||||
Build both desktop and server release executables:
|
||||
Build the desktop, server and source CLI release executables:
|
||||
|
||||
```sh
|
||||
cargo build --release --workspace --locked
|
||||
@@ -51,6 +51,21 @@ The earlier V2 package record describes `dist/ArcadiaStudio-Windows.zip` and `di
|
||||
|
||||
The current [Windows packaging instructions](docs/windows-packaging.md) stage both executables, verify their runtime DLL dependencies, and produce an archive with checksums and provenance. Native acceptance fixtures and independent expected results are retained in [the release fixture directory](harness/fixtures/release1-native/README.md).
|
||||
|
||||
## Work with source files from the command line
|
||||
|
||||
The `arcadia` CLI checks an explicit `.arcadia` entry file and its includes without
|
||||
starting the desktop or server:
|
||||
|
||||
```sh
|
||||
cargo run --locked -p oa-cli --bin arcadia -- check examples/text-first/project.arcadia
|
||||
```
|
||||
|
||||
`stabilize ROOT` previews the identities needed for persistent editing;
|
||||
`rename-alias ROOT OLD NEW` previews an alias change and its references. Add
|
||||
`--write` to save that invocation's candidate, or `--json` for structured
|
||||
diagnostics and changed-file text/diffs. Preview leaves files untouched.
|
||||
See [source CLI commands](docs/source-cli.md) for examples and exit codes.
|
||||
|
||||
## Model operational needs and system responsibilities
|
||||
|
||||
The desktop starts at **Home**, where you can create a model, open a local project, continue recent files or recover retained work. Missing recent files stay visible with **Locate**; **Remove** only forgets their history entry. Choose **Explore rescue example** for a model combining people, an organization, a System of Interest, information and physical exchange items, normal/degraded behavior and code/CAD evidence.
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "oa-cli"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "arcadia"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
oa-core = { path = "../oa-core" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -0,0 +1,451 @@
|
||||
//! Source-only authoring. Preparation captures bytes; only core owns disk writes.
|
||||
use oa_core::{
|
||||
Command, language,
|
||||
persistence::MigrationReport,
|
||||
prepare_command,
|
||||
source_files::{self, DiskProject, SourceFileError},
|
||||
source_project::{self, CompiledSourceProject, ProjectLimits, SourceDiagnostic},
|
||||
validation::Severity,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub const LIMITS: ProjectLimits = ProjectLimits {
|
||||
max_files: 256,
|
||||
max_include_depth: 32,
|
||||
max_include_edges: 1024,
|
||||
model: language::ParseLimits {
|
||||
max_source_bytes: 64 * 1024 * 1024,
|
||||
max_graph_objects: 100_000,
|
||||
max_records: 250_000,
|
||||
max_step_references: 500_000,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Operation {
|
||||
Check,
|
||||
Stabilize,
|
||||
RenameAlias { old: String, new: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct Location {
|
||||
pub path: String,
|
||||
/// Half-open UTF-8 byte offsets. Line/column are one-based Unicode scalars.
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
pub line: Option<usize>,
|
||||
pub column: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct Diagnostic {
|
||||
pub severity: String,
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
pub coordinate_space: String,
|
||||
pub location: Option<Location>,
|
||||
pub related: Vec<Location>,
|
||||
pub object_id: Option<String>,
|
||||
pub field: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct Change {
|
||||
pub path: String,
|
||||
/// Exact captured and proposed UTF-8 text, including line endings.
|
||||
pub before: String,
|
||||
pub after: String,
|
||||
pub diff: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct Report {
|
||||
pub format_version: u32,
|
||||
pub entry: String,
|
||||
pub operation: String,
|
||||
pub status: String,
|
||||
pub exit_code: u8,
|
||||
/// True only after a successful changed save. False on failure does not
|
||||
/// promise rollback: consult the specific error/recovery outcome.
|
||||
pub written: bool,
|
||||
pub diagnostics: Vec<Diagnostic>,
|
||||
pub changes: Vec<Change>,
|
||||
pub identities_to_stabilize: usize,
|
||||
pub migration: Option<MigrationReport>,
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
impl Report {
|
||||
pub fn failure(
|
||||
entry: &Path,
|
||||
operation: &str,
|
||||
exit_code: u8,
|
||||
code: &str,
|
||||
message: String,
|
||||
) -> Self {
|
||||
let mut report = Self::empty(entry, operation);
|
||||
report.fail(exit_code, code, message);
|
||||
report
|
||||
}
|
||||
|
||||
fn empty(entry: &Path, operation: &str) -> Self {
|
||||
Self {
|
||||
format_version: 1,
|
||||
entry: entry.to_string_lossy().into_owned(),
|
||||
operation: operation.into(),
|
||||
status: "checked".into(),
|
||||
exit_code: 0,
|
||||
written: false,
|
||||
diagnostics: vec![],
|
||||
changes: vec![],
|
||||
identities_to_stabilize: 0,
|
||||
migration: None,
|
||||
notes: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn fail(&mut self, exit_code: u8, code: &str, message: String) {
|
||||
self.status = "rejected".into();
|
||||
self.exit_code = exit_code;
|
||||
self.diagnostics.push(Diagnostic {
|
||||
severity: "error".into(),
|
||||
code: code.into(),
|
||||
message,
|
||||
coordinate_space: "operation".into(),
|
||||
location: None,
|
||||
related: vec![],
|
||||
object_id: None,
|
||||
field: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn located(issue: SourceDiagnostic, space: &str) -> Diagnostic {
|
||||
let d = issue.diagnostic;
|
||||
Diagnostic {
|
||||
severity: match d.severity {
|
||||
Severity::Error => "error",
|
||||
Severity::Warning => "warning",
|
||||
}
|
||||
.into(),
|
||||
code: d.code,
|
||||
message: d.message,
|
||||
coordinate_space: space.into(),
|
||||
location: Some(Location {
|
||||
path: issue.path,
|
||||
start: d.span.start,
|
||||
end: d.span.end,
|
||||
line: Some(d.line),
|
||||
column: Some(d.column),
|
||||
}),
|
||||
related: issue
|
||||
.related
|
||||
.into_iter()
|
||||
.map(|at| Location {
|
||||
path: at.path,
|
||||
start: at.span.start,
|
||||
end: at.span.end,
|
||||
line: None,
|
||||
column: None,
|
||||
})
|
||||
.collect(),
|
||||
object_id: d.object_id.map(|id| id.to_string()),
|
||||
field: d.field,
|
||||
}
|
||||
}
|
||||
|
||||
fn disk_error(report: &mut Report, error: SourceFileError) {
|
||||
let code = match &error {
|
||||
SourceFileError::Conflict(_) => "external_change",
|
||||
SourceFileError::RecoveryRequired(_) => "recovery_required",
|
||||
SourceFileError::RecoveryFailed { .. } => "recovery_failed",
|
||||
SourceFileError::Path(_) => "unsafe_path",
|
||||
SourceFileError::Limit(_) => "source_limit",
|
||||
SourceFileError::UnknownExpectation(_) => "unknown_disk_expectation",
|
||||
SourceFileError::Invalid(_) => "invalid_save_candidate",
|
||||
SourceFileError::Io { .. } => "io_error",
|
||||
};
|
||||
report.fail(3, code, error.to_string());
|
||||
}
|
||||
|
||||
/// A candidate owns its original expectations and is consumed by saving.
|
||||
/// Callers cannot replace its source/model/expectations after preparation.
|
||||
pub struct Prepared {
|
||||
report: Report,
|
||||
snapshot: DiskProject,
|
||||
compiled: CompiledSourceProject,
|
||||
mutable: bool,
|
||||
}
|
||||
|
||||
impl Prepared {
|
||||
pub fn report(&self) -> &Report {
|
||||
&self.report
|
||||
}
|
||||
|
||||
pub fn into_report(self) -> Report {
|
||||
self.report
|
||||
}
|
||||
|
||||
pub fn write(mut self) -> Report {
|
||||
if !self.mutable {
|
||||
self.report
|
||||
.fail(2, "usage", "check/lint does not accept --write".into());
|
||||
return self.report;
|
||||
}
|
||||
match source_files::save_project(&self.snapshot, &self.compiled, LIMITS) {
|
||||
Ok(_) => {
|
||||
self.report.written = !self.report.changes.is_empty();
|
||||
self.report.status = if self.report.written {
|
||||
"written"
|
||||
} else {
|
||||
"unchanged"
|
||||
}
|
||||
.into();
|
||||
}
|
||||
Err(error) => disk_error(&mut self.report, error),
|
||||
}
|
||||
self.report
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prepare(entry: &Path, operation: Operation) -> Result<Prepared, Box<Report>> {
|
||||
let name = match operation {
|
||||
Operation::Check => "check",
|
||||
Operation::Stabilize => "stabilize",
|
||||
Operation::RenameAlias { .. } => "rename-alias",
|
||||
};
|
||||
let mut report = Report::empty(entry, name);
|
||||
let snapshot = source_files::load_project(entry, LIMITS).map_err(|error| {
|
||||
disk_error(&mut report, error);
|
||||
Box::new(report.clone())
|
||||
})?;
|
||||
let raw = snapshot.reachable_bundle();
|
||||
// The loader may include warnings from stabilized compilation. Only its
|
||||
// discovery/IO diagnostics are authoritative on the original bytes.
|
||||
let mut diagnostics: Vec<_> = snapshot
|
||||
.diagnostics
|
||||
.iter()
|
||||
.filter(|d| {
|
||||
matches!(
|
||||
d.diagnostic.code.as_str(),
|
||||
"pending_save"
|
||||
| "source_encoding"
|
||||
| "source_limit"
|
||||
| "invalid_path"
|
||||
| "missing_include"
|
||||
| "project_limit"
|
||||
)
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
for diagnostic in source_project::lint_bundle(&raw, LIMITS) {
|
||||
if !diagnostics.contains(&diagnostic) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
let disk_refusal = snapshot.diagnostics.iter().any(|d| {
|
||||
matches!(
|
||||
d.diagnostic.code.as_str(),
|
||||
"pending_save" | "missing_include" | "invalid_path"
|
||||
)
|
||||
});
|
||||
let invalid = diagnostics
|
||||
.iter()
|
||||
.any(|d| d.diagnostic.severity == Severity::Error);
|
||||
report.diagnostics = diagnostics
|
||||
.into_iter()
|
||||
.map(|d| located(d, "input"))
|
||||
.collect();
|
||||
if invalid {
|
||||
// Do not run declaration analysis or any updater on invalid raw text.
|
||||
// Failed lint may also have allocated provisional object identities.
|
||||
for diagnostic in &mut report.diagnostics {
|
||||
diagnostic.object_id = None;
|
||||
}
|
||||
report.status = "invalid".into();
|
||||
report.exit_code = if disk_refusal { 3 } else { 1 };
|
||||
return Err(Box::new(report));
|
||||
}
|
||||
let explicit_ids: std::collections::BTreeSet<_> = raw
|
||||
.files
|
||||
.values()
|
||||
.flat_map(|text| language::analyze(text).declarations)
|
||||
.filter_map(|d| d.id.map(|id| id.to_string()))
|
||||
.collect();
|
||||
for diagnostic in &mut report.diagnostics {
|
||||
if diagnostic
|
||||
.object_id
|
||||
.as_ref()
|
||||
.is_some_and(|id| !explicit_ids.contains(id))
|
||||
{
|
||||
diagnostic.object_id = None;
|
||||
}
|
||||
}
|
||||
let mut compiled = source_project::compile_bundle(&raw, LIMITS).map_err(|diagnostics| {
|
||||
report.status = "invalid".into();
|
||||
report.exit_code = 1;
|
||||
report
|
||||
.diagnostics
|
||||
.extend(diagnostics.into_iter().map(|d| located(d, "candidate")));
|
||||
Box::new(report.clone())
|
||||
})?;
|
||||
report.identities_to_stabilize = raw
|
||||
.files
|
||||
.values()
|
||||
.map(|text| {
|
||||
language::analyze(text)
|
||||
.declarations
|
||||
.into_iter()
|
||||
.filter(|d| d.id.is_none())
|
||||
.count()
|
||||
})
|
||||
.sum();
|
||||
report.migration = compiled.decoded.migration.clone();
|
||||
let mutable = !matches!(operation, Operation::Check);
|
||||
if let Operation::RenameAlias { old, new } = operation {
|
||||
let project = &compiled.decoded.project;
|
||||
let Some((id, _)) = project
|
||||
.identities()
|
||||
.into_iter()
|
||||
.find(|(_, alias)| *alias == old)
|
||||
else {
|
||||
report.fail(
|
||||
1,
|
||||
"unknown_alias",
|
||||
format!("No declaration has alias '{old}'."),
|
||||
);
|
||||
return Err(Box::new(report));
|
||||
};
|
||||
let command = prepare_command(project, &Command::SetAlias(id, new)).map_err(|error| {
|
||||
report.fail(1, "semantic_command_rejected", error.to_string());
|
||||
Box::new(report.clone())
|
||||
})?;
|
||||
compiled = source_project::update_bundle_with_identity_map(
|
||||
&compiled,
|
||||
&command.project,
|
||||
&command.identity_map,
|
||||
LIMITS,
|
||||
)
|
||||
.map_err(|diagnostics| {
|
||||
report.status = "invalid".into();
|
||||
report.exit_code = 1;
|
||||
report
|
||||
.diagnostics
|
||||
.extend(diagnostics.into_iter().map(|d| located(d, "candidate")));
|
||||
Box::new(report.clone())
|
||||
})?;
|
||||
}
|
||||
if mutable {
|
||||
for (path, after) in &compiled.bundle.files {
|
||||
let before = raw.files.get(path).cloned().unwrap_or_default();
|
||||
if &before != after {
|
||||
report.changes.push(Change {
|
||||
path: path.clone(),
|
||||
diff: unified_diff(path, &before, after),
|
||||
before,
|
||||
after: after.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
report.status = "preview".into();
|
||||
report.notes.push("This invocation's complete proposed changed-file texts and diffs are reported. Without --write nothing is saved; a later invocation may allocate different missing UUIDs. This is not a saved plan or a digest of unchanged dependencies.".into());
|
||||
}
|
||||
report.notes.push("Diagnostic object IDs are omitted for declarations without authored IDs; lint's temporary identities are not the candidate's identities.".into());
|
||||
report.notes.push("Disk writes use the core journal/conflict checks. This is not cross-process atomic visibility or a power-loss durability guarantee; pending saves are never implicitly recovered. A failed write may require explicit journal recovery.".into());
|
||||
Ok(Prepared {
|
||||
report,
|
||||
snapshot,
|
||||
compiled,
|
||||
mutable,
|
||||
})
|
||||
}
|
||||
|
||||
/// One bounded hunk per file: common prefix/suffix plus three context lines.
|
||||
/// Linear in input size; no quadratic LCS allocation for large modules.
|
||||
fn unified_diff(path: &str, before: &str, after: &str) -> String {
|
||||
let a: Vec<_> = before.split_inclusive('\n').collect();
|
||||
let b: Vec<_> = after.split_inclusive('\n').collect();
|
||||
let prefix = a.iter().zip(&b).take_while(|(a, b)| a == b).count();
|
||||
let suffix = a[prefix..]
|
||||
.iter()
|
||||
.rev()
|
||||
.zip(b[prefix..].iter().rev())
|
||||
.take_while(|(a, b)| a == b)
|
||||
.count();
|
||||
let start = prefix.saturating_sub(3);
|
||||
let a_end = (a.len() - suffix + 3).min(a.len());
|
||||
let b_end = (b.len() - suffix + 3).min(b.len());
|
||||
let a_start = if a_end == start { start } else { start + 1 };
|
||||
let b_start = if b_end == start { start } else { start + 1 };
|
||||
let mut out = format!(
|
||||
"--- a/{path}\n+++ b/{path}\n@@ -{a_start},{} +{b_start},{} @@\n",
|
||||
a_end - start,
|
||||
b_end - start
|
||||
);
|
||||
let mut emit = |prefix: char, line: &str| {
|
||||
out.push(prefix);
|
||||
out.push_str(line);
|
||||
if !line.ends_with('\n') {
|
||||
out.push_str("\n\\ No newline at end of file\n");
|
||||
}
|
||||
};
|
||||
for line in &a[start..prefix] {
|
||||
emit(' ', line);
|
||||
}
|
||||
for line in &a[prefix..a.len() - suffix] {
|
||||
emit('-', line);
|
||||
}
|
||||
for line in &b[prefix..b.len() - suffix] {
|
||||
emit('+', line);
|
||||
}
|
||||
for line in &a[a.len() - suffix..a_end] {
|
||||
emit(' ', line);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub struct Arguments {
|
||||
pub entry: PathBuf,
|
||||
pub operation: Operation,
|
||||
pub write: bool,
|
||||
pub json: bool,
|
||||
}
|
||||
|
||||
pub const HELP: &str = "arcadia check ROOT [--json]\narcadia lint ROOT [--json]\narcadia stabilize ROOT [--write] [--json]\narcadia rename-alias ROOT OLD NEW [--write] [--json]\n\nROOT is an explicit .arcadia entry file; includes resolve relative to it.\nMutation commands preview by default. --write applies this invocation's candidate.\nMissing UUIDs in a preview are provisional until written. No canonical formatting.\nExit: 0 valid/success (engineering warnings allowed), 1 invalid source or semantic\ncommand rejection, 2 usage, 3 disk/path/conflict/pending-journal/output failure.\nNo implicit journal recovery. Limits: 64 MiB, 256 files, include depth32/edges1024,\n100000 graph objects, 250000 records, 500000 ordered references.\n";
|
||||
|
||||
pub fn arguments(args: &[std::ffi::OsString]) -> Result<Arguments, String> {
|
||||
let mut positional = Vec::new();
|
||||
let (mut write, mut json) = (false, false);
|
||||
for arg in args {
|
||||
if arg == "--write" && !write {
|
||||
write = true;
|
||||
} else if arg == "--json" && !json {
|
||||
json = true;
|
||||
} else if arg.to_string_lossy().starts_with('-') {
|
||||
return Err(format!(
|
||||
"Unknown or repeated option: {}",
|
||||
arg.to_string_lossy()
|
||||
));
|
||||
} else {
|
||||
positional.push(arg);
|
||||
}
|
||||
}
|
||||
let command = positional.first().and_then(|s| s.to_str()).unwrap_or("");
|
||||
let operation = match (command, positional.len()) {
|
||||
("check" | "lint", 2) if !write => Operation::Check,
|
||||
("stabilize", 2) => Operation::Stabilize,
|
||||
("rename-alias", 4) => Operation::RenameAlias {
|
||||
old: positional[2].to_str().ok_or("Alias must be UTF-8")?.into(),
|
||||
new: positional[3].to_str().ok_or("Alias must be UTF-8")?.into(),
|
||||
},
|
||||
_ => return Err(HELP.into()),
|
||||
};
|
||||
Ok(Arguments {
|
||||
entry: PathBuf::from(positional[1]),
|
||||
operation,
|
||||
write,
|
||||
json,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
use oa_cli::{Report, arguments, prepare};
|
||||
use std::{
|
||||
io::{self, Write},
|
||||
path::Path,
|
||||
process::ExitCode,
|
||||
};
|
||||
|
||||
fn human_report(report: &Report, show_diff: bool) -> io::Result<()> {
|
||||
let mut err = io::stderr().lock();
|
||||
for d in &report.diagnostics {
|
||||
let at = d
|
||||
.location
|
||||
.as_ref()
|
||||
.map(|l| {
|
||||
format!(
|
||||
"{}:{}:{}",
|
||||
l.path,
|
||||
l.line.unwrap_or(0),
|
||||
l.column.unwrap_or(0)
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| report.entry.clone());
|
||||
writeln!(
|
||||
err,
|
||||
"{at}: {}[{}] ({}): {}",
|
||||
d.severity, d.code, d.coordinate_space, d.message
|
||||
)?;
|
||||
}
|
||||
writeln!(
|
||||
err,
|
||||
"{}: {} file changes; {} identities to stabilize",
|
||||
report.status,
|
||||
report.changes.len(),
|
||||
report.identities_to_stabilize
|
||||
)?;
|
||||
if let Some(migration) = &report.migration {
|
||||
writeln!(
|
||||
err,
|
||||
"Source migration: schema {} → {} (separate from identity stabilization)",
|
||||
migration.from_schema, migration.to_schema
|
||||
)?;
|
||||
}
|
||||
for note in &report.notes {
|
||||
writeln!(err, "{note}")?;
|
||||
}
|
||||
if show_diff {
|
||||
let mut out = io::stdout().lock();
|
||||
for change in &report.changes {
|
||||
write!(out, "{}", change.diff)?;
|
||||
}
|
||||
out.flush()?;
|
||||
}
|
||||
err.flush()
|
||||
}
|
||||
|
||||
fn run() -> io::Result<u8> {
|
||||
let args: Vec<_> = std::env::args_os().skip(1).collect();
|
||||
if args.len() == 1 && (args[0] == "--help" || args[0] == "-h") {
|
||||
io::stdout().write_all(oa_cli::HELP.as_bytes())?;
|
||||
return Ok(0);
|
||||
}
|
||||
if args.len() == 1 && args[0] == "--version" {
|
||||
writeln!(io::stdout(), "arcadia {}", env!("CARGO_PKG_VERSION"))?;
|
||||
return Ok(0);
|
||||
}
|
||||
let json = args.iter().any(|a| a == "--json");
|
||||
let mut displayed = false;
|
||||
let report = match arguments(&args) {
|
||||
Err(error) => Report::failure(Path::new(""), "usage", 2, "usage", error),
|
||||
Ok(args) => match prepare(&args.entry, args.operation) {
|
||||
Err(report) => *report,
|
||||
Ok(prepared) => {
|
||||
if args.write {
|
||||
if !args.json {
|
||||
human_report(prepared.report(), true)?;
|
||||
displayed = true;
|
||||
}
|
||||
prepared.write()
|
||||
} else {
|
||||
prepared.into_report()
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
if json {
|
||||
serde_json::to_writer_pretty(io::stdout().lock(), &report)?;
|
||||
writeln!(io::stdout())?;
|
||||
} else {
|
||||
human_report(&report, !displayed)?;
|
||||
}
|
||||
Ok(report.exit_code)
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match run() {
|
||||
Ok(code) => ExitCode::from(code),
|
||||
Err(error) => {
|
||||
let _ = writeln!(io::stderr(), "Output failed: {error}");
|
||||
ExitCode::from(3)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
use oa_cli::{LIMITS, Operation, Prepared, prepare};
|
||||
use oa_core::{Id, Project, source_files, source_project};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
struct Fixture(PathBuf);
|
||||
impl Fixture {
|
||||
fn new() -> Self {
|
||||
let path = std::env::temp_dir().join(format!("arcadia-cli-test-{}", Id::new_v4()));
|
||||
fs::create_dir(&path).unwrap();
|
||||
Self(path)
|
||||
}
|
||||
fn put(&self, name: &str, text: &str) {
|
||||
let path = self.0.join(name);
|
||||
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
fs::write(path, text).unwrap();
|
||||
}
|
||||
fn entry(&self) -> PathBuf {
|
||||
self.0.join("project.arcadia")
|
||||
}
|
||||
fn read(&self, name: &str) -> String {
|
||||
fs::read_to_string(self.0.join(name)).unwrap()
|
||||
}
|
||||
fn prepare(&self, op: Operation) -> Prepared {
|
||||
prepare(&self.entry(), op).unwrap_or_else(|report| panic!("{report:#?}"))
|
||||
}
|
||||
fn model(&self) -> Project {
|
||||
let disk = source_files::load_project(&self.entry(), LIMITS).unwrap();
|
||||
source_project::compile_bundle(&disk.reachable_bundle(), LIMITS)
|
||||
.unwrap()
|
||||
.decoded
|
||||
.project
|
||||
}
|
||||
fn cli(&self, args: &[&str]) -> (i32, Value) {
|
||||
let mut command = Command::new(env!("CARGO_BIN_EXE_arcadia"));
|
||||
command
|
||||
.arg(args[0])
|
||||
.arg(self.entry())
|
||||
.args(&args[1..])
|
||||
.arg("--json");
|
||||
let output = command.output().unwrap();
|
||||
assert!(
|
||||
output.stderr.is_empty(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
(
|
||||
output.status.code().unwrap(),
|
||||
serde_json::from_slice(&output.stdout).unwrap(),
|
||||
)
|
||||
}
|
||||
}
|
||||
impl Drop for Fixture {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
const SIMPLE: &str = "// 雨: authored defaults stay omitted\r\narcadia 7;\r\nproject rescue \"Rescue\" {\r\n oa_entity team \"Team\" {}\r\n}\r\n";
|
||||
|
||||
#[test]
|
||||
fn complete_alarm_example_preview_then_write_and_fixed_point() {
|
||||
let fixture = Fixture::new();
|
||||
let source = include_str!(
|
||||
"../../../harness/experiments/textual-authoring-20260911/alarm-response.arcadia"
|
||||
);
|
||||
fixture.put("project.arcadia", source);
|
||||
let (code, check) = fixture.cli(&["check"]);
|
||||
assert_eq!(code, 0);
|
||||
assert_eq!(check["diagnostics"], serde_json::json!([]));
|
||||
let (code, preview) = fixture.cli(&["stabilize"]);
|
||||
assert_eq!(code, 0);
|
||||
assert_eq!(preview["identities_to_stabilize"], 40);
|
||||
assert_eq!(preview["status"], "preview");
|
||||
assert_eq!(fixture.read("project.arcadia"), source);
|
||||
assert!(
|
||||
preview["changes"][0]["diff"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("+project alarm_response")
|
||||
);
|
||||
let (code, written) = fixture.cli(&["stabilize", "--write"]);
|
||||
assert_eq!(code, 0);
|
||||
let saved = fixture.read("project.arcadia");
|
||||
assert_eq!(written["changes"][0]["after"], saved);
|
||||
let project = fixture.model();
|
||||
assert_eq!(project.identities().len(), 40);
|
||||
let mut stripped = saved.clone();
|
||||
for (id, _) in project.identities() {
|
||||
stripped = stripped.replace(&format!(" id \"{id}\""), "");
|
||||
}
|
||||
assert_eq!(stripped, source, "only identity clauses may be inserted");
|
||||
let (code, again) = fixture.cli(&["stabilize", "--write"]);
|
||||
assert_eq!(code, 0);
|
||||
assert_eq!(again["changes"], serde_json::json!([]));
|
||||
assert_eq!(again["status"], "unchanged");
|
||||
assert_eq!(fixture.model(), project);
|
||||
assert_eq!(fixture.read("project.arcadia"), saved);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn captured_candidate_exact_write_preserves_crlf_comments_and_noop_metadata() {
|
||||
let fixture = Fixture::new();
|
||||
fixture.put("project.arcadia", SIMPLE);
|
||||
let candidate = fixture.prepare(Operation::Stabilize);
|
||||
let expected = candidate.report().changes[0].after.clone();
|
||||
assert_eq!(candidate.report().identities_to_stabilize, 2);
|
||||
assert_eq!(fixture.read("project.arcadia"), SIMPLE);
|
||||
assert_eq!(candidate.write().exit_code, 0);
|
||||
assert_eq!(fixture.read("project.arcadia"), expected);
|
||||
assert!(expected.contains("// 雨: authored defaults stay omitted\r\n"));
|
||||
assert!(!expected.contains("description"));
|
||||
let metadata = fs::metadata(fixture.entry()).unwrap().modified().unwrap();
|
||||
let report = fixture.prepare(Operation::Stabilize).write();
|
||||
assert_eq!(report.status, "unchanged");
|
||||
assert!(!report.written);
|
||||
assert_eq!(
|
||||
fs::metadata(fixture.entry()).unwrap().modified().unwrap(),
|
||||
metadata
|
||||
);
|
||||
assert_eq!(fs::read_dir(&fixture.0).unwrap().count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_rename_across_modules_changes_only_live_tokens_and_retains_all_identity() {
|
||||
let fixture = Fixture::new();
|
||||
fixture.put("project.arcadia", "arcadia 7; project rescue \"Rescue\" { include \"participants.arcadia\"; include \"work.arcadia\"; }\n");
|
||||
fixture.put(
|
||||
"participants.arcadia",
|
||||
"// @team is quoted documentation\noa_entity team \"Team @team\" {}\n",
|
||||
);
|
||||
fixture.put("work.arcadia", "oa_activity assess \"Assess\" {}\noa_activity report \"Report\" {}\nallocation assigned \"Assigned\" { source @team; target @assess; }\ninteraction transfer \"Transfer\" { source @assess; target @report; }\n");
|
||||
assert_eq!(fixture.prepare(Operation::Stabilize).write().exit_code, 0);
|
||||
let before = fixture.model();
|
||||
let original: BTreeMap<_, _> = ["project.arcadia", "participants.arcadia", "work.arcadia"]
|
||||
.into_iter()
|
||||
.map(|p| (p, fixture.read(p)))
|
||||
.collect();
|
||||
let team = before
|
||||
.identities()
|
||||
.into_iter()
|
||||
.find(|(_, alias)| *alias == "team")
|
||||
.unwrap()
|
||||
.0;
|
||||
let candidate = fixture.prepare(Operation::RenameAlias {
|
||||
old: "team".into(),
|
||||
new: "rescue_team".into(),
|
||||
});
|
||||
assert_eq!(candidate.report().changes.len(), 2);
|
||||
assert_eq!(fixture.model(), before, "preview cannot mutate");
|
||||
assert_eq!(candidate.write().exit_code, 0);
|
||||
let mut expected = before;
|
||||
expected.elements.get_mut(&team).unwrap().alias = "rescue_team".into();
|
||||
assert_eq!(fixture.model(), expected);
|
||||
assert_eq!(fixture.read("project.arcadia"), original["project.arcadia"]);
|
||||
assert_eq!(
|
||||
fixture.read("participants.arcadia"),
|
||||
original["participants.arcadia"].replacen("oa_entity team ", "oa_entity rescue_team ", 1)
|
||||
);
|
||||
assert_eq!(
|
||||
fixture.read("work.arcadia"),
|
||||
original["work.arcadia"].replace("source @team;", "source @rescue_team;")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_unicode_raw_edit_is_located_and_never_stabilized_or_written() {
|
||||
let fixture = Fixture::new();
|
||||
let raw = "arcadia 7; project p \"雨\" {\n oa_activity a \"A\" {}\n allocation alloc \"Alloc\" { source @missing; target @a; }\n}\n";
|
||||
fixture.put("project.arcadia", raw);
|
||||
for command in [
|
||||
vec!["check"],
|
||||
vec!["stabilize", "--write"],
|
||||
vec!["rename-alias", "a", "b", "--write"],
|
||||
] {
|
||||
let (code, report) = fixture.cli(&command);
|
||||
assert_eq!(code, 1);
|
||||
assert_eq!(fixture.read("project.arcadia"), raw);
|
||||
let diagnostics = report["diagnostics"].as_array().unwrap();
|
||||
assert!(!diagnostics.is_empty());
|
||||
for d in diagnostics {
|
||||
assert_eq!(d["coordinate_space"], "input");
|
||||
let at = &d["location"];
|
||||
let start = at["start"].as_u64().unwrap() as usize;
|
||||
assert!(raw.is_char_boundary(start));
|
||||
assert_eq!(
|
||||
at["line"].as_u64().unwrap() as usize,
|
||||
raw[..start].bytes().filter(|b| *b == b'\n').count() + 1
|
||||
);
|
||||
let line = raw[..start]
|
||||
.rsplit_once('\n')
|
||||
.map_or(&raw[..start], |(_, tail)| tail);
|
||||
assert_eq!(
|
||||
at["column"].as_u64().unwrap() as usize,
|
||||
line.chars().count() + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_id_warning_uses_raw_location_and_never_exposes_ephemeral_lint_id() {
|
||||
let fixture = Fixture::new();
|
||||
let raw = "arcadia 7;\nproject p \"雨\" {\n oa_activity work \"Work\" {}\n}\n";
|
||||
fixture.put("project.arcadia", raw);
|
||||
let (code, result) = fixture.cli(&["lint"]);
|
||||
assert_eq!(code, 0, "engineering warnings do not reject admission");
|
||||
let warning = result["diagnostics"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|d| d["code"] == "work_without_behavior_context")
|
||||
.unwrap();
|
||||
assert_eq!(warning["severity"], "warning");
|
||||
assert_eq!(
|
||||
warning["location"]["start"],
|
||||
raw.find("oa_activity").unwrap()
|
||||
);
|
||||
assert_eq!(warning["location"]["line"], 3);
|
||||
assert_eq!(warning["location"]["column"], 5);
|
||||
assert!(warning["object_id"].is_null());
|
||||
assert_eq!(fixture.read("project.arcadia"), raw);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn captured_dependency_change_refuses_all_writes_even_on_noop() {
|
||||
let fixture = Fixture::new();
|
||||
fixture.put("project.arcadia", SIMPLE);
|
||||
let pending = fixture.prepare(Operation::Stabilize);
|
||||
let external = format!("{SIMPLE}// editor changed this\n");
|
||||
fixture.put("project.arcadia", &external);
|
||||
let result = pending.write();
|
||||
assert_eq!(result.exit_code, 3);
|
||||
assert_eq!(result.diagnostics.last().unwrap().code, "external_change");
|
||||
assert_eq!(fixture.read("project.arcadia"), external);
|
||||
assert_eq!(fixture.prepare(Operation::Stabilize).write().exit_code, 0);
|
||||
let noop = fixture.prepare(Operation::Stabilize);
|
||||
assert!(noop.report().changes.is_empty());
|
||||
let latest = format!("{}// another edit\n", fixture.read("project.arcadia"));
|
||||
fixture.put("project.arcadia", &latest);
|
||||
assert_eq!(noop.write().exit_code, 3);
|
||||
assert_eq!(fixture.read("project.arcadia"), latest);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pending_journal_refuses_read_preview_and_write_without_recovery() {
|
||||
let fixture = Fixture::new();
|
||||
fixture.put("project.arcadia", SIMPLE);
|
||||
assert_eq!(fixture.prepare(Operation::Stabilize).write().exit_code, 0);
|
||||
let noop = fixture.prepare(Operation::Stabilize);
|
||||
let source = fixture.read("project.arcadia");
|
||||
// An opaque incomplete journal must never be interpreted/recovered by CLI.
|
||||
let journal = fixture.0.join(".arcadia-save-project.arcadia.journal");
|
||||
fs::create_dir(&journal).unwrap();
|
||||
fs::write(journal.join("sentinel"), b"retain").unwrap();
|
||||
for op in [Operation::Check, Operation::Stabilize] {
|
||||
let report = prepare(&fixture.entry(), op).err().unwrap();
|
||||
assert_eq!(report.exit_code, 3);
|
||||
assert!(report.diagnostics.iter().any(|d| d.code == "pending_save"));
|
||||
}
|
||||
assert_eq!(
|
||||
noop.write().exit_code,
|
||||
3,
|
||||
"saver must recheck after preview"
|
||||
);
|
||||
assert_eq!(fixture.read("project.arcadia"), source);
|
||||
assert_eq!(fs::read(journal.join("sentinel")).unwrap(), b"retain");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejected_alias_and_same_alias_preserve_source() {
|
||||
let fixture = Fixture::new();
|
||||
fixture.put("project.arcadia", SIMPLE);
|
||||
assert_eq!(fixture.prepare(Operation::Stabilize).write().exit_code, 0);
|
||||
let source = fixture.read("project.arcadia");
|
||||
for (old, new) in [
|
||||
("team", "rescue"),
|
||||
("team", "invalid alias"),
|
||||
("absent", "valid"),
|
||||
] {
|
||||
let (code, result) = fixture.cli(&["rename-alias", old, new, "--write"]);
|
||||
assert_eq!(code, 1);
|
||||
assert_eq!(result["status"], "rejected");
|
||||
assert_eq!(fixture.read("project.arcadia"), source);
|
||||
}
|
||||
let (code, result) = fixture.cli(&["rename-alias", "team", "team", "--write"]);
|
||||
assert_eq!(code, 0);
|
||||
assert_eq!(result["status"], "unchanged");
|
||||
assert_eq!(fixture.read("project.arcadia"), source);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_shows_missing_identity_and_separate_source_migration() {
|
||||
let fixture = Fixture::new();
|
||||
let old = "// retain\narcadia 5; project p \"P\" { oa_entity team \"Team\" {} }\n";
|
||||
fixture.put("project.arcadia", old);
|
||||
let candidate = fixture.prepare(Operation::RenameAlias {
|
||||
old: "team".into(),
|
||||
new: "crew".into(),
|
||||
});
|
||||
let report = candidate.report();
|
||||
assert_eq!(report.identities_to_stabilize, 2);
|
||||
assert_eq!(report.migration.as_ref().unwrap().from_schema, 5);
|
||||
assert_eq!(report.migration.as_ref().unwrap().to_schema, 7);
|
||||
assert!(report.changes[0].diff.contains("-arcadia 5;"));
|
||||
assert!(report.changes[0].diff.contains("+arcadia 7;"));
|
||||
assert!(report.changes[0].after.contains("oa_entity crew"));
|
||||
assert_eq!(fixture.read("project.arcadia"), old);
|
||||
assert_eq!(candidate.write().exit_code, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_is_explicit_and_machine_readable_usage_errors_do_not_write() {
|
||||
let fixture = Fixture::new();
|
||||
fixture.put("project.arcadia", SIMPLE);
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_arcadia"))
|
||||
.current_dir(&fixture.0)
|
||||
.args(["check", "--json"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(output.status.code(), Some(2));
|
||||
let error: Value = serde_json::from_slice(&output.stdout).unwrap();
|
||||
assert_eq!(error["diagnostics"][0]["code"], "usage");
|
||||
assert_eq!(fixture.read("project.arcadia"), SIMPLE);
|
||||
let (code, _) = fixture.cli(&["check", "--write"]);
|
||||
assert_eq!(code, 2);
|
||||
assert!(Path::new(env!("CARGO_BIN_EXE_arcadia")).is_file());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loader_encoding_missing_dependency_and_invalid_old_source_keep_original_bytes() {
|
||||
let fixture = Fixture::new();
|
||||
let entry = "arcadia 7; project p \"P\" { include \"part.arcadia\"; }";
|
||||
fixture.put("project.arcadia", entry);
|
||||
let (code, report) = fixture.cli(&["stabilize", "--write"]);
|
||||
assert_eq!(code, 3, "a disk dependency could not be read");
|
||||
assert!(
|
||||
report["diagnostics"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|d| d["code"] == "missing_include")
|
||||
);
|
||||
fs::write(fixture.0.join("part.arcadia"), [0xff, 0xfe]).unwrap();
|
||||
let (code, report) = fixture.cli(&["stabilize", "--write"]);
|
||||
assert_eq!(code, 1, "invalid encoding is invalid raw source");
|
||||
assert!(
|
||||
report["diagnostics"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|d| d["code"] == "source_encoding")
|
||||
);
|
||||
assert_eq!(fixture.read("project.arcadia"), entry);
|
||||
assert_eq!(
|
||||
fs::read(fixture.0.join("part.arcadia")).unwrap(),
|
||||
[0xff, 0xfe]
|
||||
);
|
||||
for raw in [
|
||||
"arcadia 5; project p \"P\" { oa_entity a \"A\" { ??? } }",
|
||||
"arcadia 2; project p \"P\" {}",
|
||||
] {
|
||||
fixture.put("project.arcadia", raw);
|
||||
let (code, _) = fixture.cli(&["stabilize", "--write"]);
|
||||
assert_eq!(code, 1);
|
||||
assert_eq!(fixture.read("project.arcadia"), raw);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_element_alias_detached_file_and_unchanged_dependency_conflict() {
|
||||
let fixture = Fixture::new();
|
||||
fixture.put("project.arcadia", "arcadia 7; project p \"P\" { include \"part.arcadia\"; oa_activity a \"A\" {} allocation responsible \"Responsible\" { source @ // keep edge comment\nteam; target @a; } }");
|
||||
fixture.put("part.arcadia", "oa_entity team \"Team\" {}");
|
||||
fixture.put(
|
||||
"detached.arcadia",
|
||||
"invalid detached draft must stay exact 雨",
|
||||
);
|
||||
assert_eq!(fixture.prepare(Operation::Stabilize).write().exit_code, 0);
|
||||
let before = fixture.model();
|
||||
let relation = before
|
||||
.identities()
|
||||
.into_iter()
|
||||
.find(|(_, alias)| *alias == "responsible")
|
||||
.unwrap()
|
||||
.0;
|
||||
let (code, _) = fixture.cli(&["rename-alias", "responsible", "performs", "--write"]);
|
||||
assert_eq!(code, 0);
|
||||
let mut expected = before;
|
||||
expected.relations.get_mut(&relation).unwrap().alias = "performs".into();
|
||||
assert_eq!(fixture.model(), expected);
|
||||
assert!(
|
||||
fixture
|
||||
.read("project.arcadia")
|
||||
.contains("@ // keep edge comment\nteam")
|
||||
);
|
||||
let captured = fixture.prepare(Operation::RenameAlias {
|
||||
old: "p".into(),
|
||||
new: "rescue".into(),
|
||||
});
|
||||
assert!(
|
||||
captured
|
||||
.report()
|
||||
.changes
|
||||
.iter()
|
||||
.all(|c| c.path != "part.arcadia")
|
||||
);
|
||||
let root_before = fixture.read("project.arcadia");
|
||||
fixture.put("part.arcadia", "// external unchanged-dependency edit\n");
|
||||
assert_eq!(captured.write().exit_code, 3);
|
||||
assert_eq!(fixture.read("project.arcadia"), root_before);
|
||||
assert_eq!(
|
||||
fixture.read("part.arcadia"),
|
||||
"// external unchanged-dependency edit\n"
|
||||
);
|
||||
assert_eq!(
|
||||
fixture.read("detached.arcadia"),
|
||||
"invalid detached draft must stay exact 雨"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
# Source CLI
|
||||
|
||||
Use `arcadia` to check or update local `.arcadia` projects without opening Studio
|
||||
or starting a server. Pass the entry file explicitly; its relative include paths
|
||||
resolve from their containing source files.
|
||||
|
||||
Build with `cargo build --locked -p oa-cli --bin arcadia`. The resulting command
|
||||
is `target/debug/arcadia.exe` on Windows or `target/debug/arcadia` on Linux unless
|
||||
your Cargo configuration chooses another target directory. A workspace release
|
||||
build also includes it. Use the platform's usual executable path prefix when
|
||||
running these examples.
|
||||
|
||||
```sh
|
||||
arcadia check project.arcadia
|
||||
arcadia check project.arcadia --json
|
||||
arcadia stabilize project.arcadia
|
||||
arcadia stabilize project.arcadia --write
|
||||
arcadia rename-alias project.arcadia old_alias new_alias
|
||||
arcadia rename-alias project.arcadia old_alias new_alias --write --json
|
||||
```
|
||||
|
||||
`lint` is an alias for `check`. Checks accept incomplete engineering models with
|
||||
warnings; syntax errors and structural violations fail. Located diagnostics
|
||||
refer to the named file and coordinate space. Line and column numbers are
|
||||
one-based and columns count Unicode scalar values; byte ranges are half-open
|
||||
UTF-8 offsets.
|
||||
|
||||
`stabilize` previews missing persistent identities and any supported source
|
||||
migration. `rename-alias` changes one existing global alias and its typed
|
||||
references while preserving comments, strings and unrelated source. Pass alias
|
||||
names without `@`. The command does not rename an element's display name.
|
||||
|
||||
Both editing commands preview by default. Human output writes unified file
|
||||
diffs to stdout and diagnostics to stderr. `--json` writes one versioned report
|
||||
with diagnostics, migration information, exact before/after text for every
|
||||
changed file, diffs and the write result. Unchanged files are not included in
|
||||
that report. They are still checked for conflicts before a write.
|
||||
|
||||
Add `--write` to save the candidate prepared by that invocation. Even an empty
|
||||
diff checks the captured files and pending-save state before returning. A failed
|
||||
write reports `written: false` and retains its proposed diff. This does not
|
||||
guarantee rollback; the reported conflict or recovery outcome determines the
|
||||
next step. A pending save
|
||||
requires explicit recovery through the existing project recovery workflow;
|
||||
the CLI does not recover it automatically.
|
||||
|
||||
Missing IDs in a separate earlier preview remain provisional. A later command
|
||||
may allocate different IDs until you save them. This CLI does not persist a
|
||||
review plan for later application. It does not canonicalize the whole project
|
||||
or delete detached files. Source versions 3–7 use the core's supported migration
|
||||
rules; invalid source is not rewritten.
|
||||
|
||||
| Exit code | Meaning |
|
||||
| --- | --- |
|
||||
| 0 | Valid check or successful preview/write; warnings may remain |
|
||||
| 1 | Invalid source or refused semantic alias change |
|
||||
| 2 | Invalid command arguments |
|
||||
| 3 | File/path, conflict, pending-save or output failure |
|
||||
|
||||
Admission bounds match the desktop: 256 files, include depth 32, 1024 include
|
||||
edges, 64 MiB total UTF-8, 100,000 graph objects, 250,000 records and 500,000
|
||||
ordered references. `arcadia --help` lists the commands and bounds. These limits
|
||||
are not a claim of interactive performance at the maximum project size.
|
||||
|
||||
For the notation itself, start with [the text-first examples](../examples/text-first/README.md)
|
||||
and the [language contract](language.rst).
|
||||
@@ -0,0 +1,112 @@
|
||||
arcadia 7;
|
||||
project alarm_suite "Alarm response" id "a0f5c4c2-f39c-4095-9816-71f1862e24d8" {
|
||||
description "A complete small OA/SA source-authoring example. Numerical annotations are descriptive. Literal aliases: @alarm_response; @maintenance; @alarm_message; @response_view; @detector_card; @response_procedure; @assessment_refinement;";
|
||||
|
||||
oa_actor plant_operator "Plant operator" id "60539571-2921-4e7d-ab83-bae670d0cb15" {}
|
||||
oa_entity maintenance_staff "Maintenance team" id "6af96577-1b04-4ba1-b91b-6cef61adc6d6" {}
|
||||
oa_activity manage_alarm "Manage a pressure alarm" id "4154bf39-9fd5-490e-aebf-fba3d67b697c" {
|
||||
oa_activity detect_alarm "Detect alarm" id "bf9e9d8a-edb2-47f4-93f9-7ae373c42160" {}
|
||||
// Assessing the alarm is work decomposition, independently allocated below.
|
||||
oa_activity assess_alarm "Assess alarm" id "8cbd5a4f-1c31-49c9-8a01-987144ce962e" {}
|
||||
}
|
||||
oa_capability safe_response "Respond safely" id "c097b1b5-45dc-49de-bcf6-89b5ccb752d1" {}
|
||||
oa_exchange_item alarm_report "Alarm report" id "0a48a460-964a-4578-97c4-428707e8b201" {
|
||||
mechanism Flow;
|
||||
oa_data_field reported_pressure "Reported pressure" id "0bf7f442-cc8d-4782-ba65-f357e91fd477" {
|
||||
type_ref Real;
|
||||
unit "kPa";
|
||||
}
|
||||
}
|
||||
allocation operator_detects "Operator detects" id "8653fb7d-529d-495d-abde-897dee36b4bb" {
|
||||
source @plant_operator; target @detect_alarm;
|
||||
}
|
||||
allocation team_assesses "Team assesses" id "bb36afbf-9878-401b-b172-d04b08ecf603" {
|
||||
source @maintenance_staff; target @assess_alarm;
|
||||
}
|
||||
interaction alarm_payload "Pressure alarm report" id "269e5bdd-2478-4a43-9c77-84d99ac4e8a6" {
|
||||
source @detect_alarm; target @assess_alarm; items [@alarm_report];
|
||||
}
|
||||
capability_involvement assessment_supports_response "Assessment supports safe response" id "27633a6d-d962-4d19-9367-9ca3f00de0b0" {
|
||||
source @assess_alarm; target @safe_response;
|
||||
}
|
||||
|
||||
oa_process triage "Alarm triage" id "9888d781-09f3-4861-b9dc-7a97080d24d6" {
|
||||
steps [@detect_once, @assess_once];
|
||||
oa_occurrence detect_once "Detect this alarm" id "6ca87cbe-9579-49b3-884e-e6ee63ba8c5b" {
|
||||
body { Action { action @detect_alarm; lifeline none; }; };
|
||||
}
|
||||
oa_occurrence assess_once "Assess this alarm" id "528d1c78-62d9-4e45-bb4a-cf529d8ed4a9" {
|
||||
body { Action { action @assess_alarm; lifeline none; }; };
|
||||
}
|
||||
}
|
||||
sequence_link assessment_after_detection "Assess after detecting" id "bfd9c0e7-832b-4983-90aa-fc37d23193e2" {
|
||||
source @detect_once; target @assess_once;
|
||||
}
|
||||
chain_involvement triage_supports_response "Triage supports safe response" id "2aeba184-bf69-49c7-87ff-cc2da82b006c" {
|
||||
source @triage; target @safe_response;
|
||||
}
|
||||
|
||||
oa_scenario notification "Alarm notification" id "69e23f04-7bb3-4db1-a2a3-d8e65101c546" {
|
||||
viewpoint OperationalActivity;
|
||||
context @safe_response;
|
||||
steps [@notify_once];
|
||||
oa_lifeline detector_line "Detector" id "57c62024-6d19-4818-ac8c-66fb4a9b3da7" { represented @detect_alarm; }
|
||||
oa_lifeline assessor_line "Assessor" id "1b421446-9d17-4c8b-90f1-6fbe8b513a04" { represented @assess_alarm; }
|
||||
oa_occurrence notify_once "Report this pressure" id "a1c37b31-41ed-4e16-a243-a50702ff1bc1" {
|
||||
body { Exchange {
|
||||
exchange @alarm_payload; sender @detector_line; receiver @assessor_line;
|
||||
message_kind Asynchronous; reply_to none;
|
||||
values [{ field @reported_pressure; path []; expression "420"; }];
|
||||
}; };
|
||||
}
|
||||
}
|
||||
|
||||
sa_system alarm_console "Alarm console" id "9ea585b5-be12-4204-8909-02ea9bed49f0" {
|
||||
sa_state_machine console_status "Console status" id "a6c6f463-5c80-4b02-a2e6-67cd0088b2f2" {
|
||||
sa_region console_region "Main region" id "e8b62edc-2233-4ec4-a811-2e8baf81a6e9" {
|
||||
sa_initial console_start "Start" id "5a92c3a8-6af5-452a-849e-c2a17dc33260" {}
|
||||
sa_state console_ready "Ready" id "9064a197-96ea-4bdf-9dd6-2e9ef66856d8" {}
|
||||
sa_state console_alerting "Alerting" id "4d167878-2bbe-4acd-abd6-ee7dcd42ec50" {}
|
||||
}
|
||||
}
|
||||
}
|
||||
sa_function classify_alarm "Classify alarm" id "ad6941ee-f900-4a2b-a368-d8879f1ffece" {}
|
||||
sa_functional_chain classification_flow "Alarm classification" id "578dbcd5-3c28-43d8-913e-af27bc51ebde" {
|
||||
steps [@classify_once];
|
||||
sa_occurrence classify_once "Classify this alarm" id "a6c37863-8e72-44f9-a8a3-fea06341c714" {
|
||||
body { Action { action @classify_alarm; lifeline none; }; };
|
||||
}
|
||||
}
|
||||
allocation console_classifies "Console classifies" id "294d4262-303f-41db-b01e-fe47c1647693" {
|
||||
source @alarm_console; target @classify_alarm;
|
||||
}
|
||||
sa_event alarm_received "Alarm received" id "7fba860a-721f-4da2-8315-010de2e9c69e" {}
|
||||
state_transition initialize_console "Initialize" id "839d4359-c78f-414b-867e-5b40b1f145cc" {
|
||||
source @console_start; target @console_ready;
|
||||
}
|
||||
state_transition display_alarm "Display alarm" id "dd383165-1b89-46b9-8ae3-80a6b6406a4e" {
|
||||
source @console_ready; target @console_alerting; triggers [@alarm_received];
|
||||
}
|
||||
trace assessment_alignment "Alarm classification refines assessment" id "34142333-67c2-4972-a64f-47e140ed93ca" {
|
||||
oa @assess_alarm; sa @classify_alarm;
|
||||
baseline {
|
||||
oa { name "Assess alarm"; description ""; };
|
||||
sa { name "Classify alarm"; description ""; };
|
||||
};
|
||||
}
|
||||
artifact response_handbook "Response procedure" id "6ba54037-c6f1-495b-ba96-6f2e910a218c" {
|
||||
owner @assess_alarm; uri "procedures/alarm-response.md"; revision "review-1";
|
||||
}
|
||||
|
||||
diagram response_map "Operational work" id "a44d9d69-78a0-49de-8eb8-3757aa6c0919" { projection FunctionalDataflow; }
|
||||
node detector_tile id "45f016f4-3558-45b9-9559-da8ef9d5dcad" {
|
||||
diagram @response_map; element @detect_alarm;
|
||||
position { x 40; y 40; };
|
||||
}
|
||||
node assessor_card id "6ee7a0cc-013b-4a0f-8cff-513872e028b7" {
|
||||
diagram @response_map; element @assess_alarm;
|
||||
position { x 400; y 40; };
|
||||
}
|
||||
}
|
||||
|
||||
// Retain these comment references: @alarm_response; @maintenance; @alarm_message; @response_view; @detector_card; @response_procedure; @assessment_refinement;
|
||||
@@ -0,0 +1,112 @@
|
||||
arcadia 7;
|
||||
project alarm_response "Alarm response" id "a0f5c4c2-f39c-4095-9816-71f1862e24d8" {
|
||||
description "A complete small OA/SA source-authoring example. Numerical annotations are descriptive. Literal aliases: @alarm_response; @maintenance; @alarm_message; @response_view; @detector_card; @response_procedure; @assessment_refinement;";
|
||||
|
||||
oa_actor plant_operator "Plant operator" id "60539571-2921-4e7d-ab83-bae670d0cb15" {}
|
||||
oa_entity maintenance "Maintenance team" id "6af96577-1b04-4ba1-b91b-6cef61adc6d6" {}
|
||||
oa_activity manage_alarm "Manage a pressure alarm" id "4154bf39-9fd5-490e-aebf-fba3d67b697c" {
|
||||
oa_activity detect_alarm "Detect alarm" id "bf9e9d8a-edb2-47f4-93f9-7ae373c42160" {}
|
||||
// Assessing the alarm is work decomposition, independently allocated below.
|
||||
oa_activity assess_alarm "Assess alarm" id "8cbd5a4f-1c31-49c9-8a01-987144ce962e" {}
|
||||
}
|
||||
oa_capability safe_response "Respond safely" id "c097b1b5-45dc-49de-bcf6-89b5ccb752d1" {}
|
||||
oa_exchange_item alarm_report "Alarm report" id "0a48a460-964a-4578-97c4-428707e8b201" {
|
||||
mechanism Flow;
|
||||
oa_data_field reported_pressure "Reported pressure" id "0bf7f442-cc8d-4782-ba65-f357e91fd477" {
|
||||
type_ref Real;
|
||||
unit "kPa";
|
||||
}
|
||||
}
|
||||
allocation operator_detects "Operator detects" id "8653fb7d-529d-495d-abde-897dee36b4bb" {
|
||||
source @plant_operator; target @detect_alarm;
|
||||
}
|
||||
allocation team_assesses "Team assesses" id "bb36afbf-9878-401b-b172-d04b08ecf603" {
|
||||
source @maintenance; target @assess_alarm;
|
||||
}
|
||||
interaction alarm_message "Pressure alarm report" id "269e5bdd-2478-4a43-9c77-84d99ac4e8a6" {
|
||||
source @detect_alarm; target @assess_alarm; items [@alarm_report];
|
||||
}
|
||||
capability_involvement assessment_supports_response "Assessment supports safe response" id "27633a6d-d962-4d19-9367-9ca3f00de0b0" {
|
||||
source @assess_alarm; target @safe_response;
|
||||
}
|
||||
|
||||
oa_process triage "Alarm triage" id "9888d781-09f3-4861-b9dc-7a97080d24d6" {
|
||||
steps [@detect_once, @assess_once];
|
||||
oa_occurrence detect_once "Detect this alarm" id "6ca87cbe-9579-49b3-884e-e6ee63ba8c5b" {
|
||||
body { Action { action @detect_alarm; lifeline none; }; };
|
||||
}
|
||||
oa_occurrence assess_once "Assess this alarm" id "528d1c78-62d9-4e45-bb4a-cf529d8ed4a9" {
|
||||
body { Action { action @assess_alarm; lifeline none; }; };
|
||||
}
|
||||
}
|
||||
sequence_link assessment_after_detection "Assess after detecting" id "bfd9c0e7-832b-4983-90aa-fc37d23193e2" {
|
||||
source @detect_once; target @assess_once;
|
||||
}
|
||||
chain_involvement triage_supports_response "Triage supports safe response" id "2aeba184-bf69-49c7-87ff-cc2da82b006c" {
|
||||
source @triage; target @safe_response;
|
||||
}
|
||||
|
||||
oa_scenario notification "Alarm notification" id "69e23f04-7bb3-4db1-a2a3-d8e65101c546" {
|
||||
viewpoint OperationalActivity;
|
||||
context @safe_response;
|
||||
steps [@notify_once];
|
||||
oa_lifeline detector_line "Detector" id "57c62024-6d19-4818-ac8c-66fb4a9b3da7" { represented @detect_alarm; }
|
||||
oa_lifeline assessor_line "Assessor" id "1b421446-9d17-4c8b-90f1-6fbe8b513a04" { represented @assess_alarm; }
|
||||
oa_occurrence notify_once "Report this pressure" id "a1c37b31-41ed-4e16-a243-a50702ff1bc1" {
|
||||
body { Exchange {
|
||||
exchange @alarm_message; sender @detector_line; receiver @assessor_line;
|
||||
message_kind Asynchronous; reply_to none;
|
||||
values [{ field @reported_pressure; path []; expression "420"; }];
|
||||
}; };
|
||||
}
|
||||
}
|
||||
|
||||
sa_system alarm_console "Alarm console" id "9ea585b5-be12-4204-8909-02ea9bed49f0" {
|
||||
sa_state_machine console_status "Console status" id "a6c6f463-5c80-4b02-a2e6-67cd0088b2f2" {
|
||||
sa_region console_region "Main region" id "e8b62edc-2233-4ec4-a811-2e8baf81a6e9" {
|
||||
sa_initial console_start "Start" id "5a92c3a8-6af5-452a-849e-c2a17dc33260" {}
|
||||
sa_state console_ready "Ready" id "9064a197-96ea-4bdf-9dd6-2e9ef66856d8" {}
|
||||
sa_state console_alerting "Alerting" id "4d167878-2bbe-4acd-abd6-ee7dcd42ec50" {}
|
||||
}
|
||||
}
|
||||
}
|
||||
sa_function classify_alarm "Classify alarm" id "ad6941ee-f900-4a2b-a368-d8879f1ffece" {}
|
||||
sa_functional_chain classification_flow "Alarm classification" id "578dbcd5-3c28-43d8-913e-af27bc51ebde" {
|
||||
steps [@classify_once];
|
||||
sa_occurrence classify_once "Classify this alarm" id "a6c37863-8e72-44f9-a8a3-fea06341c714" {
|
||||
body { Action { action @classify_alarm; lifeline none; }; };
|
||||
}
|
||||
}
|
||||
allocation console_classifies "Console classifies" id "294d4262-303f-41db-b01e-fe47c1647693" {
|
||||
source @alarm_console; target @classify_alarm;
|
||||
}
|
||||
sa_event alarm_received "Alarm received" id "7fba860a-721f-4da2-8315-010de2e9c69e" {}
|
||||
state_transition initialize_console "Initialize" id "839d4359-c78f-414b-867e-5b40b1f145cc" {
|
||||
source @console_start; target @console_ready;
|
||||
}
|
||||
state_transition display_alarm "Display alarm" id "dd383165-1b89-46b9-8ae3-80a6b6406a4e" {
|
||||
source @console_ready; target @console_alerting; triggers [@alarm_received];
|
||||
}
|
||||
trace assessment_refinement "Alarm classification refines assessment" id "34142333-67c2-4972-a64f-47e140ed93ca" {
|
||||
oa @assess_alarm; sa @classify_alarm;
|
||||
baseline {
|
||||
oa { name "Assess alarm"; description ""; };
|
||||
sa { name "Classify alarm"; description ""; };
|
||||
};
|
||||
}
|
||||
artifact response_procedure "Response procedure" id "6ba54037-c6f1-495b-ba96-6f2e910a218c" {
|
||||
owner @assess_alarm; uri "procedures/alarm-response.md"; revision "review-1";
|
||||
}
|
||||
|
||||
diagram response_view "Operational work" id "a44d9d69-78a0-49de-8eb8-3757aa6c0919" { projection FunctionalDataflow; }
|
||||
node detector_card id "45f016f4-3558-45b9-9559-da8ef9d5dcad" {
|
||||
diagram @response_view; element @detect_alarm;
|
||||
position { x 40; y 40; };
|
||||
}
|
||||
node assessor_card id "6ee7a0cc-013b-4a0f-8cff-513872e028b7" {
|
||||
diagram @response_view; element @assess_alarm;
|
||||
position { x 400; y 40; };
|
||||
}
|
||||
}
|
||||
|
||||
// Retain these comment references: @alarm_response; @maintenance; @alarm_message; @response_view; @detector_card; @response_procedure; @assessment_refinement;
|
||||
@@ -0,0 +1,200 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Repository,
|
||||
[ValidatePattern('^[a-z0-9-]+$')][string]$RunName = 'verified'
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
$repoRoot = [IO.Path]::GetFullPath($Repository)
|
||||
$utf8 = [Text.UTF8Encoding]::new($false, $true)
|
||||
$exe = Join-Path $repoRoot '.local/source-cli-20260911/verified/arcadia.exe'
|
||||
$expectedExeHash = 'BAD169F8BE78DCFA74B32714569C886DD2132727080684FC2BA618D5822E3559'
|
||||
$runRoot = Join-Path $repoRoot ('.local/source-cli-independent-20260911/' + $RunName)
|
||||
if (Test-Path -LiteralPath $runRoot) { throw "Refusing to replace prior evidence: $runRoot" }
|
||||
$checks = [Collections.Generic.List[string]]::new()
|
||||
$operations = [Collections.Generic.List[object]]::new()
|
||||
function Assert-That([bool]$condition, [string]$label) {
|
||||
if (!$condition) { throw "Independent contract failed: $label" }
|
||||
$checks.Add($label)
|
||||
}
|
||||
function Put([string]$path, [string]$value) {
|
||||
[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($path)) | Out-Null
|
||||
[IO.File]::WriteAllText($path, $value, $utf8)
|
||||
}
|
||||
function Read-Exact([string]$path) { [IO.File]::ReadAllText($path, $utf8) }
|
||||
function Text-Hash([string]$text) {
|
||||
[Convert]::ToHexString([Security.Cryptography.SHA256]::HashData($utf8.GetBytes($text)))
|
||||
}
|
||||
function Invoke-ArcadiaProbe([string[]]$words) {
|
||||
$info = [Diagnostics.ProcessStartInfo]::new($exe)
|
||||
$info.UseShellExecute = $false
|
||||
$info.CreateNoWindow = $true
|
||||
$info.RedirectStandardOutput = $true
|
||||
$info.RedirectStandardError = $true
|
||||
$info.StandardOutputEncoding = $utf8
|
||||
$info.StandardErrorEncoding = $utf8
|
||||
foreach ($word in $words) { $info.ArgumentList.Add($word) }
|
||||
$info.ArgumentList.Add('--json')
|
||||
$process = [Diagnostics.Process]::new()
|
||||
$process.StartInfo = $info
|
||||
$process.Start() | Out-Null
|
||||
$output = $process.StandardOutput.ReadToEndAsync()
|
||||
$errorText = $process.StandardError.ReadToEndAsync()
|
||||
if (!$process.WaitForExit(60000)) {
|
||||
$process.Kill($true)
|
||||
throw 'CLI probe exceeded 60 seconds'
|
||||
}
|
||||
$raw = $output.GetAwaiter().GetResult()
|
||||
$err = $errorText.GetAwaiter().GetResult()
|
||||
$report = $raw | ConvertFrom-Json -Depth 100
|
||||
Assert-That ($process.ExitCode -eq $report.exit_code) 'process exit agrees with JSON'
|
||||
Assert-That ($err -ceq '') 'JSON invocation emits no mixed stderr'
|
||||
$operations.Add([ordered]@{
|
||||
Arguments = @($words | ForEach-Object { $_.Replace($runRoot, '<fixture>') })
|
||||
Exit = $process.ExitCode
|
||||
Status = $report.status
|
||||
Changes = @($report.changes).Count
|
||||
Diagnostics = @($report.diagnostics).Count
|
||||
StdoutSHA256 = Text-Hash $raw
|
||||
})
|
||||
$process.Dispose()
|
||||
return $report
|
||||
}
|
||||
Assert-That ((Get-FileHash -LiteralPath $exe).Hash -ceq $expectedExeHash) 'immutable CLI binary pin'
|
||||
[IO.Directory]::CreateDirectory($runRoot) | Out-Null
|
||||
|
||||
# Predict every changed header/reference before the first mutating invocation.
|
||||
# This fixture has explicit UUIDs for all seven Project::identities families.
|
||||
$sourcePath = Join-Path $repoRoot 'harness/experiments/textual-authoring-20260911/observed/stabilized.arcadia'
|
||||
$source = Read-Exact $sourcePath
|
||||
$sourceHash = Text-Hash $source
|
||||
$aliases = @(
|
||||
[pscustomobject]@{ Family = 'project'; Kind = 'project'; Old = 'alarm_response'; New = 'alarm_suite'; References = 0 },
|
||||
[pscustomobject]@{ Family = 'element'; Kind = 'oa_entity'; Old = 'maintenance'; New = 'maintenance_staff'; References = 1 },
|
||||
[pscustomobject]@{ Family = 'relation'; Kind = 'interaction'; Old = 'alarm_message'; New = 'alarm_payload'; References = 1 },
|
||||
[pscustomobject]@{ Family = 'diagram'; Kind = 'diagram'; Old = 'response_view'; New = 'response_map'; References = 2 },
|
||||
[pscustomobject]@{ Family = 'placement'; Kind = 'node'; Old = 'detector_card'; New = 'detector_tile'; References = 0 },
|
||||
[pscustomobject]@{ Family = 'artifact'; Kind = 'artifact'; Old = 'response_procedure'; New = 'response_handbook'; References = 0 },
|
||||
[pscustomobject]@{ Family = 'trace'; Kind = 'trace'; Old = 'assessment_refinement'; New = 'assessment_alignment'; References = 0 }
|
||||
)
|
||||
$literal = ($aliases | ForEach-Object { '@' + $_.Old + ';' }) -join ' '
|
||||
$source = $source.Replace('Numerical annotations are descriptive.', 'Numerical annotations are descriptive. Literal aliases: ' + $literal)
|
||||
$source += "`r`n// Retain these comment references: $literal`r`n"
|
||||
$expected = $source
|
||||
$predictions = [Collections.Generic.List[object]]::new()
|
||||
foreach ($case in $aliases) {
|
||||
$header = $case.Kind + ' ' + $case.Old + ' '
|
||||
Assert-That (([regex]::Matches($expected, [regex]::Escape($header))).Count -eq 1) ($case.Family + ' has one literal declaration header')
|
||||
$next = $expected.Replace($header, $case.Kind + ' ' + $case.New + ' ')
|
||||
# The known live references occupy these exact fields; prose/comments are excluded.
|
||||
$field = switch ($case.Family) { 'element' { 'source' }; 'relation' { 'exchange' }; 'diagram' { 'diagram' }; default { $null } }
|
||||
if ($null -ne $field) {
|
||||
$oldReference = $field + ' @' + $case.Old + ';'
|
||||
Assert-That (([regex]::Matches($next, [regex]::Escape($oldReference))).Count -eq $case.References) ($case.Family + ' live-reference count')
|
||||
$next = $next.Replace($oldReference, $field + ' @' + $case.New + ';')
|
||||
}
|
||||
$predictions.Add([pscustomobject]@{ Case = $case; Before = $expected; After = $next })
|
||||
$expected = $next
|
||||
}
|
||||
$aliasEntry = Join-Path $runRoot 'aliases/project.arcadia'
|
||||
Put $aliasEntry $source
|
||||
Put (Join-Path $PSScriptRoot 'alias-input.arcadia') $source
|
||||
Put (Join-Path $PSScriptRoot 'alias-expected-final.arcadia') $expected
|
||||
$baseline = Invoke-ArcadiaProbe @('check', $aliasEntry)
|
||||
Assert-That ($baseline.exit_code -eq 0) 'all-family baseline is admitted before mutation'
|
||||
Assert-That ($baseline.identities_to_stabilize -eq 0) 'all-family baseline has no missing identities'
|
||||
Assert-That (([regex]::Matches($source, ' id "[0-9a-f-]{36}"')).Count -eq 40) 'all-family baseline contains exactly 40 authored UUIDs'
|
||||
foreach ($prediction in $predictions) {
|
||||
$case = $prediction.Case
|
||||
$preview = Invoke-ArcadiaProbe @('rename-alias', $aliasEntry, $case.Old, $case.New)
|
||||
Assert-That ($preview.exit_code -eq 0 -and !$preview.written -and $preview.status -ceq 'preview') ($case.Family + ' preview succeeds without write')
|
||||
Assert-That ((Read-Exact $aliasEntry) -ceq $prediction.Before) ($case.Family + ' preview preserves exact input bytes')
|
||||
Assert-That (@($preview.changes).Count -eq 1) ($case.Family + ' reports exactly one changed file')
|
||||
Assert-That ($preview.changes[0].path -ceq 'project.arcadia') ($case.Family + ' preserves file ownership')
|
||||
Assert-That ($preview.changes[0].before -ceq $prediction.Before) ($case.Family + ' reports exact captured text')
|
||||
Assert-That ($preview.changes[0].after -ceq $prediction.After) ($case.Family + ' changes only independently predicted header/live-reference tokens')
|
||||
Assert-That ($preview.identities_to_stabilize -eq 0 -and $null -eq $preview.migration) ($case.Family + ' invents no identity or migration')
|
||||
$written = Invoke-ArcadiaProbe @('rename-alias', $aliasEntry, $case.Old, $case.New, '--write')
|
||||
Assert-That ($written.exit_code -eq 0 -and $written.written -and $written.status -ceq 'written') ($case.Family + ' write succeeds')
|
||||
Assert-That ($written.changes[0].after -ceq $prediction.After) ($case.Family + ' stable separate write invocation has exact preview text')
|
||||
Assert-That ((Read-Exact $aliasEntry) -ceq $prediction.After) ($case.Family + ' persisted text equals independent expectation')
|
||||
}
|
||||
$afterCheck = Invoke-ArcadiaProbe @('check', $aliasEntry)
|
||||
Assert-That ($afterCheck.exit_code -eq 0) 'all-family final source is freshly admitted'
|
||||
$mtime = [IO.File]::GetLastWriteTimeUtc($aliasEntry)
|
||||
$noop = Invoke-ArcadiaProbe @('stabilize', $aliasEntry, '--write')
|
||||
Assert-That ($noop.exit_code -eq 0 -and $noop.status -ceq 'unchanged' -and !$noop.written -and @($noop.changes).Count -eq 0) 'stable all-family source is a true no-op'
|
||||
Assert-That ([IO.File]::GetLastWriteTimeUtc($aliasEntry) -eq $mtime) 'no-op preserves source modification time'
|
||||
Assert-That ((Read-Exact $aliasEntry) -ceq $expected) 'no-op preserves every source byte'
|
||||
Assert-That (@(Get-ChildItem -LiteralPath (Split-Path -Parent $aliasEntry) -File -Recurse -Force).Count -eq 1) 'writes leave no unexpected physical file'
|
||||
|
||||
# Two warnings in one CRLF/Unicode include expose the shifted-second-span trap.
|
||||
$rawEntry = Join-Path $runRoot 'raw/project.arcadia'
|
||||
$workPath = Join-Path $runRoot 'raw/work.arcadia'
|
||||
$entryText = 'arcadia 7; project diagnostic_case "Diagnostics" id "11000000-0000-4000-8000-000000000001" { include "work.arcadia"; }' + "`r`n"
|
||||
$rawWork = "// Пролог 😀`r`n" + 'sa_function first "航海" {}' + "`r`n" + 'sa_function second "Deuxième" {}' + "`r`n"
|
||||
Put $rawEntry $entryText
|
||||
Put $workPath $rawWork
|
||||
function Check-Warnings($report, [string]$rawText, [bool]$explicit) {
|
||||
Assert-That ($report.exit_code -eq 0) 'partial unallocated functions remain valid'
|
||||
$warnings = @($report.diagnostics | Where-Object { $_.code -ceq 'unallocated_leaf' })
|
||||
Assert-That ($warnings.Count -eq 2) 'exactly two raw unallocated-leaf diagnostics without stabilized duplicates'
|
||||
$index = 0
|
||||
foreach ($alias in @('first', 'second')) {
|
||||
$start = $utf8.GetByteCount($rawText.Substring(0, $rawText.IndexOf('sa_function ' + $alias, [StringComparison]::Ordinal)))
|
||||
$warning = @($warnings | Where-Object { $_.location.start -eq $start })
|
||||
Assert-That ($warning.Count -eq 1) ($alias + ' warning starts at original UTF-8 declaration offset')
|
||||
Assert-That ($warning[0].coordinate_space -ceq 'input' -and $warning[0].location.path -ceq 'work.arcadia') ($alias + ' warning identifies raw include')
|
||||
Assert-That ($warning[0].location.line -eq ($index + 2) -and $warning[0].location.column -eq 1) ($alias + ' CRLF/Unicode line and scalar column')
|
||||
$bytes = $utf8.GetBytes($rawText)
|
||||
$location = $warning[0].location
|
||||
Assert-That ($location.end -gt $start -and $location.end -le $bytes.Length) ($alias + ' warning ends inside raw input')
|
||||
$slice = $utf8.GetString($bytes, $start, $location.end - $start)
|
||||
Assert-That ($slice.Contains('sa_function ' + $alias)) ($alias + ' raw diagnostic slice identifies intended declaration')
|
||||
$expectedId = if ($explicit) { '11000000-0000-4000-8000-00000000000' + ($index + 2) } else { $null }
|
||||
Assert-That ($warning[0].object_id -ceq $expectedId) ($alias + ' diagnostic identity has correct authored/provisional status')
|
||||
$index++
|
||||
}
|
||||
}
|
||||
$rawCheck = Invoke-ArcadiaProbe @('check', $rawEntry)
|
||||
Check-Warnings $rawCheck $rawWork $false
|
||||
$rawPreview = Invoke-ArcadiaProbe @('stabilize', $rawEntry)
|
||||
Check-Warnings $rawPreview $rawWork $false
|
||||
Assert-That ((Read-Exact $rawEntry) -ceq $entryText -and (Read-Exact $workPath) -ceq $rawWork) 'raw diagnostics and preview retain both exact source files'
|
||||
Assert-That (@($rawPreview.changes).Count -eq 1 -and $rawPreview.changes[0].path -ceq 'work.arcadia') 'only missing-ID include is proposed for stabilization'
|
||||
$candidateText = $rawPreview.changes[0].after
|
||||
Assert-That ($utf8.GetByteCount($candidateText.Substring(0, $candidateText.IndexOf('sa_function second'))) -gt $utf8.GetByteCount($rawWork.Substring(0, $rawWork.IndexOf('sa_function second')))) 'fixture genuinely shifts second declaration in proposed candidate'
|
||||
$authoredWork = $rawWork.Replace('first "航海" {', 'first "航海" id "11000000-0000-4000-8000-000000000002" {').Replace('second "Deuxième" {', 'second "Deuxième" id "11000000-0000-4000-8000-000000000003" {')
|
||||
Put $workPath $authoredWork
|
||||
$authoredCheck = Invoke-ArcadiaProbe @('lint', $rawEntry)
|
||||
Check-Warnings $authoredCheck $authoredWork $true
|
||||
Assert-That ((Read-Exact $workPath) -ceq $authoredWork) 'authored-ID lint is read-only'
|
||||
|
||||
# A non-BMP symbol before the bad reference distinguishes scalar columns from UTF-16 units.
|
||||
$invalidWork = "// Пролог 😀`r`n" + 'sa_function invalid "Deuxième 🚨" { parent @absent; }' + "`r`n"
|
||||
Put $workPath $invalidWork
|
||||
$invalid = Invoke-ArcadiaProbe @('stabilize', $rawEntry, '--write')
|
||||
Assert-That ($invalid.exit_code -eq 1 -and !$invalid.written -and @($invalid.changes).Count -eq 0) 'invalid raw Unicode source is refused before any candidate write'
|
||||
$referenceIndex = $invalidWork.IndexOf('@absent', [StringComparison]::Ordinal)
|
||||
$referenceStart = $utf8.GetByteCount($invalidWork.Substring(0, $referenceIndex))
|
||||
$errorAt = @($invalid.diagnostics | Where-Object { $_.severity -ceq 'error' -and $_.location.start -eq $referenceStart })
|
||||
Assert-That ($errorAt.Count -eq 1) 'unresolved reference uses exact raw UTF-8 byte start'
|
||||
$linePrefix = $invalidWork.Substring($invalidWork.IndexOf("`n") + 1, $referenceIndex - $invalidWork.IndexOf("`n") - 1)
|
||||
$scalarColumn = @($linePrefix.ToCharArray() | Where-Object { ![char]::IsLowSurrogate($_) }).Count + 1
|
||||
Assert-That ($errorAt[0].location.line -eq 2 -and $errorAt[0].location.column -eq $scalarColumn) 'non-BMP prefix uses Unicode scalar column rather than UTF-16 units'
|
||||
Assert-That ($errorAt[0].coordinate_space -ceq 'input' -and $null -eq $errorAt[0].object_id) 'invalid input exposes neither candidate coordinate nor ephemeral identity'
|
||||
Assert-That ((Read-Exact $workPath) -ceq $invalidWork -and (Read-Exact $rawEntry) -ceq $entryText) 'invalid write retains complete input bundle bytes'
|
||||
Assert-That ((Get-FileHash -LiteralPath $exe).Hash -ceq $expectedExeHash) 'CLI pin remains unchanged after probes'
|
||||
$result = [ordered]@{
|
||||
Reviewer = 'Codex-9e8r6t /root/behavior_audit'
|
||||
RecordedUtc = [DateTime]::UtcNow.ToString('O')
|
||||
BinarySHA256 = $expectedExeHash
|
||||
InitialPublishedFixtureSHA256 = $sourceHash
|
||||
AliasInputSHA256 = Text-Hash $source
|
||||
AliasExpectedFinalSHA256 = Text-Hash $expected
|
||||
CheckCount = $checks.Count
|
||||
Checks = $checks.ToArray()
|
||||
Operations = $operations.ToArray()
|
||||
Limits = 'Independent CLI process/source-byte checks only; no production compile, full Project comparison, concurrency proof, Linux or native GUI claim.'
|
||||
}
|
||||
Put (Join-Path $PSScriptRoot 'result.json') ($result | ConvertTo-Json -Depth 100)
|
||||
Write-Output "PASS: $($checks.Count) independent assertions across $($operations.Count) frozen CLI invocations."
|
||||
@@ -0,0 +1,465 @@
|
||||
{
|
||||
"Reviewer": "Codex-9e8r6t /root/behavior_audit",
|
||||
"RecordedUtc": "2026-09-11T11:59:00.5676616Z",
|
||||
"BinarySHA256": "BAD169F8BE78DCFA74B32714569C886DD2132727080684FC2BA618D5822E3559",
|
||||
"InitialPublishedFixtureSHA256": "E68D7162F6F7656AAD51853E5A38DECD2D84F4CC3B6A9120AF86AC7A64AA92A2",
|
||||
"AliasInputSHA256": "53ACB827AB030203411D0E29907142AA83ECD65C133D5C975DDB9AB74920308B",
|
||||
"AliasExpectedFinalSHA256": "1DCFFDEDEE6B2647F20442D1A8E74BF72AFBF30F1B067F3C947D52117B032C44",
|
||||
"CheckCount": 183,
|
||||
"Checks": [
|
||||
"immutable CLI binary pin",
|
||||
"project has one literal declaration header",
|
||||
"element has one literal declaration header",
|
||||
"element live-reference count",
|
||||
"relation has one literal declaration header",
|
||||
"relation live-reference count",
|
||||
"diagram has one literal declaration header",
|
||||
"diagram live-reference count",
|
||||
"placement has one literal declaration header",
|
||||
"artifact has one literal declaration header",
|
||||
"trace has one literal declaration header",
|
||||
"process exit agrees with JSON",
|
||||
"JSON invocation emits no mixed stderr",
|
||||
"all-family baseline is admitted before mutation",
|
||||
"all-family baseline has no missing identities",
|
||||
"all-family baseline contains exactly 40 authored UUIDs",
|
||||
"process exit agrees with JSON",
|
||||
"JSON invocation emits no mixed stderr",
|
||||
"project preview succeeds without write",
|
||||
"project preview preserves exact input bytes",
|
||||
"project reports exactly one changed file",
|
||||
"project preserves file ownership",
|
||||
"project reports exact captured text",
|
||||
"project changes only independently predicted header/live-reference tokens",
|
||||
"project invents no identity or migration",
|
||||
"process exit agrees with JSON",
|
||||
"JSON invocation emits no mixed stderr",
|
||||
"project write succeeds",
|
||||
"project stable separate write invocation has exact preview text",
|
||||
"project persisted text equals independent expectation",
|
||||
"process exit agrees with JSON",
|
||||
"JSON invocation emits no mixed stderr",
|
||||
"element preview succeeds without write",
|
||||
"element preview preserves exact input bytes",
|
||||
"element reports exactly one changed file",
|
||||
"element preserves file ownership",
|
||||
"element reports exact captured text",
|
||||
"element changes only independently predicted header/live-reference tokens",
|
||||
"element invents no identity or migration",
|
||||
"process exit agrees with JSON",
|
||||
"JSON invocation emits no mixed stderr",
|
||||
"element write succeeds",
|
||||
"element stable separate write invocation has exact preview text",
|
||||
"element persisted text equals independent expectation",
|
||||
"process exit agrees with JSON",
|
||||
"JSON invocation emits no mixed stderr",
|
||||
"relation preview succeeds without write",
|
||||
"relation preview preserves exact input bytes",
|
||||
"relation reports exactly one changed file",
|
||||
"relation preserves file ownership",
|
||||
"relation reports exact captured text",
|
||||
"relation changes only independently predicted header/live-reference tokens",
|
||||
"relation invents no identity or migration",
|
||||
"process exit agrees with JSON",
|
||||
"JSON invocation emits no mixed stderr",
|
||||
"relation write succeeds",
|
||||
"relation stable separate write invocation has exact preview text",
|
||||
"relation persisted text equals independent expectation",
|
||||
"process exit agrees with JSON",
|
||||
"JSON invocation emits no mixed stderr",
|
||||
"diagram preview succeeds without write",
|
||||
"diagram preview preserves exact input bytes",
|
||||
"diagram reports exactly one changed file",
|
||||
"diagram preserves file ownership",
|
||||
"diagram reports exact captured text",
|
||||
"diagram changes only independently predicted header/live-reference tokens",
|
||||
"diagram invents no identity or migration",
|
||||
"process exit agrees with JSON",
|
||||
"JSON invocation emits no mixed stderr",
|
||||
"diagram write succeeds",
|
||||
"diagram stable separate write invocation has exact preview text",
|
||||
"diagram persisted text equals independent expectation",
|
||||
"process exit agrees with JSON",
|
||||
"JSON invocation emits no mixed stderr",
|
||||
"placement preview succeeds without write",
|
||||
"placement preview preserves exact input bytes",
|
||||
"placement reports exactly one changed file",
|
||||
"placement preserves file ownership",
|
||||
"placement reports exact captured text",
|
||||
"placement changes only independently predicted header/live-reference tokens",
|
||||
"placement invents no identity or migration",
|
||||
"process exit agrees with JSON",
|
||||
"JSON invocation emits no mixed stderr",
|
||||
"placement write succeeds",
|
||||
"placement stable separate write invocation has exact preview text",
|
||||
"placement persisted text equals independent expectation",
|
||||
"process exit agrees with JSON",
|
||||
"JSON invocation emits no mixed stderr",
|
||||
"artifact preview succeeds without write",
|
||||
"artifact preview preserves exact input bytes",
|
||||
"artifact reports exactly one changed file",
|
||||
"artifact preserves file ownership",
|
||||
"artifact reports exact captured text",
|
||||
"artifact changes only independently predicted header/live-reference tokens",
|
||||
"artifact invents no identity or migration",
|
||||
"process exit agrees with JSON",
|
||||
"JSON invocation emits no mixed stderr",
|
||||
"artifact write succeeds",
|
||||
"artifact stable separate write invocation has exact preview text",
|
||||
"artifact persisted text equals independent expectation",
|
||||
"process exit agrees with JSON",
|
||||
"JSON invocation emits no mixed stderr",
|
||||
"trace preview succeeds without write",
|
||||
"trace preview preserves exact input bytes",
|
||||
"trace reports exactly one changed file",
|
||||
"trace preserves file ownership",
|
||||
"trace reports exact captured text",
|
||||
"trace changes only independently predicted header/live-reference tokens",
|
||||
"trace invents no identity or migration",
|
||||
"process exit agrees with JSON",
|
||||
"JSON invocation emits no mixed stderr",
|
||||
"trace write succeeds",
|
||||
"trace stable separate write invocation has exact preview text",
|
||||
"trace persisted text equals independent expectation",
|
||||
"process exit agrees with JSON",
|
||||
"JSON invocation emits no mixed stderr",
|
||||
"all-family final source is freshly admitted",
|
||||
"process exit agrees with JSON",
|
||||
"JSON invocation emits no mixed stderr",
|
||||
"stable all-family source is a true no-op",
|
||||
"no-op preserves source modification time",
|
||||
"no-op preserves every source byte",
|
||||
"writes leave no unexpected physical file",
|
||||
"process exit agrees with JSON",
|
||||
"JSON invocation emits no mixed stderr",
|
||||
"partial unallocated functions remain valid",
|
||||
"exactly two raw unallocated-leaf diagnostics without stabilized duplicates",
|
||||
"first warning starts at original UTF-8 declaration offset",
|
||||
"first warning identifies raw include",
|
||||
"first CRLF/Unicode line and scalar column",
|
||||
"first warning ends inside raw input",
|
||||
"first raw diagnostic slice identifies intended declaration",
|
||||
"first diagnostic identity has correct authored/provisional status",
|
||||
"second warning starts at original UTF-8 declaration offset",
|
||||
"second warning identifies raw include",
|
||||
"second CRLF/Unicode line and scalar column",
|
||||
"second warning ends inside raw input",
|
||||
"second raw diagnostic slice identifies intended declaration",
|
||||
"second diagnostic identity has correct authored/provisional status",
|
||||
"process exit agrees with JSON",
|
||||
"JSON invocation emits no mixed stderr",
|
||||
"partial unallocated functions remain valid",
|
||||
"exactly two raw unallocated-leaf diagnostics without stabilized duplicates",
|
||||
"first warning starts at original UTF-8 declaration offset",
|
||||
"first warning identifies raw include",
|
||||
"first CRLF/Unicode line and scalar column",
|
||||
"first warning ends inside raw input",
|
||||
"first raw diagnostic slice identifies intended declaration",
|
||||
"first diagnostic identity has correct authored/provisional status",
|
||||
"second warning starts at original UTF-8 declaration offset",
|
||||
"second warning identifies raw include",
|
||||
"second CRLF/Unicode line and scalar column",
|
||||
"second warning ends inside raw input",
|
||||
"second raw diagnostic slice identifies intended declaration",
|
||||
"second diagnostic identity has correct authored/provisional status",
|
||||
"raw diagnostics and preview retain both exact source files",
|
||||
"only missing-ID include is proposed for stabilization",
|
||||
"fixture genuinely shifts second declaration in proposed candidate",
|
||||
"process exit agrees with JSON",
|
||||
"JSON invocation emits no mixed stderr",
|
||||
"partial unallocated functions remain valid",
|
||||
"exactly two raw unallocated-leaf diagnostics without stabilized duplicates",
|
||||
"first warning starts at original UTF-8 declaration offset",
|
||||
"first warning identifies raw include",
|
||||
"first CRLF/Unicode line and scalar column",
|
||||
"first warning ends inside raw input",
|
||||
"first raw diagnostic slice identifies intended declaration",
|
||||
"first diagnostic identity has correct authored/provisional status",
|
||||
"second warning starts at original UTF-8 declaration offset",
|
||||
"second warning identifies raw include",
|
||||
"second CRLF/Unicode line and scalar column",
|
||||
"second warning ends inside raw input",
|
||||
"second raw diagnostic slice identifies intended declaration",
|
||||
"second diagnostic identity has correct authored/provisional status",
|
||||
"authored-ID lint is read-only",
|
||||
"process exit agrees with JSON",
|
||||
"JSON invocation emits no mixed stderr",
|
||||
"invalid raw Unicode source is refused before any candidate write",
|
||||
"unresolved reference uses exact raw UTF-8 byte start",
|
||||
"non-BMP prefix uses Unicode scalar column rather than UTF-16 units",
|
||||
"invalid input exposes neither candidate coordinate nor ephemeral identity",
|
||||
"invalid write retains complete input bundle bytes",
|
||||
"CLI pin remains unchanged after probes"
|
||||
],
|
||||
"Operations": [
|
||||
{
|
||||
"Arguments": [
|
||||
"check",
|
||||
"<fixture>\\aliases\\project.arcadia"
|
||||
],
|
||||
"Exit": 0,
|
||||
"Status": "checked",
|
||||
"Changes": 0,
|
||||
"Diagnostics": 0,
|
||||
"StdoutSHA256": "251E0C7CAF9243AFBCDCCD371707B8BFFF63CCF47537DABE9C398B4F3BBDC165"
|
||||
},
|
||||
{
|
||||
"Arguments": [
|
||||
"rename-alias",
|
||||
"<fixture>\\aliases\\project.arcadia",
|
||||
"alarm_response",
|
||||
"alarm_suite"
|
||||
],
|
||||
"Exit": 0,
|
||||
"Status": "preview",
|
||||
"Changes": 1,
|
||||
"Diagnostics": 0,
|
||||
"StdoutSHA256": "E577D97B27E93CF4395EA67C01177FB7427EC28AE7D83629BD3CAB82849AE712"
|
||||
},
|
||||
{
|
||||
"Arguments": [
|
||||
"rename-alias",
|
||||
"<fixture>\\aliases\\project.arcadia",
|
||||
"alarm_response",
|
||||
"alarm_suite",
|
||||
"--write"
|
||||
],
|
||||
"Exit": 0,
|
||||
"Status": "written",
|
||||
"Changes": 1,
|
||||
"Diagnostics": 0,
|
||||
"StdoutSHA256": "1134EA13A5674D0AF0237BFB08C720FF046C0D82C8AE916AA6F97C40A009471F"
|
||||
},
|
||||
{
|
||||
"Arguments": [
|
||||
"rename-alias",
|
||||
"<fixture>\\aliases\\project.arcadia",
|
||||
"maintenance",
|
||||
"maintenance_staff"
|
||||
],
|
||||
"Exit": 0,
|
||||
"Status": "preview",
|
||||
"Changes": 1,
|
||||
"Diagnostics": 0,
|
||||
"StdoutSHA256": "05CFEFA2CDD8612628906E81BA994446FC2D408642CAD8C8EAE1B0B0BDBFE5D5"
|
||||
},
|
||||
{
|
||||
"Arguments": [
|
||||
"rename-alias",
|
||||
"<fixture>\\aliases\\project.arcadia",
|
||||
"maintenance",
|
||||
"maintenance_staff",
|
||||
"--write"
|
||||
],
|
||||
"Exit": 0,
|
||||
"Status": "written",
|
||||
"Changes": 1,
|
||||
"Diagnostics": 0,
|
||||
"StdoutSHA256": "54B496AFCC671910E5109FFF3B65CFE0DC1BEA8D85871BDBC976177F28962AEF"
|
||||
},
|
||||
{
|
||||
"Arguments": [
|
||||
"rename-alias",
|
||||
"<fixture>\\aliases\\project.arcadia",
|
||||
"alarm_message",
|
||||
"alarm_payload"
|
||||
],
|
||||
"Exit": 0,
|
||||
"Status": "preview",
|
||||
"Changes": 1,
|
||||
"Diagnostics": 0,
|
||||
"StdoutSHA256": "61309AADFF80386FA73FA9E7005C4C45548C56BC015BAF11750DCB00CD379E92"
|
||||
},
|
||||
{
|
||||
"Arguments": [
|
||||
"rename-alias",
|
||||
"<fixture>\\aliases\\project.arcadia",
|
||||
"alarm_message",
|
||||
"alarm_payload",
|
||||
"--write"
|
||||
],
|
||||
"Exit": 0,
|
||||
"Status": "written",
|
||||
"Changes": 1,
|
||||
"Diagnostics": 0,
|
||||
"StdoutSHA256": "EB0317BF9C7A6C54644748D7F8BFD04E082469AE98EAAA1F39DCC2AED9EDFDE7"
|
||||
},
|
||||
{
|
||||
"Arguments": [
|
||||
"rename-alias",
|
||||
"<fixture>\\aliases\\project.arcadia",
|
||||
"response_view",
|
||||
"response_map"
|
||||
],
|
||||
"Exit": 0,
|
||||
"Status": "preview",
|
||||
"Changes": 1,
|
||||
"Diagnostics": 0,
|
||||
"StdoutSHA256": "3D2325D62D6F7D7FB7D7B7E162800DBBB15A4328BC1C5C0BF98EFD794E4AA472"
|
||||
},
|
||||
{
|
||||
"Arguments": [
|
||||
"rename-alias",
|
||||
"<fixture>\\aliases\\project.arcadia",
|
||||
"response_view",
|
||||
"response_map",
|
||||
"--write"
|
||||
],
|
||||
"Exit": 0,
|
||||
"Status": "written",
|
||||
"Changes": 1,
|
||||
"Diagnostics": 0,
|
||||
"StdoutSHA256": "E77B284EAFFBB090B73035FD584CE1D184C601112BCC0D5266D42F1CD7D2D185"
|
||||
},
|
||||
{
|
||||
"Arguments": [
|
||||
"rename-alias",
|
||||
"<fixture>\\aliases\\project.arcadia",
|
||||
"detector_card",
|
||||
"detector_tile"
|
||||
],
|
||||
"Exit": 0,
|
||||
"Status": "preview",
|
||||
"Changes": 1,
|
||||
"Diagnostics": 0,
|
||||
"StdoutSHA256": "8DBA5013A3404CB5719573CDE66C92768C764B363C937646B79D848EA356D275"
|
||||
},
|
||||
{
|
||||
"Arguments": [
|
||||
"rename-alias",
|
||||
"<fixture>\\aliases\\project.arcadia",
|
||||
"detector_card",
|
||||
"detector_tile",
|
||||
"--write"
|
||||
],
|
||||
"Exit": 0,
|
||||
"Status": "written",
|
||||
"Changes": 1,
|
||||
"Diagnostics": 0,
|
||||
"StdoutSHA256": "EB444F39F873C58A8192C0A5AFC2C48FCAF1CAB333231C66471F71D1C1D2486A"
|
||||
},
|
||||
{
|
||||
"Arguments": [
|
||||
"rename-alias",
|
||||
"<fixture>\\aliases\\project.arcadia",
|
||||
"response_procedure",
|
||||
"response_handbook"
|
||||
],
|
||||
"Exit": 0,
|
||||
"Status": "preview",
|
||||
"Changes": 1,
|
||||
"Diagnostics": 0,
|
||||
"StdoutSHA256": "6613EDCABEF4D8AC81E9FC21000FDE7AF10726E882BAF106ECA7991E553C4F53"
|
||||
},
|
||||
{
|
||||
"Arguments": [
|
||||
"rename-alias",
|
||||
"<fixture>\\aliases\\project.arcadia",
|
||||
"response_procedure",
|
||||
"response_handbook",
|
||||
"--write"
|
||||
],
|
||||
"Exit": 0,
|
||||
"Status": "written",
|
||||
"Changes": 1,
|
||||
"Diagnostics": 0,
|
||||
"StdoutSHA256": "0B740F86D607AD194EBD93D63DA65210A39DD35B1B3757CE679EC1A84FB8D3B9"
|
||||
},
|
||||
{
|
||||
"Arguments": [
|
||||
"rename-alias",
|
||||
"<fixture>\\aliases\\project.arcadia",
|
||||
"assessment_refinement",
|
||||
"assessment_alignment"
|
||||
],
|
||||
"Exit": 0,
|
||||
"Status": "preview",
|
||||
"Changes": 1,
|
||||
"Diagnostics": 0,
|
||||
"StdoutSHA256": "101A1D9E89F482E6607B8A4855A673F56897818969333F6ED0D5F5749BFA739D"
|
||||
},
|
||||
{
|
||||
"Arguments": [
|
||||
"rename-alias",
|
||||
"<fixture>\\aliases\\project.arcadia",
|
||||
"assessment_refinement",
|
||||
"assessment_alignment",
|
||||
"--write"
|
||||
],
|
||||
"Exit": 0,
|
||||
"Status": "written",
|
||||
"Changes": 1,
|
||||
"Diagnostics": 0,
|
||||
"StdoutSHA256": "DA48C339A3C387BE06F6C3DCD799B3F98E75F6F97F760195EFFB9AB1C86F6EE7"
|
||||
},
|
||||
{
|
||||
"Arguments": [
|
||||
"check",
|
||||
"<fixture>\\aliases\\project.arcadia"
|
||||
],
|
||||
"Exit": 0,
|
||||
"Status": "checked",
|
||||
"Changes": 0,
|
||||
"Diagnostics": 0,
|
||||
"StdoutSHA256": "251E0C7CAF9243AFBCDCCD371707B8BFFF63CCF47537DABE9C398B4F3BBDC165"
|
||||
},
|
||||
{
|
||||
"Arguments": [
|
||||
"stabilize",
|
||||
"<fixture>\\aliases\\project.arcadia",
|
||||
"--write"
|
||||
],
|
||||
"Exit": 0,
|
||||
"Status": "unchanged",
|
||||
"Changes": 0,
|
||||
"Diagnostics": 0,
|
||||
"StdoutSHA256": "FD9DF6ECDEF4F60AFDB07C8B37848E65ABE2E995A8A9F21506A322BE173753BE"
|
||||
},
|
||||
{
|
||||
"Arguments": [
|
||||
"check",
|
||||
"<fixture>\\raw\\project.arcadia"
|
||||
],
|
||||
"Exit": 0,
|
||||
"Status": "checked",
|
||||
"Changes": 0,
|
||||
"Diagnostics": 5,
|
||||
"StdoutSHA256": "76363050F8B22A12D5A587156C7C6846ABD9CB087378735068CE0AFC5FAA798D"
|
||||
},
|
||||
{
|
||||
"Arguments": [
|
||||
"stabilize",
|
||||
"<fixture>\\raw\\project.arcadia"
|
||||
],
|
||||
"Exit": 0,
|
||||
"Status": "preview",
|
||||
"Changes": 1,
|
||||
"Diagnostics": 5,
|
||||
"StdoutSHA256": "4B832B7D683AEED0ABCCE29262561CF3C77DF0CB1E965990C28B860ED0002F7A"
|
||||
},
|
||||
{
|
||||
"Arguments": [
|
||||
"lint",
|
||||
"<fixture>\\raw\\project.arcadia"
|
||||
],
|
||||
"Exit": 0,
|
||||
"Status": "checked",
|
||||
"Changes": 0,
|
||||
"Diagnostics": 5,
|
||||
"StdoutSHA256": "00DDDFC5A535317F6D4B5F8FFF46FACEDAF49E89D9D1ED319C4EC3D24B018BEC"
|
||||
},
|
||||
{
|
||||
"Arguments": [
|
||||
"stabilize",
|
||||
"<fixture>\\raw\\project.arcadia",
|
||||
"--write"
|
||||
],
|
||||
"Exit": 1,
|
||||
"Status": "invalid",
|
||||
"Changes": 0,
|
||||
"Diagnostics": 1,
|
||||
"StdoutSHA256": "0813BD7BEF5B4B20D500A3B547C07F52C10C3E074EC6B24B57021F42FC1AE5D1"
|
||||
}
|
||||
],
|
||||
"Limits": "Independent CLI process/source-byte checks only; no production compile, full Project comparison, concurrency proof, Linux or native GUI claim."
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
# Source-only CLI baseline — 2026-09-11
|
||||
|
||||
Codex-9e8r6t /root/palette_finish. Root authorized only new `crates/oa-cli`, its focused integration tests and this report. Root owns workspace membership, lockfile and public documentation. No grammar/schema/core/desktop change is part of this slice. The external source-language experiment remains a separate unratified design.
|
||||
|
||||
Read AGENTS, current issue1 through #143, existing local usability audit, external `release-textual-authoring-design-20260911.md`, and core source/session/file APIs. The independent reviewer supplied the loader's stabilized-warning/provisional-ID trap and verified journal/migration/no-op boundaries. Their review is `release-source-cli-review-20260911.md`.
|
||||
|
||||
## Proposed command contract implemented for validation
|
||||
|
||||
```
|
||||
arcadia check ROOT [--json]
|
||||
arcadia lint ROOT [--json]
|
||||
arcadia stabilize ROOT [--write] [--json]
|
||||
arcadia rename-alias ROOT OLD NEW [--write] [--json]
|
||||
```
|
||||
|
||||
ROOT is the explicit entry file. No project discovery, editor, server or network is required. Check and lint are equivalent admission plus engineering-diagnostic commands. A valid model with warnings returns success. Mutation commands preview by default; `--write` explicitly saves the candidate captured and displayed/reported by that same invocation. Missing IDs in an earlier separate preview are provisional: another invocation may allocate different IDs until saved. There is no persisted apply-plan protocol in this baseline.
|
||||
|
||||
Exit0 means valid/success (warnings permitted),1 means invalid source or rejected semantic alias command,2 means usage,3 means disk/path/conflict/pending journal or output failure. Pending-save diagnosis is obtained from the production loader's `pending_save` code, not a copied journal-name calculation. Invalid raw source is never migrated, stabilized or renamed.
|
||||
|
||||
Human output places located diagnostics/status/notes on stderr and per-file unified diffs on stdout. With `--write`, the human candidate is flushed before the saver is invoked. JSON output is a single versioned envelope containing diagnostics, status, exit code, write result, exact changed before/after text, unified diffs, missing-identity count and a separate structured source-migration report. JSON after a failed save retains the proposed diff with `written:false`; a failed save can require explicit recovery and is not a blanket rollback guarantee. Diagnostic locations are half-open UTF-8 offsets into the named coordinate space; input line/column are one-based Unicode scalar coordinates. Related locations carry exact byte spans. Command/disk errors without a responsible source token explicitly have no source location.
|
||||
|
||||
The raw `lint_bundle` diagnostics are authoritative for source positions. Loader-only discovery/IO/path/pending errors are retained; successful-loader warnings from stabilized compilation are discarded. Unpersisted lint object IDs are omitted so they cannot be mistaken for the later candidate's freshly allocated IDs. Explicit authored object IDs remain available. Post-compilation/update errors identify their candidate coordinate space rather than claiming to point into original bytes.
|
||||
|
||||
## API and safety boundary
|
||||
|
||||
`load_project` captures original byte expectations and discoverable source, then `lint_bundle` validates raw reachable text. On success, `compile_bundle` stabilizes identities and performs supported core migrations. Alias rename resolves the existing global alias and uses `prepare_command(SetAlias)` plus `update_bundle_with_identity_map`; it does not replace text blindly. The initial migration report is retained before the updater recompiles current source. The complete raw-to-final diff shows identity insertion, supported source migration and rename together while their summary fields remain distinct. There is no canonical formatter fallback or new file writer.
|
||||
|
||||
`Prepared` privately owns the original DiskProject and final CompiledSourceProject. Its consuming `write` invokes `source_files::save_project`, including for an empty diff. The saver rechecks pending journal and every captured dependency before its existing no-op return, preserving conflict and journal guarantees. No CLI path calls `recover_project`. The inherited contract is recoverability with entry committed last; it is not cross-process compare-and-swap, simultaneous multi-file visibility, or a Windows power-loss durability claim. Detached files are never deleted by this wrapper. No directories/files are created by a preview.
|
||||
|
||||
Admission limits match desktop policy:256 files, include depth32/edges1024,64MiB aggregate UTF-8,100000 graph objects,250000 records and500000 ordered references. These are exposed in help; no performance acceptance is inferred. The per-file diff uses a linear common-prefix/suffix algorithm with one hunk and three context lines, avoiding quadratic LCS memory on large files. Far-apart changes can produce a large hunk; JSON also preserves exact full changed-file texts and newline bytes.
|
||||
|
||||
Dependencies are existing `oa-core`, workspace `serde` and `serde_json`; no new registry package. Root added workspace membership, then granted the serialized Cargo window. Cargo.lock gained only the local `oa-cli`0.2.0 package and those three existing dependencies. Source formatting was limited to this crate.
|
||||
|
||||
## Focused acceptance and executed validation
|
||||
|
||||
Twelve integration tests cover the complete externally authored 40-identity alarm model; preview nonmutation and one-time exact ID-only stabilization; a same-invocation captured write; CRLF/comments/omitted defaults and no-op mtime/inventory; three-file semantic alias rename with independent complete-model alias-only expectation; invalid Unicode raw diagnostics and no writes; raw missing-ID warning locality/provisional-ID suppression; external change before write and before a no-op write; pending journal at load and after preview with untouched sentinel; rejected and same-alias rename; supported source5 migration separately reported through rename; and explicit root/machine-readable usage errors. Additional cases cover invalid UTF-8, unavailable includes, unsupported/invalid old source, non-element alias rename, a changed dependency absent from the proposed diff, and exact retention of a detached invalid file. Some tests contain several linked assertions, but this is twelve test functions, not an invented larger native acceptance count.
|
||||
|
||||
The complete example fixture is the frozen external authoring experiment, not a new CLI-generated model. No new syntax or broad text-usability acceptance is ratified. Large-model timing, persistent review-plan application, reparent/navigation/LSP, migration to a future grammar and publication packaging remain outside this baseline.
|
||||
|
||||
The Windows GNU/LLVM toolchain selected by `.local/toolchain/env.ps1`, existing target and `CARGO_BUILD_JOBS=1` ran these gates:
|
||||
|
||||
| Gate | Result |
|
||||
| --- | --- |
|
||||
| `cargo test --offline -p oa-cli --all-targets -- --test-threads=1` | Initial compilation succeeded;11/12 tests passed, exit101,58.621s total. The remaining fixture used unsupported `/* block comment */` syntax. |
|
||||
| `cargo test --locked --offline -p oa-cli --all-targets -- --test-threads=1` | After changing only that fixture to a supported line comment: **12/12 passed**, exit0,13.783s total /1.88s test execution. |
|
||||
| `cargo clippy --locked --offline -p oa-cli --all-targets -- -D warnings` | Passed,136.447s initially (including core compilation). Independent review then corrected a note that incorrectly described changed-file output as a complete project candidate. Final strict pass:10.245s. |
|
||||
| `cargo build --locked --offline -p oa-cli --bin arcadia` | Final wording-matched executable built successfully,9.734s. |
|
||||
| `rustfmt --check --edition 2024` on the three crate Rust files | Passed. |
|
||||
| Staged executable `--help` / `--version` | Successful; version `arcadia 0.2.0`. |
|
||||
|
||||
No production behavior was changed to accommodate the invalid block-comment fixture. The failed initial log remains preserved. The final note explicitly promises complete proposed **changed-file** text/diffs and disclaims a saved plan or digest of unchanged dependencies. Source-file expectations still capture every dependency, including those absent from the diff. No whole-project hash/apply protocol was added. The final wording-only change was compiled and strictly linted; the twelve semantic integration cases precede that wording change.
|
||||
|
||||
Immutable staging directory: `.local/source-cli-20260911/verified/`. Its `arcadia.exe` is **20312064 bytes**, SHA256 **`BAD169F8BE78DCFA74B32714569C886DD2132727080684FC2BA618D5822E3559`**. Sibling `libunwind.dll` is190976 bytes, SHA256 `F45604A12E8BCD839FD86F667C1BE1CC7C4F9DA2B3C69FF381AC708F0E1AE7D4`. `manifest.json` records source/workspace/lock/runtime/log identities; SHA256 `DD73FC2BB2190B23FFFE16D6C63D4ACDDF52338062FAC298379DDFD241D82EB8`. The matching test artifact is `.local/target-gnullvm/debug/deps/source_cli-343cec8034acf06e.exe`, SHA256 `F740CA81D0C60DEC4AA6A2BC36D3F7035844590FAB59E88F63A7F4ABE3F78AEF`.
|
||||
|
||||
Logs and separate exact command/exit/time JSON records remain under `.local/source-cli-20260911/`:
|
||||
|
||||
| Log | SHA256 |
|
||||
| --- | --- |
|
||||
| `tests-initial.log` | `00378D96D09A95265B09C9C7C12CB09A610E46055AC36FFD9ACEF3265965D56C` |
|
||||
| `tests-final.log` | `A18B708B89FE715951201D9046C414080E0820BDC9C6DC9EDE9CF3AB84E37C04` |
|
||||
| `clippy-initial.log` | `A3B5A7F051CF51B812BF5D4D0917E5CEFBD25CCCAD4033BAABD7EB36FCCFA72A` |
|
||||
| `clippy-final.log` | `29C23E6372447D06EF68A5B161F323F80109024CEE3BB7AEC85F421530BD1564` |
|
||||
| `build.log` | `B0968196245C5302F2576C087551E3F92071F85314E60512117BC632EED20742` |
|
||||
|
||||
Cargo lease was explicitly released to root/desktop owner after all gates terminated. Files remain uncommitted for root integration. The independent reviewer has the immutable executable for isolated no-compile falsifiers; that review is recorded separately and is not silently included in these author-run counts. No Linux CLI execution, native GUI or broad text-authoring release acceptance is claimed.
|
||||
|
||||
Independent review subsequently completed on the frozen BAD169F8 executable: **183 assertions across21 process invocations passed**, with no remaining implementation blocker. It covered all seven persistent identity families, exact header/live-reference changes with comment/quoted-alias retention and40 original UUIDs, saved/no-op source/mtime, raw CRLF/Unicode second-declaration warning offsets, explicit/provisional warning IDs and invalid non-BMP reference scalar columns. This is reviewer-owned evidence, not an addition to the author's12 tests: `harness/release-source-cli-review-20260911.md`, SHA256 `475645DB76CD193F2FC1CC1BDDAB57CCB0A724A62779A60C1E2AF0F756929344`; result `harness/experiments/source-cli-independent-20260911/result.json`, SHA256 `7F772FAB0E58C9901D9B87D7553C605C84E5DD2B234123C14EFD5B1BAF46DA9C`. The review discloses its corrected initial harness naming clash. Gitea issue1 comment176 records the handoff.
|
||||
@@ -0,0 +1,136 @@
|
||||
# Source CLI: independent contract review before implementation
|
||||
|
||||
Codex-9e8r6t `/root/behavior_audit`, 2026-09-11. Read-only review of the existing core and the proposed CLI contract; no production edits, Cargo, direct rustc, native input, or CLI test execution. `/root/palette_finish` owns `crates/oa-cli`. Root owns integration and release acceptance.
|
||||
|
||||
Reviewed AGENTS.md, issue #1 through #143, the empty open-PR list, Wiki `Authoring20260911`, and the imported `release-textual-authoring-design-20260911.md`. Inspected core is the working tree at HEAD `4bb4aaf3dc47b9738625b4d3f23ef8c74f286395`; this review did not inspect the author's evolving CLI implementation. References below are relative to the repository root. Existing tests are evidence of intended contracts, not newly executed results.
|
||||
|
||||
The proposed slice is coherent: explicit entry file; `check`/`lint` against original bytes; `stabilize` and semantic `rename-alias` preview by default; `--write` uses the same prepared candidate and the loader's original disk expectations. It does not select a new grammar, namespace scheme, identity policy, or source7 constructor defaults. The external design identifies this as useful immediate infrastructure, not completion of the text-only usability objective.
|
||||
|
||||
## Material traps
|
||||
|
||||
### 1. P1 — Loader diagnostics do not uniformly describe the raw files it returns
|
||||
|
||||
`source_files::discover_missing` retains original file text and bytes, then calls `compile_bundle` to populate `DiskProject.diagnostics` when discovery has no earlier diagnostic (`crates/oa-core/src/source_files.rs:528`). Compilation inserts missing UUIDs and reindexes the returned text (`source_project.rs:286`). Consequently, a successful load can return **raw files with warning spans from stabilized files**. Appending those warnings to raw lint output duplicates warnings and can point an editor beyond the intended declaration.
|
||||
|
||||
The existing counterexample is two missing-ID functions in one included file: a warning for the second function moves when the first UUID is inserted. `crates/oa-core/tests/source_project.rs:207` explicitly distinguishes these positions. Use `lint_bundle(snapshot.reachable_bundle(), limits)` as the semantic diagnostic authority for original bytes. Preserve loader-only discovery, encoding, path, and pending-save failures; replacing every loader diagnostic with lint would turn invalid UTF-8 into a less useful missing-file complaint or lose the journal refusal. Do not blindly reuse the loader's compiled semantic warnings.
|
||||
|
||||
There is a second identity dimension: `lint_bundle` calls `compile_once`, and `Parser::optional_id` allocates fresh UUIDs for omitted clauses (`language.rs:1226`). A raw lint warning's `object_id` can therefore differ from the later compiled candidate's UUID even though its raw position is correct. JSON must either omit such provisional identities, identify their provenance explicitly, or deliberately map them to the candidate. It must not imply that an independently allocated lint UUID identifies the record in the diff. Explicitly authored IDs remain stable.
|
||||
|
||||
The same separation applies to a failure during stabilization or migration after raw lint succeeds, such as the inserted IDs exceeding the source-byte budget. Its candidate-location diagnostic must not silently be presented as an original-buffer span. Spans are half-open UTF-8 byte offsets; line/column are one-based, with column counting Unicode scalar values (`language.rs:107`, `source_project.rs:640`).
|
||||
|
||||
Status: sent directly to the CLI author and root before implementation.
|
||||
|
||||
### 2. P1 — Keep the captured write baseline, including for no-op writes and failures
|
||||
|
||||
The correct write authority is the original `DiskProject.expectations`, which records bytes **before** stabilization (`source_files.rs:43`). Do not reload after preparation or replace those expectations with candidate bytes. Either would discard evidence of an intervening editor change. Check every captured dependency, including a file the proposed rename does not change.
|
||||
|
||||
An empty diff is not permission to skip `save_project`: the core checks the pending journal and all original expectations before its no-write return (`source_files.rs:880–902`). A prepared no-op followed by an external edit must refuse, rather than report an up-to-date successful write. A successful unchanged write should perform no file replacement and create no journal.
|
||||
|
||||
The core promises recoverability and checks for observed external changes, **not** a cross-process CAS, multi-file atomic visibility, or Windows power-loss durability (`source_files.rs:1–18`). A noncooperating writer can change a file between a check and its replacement. Different entry files can also have overlapping dependencies without sharing one journal. Neither this CLI nor its diff adds a stronger guarantee. No extra locking design is required for this bounded slice; describe the actual guarantee accurately.
|
||||
|
||||
Distinguish invalid-input refusal before saving from a disk failure during saving. `save_project` can have written files, attempted rollback, or left a pending journal when recovery cannot finish. Exit 3 must retain the specific conflict/recovery outcome and must not always claim that nothing was written. The saver may recover its own failed transaction internally; the CLI must not automatically recover a pre-existing transaction.
|
||||
|
||||
Status: author already proposed original expectations and no CAS claim; the empty-diff bypass and failed-write wording are explicit acceptance boundaries.
|
||||
|
||||
### 3. P2 — Preserve migration evidence before the rename updater removes it
|
||||
|
||||
`compile_bundle` can perform a targeted upgrade of accepted source3–6 in addition to UUID stabilization. The migration can add semantic defaults and generated interface allocations, not merely change a version token (`source_project/source_migration.rs:4–105`, `persistence.rs:15`). Capture this initial `decoded.migration` before applying `SetAlias`.
|
||||
|
||||
`update_bundle_with_identity_map` recompiles the already-upgraded bundle at `source_project.rs:1016`. The final schema7 result has no new migration report. Reading only `final_candidate.decoded.migration` would therefore hide the fact that a rename also upgraded the project. Preserve the initial report separately and show the complete original-to-final file diff, including generated declarations and defaults. The same-alias command may still change source through stabilization or migration; `changed` must compare original and final bytes, not only the semantic rename result.
|
||||
|
||||
The bundle parser explicitly admits source3–7 (`language.rs:1404`). Source1/2 migration belongs to the separate single-string legacy API; `tests/source_project.rs:605` explicitly proves those inputs are rejected by `compile_bundle`. Do not silently fall back to `language::parse` plus canonical export: that changes format, ownership, trivia, and the advertised command. State the supported input versions and retain unsupported/invalid raw files unchanged.
|
||||
|
||||
Source7 omitted defaults keep their existing meanings, including Scenario viewpoint `MixedLegacy`. CLI stabilization must not adopt a more helpful inferred viewpoint, allocation, parent, or occurrence order. Existing constructor-default probes and the external design are consistent on this boundary.
|
||||
|
||||
Status: sent directly to author/root; the author already intends explicit migration presentation.
|
||||
|
||||
### 4. P2 — A missing-ID preview is a proposal, not a persisted plan for a later process
|
||||
|
||||
Compiling the original missing-ID files twice allocates different UUIDs; the external design's executed probe already establishes this. Prepare the candidate once per invocation, and pass that exact model/bundle/index to saving. Do not regenerate a rename from raw files after displaying its diff or select the target by an ID allocated during the separate raw lint pass.
|
||||
|
||||
Resolve `OLD` by its exact stored alias in the compiled project's full identity namespace, then use `prepare_command(Command::SetAlias(...))` and its `identity_map` with the source updater. That namespace includes project, elements, relations, diagrams, placements, artifacts and traces; display names are not unique identity selectors (`model.rs:1727`, `session.rs:170,607,816`).
|
||||
|
||||
Separate preview and `--write` invocations may produce different *uncommitted* UUIDs. The author proposes disclosing this and deferring persisted plans, which is adequate for this slice. Do not describe `--write` as applying the previous invocation's exact patch. Once stabilized source has been written, repeated preparation must preserve all IDs. Never make name/path/content-derived IDs to conceal the preview difference.
|
||||
|
||||
For already stable source7, alias rename must change only the chosen stored alias and live reference tokens, preserving UUIDs, display names, comments, quoted prose, historical UUID literals, source membership and all other fields. Alias-like text inside a description is not a reference. The core updater already has the appropriate token behavior (`tests/source_project.rs:877`); do not implement a parallel text replacement algorithm.
|
||||
|
||||
Status: author explicitly accepted the per-invocation limitation and semantic updater path.
|
||||
|
||||
### 5. P2 — Journal and warning policy must survive the loader-to-CLI boundary
|
||||
|
||||
The existing loader appends a located `pending_save` diagnostic from the actual private journal path (`source_files.rs:534–547`). Use that diagnostic rather than recreating the journal naming convention. Loading itself is read-only but may compile internally before detecting the journal, so the accurate promise is refusal before the CLI prepares/adopts/writes a candidate, not detection before any load/compile work. The saver independently checks again. A pending journal remains a refusal even when source has no proposed change or contains a syntax error. Reserved stage/completed directories alone are not pending transactions.
|
||||
|
||||
The proposed policy—warnings exit 0, invalid source exit 1, usage exit 2, disk/conflict/journal refusal exit 3—matches core admission for structural errors versus engineering warnings (`session.rs:276`). Missing allocation, an empty scenario, or another allowed partial model must not become unsaveable merely because warnings exist. Conversely, raw invalid UTF-8/syntax, unresolved required references, unsupported versions, and path violations must never be replaced by a canonicalized or partially accepted bundle. Preserve their bytes and any pending journal. If an IO failure is represented as a discovery diagnostic, classify it consistently instead of assuming every diagnostic means exit 1.
|
||||
|
||||
`lint_bundle` may stop at the first syntax/conversion error, and structural-error admission returns errors rather than all engineering warnings (`source_project.rs:580–612`). A JSON result is not an exhaustive list of every defect; the CLI must not claim otherwise. Refactoring argument errors (absent source alias, invalid/reserved/colliding destination alias) likewise require a clear nonzero refusal with zero writes; distinguish command diagnostics from real source positions.
|
||||
|
||||
## Compact independent acceptance matrix
|
||||
|
||||
These are proposed CLI contract tests, not newly executed results. Compare complete `Project` and `SourceBundle` where a candidate exists, plus exact disk bytes for refusal/no-op cases.
|
||||
|
||||
| Case | Exact expected result |
|
||||
| --- | --- |
|
||||
| Raw diagnostics | Include a file with two missing-ID functions, CRLF and Unicode before a warning. Check/lint human and JSON locations select the original declaration; no stabilized warning duplicate or false candidate UUID. Explicit-ID version retains its real object identity. |
|
||||
| Invalid discovery and pending save | Invalid UTF-8 include, missing include, syntax failure, and real captured `pending_save` separately retain disk bytes and useful loader diagnostics. Pending save also refuses an otherwise no-op write; unrelated stage/completed directories do not imply pending recovery. A journal appearing after preparation is refused by saving. |
|
||||
| Missing-ID rename | Resolve an alias across includes, prepare once, and save that exact candidate. Readback is byte-for-byte candidate source and equal model with its exact IDs. A later fresh process can resolve the new alias. Two unwritten preparations may have different fresh IDs; the documented preview limitation is visible. |
|
||||
| Stable alias rename and rejection | Existing UUIDs, duplicate display names, comment/prose containing the old alias, an `@` reference separated by a comment, and a non-element alias target. Only intended alias/live reference tokens change. Unknown `OLD`, invalid/reserved/colliding `NEW` refuse without writing. |
|
||||
| Exact no-op and stale dependencies | Stable source7 stabilization and same-alias rename produce an empty diff and preserve exact bytes/line endings. Prepare the same no-op, edit or delete an unchanged dependency, then call the save boundary: exit 3 with no overwrite of the external bytes. |
|
||||
| Migration and admission | A valid old bundle with all UUIDs explicit still shows its migration and full diff for a real or same-alias rename. The report survives the updater. Source1/2 and invalid old-source drafts stay untouched. A candidate exceeding its budget after insertion is refused without a false raw location or any write. |
|
||||
| Warning-only acceptance | A permitted unallocated function or empty explicit-viewpoint scenario reports warnings and exits 0 for check/preview/write; acceptance does not invent allocations, change viewpoint, or populate steps. Structural errors exit 1 and preserve exact input. |
|
||||
| Detached disk files | A no-longer-reachable `.arcadia` file from a prior Undo remains byte-identical. The explicit entry's reachable source alone defines the model; do not recursively add or delete unrelated files. Ordinary exact no-op compares the reachable bundle while separately proving the retained disk file unchanged. |
|
||||
|
||||
No implementation blocker requires a core redesign. The actionable work is to preserve the distinct raw-diagnostic, compiled-candidate, original-disk-expectation, and migration-report contracts in the adapter. Root should request an independent implementation review after the CLI source freezes; this report is not CLI or release acceptance.
|
||||
|
||||
## Frozen implementation review and independent process evidence
|
||||
|
||||
Codex-9e8r6t `/root/behavior_audit`, 2026-09-11. This section supersedes only the earlier **unreviewed implementation** status. Reviewed the final `crates/oa-cli/src/lib.rs`, `src/main.rs`, all 12 author tests and their terminal logs, the relevant unchanged core parser/updater/saver paths, issue #1 through #170, Wiki `Authoring20260911`, and the empty open-PR list. Read the user's updated remote `AGENTS.md` blob `d93cddacdd45341abb7bbea1d3e3fd4493484d9e`, including coordinated milestone prereleases; root retains release ownership. No production edits, Cargo, rustc compilation, native input, commit, or push was performed for this review.
|
||||
|
||||
**Result: no remaining blocker found in this bounded CLI slice.** One presentation claim was corrected during review. The actual write and diagnostic boundaries preserve the prepared contract; broader text authoring and release acceptance remain separate.
|
||||
|
||||
### Corrected finding: P3 — describe the changed files, not a whole-candidate identity
|
||||
|
||||
The first candidate note said that the invocation's candidate was shown in full, while `Report.changes` contains only changed files. Two projects with different unchanged include bytes can therefore yield identical changed-file output. This is expected for a diff report, but the note overstated what it identified. The author corrected `lib.rs:353` to describe complete proposed **changed-file** texts/diffs and explicitly disclaim a saved plan or unchanged-dependency digest. Root confirmed that no persisted-plan or digest protocol was selected; this review does not require one.
|
||||
|
||||
Unchanged dependencies are still protected when saving. `Prepared` keeps the original `DiskProject` and compiled candidate in private fields; `Prepared::write` always calls `source_files::save_project` (`lib.rs:179–205`). It has no empty-diff bypass and does not reload or substitute expectations. The core checks all captured expectations (`source_files.rs:799–816,884`) and the actual journal before its no-write return at 902. Thus an include absent from `changes` is not absent from the write precondition. The author separately exercised a changed unchanged include and a captured no-op changed after preparation; those are author test results, not independent runtime claims from this review. No cross-process CAS guarantee is added.
|
||||
|
||||
### Other contracts traced to their actual implementation
|
||||
|
||||
- **Raw diagnostics and identities:** loader-only discovery/encoding/path/journal diagnostics survive; raw `lint_bundle` supplies semantic spans; stabilized loader warnings are filtered (`lib.rs:224–270`). Successful raw warnings retain object IDs only when those IDs were authored in the input. Invalid raw text returns before declaration analysis/updating, and later candidate failures explicitly say `candidate`. Independent process probes below exercise the shifted-second-declaration and non-BMP-column counterexamples.
|
||||
- **Semantic alias coverage and source preservation:** target resolution uses the compiled project's complete identity namespace, then `prepare_command(SetAlias)` and `update_bundle_with_identity_map` (`lib.rs:307–338`). It never uses an independently allocated lint UUID as its target. The core covers project, element, relation, diagram, placement, artifact and trace identities. Independent exact-source probes cover all seven, with comments and quoted old aliases deliberately retained.
|
||||
- **Migration/default boundary:** the adapter captures the initial migration report before the semantic updater (`lib.rs:305`), compares original raw text with the final candidate, and has no canonical-export, legacy-parser, inferred-viewpoint or implicit-allocation fallback. Bundle admission remains source3–7; source1/2 are not silently converted. The author's source5 rename test verifies migration retention and original-to-final diff. This review inspected the other admitted-version branches but did not independently execute a full source3–7 constructor matrix.
|
||||
- **Refusal and output:** `--write` consumes the same in-process candidate. Invalid source/commands use exit 1, usage 2, and path/conflict/journal/output failures 3; engineering warnings remain exit 0. A detected pending save is authoritative even when the source is invalid or the proposed write is unchanged. The CLI does not call `recover_project`. Human mode flushes its preview before saving; JSON returns the operation's final report. Failure wording correctly avoids equating `written=false` with guaranteed rollback. Separate missing-ID invocations remain distinct uncommitted proposals, explicitly disclosed.
|
||||
|
||||
### Independently executed frozen-binary probes
|
||||
|
||||
Ran the author's immutable Windows executable `.local/source-cli-20260911/verified/arcadia.exe`, 20,312,064 bytes, SHA-256 `BAD169F8BE78DCFA74B32714569C886DD2132727080684FC2BA618D5822E3559`, with its supplied `libunwind.dll`. The executable hash was checked before and after the run. No compiler was used.
|
||||
|
||||
Command:
|
||||
|
||||
```powershell
|
||||
& harness/experiments/source-cli-independent-20260911/probe.ps1 -Repository 'C:\Users\User\Documents\Git\ARCADIA-Studio'
|
||||
```
|
||||
|
||||
**183 assertions passed across 21 fresh CLI invocations.** The script computes every expected alias edit before the first mutation and writes only isolated `.local/source-cli-independent-20260911/verified/` fixture files. It refuses to overwrite a prior run directory; a new `-RunName` allows a separate repeat. Evidence lives in `harness/experiments/source-cli-independent-20260911/`: `probe.ps1`, `alias-input.arcadia`, `alias-expected-final.arcadia`, and `result.json` with individual assertions, commands, outcome counts and hashes.
|
||||
|
||||
| Independent case | Observed result |
|
||||
| --- | --- |
|
||||
| All seven identity families | Project `alarm_response`, element `maintenance`, relation `alarm_message`, diagram `response_view`, placement `detector_card`, artifact `response_procedure`, and trace `assessment_refinement` each previewed and wrote exactly the precomputed declaration/live-reference token changes. All 40 existing UUIDs, display names, other fields, comments, quoted aliases and source ownership remained byte-exact under the independent expectation. |
|
||||
| Preview, repeat invocation and stable no-op | Every preview left its input unchanged; each separate write of already-stable input matched its predicted preview exactly. Fresh final check succeeded. Stabilization then returned `unchanged`, no diff and `written=false`, preserving bytes and modification time and leaving no unexpected physical file. |
|
||||
| Raw warning position versus stabilized candidate | Two missing-ID SA functions in one CRLF include with Cyrillic, CJK and non-BMP text produced exactly two raw `unallocated_leaf` warnings, without loader-warning duplicates or provisional IDs. The second candidate declaration genuinely shifted after UUID insertion; its reported warning remained at the original input byte offset. Explicit-ID variants retained the exact authored warning IDs. |
|
||||
| Invalid Unicode reference | A non-BMP symbol before an unresolved reference distinguished UTF-8 byte offsets, Unicode scalar columns and UTF-16 units. The write attempt reported the exact original reference position, exit 1, no changes, no ephemeral identity, and preserved the complete input bundle bytes. |
|
||||
|
||||
The first harness launch stopped before starting the CLI because PowerShell's built-in `cli` alias resolved to `Clear-Item` instead of the intended helper. With stop-on-error, the missing literal `check` path stopped that invocation before the isolated source argument was handled. The helper was renamed to `Invoke-ArcadiaProbe`; the final clean run above passed. This was a harness error, not a product failure. The initial isolated input directory is retained; no production or original example file was passed to that mistaken command.
|
||||
|
||||
The author's separate gate is **12/12 tests passed**, 1.88 seconds execution, plus strict all-target Clippy, formatting and matching build. I read their preserved initial 11/12 log: the failed fixture used unsupported block-comment syntax, then was corrected to supported line-comment syntax without a product change. These compiled gates remain attributed to `/root/palette_finish`.
|
||||
|
||||
This independent run proves exact source-byte predictions and process/report behavior for the stated cases. It does **not** claim an independent full-`Project` oracle, adversarial simultaneous-writer proof, exhaustive source-version/default coverage, Linux execution, native GUI usability, packaging or release acceptance. Those limits do not create an additional blocker for this bounded source CLI adapter.
|
||||
|
||||
Reviewed source fingerprints:
|
||||
|
||||
| File | SHA-256 |
|
||||
| --- | --- |
|
||||
| `crates/oa-cli/src/lib.rs` | `97BEBAB63B4A611A1D36AD0C20FE452CBE1985FE3A6FBD39C954603DA11410FC` |
|
||||
| `crates/oa-cli/src/main.rs` | `DE964F9B6C59C0F31C080AFBD3B31D442B07B1B44F6A187A248D3409C45D35FB` |
|
||||
| `crates/oa-cli/tests/source_cli.rs` | `A3A1B856C7A9FD3D80BC8074150A24FBB0EF0E9DDF736C7CEA036725FC3DC2B8` |
|
||||
| `crates/oa-core/src/source_files.rs` | `669491DBDDB8E8E6556161426DFFA1A49AC1C826D31020BFF0956D81A4D84683` |
|
||||
| `crates/oa-core/src/source_project.rs` | `1394370BCC5908873B830C2C11312E6E1611AF41F46C4F43CA0A6BCB713BB466` |
|
||||
Reference in New Issue
Block a user