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,1036 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import shutil
6
+ import hashlib
7
+ import tempfile
8
+ from dataclasses import dataclass, asdict, field
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from multi_agent_brief.contracts.schemas.audit_report import AuditReportContract
14
+ from multi_agent_brief.outputs.naming import render_output_stem
15
+ from multi_agent_brief.outputs.reader_projection import (
16
+ build_reader_clean_report,
17
+ build_reader_projection,
18
+ )
19
+ from multi_agent_brief.outputs.source_appendix import cited_claim_ids
20
+ from multi_agent_brief.product.policy_gate_adapter import policy_forbidden_phrases
21
+ from multi_agent_brief.product.quality_closeout import quality_panel_closeout_projection
22
+ from multi_agent_brief.product.template_conformance import project_workspace_report_template_conformance
23
+
24
+ _AUDIT_CLAIM_ID_RE = re.compile(
25
+ r"(?<![A-Za-z0-9_])(?:CL-\d{3,}|CLM-\d{3,}|SYN_CLAIM_[A-Z0-9_-]+|CLAIM_[A-Z0-9_-]+)(?![A-Za-z0-9_])"
26
+ )
27
+
28
+
29
+ @dataclass
30
+ class FinalizeResult:
31
+ """Result of the reader-facing delivery finalization step."""
32
+
33
+ status: str
34
+ audited_brief: str
35
+ reader_brief: str
36
+ named_reader_brief: str = ""
37
+ reader_docx: str = ""
38
+ named_reader_docx: str = ""
39
+ docx_generation: str = "not_requested"
40
+ stripped_src_marker_count: int = 0
41
+ source_appendix: str = ""
42
+ source_appendix_generation: str = "not_requested"
43
+ source_appendix_requested_by: str = "none"
44
+ source_appendix_mode: str = "separate"
45
+ source_appendix_source_count: int = 0
46
+ source_appendix_cited_claim_count: int = 0
47
+ source_appendix_resolved_claim_count: int = 0
48
+ source_appendix_warnings: list[str] | None = None
49
+ source_appendix_claim_map: dict[str, dict[str, str]] = field(default_factory=dict)
50
+ source_appendix_trace: str = ""
51
+ source_appendix_trace_generation: str = "not_available"
52
+ source_appendix_trace_source_count: int = 0
53
+ source_appendix_trace_span_count: int = 0
54
+ source_appendix_trace_warnings: list[str] | None = None
55
+ delivery_markdown: str = ""
56
+ delivery_docx: str = ""
57
+ delivery_latest_dir: str = ""
58
+ delivery_artifacts: list[str] = field(default_factory=list)
59
+ delivery_artifact_sha256: dict[str, str] = field(default_factory=dict)
60
+ delivery_snapshot_dir: str = ""
61
+ delivery_snapshot_artifacts: list[str] = field(default_factory=list)
62
+ delivery_snapshot_artifact_sha256: dict[str, str] = field(default_factory=dict)
63
+ delivery_snapshot_semantics: str = "convenience_copy_not_immutable_archive"
64
+ delivery_snapshot_error: str = ""
65
+ template_rendering: dict[str, Any] = field(default_factory=dict)
66
+ report_template_conformance: dict[str, Any] = field(default_factory=dict)
67
+ reader_clean: dict[str, Any] | None = None
68
+ audit_binding: dict[str, Any] | None = None
69
+ policy_gate_adapter: dict[str, Any] = field(default_factory=dict)
70
+ citation_profile: str = "executive"
71
+ citation_profile_source: str = "default"
72
+ citation_profile_runtime_effect: str = "citation_profile_resolution_only"
73
+ citation_profile_reader_citation_style: str = "source_label"
74
+ citation_profile_reader_metadata_level: str = "low_interference"
75
+ citation_profile_audit_trace_level: str = "complete_when_available"
76
+ citation_profile_delivery_exposes_internal_ids: bool = False
77
+ citation_profile_delivery_exposes_local_paths: bool = False
78
+ citation_profile_audit_bundle_keeps_trace: bool = True
79
+ citation_profile_warnings: list[str] = field(default_factory=list)
80
+ quality_panel_closeout: dict[str, Any] = field(default_factory=dict)
81
+
82
+ def to_dict(self) -> dict[str, Any]:
83
+ data = asdict(self)
84
+ if data["source_appendix_warnings"] is None:
85
+ data["source_appendix_warnings"] = []
86
+ if data["source_appendix_trace_warnings"] is None:
87
+ data["source_appendix_trace_warnings"] = []
88
+ if data["reader_clean"] is None:
89
+ data["reader_clean"] = _empty_reader_clean_report()
90
+ if data["audit_binding"] is None:
91
+ data["audit_binding"] = _empty_audit_binding_report()
92
+ if not data["quality_panel_closeout"]:
93
+ data["quality_panel_closeout"] = quality_panel_closeout_projection(
94
+ finalize_report=data,
95
+ )
96
+ return data
97
+
98
+
99
+ _FINALIZE_REPORT_PATH_FIELDS = (
100
+ "audited_brief",
101
+ "reader_brief",
102
+ "named_reader_brief",
103
+ "reader_docx",
104
+ "named_reader_docx",
105
+ "source_appendix",
106
+ "source_appendix_trace",
107
+ "delivery_markdown",
108
+ "delivery_docx",
109
+ "delivery_latest_dir",
110
+ "delivery_snapshot_dir",
111
+ )
112
+ _FINALIZE_REPORT_PATH_LIST_FIELDS = (
113
+ "delivery_artifacts",
114
+ "delivery_snapshot_artifacts",
115
+ )
116
+ _FINALIZE_REPORT_PATH_HASH_FIELDS = (
117
+ "delivery_artifact_sha256",
118
+ "delivery_snapshot_artifact_sha256",
119
+ )
120
+
121
+
122
+ def _finalize_report_payload(
123
+ result: FinalizeResult,
124
+ *,
125
+ output_dir: Path,
126
+ workspace_dir: Path | None = None,
127
+ ) -> dict[str, Any]:
128
+ """Serialize finalize reports with workspace-relative path identities."""
129
+
130
+ payload = result.to_dict()
131
+ workspace = (
132
+ workspace_dir.resolve()
133
+ if workspace_dir is not None
134
+ else output_dir.resolve().parent
135
+ )
136
+ for field_name in _FINALIZE_REPORT_PATH_FIELDS:
137
+ payload[field_name] = _workspace_relative_value(workspace, payload.get(field_name))
138
+ for field_name in _FINALIZE_REPORT_PATH_LIST_FIELDS:
139
+ values = payload.get(field_name) or []
140
+ payload[field_name] = [_workspace_relative_value(workspace, value) for value in values]
141
+ for field_name in _FINALIZE_REPORT_PATH_HASH_FIELDS:
142
+ values = payload.get(field_name) or {}
143
+ if isinstance(values, dict):
144
+ payload[field_name] = {
145
+ _workspace_relative_value(workspace, key): value
146
+ for key, value in values.items()
147
+ }
148
+ return payload
149
+
150
+
151
+ def _write_finalize_report(
152
+ path: Path,
153
+ result: FinalizeResult,
154
+ *,
155
+ output_dir: Path,
156
+ workspace_dir: Path | None = None,
157
+ ) -> None:
158
+ path.write_text(
159
+ json.dumps(
160
+ _finalize_report_payload(result, output_dir=output_dir, workspace_dir=workspace_dir),
161
+ ensure_ascii=False,
162
+ indent=2,
163
+ ),
164
+ encoding="utf-8",
165
+ )
166
+
167
+
168
+ def _workspace_relative_value(workspace: Path, value: Any) -> str:
169
+ if not isinstance(value, str) or not value.strip():
170
+ return ""
171
+ path = Path(value).expanduser()
172
+ if not path.is_absolute():
173
+ return path.as_posix()
174
+ try:
175
+ return path.resolve().relative_to(workspace).as_posix()
176
+ except ValueError:
177
+ return str(value)
178
+
179
+
180
+ def finalize_reader_outputs(
181
+ *,
182
+ output_dir: str | Path,
183
+ project_name: str,
184
+ output_formats: list[str] | tuple[str, ...] | None = None,
185
+ output_footer: str = "",
186
+ output_named_outputs: bool = True,
187
+ output_filename_template: str = "",
188
+ output_filename_tokens: dict[str, str] | None = None,
189
+ docx_template: str = "default",
190
+ source_appendix_config: dict[str, Any] | None = None,
191
+ workspace_dir: str | Path | None = None,
192
+ ) -> FinalizeResult:
193
+ """Regenerate reader-facing artifacts from internal audited markdown.
194
+
195
+ Agent-assisted workflows must finish any owner-stage edits to
196
+ ``output/intermediate/audited_brief.md`` before finalization begins.
197
+ This function reads that frozen audited artifact as input and writes only
198
+ reader-facing Markdown/DOCX delivery outputs plus finalize control records,
199
+ with internal claim citations rendered as reader-facing source labels when
200
+ Claim Ledger evidence is available.
201
+ """
202
+ out = Path(output_dir)
203
+ workspace = (
204
+ Path(workspace_dir).expanduser().resolve()
205
+ if workspace_dir is not None
206
+ else out.resolve().parent
207
+ )
208
+ intermediate_dir = out / "intermediate"
209
+ formats = set(output_formats or ["markdown"])
210
+ appendix_path = out / "source_appendix.md"
211
+ appendix_trace_path = out / "source_appendix_trace.md"
212
+ if appendix_path.exists():
213
+ appendix_path.unlink()
214
+ if appendix_trace_path.exists():
215
+ appendix_trace_path.unlink()
216
+ with tempfile.TemporaryDirectory(
217
+ prefix=".briefloop-finalize-candidate-",
218
+ dir=intermediate_dir,
219
+ ) as candidate_root_text:
220
+ projection = build_reader_projection(
221
+ output_dir=out,
222
+ output_formats=formats,
223
+ source_appendix_config=source_appendix_config or {},
224
+ workspace_dir=workspace,
225
+ candidate_root=Path(candidate_root_text),
226
+ )
227
+ if projection.source_appendix:
228
+ shutil.copyfile(Path(projection.source_appendix), appendix_path)
229
+ if projection.source_appendix_trace:
230
+ shutil.copyfile(Path(projection.source_appendix_trace), appendix_trace_path)
231
+ audited_path = Path(projection.audited_brief)
232
+ audited_markdown = projection.audited_markdown
233
+ stripped_count = projection.stripped_src_marker_count
234
+ reader_markdown = projection.reader_markdown
235
+
236
+ brief_path = out / "brief.md"
237
+ brief_path.write_text(reader_markdown, encoding="utf-8")
238
+
239
+ named_brief_path: Path | None = None
240
+ if output_named_outputs:
241
+ tokens = dict(output_filename_tokens or {})
242
+ tokens.setdefault("project_name", project_name)
243
+ tokens.setdefault("title", project_name)
244
+ named_stem = render_output_stem(output_filename_template, tokens) if output_filename_template else ""
245
+ if named_stem:
246
+ named_brief_path = out / f"{named_stem}.md"
247
+ if named_brief_path != brief_path:
248
+ named_brief_path.write_text(reader_markdown, encoding="utf-8")
249
+
250
+ docx_status = "not_requested"
251
+ docx_path = out / "brief.docx"
252
+ named_docx_path: Path | None = None
253
+ if "docx" in formats:
254
+ # Avoid leaving a stale rendered file that may still contain internal
255
+ # [src:<claim_id>] markers when regeneration fails or dependencies are missing.
256
+ if docx_path.exists():
257
+ docx_path.unlink()
258
+ if named_brief_path is not None:
259
+ possible_named_docx = named_brief_path.with_suffix(".docx")
260
+ if possible_named_docx.exists():
261
+ possible_named_docx.unlink()
262
+ try:
263
+ from multi_agent_brief.outputs.ib_docx import convert
264
+
265
+ convert(
266
+ brief_path,
267
+ docx_path,
268
+ title=project_name,
269
+ footer=output_footer or None,
270
+ template=docx_template or "default",
271
+ )
272
+ docx_status = "generated"
273
+ if named_brief_path is not None and named_brief_path.stem != "brief":
274
+ named_docx_path = named_brief_path.with_suffix(".docx")
275
+ shutil.copyfile(docx_path, named_docx_path)
276
+ except ImportError:
277
+ docx_status = "skipped_missing_dependency"
278
+ except Exception:
279
+ docx_status = "failed"
280
+ raise
281
+
282
+ result = FinalizeResult(
283
+ status="pass",
284
+ audited_brief=str(audited_path),
285
+ reader_brief=str(brief_path),
286
+ named_reader_brief=str(named_brief_path or ""),
287
+ reader_docx=str(docx_path) if docx_path.exists() else "",
288
+ named_reader_docx=str(named_docx_path or ""),
289
+ docx_generation=docx_status,
290
+ stripped_src_marker_count=stripped_count,
291
+ source_appendix=str(appendix_path) if projection.source_appendix and appendix_path.exists() else "",
292
+ source_appendix_generation=projection.source_appendix_generation,
293
+ source_appendix_requested_by=projection.source_appendix_requested_by,
294
+ source_appendix_mode=projection.source_appendix_mode,
295
+ source_appendix_source_count=projection.source_appendix_source_count,
296
+ source_appendix_cited_claim_count=projection.source_appendix_cited_claim_count,
297
+ source_appendix_resolved_claim_count=projection.source_appendix_resolved_claim_count,
298
+ source_appendix_warnings=projection.source_appendix_warnings,
299
+ source_appendix_claim_map=projection.source_appendix_claim_map,
300
+ source_appendix_trace=(
301
+ str(appendix_trace_path)
302
+ if projection.source_appendix_trace and appendix_trace_path.exists()
303
+ else ""
304
+ ),
305
+ source_appendix_trace_generation=projection.source_appendix_trace_generation,
306
+ source_appendix_trace_source_count=projection.source_appendix_trace_source_count,
307
+ source_appendix_trace_span_count=projection.source_appendix_trace_span_count,
308
+ source_appendix_trace_warnings=projection.source_appendix_trace_warnings,
309
+ template_rendering=projection.template_rendering,
310
+ audit_binding=_audit_binding_report(
311
+ intermediate_dir=intermediate_dir,
312
+ audited_markdown=audited_markdown,
313
+ ),
314
+ policy_gate_adapter=projection.policy_gate_adapter,
315
+ citation_profile=projection.citation_profile,
316
+ citation_profile_source=projection.citation_profile_source,
317
+ citation_profile_runtime_effect=projection.citation_profile_runtime_effect,
318
+ citation_profile_reader_citation_style=projection.citation_profile_reader_citation_style,
319
+ citation_profile_reader_metadata_level=projection.citation_profile_reader_metadata_level,
320
+ citation_profile_audit_trace_level=projection.citation_profile_audit_trace_level,
321
+ citation_profile_delivery_exposes_internal_ids=projection.citation_profile_delivery_exposes_internal_ids,
322
+ citation_profile_delivery_exposes_local_paths=projection.citation_profile_delivery_exposes_local_paths,
323
+ citation_profile_audit_bundle_keeps_trace=projection.citation_profile_audit_bundle_keeps_trace,
324
+ citation_profile_warnings=projection.citation_profile_warnings,
325
+ )
326
+ delivery_bundle = _build_delivery_bundle(
327
+ output_dir=out,
328
+ brief_path=brief_path,
329
+ docx_path=docx_path if docx_path.exists() else None,
330
+ named_docx_path=named_docx_path,
331
+ )
332
+ result.delivery_markdown = delivery_bundle["delivery_markdown"]
333
+ result.delivery_docx = delivery_bundle["delivery_docx"]
334
+ result.delivery_latest_dir = delivery_bundle["delivery_latest_dir"]
335
+ result.delivery_artifacts = delivery_bundle["delivery_artifacts"]
336
+ result.delivery_artifact_sha256 = delivery_bundle["delivery_artifact_sha256"]
337
+
338
+ report_path = intermediate_dir / "finalize_report.json"
339
+ reader_clean = build_reader_clean_report(
340
+ markdown_paths=[
341
+ path
342
+ for path in (
343
+ Path(result.delivery_markdown) if result.delivery_markdown else None,
344
+ appendix_path if appendix_path.exists() else None,
345
+ )
346
+ if path is not None and path.exists()
347
+ ],
348
+ docx_paths=[
349
+ path
350
+ for path in (Path(result.delivery_docx) if result.delivery_docx else None,)
351
+ if path is not None and path.exists()
352
+ ],
353
+ forbidden_phrases=policy_forbidden_phrases(projection.policy_gate_adapter),
354
+ )
355
+ result.reader_clean = reader_clean
356
+ result.report_template_conformance = project_workspace_report_template_conformance(workspace)
357
+ if result.audit_binding and result.audit_binding.get("status") == "fail":
358
+ result.status = "fail"
359
+ _write_finalize_report(report_path, result, output_dir=out, workspace_dir=workspace)
360
+ findings = result.audit_binding.get("findings") or []
361
+ raise RuntimeError(
362
+ "Audit report binding check failed: "
363
+ f"{len(findings)} blocking finding{'s' if len(findings) != 1 else ''}. "
364
+ f"See {report_path}."
365
+ )
366
+ if reader_clean["status"] == "fail":
367
+ result.status = "fail"
368
+ _write_finalize_report(report_path, result, output_dir=out, workspace_dir=workspace)
369
+ finding_count = len(reader_clean.get("sample_findings", []))
370
+ total_count = sum(
371
+ int(value)
372
+ for key, value in reader_clean.items()
373
+ if key.endswith("_count") and isinstance(value, int)
374
+ )
375
+ raise RuntimeError(
376
+ "Reader final output gate failed: "
377
+ f"{total_count or finding_count} blocking residue findings. "
378
+ f"See {report_path}."
379
+ )
380
+ try:
381
+ delivery_snapshot = _build_delivery_snapshot(
382
+ output_dir=out,
383
+ delivery_artifacts=[Path(path) for path in result.delivery_artifacts],
384
+ )
385
+ except Exception as exc:
386
+ result.status = "fail"
387
+ result.delivery_snapshot_error = f"{type(exc).__name__}: {exc}"
388
+ _write_finalize_report(report_path, result, output_dir=out, workspace_dir=workspace)
389
+ raise RuntimeError(
390
+ f"Delivery snapshot creation failed. See {report_path}."
391
+ ) from exc
392
+ result.delivery_snapshot_dir = delivery_snapshot["delivery_snapshot_dir"]
393
+ result.delivery_snapshot_artifacts = delivery_snapshot["delivery_snapshot_artifacts"]
394
+ result.delivery_snapshot_artifact_sha256 = delivery_snapshot["delivery_snapshot_artifact_sha256"]
395
+ _write_finalize_report(report_path, result, output_dir=out, workspace_dir=workspace)
396
+ return result
397
+
398
+
399
+ def _build_delivery_bundle(
400
+ *,
401
+ output_dir: Path,
402
+ brief_path: Path,
403
+ docx_path: Path | None,
404
+ named_docx_path: Path | None,
405
+ ) -> dict[str, Any]:
406
+ """Create the minimal reader delivery bundle.
407
+
408
+ The root output files remain available for compatibility and audit/debugging,
409
+ while ``output/delivery`` is the only surface intended to be handed to the
410
+ final reader.
411
+ """
412
+ delivery_dir = output_dir / "delivery"
413
+ if delivery_dir.exists():
414
+ for child in delivery_dir.iterdir():
415
+ if child.is_dir():
416
+ shutil.rmtree(child)
417
+ else:
418
+ child.unlink()
419
+ else:
420
+ delivery_dir.mkdir(parents=True, exist_ok=True)
421
+
422
+ delivery_markdown = delivery_dir / "brief.md"
423
+ shutil.copyfile(brief_path, delivery_markdown)
424
+
425
+ delivery_docx = ""
426
+ source_docx = named_docx_path if named_docx_path and named_docx_path.exists() else docx_path
427
+ if source_docx is not None and source_docx.exists():
428
+ docx_target = delivery_dir / source_docx.name
429
+ shutil.copyfile(source_docx, docx_target)
430
+ delivery_docx = str(docx_target)
431
+
432
+ artifacts = [str(delivery_markdown)]
433
+ if delivery_docx:
434
+ artifacts.append(delivery_docx)
435
+ artifact_sha256 = {artifact: _sha256_file(Path(artifact)) for artifact in artifacts}
436
+ return {
437
+ "delivery_latest_dir": str(delivery_dir),
438
+ "delivery_markdown": str(delivery_markdown),
439
+ "delivery_docx": delivery_docx,
440
+ "delivery_artifacts": artifacts,
441
+ "delivery_artifact_sha256": artifact_sha256,
442
+ }
443
+
444
+
445
+ def _build_delivery_snapshot(
446
+ *,
447
+ output_dir: Path,
448
+ delivery_artifacts: list[Path],
449
+ ) -> dict[str, Any]:
450
+ """Copy latest reader delivery files into a convenience snapshot directory."""
451
+ history_root = output_dir / "delivery-history"
452
+ history_root.mkdir(parents=True, exist_ok=True)
453
+ snapshot_name = _delivery_snapshot_name(output_dir)
454
+ snapshot_dir = _resolve_delivery_snapshot_dir(
455
+ history_root=history_root,
456
+ snapshot_name=snapshot_name,
457
+ delivery_artifacts=delivery_artifacts,
458
+ )
459
+ snapshot_dir.mkdir(parents=True, exist_ok=True)
460
+ snapshot_artifacts: list[str] = []
461
+ for artifact in delivery_artifacts:
462
+ target = snapshot_dir / artifact.name
463
+ if not target.exists() or _sha256_file(target) != _sha256_file(artifact):
464
+ shutil.copyfile(artifact, target)
465
+ snapshot_artifacts.append(str(target))
466
+ return {
467
+ "delivery_snapshot_dir": str(snapshot_dir),
468
+ "delivery_snapshot_artifacts": snapshot_artifacts,
469
+ "delivery_snapshot_artifact_sha256": {
470
+ artifact: _sha256_file(Path(artifact)) for artifact in snapshot_artifacts
471
+ },
472
+ }
473
+
474
+
475
+ def _delivery_snapshot_name(output_dir: Path) -> str:
476
+ manifest = output_dir / "intermediate" / "runtime_manifest.json"
477
+ if manifest.exists():
478
+ try:
479
+ payload = json.loads(manifest.read_text(encoding="utf-8"))
480
+ run_id = str(payload.get("run_id") or "").strip()
481
+ if run_id:
482
+ return _safe_snapshot_component(run_id)
483
+ except (OSError, json.JSONDecodeError):
484
+ pass
485
+ return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
486
+
487
+
488
+ def _safe_snapshot_component(value: str) -> str:
489
+ cleaned = re.sub(r"[^A-Za-z0-9._-]+", "-", value).strip(".-")
490
+ return cleaned or datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
491
+
492
+
493
+ def _resolve_delivery_snapshot_dir(
494
+ *,
495
+ history_root: Path,
496
+ snapshot_name: str,
497
+ delivery_artifacts: list[Path],
498
+ ) -> Path:
499
+ candidate = history_root / snapshot_name
500
+ if not candidate.exists() or _snapshot_matches_delivery(candidate, delivery_artifacts):
501
+ return candidate
502
+ suffix = 2
503
+ while True:
504
+ suffixed = history_root / f"{snapshot_name}-{suffix}"
505
+ if not suffixed.exists() or _snapshot_matches_delivery(suffixed, delivery_artifacts):
506
+ return suffixed
507
+ suffix += 1
508
+
509
+
510
+ def _snapshot_matches_delivery(snapshot_dir: Path, delivery_artifacts: list[Path]) -> bool:
511
+ if not snapshot_dir.is_dir():
512
+ return False
513
+ expected_names = {artifact.name for artifact in delivery_artifacts}
514
+ existing_files = {path.name for path in snapshot_dir.iterdir() if path.is_file()}
515
+ if existing_files != expected_names:
516
+ return False
517
+ return all(
518
+ (snapshot_dir / artifact.name).exists()
519
+ and _sha256_file(snapshot_dir / artifact.name) == _sha256_file(artifact)
520
+ for artifact in delivery_artifacts
521
+ )
522
+
523
+
524
+ def _sha256_file(path: Path) -> str:
525
+ h = hashlib.sha256()
526
+ with path.open("rb") as f:
527
+ for chunk in iter(lambda: f.read(1024 * 1024), b""):
528
+ h.update(chunk)
529
+ return h.hexdigest()
530
+
531
+
532
+ def _empty_reader_clean_report() -> dict[str, Any]:
533
+ return {
534
+ "status": "pass",
535
+ "src_marker_count": 0,
536
+ "bare_claim_id_count": 0,
537
+ "source_id_count": 0,
538
+ "process_wording_count": 0,
539
+ "blank_citation_row_count": 0,
540
+ "local_path_count": 0,
541
+ "debug_residue_count": 0,
542
+ "atom_id_count": 0,
543
+ "policy_forbidden_phrase_count": 0,
544
+ "sample_findings": [],
545
+ }
546
+
547
+
548
+ def _empty_audit_binding_report() -> dict[str, Any]:
549
+ return {
550
+ "status": "not_checked",
551
+ "claim_ledger_sha256": "",
552
+ "audited_brief_sha256": "",
553
+ "audit_report_sha256": "",
554
+ "ledger_claim_count": 0,
555
+ "audited_brief_cited_claim_count": 0,
556
+ "findings": [],
557
+ "warnings": [],
558
+ }
559
+
560
+
561
+ @dataclass(frozen=True)
562
+ class FinalizeAuditBindingVerdict:
563
+ """Single interpretation of finalize_report audit binding."""
564
+
565
+ kind: str
566
+ value: dict[str, Any]
567
+ reasons: tuple[str, ...] = ()
568
+
569
+
570
+ def interpret_finalize_audit_binding(
571
+ *,
572
+ workspace: str | Path,
573
+ finalize_report: dict[str, Any],
574
+ ) -> FinalizeAuditBindingVerdict:
575
+ binding = finalize_report.get("audit_binding")
576
+ if not isinstance(binding, dict):
577
+ return _degraded_finalize_audit_binding("finalize_report.json audit_binding.status must be pass.")
578
+ if binding.get("status") != "pass":
579
+ return _degraded_finalize_audit_binding("finalize_report.json audit_binding.status must be pass.")
580
+ findings = binding.get("findings", [])
581
+ if not isinstance(findings, list):
582
+ return _degraded_finalize_audit_binding(
583
+ "finalize_report.json audit_binding.findings must be a list when present."
584
+ )
585
+ if findings:
586
+ return _degraded_finalize_audit_binding(
587
+ "finalize_report.json audit_binding.findings must be empty when audit_binding.status is pass."
588
+ )
589
+
590
+ ws = Path(workspace).expanduser().resolve()
591
+ reasons: list[str] = []
592
+ audit_binding_paths = {
593
+ "claim_ledger_sha256": ws / "output" / "intermediate" / "claim_ledger.json",
594
+ "audited_brief_sha256": ws / "output" / "intermediate" / "audited_brief.md",
595
+ "audit_report_sha256": ws / "output" / "intermediate" / "audit_report.json",
596
+ }
597
+ for field, path in audit_binding_paths.items():
598
+ value = binding.get(field)
599
+ if not isinstance(value, str) or not value.strip():
600
+ reasons.append(f"finalize_report.json audit_binding.{field} is required.")
601
+ continue
602
+ if not path.exists():
603
+ reasons.append(f"finalize_report.json audit_binding.{field} target is missing: {path}.")
604
+ continue
605
+ try:
606
+ current_sha256 = _sha256_file(path)
607
+ except OSError as exc:
608
+ reasons.append(f"finalize_report.json audit_binding.{field} target could not be read: {exc}")
609
+ continue
610
+ if value != current_sha256:
611
+ reasons.append(f"finalize_report.json audit_binding.{field} does not match current artifact bytes.")
612
+ if reasons:
613
+ return FinalizeAuditBindingVerdict(
614
+ kind="degraded",
615
+ value=_finalize_audit_binding_projection(binding, status="blocked"),
616
+ reasons=tuple(reasons),
617
+ )
618
+ return FinalizeAuditBindingVerdict(
619
+ kind="canonical",
620
+ value=_finalize_audit_binding_projection(binding, status="pass"),
621
+ )
622
+
623
+
624
+ def project_finalize_audit_binding_for_read(verdict: FinalizeAuditBindingVerdict) -> dict[str, Any]:
625
+ return dict(verdict.value)
626
+
627
+
628
+ def require_finalize_audit_binding_pass(verdict: FinalizeAuditBindingVerdict) -> list[str]:
629
+ if verdict.kind == "canonical":
630
+ return []
631
+ return list(verdict.reasons)
632
+
633
+
634
+ def _degraded_finalize_audit_binding(reason: str) -> FinalizeAuditBindingVerdict:
635
+ return FinalizeAuditBindingVerdict(
636
+ kind="degraded",
637
+ value={
638
+ "status": "blocked",
639
+ "binding_status": "unknown",
640
+ },
641
+ reasons=(reason,),
642
+ )
643
+
644
+
645
+ def _finalize_audit_binding_projection(binding: dict[str, Any], *, status: str) -> dict[str, Any]:
646
+ return {
647
+ "status": status,
648
+ "binding_status": binding.get("status"),
649
+ "claim_ledger_sha256": binding.get("claim_ledger_sha256"),
650
+ "audited_brief_sha256": binding.get("audited_brief_sha256"),
651
+ "audit_report_sha256": binding.get("audit_report_sha256"),
652
+ }
653
+
654
+
655
+ def _audit_binding_report(
656
+ *,
657
+ intermediate_dir: Path,
658
+ audited_markdown: str,
659
+ ) -> dict[str, Any]:
660
+ """Check that an existing audit report still matches this run.
661
+
662
+ This is a control-plane consistency check, not semantic fact verification.
663
+ It catches stale audit reports that mention old Claim Ledger entries or
664
+ still require repair while finalize would otherwise publish clean reader
665
+ artifacts.
666
+ """
667
+ ledger_path = intermediate_dir / "claim_ledger.json"
668
+ audited_brief_path = intermediate_dir / "audited_brief.md"
669
+ audit_report_path = intermediate_dir / "audit_report.json"
670
+ cited_ids = cited_claim_ids(audited_markdown)
671
+ ledger_ids: set[str] = set()
672
+ ledger_sha = ""
673
+ audited_brief_sha = _sha256_file(audited_brief_path) if audited_brief_path.exists() else ""
674
+ audit_sha = ""
675
+ findings: list[dict[str, Any]] = []
676
+ warnings: list[dict[str, Any]] = []
677
+
678
+ if ledger_path.exists():
679
+ ledger_sha = _sha256_file(ledger_path)
680
+ try:
681
+ ledger_ids = _claim_ids_from_ledger(ledger_path)
682
+ except (json.JSONDecodeError, TypeError, ValueError) as exc:
683
+ findings.append(
684
+ {
685
+ "kind": "malformed_claim_ledger",
686
+ "message": f"Claim Ledger could not be read for audit binding: {exc}",
687
+ }
688
+ )
689
+ if audit_report_path.exists():
690
+ audit_sha = _sha256_file(audit_report_path)
691
+
692
+ report: dict[str, Any] = {
693
+ "status": "pass",
694
+ "claim_ledger_sha256": ledger_sha,
695
+ "audited_brief_sha256": audited_brief_sha,
696
+ "audit_report_sha256": audit_sha,
697
+ "ledger_claim_count": len(ledger_ids),
698
+ "audited_brief_cited_claim_count": len(cited_ids),
699
+ "findings": findings,
700
+ "warnings": warnings,
701
+ }
702
+
703
+ if not audit_report_path.exists():
704
+ report["status"] = "not_checked"
705
+ return report
706
+
707
+ try:
708
+ payload = json.loads(audit_report_path.read_text(encoding="utf-8"))
709
+ except json.JSONDecodeError as exc:
710
+ findings.append(
711
+ {
712
+ "kind": "malformed_audit_report",
713
+ "message": f"audit_report.json is not valid JSON: {exc}",
714
+ }
715
+ )
716
+ report["status"] = "fail"
717
+ return report
718
+ if not isinstance(payload, dict):
719
+ findings.append(
720
+ {
721
+ "kind": "malformed_audit_report",
722
+ "message": "audit_report.json must be an object.",
723
+ }
724
+ )
725
+ report["status"] = "fail"
726
+ return report
727
+
728
+ contract_violations = AuditReportContract.validate(payload)
729
+ contract_errors = [violation for violation in contract_violations if violation.severity == "error"]
730
+ if contract_errors:
731
+ findings.append(
732
+ {
733
+ "kind": "malformed_audit_report_contract",
734
+ "message": "audit_report.json does not satisfy the current AuditReport contract.",
735
+ "count": len(contract_errors),
736
+ "errors": [
737
+ {
738
+ "field": violation.field,
739
+ "error": violation.error,
740
+ }
741
+ for violation in contract_errors[:10]
742
+ ],
743
+ }
744
+ )
745
+
746
+ audit_status = str(payload.get("audit_status") or "").strip().lower()
747
+ if audit_status == "fail":
748
+ findings.append(
749
+ {
750
+ "kind": "audit_status_failed",
751
+ "message": "audit_report.json records audit_status=fail.",
752
+ }
753
+ )
754
+
755
+ structured_findings = payload.get("findings")
756
+ if isinstance(structured_findings, list):
757
+ high_findings: list[dict[str, Any]] = [
758
+ finding
759
+ for finding in structured_findings
760
+ if isinstance(finding, dict) and str(finding.get("severity") or "").strip().lower() == "high"
761
+ ]
762
+ if high_findings:
763
+ findings.append(
764
+ {
765
+ "kind": "audit_high_severity_findings",
766
+ "message": "audit_report.json contains high-severity findings.",
767
+ "count": len(high_findings),
768
+ "finding_ids": [
769
+ str(finding.get("finding_id") or "")
770
+ for finding in high_findings[:10]
771
+ if str(finding.get("finding_id") or "")
772
+ ],
773
+ }
774
+ )
775
+
776
+ if payload.get("passed") is False:
777
+ findings.append(
778
+ {
779
+ "kind": "audit_not_passed",
780
+ "message": "audit_report.json records passed=false.",
781
+ }
782
+ )
783
+ blocking_findings = payload.get("blocking_findings")
784
+ if isinstance(blocking_findings, list) and blocking_findings:
785
+ findings.append(
786
+ {
787
+ "kind": "audit_blocking_findings",
788
+ "message": "audit_report.json still contains blocking_findings.",
789
+ "count": len(blocking_findings),
790
+ }
791
+ )
792
+ recommendation = str(payload.get("recommendation") or "").strip().lower()
793
+ if recommendation in {"repair_required", "block", "blocked", "reject"}:
794
+ findings.append(
795
+ {
796
+ "kind": "audit_recommendation_not_ready",
797
+ "message": f"audit_report.json recommendation is {recommendation}.",
798
+ }
799
+ )
800
+
801
+ metadata = payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {}
802
+ if isinstance(metadata.get("audit_binding"), dict):
803
+ warnings.append(
804
+ {
805
+ "kind": "legacy_audit_binding_ignored",
806
+ "message": (
807
+ "audit_report.json metadata.audit_binding is deprecated and ignored; "
808
+ "Python workflow_state/artifact_registry hashes are authoritative."
809
+ ),
810
+ }
811
+ )
812
+
813
+ _append_python_audit_binding_findings(
814
+ findings=findings,
815
+ intermediate_dir=intermediate_dir,
816
+ current_claim_ledger_sha256=ledger_sha,
817
+ current_audited_brief_sha256=audited_brief_sha,
818
+ current_audit_report_sha256=audit_sha,
819
+ )
820
+
821
+ mentioned_ids = set(_AUDIT_CLAIM_ID_RE.findall(json.dumps(payload, ensure_ascii=False)))
822
+ unknown_ids = sorted(mentioned_ids - ledger_ids)
823
+ if unknown_ids:
824
+ findings.append(
825
+ {
826
+ "kind": "audit_mentions_unknown_claim_ids",
827
+ "message": "audit_report.json mentions claim IDs that are absent from the current Claim Ledger.",
828
+ "claim_ids": unknown_ids[:20],
829
+ }
830
+ )
831
+
832
+ report["findings"] = findings
833
+ report["warnings"] = warnings
834
+ report["status"] = "fail" if findings else "pass"
835
+ return report
836
+
837
+
838
+ def _claim_ids_from_ledger(path: Path) -> set[str]:
839
+ data = json.loads(path.read_text(encoding="utf-8"))
840
+ if isinstance(data, dict):
841
+ raw_claims = data.get("claims") or data.get("items") or []
842
+ else:
843
+ raw_claims = data
844
+ if not isinstance(raw_claims, list):
845
+ raise ValueError("Claim Ledger must be a list or object with claims/items.")
846
+ claim_ids: set[str] = set()
847
+ for item in raw_claims:
848
+ if not isinstance(item, dict):
849
+ continue
850
+ claim_id = item.get("claim_id")
851
+ if isinstance(claim_id, str) and claim_id.strip():
852
+ claim_ids.add(claim_id.strip())
853
+ return claim_ids
854
+
855
+
856
+ def _read_json_object_for_binding(
857
+ path: Path,
858
+ findings: list[dict[str, Any]],
859
+ *,
860
+ kind: str,
861
+ ) -> dict[str, Any] | None:
862
+ if not path.exists():
863
+ findings.append(
864
+ {
865
+ "kind": "audit_binding_missing_control_chain",
866
+ "field": kind,
867
+ "message": f"{path.name} is required for Python audit binding verification.",
868
+ }
869
+ )
870
+ return None
871
+ try:
872
+ payload = json.loads(path.read_text(encoding="utf-8"))
873
+ except json.JSONDecodeError as exc:
874
+ findings.append(
875
+ {
876
+ "kind": "audit_binding_malformed_control_chain",
877
+ "field": kind,
878
+ "message": f"{path.name} is not valid JSON: {exc}",
879
+ }
880
+ )
881
+ return None
882
+ if not isinstance(payload, dict):
883
+ findings.append(
884
+ {
885
+ "kind": "audit_binding_malformed_control_chain",
886
+ "field": kind,
887
+ "message": f"{path.name} must be a JSON object.",
888
+ }
889
+ )
890
+ return None
891
+ return payload
892
+
893
+
894
+ def _control_chain_sha(
895
+ findings: list[dict[str, Any]],
896
+ *,
897
+ value: Any,
898
+ field: str,
899
+ source: str,
900
+ ) -> str | None:
901
+ if not isinstance(value, str) or not value.strip():
902
+ findings.append(
903
+ {
904
+ "kind": "audit_binding_missing_control_chain",
905
+ "field": field,
906
+ "source": source,
907
+ "message": f"{source} is missing required sha256 field {field}.",
908
+ }
909
+ )
910
+ return None
911
+ return value.strip()
912
+
913
+
914
+ def _append_sha_binding_finding(
915
+ findings: list[dict[str, Any]],
916
+ *,
917
+ field: str,
918
+ registry_sha256: str | None,
919
+ workflow_sha256: str | None,
920
+ current_sha256: str,
921
+ ) -> None:
922
+ if not registry_sha256 or not workflow_sha256:
923
+ return
924
+ if registry_sha256 != workflow_sha256 or registry_sha256 != current_sha256:
925
+ findings.append(
926
+ {
927
+ "kind": "audit_binding_mismatch",
928
+ "field": field,
929
+ "registry_sha256": registry_sha256,
930
+ "workflow_sha256": workflow_sha256,
931
+ "current_sha256": current_sha256,
932
+ "message": (
933
+ f"{field} does not match Python control-chain binding; route repair "
934
+ "back to the owner stage instead of downstream in-place changes."
935
+ ),
936
+ }
937
+ )
938
+
939
+
940
+ def _append_python_audit_binding_findings(
941
+ *,
942
+ findings: list[dict[str, Any]],
943
+ intermediate_dir: Path,
944
+ current_claim_ledger_sha256: str,
945
+ current_audited_brief_sha256: str,
946
+ current_audit_report_sha256: str,
947
+ ) -> None:
948
+ workflow = _read_json_object_for_binding(
949
+ intermediate_dir / "workflow_state.json",
950
+ findings,
951
+ kind="workflow_state",
952
+ )
953
+ registry = _read_json_object_for_binding(
954
+ intermediate_dir / "artifact_registry.json",
955
+ findings,
956
+ kind="artifact_registry",
957
+ )
958
+ if workflow is None or registry is None:
959
+ return
960
+
961
+ artifacts = registry.get("artifacts") if isinstance(registry.get("artifacts"), dict) else {}
962
+ claim_record = artifacts.get("claim_ledger") if isinstance(artifacts.get("claim_ledger"), dict) else {}
963
+ audited_brief_record = artifacts.get("audited_brief") if isinstance(artifacts.get("audited_brief"), dict) else {}
964
+ audit_record = artifacts.get("audit_report") if isinstance(artifacts.get("audit_report"), dict) else {}
965
+ statuses = workflow.get("stage_statuses") if isinstance(workflow.get("stage_statuses"), dict) else {}
966
+ auditor = statuses.get("auditor") if isinstance(statuses.get("auditor"), dict) else {}
967
+ metadata = auditor.get("metadata") if isinstance(auditor.get("metadata"), dict) else {}
968
+ upstream = (
969
+ metadata.get("upstream_artifact_sha256")
970
+ if isinstance(metadata.get("upstream_artifact_sha256"), dict)
971
+ else {}
972
+ )
973
+ produced = (
974
+ metadata.get("produced_artifact_sha256")
975
+ if isinstance(metadata.get("produced_artifact_sha256"), dict)
976
+ else {}
977
+ )
978
+
979
+ registry_ledger_sha = _control_chain_sha(
980
+ findings,
981
+ value=claim_record.get("sha256"),
982
+ field="artifacts.claim_ledger.sha256",
983
+ source="artifact_registry.json",
984
+ )
985
+ workflow_ledger_sha = _control_chain_sha(
986
+ findings,
987
+ value=upstream.get("claim_ledger"),
988
+ field="stage_statuses.auditor.metadata.upstream_artifact_sha256.claim_ledger",
989
+ source="workflow_state.json",
990
+ )
991
+ registry_audited_brief_sha = _control_chain_sha(
992
+ findings,
993
+ value=audited_brief_record.get("sha256"),
994
+ field="artifacts.audited_brief.sha256",
995
+ source="artifact_registry.json",
996
+ )
997
+ workflow_audited_brief_sha = _control_chain_sha(
998
+ findings,
999
+ value=upstream.get("audited_brief"),
1000
+ field="stage_statuses.auditor.metadata.upstream_artifact_sha256.audited_brief",
1001
+ source="workflow_state.json",
1002
+ )
1003
+ registry_audit_sha = _control_chain_sha(
1004
+ findings,
1005
+ value=audit_record.get("sha256"),
1006
+ field="artifacts.audit_report.sha256",
1007
+ source="artifact_registry.json",
1008
+ )
1009
+ workflow_audit_sha = _control_chain_sha(
1010
+ findings,
1011
+ value=produced.get("audit_report"),
1012
+ field="stage_statuses.auditor.metadata.produced_artifact_sha256.audit_report",
1013
+ source="workflow_state.json",
1014
+ )
1015
+
1016
+ _append_sha_binding_finding(
1017
+ findings,
1018
+ field="claim_ledger_sha256",
1019
+ registry_sha256=registry_ledger_sha,
1020
+ workflow_sha256=workflow_ledger_sha,
1021
+ current_sha256=current_claim_ledger_sha256,
1022
+ )
1023
+ _append_sha_binding_finding(
1024
+ findings,
1025
+ field="audited_brief_sha256",
1026
+ registry_sha256=registry_audited_brief_sha,
1027
+ workflow_sha256=workflow_audited_brief_sha,
1028
+ current_sha256=current_audited_brief_sha256,
1029
+ )
1030
+ _append_sha_binding_finding(
1031
+ findings,
1032
+ field="audit_report_sha256",
1033
+ registry_sha256=registry_audit_sha,
1034
+ workflow_sha256=workflow_audit_sha,
1035
+ current_sha256=current_audit_report_sha256,
1036
+ )