mirror of
https://github.com/instructkr/claude-code.git
synced 2026-05-27 16:06:44 +00:00
Unify inventory provenance for generic parsers
Expose a stable source object on agent and skill inventory entries with the same id/label/detail_label keys while preserving skill origin for compatibility.\n\nConstraint: ROADMAP #702 scope only; keep existing skills origin field for compatibility.\nRejected: Rename agent source to origin | would break existing agents consumers and still require per-resource branching during migration.\nConfidence: high\nScope-risk: narrow\nDirective: Future inventory resources should expose provenance through source.id, source.label, and source.detail_label.\nTested: cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo test --manifest-path rust/Cargo.toml -p commands renders_skills_reports_as_json -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --test output_format_contract -- --nocapture; cargo build --manifest-path rust/Cargo.toml --workspace --locked; git diff --check\nNot-tested: full cargo test --manifest-path rust/Cargo.toml --workspace
This commit is contained in:
@@ -4125,9 +4125,17 @@ fn definition_source_id(source: DefinitionSource) -> &'static str {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn definition_source_json(source: DefinitionSource) -> Value {
|
fn definition_source_json(source: DefinitionSource) -> Value {
|
||||||
|
definition_source_json_with_detail(source, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn definition_source_json_with_detail(
|
||||||
|
source: DefinitionSource,
|
||||||
|
detail_label: Option<&'static str>,
|
||||||
|
) -> Value {
|
||||||
json!({
|
json!({
|
||||||
"id": definition_source_id(source),
|
"id": definition_source_id(source),
|
||||||
"label": source.label(),
|
"label": source.label(),
|
||||||
|
"detail_label": detail_label,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4161,7 +4169,7 @@ fn skill_summary_json(skill: &SkillSummary) -> Value {
|
|||||||
json!({
|
json!({
|
||||||
"name": &skill.name,
|
"name": &skill.name,
|
||||||
"description": &skill.description,
|
"description": &skill.description,
|
||||||
"source": definition_source_json(skill.source),
|
"source": definition_source_json_with_detail(skill.source, skill.origin.detail_label()),
|
||||||
"origin": skill_origin_json(skill.origin),
|
"origin": skill_origin_json(skill.origin),
|
||||||
"active": skill.shadowed_by.is_none(),
|
"active": skill.shadowed_by.is_none(),
|
||||||
"shadowed_by": skill.shadowed_by.map(definition_source_json),
|
"shadowed_by": skill.shadowed_by.map(definition_source_json),
|
||||||
@@ -5464,7 +5472,18 @@ mod tests {
|
|||||||
assert_eq!(report["summary"]["shadowed"], 1);
|
assert_eq!(report["summary"]["shadowed"], 1);
|
||||||
assert_eq!(report["skills"][0]["name"], "plan");
|
assert_eq!(report["skills"][0]["name"], "plan");
|
||||||
assert_eq!(report["skills"][0]["source"]["id"], "project_claw");
|
assert_eq!(report["skills"][0]["source"]["id"], "project_claw");
|
||||||
|
assert_eq!(report["skills"][0]["source"]["label"], "Project roots");
|
||||||
|
assert_eq!(
|
||||||
|
report["skills"][0]["source"]["detail_label"],
|
||||||
|
serde_json::Value::Null
|
||||||
|
);
|
||||||
assert_eq!(report["skills"][1]["name"], "deploy");
|
assert_eq!(report["skills"][1]["name"], "deploy");
|
||||||
|
assert_eq!(report["skills"][1]["source"]["id"], "project_claw");
|
||||||
|
assert_eq!(report["skills"][1]["source"]["label"], "Project roots");
|
||||||
|
assert_eq!(
|
||||||
|
report["skills"][1]["source"]["detail_label"],
|
||||||
|
"legacy /commands"
|
||||||
|
);
|
||||||
assert_eq!(report["skills"][1]["origin"]["id"], "legacy_commands_dir");
|
assert_eq!(report["skills"][1]["origin"]["id"], "legacy_commands_dir");
|
||||||
assert_eq!(report["skills"][3]["shadowed_by"]["id"], "project_claw");
|
assert_eq!(report["skills"][3]["shadowed_by"]["id"], "project_claw");
|
||||||
|
|
||||||
|
|||||||
@@ -359,6 +359,8 @@ fn agents_command_emits_structured_agent_entries_when_requested() {
|
|||||||
assert_eq!(parsed["summary"]["shadowed"], 1);
|
assert_eq!(parsed["summary"]["shadowed"], 1);
|
||||||
assert_eq!(parsed["agents"][0]["name"], "planner");
|
assert_eq!(parsed["agents"][0]["name"], "planner");
|
||||||
assert_eq!(parsed["agents"][0]["source"]["id"], "project_claw");
|
assert_eq!(parsed["agents"][0]["source"]["id"], "project_claw");
|
||||||
|
assert_eq!(parsed["agents"][0]["source"]["label"], "Project roots");
|
||||||
|
assert_eq!(parsed["agents"][0]["source"]["detail_label"], Value::Null);
|
||||||
assert_eq!(parsed["agents"][0]["active"], true);
|
assert_eq!(parsed["agents"][0]["active"], true);
|
||||||
assert_eq!(parsed["agents"][1]["name"], "verifier");
|
assert_eq!(parsed["agents"][1]["name"], "verifier");
|
||||||
assert_eq!(parsed["agents"][2]["name"], "planner");
|
assert_eq!(parsed["agents"][2]["name"], "planner");
|
||||||
@@ -366,6 +368,83 @@ fn agents_command_emits_structured_agent_entries_when_requested() {
|
|||||||
assert_eq!(parsed["agents"][2]["shadowed_by"]["id"], "project_claw");
|
assert_eq!(parsed["agents"][2]["shadowed_by"]["id"], "project_claw");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn agents_and_skills_inventory_share_source_schema_702() {
|
||||||
|
let root = unique_temp_dir("inventory-source-schema-702");
|
||||||
|
let workspace = root.join("workspace");
|
||||||
|
let project_agents = workspace.join(".codex").join("agents");
|
||||||
|
let project_skills = workspace.join(".codex").join("skills");
|
||||||
|
let legacy_commands = workspace.join(".claude").join("commands");
|
||||||
|
let home = root.join("home");
|
||||||
|
let isolated_config = root.join("config-home");
|
||||||
|
let isolated_codex = root.join("codex-home");
|
||||||
|
fs::create_dir_all(&workspace).expect("workspace should exist");
|
||||||
|
fs::create_dir_all(&home).expect("home should exist");
|
||||||
|
|
||||||
|
write_agent(
|
||||||
|
&project_agents,
|
||||||
|
"planner",
|
||||||
|
"Project planner",
|
||||||
|
"gpt-5.4",
|
||||||
|
"medium",
|
||||||
|
);
|
||||||
|
write_skill(&project_skills, "plan", "Project planning guidance");
|
||||||
|
write_legacy_command(&legacy_commands, "deploy", "Legacy deployment guidance");
|
||||||
|
|
||||||
|
let envs = [
|
||||||
|
("HOME", home.to_str().expect("utf8 home")),
|
||||||
|
(
|
||||||
|
"CLAW_CONFIG_HOME",
|
||||||
|
isolated_config.to_str().expect("utf8 config home"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"CODEX_HOME",
|
||||||
|
isolated_codex.to_str().expect("utf8 codex home"),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
let agents =
|
||||||
|
assert_json_command_with_env(&workspace, &["--output-format", "json", "agents"], &envs);
|
||||||
|
let skills =
|
||||||
|
assert_json_command_with_env(&workspace, &["--output-format", "json", "skills"], &envs);
|
||||||
|
|
||||||
|
let agent_source = &agents["agents"][0]["source"];
|
||||||
|
let skill_source = &skills["skills"][0]["source"];
|
||||||
|
for source in [agent_source, skill_source] {
|
||||||
|
assert!(
|
||||||
|
source.get("id").is_some(),
|
||||||
|
"inventory source must expose id: {source}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
source.get("label").is_some(),
|
||||||
|
"inventory source must expose label: {source}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
source.get("detail_label").is_some(),
|
||||||
|
"inventory source must expose detail_label for a stable cross-resource path: {source}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(agent_source["id"], "project_claw");
|
||||||
|
assert_eq!(agent_source["label"], "Project roots");
|
||||||
|
assert_eq!(agent_source["detail_label"], Value::Null);
|
||||||
|
assert_eq!(skill_source["id"], "project_claw");
|
||||||
|
assert_eq!(skill_source["label"], "Project roots");
|
||||||
|
assert_eq!(skill_source["detail_label"], Value::Null);
|
||||||
|
|
||||||
|
let legacy_skill = skills["skills"]
|
||||||
|
.as_array()
|
||||||
|
.expect("skills array")
|
||||||
|
.iter()
|
||||||
|
.find(|skill| skill["name"] == "deploy")
|
||||||
|
.expect("legacy command skill should be listed");
|
||||||
|
assert_eq!(legacy_skill["source"]["id"], "project_claw");
|
||||||
|
assert_eq!(legacy_skill["source"]["label"], "Project roots");
|
||||||
|
assert_eq!(legacy_skill["source"]["detail_label"], "legacy /commands");
|
||||||
|
assert_eq!(
|
||||||
|
legacy_skill["origin"]["id"], "legacy_commands_dir",
|
||||||
|
"legacy origin stays for compatibility while generic parsers use source"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bootstrap_and_system_prompt_emit_json_when_requested() {
|
fn bootstrap_and_system_prompt_emit_json_when_requested() {
|
||||||
let root = unique_temp_dir("bootstrap-system-prompt-json");
|
let root = unique_temp_dir("bootstrap-system-prompt-json");
|
||||||
@@ -905,6 +984,25 @@ fn write_agent(root: &Path, name: &str, description: &str, model: &str, reasonin
|
|||||||
.expect("agent fixture should write");
|
.expect("agent fixture should write");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_skill(root: &Path, name: &str, description: &str) {
|
||||||
|
let skill_root = root.join(name);
|
||||||
|
fs::create_dir_all(&skill_root).expect("skill root should exist");
|
||||||
|
fs::write(
|
||||||
|
skill_root.join("SKILL.md"),
|
||||||
|
format!("---\nname: {name}\ndescription: {description}\n---\n\n# {name}\n"),
|
||||||
|
)
|
||||||
|
.expect("skill fixture should write");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_legacy_command(root: &Path, name: &str, description: &str) {
|
||||||
|
fs::create_dir_all(root).expect("legacy command root should exist");
|
||||||
|
fs::write(
|
||||||
|
root.join(format!("{name}.md")),
|
||||||
|
format!("---\nname: {name}\ndescription: {description}\n---\n\n# {name}\n"),
|
||||||
|
)
|
||||||
|
.expect("legacy command fixture should write");
|
||||||
|
}
|
||||||
|
|
||||||
fn unique_temp_dir(label: &str) -> PathBuf {
|
fn unique_temp_dir(label: &str) -> PathBuf {
|
||||||
let millis = SystemTime::now()
|
let millis = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
|
|||||||
Reference in New Issue
Block a user