mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-07 00:24:50 +08:00
Finish the remaining roadmap work by making direct CLI JSON output deterministic across the non-interactive surface, restoring the degraded-startup MCP test as a real workspace test, and adding branch-lock plus commit-lineage primitives so downstream lane consumers can distinguish superseded worktree commits from canonical lineage. Constraint: Keep the user-facing config namespace centered on .claw while preserving legacy fallback discovery for compatibility Constraint: Verification needed to stay clean-room and reproducible from the checked-in workspace alone Rejected: Leave the output-format contract implied by ad-hoc smoke runs only | too easy for direct CLI regressions to slip back into prose-only output Rejected: Keep commit provenance as free-form detail text | downstream consumers need structured branch/worktree/supersession metadata Confidence: medium Scope-risk: moderate Directive: Extend the JSON contract through the same direct CLI entrypoints instead of adding one-off serializers on parallel code paths Tested: python .github/scripts/check_doc_source_of_truth.py Tested: cd rust && cargo fmt --all --check Tested: cd rust && cargo test --workspace Tested: cd rust && cargo clippy -p commands -p tools -p rusty-claude-cli --all-targets --no-deps -- -D warnings Not-tested: full cargo clippy --workspace --all-targets -- -D warnings still reports unrelated pre-existing runtime lint debt outside this change set
145 lines
4.3 KiB
Rust
145 lines
4.3 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct BranchLockIntent {
|
|
#[serde(rename = "laneId")]
|
|
pub lane_id: String,
|
|
pub branch: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub worktree: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
pub modules: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct BranchLockCollision {
|
|
pub branch: String,
|
|
pub module: String,
|
|
#[serde(rename = "laneIds")]
|
|
pub lane_ids: Vec<String>,
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn detect_branch_lock_collisions(intents: &[BranchLockIntent]) -> Vec<BranchLockCollision> {
|
|
let mut collisions = Vec::new();
|
|
|
|
for (index, left) in intents.iter().enumerate() {
|
|
for right in &intents[index + 1..] {
|
|
if left.branch != right.branch {
|
|
continue;
|
|
}
|
|
for module in overlapping_modules(&left.modules, &right.modules) {
|
|
collisions.push(BranchLockCollision {
|
|
branch: left.branch.clone(),
|
|
module,
|
|
lane_ids: vec![left.lane_id.clone(), right.lane_id.clone()],
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
collisions.sort_by(|a, b| {
|
|
a.branch
|
|
.cmp(&b.branch)
|
|
.then(a.module.cmp(&b.module))
|
|
.then(a.lane_ids.cmp(&b.lane_ids))
|
|
});
|
|
collisions.dedup();
|
|
collisions
|
|
}
|
|
|
|
fn overlapping_modules(left: &[String], right: &[String]) -> Vec<String> {
|
|
let mut overlaps = Vec::new();
|
|
for left_module in left {
|
|
for right_module in right {
|
|
if modules_overlap(left_module, right_module) {
|
|
overlaps.push(shared_scope(left_module, right_module));
|
|
}
|
|
}
|
|
}
|
|
overlaps.sort();
|
|
overlaps.dedup();
|
|
overlaps
|
|
}
|
|
|
|
fn modules_overlap(left: &str, right: &str) -> bool {
|
|
left == right
|
|
|| left.starts_with(&format!("{right}/"))
|
|
|| right.starts_with(&format!("{left}/"))
|
|
}
|
|
|
|
fn shared_scope(left: &str, right: &str) -> String {
|
|
if left.starts_with(&format!("{right}/")) || left == right {
|
|
right.to_string()
|
|
} else {
|
|
left.to_string()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{detect_branch_lock_collisions, BranchLockIntent};
|
|
|
|
#[test]
|
|
fn detects_same_branch_same_module_collisions() {
|
|
let collisions = detect_branch_lock_collisions(&[
|
|
BranchLockIntent {
|
|
lane_id: "lane-a".to_string(),
|
|
branch: "feature/lock".to_string(),
|
|
worktree: Some("wt-a".to_string()),
|
|
modules: vec!["runtime/mcp".to_string()],
|
|
},
|
|
BranchLockIntent {
|
|
lane_id: "lane-b".to_string(),
|
|
branch: "feature/lock".to_string(),
|
|
worktree: Some("wt-b".to_string()),
|
|
modules: vec!["runtime/mcp".to_string()],
|
|
},
|
|
]);
|
|
|
|
assert_eq!(collisions.len(), 1);
|
|
assert_eq!(collisions[0].branch, "feature/lock");
|
|
assert_eq!(collisions[0].module, "runtime/mcp");
|
|
}
|
|
|
|
#[test]
|
|
fn detects_nested_module_scope_collisions() {
|
|
let collisions = detect_branch_lock_collisions(&[
|
|
BranchLockIntent {
|
|
lane_id: "lane-a".to_string(),
|
|
branch: "feature/lock".to_string(),
|
|
worktree: None,
|
|
modules: vec!["runtime".to_string()],
|
|
},
|
|
BranchLockIntent {
|
|
lane_id: "lane-b".to_string(),
|
|
branch: "feature/lock".to_string(),
|
|
worktree: None,
|
|
modules: vec!["runtime/mcp".to_string()],
|
|
},
|
|
]);
|
|
|
|
assert_eq!(collisions[0].module, "runtime");
|
|
}
|
|
|
|
#[test]
|
|
fn ignores_different_branches() {
|
|
let collisions = detect_branch_lock_collisions(&[
|
|
BranchLockIntent {
|
|
lane_id: "lane-a".to_string(),
|
|
branch: "feature/a".to_string(),
|
|
worktree: None,
|
|
modules: vec!["runtime/mcp".to_string()],
|
|
},
|
|
BranchLockIntent {
|
|
lane_id: "lane-b".to_string(),
|
|
branch: "feature/b".to_string(),
|
|
worktree: None,
|
|
modules: vec!["runtime/mcp".to_string()],
|
|
},
|
|
]);
|
|
|
|
assert!(collisions.is_empty());
|
|
}
|
|
}
|