briefloop 0.11.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (424) hide show
  1. briefloop-0.11.12.dist-info/METADATA +473 -0
  2. briefloop-0.11.12.dist-info/RECORD +424 -0
  3. briefloop-0.11.12.dist-info/WHEEL +5 -0
  4. briefloop-0.11.12.dist-info/entry_points.txt +3 -0
  5. briefloop-0.11.12.dist-info/licenses/LICENSE +22 -0
  6. briefloop-0.11.12.dist-info/top_level.txt +1 -0
  7. multi_agent_brief/__init__.py +36 -0
  8. multi_agent_brief/analysis_blocks/__init__.py +11 -0
  9. multi_agent_brief/analysis_blocks/builder.py +206 -0
  10. multi_agent_brief/analysis_blocks/renderer.py +237 -0
  11. multi_agent_brief/analysis_blocks/schemas.py +52 -0
  12. multi_agent_brief/analysis_modules/__init__.py +8 -0
  13. multi_agent_brief/analysis_modules/base.py +87 -0
  14. multi_agent_brief/analysis_modules/market_competitor/__init__.py +68 -0
  15. multi_agent_brief/analysis_modules/market_competitor/auditor.py +290 -0
  16. multi_agent_brief/analysis_modules/market_competitor/config.py +187 -0
  17. multi_agent_brief/analysis_modules/market_competitor/event_builder.py +226 -0
  18. multi_agent_brief/analysis_modules/market_competitor/renderer.py +199 -0
  19. multi_agent_brief/analysis_modules/market_competitor/schemas.py +518 -0
  20. multi_agent_brief/analysis_modules/policy_regulatory/__init__.py +7 -0
  21. multi_agent_brief/analysis_modules/policy_regulatory/audit.py +195 -0
  22. multi_agent_brief/analysis_modules/policy_regulatory/module.py +387 -0
  23. multi_agent_brief/analysis_modules/policy_regulatory/schemas.py +129 -0
  24. multi_agent_brief/analysis_modules/registry.py +73 -0
  25. multi_agent_brief/audience/__init__.py +19 -0
  26. multi_agent_brief/audience/profiles.py +246 -0
  27. multi_agent_brief/audience_memory/__init__.py +23 -0
  28. multi_agent_brief/audience_memory/profile.py +358 -0
  29. multi_agent_brief/audit/__init__.py +36 -0
  30. multi_agent_brief/audit/case_applicability.py +181 -0
  31. multi_agent_brief/audit/deterministic.py +471 -0
  32. multi_agent_brief/audit/editorial_governance.py +404 -0
  33. multi_agent_brief/audit/final_quality.py +764 -0
  34. multi_agent_brief/audit/harness.py +242 -0
  35. multi_agent_brief/audit/interfaces.py +96 -0
  36. multi_agent_brief/audit/limitation_hygiene.py +238 -0
  37. multi_agent_brief/audit/proposal_boundary.py +34 -0
  38. multi_agent_brief/audit/redaction.py +27 -0
  39. multi_agent_brief/audit/rule_packs.py +96 -0
  40. multi_agent_brief/audit/semantic.py +435 -0
  41. multi_agent_brief/capabilities/__init__.py +22 -0
  42. multi_agent_brief/capabilities/catalog.py +215 -0
  43. multi_agent_brief/capabilities/detect.py +199 -0
  44. multi_agent_brief/capabilities/models.py +60 -0
  45. multi_agent_brief/capabilities/recommend.py +206 -0
  46. multi_agent_brief/cli/__init__.py +2 -0
  47. multi_agent_brief/cli/analysis_commands.py +140 -0
  48. multi_agent_brief/cli/approval_commands.py +107 -0
  49. multi_agent_brief/cli/audit_commands.py +59 -0
  50. multi_agent_brief/cli/capability_commands.py +548 -0
  51. multi_agent_brief/cli/claude_commands.py +159 -0
  52. multi_agent_brief/cli/competitors_commands.py +182 -0
  53. multi_agent_brief/cli/controls_commands.py +137 -0
  54. multi_agent_brief/cli/deliver_commands.py +942 -0
  55. multi_agent_brief/cli/eval_cases_commands.py +99 -0
  56. multi_agent_brief/cli/experiments_commands.py +395 -0
  57. multi_agent_brief/cli/feedback_commands.py +192 -0
  58. multi_agent_brief/cli/finalize_commands.py +153 -0
  59. multi_agent_brief/cli/gates_commands.py +174 -0
  60. multi_agent_brief/cli/hermes_commands.py +356 -0
  61. multi_agent_brief/cli/improve_commands.py +224 -0
  62. multi_agent_brief/cli/init_commands.py +564 -0
  63. multi_agent_brief/cli/init_wizard.py +1473 -0
  64. multi_agent_brief/cli/input_commands.py +237 -0
  65. multi_agent_brief/cli/main.py +288 -0
  66. multi_agent_brief/cli/onboard_commands.py +193 -0
  67. multi_agent_brief/cli/product_commands.py +1449 -0
  68. multi_agent_brief/cli/provenance_commands.py +100 -0
  69. multi_agent_brief/cli/release_commands.py +68 -0
  70. multi_agent_brief/cli/repair_commands.py +169 -0
  71. multi_agent_brief/cli/run_commands.py +292 -0
  72. multi_agent_brief/cli/runtime_commands.py +81 -0
  73. multi_agent_brief/cli/secrets_commands.py +232 -0
  74. multi_agent_brief/cli/semantic_support_commands.py +80 -0
  75. multi_agent_brief/cli/sources_commands.py +647 -0
  76. multi_agent_brief/cli/start_commands.py +8 -0
  77. multi_agent_brief/cli/state_commands.py +446 -0
  78. multi_agent_brief/cli/status_commands.py +26 -0
  79. multi_agent_brief/cli/workbuddy_commands.py +103 -0
  80. multi_agent_brief/configs/artifact_contracts.yaml +514 -0
  81. multi_agent_brief/configs/orchestrator_contract.yaml +181 -0
  82. multi_agent_brief/configs/policy_packs/default.yaml +65 -0
  83. multi_agent_brief/configs/policy_profiles/evidence_extract_default.yaml +58 -0
  84. multi_agent_brief/configs/policy_profiles/finance_default.yaml +44 -0
  85. multi_agent_brief/configs/policy_profiles/internet_default.yaml +44 -0
  86. multi_agent_brief/configs/policy_profiles/manufacturing_default.yaml +33 -0
  87. multi_agent_brief/configs/policy_profiles/solar_manufacturing_default.yaml +64 -0
  88. multi_agent_brief/configs/report_packs/evidence_extract.yaml +48 -0
  89. multi_agent_brief/configs/report_packs/management_monthly.yaml +36 -0
  90. multi_agent_brief/configs/report_packs/market_weekly.yaml +36 -0
  91. multi_agent_brief/configs/report_packs/solar_industry_periodic.yaml +50 -0
  92. multi_agent_brief/configs/report_templates/evidence_extract.yaml +55 -0
  93. multi_agent_brief/configs/report_templates/management_monthly.yaml +48 -0
  94. multi_agent_brief/configs/report_templates/market_weekly.yaml +49 -0
  95. multi_agent_brief/configs/report_templates/solar_industry_periodic.yaml +75 -0
  96. multi_agent_brief/configs/stage_specs.yaml +180 -0
  97. multi_agent_brief/contracts/__init__.py +41 -0
  98. multi_agent_brief/contracts/base.py +99 -0
  99. multi_agent_brief/contracts/errors.py +41 -0
  100. multi_agent_brief/contracts/migrations/__init__.py +5 -0
  101. multi_agent_brief/contracts/migrations/claim_v1_to_v2.py +33 -0
  102. multi_agent_brief/contracts/registry.py +159 -0
  103. multi_agent_brief/contracts/role_topology.py +25 -0
  104. multi_agent_brief/contracts/schemas/__init__.py +35 -0
  105. multi_agent_brief/contracts/schemas/analysis_pack.py +135 -0
  106. multi_agent_brief/contracts/schemas/atomic_claim_graph.py +304 -0
  107. multi_agent_brief/contracts/schemas/audit_report.py +80 -0
  108. multi_agent_brief/contracts/schemas/candidate_item.py +51 -0
  109. multi_agent_brief/contracts/schemas/claim.py +197 -0
  110. multi_agent_brief/contracts/schemas/claim_draft.py +386 -0
  111. multi_agent_brief/contracts/schemas/claim_support_matrix.py +334 -0
  112. multi_agent_brief/contracts/schemas/evidence_span_registry.py +270 -0
  113. multi_agent_brief/contracts/schemas/policy_profile.py +247 -0
  114. multi_agent_brief/contracts/schemas/report_spec.py +257 -0
  115. multi_agent_brief/contracts/schemas/semantic_assessment_report.py +480 -0
  116. multi_agent_brief/contracts/schemas/source_evidence_pack_manifest.py +233 -0
  117. multi_agent_brief/contracts/schemas/source_item.py +96 -0
  118. multi_agent_brief/contracts/semantic_assessment_status.py +12 -0
  119. multi_agent_brief/contracts/source_metadata.py +252 -0
  120. multi_agent_brief/contracts/target_contract.py +460 -0
  121. multi_agent_brief/contracts/validator.py +225 -0
  122. multi_agent_brief/controls/__init__.py +23 -0
  123. multi_agent_brief/controls/contract.py +106 -0
  124. multi_agent_brief/controls/switchboard.py +1076 -0
  125. multi_agent_brief/core/__init__.py +2 -0
  126. multi_agent_brief/core/citations.py +14 -0
  127. multi_agent_brief/core/claim_ledger.py +94 -0
  128. multi_agent_brief/core/config.py +191 -0
  129. multi_agent_brief/core/env.py +69 -0
  130. multi_agent_brief/core/previous.py +83 -0
  131. multi_agent_brief/core/schemas.py +181 -0
  132. multi_agent_brief/core/selection.py +189 -0
  133. multi_agent_brief/delivery/__init__.py +6 -0
  134. multi_agent_brief/delivery/base.py +36 -0
  135. multi_agent_brief/delivery/feishu.py +204 -0
  136. multi_agent_brief/delivery/gws.py +237 -0
  137. multi_agent_brief/evaluation_cases/__init__.py +15 -0
  138. multi_agent_brief/evaluation_cases/contract.py +429 -0
  139. multi_agent_brief/evaluation_cases/fixtures/cases/approved_guidance_materialized/workspace/config.yaml +18 -0
  140. multi_agent_brief/evaluation_cases/fixtures/cases/approved_guidance_materialized/workspace/improvement/ledger.jsonl +2 -0
  141. multi_agent_brief/evaluation_cases/fixtures/cases/approved_guidance_materialized/workspace/sources.yaml +2 -0
  142. multi_agent_brief/evaluation_cases/fixtures/cases/approved_guidance_materialized/workspace/user.md +4 -0
  143. multi_agent_brief/evaluation_cases/fixtures/cases/company_event_missing_latest_official_check/workspace/config.yaml +13 -0
  144. multi_agent_brief/evaluation_cases/fixtures/cases/company_event_missing_latest_official_check/workspace/sources.yaml +9 -0
  145. multi_agent_brief/evaluation_cases/fixtures/cases/company_event_missing_latest_official_check/workspace/user.md +4 -0
  146. multi_agent_brief/evaluation_cases/fixtures/cases/control_switchboard_selection_is_not_execution/workspace/config.yaml +12 -0
  147. multi_agent_brief/evaluation_cases/fixtures/cases/control_switchboard_selection_is_not_execution/workspace/sources.yaml +6 -0
  148. multi_agent_brief/evaluation_cases/fixtures/cases/control_switchboard_selection_is_not_execution/workspace/user.md +4 -0
  149. multi_agent_brief/evaluation_cases/fixtures/cases/feedback_triage_required/workspace/config.yaml +6 -0
  150. multi_agent_brief/evaluation_cases/fixtures/cases/feedback_triage_required/workspace/input/human_feedback.md +1 -0
  151. multi_agent_brief/evaluation_cases/fixtures/cases/feedback_triage_required/workspace/sources.yaml +2 -0
  152. multi_agent_brief/evaluation_cases/fixtures/cases/feedback_triage_required/workspace/user.md +3 -0
  153. multi_agent_brief/evaluation_cases/fixtures/cases/final_abstract_quality_warning_surface/workspace/config.yaml +8 -0
  154. multi_agent_brief/evaluation_cases/fixtures/cases/final_abstract_quality_warning_surface/workspace/output/brief.md +14 -0
  155. multi_agent_brief/evaluation_cases/fixtures/cases/final_abstract_quality_warning_surface/workspace/output/intermediate/audit_report.json +1 -0
  156. multi_agent_brief/evaluation_cases/fixtures/cases/final_abstract_quality_warning_surface/workspace/output/intermediate/audited_brief.md +5 -0
  157. multi_agent_brief/evaluation_cases/fixtures/cases/final_abstract_quality_warning_surface/workspace/output/intermediate/candidate_claims.json +1 -0
  158. multi_agent_brief/evaluation_cases/fixtures/cases/final_abstract_quality_warning_surface/workspace/output/intermediate/claim_ledger.json +1 -0
  159. multi_agent_brief/evaluation_cases/fixtures/cases/final_abstract_quality_warning_surface/workspace/output/intermediate/screened_candidates.json +1 -0
  160. multi_agent_brief/evaluation_cases/fixtures/cases/final_abstract_quality_warning_surface/workspace/sources.yaml +2 -0
  161. multi_agent_brief/evaluation_cases/fixtures/cases/final_abstract_quality_warning_surface/workspace/user.md +3 -0
  162. multi_agent_brief/evaluation_cases/fixtures/cases/formal_release_missing_human_approval/workspace/config.yaml +13 -0
  163. multi_agent_brief/evaluation_cases/fixtures/cases/formal_release_missing_human_approval/workspace/sources.yaml +9 -0
  164. multi_agent_brief/evaluation_cases/fixtures/cases/formal_release_missing_human_approval/workspace/user.md +4 -0
  165. multi_agent_brief/evaluation_cases/fixtures/cases/guidance_manifestation_not_observable/workspace/config.yaml +4 -0
  166. multi_agent_brief/evaluation_cases/fixtures/cases/guidance_manifestation_not_observable/workspace/sources.yaml +2 -0
  167. multi_agent_brief/evaluation_cases/fixtures/cases/guidance_manifestation_not_observable/workspace/user.md +3 -0
  168. multi_agent_brief/evaluation_cases/fixtures/cases/media_only_legal_policy_blocks_research_review/workspace/config.yaml +13 -0
  169. multi_agent_brief/evaluation_cases/fixtures/cases/media_only_legal_policy_blocks_research_review/workspace/sources.yaml +9 -0
  170. multi_agent_brief/evaluation_cases/fixtures/cases/media_only_legal_policy_blocks_research_review/workspace/user.md +4 -0
  171. multi_agent_brief/evaluation_cases/fixtures/cases/mixed_metric_scope_support_blocker/workspace/config.yaml +13 -0
  172. multi_agent_brief/evaluation_cases/fixtures/cases/mixed_metric_scope_support_blocker/workspace/sources.yaml +9 -0
  173. multi_agent_brief/evaluation_cases/fixtures/cases/mixed_metric_scope_support_blocker/workspace/user.md +5 -0
  174. multi_agent_brief/evaluation_cases/fixtures/cases/planned_blocking_issue_cannot_continue/workspace/config.yaml +6 -0
  175. multi_agent_brief/evaluation_cases/fixtures/cases/planned_blocking_issue_cannot_continue/workspace/output/intermediate/audited_brief.md +3 -0
  176. multi_agent_brief/evaluation_cases/fixtures/cases/planned_blocking_issue_cannot_continue/workspace/output/intermediate/candidate_claims.json +1 -0
  177. multi_agent_brief/evaluation_cases/fixtures/cases/planned_blocking_issue_cannot_continue/workspace/output/intermediate/claim_ledger.json +1 -0
  178. multi_agent_brief/evaluation_cases/fixtures/cases/planned_blocking_issue_cannot_continue/workspace/output/intermediate/feedback_issues.json +25 -0
  179. multi_agent_brief/evaluation_cases/fixtures/cases/planned_blocking_issue_cannot_continue/workspace/output/intermediate/repair_plan.json +27 -0
  180. multi_agent_brief/evaluation_cases/fixtures/cases/planned_blocking_issue_cannot_continue/workspace/output/intermediate/screened_candidates.json +1 -0
  181. multi_agent_brief/evaluation_cases/fixtures/cases/planned_blocking_issue_cannot_continue/workspace/sources.yaml +2 -0
  182. multi_agent_brief/evaluation_cases/fixtures/cases/planned_blocking_issue_cannot_continue/workspace/user.md +3 -0
  183. multi_agent_brief/evaluation_cases/fixtures/cases/provenance_projection_minimal/workspace/config.yaml +8 -0
  184. multi_agent_brief/evaluation_cases/fixtures/cases/provenance_projection_minimal/workspace/output/intermediate/audit_report.json +1 -0
  185. multi_agent_brief/evaluation_cases/fixtures/cases/provenance_projection_minimal/workspace/output/intermediate/audited_brief.md +3 -0
  186. multi_agent_brief/evaluation_cases/fixtures/cases/provenance_projection_minimal/workspace/output/intermediate/candidate_claims.json +1 -0
  187. multi_agent_brief/evaluation_cases/fixtures/cases/provenance_projection_minimal/workspace/output/intermediate/claim_ledger.json +11 -0
  188. multi_agent_brief/evaluation_cases/fixtures/cases/provenance_projection_minimal/workspace/output/intermediate/quality_gate_report.json +22 -0
  189. multi_agent_brief/evaluation_cases/fixtures/cases/provenance_projection_minimal/workspace/output/intermediate/screened_candidates.json +1 -0
  190. multi_agent_brief/evaluation_cases/fixtures/cases/provenance_projection_minimal/workspace/sources.yaml +2 -0
  191. multi_agent_brief/evaluation_cases/fixtures/cases/provenance_projection_minimal/workspace/user.md +3 -0
  192. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_source_appendix/workspace/config.yaml +13 -0
  193. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_source_appendix/workspace/output/intermediate/audit_report.json +1 -0
  194. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_source_appendix/workspace/output/intermediate/audited_brief.md +5 -0
  195. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_source_appendix/workspace/output/intermediate/candidate_claims.json +1 -0
  196. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_source_appendix/workspace/output/intermediate/claim_ledger.json +40 -0
  197. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_source_appendix/workspace/output/intermediate/screened_candidates.json +1 -0
  198. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_source_appendix/workspace/sources.yaml +3 -0
  199. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_source_appendix/workspace/user.md +3 -0
  200. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_target_relevance/workspace/config.yaml +8 -0
  201. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_target_relevance/workspace/output/brief.md +3 -0
  202. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_target_relevance/workspace/output/intermediate/audit_report.json +1 -0
  203. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_target_relevance/workspace/output/intermediate/audited_brief.md +3 -0
  204. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_target_relevance/workspace/output/intermediate/candidate_claims.json +1 -0
  205. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_target_relevance/workspace/output/intermediate/claim_ledger.json +13 -0
  206. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_target_relevance/workspace/output/intermediate/screened_candidates.json +1 -0
  207. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_target_relevance/workspace/sources.yaml +2 -0
  208. multi_agent_brief/evaluation_cases/fixtures/cases/reader_facing_target_relevance/workspace/user.md +3 -0
  209. multi_agent_brief/evaluation_cases/fixtures/cases/release_readiness_forged_event_blocker/workspace/config.yaml +13 -0
  210. multi_agent_brief/evaluation_cases/fixtures/cases/release_readiness_forged_event_blocker/workspace/output/intermediate/release_readiness_report.json +31 -0
  211. multi_agent_brief/evaluation_cases/fixtures/cases/release_readiness_forged_event_blocker/workspace/sources.yaml +9 -0
  212. multi_agent_brief/evaluation_cases/fixtures/cases/release_readiness_forged_event_blocker/workspace/user.md +4 -0
  213. multi_agent_brief/evaluation_cases/fixtures/cases/reverted_entry_removed_from_next_snapshot/workspace/config.yaml +18 -0
  214. multi_agent_brief/evaluation_cases/fixtures/cases/reverted_entry_removed_from_next_snapshot/workspace/improvement/ledger.jsonl +3 -0
  215. multi_agent_brief/evaluation_cases/fixtures/cases/reverted_entry_removed_from_next_snapshot/workspace/improvement/memory.md +3 -0
  216. multi_agent_brief/evaluation_cases/fixtures/cases/reverted_entry_removed_from_next_snapshot/workspace/output/intermediate/improvement_memory_snapshot.md +3 -0
  217. multi_agent_brief/evaluation_cases/fixtures/cases/reverted_entry_removed_from_next_snapshot/workspace/sources.yaml +2 -0
  218. multi_agent_brief/evaluation_cases/fixtures/cases/reverted_entry_removed_from_next_snapshot/workspace/user.md +4 -0
  219. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/config.yaml +12 -0
  220. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/input/sources/source-001.txt +1 -0
  221. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/brief.md +37 -0
  222. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/delivery/brief.md +37 -0
  223. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/intermediate/atomic_claim_graph.json +17 -0
  224. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/intermediate/audit_report.json +5 -0
  225. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/intermediate/audited_brief.md +37 -0
  226. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/intermediate/claim_ledger.json +20 -0
  227. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/intermediate/evidence_span_registry.json +20 -0
  228. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/intermediate/finalize_report.json +40 -0
  229. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/intermediate/gates/auditor_quality_gate_report.json +42 -0
  230. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/intermediate/gates/finalize_quality_gate_report.json +42 -0
  231. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/intermediate/screened_candidates.json +33 -0
  232. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/source_appendix.md +12 -0
  233. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/output/source_appendix_trace.md +10 -0
  234. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/report_spec.yaml +24 -0
  235. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/sources.yaml +4 -0
  236. multi_agent_brief/evaluation_cases/fixtures/cases/same_evidence_reader_quality_regression/workspace/user.md +3 -0
  237. multi_agent_brief/evaluation_cases/fixtures/cases/source_evidence_pack_blocks_non_evidence_file/workspace/config.yaml +13 -0
  238. multi_agent_brief/evaluation_cases/fixtures/cases/source_evidence_pack_blocks_non_evidence_file/workspace/input/sources/README.md +4 -0
  239. multi_agent_brief/evaluation_cases/fixtures/cases/source_evidence_pack_blocks_non_evidence_file/workspace/output/intermediate/source_evidence_pack_manifest.json +26 -0
  240. multi_agent_brief/evaluation_cases/fixtures/cases/source_evidence_pack_blocks_non_evidence_file/workspace/sources.yaml +9 -0
  241. multi_agent_brief/evaluation_cases/fixtures/cases/source_evidence_pack_blocks_non_evidence_file/workspace/user.md +4 -0
  242. multi_agent_brief/evaluation_cases/fixtures/cases/stale_current_claim/workspace/config.yaml +9 -0
  243. multi_agent_brief/evaluation_cases/fixtures/cases/stale_current_claim/workspace/output/intermediate/audit_report.json +1 -0
  244. multi_agent_brief/evaluation_cases/fixtures/cases/stale_current_claim/workspace/output/intermediate/audited_brief.md +3 -0
  245. multi_agent_brief/evaluation_cases/fixtures/cases/stale_current_claim/workspace/output/intermediate/candidate_claims.json +1 -0
  246. multi_agent_brief/evaluation_cases/fixtures/cases/stale_current_claim/workspace/output/intermediate/claim_ledger.json +13 -0
  247. multi_agent_brief/evaluation_cases/fixtures/cases/stale_current_claim/workspace/output/intermediate/screened_candidates.json +1 -0
  248. multi_agent_brief/evaluation_cases/fixtures/cases/stale_current_claim/workspace/sources.yaml +2 -0
  249. multi_agent_brief/evaluation_cases/fixtures/cases/stale_current_claim/workspace/user.md +3 -0
  250. multi_agent_brief/evaluation_cases/fixtures/cases/third_party_price_snapshot_formal_block/workspace/config.yaml +13 -0
  251. multi_agent_brief/evaluation_cases/fixtures/cases/third_party_price_snapshot_formal_block/workspace/sources.yaml +9 -0
  252. multi_agent_brief/evaluation_cases/fixtures/cases/third_party_price_snapshot_formal_block/workspace/user.md +4 -0
  253. multi_agent_brief/evaluation_cases/fixtures/cases/trajectory_retry_budget_exhausted/workspace/config.yaml +4 -0
  254. multi_agent_brief/evaluation_cases/fixtures/cases/trajectory_retry_budget_exhausted/workspace/sources.yaml +2 -0
  255. multi_agent_brief/evaluation_cases/fixtures/cases/trajectory_retry_budget_exhausted/workspace/user.md +3 -0
  256. multi_agent_brief/evaluation_cases/fixtures/cases/unapproved_entry_not_materialized/workspace/config.yaml +18 -0
  257. multi_agent_brief/evaluation_cases/fixtures/cases/unapproved_entry_not_materialized/workspace/improvement/ledger.jsonl +1 -0
  258. multi_agent_brief/evaluation_cases/fixtures/cases/unapproved_entry_not_materialized/workspace/sources.yaml +2 -0
  259. multi_agent_brief/evaluation_cases/fixtures/cases/unapproved_entry_not_materialized/workspace/user.md +4 -0
  260. multi_agent_brief/evaluation_cases/fixtures/cases/unauthorized_institution_branding_blocks_release/workspace/config.yaml +18 -0
  261. multi_agent_brief/evaluation_cases/fixtures/cases/unauthorized_institution_branding_blocks_release/workspace/sources.yaml +9 -0
  262. multi_agent_brief/evaluation_cases/fixtures/cases/unauthorized_institution_branding_blocks_release/workspace/user.md +5 -0
  263. multi_agent_brief/evaluation_cases/fixtures/cases/unsupported_material_fact/workspace/config.yaml +8 -0
  264. multi_agent_brief/evaluation_cases/fixtures/cases/unsupported_material_fact/workspace/output/intermediate/audit_report.json +1 -0
  265. multi_agent_brief/evaluation_cases/fixtures/cases/unsupported_material_fact/workspace/output/intermediate/audited_brief.md +8 -0
  266. multi_agent_brief/evaluation_cases/fixtures/cases/unsupported_material_fact/workspace/output/intermediate/candidate_claims.json +1 -0
  267. multi_agent_brief/evaluation_cases/fixtures/cases/unsupported_material_fact/workspace/output/intermediate/claim_ledger.json +1 -0
  268. multi_agent_brief/evaluation_cases/fixtures/cases/unsupported_material_fact/workspace/output/intermediate/screened_candidates.json +1 -0
  269. multi_agent_brief/evaluation_cases/fixtures/cases/unsupported_material_fact/workspace/sources.yaml +2 -0
  270. multi_agent_brief/evaluation_cases/fixtures/cases/unsupported_material_fact/workspace/user.md +3 -0
  271. multi_agent_brief/evaluation_cases/fixtures/manifest.yaml +861 -0
  272. multi_agent_brief/evaluation_cases/fixtures.py +20 -0
  273. multi_agent_brief/evaluation_cases/runner.py +1603 -0
  274. multi_agent_brief/experiments/__init__.py +45 -0
  275. multi_agent_brief/experiments/experiment_080.py +5835 -0
  276. multi_agent_brief/experiments/schemas.py +63 -0
  277. multi_agent_brief/experiments/target_contract.py +10 -0
  278. multi_agent_brief/feedback/__init__.py +17 -0
  279. multi_agent_brief/feedback/feedback_contract.py +482 -0
  280. multi_agent_brief/feedback/feedback_state.py +898 -0
  281. multi_agent_brief/hermes/__init__.py +25 -0
  282. multi_agent_brief/hermes/adapter.py +832 -0
  283. multi_agent_brief/improvement/__init__.py +83 -0
  284. multi_agent_brief/improvement/contract.py +1114 -0
  285. multi_agent_brief/improvement/memory.py +451 -0
  286. multi_agent_brief/improvement/product_definition.py +228 -0
  287. multi_agent_brief/improvement/state.py +961 -0
  288. multi_agent_brief/inputs/__init__.py +2 -0
  289. multi_agent_brief/inputs/classifier.py +322 -0
  290. multi_agent_brief/inputs/contracts.py +59 -0
  291. multi_agent_brief/inputs/extractor.py +310 -0
  292. multi_agent_brief/install/__init__.py +2 -0
  293. multi_agent_brief/install/writer.py +97 -0
  294. multi_agent_brief/onboarding/__init__.py +11 -0
  295. multi_agent_brief/onboarding/io.py +103 -0
  296. multi_agent_brief/onboarding/mapper.py +484 -0
  297. multi_agent_brief/onboarding/schema.py +46 -0
  298. multi_agent_brief/orchestrator/__init__.py +2 -0
  299. multi_agent_brief/orchestrator/fact_layer_import.py +352 -0
  300. multi_agent_brief/orchestrator/handoff.py +2209 -0
  301. multi_agent_brief/orchestrator/role_topology.py +42 -0
  302. multi_agent_brief/orchestrator/run_archive.py +888 -0
  303. multi_agent_brief/orchestrator/run_integrity.py +346 -0
  304. multi_agent_brief/orchestrator/runtime_state/__init__.py +70 -0
  305. multi_agent_brief/orchestrator/runtime_state/_io.py +167 -0
  306. multi_agent_brief/orchestrator/runtime_state/_transactions.py +298 -0
  307. multi_agent_brief/orchestrator/runtime_state/artifact_registry.py +1825 -0
  308. multi_agent_brief/orchestrator/runtime_state/atomic_claim_graph.py +96 -0
  309. multi_agent_brief/orchestrator/runtime_state/claim_ledger_freeze.py +510 -0
  310. multi_agent_brief/orchestrator/runtime_state/claim_metadata_enrichment.py +836 -0
  311. multi_agent_brief/orchestrator/runtime_state/claim_support_matrix.py +582 -0
  312. multi_agent_brief/orchestrator/runtime_state/completion_gates.py +672 -0
  313. multi_agent_brief/orchestrator/runtime_state/contracts_loader.py +66 -0
  314. multi_agent_brief/orchestrator/runtime_state/decisions.py +239 -0
  315. multi_agent_brief/orchestrator/runtime_state/errors.py +61 -0
  316. multi_agent_brief/orchestrator/runtime_state/event_log.py +256 -0
  317. multi_agent_brief/orchestrator/runtime_state/evidence_span_registry.py +233 -0
  318. multi_agent_brief/orchestrator/runtime_state/fact_layer.py +755 -0
  319. multi_agent_brief/orchestrator/runtime_state/identity.py +90 -0
  320. multi_agent_brief/orchestrator/runtime_state/lifecycle.py +675 -0
  321. multi_agent_brief/orchestrator/runtime_state/manifest.py +82 -0
  322. multi_agent_brief/orchestrator/runtime_state/operations.py +224 -0
  323. multi_agent_brief/orchestrator/runtime_state/paths.py +38 -0
  324. multi_agent_brief/orchestrator/runtime_state/repair.py +984 -0
  325. multi_agent_brief/orchestrator/runtime_state/safety_surfaces.py +147 -0
  326. multi_agent_brief/orchestrator/runtime_state/semantic_assessment_report.py +692 -0
  327. multi_agent_brief/orchestrator/runtime_state/semantic_support_acceptance.py +683 -0
  328. multi_agent_brief/orchestrator/runtime_state/source_evidence_pack.py +131 -0
  329. multi_agent_brief/orchestrator/runtime_state/stage_completion.py +1320 -0
  330. multi_agent_brief/orchestrator/runtime_state/trajectory.py +200 -0
  331. multi_agent_brief/orchestrator/runtime_state/workflow.py +350 -0
  332. multi_agent_brief/orchestrator/source_evidence.py +43 -0
  333. multi_agent_brief/orchestrator/timing.py +410 -0
  334. multi_agent_brief/orchestrator_contract.py +96 -0
  335. multi_agent_brief/outputs/__init__.py +2 -0
  336. multi_agent_brief/outputs/atomic_reader_projection.py +288 -0
  337. multi_agent_brief/outputs/docx.py +32 -0
  338. multi_agent_brief/outputs/finalize.py +1036 -0
  339. multi_agent_brief/outputs/ib_docx.py +983 -0
  340. multi_agent_brief/outputs/naming.py +38 -0
  341. multi_agent_brief/outputs/pdf.py +20 -0
  342. multi_agent_brief/outputs/reader_final_gate.py +509 -0
  343. multi_agent_brief/outputs/reader_projection.py +397 -0
  344. multi_agent_brief/outputs/source_appendix.py +753 -0
  345. multi_agent_brief/outputs/templates/__init__.py +112 -0
  346. multi_agent_brief/product/__init__.py +49 -0
  347. multi_agent_brief/product/bundle_projection.py +686 -0
  348. multi_agent_brief/product/citation_profile.py +145 -0
  349. multi_agent_brief/product/guidance_manifestation.py +421 -0
  350. multi_agent_brief/product/materiality_selection.py +472 -0
  351. multi_agent_brief/product/policy_gate_adapter.py +116 -0
  352. multi_agent_brief/product/policy_profile.py +39 -0
  353. multi_agent_brief/product/policy_projection.py +161 -0
  354. multi_agent_brief/product/policy_registry.py +83 -0
  355. multi_agent_brief/product/policy_resolver.py +225 -0
  356. multi_agent_brief/product/quality_closeout.py +188 -0
  357. multi_agent_brief/product/quality_panel.py +2217 -0
  358. multi_agent_brief/product/release_approval.py +1034 -0
  359. multi_agent_brief/product/report_pack.py +130 -0
  360. multi_agent_brief/product/report_pack_aliases.py +68 -0
  361. multi_agent_brief/product/report_registry.py +101 -0
  362. multi_agent_brief/product/report_spec.py +181 -0
  363. multi_agent_brief/product/support_wording.py +525 -0
  364. multi_agent_brief/product/template_conformance.py +516 -0
  365. multi_agent_brief/product/template_projection.py +123 -0
  366. multi_agent_brief/product/template_registry.py +244 -0
  367. multi_agent_brief/product/template_render_plan.py +315 -0
  368. multi_agent_brief/product/template_renderer.py +224 -0
  369. multi_agent_brief/product/trajectory_regulation.py +441 -0
  370. multi_agent_brief/provenance/__init__.py +5 -0
  371. multi_agent_brief/provenance/builder.py +841 -0
  372. multi_agent_brief/provenance/contract.py +21 -0
  373. multi_agent_brief/provenance/io.py +144 -0
  374. multi_agent_brief/provenance/model.py +110 -0
  375. multi_agent_brief/provenance/references.py +43 -0
  376. multi_agent_brief/provenance/validator.py +135 -0
  377. multi_agent_brief/quality_gates/__init__.py +19 -0
  378. multi_agent_brief/quality_gates/contract.py +581 -0
  379. multi_agent_brief/quality_gates/state.py +2498 -0
  380. multi_agent_brief/repair/__init__.py +5 -0
  381. multi_agent_brief/repair/router.py +842 -0
  382. multi_agent_brief/runtime_assets.py +431 -0
  383. multi_agent_brief/sources/__init__.py +16 -0
  384. multi_agent_brief/sources/api_filings.py +236 -0
  385. multi_agent_brief/sources/api_news.py +174 -0
  386. multi_agent_brief/sources/base.py +142 -0
  387. multi_agent_brief/sources/cached_package.py +119 -0
  388. multi_agent_brief/sources/cli_provider.py +263 -0
  389. multi_agent_brief/sources/coverage.py +336 -0
  390. multi_agent_brief/sources/decider.py +756 -0
  391. multi_agent_brief/sources/doctor.py +373 -0
  392. multi_agent_brief/sources/evidence_pack.py +364 -0
  393. multi_agent_brief/sources/feishu_provider.py +326 -0
  394. multi_agent_brief/sources/filing_resolver.py +400 -0
  395. multi_agent_brief/sources/industry_packs.py +130 -0
  396. multi_agent_brief/sources/join.py +206 -0
  397. multi_agent_brief/sources/local_signal.py +81 -0
  398. multi_agent_brief/sources/local_signal_planner.py +636 -0
  399. multi_agent_brief/sources/manual.py +259 -0
  400. multi_agent_brief/sources/mcp_provider.py +309 -0
  401. multi_agent_brief/sources/mineru_provider.py +601 -0
  402. multi_agent_brief/sources/normalizer.py +95 -0
  403. multi_agent_brief/sources/opencli_provider.py +282 -0
  404. multi_agent_brief/sources/planner.py +178 -0
  405. multi_agent_brief/sources/registry.py +367 -0
  406. multi_agent_brief/sources/rss.py +190 -0
  407. multi_agent_brief/sources/search_backends/__init__.py +31 -0
  408. multi_agent_brief/sources/search_backends/base.py +51 -0
  409. multi_agent_brief/sources/search_backends/brave.py +192 -0
  410. multi_agent_brief/sources/search_backends/capabilities.py +94 -0
  411. multi_agent_brief/sources/search_backends/exa.py +180 -0
  412. multi_agent_brief/sources/search_backends/firecrawl.py +164 -0
  413. multi_agent_brief/sources/search_backends/serper.py +241 -0
  414. multi_agent_brief/sources/search_backends/tavily.py +124 -0
  415. multi_agent_brief/sources/sourcehub.py +505 -0
  416. multi_agent_brief/sources/web_search.py +267 -0
  417. multi_agent_brief/status.py +966 -0
  418. multi_agent_brief/tools/__init__.py +3 -0
  419. multi_agent_brief/tools/draft_cleanup.py +198 -0
  420. multi_agent_brief/workbuddy/__init__.py +1 -0
  421. multi_agent_brief/workbuddy/diagnose.py +569 -0
  422. multi_agent_brief/workbuddy/skill_pack.py +309 -0
  423. multi_agent_brief/workspace/__init__.py +1 -0
  424. multi_agent_brief/workspace/init_profile.py +45 -0
