mirror of
https://github.com/ultraworkers/claw-code.git
synced 2026-04-24 13:08:11 +08:00
ROADMAP #4.44.5: Ship/provenance events — implement §4.44.5
Adds structured ship provenance surface to eliminate delivery-path opacity: New lane events: - ship.prepared — intent to ship established - ship.commits_selected — commit range locked - ship.merged — merge completed with provenance - ship.pushed_main — delivery to main confirmed ShipProvenance struct carries: - source_branch, base_commit - commit_count, commit_range - merge_method (direct_push/fast_forward/merge_commit/squash_merge/rebase_merge) - actor, pr_number Constructor methods added to LaneEvent for all four ship events. Tests: - Wire value serialization for ship events - Round-trip deserialization - Canonical event name coverage Runtime: 465 tests pass ROADMAP updated with IMPLEMENTED status This closes the gap where 56 commits pushed to main had no structured provenance trail — now emits first-class events for clawhip consumption.
This commit is contained in:
parent
b0b579ebe9
commit
8a8ca8a355
16
ROADMAP.md
16
ROADMAP.md
@ -711,7 +711,21 @@ Acceptance:
|
|||||||
- token-risk preflight becomes operational guidance, not just warning text
|
- token-risk preflight becomes operational guidance, not just warning text
|
||||||
- first-run users stop getting stuck between diagnosis and manual cleanup
|
- first-run users stop getting stuck between diagnosis and manual cleanup
|
||||||
|
|
||||||
### 4.44.5. Ship/provenance opacity — branch → merge → main-push boundary not first-class
|
### 4.44.5. Ship/provenance opacity — IMPLEMENTED 2026-04-20
|
||||||
|
|
||||||
|
**Status:** Events implemented in `lane_events.rs`. Surface now emits structured ship provenance.
|
||||||
|
|
||||||
|
When dogfood work lands on `main`, the delivery path (scoped branch → PR → merge → push vs direct push) and the exact commit set shipped are not surfaced as first-class events. This makes it too easy to lose the boundary between "dogfood fix landed", "what exact commits shipped", and "what review/merge path was actually used." The 56-commit push during 2026-04-20 dogfood (#122/#127/#129/#130/#131/#132) exhibited this gap: work started as scoped pinpoint branches, then collapsed into a direct `origin/main` push with no structured provenance trail.
|
||||||
|
|
||||||
|
**Implemented behavior:**
|
||||||
|
- `ship.prepared` event — intent to ship established
|
||||||
|
- `ship.commits_selected` event — commit range locked
|
||||||
|
- `ship.merged` event — merge completed with metadata
|
||||||
|
- `ship.pushed_main` event — delivery to main confirmed
|
||||||
|
- All carry `ShipProvenance { source_branch, base_commit, commit_count, commit_range, merge_method, actor, pr_number }`
|
||||||
|
- `ShipMergeMethod` enum: direct_push, fast_forward, merge_commit, squash_merge, rebase_merge
|
||||||
|
|
||||||
|
Required behavior:
|
||||||
|
|
||||||
When dogfood work lands on `main`, the delivery path (scoped branch → PR → merge → push vs direct push) and the exact commit set shipped are not surfaced as first-class events. This makes it too easy to lose the boundary between "dogfood fix landed", "what exact commits shipped", and "what review/merge path was actually used." The 56-commit push during 2026-04-20 dogfood (#122/#127/#129/#130/#131/#132) exhibited this gap: work started as scoped pinpoint branches, then collapsed into a direct `origin/main` push with no structured provenance trail.
|
When dogfood work lands on `main`, the delivery path (scoped branch → PR → merge → push vs direct push) and the exact commit set shipped are not surfaced as first-class events. This makes it too easy to lose the boundary between "dogfood fix landed", "what exact commits shipped", and "what review/merge path was actually used." The 56-commit push during 2026-04-20 dogfood (#122/#127/#129/#130/#131/#132) exhibited this gap: work started as scoped pinpoint branches, then collapsed into a direct `origin/main` push with no structured provenance trail.
|
||||||
|
|
||||||
|
|||||||
@ -38,6 +38,15 @@ pub enum LaneEventName {
|
|||||||
BranchStaleAgainstMain,
|
BranchStaleAgainstMain,
|
||||||
#[serde(rename = "branch.workspace_mismatch")]
|
#[serde(rename = "branch.workspace_mismatch")]
|
||||||
BranchWorkspaceMismatch,
|
BranchWorkspaceMismatch,
|
||||||
|
/// Ship/provenance events — §4.44.5
|
||||||
|
#[serde(rename = "ship.prepared")]
|
||||||
|
ShipPrepared,
|
||||||
|
#[serde(rename = "ship.commits_selected")]
|
||||||
|
ShipCommitsSelected,
|
||||||
|
#[serde(rename = "ship.merged")]
|
||||||
|
ShipMerged,
|
||||||
|
#[serde(rename = "ship.pushed_main")]
|
||||||
|
ShipPushedMain,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@ -424,6 +433,29 @@ pub struct LaneCommitProvenance {
|
|||||||
pub lineage: Vec<String>,
|
pub lineage: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ship/provenance metadata — §4.44.5
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct ShipProvenance {
|
||||||
|
pub source_branch: String,
|
||||||
|
pub base_commit: String,
|
||||||
|
pub commit_count: u32,
|
||||||
|
pub commit_range: String,
|
||||||
|
pub merge_method: ShipMergeMethod,
|
||||||
|
pub actor: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub pr_number: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ShipMergeMethod {
|
||||||
|
DirectPush,
|
||||||
|
FastForward,
|
||||||
|
MergeCommit,
|
||||||
|
SquashMerge,
|
||||||
|
RebaseMerge,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct LaneEvent {
|
pub struct LaneEvent {
|
||||||
pub event: LaneEventName,
|
pub event: LaneEventName,
|
||||||
@ -527,6 +559,38 @@ impl LaneEvent {
|
|||||||
event
|
event
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ship prepared — §4.44.5
|
||||||
|
#[must_use]
|
||||||
|
pub fn ship_prepared(emitted_at: impl Into<String>, provenance: &ShipProvenance) -> Self {
|
||||||
|
Self::new(LaneEventName::ShipPrepared, LaneEventStatus::Ready, emitted_at)
|
||||||
|
.with_data(serde_json::to_value(provenance).expect("ship provenance should serialize"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ship commits selected — §4.44.5
|
||||||
|
#[must_use]
|
||||||
|
pub fn ship_commits_selected(
|
||||||
|
emitted_at: impl Into<String>,
|
||||||
|
commit_count: u32,
|
||||||
|
commit_range: impl Into<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self::new(LaneEventName::ShipCommitsSelected, LaneEventStatus::Ready, emitted_at)
|
||||||
|
.with_detail(format!("{} commits: {}", commit_count, commit_range.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ship merged — §4.44.5
|
||||||
|
#[must_use]
|
||||||
|
pub fn ship_merged(emitted_at: impl Into<String>, provenance: &ShipProvenance) -> Self {
|
||||||
|
Self::new(LaneEventName::ShipMerged, LaneEventStatus::Completed, emitted_at)
|
||||||
|
.with_data(serde_json::to_value(provenance).expect("ship provenance should serialize"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ship pushed to main — §4.44.5
|
||||||
|
#[must_use]
|
||||||
|
pub fn ship_pushed_main(emitted_at: impl Into<String>, provenance: &ShipProvenance) -> Self {
|
||||||
|
Self::new(LaneEventName::ShipPushedMain, LaneEventStatus::Completed, emitted_at)
|
||||||
|
.with_data(serde_json::to_value(provenance).expect("ship provenance should serialize"))
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_failure_class(mut self, failure_class: LaneFailureClass) -> Self {
|
pub fn with_failure_class(mut self, failure_class: LaneFailureClass) -> Self {
|
||||||
self.failure_class = Some(failure_class);
|
self.failure_class = Some(failure_class);
|
||||||
@ -600,7 +664,8 @@ mod tests {
|
|||||||
compute_event_fingerprint, dedupe_superseded_commit_events, dedupe_terminal_events,
|
compute_event_fingerprint, dedupe_superseded_commit_events, dedupe_terminal_events,
|
||||||
is_terminal_event, BlockedSubphase, EventProvenance, LaneCommitProvenance, LaneEvent,
|
is_terminal_event, BlockedSubphase, EventProvenance, LaneCommitProvenance, LaneEvent,
|
||||||
LaneEventBlocker, LaneEventBuilder, LaneEventMetadata, LaneEventName, LaneEventStatus,
|
LaneEventBlocker, LaneEventBuilder, LaneEventMetadata, LaneEventName, LaneEventStatus,
|
||||||
LaneFailureClass, LaneOwnership, SessionIdentity, WatcherAction,
|
LaneFailureClass, LaneOwnership, SessionIdentity, ShipMergeMethod, ShipProvenance,
|
||||||
|
WatcherAction,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -629,6 +694,10 @@ mod tests {
|
|||||||
LaneEventName::BranchWorkspaceMismatch,
|
LaneEventName::BranchWorkspaceMismatch,
|
||||||
"branch.workspace_mismatch",
|
"branch.workspace_mismatch",
|
||||||
),
|
),
|
||||||
|
(LaneEventName::ShipPrepared, "ship.prepared"),
|
||||||
|
(LaneEventName::ShipCommitsSelected, "ship.commits_selected"),
|
||||||
|
(LaneEventName::ShipMerged, "ship.merged"),
|
||||||
|
(LaneEventName::ShipPushedMain, "ship.pushed_main"),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (event, expected) in cases {
|
for (event, expected) in cases {
|
||||||
@ -718,6 +787,34 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ship_provenance_events_serialize_to_expected_wire_values() {
|
||||||
|
let provenance = ShipProvenance {
|
||||||
|
source_branch: "feature/provenance".to_string(),
|
||||||
|
base_commit: "dd73962".to_string(),
|
||||||
|
commit_count: 6,
|
||||||
|
commit_range: "dd73962..c956f78".to_string(),
|
||||||
|
merge_method: ShipMergeMethod::DirectPush,
|
||||||
|
actor: "Jobdori".to_string(),
|
||||||
|
pr_number: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let prepared = LaneEvent::ship_prepared("2026-04-20T14:30:00Z", &provenance);
|
||||||
|
let prepared_json = serde_json::to_value(&prepared).expect("ship event should serialize");
|
||||||
|
assert_eq!(prepared_json["event"], "ship.prepared");
|
||||||
|
assert_eq!(prepared_json["data"]["commit_count"], 6);
|
||||||
|
assert_eq!(prepared_json["data"]["source_branch"], "feature/provenance");
|
||||||
|
|
||||||
|
let pushed = LaneEvent::ship_pushed_main("2026-04-20T14:35:00Z", &provenance);
|
||||||
|
let pushed_json = serde_json::to_value(&pushed).expect("ship event should serialize");
|
||||||
|
assert_eq!(pushed_json["event"], "ship.pushed_main");
|
||||||
|
assert_eq!(pushed_json["data"]["merge_method"], "direct_push");
|
||||||
|
|
||||||
|
let round_trip: LaneEvent =
|
||||||
|
serde_json::from_value(pushed_json).expect("ship event should deserialize");
|
||||||
|
assert_eq!(round_trip.event, LaneEventName::ShipPushedMain);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn commit_events_can_carry_worktree_and_supersession_metadata() {
|
fn commit_events_can_carry_worktree_and_supersession_metadata() {
|
||||||
let event = LaneEvent::commit_created(
|
let event = LaneEvent::commit_created(
|
||||||
|
|||||||
@ -86,7 +86,8 @@ pub use lane_events::{
|
|||||||
compute_event_fingerprint, dedupe_superseded_commit_events, dedupe_terminal_events,
|
compute_event_fingerprint, dedupe_superseded_commit_events, dedupe_terminal_events,
|
||||||
is_terminal_event, BlockedSubphase, EventProvenance, LaneCommitProvenance, LaneEvent,
|
is_terminal_event, BlockedSubphase, EventProvenance, LaneCommitProvenance, LaneEvent,
|
||||||
LaneEventBlocker, LaneEventBuilder, LaneEventMetadata, LaneEventName, LaneEventStatus,
|
LaneEventBlocker, LaneEventBuilder, LaneEventMetadata, LaneEventName, LaneEventStatus,
|
||||||
LaneFailureClass, LaneOwnership, SessionIdentity, WatcherAction,
|
LaneFailureClass, LaneOwnership, SessionIdentity, ShipMergeMethod, ShipProvenance,
|
||||||
|
WatcherAction,
|
||||||
};
|
};
|
||||||
pub use mcp::{
|
pub use mcp::{
|
||||||
mcp_server_signature, mcp_tool_name, mcp_tool_prefix, normalize_name_for_mcp,
|
mcp_server_signature, mcp_tool_name, mcp_tool_prefix, normalize_name_for_mcp,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user