Add explicit read-only mixed source8 CLI checking

This commit is contained in:
2026-09-24 21:57:06 -07:00
parent bab6f96a5b
commit f83f3bf45b
5 changed files with 663 additions and 30 deletions
+66 -11
View File
@@ -170,6 +170,47 @@ fn disk_error(report: &mut Report, error: SourceFileError) {
report.fail(3, code, error.to_string());
}
/// Validate captured mixed source8 bytes without creating an editable candidate.
pub fn check_mixed(entry: &Path) -> Report {
let mut report = Report::empty(entry, "check-mixed");
let snapshot = match source_files::load_mixed_source(entry, LIMITS) {
Ok(snapshot) => snapshot,
Err(error) => {
disk_error(&mut report, error);
return report;
}
};
let diagnostics = if snapshot.diagnostics().is_empty() {
match language::check_mixed_bundle(snapshot.bundle(), LIMITS) {
Ok(checked) => checked.warnings().to_vec(),
Err(errors) => errors,
}
} else {
snapshot.diagnostics().to_vec()
};
if diagnostics
.iter()
.any(|d| d.diagnostic.severity == Severity::Error)
{
report.status = "invalid".into();
report.exit_code = if diagnostics.iter().any(|d| {
matches!(
d.diagnostic.code.as_str(),
"pending_save" | "missing_include" | "invalid_path"
)
}) {
3
} else {
1
};
}
report.diagnostics = diagnostics
.into_iter()
.map(|d| located(d, "input"))
.collect();
report
}
/// A candidate owns its original expectations and is consumed by saving.
/// Callers cannot replace its source/model/expectations after preparation.
pub struct Prepared {
@@ -406,14 +447,19 @@ fn unified_diff(path: &str, before: &str, after: &str) -> String {
out
}
/// Read-only mixed checking cannot acquire ordinary edit/write authority.
pub enum Dispatch {
Editable { operation: Operation, write: bool },
CheckMixed,
}
pub struct Arguments {
pub entry: PathBuf,
pub operation: Operation,
pub write: bool,
pub dispatch: Dispatch,
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 const HELP: &str = "arcadia check ROOT [--json]\narcadia lint ROOT [--json]\narcadia check-mixed 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.\ncheck-mixed requires source8 and explicit UUIDs; it never migrates, stabilizes or writes.\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();
@@ -433,19 +479,28 @@ pub fn arguments(args: &[std::ffi::OsString]) -> Result<Arguments, String> {
}
}
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(),
let dispatch = match (command, positional.len()) {
("check-mixed", 2) if !write => Dispatch::CheckMixed,
("check" | "lint", 2) if !write => Dispatch::Editable {
operation: Operation::Check,
write,
},
("stabilize", 2) => Dispatch::Editable {
operation: Operation::Stabilize,
write,
},
("rename-alias", 4) => Dispatch::Editable {
operation: 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(),
},
write,
},
_ => return Err(HELP.into()),
};
Ok(Arguments {
entry: PathBuf::from(positional[1]),
operation,
write,
dispatch,
json,
})
}
+30 -19
View File
@@ -1,4 +1,4 @@
use oa_cli::{Report, arguments, prepare};
use oa_cli::{Dispatch, Report, arguments, check_mixed, prepare};
use std::{
io::{self, Write},
path::Path,
@@ -26,13 +26,21 @@ fn human_report(report: &Report, show_diff: bool) -> io::Result<()> {
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 report.operation == "check-mixed" {
writeln!(
err,
"{}: mixed source8 check; no files changed",
report.status
)?;
} else {
writeln!(
err,
"{}: {} file changes; {} identities to stabilize",
report.status,
report.changes.len(),
report.identities_to_stabilize
)?;
}
if let Some(migration) = &report.migration {
writeln!(
err,
@@ -67,19 +75,22 @@ fn run() -> io::Result<u8> {
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;
Ok(args) => match args.dispatch {
Dispatch::CheckMixed => check_mixed(&args.entry),
Dispatch::Editable { operation, write } => match prepare(&args.entry, operation) {
Err(report) => *report,
Ok(prepared) => {
if write {
if !args.json {
human_report(prepared.report(), true)?;
displayed = true;
}
prepared.write()
} else {
prepared.into_report()
}
prepared.write()
} else {
prepared.into_report()
}
}
},
},
};
if json {