@@ -5,7 +5,6 @@ mod create;
55mod mutate;
66mod query;
77
8- use std:: fs;
98use std:: io:: { self , Read as _} ;
109use std:: path:: { Path , PathBuf } ;
1110
@@ -15,9 +14,9 @@ use regex::Regex;
1514use crate :: output:: error_exit;
1615
1716use flowctl_core:: frontmatter;
18- use flowctl_core:: id:: { epic_id_from_task, is_task_id } ;
17+ use flowctl_core:: id:: epic_id_from_task;
1918use flowctl_core:: types:: {
20- Domain , Epic , Task , EPICS_DIR , TASKS_DIR ,
19+ Domain , Epic , Task ,
2120} ;
2221
2322#[ derive( Subcommand , Debug ) ]
@@ -143,34 +142,17 @@ fn read_file_or_stdin(path: &str) -> String {
143142 . unwrap_or_else ( |e| error_exit ( & format ! ( "Failed to read stdin: {e}" ) ) ) ;
144143 buf
145144 } else {
146- fs:: read_to_string ( path)
145+ std :: fs:: read_to_string ( path)
147146 . unwrap_or_else ( |e| error_exit ( & format ! ( "Failed to read file '{}': {e}" , path) ) )
148147 }
149148}
150149
151- /// Scan .flow/tasks/ to find max task number for an epic. Returns 0 if none exist.
152- fn scan_max_task_id ( flow_dir : & Path , epic_id : & str ) -> u32 {
153- let tasks_dir = flow_dir. join ( TASKS_DIR ) ;
154- if !tasks_dir. exists ( ) {
155- return 0 ;
156- }
157-
158- let pattern = format ! ( r"^{}\.(\d+)\.md$" , regex:: escape( epic_id) ) ;
159- let re = Regex :: new ( & pattern) . expect ( "task ID regex is valid" ) ;
160-
161- let mut max_n: u32 = 0 ;
162- if let Ok ( entries) = fs:: read_dir ( & tasks_dir) {
163- for entry in entries. flatten ( ) {
164- let name = entry. file_name ( ) ;
165- let name_str = name. to_string_lossy ( ) ;
166- if let Some ( caps) = re. captures ( & name_str) {
167- if let Ok ( n) = caps[ 1 ] . parse :: < u32 > ( ) {
168- max_n = max_n. max ( n) ;
169- }
170- }
171- }
172- }
173- max_n
150+ /// Get max task number for an epic from DB. Returns 0 if none exist.
151+ fn scan_max_task_id ( _flow_dir : & Path , epic_id : & str ) -> u32 {
152+ let conn = require_db ( )
153+ . unwrap_or_else ( |e| error_exit ( & format ! ( "DB required for scan_max_task_id: {e}" ) ) ) ;
154+ let n = crate :: commands:: db_shim:: max_task_num ( & conn, epic_id) . unwrap_or ( 0 ) ;
155+ n as u32
174156}
175157
176158/// Parse a domain string into a Domain enum.
@@ -195,82 +177,43 @@ fn create_task_spec(id: &str, title: &str, acceptance: Option<&str>) -> String {
195177 )
196178}
197179
198- /// Load a task: DB first, markdown fallback.
180+ /// Load a task from DB (no MD fallback) .
199181fn load_task_md ( _flow_dir : & Path , task_id : & str ) -> Task {
200- if let Ok ( conn) = require_db ( ) {
201- let repo = crate :: commands:: db_shim:: TaskRepo :: new ( & conn) ;
202- if let Ok ( task) = repo. get ( task_id) {
203- return task;
204- }
205- }
206- // Fallback to markdown.
207- let flow_dir = _flow_dir;
208- let spec_path = flow_dir. join ( TASKS_DIR ) . join ( format ! ( "{}.md" , task_id) ) ;
209- if !spec_path. exists ( ) {
210- error_exit ( & format ! ( "Task {} not found" , task_id) ) ;
211- }
212- let content = fs:: read_to_string ( & spec_path)
213- . unwrap_or_else ( |e| error_exit ( & format ! ( "Failed to read task {}: {e}" , task_id) ) ) ;
214- let doc: frontmatter:: Document < Task > = frontmatter:: parse ( & content)
215- . unwrap_or_else ( |e| error_exit ( & format ! ( "Failed to parse task {}: {e}" , task_id) ) ) ;
216- doc. frontmatter
182+ let conn = require_db ( )
183+ . unwrap_or_else ( |e| error_exit ( & format ! ( "DB required: {e}" ) ) ) ;
184+ let repo = crate :: commands:: db_shim:: TaskRepo :: new ( & conn) ;
185+ repo. get ( task_id)
186+ . unwrap_or_else ( |_| error_exit ( & format ! ( "Task {} not found" , task_id) ) )
217187}
218188
219- /// Load an epic: DB first, markdown fallback.
189+ /// Load an epic from DB (no MD fallback) .
220190fn load_epic_md ( _flow_dir : & Path , epic_id : & str ) -> Option < Epic > {
221- if let Ok ( conn) = require_db ( ) {
222- let repo = crate :: commands:: db_shim:: EpicRepo :: new ( & conn) ;
223- if let Ok ( epic) = repo. get ( epic_id) {
224- return Some ( epic) ;
225- }
226- }
227- let flow_dir = _flow_dir;
228- let spec_path = flow_dir. join ( EPICS_DIR ) . join ( format ! ( "{}.md" , epic_id) ) ;
229- if !spec_path. exists ( ) {
230- return None ;
231- }
232- let content = fs:: read_to_string ( & spec_path) . ok ( ) ?;
233- let doc: frontmatter:: Document < Epic > = frontmatter:: parse ( & content) . ok ( ) ?;
234- Some ( doc. frontmatter )
191+ let conn = require_db ( ) . ok ( ) ?;
192+ let repo = crate :: commands:: db_shim:: EpicRepo :: new ( & conn) ;
193+ repo. get ( epic_id) . ok ( )
235194}
236195
237- /// Load task's full document (frontmatter + body): DB first, markdown fallback.
238- fn load_task_doc ( flow_dir : & Path , task_id : & str ) -> frontmatter:: Document < Task > {
239- if let Ok ( conn) = require_db ( ) {
240- let repo = crate :: commands:: db_shim:: TaskRepo :: new ( & conn) ;
241- if let Ok ( ( task, body) ) = repo. get_with_body ( task_id) {
242- return frontmatter:: Document {
243- frontmatter : task,
244- body,
245- } ;
246- }
247- }
248- // Fallback to markdown.
249- let spec_path = flow_dir. join ( TASKS_DIR ) . join ( format ! ( "{}.md" , task_id) ) ;
250- if !spec_path. exists ( ) {
251- error_exit ( & format ! ( "Task {} not found" , task_id) ) ;
196+ /// Load task's full document (frontmatter + body) from DB (no MD fallback).
197+ fn load_task_doc ( _flow_dir : & Path , task_id : & str ) -> frontmatter:: Document < Task > {
198+ let conn = require_db ( )
199+ . unwrap_or_else ( |e| error_exit ( & format ! ( "DB required: {e}" ) ) ) ;
200+ let repo = crate :: commands:: db_shim:: TaskRepo :: new ( & conn) ;
201+ let ( task, body) = repo. get_with_body ( task_id)
202+ . unwrap_or_else ( |_| error_exit ( & format ! ( "Task {} not found" , task_id) ) ) ;
203+ frontmatter:: Document {
204+ frontmatter : task,
205+ body,
252206 }
253- let content = fs:: read_to_string ( & spec_path)
254- . unwrap_or_else ( |e| error_exit ( & format ! ( "Failed to read task {}: {e}" , task_id) ) ) ;
255- frontmatter:: parse ( & content)
256- . unwrap_or_else ( |e| error_exit ( & format ! ( "Failed to parse task {}: {e}" , task_id) ) )
257207}
258208
259- /// Write a task document: DB first, then export markdown.
260- fn write_task_doc ( flow_dir : & Path , task_id : & str , doc : & frontmatter:: Document < Task > ) {
261- // Write to DB.
262- if let Ok ( conn) = require_db ( ) {
263- let repo = crate :: commands:: db_shim:: TaskRepo :: new ( & conn) ;
264- if let Err ( e) = repo. upsert_with_body ( & doc. frontmatter , & doc. body ) {
265- eprintln ! ( "warning: DB write failed for {task_id}: {e}" ) ;
266- }
209+ /// Write a task document to DB only (no MD export).
210+ fn write_task_doc ( _flow_dir : & Path , task_id : & str , doc : & frontmatter:: Document < Task > ) {
211+ let conn = require_db ( )
212+ . unwrap_or_else ( |e| error_exit ( & format ! ( "DB required for write: {e}" ) ) ) ;
213+ let repo = crate :: commands:: db_shim:: TaskRepo :: new ( & conn) ;
214+ if let Err ( e) = repo. upsert_with_body ( & doc. frontmatter , & doc. body ) {
215+ error_exit ( & format ! ( "DB write failed for {task_id}: {e}" ) ) ;
267216 }
268- // Export to markdown.
269- let spec_path = flow_dir. join ( TASKS_DIR ) . join ( format ! ( "{}.md" , task_id) ) ;
270- let content = frontmatter:: write ( doc)
271- . unwrap_or_else ( |e| error_exit ( & format ! ( "Failed to serialize task {}: {e}" , task_id) ) ) ;
272- fs:: write ( & spec_path, content)
273- . unwrap_or_else ( |e| error_exit ( & format ! ( "Failed to write task {}: {e}" , task_id) ) ) ;
274217}
275218
276219/// Patch a specific section in a Markdown body. Replaces content under `section`
@@ -319,38 +262,24 @@ fn patch_body_section(body: &str, section: &str, new_content: &str) -> String {
319262 result. join ( "\n " )
320263}
321264
322- /// Find tasks that depend on a given task (recursive BFS within same epic).
323- fn find_dependents ( flow_dir : & Path , task_id : & str ) -> Vec < String > {
324- let tasks_dir = flow_dir. join ( TASKS_DIR ) ;
325- if !tasks_dir. exists ( ) {
326- return vec ! [ ] ;
327- }
328-
265+ /// Find tasks that depend on a given task (recursive BFS within same epic) via DB.
266+ fn find_dependents ( _flow_dir : & Path , task_id : & str ) -> Vec < String > {
329267 let epic_id = match epic_id_from_task ( task_id) {
330268 Ok ( id) => id,
331269 Err ( _) => return vec ! [ ] ,
332270 } ;
333271
334- // Load all tasks in the epic
335- let mut all_tasks: Vec < ( String , Vec < String > ) > = Vec :: new ( ) ;
336- if let Ok ( entries) = fs:: read_dir ( & tasks_dir) {
337- for entry in entries. flatten ( ) {
338- let name = entry. file_name ( ) ;
339- let name_str = name. to_string_lossy ( ) ;
340- if !name_str. starts_with ( & epic_id) || !name_str. ends_with ( ".md" ) {
341- continue ;
342- }
343- let tid = name_str. trim_end_matches ( ".md" ) . to_string ( ) ;
344- if !is_task_id ( & tid) {
345- continue ;
346- }
347- if let Ok ( content) = fs:: read_to_string ( entry. path ( ) ) {
348- if let Ok ( doc) = frontmatter:: parse :: < Task > ( & content) {
349- all_tasks. push ( ( doc. frontmatter . id . clone ( ) , doc. frontmatter . depends_on . clone ( ) ) ) ;
350- }
351- }
352- }
353- }
272+ let conn = match require_db ( ) {
273+ Ok ( c) => c,
274+ Err ( _) => return vec ! [ ] ,
275+ } ;
276+
277+ // Load all tasks in the epic from DB
278+ let repo = crate :: commands:: db_shim:: TaskRepo :: new ( & conn) ;
279+ let all_tasks: Vec < ( String , Vec < String > ) > = match repo. list_by_epic ( & epic_id) {
280+ Ok ( tasks) => tasks. into_iter ( ) . map ( |t| ( t. id . clone ( ) , t. depends_on . clone ( ) ) ) . collect ( ) ,
281+ Err ( _) => return vec ! [ ] ,
282+ } ;
354283
355284 // BFS
356285 let mut dependents: Vec < String > = Vec :: new ( ) ;
0 commit comments