huskies: merge 891

This commit is contained in:
dave
2026-05-12 17:03:41 +00:00
parent b76633b79b
commit 148ce37beb
20 changed files with 418 additions and 262 deletions
+2 -2
View File
@@ -48,8 +48,8 @@ pub use state::init;
pub use types::{
ActiveAgentCrdt, ActiveAgentView, AgentThrottleCrdt, AgentThrottleView, CrdtEvent,
GatewayConfigCrdt, GatewayProjectCrdt, GatewayProjectView, MergeJobCrdt, MergeJobView,
NodePresenceCrdt, NodePresenceView, PipelineDoc, PipelineItemCrdt, PipelineItemView,
TestJobCrdt, TestJobView, TokenUsageCrdt, TokenUsageView, subscribe,
NodePresenceCrdt, NodePresenceView, PipelineDoc, PipelineItemCrdt, PipelineItemView, Stage,
TestJobCrdt, TestJobView, TokenUsageCrdt, TokenUsageView, WorkItem, subscribe,
};
pub use write::{
bump_retry_count, migrate_names_from_slugs, migrate_story_ids_to_numeric, name_from_story_id,
+8 -8
View File
@@ -406,10 +406,10 @@ pub fn check_unmet_deps_crdt(story_id: &str) -> Vec<u32> {
Some(i) => i,
None => return Vec::new(),
};
let deps = match item.depends_on {
Some(d) => d,
None => return Vec::new(),
};
let deps = item.depends_on().to_vec();
if deps.is_empty() {
return Vec::new();
}
deps.into_iter()
.filter(|&dep| !dep_is_done_crdt(dep))
.collect()
@@ -425,10 +425,10 @@ pub fn check_archived_deps_crdt(story_id: &str) -> Vec<u32> {
Some(i) => i,
None => return Vec::new(),
};
let deps = match item.depends_on {
Some(d) => d,
None => return Vec::new(),
};
let deps = item.depends_on().to_vec();
if deps.is_empty() {
return Vec::new();
}
deps.into_iter()
.filter(|&dep| dep_is_archived_crdt(dep))
.collect()
+201 -23
View File
@@ -112,31 +112,209 @@ pub struct NodePresenceCrdt {
// ── Read-side view types ─────────────────────────────────────────────
/// A snapshot of a single pipeline item derived from the CRDT document.
#[derive(Clone, Debug)]
pub struct PipelineItemView {
pub story_id: String,
pub stage: String,
pub name: Option<String>,
pub agent: Option<String>,
pub retry_count: Option<i64>,
pub blocked: Option<bool>,
pub depends_on: Option<Vec<u32>>,
/// Node ID of the node that claimed this item (hex-encoded Ed25519 pubkey).
pub claimed_by: Option<String>,
/// Unix timestamp when the item was claimed.
pub claimed_at: Option<f64>,
/// Unix timestamp (seconds) when the item was merged to master.
/// `None` for items that were never in `5_done` or for legacy items.
pub merged_at: Option<f64>,
/// QA mode override from the CRDT register: `"server"`, `"agent"`, or `"human"`.
/// `None` means the register is unset (use project default).
pub qa_mode: Option<String>,
/// Whether the auto-assigner has already spawned a mergemaster session for
/// this item. `None` means the register has never been set (treat as false).
pub mergemaster_attempted: Option<bool>,
/// Pipeline stage inferred from the CRDT `stage` register.
///
/// This is the low-level typed stage for [`WorkItem`] accessors. For rich
/// transition metadata (merge commits, timestamps, etc.) project via
/// `pipeline_state::Stage` instead.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Stage {
/// Story created but not yet triaged (`0_upcoming`).
Upcoming,
/// Waiting for dependencies or auto-assign (`1_backlog`).
Backlog,
/// Actively being coded (`2_current`).
Coding,
/// Blocked awaiting human resolution (`2_blocked`).
Blocked,
/// Coder done; gates running (`3_qa`).
Qa,
/// Gates passed; ready to merge (`4_merge`).
Merge,
/// Merge failed; awaiting intervention (`4_merge_failure`).
MergeFailure,
/// Merged to master (`5_done`).
Done,
/// Out of the active flow (`6_archived`).
Archived,
/// Frozen, awaiting human review (`7_frozen`).
Frozen,
/// An unrecognised stage string — forward-compatible catch-all.
Unknown(String),
}
impl Stage {
/// Parse a stage directory string into the typed enum.
pub fn from_dir(s: &str) -> Self {
match s {
"0_upcoming" => Stage::Upcoming,
"1_backlog" => Stage::Backlog,
"2_current" => Stage::Coding,
"2_blocked" => Stage::Blocked,
"3_qa" => Stage::Qa,
"4_merge" => Stage::Merge,
"4_merge_failure" => Stage::MergeFailure,
"5_done" => Stage::Done,
"6_archived" => Stage::Archived,
"7_frozen" => Stage::Frozen,
other => Stage::Unknown(other.to_string()),
}
}
/// Convert back to the filesystem directory name string.
pub fn as_dir(&self) -> &str {
match self {
Stage::Upcoming => "0_upcoming",
Stage::Backlog => "1_backlog",
Stage::Coding => "2_current",
Stage::Blocked => "2_blocked",
Stage::Qa => "3_qa",
Stage::Merge => "4_merge",
Stage::MergeFailure => "4_merge_failure",
Stage::Done => "5_done",
Stage::Archived => "6_archived",
Stage::Frozen => "7_frozen",
Stage::Unknown(s) => s.as_str(),
}
}
}
/// A typed snapshot of a single pipeline work item derived from the CRDT document.
///
/// Access fields exclusively through the typed accessor methods — raw field access is
/// restricted to the `crdt_state` module tree. All `JsonValue` interpretation is
/// confined to `crdt_state::read::extract_item_view`, so no `JsonValue` escapes into
/// the public API.
///
/// Adding a new field here without also reading it in an accessor produces an
/// `unused field` compiler warning, enforcing the read-side contract at compile time.
#[derive(Clone, Debug)]
pub struct WorkItem {
pub(super) story_id: String,
pub(super) stage: String,
pub(super) name: Option<String>,
pub(super) agent: Option<String>,
pub(super) retry_count: Option<i64>,
pub(super) blocked: Option<bool>,
pub(super) depends_on: Option<Vec<u32>>,
/// Node ID of the node that claimed this item (hex-encoded Ed25519 pubkey).
pub(super) claimed_by: Option<String>,
/// Unix timestamp (seconds) when the claim was written.
pub(super) claimed_at: Option<f64>,
/// Unix timestamp (seconds) when the item was merged to master.
pub(super) merged_at: Option<f64>,
/// QA mode override: `"server"`, `"agent"`, or `"human"`.
pub(super) qa_mode: Option<String>,
/// Whether the auto-assigner has already attempted a mergemaster spawn.
pub(super) mergemaster_attempted: Option<bool>,
}
impl WorkItem {
/// The story identifier (e.g. `"42"` or `"42_story_my_feature"`).
pub fn story_id(&self) -> &str {
&self.story_id
}
/// Pipeline stage as a typed enum.
pub fn stage(&self) -> Stage {
Stage::from_dir(&self.stage)
}
/// Raw stage directory string (e.g. `"2_current"`).
pub fn stage_str(&self) -> &str {
&self.stage
}
/// Human-readable story name, or `None` when unset.
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
/// Agent name pinned to this item, or `None` when unset.
pub fn agent(&self) -> Option<&str> {
self.agent.as_deref()
}
/// Whether the item is blocked. Returns `false` when the register is unset.
pub fn blocked(&self) -> bool {
self.blocked.unwrap_or(false)
}
/// Retry counter. Returns `0` when the register is unset.
pub fn retry_count(&self) -> u32 {
self.retry_count.unwrap_or(0).max(0) as u32
}
/// Dependency story numbers. Returns an empty slice when unset.
pub fn depends_on(&self) -> &[u32] {
self.depends_on.as_deref().unwrap_or(&[])
}
/// Node ID of the current claim holder, or `None` when unclaimed.
pub fn claimed_by(&self) -> Option<&str> {
self.claimed_by.as_deref()
}
/// Unix timestamp (seconds) when the current claim was written, or `None`.
pub fn claimed_at(&self) -> Option<f64> {
self.claimed_at
}
/// Unix timestamp (seconds) when the item was merged to master, or `None`.
pub fn merged_at(&self) -> Option<f64> {
self.merged_at
}
/// QA mode override (`"server"`, `"agent"`, or `"human"`), or `None` when unset.
pub fn qa_mode(&self) -> Option<&str> {
self.qa_mode.as_deref()
}
/// Whether a mergemaster spawn has already been attempted. Returns `false` when unset.
pub fn mergemaster_attempted(&self) -> bool {
self.mergemaster_attempted.unwrap_or(false)
}
/// Construct a `WorkItem` for use in tests outside `crdt_state::*`.
///
/// Within `crdt_state` use a struct literal directly (fields are `pub(super)`).
/// Each field must be supplied — adding a new field to `WorkItem` without updating
/// this constructor produces a compile error, enforcing the read-side contract.
#[allow(clippy::too_many_arguments)]
pub fn for_test(
story_id: impl Into<String>,
stage: impl Into<String>,
name: Option<String>,
agent: Option<String>,
retry_count: Option<i64>,
blocked: Option<bool>,
depends_on: Option<Vec<u32>>,
claimed_by: Option<String>,
claimed_at: Option<f64>,
merged_at: Option<f64>,
qa_mode: Option<String>,
mergemaster_attempted: Option<bool>,
) -> Self {
Self {
story_id: story_id.into(),
stage: stage.into(),
name,
agent,
retry_count,
blocked,
depends_on,
claimed_by,
claimed_at,
merged_at,
qa_mode,
mergemaster_attempted,
}
}
}
/// Backward-compatibility alias; prefer [`WorkItem`].
pub type PipelineItemView = WorkItem;
/// A snapshot of a single node presence entry derived from the CRDT document.
#[derive(Clone, Debug, serde::Serialize)]
pub struct NodePresenceView {