99//! 执行面=前端 Overlay(commands_sop.rs 出口)。证据路径白名单
1010//! `notes-images/` 前缀在命令层校验(G3)。
1111
12- use rusqlite:: { params, Connection } ;
12+ use rusqlite:: { params, Connection , OptionalExtension } ;
1313use serde:: Serialize ;
1414
1515use crate :: db:: Db ;
@@ -150,7 +150,8 @@ pub fn lines_to_steps(content: &str, start_line: i64, end_line: i64) -> Vec<Stri
150150}
151151
152152impl Db {
153- /// 建模板(范围行数 ≤50;同笔记同名先删后插=覆盖校准)。
153+ /// 建/校准模板(审查中-5:同名存在=原地 UPDATE——覆盖校准不得级联销毁
154+ /// run 档案链/失败聚合,REQ-297 演进原料必须保留)。
154155 pub fn create_sop_template (
155156 & self ,
156157 note_id : i64 ,
@@ -162,19 +163,43 @@ impl Db {
162163 let mode = if mode == MODE_CONFIRM { MODE_CONFIRM } else { MODE_READDO } ;
163164 let now = crate :: db:: unix_seconds ( ) ;
164165 self . with_conn ( |conn| {
165- conn. execute (
166- "DELETE FROM sop_templates WHERE note_id = ?1 AND name = ?2" ,
167- params ! [ note_id, name] ,
168- ) ?;
169- conn. execute (
170- "INSERT INTO sop_templates (note_id, name, start_line, end_line, mode, created_at, updated_at)
171- VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?6)" ,
172- params ! [ note_id, name, start_line, end_line, mode, now] ,
173- ) ?;
174- Ok ( conn. last_insert_rowid ( ) )
166+ let existing: Option < i64 > = conn
167+ . query_row (
168+ "SELECT id FROM sop_templates WHERE note_id = ?1 AND name = ?2" ,
169+ params ! [ note_id, name] ,
170+ |r| r. get ( 0 ) ,
171+ )
172+ . optional ( ) ?;
173+ match existing {
174+ Some ( id) => {
175+ conn. execute (
176+ "UPDATE sop_templates SET start_line = ?1, end_line = ?2, mode = ?3, updated_at = ?4 WHERE id = ?5" ,
177+ params ! [ start_line, end_line, mode, now, id] ,
178+ ) ?;
179+ Ok ( id)
180+ }
181+ None => {
182+ conn. execute (
183+ "INSERT INTO sop_templates (note_id, name, start_line, end_line, mode, created_at, updated_at)
184+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?6)" ,
185+ params ! [ note_id, name, start_line, end_line, mode, now] ,
186+ ) ?;
187+ Ok ( conn. last_insert_rowid ( ) )
188+ }
189+ }
175190 } )
176191 }
177192
193+ /// 行范围步骤数(不截断——命令层 ≤50 校验用;审查中-6:先判后建,
194+ /// truncate 只作 DB 层兜底)。
195+ pub fn count_template_steps ( content : & str , start_line : i64 , end_line : i64 ) -> usize {
196+ let lines: Vec < & str > = content. split ( '\n' ) . collect ( ) ;
197+ ( start_line..=end_line)
198+ . filter_map ( |i| lines. get ( i as usize ) )
199+ . filter ( |l| !l. trim ( ) . is_empty ( ) )
200+ . count ( )
201+ }
202+
178203 pub fn list_sop_templates ( & self , note_id : Option < i64 > ) -> Result < Vec < SopTemplate > > {
179204 self . with_conn ( |conn| {
180205 let base = "SELECT t.id, t.note_id, t.name, t.start_line, t.end_line, t.mode, n.title
@@ -313,7 +338,8 @@ impl Db {
313338 } )
314339 }
315340
316- /// 步状态更新(done|skipped|failed;失败必填 failure_note 或忽略——命令层校验)。
341+ /// 步状态更新(done|skipped|failed;审查中-7:DB 层守卫 run active——
342+ /// 已收尾 run 拒写,防层间复用裸奔)。
317343 pub fn update_sop_step (
318344 & self ,
319345 run_id : i64 ,
@@ -328,6 +354,16 @@ impl Db {
328354 ) ) ) ;
329355 }
330356 self . with_conn ( |conn| {
357+ let run_status: Option < String > = conn
358+ . query_row (
359+ "SELECT status FROM sop_runs WHERE id = ?1" ,
360+ params ! [ run_id] ,
361+ |r| r. get ( 0 ) ,
362+ )
363+ . optional ( ) ?;
364+ if run_status. as_deref ( ) != Some ( RUN_ACTIVE ) {
365+ return Ok ( false ) ; // 不存在或已收尾——统一不可写语义
366+ }
331367 let affected = conn. execute (
332368 "UPDATE sop_run_steps SET status = ?1, evidence_path = ?2, failure_note = ?3, checked_at = ?4
333369 WHERE run_id = ?5 AND step_no = ?6" ,
@@ -338,16 +374,23 @@ impl Db {
338374 }
339375
340376 /// 结束 run(done/aborted)→ 结算统计写史(sop_run 事件;meta=统计 JSON)。
377+ /// 审查中-7:幂等守卫——仅 active run 可收尾(重复 finish 报错且不重复入史)。
341378 pub fn finish_sop_run ( & self , run_id : i64 , status : & str ) -> Result < SopRunDetail > {
342379 if !matches ! ( status, RUN_DONE | RUN_ABORTED ) {
343380 return Err ( crate :: error:: AppError :: Asr ( "仅支持 done/aborted 收尾" . to_string ( ) ) ) ;
344381 }
345382 let Some ( detail) = self . sop_run_detail ( run_id) ? else {
346383 return Err ( crate :: error:: AppError :: Io ( "run 不存在" . to_string ( ) ) ) ;
347384 } ;
385+ if detail. run . status != RUN_ACTIVE {
386+ return Err ( crate :: error:: AppError :: Asr ( format ! (
387+ "run 已收尾({})——不可重复结算" ,
388+ detail. run. status
389+ ) ) ) ;
390+ }
348391 self . with_conn ( |conn| {
349392 conn. execute (
350- "UPDATE sop_runs SET status = ?1, finished_at = ?2 WHERE id = ?3" ,
393+ "UPDATE sop_runs SET status = ?1, finished_at = ?2 WHERE id = ?3 AND status = 'active' " ,
351394 params ! [ status, crate :: db:: unix_seconds( ) , run_id] ,
352395 ) ?;
353396 Ok ( ( ) )
0 commit comments