Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 63 additions & 7 deletions crates/jett_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ fn main() {
agent,
} => {
let start_path = start.unwrap_or_else(|| ".".to_string());
match jett_driver::bundle_project(Path::new(&start_path), Path::new(&output)) {
match jett_driver::bundle_project_detailed(Path::new(&start_path), Path::new(&output)) {
Ok(result) => {
if agent {
print!("{}", render_bundle_agent_output(&result));
Expand Down Expand Up @@ -757,13 +757,35 @@ fn render_bundle_agent_output(result: &jett_driver::BundleResult) -> String {
out
}

fn render_bundle_agent_error(start: &str, output: &str, error: &str) -> String {
format!(
"status: error\nstart: {}\noutput: {}\nerror: {}\n",
fn render_bundle_agent_error(
start: &str,
output: &str,
error: &jett_driver::BundleError,
) -> String {
let mut out = format!(
"status: error\nstart: {}\noutput: {}\n",
escape_toon_scalar(start),
escape_toon_scalar(output),
escape_toon_scalar(error)
)
escape_toon_scalar(output)
);
if let Some(validation) = error.validation_result() {
out.push_str("kind: validation\n");
let diagnostics = jett_diagnostics::toon::render_toon(
&validation.diagnostics,
&validation.source,
&validation.file_path,
);
out.push_str(
diagnostics
.strip_prefix("status: error\n")
.unwrap_or(&diagnostics),
);
} else {
out.push_str(&format!(
"error: {}\n",
escape_toon_scalar(&error.to_string())
));
}
out
}

fn render_query_namespaces_agent_output(result: &jett_driver::NamespaceQueryResult) -> String {
Expand Down Expand Up @@ -1429,6 +1451,40 @@ mod tests {
);
}

#[test]
fn bundle_agent_validation_error_lists_structured_diagnostics() {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time should be after the Unix epoch")
.as_nanos();
let root = std::env::temp_dir().join(format!("jett_cli_bundle_agent_error_{nanos}"));
std::fs::create_dir_all(root.join("src"))
.expect("temporary bundle project should be created");
std::fs::write(root.join("jett.proj"), "name: bundle_error\n")
.expect("project marker should be written");
std::fs::write(
root.join("src/broken.jett"),
"function broken() returns int64:\n return missing\n",
)
.expect("invalid source should be written");
let error = match jett_driver::bundle_project_detailed(&root, Path::new("dist/lib.jett")) {
Ok(_) => panic!("invalid bundle should fail validation"),
Err(error) => error,
};

let rendered = render_bundle_agent_error(".", "dist/lib.jett", &error);
std::fs::remove_dir_all(&root).expect("temporary bundle project should be removed");

assert!(rendered.starts_with(
"status: error\nstart: .\noutput: dist/lib.jett\nkind: validation\nfile: "
));
assert!(rendered.contains(
"diagnostics[1]{code,severity,message,file,line,column,end_line,end_column}:"
));
assert!(rendered.contains("E0200,error,undefined name: `missing`,"));
assert!(!rendered.contains("error: candidate bundle failed validation"));
}

#[test]
fn query_namespaces_agent_output_lists_definition_rows() {
let result = jett_driver::NamespaceQueryResult {
Expand Down
86 changes: 78 additions & 8 deletions crates/jett_driver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2507,6 +2507,53 @@ pub struct BundleResult {
pub files: Vec<BundleFileResult>,
}

/// A bundle failure, optionally retaining candidate-validation diagnostics for
/// structured agent output.
pub struct BundleError {
message: String,
validation: Option<BuildResult>,
}

impl BundleError {
fn from_validation(validation: BuildResult) -> Option<Self> {
if !validation.has_errors {
return None;
}
let errors = error_messages_from_diagnostics(&validation.diagnostics);
Some(Self {
message: format!("candidate bundle failed validation:\n{}", errors.join("\n")),
validation: Some(validation),
})
}

pub fn validation_result(&self) -> Option<&BuildResult> {
self.validation.as_ref()
}
}

impl From<String> for BundleError {
fn from(message: String) -> Self {
Self {
message,
validation: None,
}
}
}

impl std::fmt::Display for BundleError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}

impl std::fmt::Debug for BundleError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
}
}

impl std::error::Error for BundleError {}

/// Parse a .jett file and run all verify blocks, reporting per-block results.
pub fn test_file(path: &Path) -> Result<TestResult, String> {
let source = fs::read_to_string(path)
Expand Down Expand Up @@ -2706,6 +2753,14 @@ pub fn test_project(start_dir: &Path) -> Result<ProjectTestResult, String> {
/// Bundle all project `.jett` files into a single file, then validate it before
/// writing the output path.
pub fn bundle_project(start_dir: &Path, output: &Path) -> Result<BundleResult, String> {
bundle_project_detailed(start_dir, output).map_err(|error| error.to_string())
}

/// Bundle a project while retaining candidate-validation diagnostics on error.
pub fn bundle_project_detailed(
start_dir: &Path,
output: &Path,
) -> Result<BundleResult, BundleError> {
let project_dir = find_project_root(start_dir)?;
let output_abs = if output.is_absolute() {
output.to_path_buf()
Expand All @@ -2724,7 +2779,8 @@ pub fn bundle_project(start_dir: &Path, output: &Path) -> Result<BundleResult, S
return Err(format!(
"no .jett files found in project at {}",
project_dir.display()
));
)
.into());
}

let mut bundled = String::new();
Expand Down Expand Up @@ -2761,11 +2817,9 @@ pub fn bundle_project(start_dir: &Path, output: &Path) -> Result<BundleResult, S

let validation = build_source(&bundled, &output_abs.display().to_string());
if validation.has_errors {
let errors = error_messages_from_diagnostics(&validation.diagnostics);
return Err(format!(
"candidate bundle failed validation:\n{}",
errors.join("\n")
));
return Err(
BundleError::from_validation(validation).expect("candidate validation reported errors")
);
}

if let Some(parent) = output_abs.parent()
Expand Down Expand Up @@ -3271,6 +3325,16 @@ mod tests {
);
}

#[test]
fn bundle_validation_error_rejects_successful_build() {
let validation = build_source(
"function answer() returns int64:\n return 42\n",
"dist/lib.jett",
);

assert!(BundleError::from_validation(validation).is_none());
}

#[test]
fn bundle_project_leaves_output_untouched_when_validation_fails() {
let root = temp_test_dir("jett_driver_bundle_validation_failure");
Expand All @@ -3286,7 +3350,7 @@ mod tests {
let output = root.join("dist").join("lib.jett");
fs::write(&output, "existing bundle\n").expect("existing bundle output should be written");

let error = match bundle_project(&root, &output) {
let error = match bundle_project_detailed(&root, &output) {
Ok(_) => panic!("bundle validation should fail"),
Err(error) => error,
};
Expand All @@ -3296,9 +3360,15 @@ mod tests {
fs::remove_dir_all(&root).expect("temp bundle dir should be removed");

assert!(
error.contains("candidate bundle failed validation"),
error
.to_string()
.contains("candidate bundle failed validation"),
"expected validation failure, got {error}"
);
assert!(
error.validation_result().is_some(),
"expected structured candidate diagnostics"
);
assert_eq!(preserved, "existing bundle\n");
}

Expand Down