@@ -0,0 +1,836 @@
1
+ """Claim metadata enrichment transaction.
2
+
3
+ Enriches frozen Claim Ledger entries with provenance metadata from durable
4
+ source-evidence files, under single-writer authority rules. Never alters
5
+ claim text or claim IDs.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import re
12
+ import uuid
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from multi_agent_brief.contracts.source_metadata import (
17
+ normalize_retrieval_source_type,
18
+ normalize_source_category,
19
+ normalize_underlying_evidence_type,
20
+ )
21
+ from multi_agent_brief.core.claim_ledger import ClaimLedger
22
+ from multi_agent_brief.orchestrator_contract import resolve_repo_workdir
23
+ from multi_agent_brief.orchestrator.runtime_state._io import (
24
+ _restore_state_files,
25
+ _sha256_file,
26
+ _snapshot_state_files,
27
+ _write_json_atomic,
28
+ )
29
+ from multi_agent_brief.orchestrator.runtime_state._transactions import (
30
+ _current_run_start_event_exists,
31
+ _load_manifest_and_workflow,
32
+ _preflight_transaction_files,
33
+ _restore_file_paths,
34
+ _sha256_bytes,
35
+ _snapshot_file_paths,
36
+ _write_bytes_atomic,
37
+ )
38
+ from multi_agent_brief.orchestrator.runtime_state.artifact_registry import (
39
+ ARTIFACT_VALID,
40
+ _build_artifact_registry,
41
+ )
42
+ from multi_agent_brief.orchestrator.runtime_state.claim_ledger_freeze import (
43
+ CLAIM_DRAFT_PROVENANCE_METADATA_FIELDS,
44
+ CLAIM_LEDGER_PATH,
45
+ _claim_ledger_bytes,
46
+ _claim_ledger_freeze_reasons,
47
+ )
48
+ from multi_agent_brief.orchestrator.runtime_state.completion_gates import (
49
+ _raise_completion_reasons,
50
+ )
51
+ from multi_agent_brief.orchestrator.runtime_state.contracts_loader import (
52
+ _stage_ids,
53
+ load_artifact_contracts,
54
+ load_stage_specs,
55
+ )
56
+ from multi_agent_brief.orchestrator.runtime_state.errors import (
57
+ E_ARTIFACT_INVALID,
58
+ E_ILLEGAL_TRANSITION,
59
+ E_RUNTIME_STATE_NOT_INITIALIZED,
60
+ E_TRANSACTION_INTEGRITY,
61
+ E_TRANSACTION_PARTIAL_WRITE,
62
+ RuntimeStateError,
63
+ )
64
+ from multi_agent_brief.orchestrator.runtime_state.event_log import (
65
+ append_event,
66
+ read_event_log_records_strict,
67
+ )
68
+ from multi_agent_brief.orchestrator.runtime_state.fact_layer import (
69
+ FACT_LAYER_IMPORT_SOURCE_PACK_ARTIFACT_ID,
70
+ _target_workspace_path,
71
+ )
72
+ from multi_agent_brief.orchestrator.runtime_state.identity import utc_now
73
+ from multi_agent_brief.orchestrator.runtime_state.lifecycle import show_runtime_state
74
+ from multi_agent_brief.orchestrator.runtime_state.paths import (
75
+ _require_workspace,
76
+ _workspace_relative,
77
+ runtime_state_paths,
78
+ )
79
+ from multi_agent_brief.orchestrator.runtime_state.workflow import (
80
+ STAGE_COMPLETE,
81
+ STAGE_SKIPPED,
82
+ _stage_status,
83
+ _workflow_is_finalized,
84
+ )
85
+
86
+
87
+ CLAIM_METADATA_ENRICHMENT_ALLOWED_FIELDS = CLAIM_DRAFT_PROVENANCE_METADATA_FIELDS
88
+
89
+
90
+ CLAIM_METADATA_ENRICHMENT_FORBIDDEN_FIELDS = (
91
+ "claim_id",
92
+ "statement",
93
+ "evidence_text",
94
+ "source_id",
95
+ "source_url",
96
+ "claim_type",
97
+ "support_strength",
98
+ "confidence",
99
+ )
100
+
101
+
102
+ CLAIM_LEDGER_METADATA_ENRICHMENT_SCHEMA = "mabw.claim_ledger_metadata_enrichment.v1"
103
+
104
+
105
+ CLAIM_METADATA_REPLACEABLE_DEFAULTS = {
106
+ "retrieval_source_type": {"other"},
107
+ "source_category": {"other"},
108
+ "underlying_evidence_type": {"unknown"},
109
+ }
110
+
111
+
112
+ def _imported_claim_ledger_record(manifest: dict[str, Any]) -> dict[str, Any] | None:
113
+ import_record = manifest.get("fact_layer_import")
114
+ if not isinstance(import_record, dict):
115
+ return None
116
+ imported_files = import_record.get("imported_files")
117
+ if not isinstance(imported_files, list):
118
+ return None
119
+ for record in imported_files:
120
+ if not isinstance(record, dict):
121
+ continue
122
+ if (
123
+ record.get("artifact_id") == "claim_ledger"
124
+ and str(record.get("workspace_path") or "") == CLAIM_LEDGER_PATH.as_posix()
125
+ ):
126
+ return record
127
+ return None
128
+
129
+
130
+ def _valid_imported_claim_ledger_derivation(
131
+ *,
132
+ manifest: dict[str, Any],
133
+ import_record: dict[str, Any],
134
+ current_sha256: str,
135
+ ) -> bool:
136
+ enrichment = manifest.get("claim_ledger_metadata_enrichment")
137
+ if not isinstance(enrichment, dict):
138
+ return False
139
+ imported_sha256 = import_record.get("sha256")
140
+ derives_from_imported_sha = (
141
+ enrichment.get("source_claim_ledger_sha256") == imported_sha256
142
+ or enrichment.get("previous_claim_ledger_sha256") == imported_sha256
143
+ )
144
+ return (
145
+ enrichment.get("schema_version") == CLAIM_LEDGER_METADATA_ENRICHMENT_SCHEMA
146
+ and enrichment.get("status") == "applied"
147
+ and enrichment.get("claim_ledger_path") == CLAIM_LEDGER_PATH.as_posix()
148
+ and derives_from_imported_sha
149
+ and enrichment.get("claim_ledger_sha256") == current_sha256
150
+ )
151
+
152
+
153
+ def _claim_ledger_enrichment_authority(
154
+ *,
155
+ workspace: Path,
156
+ manifest: dict[str, Any],
157
+ ledger_path: Path,
158
+ ) -> dict[str, str]:
159
+ freeze = manifest.get("claim_ledger_freeze")
160
+ if isinstance(freeze, dict):
161
+ freeze_reasons = _claim_ledger_freeze_reasons(
162
+ workspace=workspace, manifest=manifest
163
+ )
164
+ if freeze_reasons:
165
+ _raise_completion_reasons(
166
+ message="Cannot enrich Claim Ledger metadata before Claim Ledger freeze is valid",
167
+ reasons=freeze_reasons,
168
+ error_code=E_TRANSACTION_INTEGRITY,
169
+ details={"stage_id": "claim-ledger"},
170
+ )
171
+ return {
172
+ "kind": "claim_ledger_freeze",
173
+ "source_claim_ledger_sha256": str(freeze.get("claim_ledger_sha256") or ""),
174
+ }
175
+ if freeze is not None:
176
+ raise RuntimeStateError(
177
+ "Claim Ledger freeze metadata is malformed; refusing metadata enrichment.",
178
+ details={"field": "claim_ledger_freeze"},
179
+ error_code=E_TRANSACTION_INTEGRITY,
180
+ )
181
+
182
+ import_record = _imported_claim_ledger_record(manifest)
183
+ if not isinstance(import_record, dict):
184
+ raise RuntimeStateError(
185
+ "Claim metadata enrichment requires a frozen Claim Ledger from local freeze or fact-layer import.",
186
+ details={"required_authority": "claim_ledger_freeze_or_fact_layer_import"},
187
+ error_code=E_TRANSACTION_INTEGRITY,
188
+ )
189
+ if not ledger_path.exists() or not ledger_path.is_file():
190
+ raise RuntimeStateError(
191
+ "Imported Claim Ledger is missing; refusing metadata enrichment.",
192
+ details={"workspace_path": CLAIM_LEDGER_PATH.as_posix()},
193
+ error_code=E_TRANSACTION_INTEGRITY,
194
+ )
195
+ expected_sha = str(import_record.get("sha256") or "")
196
+ expected_size = import_record.get("size_bytes")
197
+ current_sha = _sha256_file(ledger_path)
198
+ current_size = ledger_path.stat().st_size
199
+ if current_sha == expected_sha and (
200
+ not isinstance(expected_size, int) or current_size == expected_size
201
+ ):
202
+ return {
203
+ "kind": "fact_layer_import",
204
+ "source_claim_ledger_sha256": expected_sha,
205
+ }
206
+ if _valid_imported_claim_ledger_derivation(
207
+ manifest=manifest,
208
+ import_record=import_record,
209
+ current_sha256=current_sha,
210
+ ):
211
+ return {
212
+ "kind": "fact_layer_import_derived",
213
+ "source_claim_ledger_sha256": expected_sha,
214
+ }
215
+ raise RuntimeStateError(
216
+ "Current Claim Ledger does not match imported fact-layer authority.",
217
+ details={
218
+ "workspace_path": CLAIM_LEDGER_PATH.as_posix(),
219
+ "expected_sha256": expected_sha,
220
+ "actual_sha256": current_sha,
221
+ "expected_size_bytes": expected_size,
222
+ "actual_size_bytes": current_size,
223
+ },
224
+ error_code=E_TRANSACTION_INTEGRITY,
225
+ )
226
+
227
+
228
+ def _imported_source_evidence_authority(
229
+ manifest: dict[str, Any], *, workspace: Path
230
+ ) -> dict[str, dict[str, Any]]:
231
+ import_record = manifest.get("fact_layer_import")
232
+ if not isinstance(import_record, dict):
233
+ raise RuntimeStateError(
234
+ "Claim metadata enrichment requires imported frozen source evidence.",
235
+ details={"required_manifest_field": "fact_layer_import"},
236
+ error_code=E_TRANSACTION_INTEGRITY,
237
+ )
238
+ imported_files = import_record.get("imported_files")
239
+ if not isinstance(imported_files, list):
240
+ raise RuntimeStateError(
241
+ "Fact layer import metadata is missing imported_files.",
242
+ details={"required_manifest_field": "fact_layer_import.imported_files"},
243
+ error_code=E_TRANSACTION_INTEGRITY,
244
+ )
245
+
246
+ by_source_id: dict[str, dict[str, Any]] = {}
247
+ for record in imported_files:
248
+ if not isinstance(record, dict):
249
+ continue
250
+ if record.get("artifact_id") != FACT_LAYER_IMPORT_SOURCE_PACK_ARTIFACT_ID:
251
+ continue
252
+ workspace_path = str(record.get("workspace_path") or "")
253
+ if not workspace_path.startswith("input/sources/"):
254
+ continue
255
+ source_path = _target_workspace_path(workspace, workspace_path)
256
+ if not source_path.exists() or not source_path.is_file():
257
+ raise RuntimeStateError(
258
+ "Imported source evidence file is missing; refusing metadata enrichment.",
259
+ details={"workspace_path": workspace_path},
260
+ error_code=E_TRANSACTION_INTEGRITY,
261
+ )
262
+ expected_sha = str(record.get("sha256") or "")
263
+ actual_sha = _sha256_file(source_path)
264
+ if not expected_sha or actual_sha != expected_sha:
265
+ raise RuntimeStateError(
266
+ "Imported source evidence hash does not match fact_layer_import metadata.",
267
+ details={
268
+ "workspace_path": workspace_path,
269
+ "expected_sha256": expected_sha,
270
+ "actual_sha256": actual_sha,
271
+ },
272
+ error_code=E_TRANSACTION_INTEGRITY,
273
+ )
274
+ if isinstance(
275
+ record.get("size_bytes"), int
276
+ ) and source_path.stat().st_size != record.get("size_bytes"):
277
+ raise RuntimeStateError(
278
+ "Imported source evidence size does not match fact_layer_import metadata.",
279
+ details={
280
+ "workspace_path": workspace_path,
281
+ "expected_size_bytes": record.get("size_bytes"),
282
+ "actual_size_bytes": source_path.stat().st_size,
283
+ },
284
+ error_code=E_TRANSACTION_INTEGRITY,
285
+ )
286
+ metadata = _source_evidence_metadata_from_file(
287
+ source_path, workspace_path=workspace_path
288
+ )
289
+ if not metadata:
290
+ continue
291
+ source_ids = _source_evidence_ids(source_path, metadata)
292
+ authority = {
293
+ "workspace_path": workspace_path,
294
+ "sha256": actual_sha,
295
+ "metadata": metadata,
296
+ }
297
+ by_source_id.setdefault(workspace_path, authority)
298
+ for source_id in source_ids:
299
+ by_source_id.setdefault(source_id, authority)
300
+
301
+ return by_source_id
302
+
303
+
304
+ def _source_evidence_ids(path: Path, metadata: dict[str, str]) -> set[str]:
305
+ ids: set[str] = set()
306
+ for key in ("source_id", "id"):
307
+ value = metadata.get(key)
308
+ if value:
309
+ ids.add(value)
310
+ stem = path.stem.strip()
311
+ if stem:
312
+ ids.add(stem)
313
+ ids.add(stem.upper().replace("-", "_"))
314
+ ids.add(stem.upper().replace("_", "-"))
315
+ normalized_stem = stem.lower().replace("_", "-")
316
+ match = re.fullmatch(r"(?:source|src)-?([0-9]+)", normalized_stem)
317
+ if match:
318
+ numeric_id = match.group(1)
319
+ ids.add(f"SRC-{numeric_id}")
320
+ ids.add(f"SRC-{numeric_id.zfill(3)}")
321
+ return {item for item in ids if item}
322
+
323
+
324
+ def _source_evidence_metadata_from_file(
325
+ path: Path, *, workspace_path: str
326
+ ) -> dict[str, str]:
327
+ suffix = path.suffix.lower()
328
+ if suffix not in {".json", ".md", ".markdown"}:
329
+ return {"source_path": workspace_path}
330
+ if suffix in {".md", ".markdown"}:
331
+ return _source_evidence_metadata_from_markdown(
332
+ path, workspace_path=workspace_path
333
+ )
334
+ try:
335
+ payload = json.loads(path.read_text(encoding="utf-8"))
336
+ except (json.JSONDecodeError, OSError, UnicodeDecodeError):
337
+ return {"source_path": workspace_path}
338
+ if not isinstance(payload, dict):
339
+ return {"source_path": workspace_path}
340
+
341
+ metadata: dict[str, str] = {"source_path": workspace_path}
342
+ aliases: dict[str, tuple[str, ...]] = {
343
+ "published_at": ("published_at", "publishedAt", "date", "source_published_at"),
344
+ "retrieved_at": ("retrieved_at", "retrievedAt", "accessed_at", "accessedAt"),
345
+ "source_title": ("source_title", "title"),
346
+ "source_name": ("source_name", "name"),
347
+ "publisher": ("publisher", "source_publisher"),
348
+ "source_url": ("source_url", "url"),
349
+ "source_type": ("source_type", "provider_type", "storage_type"),
350
+ "retrieval_source_type": ("retrieval_source_type",),
351
+ "underlying_evidence_type": ("underlying_evidence_type",),
352
+ "raw_underlying_evidence_type": ("raw_underlying_evidence_type",),
353
+ "source_category": ("source_category", "evidence_category"),
354
+ "topic": ("topic", "category"),
355
+ "source_id": ("source_id", "id"),
356
+ }
357
+ for field, names in aliases.items():
358
+ for name in names:
359
+ value = payload.get(name)
360
+ if isinstance(value, str) and value.strip():
361
+ metadata[field] = value.strip()
362
+ break
363
+ _normalize_source_evidence_taxonomy(metadata)
364
+ return metadata
365
+
366
+
367
+ def _source_evidence_metadata_from_markdown(
368
+ path: Path, *, workspace_path: str
369
+ ) -> dict[str, str]:
370
+ metadata: dict[str, str] = {"source_path": workspace_path}
371
+ aliases: dict[str, tuple[str, ...]] = {
372
+ "published_at": ("published", "published_at", "date", "source_published_at"),
373
+ "retrieved_at": ("retrieved", "retrieved_at", "accessed", "accessed_at"),
374
+ "source_title": ("title", "source_title"),
375
+ "source_name": ("source_name", "name"),
376
+ "publisher": ("publisher", "source_publisher"),
377
+ "source_url": ("source_url", "url"),
378
+ "source_type": ("source_type", "provider_type", "storage_type"),
379
+ "retrieval_source_type": ("retrieval_source_type",),
380
+ "underlying_evidence_type": ("underlying_evidence_type",),
381
+ "raw_underlying_evidence_type": ("raw_underlying_evidence_type",),
382
+ "source_category": ("source_category", "evidence_category"),
383
+ "topic": ("topic", "category"),
384
+ "source_id": ("source_id", "source id", "id"),
385
+ }
386
+ key_to_field = {
387
+ alias.lower().replace("-", "_").replace(" ", "_"): field
388
+ for field, field_aliases in aliases.items()
389
+ for alias in field_aliases
390
+ }
391
+ try:
392
+ lines = path.read_text(encoding="utf-8").splitlines()
393
+ except (OSError, UnicodeDecodeError):
394
+ return metadata
395
+ in_code_block = False
396
+ for line in lines:
397
+ stripped = line.strip()
398
+ if stripped.startswith("```"):
399
+ in_code_block = not in_code_block
400
+ continue
401
+ if in_code_block or not stripped:
402
+ continue
403
+ match = re.match(
404
+ r"^\s*(?:[-*]\s*)?([A-Za-z][A-Za-z0-9 _-]{0,48})\s*:\s*(.+?)\s*$", line
405
+ )
406
+ if not match:
407
+ continue
408
+ key = match.group(1).strip().lower().replace("-", "_").replace(" ", "_")
409
+ field = key_to_field.get(key)
410
+ value = match.group(2).strip()
411
+ if field and value and field not in metadata:
412
+ metadata[field] = value
413
+ _normalize_source_evidence_taxonomy(metadata)
414
+ return metadata
415
+
416
+
417
+ def _normalize_source_evidence_taxonomy(metadata: dict[str, str]) -> None:
418
+ raw_underlying = (
419
+ metadata.get("raw_underlying_evidence_type")
420
+ or metadata.get("underlying_evidence_type")
421
+ or metadata.get("source_category")
422
+ or metadata.get("topic")
423
+ or ""
424
+ )
425
+ metadata["retrieval_source_type"] = normalize_retrieval_source_type(
426
+ metadata.get("retrieval_source_type"),
427
+ metadata.get("source_type"),
428
+ raw_underlying,
429
+ )
430
+ if raw_underlying:
431
+ metadata["underlying_evidence_type"] = normalize_underlying_evidence_type(
432
+ raw_underlying
433
+ )
434
+ metadata["source_category"] = normalize_source_category(
435
+ metadata.get("source_category"),
436
+ metadata["underlying_evidence_type"],
437
+ raw_underlying,
438
+ )
439
+ metadata["raw_underlying_evidence_type"] = raw_underlying
440
+
441
+
442
+ def _claims_with_enriched_metadata(
443
+ *,
444
+ claims: list[dict[str, Any]],
445
+ source_authority: dict[str, dict[str, Any]],
446
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
447
+ enriched_claims: list[dict[str, Any]] = []
448
+ enrichment_records: list[dict[str, Any]] = []
449
+ missing_sources: list[str] = []
450
+
451
+ for claim in claims:
452
+ next_claim = dict(claim)
453
+ source_id = str(next_claim.get("source_id") or "").strip()
454
+ current_metadata = dict(next_claim.get("metadata") or {})
455
+ metadata_source_path = current_metadata.get("source_path")
456
+ authority = (
457
+ source_authority.get(metadata_source_path)
458
+ if isinstance(metadata_source_path, str) and metadata_source_path.strip()
459
+ else None
460
+ )
461
+ if authority is None:
462
+ authority = source_authority.get(source_id)
463
+ if authority is None:
464
+ missing_sources.append(source_id)
465
+ enriched_claims.append(next_claim)
466
+ continue
467
+ authority_metadata = authority["metadata"]
468
+ original_metadata = dict(current_metadata)
469
+ changed_fields: list[str] = []
470
+ for field in CLAIM_METADATA_ENRICHMENT_ALLOWED_FIELDS:
471
+ new_value = authority_metadata.get(field)
472
+ if not isinstance(new_value, str) or not new_value.strip():
473
+ continue
474
+ existing = current_metadata.get(field)
475
+ if existing is not None and not isinstance(existing, str):
476
+ raise RuntimeStateError(
477
+ "Claim metadata enrichment found non-string existing metadata.",
478
+ details={
479
+ "claim_id": next_claim.get("claim_id"),
480
+ "source_id": source_id,
481
+ "field": field,
482
+ "existing_type": type(existing).__name__,
483
+ },
484
+ error_code=E_TRANSACTION_INTEGRITY,
485
+ )
486
+ existing_text = existing.strip() if isinstance(existing, str) else ""
487
+ new_text = new_value.strip()
488
+ if existing_text and existing_text != new_text:
489
+ replaceable_values = CLAIM_METADATA_REPLACEABLE_DEFAULTS.get(
490
+ field, set()
491
+ )
492
+ if (
493
+ existing_text in replaceable_values
494
+ and new_text not in replaceable_values
495
+ ):
496
+ current_metadata[field] = new_text
497
+ changed_fields.append(field)
498
+ continue
499
+ raise RuntimeStateError(
500
+ "Claim metadata enrichment would overwrite existing metadata with a different value.",
501
+ details={
502
+ "claim_id": next_claim.get("claim_id"),
503
+ "source_id": source_id,
504
+ "field": field,
505
+ "existing": existing,
506
+ "source_value": new_value,
507
+ },
508
+ error_code=E_TRANSACTION_INTEGRITY,
509
+ )
510
+ if not existing_text:
511
+ current_metadata[field] = new_value.strip()
512
+ changed_fields.append(field)
513
+ changed_fields.extend(
514
+ _sync_enriched_claim_source_fields(
515
+ next_claim=next_claim,
516
+ original_metadata=original_metadata,
517
+ authority_metadata=authority_metadata,
518
+ )
519
+ )
520
+ if changed_fields:
521
+ next_claim["metadata"] = current_metadata
522
+ enrichment_records.append(
523
+ {
524
+ "claim_id": str(next_claim.get("claim_id") or ""),
525
+ "source_id": source_id,
526
+ "fields": sorted(set(changed_fields)),
527
+ "source_workspace_path": authority["workspace_path"],
528
+ "source_sha256": authority["sha256"],
529
+ }
530
+ )
531
+ enriched_claims.append(next_claim)
532
+
533
+ if missing_sources:
534
+ raise RuntimeStateError(
535
+ "Claim metadata enrichment could not find imported source evidence for every claim.",
536
+ details={"missing_source_ids": sorted(set(missing_sources))},
537
+ error_code=E_TRANSACTION_INTEGRITY,
538
+ )
539
+ if not enrichment_records:
540
+ raise RuntimeStateError(
541
+ "Claim metadata enrichment found no missing metadata to add.",
542
+ details={"allowed_fields": list(CLAIM_METADATA_ENRICHMENT_ALLOWED_FIELDS)},
543
+ error_code=E_TRANSACTION_INTEGRITY,
544
+ )
545
+ return enriched_claims, enrichment_records
546
+
547
+
548
+ def _sync_enriched_claim_source_fields(
549
+ *,
550
+ next_claim: dict[str, Any],
551
+ original_metadata: dict[str, Any],
552
+ authority_metadata: dict[str, Any],
553
+ ) -> list[str]:
554
+ """Mirror enriched source identity into Claim top-level fields used by readers."""
555
+
556
+ changed_fields: list[str] = []
557
+
558
+ source_url = authority_metadata.get("source_url")
559
+ if isinstance(source_url, str) and source_url.strip():
560
+ existing_url = next_claim.get("source_url")
561
+ if not (isinstance(existing_url, str) and existing_url.strip()):
562
+ next_claim["source_url"] = source_url.strip()
563
+ changed_fields.append("source_url")
564
+
565
+ source_type = authority_metadata.get("source_type")
566
+ if isinstance(source_type, str) and source_type.strip():
567
+ existing_type = next_claim.get("source_type")
568
+ existing_metadata_type = original_metadata.get("source_type")
569
+ has_explicit_metadata_type = isinstance(existing_metadata_type, str) and bool(
570
+ existing_metadata_type.strip()
571
+ )
572
+ metadata_type_matches_authority = (
573
+ has_explicit_metadata_type
574
+ and existing_metadata_type.strip() == source_type.strip()
575
+ )
576
+ if not (isinstance(existing_type, str) and existing_type.strip()):
577
+ next_claim["source_type"] = source_type.strip()
578
+ changed_fields.append("source_type")
579
+ elif existing_type.strip() == "local_file" and (
580
+ not has_explicit_metadata_type or metadata_type_matches_authority
581
+ ):
582
+ next_claim["source_type"] = source_type.strip()
583
+ changed_fields.append("source_type")
584
+
585
+ return changed_fields
586
+
587
+
588
+ def _workflow_allows_claim_metadata_enrichment(
589
+ workflow: dict[str, Any], stages: list[dict[str, Any]]
590
+ ) -> None:
591
+ if _workflow_is_finalized(workflow):
592
+ raise RuntimeStateError(
593
+ "Cannot enrich Claim Ledger metadata after finalize; start a new run or explicit revision path.",
594
+ details={"current_stage": workflow.get("current_stage")},
595
+ error_code=E_TRANSACTION_INTEGRITY,
596
+ )
597
+ stage_ids = _stage_ids(stages)
598
+ if "claim-ledger" not in stage_ids:
599
+ raise RuntimeStateError(
600
+ "Workflow does not contain claim-ledger stage.",
601
+ details={"known_stages": stage_ids},
602
+ error_code=E_ILLEGAL_TRANSITION,
603
+ )
604
+ claim_index = stage_ids.index("claim-ledger")
605
+ completed_downstream = [
606
+ stage_id
607
+ for stage_id in stage_ids[claim_index + 1 :]
608
+ if _stage_status(workflow, stage_id) in {STAGE_COMPLETE, STAGE_SKIPPED}
609
+ ]
610
+ if completed_downstream:
611
+ raise RuntimeStateError(
612
+ "Cannot enrich Claim Ledger metadata after downstream stages completed; start a new run or owner-stage repair.",
613
+ details={"completed_downstream_stages": completed_downstream},
614
+ error_code=E_TRANSACTION_INTEGRITY,
615
+ )
616
+
617
+
618
+ def _workflow_with_enriched_claim_ledger_hash(
619
+ *,
620
+ workflow: dict[str, Any],
621
+ ledger_sha: str,
622
+ transaction_id: str,
623
+ now: str,
624
+ ) -> dict[str, Any]:
625
+ statuses = dict(workflow.get("stage_statuses") or {})
626
+ claim_status = dict(statuses.get("claim-ledger") or {})
627
+ if claim_status.get("status") == STAGE_COMPLETE:
628
+ metadata = dict(claim_status.get("metadata") or {})
629
+ produced = dict(metadata.get("produced_artifact_sha256") or {})
630
+ produced["claim_ledger"] = ledger_sha
631
+ metadata["produced_artifact_sha256"] = produced
632
+ metadata["claim_ledger_metadata_enrichment"] = {
633
+ "transaction_id": transaction_id,
634
+ "enriched_at": now,
635
+ "claim_ledger_sha256": ledger_sha,
636
+ }
637
+ claim_status["metadata"] = metadata
638
+ statuses["claim-ledger"] = claim_status
639
+ updated = dict(workflow)
640
+ updated["updated_at"] = now
641
+ updated["stage_statuses"] = statuses
642
+ return updated
643
+
644
+
645
+ def enrich_claim_metadata_transaction(
646
+ *,
647
+ workspace: str | Path,
648
+ repo_workdir: str | Path | None = None,
649
+ actor: str = "cli",
650
+ from_source_evidence: bool = True,
651
+ ) -> dict[str, Any]:
652
+ if not from_source_evidence:
653
+ raise RuntimeStateError(
654
+ "Claim metadata enrichment only supports --from-source-evidence.",
655
+ details={"from_source_evidence": from_source_evidence},
656
+ error_code=E_ILLEGAL_TRANSITION,
657
+ )
658
+ ws = _require_workspace(workspace)
659
+ paths = runtime_state_paths(ws)
660
+ _preflight_transaction_files(paths)
661
+ ws, paths, manifest, workflow = _load_manifest_and_workflow(ws)
662
+ if not paths["event_log"].exists():
663
+ raise RuntimeStateError(
664
+ "Event log is required before enriching Claim Ledger metadata.",
665
+ details={"missing": str(paths["event_log"])},
666
+ error_code=E_RUNTIME_STATE_NOT_INITIALIZED,
667
+ )
668
+ event_records = read_event_log_records_strict(paths["event_log"])
669
+ run_id = str(manifest["run_id"])
670
+ if not _current_run_start_event_exists(event_records, run_id):
671
+ raise RuntimeStateError(
672
+ "Event log does not contain a current-run start event; refusing Claim Ledger metadata enrichment.",
673
+ details={"run_id": run_id, "event_log": str(paths["event_log"])},
674
+ error_code=E_TRANSACTION_INTEGRITY,
675
+ )
676
+ repo = resolve_repo_workdir(repo_workdir, workspace=ws)
677
+ stages = load_stage_specs(repo)
678
+ artifacts = load_artifact_contracts(repo)
679
+ _workflow_allows_claim_metadata_enrichment(workflow, stages)
680
+
681
+ ledger_path = ws / CLAIM_LEDGER_PATH
682
+ ledger_authority = _claim_ledger_enrichment_authority(
683
+ workspace=ws,
684
+ manifest=manifest,
685
+ ledger_path=ledger_path,
686
+ )
687
+ try:
688
+ ledger_payload = json.loads(ledger_path.read_text(encoding="utf-8"))
689
+ claims = ClaimLedger._claim_items_from_json(ledger_payload)
690
+ except (json.JSONDecodeError, OSError, UnicodeDecodeError, ValueError) as exc:
691
+ raise RuntimeStateError(
692
+ "Claim Ledger is not readable for metadata enrichment.",
693
+ details={"path": _workspace_relative(ws, ledger_path), "reason": str(exc)},
694
+ error_code=E_ARTIFACT_INVALID,
695
+ ) from exc
696
+
697
+ source_authority = _imported_source_evidence_authority(manifest, workspace=ws)
698
+ enriched_claims, enrichment_records = _claims_with_enriched_metadata(
699
+ claims=claims,
700
+ source_authority=source_authority,
701
+ )
702
+ ledger_bytes = _claim_ledger_bytes(enriched_claims)
703
+ previous_sha = _sha256_file(ledger_path)
704
+ ledger_sha = _sha256_bytes(ledger_bytes)
705
+ if previous_sha == ledger_sha:
706
+ raise RuntimeStateError(
707
+ "Claim metadata enrichment produced identical Claim Ledger bytes.",
708
+ details={"claim_ledger_sha256": ledger_sha},
709
+ error_code=E_TRANSACTION_INTEGRITY,
710
+ )
711
+
712
+ transaction_id = uuid.uuid4().hex
713
+ now = utc_now()
714
+ next_manifest = dict(manifest)
715
+ if isinstance(next_manifest.get("claim_ledger_freeze"), dict):
716
+ freeze = dict(next_manifest["claim_ledger_freeze"])
717
+ freeze["claim_ledger_sha256"] = ledger_sha
718
+ freeze["metadata_enriched_at"] = now
719
+ freeze["metadata_enrichment_transaction_id"] = transaction_id
720
+ next_manifest["claim_ledger_freeze"] = freeze
721
+ enrichment_record = {
722
+ "schema_version": CLAIM_LEDGER_METADATA_ENRICHMENT_SCHEMA,
723
+ "status": "applied",
724
+ "enriched_at": now,
725
+ "transaction_id": transaction_id,
726
+ "source": "fact_layer_imported_source_evidence",
727
+ "claim_ledger_authority": ledger_authority["kind"],
728
+ "source_claim_ledger_sha256": ledger_authority["source_claim_ledger_sha256"],
729
+ "claim_ledger_path": _workspace_relative(ws, ledger_path),
730
+ "previous_claim_ledger_sha256": previous_sha,
731
+ "claim_ledger_sha256": ledger_sha,
732
+ "allowed_fields": list(CLAIM_METADATA_ENRICHMENT_ALLOWED_FIELDS),
733
+ "forbidden_fields": list(CLAIM_METADATA_ENRICHMENT_FORBIDDEN_FIELDS),
734
+ "enriched_claim_count": len(enrichment_records),
735
+ "enriched_claims": enrichment_records,
736
+ }
737
+ next_manifest["claim_ledger_metadata_enrichment"] = enrichment_record
738
+ existing_history = next_manifest.get("claim_ledger_metadata_enrichments")
739
+ history = list(existing_history) if isinstance(existing_history, list) else []
740
+ history.append(enrichment_record)
741
+ next_manifest["claim_ledger_metadata_enrichments"] = history
742
+ next_manifest["updated_at"] = now
743
+ next_workflow = _workflow_with_enriched_claim_ledger_hash(
744
+ workflow=workflow,
745
+ ledger_sha=ledger_sha,
746
+ transaction_id=transaction_id,
747
+ now=now,
748
+ )
749
+
750
+ file_snapshots = _snapshot_file_paths([ledger_path])
751
+ state_snapshots = _snapshot_state_files(
752
+ paths, ("runtime_manifest", "artifact_registry", "workflow_state", "event_log")
753
+ )
754
+ try:
755
+ _write_bytes_atomic(ledger_path, ledger_bytes)
756
+ registry = _build_artifact_registry(
757
+ workspace=ws,
758
+ run_id=run_id,
759
+ artifacts=artifacts,
760
+ workflow=next_workflow,
761
+ updated_at=now,
762
+ )
763
+ ledger_record = (registry.get("artifacts") or {}).get("claim_ledger") or {}
764
+ if (
765
+ ledger_record.get("status") != ARTIFACT_VALID
766
+ or ledger_record.get("sha256") != ledger_sha
767
+ ):
768
+ raise RuntimeStateError(
769
+ "Enriched Claim Ledger failed artifact validation.",
770
+ details={
771
+ "artifact_id": "claim_ledger",
772
+ "status": ledger_record.get("status"),
773
+ "validation_result": ledger_record.get("validation_result"),
774
+ "expected_sha256": ledger_sha,
775
+ "actual_sha256": ledger_record.get("sha256"),
776
+ },
777
+ error_code=E_ARTIFACT_INVALID,
778
+ )
779
+ _write_json_atomic(paths["runtime_manifest"], next_manifest)
780
+ _write_json_atomic(paths["artifact_registry"], registry)
781
+ _write_json_atomic(paths["workflow_state"], next_workflow)
782
+ append_event(
783
+ workspace=ws,
784
+ run_id=run_id,
785
+ event_type="claim_ledger_metadata_enriched",
786
+ actor=actor,
787
+ stage_id="claim-ledger",
788
+ artifact_id="claim_ledger",
789
+ reason="Claim Ledger metadata enriched from imported source evidence.",
790
+ metadata={
791
+ "transaction_id": transaction_id,
792
+ "claim_ledger_path": _workspace_relative(ws, ledger_path),
793
+ "previous_claim_ledger_sha256": previous_sha,
794
+ "claim_ledger_sha256": ledger_sha,
795
+ "enriched_claim_count": len(enrichment_records),
796
+ "allowed_fields": list(CLAIM_METADATA_ENRICHMENT_ALLOWED_FIELDS),
797
+ },
798
+ )
799
+ except RuntimeStateError as exc:
800
+ try:
801
+ _restore_state_files(paths, state_snapshots)
802
+ _restore_file_paths(
803
+ file_snapshots,
804
+ rollback_message="Claim metadata enrichment rollback failed after partial write.",
805
+ )
806
+ except RuntimeStateError as rollback_exc:
807
+ raise RuntimeStateError(
808
+ "Claim metadata enrichment partially wrote files and failed rollback.",
809
+ details={
810
+ "transaction_id": transaction_id,
811
+ "enrichment_error": str(exc),
812
+ "enrichment_details": exc.details,
813
+ "rollback_error": str(rollback_exc),
814
+ "rollback_details": rollback_exc.details,
815
+ },
816
+ error_code=E_TRANSACTION_PARTIAL_WRITE,
817
+ ) from rollback_exc
818
+ raise RuntimeStateError(
819
+ "Claim metadata enrichment failed; written files were restored.",
820
+ details={
821
+ "transaction_id": transaction_id,
822
+ "enrichment_error": str(exc),
823
+ "enrichment_details": exc.details,
824
+ "restored": True,
825
+ },
826
+ error_code=exc.error_code,
827
+ ) from exc
828
+
829
+ state = show_runtime_state(workspace=ws)
830
+ state["claim_ledger_metadata_enrichment"] = enrichment_record
831
+ state["transaction"] = {
832
+ "transaction_id": transaction_id,
833
+ "stage_id": "claim-ledger",
834
+ "decision": "enrich_claim_metadata",
835
+ }
836
+ return state