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,756 @@
1
+ """Source Decider: resolve llm_decide profile into concrete source candidates.
2
+
3
+ Reads source_discovery from sources.yaml, searches for relevant sources,
4
+ and generates source_candidates.yaml for user review before merging.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from datetime import date, datetime, timedelta
9
+ from pathlib import Path
10
+ import re
11
+ from typing import Any
12
+ from urllib.parse import urlparse
13
+
14
+ try:
15
+ import yaml
16
+ except ModuleNotFoundError:
17
+ yaml = None # type: ignore[assignment]
18
+
19
+ from multi_agent_brief.sources.local_signal_planner import (
20
+ build_local_signal_tasks,
21
+ generate_collector_tasks,
22
+ )
23
+
24
+ E_SOURCE_CANDIDATES_PLAN_ONLY = "E_SOURCE_CANDIDATES_PLAN_ONLY"
25
+ E_SOURCE_CANDIDATES_UNSUPPORTED_SCHEMA = "E_SOURCE_CANDIDATES_UNSUPPORTED_SCHEMA"
26
+
27
+
28
+ class SourceCandidatesError(RuntimeError):
29
+ """Raised when source_candidates.yaml cannot be safely consumed."""
30
+
31
+ def __init__(
32
+ self,
33
+ message: str,
34
+ *,
35
+ error_code: str,
36
+ details: dict[str, Any] | None = None,
37
+ ) -> None:
38
+ super().__init__(message)
39
+ self.error_code = error_code
40
+ self.details = details or {}
41
+
42
+
43
+
44
+ def _load_yaml(path: Path) -> dict[str, Any]:
45
+ """Load YAML file, return empty dict if unavailable."""
46
+ if yaml is None:
47
+ raise RuntimeError("PyYAML is required: pip install pyyaml")
48
+ if not path.exists():
49
+ return {}
50
+ with open(path, encoding="utf-8") as f:
51
+ return yaml.safe_load(f) or {}
52
+
53
+
54
+ def _save_yaml(path: Path, data: dict[str, Any]) -> None:
55
+ """Save dict as YAML."""
56
+ if yaml is None:
57
+ raise RuntimeError("PyYAML is required: pip install pyyaml")
58
+ with open(path, "w", encoding="utf-8") as f:
59
+ yaml.dump(data, f, allow_unicode=True, default_flow_style=False, sort_keys=False)
60
+
61
+
62
+ def _validate_mergeable_candidates(candidates: Any) -> None:
63
+ """Reject non-evidence candidate schemas before mutating sources.yaml."""
64
+ if not isinstance(candidates, dict):
65
+ raise SourceCandidatesError(
66
+ "source_candidates.yaml uses an unsupported schema: expected a mapping.",
67
+ error_code=E_SOURCE_CANDIDATES_UNSUPPORTED_SCHEMA,
68
+ )
69
+
70
+ artifact_type = str(candidates.get("artifact_type") or "")
71
+ evidence_status = str(candidates.get("evidence_status") or "")
72
+ if artifact_type == "source_plan_only" or evidence_status == "not_evidence":
73
+ raise SourceCandidatesError(
74
+ "source_candidates.yaml is a source plan only; it cannot be "
75
+ "merged into sources.yaml as evidence. Collect approved sources "
76
+ "into input/sources/ or use a supported candidate schema.",
77
+ error_code=E_SOURCE_CANDIDATES_PLAN_ONLY,
78
+ details={
79
+ "artifact_type": artifact_type,
80
+ "evidence_status": evidence_status,
81
+ },
82
+ )
83
+
84
+ metadata = candidates.get("metadata")
85
+ if not isinstance(metadata, dict):
86
+ raise SourceCandidatesError(
87
+ "source_candidates.yaml uses an unsupported schema for merge: "
88
+ "missing metadata object.",
89
+ error_code=E_SOURCE_CANDIDATES_UNSUPPORTED_SCHEMA,
90
+ )
91
+ if metadata.get("generated_by") != "source_decider":
92
+ raise SourceCandidatesError(
93
+ "source_candidates.yaml uses an unsupported schema for merge: "
94
+ "metadata.generated_by must be source_decider.",
95
+ error_code=E_SOURCE_CANDIDATES_UNSUPPORTED_SCHEMA,
96
+ )
97
+
98
+
99
+ def _ensure_mapping(
100
+ container: dict[str, Any],
101
+ key: str,
102
+ default: dict[str, Any],
103
+ *,
104
+ path: str | None = None,
105
+ ) -> dict[str, Any]:
106
+ """Return a mapping section, normalizing YAML null to defaults."""
107
+ value = container.get(key)
108
+ if value is None:
109
+ value = dict(default)
110
+ container[key] = value
111
+ if not isinstance(value, dict):
112
+ label = path or key
113
+ raise ValueError(f"{label} must be a mapping.")
114
+ for default_key, default_value in default.items():
115
+ value.setdefault(default_key, default_value)
116
+ return value
117
+
118
+
119
+ def _ensure_list(
120
+ container: dict[str, Any],
121
+ key: str,
122
+ *,
123
+ path: str,
124
+ default: list[Any] | None = None,
125
+ ) -> list[Any]:
126
+ """Return a list field, normalizing YAML null to a concrete list."""
127
+ value = container.get(key)
128
+ if value is None:
129
+ value = list(default or [])
130
+ container[key] = value
131
+ if not isinstance(value, list):
132
+ raise ValueError(f"{path} must be a list.")
133
+ return value
134
+
135
+
136
+ def _canonical_filing_ticker_entry(entry: Any) -> dict[str, Any] | None:
137
+ """Return canonical filing_resolver ticker config for writer paths."""
138
+ if isinstance(entry, dict):
139
+ return dict(entry)
140
+ if isinstance(entry, str):
141
+ value = entry.strip()
142
+ if not value:
143
+ return None
144
+ if re.fullmatch(r"[A-Z0-9][A-Z0-9.\-]{0,9}", value):
145
+ return {"ticker": value}
146
+ return {"company_name": value}
147
+ return None
148
+
149
+
150
+ def _filing_ticker_key(entry: Any) -> str | None:
151
+ normalized = _canonical_filing_ticker_entry(entry)
152
+ if normalized is None:
153
+ return None
154
+ for key in ("ticker", "company_name", "cik"):
155
+ value = normalized.get(key)
156
+ if value:
157
+ return f"{key}:{str(value).strip()}"
158
+ return None
159
+
160
+
161
+ def load_source_discovery(sources_path: Path) -> dict[str, Any]:
162
+ """Extract source_discovery section from sources.yaml."""
163
+ data = _load_yaml(sources_path)
164
+ return data.get("source_discovery", {})
165
+
166
+
167
+ def build_search_queries(discovery: dict[str, Any]) -> list[str]:
168
+ """Build standard web search queries from source_discovery fields.
169
+
170
+ Does NOT include local signal queries — those are handled by
171
+ build_search_tasks_with_metadata() which adds platform/market metadata.
172
+ """
173
+ company = discovery.get("company", "")
174
+ industry = discovery.get("industry", "")
175
+ focus_areas = discovery.get("focus_areas", [])
176
+
177
+ queries = []
178
+
179
+ # Industry-level query
180
+ if industry:
181
+ queries.append(f"{industry} industry news recent")
182
+
183
+ # Company-level query
184
+ if company:
185
+ queries.append(f"{company} official announcements news")
186
+
187
+ # Focus area queries
188
+ if isinstance(focus_areas, str):
189
+ focus_areas = [a.strip() for a in focus_areas.split(",") if a.strip()]
190
+ for area in focus_areas[:5]: # cap at 5 focus areas
191
+ if company:
192
+ queries.append(f"{company} {area}")
193
+ elif industry:
194
+ queries.append(f"{industry} {area}")
195
+
196
+ return queries
197
+
198
+
199
+ def build_daily_news_search_tasks(
200
+ discovery: dict[str, Any],
201
+ *,
202
+ days: int = 7,
203
+ daily_max_results: int = 20,
204
+ report_date: str | date | None = None,
205
+ ) -> list[dict[str, Any]]:
206
+ """Build one user-need-customized news search task per day.
207
+
208
+ This is a source discovery helper. It does not execute searches and does
209
+ not write report content.
210
+ """
211
+ if days <= 0:
212
+ return []
213
+ if daily_max_results <= 0:
214
+ daily_max_results = 20
215
+
216
+ end_date = _parse_report_date(report_date)
217
+ terms = _build_user_need_terms(discovery)
218
+ if not terms:
219
+ terms = ["industry news"]
220
+ base_query = " ".join(terms)
221
+ language = str(discovery.get("language", "en")).lower()
222
+ news_word = "新闻 动态" if language.startswith("zh") else "news updates"
223
+ preferred_domains, excluded_domains = build_news_domain_preferences(discovery)
224
+
225
+ tasks: list[dict[str, Any]] = []
226
+ for offset in range(days, 0, -1):
227
+ window_start = end_date - timedelta(days=offset)
228
+ window_end = window_start + timedelta(days=1)
229
+ query = (
230
+ f"{base_query} {news_word}"
231
+ f" after:{window_start.isoformat()}"
232
+ f" before:{window_end.isoformat()}"
233
+ )
234
+ tasks.append(
235
+ {
236
+ "query": query,
237
+ "domains": preferred_domains or None,
238
+ "vertical": "news",
239
+ "topic": "news",
240
+ "source_intent": "initial_daily_news_backfill",
241
+ "date_window_start": window_start.isoformat(),
242
+ "date_window_end": window_end.isoformat(),
243
+ "max_results": daily_max_results,
244
+ "preferred_domains": preferred_domains,
245
+ "excluded_domains": excluded_domains,
246
+ "customized_from": [
247
+ field
248
+ for field in (
249
+ "company",
250
+ "industry",
251
+ "task_objective",
252
+ "focus_areas",
253
+ "audience",
254
+ )
255
+ if discovery.get(field)
256
+ ],
257
+ "tbs": _google_custom_date_range(window_start, window_end),
258
+ }
259
+ )
260
+ return tasks
261
+
262
+
263
+ def build_news_domain_preferences(discovery: dict[str, Any]) -> tuple[list[str], list[str]]:
264
+ """Return user-configured preferred and excluded news domains."""
265
+ selection = discovery.get("news_source_selection") or {}
266
+ preferred = _extract_domain_list(selection, "preferred_domains")
267
+ excluded = _extract_domain_list(selection, "excluded_domains")
268
+
269
+ if not preferred:
270
+ preferred = _extract_domain_list(discovery, "preferred_news_domains")
271
+ if not excluded:
272
+ excluded = _extract_domain_list(discovery, "excluded_news_domains")
273
+
274
+ return preferred, excluded
275
+
276
+
277
+ def _extract_domain_list(container: dict[str, Any], key: str) -> list[str]:
278
+ raw = container.get(key)
279
+ values: list[str] = []
280
+ if isinstance(raw, str):
281
+ values = [item.strip() for item in raw.split(",") if item.strip()]
282
+ elif isinstance(raw, list):
283
+ values = [str(item).strip() for item in raw if str(item).strip()]
284
+
285
+ normalized: list[str] = []
286
+ seen: set[str] = set()
287
+ for value in values:
288
+ domain = _normalize_domain(value)
289
+ if domain and domain not in seen:
290
+ normalized.append(domain)
291
+ seen.add(domain)
292
+ return normalized
293
+
294
+
295
+ def _normalize_domain(value: str) -> str:
296
+ candidate = value.strip().lower()
297
+ if not candidate:
298
+ return ""
299
+ if "://" not in candidate:
300
+ candidate = f"//{candidate}"
301
+ parsed = urlparse(candidate)
302
+ host = parsed.netloc or parsed.path.split("/", 1)[0]
303
+ host = host.split("@")[-1].split(":")[0].strip().strip(".")
304
+ if host.startswith("www."):
305
+ host = host[4:]
306
+ return host
307
+
308
+
309
+ def _url_matches_domain(url: str, domains: list[str]) -> bool:
310
+ host = _normalize_domain(url)
311
+ if not host:
312
+ return False
313
+ for domain in domains:
314
+ if host == domain or host.endswith(f".{domain}"):
315
+ return True
316
+ return False
317
+
318
+
319
+ def _parse_report_date(value: str | date | None) -> date:
320
+ if isinstance(value, date):
321
+ return value
322
+ if isinstance(value, str) and value:
323
+ try:
324
+ return datetime.fromisoformat(value).date()
325
+ except ValueError:
326
+ return datetime.strptime(value[:10], "%Y-%m-%d").date()
327
+ return date.today()
328
+
329
+
330
+ def _google_custom_date_range(start: date, end: date) -> str:
331
+ return (
332
+ "cdr:1,"
333
+ f"cd_min:{start.month}/{start.day}/{start.year},"
334
+ f"cd_max:{end.month}/{end.day}/{end.year}"
335
+ )
336
+
337
+
338
+ def _build_user_need_terms(discovery: dict[str, Any]) -> list[str]:
339
+ terms: list[str] = []
340
+ for key in ("company", "industry"):
341
+ value = str(discovery.get(key) or "").strip()
342
+ if value:
343
+ terms.append(value)
344
+
345
+ focus_areas = discovery.get("focus_areas", [])
346
+ if isinstance(focus_areas, str):
347
+ focus_areas = [a.strip() for a in focus_areas.split(",") if a.strip()]
348
+ for area in list(focus_areas)[:5]:
349
+ value = str(area).strip()
350
+ if value:
351
+ terms.append(value)
352
+
353
+ task_objective = str(discovery.get("task_objective") or "").strip()
354
+ if task_objective:
355
+ terms.append(task_objective[:120])
356
+
357
+ audience = str(discovery.get("audience") or "").strip()
358
+ if audience:
359
+ terms.append(audience[:80])
360
+
361
+ compact: list[str] = []
362
+ seen: set[str] = set()
363
+ for term in terms:
364
+ normalized = " ".join(term.split())
365
+ key = normalized.lower()
366
+ if normalized and key not in seen:
367
+ compact.append(normalized)
368
+ seen.add(key)
369
+ return compact
370
+
371
+
372
+ def build_search_tasks_with_metadata(discovery: dict[str, Any]) -> list[dict[str, Any]]:
373
+ """Build search tasks as dicts with metadata for pipeline injection.
374
+
375
+ Returns list of dicts with 'query' plus optional metadata keys:
376
+ topic, market, language, platform_group, signal_type.
377
+ """
378
+ tasks: list[dict[str, Any]] = []
379
+
380
+ # Standard queries — delegate to build_search_queries
381
+ preferred_domains, excluded_domains = build_news_domain_preferences(discovery)
382
+ for q in build_search_queries(discovery):
383
+ task: dict[str, Any] = {"query": q, "domains": preferred_domains or None}
384
+ if preferred_domains:
385
+ task["preferred_domains"] = preferred_domains
386
+ if excluded_domains:
387
+ task["excluded_domains"] = excluded_domains
388
+ tasks.append(task)
389
+
390
+ # Local signal tasks with metadata
391
+ local_tasks = build_local_signal_tasks(discovery)
392
+ existing_q = {t.get("query") for t in tasks}
393
+ for task in local_tasks:
394
+ if task.query and task.query not in existing_q:
395
+ tasks.append({
396
+ "query": task.query,
397
+ "domains": None,
398
+ "topic": "consumer_signal",
399
+ "market": task.market,
400
+ "language": task.language,
401
+ "platform_group": task.platform_group,
402
+ "signal_type": task.signal_type,
403
+ })
404
+ existing_q.add(task.query)
405
+
406
+ return tasks
407
+
408
+
409
+ def generate_source_candidates(
410
+ discovery: dict[str, Any],
411
+ search_results: list[dict[str, Any]] | None = None,
412
+ ) -> dict[str, Any]:
413
+ """Generate source_candidates.yaml content from discovery + search results.
414
+
415
+ Args:
416
+ discovery: source_discovery section from sources.yaml
417
+ search_results: list of {"query": str, "results": [{"title", "url", "snippet"}]}
418
+ """
419
+ company = discovery.get("company", "")
420
+ industry = discovery.get("industry", "")
421
+ language = discovery.get("language", "zh")
422
+ max_age = discovery.get("max_source_age_days", 14)
423
+
424
+ candidates: dict[str, Any] = {
425
+ "metadata": {
426
+ "company": company,
427
+ "industry": industry,
428
+ "language": language,
429
+ "max_source_age_days": max_age,
430
+ "generated_by": "source_decider",
431
+ "status": "pending_review",
432
+ },
433
+ "recommended_sources": [],
434
+ }
435
+
436
+ if search_results:
437
+ for sr in search_results:
438
+ query = sr.get("query", "")
439
+ search_metadata = sr.get("metadata") or {}
440
+ excluded_domains = _extract_domain_list(
441
+ search_metadata,
442
+ "excluded_domains",
443
+ )
444
+ for result in sr.get("results", []):
445
+ url = result.get("url", "")
446
+ if excluded_domains and _url_matches_domain(url, excluded_domains):
447
+ continue
448
+ title = result.get("title", "")
449
+ snippet = result.get("snippet", "")
450
+
451
+ # Simple tier classification based on URL patterns
452
+ tier = "industry_media"
453
+ if any(kw in url for kw in [".gov", "gov.cn", "regulator"]):
454
+ tier = "government_regulator"
455
+ elif any(kw in url for kw in ["research", "report", "analysis", "journal"]):
456
+ tier = "research_institution"
457
+ elif company and company.lower() in url.lower():
458
+ tier = "company_official"
459
+
460
+ candidates["recommended_sources"].append({
461
+ "name": title[:80],
462
+ "url": url,
463
+ "category": tier,
464
+ "query": query,
465
+ "snippet": snippet[:200],
466
+ "published_at": result.get("published_at", ""),
467
+ "source_name": result.get("source_name", ""),
468
+ "search_intent": search_metadata.get("source_intent", ""),
469
+ "date_window_start": search_metadata.get("date_window_start", ""),
470
+ "date_window_end": search_metadata.get("date_window_end", ""),
471
+ "enabled": True,
472
+ })
473
+
474
+ # Add template entries for common source types
475
+ template_sources = _get_template_sources(industry, language)
476
+ candidates["template_sources"] = template_sources
477
+
478
+ # Add filing sources for companies that likely have SEC/public filings
479
+ filing_sources = _get_filing_sources(discovery)
480
+ if filing_sources:
481
+ candidates["filing_sources"] = filing_sources
482
+
483
+ # Add local social listening tasks from local signal planner
484
+ local_tasks = build_local_signal_tasks(discovery)
485
+ if local_tasks:
486
+ candidates["local_social_listening_tasks"] = [
487
+ task.to_dict() for task in local_tasks
488
+ ]
489
+
490
+ return candidates
491
+
492
+
493
+ def _get_template_sources(industry: str, language: str) -> list[dict[str, Any]]:
494
+ """Get template source entries based on industry."""
495
+ templates = {
496
+ "finance": [
497
+ {"name": "Industry regulator website", "category": "government_regulator", "enabled": True},
498
+ {"name": "Stock exchange filings", "category": "company_official", "enabled": True},
499
+ {"name": "Financial news outlet", "category": "industry_media", "enabled": True},
500
+ ],
501
+ "technology": [
502
+ {"name": "Tech company blogs", "category": "company_official", "enabled": True},
503
+ {"name": "Industry research reports", "category": "research_institution", "enabled": True},
504
+ {"name": "Tech news media", "category": "industry_media", "enabled": True},
505
+ ],
506
+ "manufacturing": [
507
+ {"name": "Industry association", "category": "industry_media", "enabled": True},
508
+ {"name": "Trade publications", "category": "industry_media", "enabled": True},
509
+ {"name": "Government policy portal", "category": "government_regulator", "enabled": True},
510
+ ],
511
+ }
512
+ return templates.get(industry, templates.get("finance", []))
513
+
514
+
515
+ def _get_filing_sources(discovery: dict[str, Any]) -> list[dict[str, Any]]:
516
+ """Suggest filing-resolver sources for companies with public disclosure filings.
517
+
518
+ Returns a list of filing source candidates. Each entry represents a ticker/entity
519
+ that disclosure-filing-resolver can fetch SEC EDGAR filings for.
520
+ Only generates suggestions when company info is available.
521
+ """
522
+ company = discovery.get("company", "").strip()
523
+ if not company:
524
+ return []
525
+
526
+ # Simple heuristic: suggest SEC EDGAR as a filing source for the company.
527
+ # The actual ticker/CIK resolution happens at pipeline time via filing-resolver.
528
+ # Users can edit tickers in source_candidates.yaml before merging.
529
+ sources = []
530
+ sources.append({
531
+ "name": f"{company} — SEC EDGAR filings",
532
+ "provider": "filing_resolver",
533
+ "tickers": [company], # placeholder; user should refine to actual ticker
534
+ "filing_types": ["10-K", "10-Q", "8-K"],
535
+ "category": "company_official",
536
+ "enabled": True,
537
+ })
538
+
539
+ return sources
540
+
541
+
542
+ def merge_candidates_to_sources(
543
+ sources_path: Path,
544
+ candidates_path: Path,
545
+ *,
546
+ overwrite: bool = False,
547
+ ) -> dict[str, Any]:
548
+ """Merge approved candidates into sources.yaml.
549
+
550
+ Args:
551
+ sources_path: path to sources.yaml
552
+ candidates_path: path to source_candidates.yaml
553
+ overwrite: if True, replace rss/web_search sections; if False, append
554
+
555
+ Returns:
556
+ Summary of changes
557
+ """
558
+ sources = _load_yaml(sources_path)
559
+ candidates = _load_yaml(candidates_path)
560
+ _validate_mergeable_candidates(candidates)
561
+
562
+ recommended = candidates.get("recommended_sources") or []
563
+ enabled = [s for s in recommended if s.get("enabled", True)]
564
+
565
+ # Group by category.
566
+ # Only explicitly verified rss_feed sources go to rss.feeds.
567
+ # All other URL categories (industry_media, research_institution,
568
+ # government_regulator, company_official) go to manual sources as
569
+ # URL entries — they are web pages, not RSS/Atom feeds.
570
+ rss_feeds = []
571
+ manual_sources = []
572
+
573
+ for src in enabled:
574
+ category = src.get("category", "")
575
+ url = src.get("url", "")
576
+ name = src.get("name", "")
577
+
578
+ if not url:
579
+ continue
580
+
581
+ if category == "rss_feed":
582
+ rss_feeds.append({"name": name, "url": url, "category": category, "enabled": True})
583
+ else:
584
+ # All other URL types go to manual sources (not RSS)
585
+ manual_source = {
586
+ "name": name,
587
+ "url": url,
588
+ "category": category,
589
+ "enabled": True,
590
+ }
591
+ for key in ("published_at", "source_name", "search_intent", "date_window_start", "date_window_end"):
592
+ if src.get(key):
593
+ manual_source[key] = src[key]
594
+ manual_sources.append(manual_source)
595
+
596
+ # Merge into sources. YAML empty fields parse as None, so normalize known
597
+ # list sections before appending or iterating.
598
+ manual = _ensure_mapping(
599
+ sources,
600
+ "manual",
601
+ {"enabled": True, "sources": []},
602
+ path="manual",
603
+ )
604
+ manual_entries = _ensure_list(manual, "sources", path="manual.sources")
605
+ rss = _ensure_mapping(
606
+ sources,
607
+ "rss",
608
+ {"enabled": True, "feeds": []},
609
+ path="rss",
610
+ )
611
+ rss_entries = _ensure_list(rss, "feeds", path="rss.feeds")
612
+
613
+ if overwrite:
614
+ manual_entries[:] = [
615
+ src for src in manual_entries
616
+ if src.get("category") == "local_files" or (src.get("path") and not src.get("url"))
617
+ ]
618
+ rss_entries.clear()
619
+
620
+ existing_manual_urls = {s.get("url") for s in manual_entries}
621
+ existing_rss_urls = {f.get("url") for f in rss_entries}
622
+
623
+ added_manual = 0
624
+ added_rss = 0
625
+
626
+ for src in manual_sources:
627
+ if src["url"] not in existing_manual_urls:
628
+ manual_entries.append(src)
629
+ added_manual += 1
630
+
631
+ for feed in rss_feeds:
632
+ if feed["url"] not in existing_rss_urls:
633
+ rss_entries.append(feed)
634
+ added_rss += 1
635
+
636
+ # Ensure web_search section exists, but do NOT auto-enable it.
637
+ # Only enable web_search if it was already enabled OR the user explicitly
638
+ # set a real backend (not mock). Mock data must never leak into real reports
639
+ # unless the user explicitly opted in with allow_mock_search: true.
640
+ web_search = _ensure_mapping(
641
+ sources,
642
+ "web_search",
643
+ {"enabled": False, "max_results": 20, "recency_days": 7},
644
+ path="web_search",
645
+ )
646
+ # Do not auto-enable web_search on merge.
647
+
648
+ # Update source_strategy
649
+ source_strategy = _ensure_mapping(
650
+ sources,
651
+ "source_strategy",
652
+ {"profile": "research", "enabled_providers": ["manual"]},
653
+ path="source_strategy",
654
+ )
655
+ providers = _ensure_list(
656
+ source_strategy,
657
+ "enabled_providers",
658
+ path="source_strategy.enabled_providers",
659
+ default=["manual"],
660
+ )
661
+ if "rss" not in providers and added_rss > 0:
662
+ providers.append("rss")
663
+ # Only add web_search to enabled_providers if it is actually enabled
664
+ if "web_search" not in providers and web_search.get("enabled"):
665
+ providers.append("web_search")
666
+
667
+ # Merge filing_sources into filing_resolver config
668
+ filing_sources = [
669
+ s for s in (candidates.get("filing_sources") or []) if s.get("enabled", True)
670
+ ]
671
+ added_filing = 0
672
+ if filing_sources:
673
+ fr = _ensure_mapping(
674
+ sources,
675
+ "filing_resolver",
676
+ {"enabled": True, "tickers": [], "filing_types": ["10-K", "10-Q", "8-K"]},
677
+ path="filing_resolver",
678
+ )
679
+ tickers = _ensure_list(fr, "tickers", path="filing_resolver.tickers")
680
+ filing_types = _ensure_list(
681
+ fr,
682
+ "filing_types",
683
+ path="filing_resolver.filing_types",
684
+ default=["10-K", "10-Q", "8-K"],
685
+ )
686
+ canonical_tickers: list[dict[str, Any]] = []
687
+ existing_tickers: set[str] = set()
688
+ for existing in tickers:
689
+ entry = _canonical_filing_ticker_entry(existing)
690
+ key = _filing_ticker_key(entry)
691
+ if entry is not None and key and key not in existing_tickers:
692
+ canonical_tickers.append(entry)
693
+ existing_tickers.add(key)
694
+ tickers[:] = canonical_tickers
695
+ for fs in filing_sources:
696
+ for ticker in (fs.get("tickers") or []):
697
+ entry = _canonical_filing_ticker_entry(ticker)
698
+ key = _filing_ticker_key(entry)
699
+ if entry is not None and key and key not in existing_tickers:
700
+ tickers.append(entry)
701
+ existing_tickers.add(key)
702
+ added_filing += 1
703
+ # Merge filing_types if provided
704
+ for ft in (fs.get("filing_types") or []):
705
+ if ft not in filing_types:
706
+ filing_types.append(ft)
707
+ # Add filing_resolver to enabled_providers
708
+ if "filing_resolver" not in providers:
709
+ providers.append("filing_resolver")
710
+
711
+ # Mark candidates as merged
712
+ candidates["metadata"]["status"] = "merged"
713
+ candidates["metadata"]["merged_manual"] = added_manual
714
+ candidates["metadata"]["merged_rss"] = added_rss
715
+ candidates["metadata"]["merged_filing"] = added_filing
716
+
717
+ # Inject local social listening tasks into web_search search_tasks
718
+ local_tasks = [
719
+ t for t in (candidates.get("local_social_listening_tasks") or [])
720
+ if t.get("enabled", True)
721
+ ]
722
+ added_local = 0
723
+ if local_tasks and web_search.get("enabled"):
724
+ search_tasks = _ensure_list(
725
+ web_search,
726
+ "search_tasks",
727
+ path="web_search.search_tasks",
728
+ )
729
+ existing_search_q = {t.get("query") for t in search_tasks}
730
+ for task in local_tasks:
731
+ query = task.get("query", "")
732
+ if query and query not in existing_search_q:
733
+ search_tasks.append({
734
+ "query": query,
735
+ "domains": None,
736
+ "topic": "consumer_signal",
737
+ "market": task.get("market", ""),
738
+ "language": task.get("language", ""),
739
+ "platform_group": task.get("platform_group", ""),
740
+ "signal_type": task.get("signal_type", "consumer_discussion"),
741
+ })
742
+ existing_search_q.add(query)
743
+ added_local += 1
744
+ candidates["metadata"]["merged_local_tasks"] = added_local
745
+
746
+ _save_yaml(sources_path, sources)
747
+ _save_yaml(candidates_path, candidates)
748
+
749
+ return {
750
+ "added_manual": added_manual,
751
+ "added_rss": added_rss,
752
+ "added_filing": added_filing,
753
+ "added_local": added_local,
754
+ "total_enabled": len(enabled),
755
+ "total_disabled": len(recommended) - len(enabled),
756
+ }