本卡屬 FR-147,棒 3 第一張。新 service 與三支端點,給資源庫頁的「任務說明」區塊與 Excel 用。寫圖用 2a 的 helper。
資源庫頁每條評估目標底下要列任務、讓顧問填名稱、描述、指南。專案還沒啟動時任務在流程圖的方塊上(固定一個);啟動後任務在專案的任務表(PM 改過流程就多個)。本卡做後端:一支讀(自動判要讀圖還是讀任務,回同一種形狀)、一支寫(只寫三欄)、一支每條控制項的「已填/任務數」計數給左樹。
infra/readmodel/oscal/resource_library_binding_query.py ever_bound(mf_id)/has_live_project(mf_id);專案啟動 project_start_app_service.py:165 _assert_never_bound 用它。domain/module_frame/service/module_frame_domain_service.py:79 verify_module_frame_exists_by_uid(uid, writable=True)、:98 assert_template_not_audit_locked(template_ssp_id);守衛測試 test/security/code/test_module_frame_write_ownership_guard.py 會查寫入方法有沒有傳 writable=True。infra/readmodel/tasks/job_export_query.py:186 session.query(Wecm.control_id, Wecm.ao_part_id, JobExecution).join(...).filter(Wecm.round_id==round_id, JobExecution.type==USER);wecm 表是 public.workflow_execution_control_mapping。oscal.catalog_control_parts.props 陣列裡 {name:'workflow-template-uid', value:...},查法見 infra/readmodel/oscal/resource_library_copy_query.py:36 list_ao_workflow_uids。infra/flow_control/repository/flow_control_job_repo_impl.py write_template_xml(template_id, xml)——主表與翻譯列一起寫,只寫主表編輯器看不到(prep_job_generation_service.py:262 註解)。app/flow_control/service/job_service.py:331 update_job 守專案 manager,顧問打會 403;要走 flow_control domain service 的 update_job 只帶 name/description/guide(grep def update_job 在 domain 層)。infra/readmodel/module_frame/ao_task_guidance_query.py:bound_project(mf_id) -> {project_id, current_round_id}|None(compliance.projects.module_frame_id 反查,目前輪次照既有「最新一輪」查法,grep get_current_round);list_ao_template_xml(catalog_id, control_code) -> [{part_id, template_id, template_uid, xml}];list_ao_round_jobs(catalog_id, control_code, round_id) -> [{part_id, job_uid, job_id, name, description, guide, status, assignee_names, template_xml, template_job_id}];summary_* 兩支給計數。app/module_frame/service/ao_task_guidance_service.py(@transaction 每個 public method):list_for_control(mf_uid, control_code, locale) 回 {source:'template'|'project', round_uid?, aos:[{part_id, tasks:[{key, order, name, description, guide, status?, assignee_names?}]}]}——template 模式每 AO 一筆 key='<part_id>|<node_id>'、三欄用 2a read_first_task;project 模式依流程順序(BpmnUtils(template_xml).get_job_execution_order() 對 template_job_id)排 order,key=job_uid。save(mf_uid, items:[{key,name,description,guide}], curr_user):先 verify_module_frame_exists_by_uid(writable=True)+assert_template_not_audit_locked;template 模式 write_first_task 後 write_template_xml;project 模式走 domain update_job 只帶三欄。summary(mf_uid) 回 [{control_code, filled, total}],filled=描述或指南非空。api/module_frame/routes/ao_task_guidance_route.py:GET /module-frame/<uid>/control/<control_code>/task-guidance、PUT /module-frame/<uid>/task-guidance(@require_capability('oscal.resource-library.update'))、GET /module-frame/<uid>/task-guidance/summary。註冊到 api/module_frame/__init__.py,DI 接進 di_containers/module_frame/module_frame_containers.py(改 __init__ 必同步 di_containers,Factory 是 lazy、對不上只在被打時炸)。bound_project 查到多個專案時退回 template 模式(只讀圖),不報錯。碎片 case 不寫新 unit test;跑 pytest test/security/code/test_module_frame_write_ownership_guard.py test/test_module_boundaries.py 必綠(新寫入方法沒傳 writable=True 守衛會紅)。
scripts/ 既有產 token 腳本)source=template、每 AO 一筆;PUT 兩筆三欄 → GET 讀回一致;psql 看 workflow_templates.xml 與翻譯表都有新屬性。source=project、PM 改成多站的 AO 多筆且 order 照流程;PUT 改一筆指南 → 專案規劃頁 GET 任務詳情指南同步。