Story 60: Status-Based Directory Layout with work/ pipeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -692,48 +692,41 @@ pub fn git_stage_and_commit(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Determine the work item type from its ID.
|
||||
/// Returns "bug" for `bug-*` IDs, "spike" for `spike-*` IDs, "story" otherwise.
|
||||
/// Determine the work item type from its ID (new naming: `{N}_{type}_{slug}`).
|
||||
/// Returns "bug", "spike", or "story".
|
||||
fn item_type_from_id(item_id: &str) -> &'static str {
|
||||
if item_id.starts_with("bug-") {
|
||||
// New format: {digits}_{type}_{slug}
|
||||
let after_num = item_id.trim_start_matches(|c: char| c.is_ascii_digit());
|
||||
if after_num.starts_with("_bug_") {
|
||||
"bug"
|
||||
} else if item_id.starts_with("spike-") {
|
||||
} else if after_num.starts_with("_spike_") {
|
||||
"spike"
|
||||
} else {
|
||||
"story"
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the source directory path for a work item based on its type.
|
||||
fn item_source_dir(project_root: &Path, item_id: &str) -> PathBuf {
|
||||
let sk = project_root.join(".story_kit");
|
||||
match item_type_from_id(item_id) {
|
||||
"bug" => sk.join("bugs"),
|
||||
"spike" => sk.join("spikes"),
|
||||
_ => sk.join("stories").join("upcoming"),
|
||||
}
|
||||
/// Return the source directory path for a work item (always work/1_upcoming/).
|
||||
fn item_source_dir(project_root: &Path, _item_id: &str) -> PathBuf {
|
||||
project_root.join(".story_kit").join("work").join("1_upcoming")
|
||||
}
|
||||
|
||||
/// Return the archive directory path for a work item based on its type.
|
||||
fn item_archive_dir(project_root: &Path, item_id: &str) -> PathBuf {
|
||||
let sk = project_root.join(".story_kit");
|
||||
match item_type_from_id(item_id) {
|
||||
"bug" => sk.join("bugs").join("archive"),
|
||||
"spike" => sk.join("spikes").join("archive"),
|
||||
_ => sk.join("stories").join("archived"),
|
||||
}
|
||||
/// Return the archive directory path for a work item (always work/5_archived/).
|
||||
fn item_archive_dir(project_root: &Path, _item_id: &str) -> PathBuf {
|
||||
project_root.join(".story_kit").join("work").join("5_archived")
|
||||
}
|
||||
|
||||
/// Move a work item (story, bug, or spike) to the unified `.story_kit/current/` directory.
|
||||
/// Move a work item (story, bug, or spike) from `work/1_upcoming/` to `work/2_current/`.
|
||||
///
|
||||
/// Idempotent: if the item is already in `current/`, returns Ok without committing.
|
||||
/// If the item is not found in its source directory, logs a warning and returns Ok.
|
||||
/// Idempotent: if the item is already in `2_current/`, returns Ok without committing.
|
||||
/// If the item is not found in `1_upcoming/`, logs a warning and returns Ok.
|
||||
pub fn move_story_to_current(project_root: &Path, story_id: &str) -> Result<(), String> {
|
||||
let current_dir = project_root.join(".story_kit").join("current");
|
||||
let sk = project_root.join(".story_kit").join("work");
|
||||
let current_dir = sk.join("2_current");
|
||||
let current_path = current_dir.join(format!("{story_id}.md"));
|
||||
|
||||
if current_path.exists() {
|
||||
// Already in current/ — idempotent, nothing to do.
|
||||
// Already in 2_current/ — idempotent, nothing to do.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -742,20 +735,20 @@ pub fn move_story_to_current(project_root: &Path, story_id: &str) -> Result<(),
|
||||
|
||||
if !source_path.exists() {
|
||||
eprintln!(
|
||||
"[lifecycle] Work item '{story_id}' not found in {}; skipping move to current/",
|
||||
"[lifecycle] Work item '{story_id}' not found in {}; skipping move to 2_current/",
|
||||
source_dir.display()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
std::fs::create_dir_all(¤t_dir)
|
||||
.map_err(|e| format!("Failed to create .story_kit/current/ directory: {e}"))?;
|
||||
.map_err(|e| format!("Failed to create work/2_current/ directory: {e}"))?;
|
||||
|
||||
std::fs::rename(&source_path, ¤t_path)
|
||||
.map_err(|e| format!("Failed to move '{story_id}' to current/: {e}"))?;
|
||||
.map_err(|e| format!("Failed to move '{story_id}' to 2_current/: {e}"))?;
|
||||
|
||||
eprintln!(
|
||||
"[lifecycle] Moved '{story_id}' from {} to current/",
|
||||
"[lifecycle] Moved '{story_id}' from {} to work/2_current/",
|
||||
source_dir.display()
|
||||
);
|
||||
|
||||
@@ -767,20 +760,15 @@ pub fn move_story_to_current(project_root: &Path, story_id: &str) -> Result<(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Move a story from `.story_kit/current/` to `.story_kit/stories/archived/` and auto-commit.
|
||||
/// Move a story from `work/2_current/` to `work/5_archived/` and auto-commit.
|
||||
///
|
||||
/// * If the story is in `current/`, it is renamed to `stories/archived/` and committed.
|
||||
/// * If the story is already in `stories/archived/`, this is a no-op (idempotent).
|
||||
/// * If the story is not found in `current/` or `stories/archived/`, an error is returned.
|
||||
/// * If the story is in `2_current/`, it is moved to `5_archived/` and committed.
|
||||
/// * If the story is already in `5_archived/`, this is a no-op (idempotent).
|
||||
/// * If the story is not found in `2_current/` or `5_archived/`, an error is returned.
|
||||
pub fn move_story_to_archived(project_root: &Path, story_id: &str) -> Result<(), String> {
|
||||
let current_path = project_root
|
||||
.join(".story_kit")
|
||||
.join("current")
|
||||
.join(format!("{story_id}.md"));
|
||||
let archived_dir = project_root
|
||||
.join(".story_kit")
|
||||
.join("stories")
|
||||
.join("archived");
|
||||
let sk = project_root.join(".story_kit").join("work");
|
||||
let current_path = sk.join("2_current").join(format!("{story_id}.md"));
|
||||
let archived_dir = sk.join("5_archived");
|
||||
let archived_path = archived_dir.join(format!("{story_id}.md"));
|
||||
|
||||
if archived_path.exists() {
|
||||
@@ -790,10 +778,10 @@ pub fn move_story_to_archived(project_root: &Path, story_id: &str) -> Result<(),
|
||||
|
||||
if current_path.exists() {
|
||||
std::fs::create_dir_all(&archived_dir)
|
||||
.map_err(|e| format!("Failed to create stories/archived/ directory: {e}"))?;
|
||||
.map_err(|e| format!("Failed to create work/5_archived/ directory: {e}"))?;
|
||||
std::fs::rename(¤t_path, &archived_path)
|
||||
.map_err(|e| format!("Failed to move story '{story_id}' to archived/: {e}"))?;
|
||||
eprintln!("[lifecycle] Moved story '{story_id}' from current/ to stories/archived/");
|
||||
.map_err(|e| format!("Failed to move story '{story_id}' to 5_archived/: {e}"))?;
|
||||
eprintln!("[lifecycle] Moved story '{story_id}' from work/2_current/ to work/5_archived/");
|
||||
|
||||
let msg = format!("story-kit: accept story {story_id}");
|
||||
git_stage_and_commit(
|
||||
@@ -805,20 +793,20 @@ pub fn move_story_to_archived(project_root: &Path, story_id: &str) -> Result<(),
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"Story '{story_id}' not found in current/. Cannot accept story."
|
||||
"Story '{story_id}' not found in work/2_current/. Cannot accept story."
|
||||
))
|
||||
}
|
||||
|
||||
/// Move a bug from `.story_kit/current/` to `.story_kit/bugs/archive/` and auto-commit.
|
||||
/// Move a bug from `work/2_current/` or `work/1_upcoming/` to `work/5_archived/` and auto-commit.
|
||||
///
|
||||
/// * If the bug is in `current/`, it is moved to `bugs/archive/` and committed.
|
||||
/// * If the bug is still in `bugs/` (never started), it is moved directly to `bugs/archive/`.
|
||||
/// * If the bug is already in `bugs/archive/`, this is a no-op (idempotent).
|
||||
/// * If the bug is in `2_current/`, it is moved to `5_archived/` and committed.
|
||||
/// * If the bug is still in `1_upcoming/` (never started), it is moved directly to `5_archived/`.
|
||||
/// * If the bug is already in `5_archived/`, this is a no-op (idempotent).
|
||||
/// * If the bug is not found anywhere, an error is returned.
|
||||
pub fn close_bug_to_archive(project_root: &Path, bug_id: &str) -> Result<(), String> {
|
||||
let sk = project_root.join(".story_kit");
|
||||
let current_path = sk.join("current").join(format!("{bug_id}.md"));
|
||||
let bugs_path = sk.join("bugs").join(format!("{bug_id}.md"));
|
||||
let sk = project_root.join(".story_kit").join("work");
|
||||
let current_path = sk.join("2_current").join(format!("{bug_id}.md"));
|
||||
let upcoming_path = sk.join("1_upcoming").join(format!("{bug_id}.md"));
|
||||
let archive_dir = item_archive_dir(project_root, bug_id);
|
||||
let archive_path = archive_dir.join(format!("{bug_id}.md"));
|
||||
|
||||
@@ -828,21 +816,21 @@ pub fn close_bug_to_archive(project_root: &Path, bug_id: &str) -> Result<(), Str
|
||||
|
||||
let source_path = if current_path.exists() {
|
||||
current_path.clone()
|
||||
} else if bugs_path.exists() {
|
||||
bugs_path.clone()
|
||||
} else if upcoming_path.exists() {
|
||||
upcoming_path.clone()
|
||||
} else {
|
||||
return Err(format!(
|
||||
"Bug '{bug_id}' not found in current/ or bugs/. Cannot close bug."
|
||||
"Bug '{bug_id}' not found in work/2_current/ or work/1_upcoming/. Cannot close bug."
|
||||
));
|
||||
};
|
||||
|
||||
std::fs::create_dir_all(&archive_dir)
|
||||
.map_err(|e| format!("Failed to create bugs/archive/ directory: {e}"))?;
|
||||
.map_err(|e| format!("Failed to create work/5_archived/ directory: {e}"))?;
|
||||
std::fs::rename(&source_path, &archive_path)
|
||||
.map_err(|e| format!("Failed to move bug '{bug_id}' to archive: {e}"))?;
|
||||
.map_err(|e| format!("Failed to move bug '{bug_id}' to 5_archived/: {e}"))?;
|
||||
|
||||
eprintln!(
|
||||
"[lifecycle] Closed bug '{bug_id}' → bugs/archive/"
|
||||
"[lifecycle] Closed bug '{bug_id}' → work/5_archived/"
|
||||
);
|
||||
|
||||
let msg = format!("story-kit: close bug {bug_id}");
|
||||
|
||||
@@ -843,7 +843,7 @@ fn tool_get_story_todos(args: &Value, ctx: &AppContext) -> Result<String, String
|
||||
.ok_or("Missing required argument: story_id")?;
|
||||
|
||||
let root = ctx.state.get_project_root()?;
|
||||
let current_dir = root.join(".story_kit").join("current");
|
||||
let current_dir = root.join(".story_kit").join("work").join("2_current");
|
||||
let filepath = current_dir.join(format!("{story_id}.md"));
|
||||
|
||||
if !filepath.exists() {
|
||||
|
||||
@@ -142,7 +142,7 @@ struct ValidateStoriesResponse {
|
||||
|
||||
pub fn load_upcoming_stories(ctx: &AppContext) -> Result<Vec<UpcomingStory>, String> {
|
||||
let root = ctx.state.get_project_root()?;
|
||||
let upcoming_dir = root.join(".story_kit").join("stories").join("upcoming");
|
||||
let upcoming_dir = root.join(".story_kit").join("work").join("1_upcoming");
|
||||
|
||||
if !upcoming_dir.exists() {
|
||||
return Ok(Vec::new());
|
||||
@@ -177,7 +177,7 @@ pub fn load_upcoming_stories(ctx: &AppContext) -> Result<Vec<UpcomingStory>, Str
|
||||
|
||||
fn load_current_story_metadata(ctx: &AppContext) -> Result<Vec<(String, StoryMetadata)>, String> {
|
||||
let root = ctx.state.get_project_root()?;
|
||||
let current_dir = root.join(".story_kit").join("current");
|
||||
let current_dir = root.join(".story_kit").join("work").join("2_current");
|
||||
|
||||
if !current_dir.exists() {
|
||||
return Ok(Vec::new());
|
||||
@@ -513,7 +513,7 @@ impl WorkflowApi {
|
||||
#[oai(path = "/workflow/todos", method = "get")]
|
||||
async fn story_todos(&self) -> OpenApiResult<Json<TodoListResponse>> {
|
||||
let root = self.ctx.state.get_project_root().map_err(bad_request)?;
|
||||
let current_dir = root.join(".story_kit").join("current");
|
||||
let current_dir = root.join(".story_kit").join("work").join("2_current");
|
||||
|
||||
if !current_dir.exists() {
|
||||
return Ok(Json(TodoListResponse {
|
||||
@@ -631,15 +631,15 @@ pub fn create_story_file(
|
||||
acceptance_criteria: Option<&[String]>,
|
||||
commit: bool,
|
||||
) -> Result<String, String> {
|
||||
let story_number = next_story_number(root)?;
|
||||
let story_number = next_item_number(root)?;
|
||||
let slug = slugify_name(name);
|
||||
|
||||
if slug.is_empty() {
|
||||
return Err("Name must contain at least one alphanumeric character.".to_string());
|
||||
}
|
||||
|
||||
let filename = format!("{story_number}_{slug}.md");
|
||||
let upcoming_dir = root.join(".story_kit").join("stories").join("upcoming");
|
||||
let filename = format!("{story_number}_story_{slug}.md");
|
||||
let upcoming_dir = root.join(".story_kit").join("work").join("1_upcoming");
|
||||
fs::create_dir_all(&upcoming_dir)
|
||||
.map_err(|e| format!("Failed to create upcoming directory: {e}"))?;
|
||||
|
||||
@@ -701,39 +701,9 @@ fn git_commit_story_file(root: &Path, filepath: &Path, story_id: &str) -> Result
|
||||
|
||||
// ── Bug file helpers ──────────────────────────────────────────────
|
||||
|
||||
/// Determine the next bug number by scanning `.story_kit/bugs/` and `.story_kit/bugs/archive/`.
|
||||
fn next_bug_number(root: &Path) -> Result<u32, String> {
|
||||
let bugs_base = root.join(".story_kit").join("bugs");
|
||||
let mut max_num: u32 = 0;
|
||||
|
||||
for dir in [bugs_base.clone(), bugs_base.join("archive")] {
|
||||
if !dir.exists() {
|
||||
continue;
|
||||
}
|
||||
for entry in
|
||||
fs::read_dir(dir).map_err(|e| format!("Failed to read bugs directory: {e}"))?
|
||||
{
|
||||
let entry = entry.map_err(|e| format!("Failed to read entry: {e}"))?;
|
||||
let name = entry.file_name();
|
||||
let name_str = name.to_string_lossy();
|
||||
// Bug filenames: bug-N-slug.md — extract the N after "bug-"
|
||||
if let Some(rest) = name_str.strip_prefix("bug-") {
|
||||
let num_str: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
|
||||
if let Ok(n) = num_str.parse::<u32>()
|
||||
&& n > max_num
|
||||
{
|
||||
max_num = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(max_num + 1)
|
||||
}
|
||||
|
||||
/// Create a bug file in `.story_kit/bugs/` with a deterministic filename and auto-commit.
|
||||
/// Create a bug file in `work/1_upcoming/` with a deterministic filename and auto-commit.
|
||||
///
|
||||
/// Returns the bug_id (e.g. `"bug-3-login_crash"`).
|
||||
/// Returns the bug_id (e.g. `"4_bug_login_crash"`).
|
||||
pub fn create_bug_file(
|
||||
root: &Path,
|
||||
name: &str,
|
||||
@@ -743,17 +713,17 @@ pub fn create_bug_file(
|
||||
expected_result: &str,
|
||||
acceptance_criteria: Option<&[String]>,
|
||||
) -> Result<String, String> {
|
||||
let bug_number = next_bug_number(root)?;
|
||||
let bug_number = next_item_number(root)?;
|
||||
let slug = slugify_name(name);
|
||||
|
||||
if slug.is_empty() {
|
||||
return Err("Name must contain at least one alphanumeric character.".to_string());
|
||||
}
|
||||
|
||||
let filename = format!("bug-{bug_number}-{slug}.md");
|
||||
let bugs_dir = root.join(".story_kit").join("bugs");
|
||||
let filename = format!("{bug_number}_bug_{slug}.md");
|
||||
let bugs_dir = root.join(".story_kit").join("work").join("1_upcoming");
|
||||
fs::create_dir_all(&bugs_dir)
|
||||
.map_err(|e| format!("Failed to create bugs directory: {e}"))?;
|
||||
.map_err(|e| format!("Failed to create upcoming directory: {e}"))?;
|
||||
|
||||
let filepath = bugs_dir.join(&filename);
|
||||
if filepath.exists() {
|
||||
@@ -797,6 +767,14 @@ pub fn create_bug_file(
|
||||
Ok(bug_id)
|
||||
}
|
||||
|
||||
/// Returns true if the item stem (filename without extension) is a bug item.
|
||||
/// Bug items follow the pattern: {N}_bug_{slug}
|
||||
fn is_bug_item(stem: &str) -> bool {
|
||||
// Format: {digits}_bug_{rest}
|
||||
let after_num = stem.trim_start_matches(|c: char| c.is_ascii_digit());
|
||||
after_num.starts_with("_bug_")
|
||||
}
|
||||
|
||||
/// Extract the human-readable name from a bug file's first heading.
|
||||
fn extract_bug_name(path: &Path) -> Option<String> {
|
||||
let contents = fs::read_to_string(path).ok()?;
|
||||
@@ -811,23 +789,22 @@ fn extract_bug_name(path: &Path) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// List all open bugs — files directly in `.story_kit/bugs/` (excluding `archive/` subdir).
|
||||
/// List all open bugs — files in `work/1_upcoming/` matching the `_bug_` naming pattern.
|
||||
///
|
||||
/// Returns a sorted list of `(bug_id, name)` pairs.
|
||||
pub fn list_bug_files(root: &Path) -> Result<Vec<(String, String)>, String> {
|
||||
let bugs_dir = root.join(".story_kit").join("bugs");
|
||||
if !bugs_dir.exists() {
|
||||
let upcoming_dir = root.join(".story_kit").join("work").join("1_upcoming");
|
||||
if !upcoming_dir.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut bugs = Vec::new();
|
||||
for entry in
|
||||
fs::read_dir(&bugs_dir).map_err(|e| format!("Failed to read bugs directory: {e}"))?
|
||||
fs::read_dir(&upcoming_dir).map_err(|e| format!("Failed to read upcoming directory: {e}"))?
|
||||
{
|
||||
let entry = entry.map_err(|e| format!("Failed to read entry: {e}"))?;
|
||||
let path = entry.path();
|
||||
|
||||
// Skip subdirectories (archive/)
|
||||
if path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
@@ -836,12 +813,17 @@ pub fn list_bug_files(root: &Path) -> Result<Vec<(String, String)>, String> {
|
||||
continue;
|
||||
}
|
||||
|
||||
let bug_id = path
|
||||
let stem = path
|
||||
.file_stem()
|
||||
.and_then(|stem| stem.to_str())
|
||||
.ok_or_else(|| "Invalid bug file name.".to_string())?
|
||||
.to_string();
|
||||
.and_then(|s| s.to_str())
|
||||
.ok_or_else(|| "Invalid file name.".to_string())?;
|
||||
|
||||
// Only include bug items: {N}_bug_{slug}
|
||||
if !is_bug_item(stem) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let bug_id = stem.to_string();
|
||||
let name = extract_bug_name(&path).unwrap_or_else(|| bug_id.clone());
|
||||
bugs.push((bug_id, name));
|
||||
}
|
||||
@@ -850,21 +832,22 @@ pub fn list_bug_files(root: &Path) -> Result<Vec<(String, String)>, String> {
|
||||
Ok(bugs)
|
||||
}
|
||||
|
||||
/// Locate a story file by searching .story_kit/current/ then stories/upcoming/.
|
||||
/// Locate a work item file by searching work/2_current/ then work/1_upcoming/.
|
||||
fn find_story_file(project_root: &Path, story_id: &str) -> Result<PathBuf, String> {
|
||||
let filename = format!("{story_id}.md");
|
||||
// Check unified current/ directory first
|
||||
let current_path = project_root.join(".story_kit").join("current").join(&filename);
|
||||
let sk = project_root.join(".story_kit").join("work");
|
||||
// Check 2_current/ first
|
||||
let current_path = sk.join("2_current").join(&filename);
|
||||
if current_path.exists() {
|
||||
return Ok(current_path);
|
||||
}
|
||||
// Fall back to stories/upcoming/
|
||||
let upcoming_path = project_root.join(".story_kit").join("stories").join("upcoming").join(&filename);
|
||||
// Fall back to 1_upcoming/
|
||||
let upcoming_path = sk.join("1_upcoming").join(&filename);
|
||||
if upcoming_path.exists() {
|
||||
return Ok(upcoming_path);
|
||||
}
|
||||
Err(format!(
|
||||
"Story '{story_id}' not found in current/ or upcoming/."
|
||||
"Story '{story_id}' not found in work/2_current/ or work/1_upcoming/."
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1005,13 +988,13 @@ fn slugify_name(name: &str) -> String {
|
||||
result
|
||||
}
|
||||
|
||||
fn next_story_number(root: &std::path::Path) -> Result<u32, String> {
|
||||
let stories_base = root.join(".story_kit").join("stories");
|
||||
/// Scan all `work/` subdirectories for the highest item number across all types (stories, bugs, spikes).
|
||||
fn next_item_number(root: &std::path::Path) -> Result<u32, String> {
|
||||
let work_base = root.join(".story_kit").join("work");
|
||||
let mut max_num: u32 = 0;
|
||||
|
||||
// Scan stories/upcoming/ and stories/archived/ for story numbers
|
||||
for subdir in &["upcoming", "archived"] {
|
||||
let dir = stories_base.join(subdir);
|
||||
for subdir in &["1_upcoming", "2_current", "3_qa", "4_merge", "5_archived"] {
|
||||
let dir = work_base.join(subdir);
|
||||
if !dir.exists() {
|
||||
continue;
|
||||
}
|
||||
@@ -1021,24 +1004,7 @@ fn next_story_number(root: &std::path::Path) -> Result<u32, String> {
|
||||
let entry = entry.map_err(|e| format!("Failed to read entry: {e}"))?;
|
||||
let name = entry.file_name();
|
||||
let name_str = name.to_string_lossy();
|
||||
let num_str: String = name_str.chars().take_while(|c| c.is_ascii_digit()).collect();
|
||||
if let Ok(n) = num_str.parse::<u32>()
|
||||
&& n > max_num
|
||||
{
|
||||
max_num = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also scan unified .story_kit/current/ for story numbers
|
||||
let current_dir = root.join(".story_kit").join("current");
|
||||
if current_dir.exists() {
|
||||
for entry in
|
||||
fs::read_dir(¤t_dir).map_err(|e| format!("Failed to read current directory: {e}"))?
|
||||
{
|
||||
let entry = entry.map_err(|e| format!("Failed to read entry: {e}"))?;
|
||||
let name = entry.file_name();
|
||||
let name_str = name.to_string_lossy();
|
||||
// Filename format: {N}_{type}_{slug}.md — extract leading N
|
||||
let num_str: String = name_str.chars().take_while(|c| c.is_ascii_digit()).collect();
|
||||
if let Ok(n) = num_str.parse::<u32>()
|
||||
&& n > max_num
|
||||
@@ -1056,10 +1022,10 @@ pub fn validate_story_dirs(
|
||||
) -> Result<Vec<StoryValidationResult>, String> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
// Directories to validate: unified current/ + stories/upcoming/
|
||||
// Directories to validate: work/2_current/ + work/1_upcoming/
|
||||
let dirs_to_validate: Vec<PathBuf> = vec![
|
||||
root.join(".story_kit").join("current"),
|
||||
root.join(".story_kit").join("stories").join("upcoming"),
|
||||
root.join(".story_kit").join("work").join("2_current"),
|
||||
root.join(".story_kit").join("work").join("1_upcoming"),
|
||||
];
|
||||
|
||||
for dir in &dirs_to_validate {
|
||||
|
||||
Reference in New Issue
Block a user