# Fix Double Finalize + Rule Type Bindings API Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Fix two blocking issues: (1) eliminate the duplicate `result_status` / `finished_at` write in `save_evaluation_results`, and (2) add full CRUD API for the `leaudit_rule_type_bindings` table. **Architecture:** Fix 1 is a one-line removal in `storage_adapter.py` — strip the premature run summary UPDATE from `save_evaluation_results` so `finalize_run` is the single source of truth for terminal state. Fix 2 follows the existing RuleController → IRuleService → RuleServiceImpl layered pattern, adding DTO/VO types and 4 endpoints for binding management. **Tech Stack:** Python, FastAPI, SQLAlchemy async, PostgreSQL --- ## File Map | File | Action | Responsibility | |---|---|---| | `fastapi_modules/fastapi_leaudit/leaudit_bridge/storage_adapter.py` | Modify | Remove premature UPDATE from `save_evaluation_results` | | `fastapi_modules/fastapi_leaudit/domian/Dto/ruleBindingDto.py` | Create | `RuleBindingCreateDTO`, `RuleBindingUpdateDTO` | | `fastapi_modules/fastapi_leaudit/domian/vo/ruleVo.py` | Modify | Add `RuleBindingVO` | | `fastapi_modules/fastapi_leaudit/services/ruleService.py` | Modify | Add 4 abstract methods | | `fastapi_modules/fastapi_leaudit/services/impl/ruleServiceImpl.py` | Modify | Add 4 method implementations | | `fastapi_modules/fastapi_leaudit/controllers/ruleController.py` | Modify | Add 4 endpoints | --- ### Task 1: Fix Double Finalize — Strip Premature UPDATE from save_evaluation_results **Files:** - Modify: `fastapi_modules/fastapi_leaudit/leaudit_bridge/storage_adapter.py:167-181` - [ ] **Step 1: Remove result_status and finished_at from the UPDATE clause** Replace lines 167-181 of `storage_adapter.py`: ```python # Update audit_runs summary await session.execute( text("""UPDATE leaudit_audit_runs SET total_score = :ts, passed_count = :pc, failed_count = :fc, skipped_count = :sc, result_status = :rs, finished_at = now(), update_time = now() WHERE id = :rid"""), { "ts": evaluation.total_score, "pc": evaluation.passed_count, "fc": evaluation.failed_count, "sc": evaluation.skipped_count, "rs": "pass" if evaluation.failed_count == 0 else "fail", "rid": resolved_run_id, }, ) ``` With: ```python # Update audit_runs summary (scores only — terminal state set by finalize_run) await session.execute( text("""UPDATE leaudit_audit_runs SET total_score = :ts, passed_count = :pc, failed_count = :fc, skipped_count = :sc, update_time = now() WHERE id = :rid"""), { "ts": evaluation.total_score, "pc": evaluation.passed_count, "fc": evaluation.failed_count, "sc": evaluation.skipped_count, "rid": resolved_run_id, }, ) ``` - [ ] **Step 2: Verify finalize_run is still the last writer in persist_result** Read `nativeRunner.py:149-157` to confirm `finalize_run` runs after all other persist steps, including `save_evaluation_results`. The order is: ``` save_ocr_result → save_extraction_result → save_evaluation_results → save_run_errors → save_rescue_outcomes → save_run_metrics → finalize_run ``` Confirmed: `finalize_run` is the LAST call in `persist_result()`, so it will always set the definitive terminal state. - [ ] **Step 3: Syntax check** ```bash cd /home/wren-dev/Porject/leaudit-platform && python -m compileall fastapi_modules/fastapi_leaudit/leaudit_bridge/storage_adapter.py ``` Expected: Compile successful, no errors. - [ ] **Step 4: Commit** ```bash cd /home/wren-dev/Porject/leaudit-platform git add fastapi_modules/fastapi_leaudit/leaudit_bridge/storage_adapter.py git commit -m "fix: remove premature result_status/finished_at from save_evaluation_results finalize_run() is the single source of truth for terminal run state. Previously save_evaluation_results wrote a binary pass/fail status and finished_at BEFORE rescue outcomes/metrics were saved, then finalize_run overwrote it. Now scores only are written here; terminal state is set once by finalize_run after all sub-results are persisted." ``` --- ### Task 2: Create RuleBinding DTOs **Files:** - Create: `fastapi_modules/fastapi_leaudit/domian/Dto/ruleBindingDto.py` - [ ] **Step 1: Create the DTO file** ```python """规则类型绑定 DTO。""" from pydantic import BaseModel, Field class RuleBindingCreateDTO(BaseModel): """创建规则类型绑定请求。""" docTypeId: int = Field(..., description="文档类型ID → leaudit_document_types.id") docTypeCode: str | None = Field(None, description="文档类型编码(冗余快速匹配)") ruleSetId: int = Field(..., description="规则集ID → leaudit_rule_sets.id") bindingMode: str = Field("explicit", description="绑定模式: explicit / wildcard / fallback") priority: int = Field(0, description="优先级(数值越大优先级越高)") note: str | None = Field(None, description="备注说明") class RuleBindingUpdateDTO(BaseModel): """更新规则类型绑定请求。""" isActive: bool | None = Field(None, description="是否激活") priority: int | None = Field(None, description="优先级") bindingMode: str | None = Field(None, description="绑定模式") note: str | None = Field(None, description="备注说明") ``` - [ ] **Step 2: Syntax check** ```bash cd /home/wren-dev/Porject/leaudit-platform && python -m compileall fastapi_modules/fastapi_leaudit/domian/Dto/ruleBindingDto.py ``` Expected: Compile successful. - [ ] **Step 3: Commit** ```bash cd /home/wren-dev/Porject/leaudit-platform git add fastapi_modules/fastapi_leaudit/domian/Dto/ruleBindingDto.py git commit -m "feat: add RuleBindingCreateDTO and RuleBindingUpdateDTO" ``` --- ### Task 3: Add RuleBindingVO to ruleVo.py **Files:** - Modify: `fastapi_modules/fastapi_leaudit/domian/vo/ruleVo.py` - [ ] **Step 1: Append RuleBindingVO class at end of file** Add after line 49 (after the `RuleValidationVO` class): ```python class RuleBindingVO(BaseModel): """规则类型绑定响应。""" id: int = Field(..., description="绑定ID") docTypeId: int = Field(..., description="文档类型ID") docTypeCode: str | None = Field(None, description="文档类型编码") ruleSetId: int = Field(..., description="规则集ID") ruleType: str | None = Field(None, description="规则类型编码(来自关联查询)") ruleName: str | None = Field(None, description="规则集名称(来自关联查询)") bindingMode: str = Field(..., description="绑定模式: explicit / wildcard / fallback") priority: int = Field(0, description="优先级") isActive: bool = Field(True, description="是否激活") note: str | None = Field(None, description="备注说明") ``` - [ ] **Step 2: Syntax check** ```bash cd /home/wren-dev/Porject/leaudit-platform && python -m compileall fastapi_modules/fastapi_leaudit/domian/vo/ruleVo.py ``` Expected: Compile successful. - [ ] **Step 3: Commit** ```bash cd /home/wren-dev/Porject/leaudit-platform git add fastapi_modules/fastapi_leaudit/domian/vo/ruleVo.py git commit -m "feat: add RuleBindingVO for rule type bindings response" ``` --- ### Task 4: Add Binding Methods to IRuleService Interface **Files:** - Modify: `fastapi_modules/fastapi_leaudit/services/ruleService.py` - [ ] **Step 1: Add import for RuleBindingVO at top of file** Add `RuleBindingVO` to the existing import block (line 5-10): ```python from fastapi_modules.fastapi_leaudit.domian.vo.ruleVo import ( RuleBindingVO, RuleContentVO, RuleSetVO, RuleValidationVO, RuleVersionVO, ) ``` - [ ] **Step 2: Add 4 abstract methods before the closing of the class** Add after the `Rollback` method (before the last blank line of the class): ```python @abstractmethod async def ListBindings(self, RuleType: str | None = None) -> list[RuleBindingVO]: """列出规则类型绑定。可按规则类型过滤。""" ... @abstractmethod async def CreateBinding( self, DocTypeId: int, RuleSetId: int, BindingMode: str = "explicit", Priority: int = 0, DocTypeCode: str | None = None, Note: str | None = None, ) -> RuleBindingVO: """创建规则类型绑定。""" ... @abstractmethod async def UpdateBinding( self, BindingId: int, IsActive: bool | None = None, Priority: int | None = None, BindingMode: str | None = None, Note: str | None = None, ) -> RuleBindingVO: """更新规则类型绑定。""" ... @abstractmethod async def DeleteBinding(self, BindingId: int) -> None: """删除规则类型绑定。""" ... ``` - [ ] **Step 3: Syntax check** ```bash cd /home/wren-dev/Porject/leaudit-platform && python -m compileall fastapi_modules/fastapi_leaudit/services/ruleService.py ``` Expected: Compile successful. - [ ] **Step 4: Commit** ```bash cd /home/wren-dev/Porject/leaudit-platform git add fastapi_modules/fastapi_leaudit/services/ruleService.py git commit -m "feat: add binding CRUD methods to IRuleService interface" ``` --- ### Task 5: Implement Binding Methods in RuleServiceImpl **Files:** - Modify: `fastapi_modules/fastapi_leaudit/services/impl/ruleServiceImpl.py` - [ ] **Step 1: Add RuleBindingVO import** Add `RuleBindingVO` to the import from `ruleVo` (line 12-17): ```python from fastapi_modules.fastapi_leaudit.domian.vo.ruleVo import ( RuleBindingVO, RuleContentVO, RuleSetVO, RuleValidationVO, RuleVersionVO, ) ``` - [ ] **Step 2: Add 4 method implementations after Rollback method (before _SwitchVersion)** Insert before the `_SwitchVersion` method (before line 342): ```python async def ListBindings(self, RuleType: str | None = None) -> list[RuleBindingVO]: """列出规则类型绑定,可按规则类型过滤。""" async with GetAsyncSession() as Session: if RuleType: Result = await Session.execute( text( """ SELECT b.id, b.doc_type_id, b.doc_type_code, b.rule_set_id, b.binding_mode, b.priority, b.is_active, b.note, rs.rule_type, rs.rule_name FROM leaudit_rule_type_bindings b JOIN leaudit_rule_sets rs ON rs.id = b.rule_set_id WHERE rs.rule_type = :rule_type AND rs.delete_time IS NULL ORDER BY b.priority DESC, b.id DESC """ ), {"rule_type": RuleType}, ) else: Result = await Session.execute( text( """ SELECT b.id, b.doc_type_id, b.doc_type_code, b.rule_set_id, b.binding_mode, b.priority, b.is_active, b.note, rs.rule_type, rs.rule_name FROM leaudit_rule_type_bindings b JOIN leaudit_rule_sets rs ON rs.id = b.rule_set_id WHERE rs.delete_time IS NULL ORDER BY rs.rule_type, b.priority DESC, b.id DESC """ ), ) return [ RuleBindingVO( id=int(Row["id"]), docTypeId=int(Row["doc_type_id"]), docTypeCode=Row["doc_type_code"], ruleSetId=int(Row["rule_set_id"]), ruleType=Row["rule_type"], ruleName=Row["rule_name"], bindingMode=Row["binding_mode"], priority=int(Row["priority"]), isActive=bool(Row["is_active"]), note=Row["note"], ) for Row in Result.mappings().all() ] async def CreateBinding( self, DocTypeId: int, RuleSetId: int, BindingMode: str = "explicit", Priority: int = 0, DocTypeCode: str | None = None, Note: str | None = None, ) -> RuleBindingVO: """创建规则类型绑定。""" async with GetAsyncSession() as Session: RuleSet = await Session.execute( text("SELECT id, rule_type, rule_name FROM leaudit_rule_sets WHERE id = :rid AND delete_time IS NULL LIMIT 1"), {"rid": RuleSetId}, ) if not RuleSet.mappings().first(): raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "规则集不存在") Existing = await Session.execute( text( """ SELECT id FROM leaudit_rule_type_bindings WHERE doc_type_id = :dtid AND rule_set_id = :rsid LIMIT 1 """ ), {"dtid": DocTypeId, "rsid": RuleSetId}, ) if Existing.mappings().first(): raise LeauditException(StatusCodeEnum.HTTP_409_CONFLICT, "该文档类型已绑定此规则集") Result = await Session.execute( text( """ INSERT INTO leaudit_rule_type_bindings ( doc_type_id, doc_type_code, rule_set_id, binding_mode, priority, is_active, note ) VALUES ( :doc_type_id, :doc_type_code, :rule_set_id, :binding_mode, :priority, true, :note ) RETURNING id, doc_type_id, doc_type_code, rule_set_id, binding_mode, priority, is_active, note """ ), { "doc_type_id": DocTypeId, "doc_type_code": DocTypeCode, "rule_set_id": RuleSetId, "binding_mode": BindingMode, "priority": Priority, "note": Note, }, ) await Session.commit() Row = Result.mappings().first() RsRow = RuleSet.mappings().first() return RuleBindingVO( id=int(Row["id"]), docTypeId=int(Row["doc_type_id"]), docTypeCode=Row["doc_type_code"], ruleSetId=int(Row["rule_set_id"]), ruleType=RsRow["rule_type"], ruleName=RsRow["rule_name"], bindingMode=Row["binding_mode"], priority=int(Row["priority"]), isActive=bool(Row["is_active"]), note=Row["note"], ) async def UpdateBinding( self, BindingId: int, IsActive: bool | None = None, Priority: int | None = None, BindingMode: str | None = None, Note: str | None = None, ) -> RuleBindingVO: """更新规则类型绑定。""" async with GetAsyncSession() as Session: Existing = await Session.execute( text( """ SELECT b.id, b.doc_type_id, b.doc_type_code, b.rule_set_id, b.binding_mode, b.priority, b.is_active, b.note, rs.rule_type, rs.rule_name FROM leaudit_rule_type_bindings b JOIN leaudit_rule_sets rs ON rs.id = b.rule_set_id WHERE b.id = :bid LIMIT 1 """ ), {"bid": BindingId}, ) Row = Existing.mappings().first() if not Row: raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "绑定记录不存在") SetClauses: list[str] = [] Params: dict[str, object] = {"bid": BindingId} if IsActive is not None: SetClauses.append("is_active = :is_active") Params["is_active"] = IsActive if Priority is not None: SetClauses.append("priority = :priority") Params["priority"] = Priority if BindingMode is not None: SetClauses.append("binding_mode = :binding_mode") Params["binding_mode"] = BindingMode if Note is not None: SetClauses.append("note = :note") Params["note"] = Note if SetClauses: SetClauses.append("update_time = now()") await Session.execute( text(f"UPDATE leaudit_rule_type_bindings SET {', '.join(SetClauses)} WHERE id = :bid"), Params, ) await Session.commit() Result = await Session.execute( text( """ SELECT b.id, b.doc_type_id, b.doc_type_code, b.rule_set_id, b.binding_mode, b.priority, b.is_active, b.note, rs.rule_type, rs.rule_name FROM leaudit_rule_type_bindings b JOIN leaudit_rule_sets rs ON rs.id = b.rule_set_id WHERE b.id = :bid LIMIT 1 """ ), {"bid": BindingId}, ) Row = Result.mappings().first() return RuleBindingVO( id=int(Row["id"]), docTypeId=int(Row["doc_type_id"]), docTypeCode=Row["doc_type_code"], ruleSetId=int(Row["rule_set_id"]), ruleType=Row["rule_type"], ruleName=Row["rule_name"], bindingMode=Row["binding_mode"], priority=int(Row["priority"]), isActive=bool(Row["is_active"]), note=Row["note"], ) async def DeleteBinding(self, BindingId: int) -> None: """删除规则类型绑定。""" async with GetAsyncSession() as Session: Result = await Session.execute( text("DELETE FROM leaudit_rule_type_bindings WHERE id = :bid"), {"bid": BindingId}, ) await Session.commit() if Result.rowcount == 0: raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "绑定记录不存在") ``` - [ ] **Step 2: Syntax check** ```bash cd /home/wren-dev/Porject/leaudit-platform && python -m compileall fastapi_modules/fastapi_leaudit/services/impl/ruleServiceImpl.py ``` Expected: Compile successful. - [ ] **Step 3: Commit** ```bash cd /home/wren-dev/Porject/leaudit-platform git add fastapi_modules/fastapi_leaudit/services/impl/ruleServiceImpl.py git commit -m "feat: implement binding CRUD in RuleServiceImpl" ``` --- ### Task 6: Add Binding Endpoints to RuleController **Files:** - Modify: `fastapi_modules/fastapi_leaudit/controllers/ruleController.py` - [ ] **Step 1: Add imports for new types** Update the imports (lines 3-16) to include binding DTOs and VO: ```python """规则管理控制器。""" from fastapi_common.fastapi_common_web.controller import BaseController from fastapi_common.fastapi_common_web.domain.responses import Result from fastapi_modules.fastapi_leaudit.domian.Dto.ruleBindingDto import ( RuleBindingCreateDTO, RuleBindingUpdateDTO, ) from fastapi_modules.fastapi_leaudit.domian.Dto.rulePublishDto import RulePublishDTO from fastapi_modules.fastapi_leaudit.domian.Dto.ruleValidateDto import RuleValidateDTO from fastapi_modules.fastapi_leaudit.domian.Dto.ruleVersionCreateDto import RuleVersionCreateDTO from fastapi_modules.fastapi_leaudit.domian.vo.ruleVo import ( RuleBindingVO, RuleContentVO, RuleSetVO, RuleValidationVO, RuleVersionVO, ) from fastapi_modules.fastapi_leaudit.services import IRuleService from fastapi_modules.fastapi_leaudit.services.impl.ruleServiceImpl import RuleServiceImpl ``` - [ ] **Step 2: Add 4 endpoint definitions inside __init__** Add after the `RollbackRuleVersion` endpoint (after line 82, before the closing of `__init__`): ```python # ── Rule Type Bindings ────────────────────────────────────── @self.router.get("/bindings", response_model=Result[list[RuleBindingVO]]) async def ListBindings(ruleType: str | None = None): """列出规则类型绑定。可按规则类型过滤。""" Data = await self.RuleService.ListBindings(RuleType=ruleType) return Result.success(data=Data) @self.router.post("/{RuleType}/bindings", response_model=Result[RuleBindingVO]) async def CreateBinding(RuleType: str, body: RuleBindingCreateDTO): """创建规则类型绑定。""" Data = await self.RuleService.CreateBinding( DocTypeId=body.docTypeId, RuleSetId=body.ruleSetId, BindingMode=body.bindingMode, Priority=body.priority, DocTypeCode=body.docTypeCode, Note=body.note, ) return Result.success(data=Data) @self.router.put("/bindings/{BindingId}", response_model=Result[RuleBindingVO]) async def UpdateBinding(BindingId: int, body: RuleBindingUpdateDTO): """更新规则类型绑定。""" Data = await self.RuleService.UpdateBinding( BindingId=BindingId, IsActive=body.isActive, Priority=body.priority, BindingMode=body.bindingMode, Note=body.note, ) return Result.success(data=Data) @self.router.delete("/bindings/{BindingId}", response_model=Result[None]) async def DeleteBinding(BindingId: int): """删除规则类型绑定。""" await self.RuleService.DeleteBinding(BindingId=BindingId) return Result.success() ``` - [ ] **Step 3: Syntax check** ```bash cd /home/wren-dev/Porject/leaudit-platform && python -m compileall fastapi_modules/fastapi_leaudit/controllers/ruleController.py ``` Expected: Compile successful. - [ ] **Step 4: Commit** ```bash cd /home/wren-dev/Porject/leaudit-platform git add fastapi_modules/fastapi_leaudit/controllers/ruleController.py git commit -m "feat: add rule type binding CRUD endpoints to RuleController" ``` --- ### Task 7: Verification — Cross-Module Import Check **Files:** None (verification only) - [ ] **Step 1: Verify all modified modules compile together** ```bash cd /home/wren-dev/Porject/leaudit-platform && python -c " from fastapi_modules.fastapi_leaudit.domian.Dto.ruleBindingDto import RuleBindingCreateDTO, RuleBindingUpdateDTO from fastapi_modules.fastapi_leaudit.domian.vo.ruleVo import RuleBindingVO from fastapi_modules.fastapi_leaudit.services.ruleService import IRuleService from fastapi_modules.fastapi_leaudit.services.impl.ruleServiceImpl import RuleServiceImpl from fastapi_modules.fastapi_leaudit.leaudit_bridge.storage_adapter import StorageAdapter print('All imports OK') " ``` Expected: `All imports OK` - [ ] **Step 2: Verify the double finalize fix — confirm finalize_run is the only terminal state writer** ```bash cd /home/wren-dev/Porject/leaudit-platform && grep -n "result_status\|finished_at" fastapi_modules/fastapi_leaudit/leaudit_bridge/storage_adapter.py ``` Expected output should show `result_status` and `finished_at` ONLY in `finalize_run` and `fail_run`, NOT in `save_evaluation_results`. - [ ] **Step 3: Commit final verification** ```bash cd /home/wren-dev/Porject/leaudit-platform git add -A git diff --cached --stat git commit -m "chore: verify cross-module imports and finalize consistency" ```