huskies: merge 619_story_service_common_consolidation_sweep
This commit is contained in:
@@ -0,0 +1,70 @@
|
|||||||
|
//! Pure helpers for pipeline item ID parsing.
|
||||||
|
//!
|
||||||
|
//! Pipeline item IDs share the format `{number}_{type}_{slug}`, e.g.
|
||||||
|
//! `"42_story_foo"`, `"7_bug_bar"`, `"100_refactor_baz"`. The functions here
|
||||||
|
//! extract or validate the leading numeric segment without performing any I/O.
|
||||||
|
|
||||||
|
/// Extract the numeric prefix from a pipeline item ID.
|
||||||
|
///
|
||||||
|
/// Returns the leading digit sequence from IDs like `"42_story_foo"` → `"42"`.
|
||||||
|
/// Returns `None` if the ID has no leading digit sequence.
|
||||||
|
pub fn extract_item_number(item_id: &str) -> Option<&str> {
|
||||||
|
item_id
|
||||||
|
.split('_')
|
||||||
|
.next()
|
||||||
|
.filter(|s| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
/// Return `true` if `item_id` has a valid `{digits}_` prefix format.
|
||||||
|
///
|
||||||
|
/// Valid: `"42_story_foo"`, `"1_bug_bar"`.
|
||||||
|
/// Invalid: `"story_without_number"`, `""`, `"abc_story"`.
|
||||||
|
pub fn has_valid_id_prefix(item_id: &str) -> bool {
|
||||||
|
extract_item_number(item_id).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_item_number_extracts_prefix() {
|
||||||
|
assert_eq!(extract_item_number("42_story_foo"), Some("42"));
|
||||||
|
assert_eq!(extract_item_number("1_bug_bar"), Some("1"));
|
||||||
|
assert_eq!(extract_item_number("100_refactor_baz"), Some("100"));
|
||||||
|
assert_eq!(
|
||||||
|
extract_item_number("261_story_bot_notifications"),
|
||||||
|
Some("261")
|
||||||
|
);
|
||||||
|
assert_eq!(extract_item_number("1_spike_research"), Some("1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_item_number_returns_none_for_no_numeric_prefix() {
|
||||||
|
assert_eq!(extract_item_number("story_without_number"), None);
|
||||||
|
assert_eq!(extract_item_number("abc_story"), None);
|
||||||
|
assert_eq!(extract_item_number("abc_story_thing"), None);
|
||||||
|
assert_eq!(extract_item_number(""), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_item_number_returns_none_for_empty_first_segment() {
|
||||||
|
// Leading underscore: first segment is "".
|
||||||
|
assert_eq!(extract_item_number("_story_thing"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn has_valid_id_prefix_returns_true_for_valid_ids() {
|
||||||
|
assert!(has_valid_id_prefix("42_story_foo"));
|
||||||
|
assert!(has_valid_id_prefix("1_bug_bar"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn has_valid_id_prefix_returns_false_for_invalid_ids() {
|
||||||
|
assert!(!has_valid_id_prefix("story_no_number"));
|
||||||
|
assert!(!has_valid_id_prefix(""));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
//! Shared pure helpers used by multiple service modules.
|
||||||
|
//!
|
||||||
|
//! All sub-modules here are pure (no I/O, no side effects). Any helper that
|
||||||
|
//! duplicates logic across two or more service modules belongs here; anything
|
||||||
|
//! used by only one service stays in that service.
|
||||||
|
pub mod item_id;
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
pub mod agents;
|
pub mod agents;
|
||||||
pub mod anthropic;
|
pub mod anthropic;
|
||||||
pub mod bot_command;
|
pub mod bot_command;
|
||||||
|
pub mod common;
|
||||||
pub mod diagnostics;
|
pub mod diagnostics;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod file_io;
|
pub mod file_io;
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
//! or borrowed string data. They return `(plain_text, html)` pairs suitable
|
//! or borrowed string data. They return `(plain_text, html)` pairs suitable
|
||||||
//! for `ChatTransport::send_message`.
|
//! for `ChatTransport::send_message`.
|
||||||
|
|
||||||
|
use crate::service::common::item_id::extract_item_number;
|
||||||
|
|
||||||
/// Human-readable display name for a pipeline stage directory.
|
/// Human-readable display name for a pipeline stage directory.
|
||||||
pub fn stage_display_name(stage: &str) -> &'static str {
|
pub fn stage_display_name(stage: &str) -> &'static str {
|
||||||
match stage {
|
match stage {
|
||||||
@@ -17,14 +19,6 @@ pub fn stage_display_name(stage: &str) -> &'static str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract the numeric story number from an item ID like `"261_story_slug"`.
|
|
||||||
pub fn extract_story_number(item_id: &str) -> Option<&str> {
|
|
||||||
item_id
|
|
||||||
.split('_')
|
|
||||||
.next()
|
|
||||||
.filter(|s| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Format a stage transition notification message.
|
/// Format a stage transition notification message.
|
||||||
///
|
///
|
||||||
/// Returns `(plain_text, html)` suitable for `ChatTransport::send_message`.
|
/// Returns `(plain_text, html)` suitable for `ChatTransport::send_message`.
|
||||||
@@ -34,7 +28,7 @@ pub fn format_stage_notification(
|
|||||||
from_stage: &str,
|
from_stage: &str,
|
||||||
to_stage: &str,
|
to_stage: &str,
|
||||||
) -> (String, String) {
|
) -> (String, String) {
|
||||||
let number = extract_story_number(item_id).unwrap_or(item_id);
|
let number = extract_item_number(item_id).unwrap_or(item_id);
|
||||||
let name = story_name.unwrap_or(item_id);
|
let name = story_name.unwrap_or(item_id);
|
||||||
|
|
||||||
let prefix = if to_stage == "Done" { "\u{1f389} " } else { "" };
|
let prefix = if to_stage == "Done" { "\u{1f389} " } else { "" };
|
||||||
@@ -53,7 +47,7 @@ pub fn format_error_notification(
|
|||||||
story_name: Option<&str>,
|
story_name: Option<&str>,
|
||||||
reason: &str,
|
reason: &str,
|
||||||
) -> (String, String) {
|
) -> (String, String) {
|
||||||
let number = extract_story_number(item_id).unwrap_or(item_id);
|
let number = extract_item_number(item_id).unwrap_or(item_id);
|
||||||
let name = story_name.unwrap_or(item_id);
|
let name = story_name.unwrap_or(item_id);
|
||||||
|
|
||||||
let plain = format!("\u{274c} #{number} {name} \u{2014} {reason}");
|
let plain = format!("\u{274c} #{number} {name} \u{2014} {reason}");
|
||||||
@@ -69,7 +63,7 @@ pub fn format_blocked_notification(
|
|||||||
story_name: Option<&str>,
|
story_name: Option<&str>,
|
||||||
reason: &str,
|
reason: &str,
|
||||||
) -> (String, String) {
|
) -> (String, String) {
|
||||||
let number = extract_story_number(item_id).unwrap_or(item_id);
|
let number = extract_item_number(item_id).unwrap_or(item_id);
|
||||||
let name = story_name.unwrap_or(item_id);
|
let name = story_name.unwrap_or(item_id);
|
||||||
|
|
||||||
let plain = format!("\u{1f6ab} #{number} {name} \u{2014} BLOCKED: {reason}");
|
let plain = format!("\u{1f6ab} #{number} {name} \u{2014} BLOCKED: {reason}");
|
||||||
@@ -86,7 +80,7 @@ pub fn format_rate_limit_notification(
|
|||||||
story_name: Option<&str>,
|
story_name: Option<&str>,
|
||||||
agent_name: &str,
|
agent_name: &str,
|
||||||
) -> (String, String) {
|
) -> (String, String) {
|
||||||
let number = extract_story_number(item_id).unwrap_or(item_id);
|
let number = extract_item_number(item_id).unwrap_or(item_id);
|
||||||
let name = story_name.unwrap_or(item_id);
|
let name = story_name.unwrap_or(item_id);
|
||||||
|
|
||||||
let plain =
|
let plain =
|
||||||
@@ -121,30 +115,6 @@ mod tests {
|
|||||||
assert_eq!(stage_display_name(""), "Unknown");
|
assert_eq!(stage_display_name(""), "Unknown");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── extract_story_number ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn extract_story_number_parses_numeric_prefix() {
|
|
||||||
assert_eq!(
|
|
||||||
extract_story_number("261_story_bot_notifications"),
|
|
||||||
Some("261")
|
|
||||||
);
|
|
||||||
assert_eq!(extract_story_number("42_bug_fix_thing"), Some("42"));
|
|
||||||
assert_eq!(extract_story_number("1_spike_research"), Some("1"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn extract_story_number_returns_none_for_non_numeric() {
|
|
||||||
assert_eq!(extract_story_number("abc_story_thing"), None);
|
|
||||||
assert_eq!(extract_story_number(""), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn extract_story_number_returns_none_for_empty_first_segment() {
|
|
||||||
// Leading underscore: first segment is ""
|
|
||||||
assert_eq!(extract_story_number("_story_thing"), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── format_stage_notification ─────────────────────────────────────────────
|
// ── format_stage_notification ─────────────────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -3,26 +3,9 @@
|
|||||||
//! These functions reason about story IDs and dependencies without performing
|
//! These functions reason about story IDs and dependencies without performing
|
||||||
//! any I/O. They inform routing decisions in `mod.rs` and the MCP adapter.
|
//! any I/O. They inform routing decisions in `mod.rs` and the MCP adapter.
|
||||||
|
|
||||||
#[allow(dead_code)]
|
pub use crate::service::common::item_id::{
|
||||||
/// Extract the numeric prefix from a story ID (e.g. `"42"` from `"42_story_foo"`).
|
extract_item_number as story_number, has_valid_id_prefix,
|
||||||
///
|
};
|
||||||
/// Returns `None` if the ID has no leading digit sequence.
|
|
||||||
pub fn story_number(story_id: &str) -> Option<&str> {
|
|
||||||
let num = story_id.split('_').next()?;
|
|
||||||
if num.is_empty() || !num.chars().all(|c| c.is_ascii_digit()) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(num)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
/// Return `true` if `story_id` has a valid `{digits}_` prefix format.
|
|
||||||
///
|
|
||||||
/// Valid: `"42_story_foo"`, `"1_bug_bar"`.
|
|
||||||
/// Invalid: `"story_without_number"`, `""`, `"abc_story"`.
|
|
||||||
pub fn has_valid_id_prefix(story_id: &str) -> bool {
|
|
||||||
story_number(story_id).is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Tests ─────────────────────────────────────────────────────────────────────
|
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user