huskies: merge 891
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user