mirror of
https://github.com/ultraworkers/claw-code.git
synced 2026-04-27 06:57:37 +08:00
feat: #142 structured fields in claw init --output-format json
Previously `claw init --output-format json` emitted a valid JSON envelope but packed the entire human-formatted output into a single `message` string. Claw scripts had to substring-match human language to tell `created` from `skipped`. Changes: - Add InitStatus::json_tag() returning machine-stable "created"|"updated"|"skipped" (unlike label() which includes the human " (already exists)" suffix). - Add InitReport::NEXT_STEP constant so claws can read the next-step hint without grepping the message string. - Add InitReport::artifacts_with_status() to partition artifacts by state. - Add InitReport::artifact_json_entries() for the structured artifacts[] array. - Rewrite run_init + init_json_value to emit first-class fields alongside the legacy message string (kept for text consumers): project_path, created[], updated[], skipped[], artifacts[], next_step, message. - Update the slash-command Init dispatch to use the same structured JSON. - Add regression test artifacts_with_status_partitions_fresh_and_idempotent_runs asserting both fresh + idempotent runs produce the right partitioning and that the machine-stable tag is bare 'skipped' not label()'s phrasing. Verified output: - Fresh dir: created[] has 4 entries, skipped[] empty - Idempotent call: created[] empty, skipped[] has 4 entries - project_path, next_step as first-class keys - message preserved verbatim for backward compat Full workspace test green except pre-existing resume_latest flake (unrelated). Closes ROADMAP #142.
This commit is contained in:
parent
7763ca3260
commit
611eed1537
@ -27,6 +27,18 @@ impl InitStatus {
|
|||||||
Self::Skipped => "skipped (already exists)",
|
Self::Skipped => "skipped (already exists)",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Machine-stable identifier for structured output (#142).
|
||||||
|
/// Unlike `label()`, this never changes wording: claws can switch on
|
||||||
|
/// these values without brittle substring matching.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn json_tag(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Created => "created",
|
||||||
|
Self::Updated => "updated",
|
||||||
|
Self::Skipped => "skipped",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@ -58,6 +70,36 @@ impl InitReport {
|
|||||||
lines.push(" Next step Review and tailor the generated guidance".to_string());
|
lines.push(" Next step Review and tailor the generated guidance".to_string());
|
||||||
lines.join("\n")
|
lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Summary constant that claws can embed in JSON output without having
|
||||||
|
/// to read it out of the human-formatted `message` string (#142).
|
||||||
|
pub(crate) const NEXT_STEP: &'static str = "Review and tailor the generated guidance";
|
||||||
|
|
||||||
|
/// Artifact names that ended in the given status. Used to build the
|
||||||
|
/// structured `created[]`/`updated[]`/`skipped[]` arrays for #142.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn artifacts_with_status(&self, status: InitStatus) -> Vec<String> {
|
||||||
|
self.artifacts
|
||||||
|
.iter()
|
||||||
|
.filter(|artifact| artifact.status == status)
|
||||||
|
.map(|artifact| artifact.name.to_string())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Structured artifact list for JSON output (#142). Each entry carries
|
||||||
|
/// `name` and machine-stable `status` tag.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn artifact_json_entries(&self) -> Vec<serde_json::Value> {
|
||||||
|
self.artifacts
|
||||||
|
.iter()
|
||||||
|
.map(|artifact| {
|
||||||
|
serde_json::json!({
|
||||||
|
"name": artifact.name,
|
||||||
|
"status": artifact.status.json_tag(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
@ -333,7 +375,7 @@ fn framework_notes(detection: &RepoDetection) -> Vec<String> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{initialize_repo, render_init_claude_md};
|
use super::{initialize_repo, render_init_claude_md, InitStatus};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
@ -413,6 +455,63 @@ mod tests {
|
|||||||
fs::remove_dir_all(root).expect("cleanup temp dir");
|
fs::remove_dir_all(root).expect("cleanup temp dir");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn artifacts_with_status_partitions_fresh_and_idempotent_runs() {
|
||||||
|
// #142: the structured JSON output needs to be able to partition
|
||||||
|
// artifacts into created/updated/skipped without substring matching
|
||||||
|
// the human-formatted `message` string.
|
||||||
|
let root = temp_dir();
|
||||||
|
fs::create_dir_all(&root).expect("create root");
|
||||||
|
|
||||||
|
let fresh = initialize_repo(&root).expect("fresh init should succeed");
|
||||||
|
let created_names = fresh.artifacts_with_status(InitStatus::Created);
|
||||||
|
assert_eq!(
|
||||||
|
created_names,
|
||||||
|
vec![
|
||||||
|
".claw/".to_string(),
|
||||||
|
".claw.json".to_string(),
|
||||||
|
".gitignore".to_string(),
|
||||||
|
"CLAUDE.md".to_string(),
|
||||||
|
],
|
||||||
|
"fresh init should place all four artifacts in created[]"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
fresh.artifacts_with_status(InitStatus::Skipped).is_empty(),
|
||||||
|
"fresh init should have no skipped artifacts"
|
||||||
|
);
|
||||||
|
|
||||||
|
let second = initialize_repo(&root).expect("second init should succeed");
|
||||||
|
let skipped_names = second.artifacts_with_status(InitStatus::Skipped);
|
||||||
|
assert_eq!(
|
||||||
|
skipped_names,
|
||||||
|
vec![
|
||||||
|
".claw/".to_string(),
|
||||||
|
".claw.json".to_string(),
|
||||||
|
".gitignore".to_string(),
|
||||||
|
"CLAUDE.md".to_string(),
|
||||||
|
],
|
||||||
|
"idempotent init should place all four artifacts in skipped[]"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
second.artifacts_with_status(InitStatus::Created).is_empty(),
|
||||||
|
"idempotent init should have no created artifacts"
|
||||||
|
);
|
||||||
|
|
||||||
|
// artifact_json_entries() uses the machine-stable `json_tag()` which
|
||||||
|
// never changes wording (unlike `label()` which says "skipped (already exists)").
|
||||||
|
let entries = second.artifact_json_entries();
|
||||||
|
assert_eq!(entries.len(), 4);
|
||||||
|
for entry in &entries {
|
||||||
|
let status = entry.get("status").and_then(|v| v.as_str()).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
status, "skipped",
|
||||||
|
"machine status tag should be the bare word 'skipped', not label()'s 'skipped (already exists)'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::remove_dir_all(root).expect("cleanup temp dir");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_init_template_mentions_detected_python_and_nextjs_markers() {
|
fn render_init_template_mentions_detected_python_and_nextjs_markers() {
|
||||||
let root = temp_dir();
|
let root = temp_dir();
|
||||||
|
|||||||
@ -2948,11 +2948,15 @@ fn run_resume_command(
|
|||||||
json: Some(render_memory_json()?),
|
json: Some(render_memory_json()?),
|
||||||
}),
|
}),
|
||||||
SlashCommand::Init => {
|
SlashCommand::Init => {
|
||||||
let message = init_claude_md()?;
|
// #142: run the init once, then render both text + structured JSON
|
||||||
|
// from the same InitReport so both surfaces stay in sync.
|
||||||
|
let cwd = env::current_dir()?;
|
||||||
|
let report = crate::init::initialize_repo(&cwd)?;
|
||||||
|
let message = report.render();
|
||||||
Ok(ResumeCommandOutcome {
|
Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(message.clone()),
|
message: Some(message.clone()),
|
||||||
json: Some(init_json_value(&message)),
|
json: Some(init_json_value(&report, &message)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SlashCommand::Diff => {
|
SlashCommand::Diff => {
|
||||||
@ -5666,20 +5670,31 @@ fn init_claude_md() -> Result<String, Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run_init(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
fn run_init(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let message = init_claude_md()?;
|
let cwd = env::current_dir()?;
|
||||||
|
let report = initialize_repo(&cwd)?;
|
||||||
|
let message = report.render();
|
||||||
match output_format {
|
match output_format {
|
||||||
CliOutputFormat::Text => println!("{message}"),
|
CliOutputFormat::Text => println!("{message}"),
|
||||||
CliOutputFormat::Json => println!(
|
CliOutputFormat::Json => println!(
|
||||||
"{}",
|
"{}",
|
||||||
serde_json::to_string_pretty(&init_json_value(&message))?
|
serde_json::to_string_pretty(&init_json_value(&report, &message))?
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_json_value(message: &str) -> serde_json::Value {
|
/// #142: emit first-class structured fields alongside the legacy `message`
|
||||||
|
/// string so claws can detect per-artifact state without substring matching.
|
||||||
|
fn init_json_value(report: &crate::init::InitReport, message: &str) -> serde_json::Value {
|
||||||
|
use crate::init::InitStatus;
|
||||||
json!({
|
json!({
|
||||||
"kind": "init",
|
"kind": "init",
|
||||||
|
"project_path": report.project_root.display().to_string(),
|
||||||
|
"created": report.artifacts_with_status(InitStatus::Created),
|
||||||
|
"updated": report.artifacts_with_status(InitStatus::Updated),
|
||||||
|
"skipped": report.artifacts_with_status(InitStatus::Skipped),
|
||||||
|
"artifacts": report.artifact_json_entries(),
|
||||||
|
"next_step": crate::init::InitReport::NEXT_STEP,
|
||||||
"message": message,
|
"message": message,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user