qwen-sci 0.2.2__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 (549) hide show
  1. qwen_sci-0.2.2.dist-info/METADATA +492 -0
  2. qwen_sci-0.2.2.dist-info/RECORD +549 -0
  3. qwen_sci-0.2.2.dist-info/WHEEL +4 -0
  4. qwen_sci-0.2.2.dist-info/entry_points.txt +6 -0
  5. qwen_sci-0.2.2.dist-info/licenses/LICENSE +21 -0
  6. src/1.txt +1 -0
  7. src/__init__.py +1 -0
  8. src/__main__.py +5 -0
  9. src/agents/__init__.py +1 -0
  10. src/agents/experiment_design_agent/__init__.py +268 -0
  11. src/agents/experiment_design_agent/artifacts.py +747 -0
  12. src/agents/experiment_design_agent/cache.py +319 -0
  13. src/agents/experiment_design_agent/completeness.py +130 -0
  14. src/agents/experiment_design_agent/contracts.py +771 -0
  15. src/agents/experiment_design_agent/counterexample_analyzer.py +187 -0
  16. src/agents/experiment_design_agent/design_evidence_paper_screener.py +470 -0
  17. src/agents/experiment_design_agent/discipline_catalog.py +178 -0
  18. src/agents/experiment_design_agent/evidence_cards.py +338 -0
  19. src/agents/experiment_design_agent/evidence_planner.py +452 -0
  20. src/agents/experiment_design_agent/formal_reasoning_planner.py +884 -0
  21. src/agents/experiment_design_agent/fulltext_acquisition.py +863 -0
  22. src/agents/experiment_design_agent/idea_adapter.py +100 -0
  23. src/agents/experiment_design_agent/idea_intake.py +87 -0
  24. src/agents/experiment_design_agent/llm_json.py +328 -0
  25. src/agents/experiment_design_agent/orchestrator.py +1220 -0
  26. src/agents/experiment_design_agent/reasoning_context.py +183 -0
  27. src/agents/experiment_design_agent/reasoning_validation.py +546 -0
  28. src/agents/experiment_design_agent/run.py +481 -0
  29. src/agents/experiment_design_agent/run_logging.py +292 -0
  30. src/agents/experiment_design_agent/scope_gate.py +198 -0
  31. src/agents/experiment_design_agent/study_type_composer.py +1360 -0
  32. src/agents/experiment_design_agent/survey_evidence.py +1381 -0
  33. src/agents/experiment_design_agent/template_router.py +195 -0
  34. src/agents/experiment_design_agent/variable_claim_extractor.py +93 -0
  35. src/agents/idea_agent/__init__.py +1 -0
  36. src/agents/idea_agent/agent/__init__.py +6 -0
  37. src/agents/idea_agent/agent/artifacts.py +308 -0
  38. src/agents/idea_agent/agent/base.py +122 -0
  39. src/agents/idea_agent/agent/ligagent.py +488 -0
  40. src/agents/idea_agent/agent/mcts.py +2060 -0
  41. src/agents/idea_agent/agent/prompts/__init__.py +48 -0
  42. src/agents/idea_agent/agent/prompts/advanced_analysis.py +235 -0
  43. src/agents/idea_agent/agent/prompts/algorithm_alignment.py +55 -0
  44. src/agents/idea_agent/agent/prompts/algorithm_structuring.py +48 -0
  45. src/agents/idea_agent/agent/prompts/component_extraction.py +62 -0
  46. src/agents/idea_agent/agent/prompts/component_novelty_evaluation.py +54 -0
  47. src/agents/idea_agent/agent/prompts/experiment_findings_extraction.py +39 -0
  48. src/agents/idea_agent/agent/prompts/idea_fusion.py +295 -0
  49. src/agents/idea_agent/agent/prompts/idea_introduction.py +30 -0
  50. src/agents/idea_agent/agent/prompts/idea_result_alignment.py +39 -0
  51. src/agents/idea_agent/agent/prompts/input_interpreter.py +52 -0
  52. src/agents/idea_agent/agent/prompts/keynote_ops.py +78 -0
  53. src/agents/idea_agent/agent/prompts/mcts_evaluation.py +235 -0
  54. src/agents/idea_agent/agent/prompts/mcts_generation.py +140 -0
  55. src/agents/idea_agent/agent/prompts/mechanism_commit_query.py +99 -0
  56. src/agents/idea_agent/agent/prompts/memory_selection.py +0 -0
  57. src/agents/idea_agent/agent/prompts/prompt_modes.py +16 -0
  58. src/agents/idea_agent/agent/prompts/rag_query.py +34 -0
  59. src/agents/idea_agent/agent/prompts/re_analysis_replan.py +48 -0
  60. src/agents/idea_agent/agent/prompts/reference_grounding.py +34 -0
  61. src/agents/idea_agent/agent/prompts/root_domain_classification.py +32 -0
  62. src/agents/idea_agent/agent/prompts/scientific_debate.py +88 -0
  63. src/agents/idea_agent/agent/prompts/scientific_materialization.py +45 -0
  64. src/agents/idea_agent/agent/prompts/skill_instantiation.py +193 -0
  65. src/agents/idea_agent/agent/prompts/theory_transfer_query.py +93 -0
  66. src/agents/idea_agent/agent/prompts/topic_background.py +15 -0
  67. src/agents/idea_agent/agent/skills/DEFAULT_SKILL_TEMPLATES.json +263 -0
  68. src/agents/idea_agent/agent/skills/edit_operator_skills/alternative-path-contrast/SKILL.md +40 -0
  69. src/agents/idea_agent/agent/skills/edit_operator_skills/alternative-path-contrast/references/contrast_patterns.md +22 -0
  70. src/agents/idea_agent/agent/skills/edit_operator_skills/feedback-closed-loop/SKILL.md +40 -0
  71. src/agents/idea_agent/agent/skills/edit_operator_skills/feedback-closed-loop/references/mechanism_cards.md +24 -0
  72. src/agents/idea_agent/agent/skills/edit_operator_skills/hierarchical-decomposition/SKILL.md +39 -0
  73. src/agents/idea_agent/agent/skills/edit_operator_skills/hierarchical-decomposition/references/hierarchy_patterns.md +22 -0
  74. src/agents/idea_agent/agent/skills/edit_operator_skills/mechanism-commit-innovation/SKILL.md +39 -0
  75. src/agents/idea_agent/agent/skills/edit_operator_skills/mechanism-commit-innovation/references/mechanism_commit_patterns.md +24 -0
  76. src/agents/idea_agent/agent/skills/edit_operator_skills/multi-scale-coordinator/SKILL.md +39 -0
  77. src/agents/idea_agent/agent/skills/edit_operator_skills/multi-scale-coordinator/references/coordination_patterns.md +22 -0
  78. src/agents/idea_agent/agent/skills/edit_operator_skills/speculative-execution-with-repair/SKILL.md +41 -0
  79. src/agents/idea_agent/agent/skills/edit_operator_skills/speculative-execution-with-repair/references/speculation_patterns.md +22 -0
  80. src/agents/idea_agent/agent/skills/edit_operator_skills/surgical-modularity/SKILL.md +39 -0
  81. src/agents/idea_agent/agent/skills/edit_operator_skills/surgical-modularity/references/modularity_patterns.md +22 -0
  82. src/agents/idea_agent/agent/skills/edit_operator_skills/theory-transfer-injection/SKILL.md +40 -0
  83. src/agents/idea_agent/agent/skills/edit_operator_skills/theory-transfer-injection/references/transfer_patterns.md +24 -0
  84. src/agents/idea_agent/example/idea_result.json +900 -0
  85. src/agents/idea_agent/run.py +276 -0
  86. src/agents/idea_agent/tests/test_advanced_analysis_contract.py +90 -0
  87. src/agents/idea_agent/tests/test_gap_hypothesis_bridge.py +166 -0
  88. src/agents/idea_agent/tests/test_idea_debate.py +480 -0
  89. src/agents/idea_agent/tests/test_idea_result_v5.py +99 -0
  90. src/agents/idea_agent/tests/test_idea_synthesis.py +92 -0
  91. src/agents/idea_agent/tests/test_mature_idea_sources.py +27 -0
  92. src/agents/idea_agent/tests/test_multimodal_idea_integration.py +386 -0
  93. src/agents/idea_agent/tests/test_profile_materialization_and_scoring.py +219 -0
  94. src/agents/idea_agent/tests/test_profile_native_skill_rendering.py +122 -0
  95. src/agents/idea_agent/tests/test_result_persistence.py +183 -0
  96. src/agents/idea_agent/tests/test_scientific_intervention_profiles.py +241 -0
  97. src/agents/idea_agent/tests/test_scientific_rubric_and_prompts.py +559 -0
  98. src/agents/idea_agent/tests/test_strict_json_object_responses.py +48 -0
  99. src/agents/idea_agent/utils/core/__init__.py +1 -0
  100. src/agents/idea_agent/utils/core/ablation_inputs.py +60 -0
  101. src/agents/idea_agent/utils/core/chat_errors.py +95 -0
  102. src/agents/idea_agent/utils/core/chat_router.py +130 -0
  103. src/agents/idea_agent/utils/core/chat_transport.py +188 -0
  104. src/agents/idea_agent/utils/core/config_loader.py +166 -0
  105. src/agents/idea_agent/utils/core/json_utils.py +28 -0
  106. src/agents/idea_agent/utils/core/logger.py +287 -0
  107. src/agents/idea_agent/utils/core/progress.py +58 -0
  108. src/agents/idea_agent/utils/core/response_parsing.py +57 -0
  109. src/agents/idea_agent/utils/core/run_inputs.py +198 -0
  110. src/agents/idea_agent/utils/mcts/__init__.py +1 -0
  111. src/agents/idea_agent/utils/mcts/component_novelty.py +342 -0
  112. src/agents/idea_agent/utils/mcts/defect_registry.py +302 -0
  113. src/agents/idea_agent/utils/mcts/idea_routes.py +61 -0
  114. src/agents/idea_agent/utils/mcts/idea_taste_presets.py +312 -0
  115. src/agents/idea_agent/utils/mcts/mcts_helpers.py +738 -0
  116. src/agents/idea_agent/utils/mcts/mcts_runtime.py +3000 -0
  117. src/agents/idea_agent/utils/mcts/scientific_intervention_ontology.py +1561 -0
  118. src/agents/idea_agent/utils/mcts/scientific_rubric.py +211 -0
  119. src/agents/idea_agent/utils/mcts/skill_parsing.py +92 -0
  120. src/agents/idea_agent/utils/papers/__init__.py +1 -0
  121. src/agents/idea_agent/utils/papers/paper_graph_vector_store.py +499 -0
  122. src/agents/idea_agent/utils/papers/paper_repository.py +372 -0
  123. src/agents/idea_agent/utils/prompting/__init__.py +1 -0
  124. src/agents/idea_agent/utils/prompting/prompt_views.py +637 -0
  125. src/agents/idea_agent/utils/workflow/__init__.py +1 -0
  126. src/agents/idea_agent/utils/workflow/advanced_analysis_contract.py +244 -0
  127. src/agents/idea_agent/utils/workflow/gap_hypothesis_bridge.py +461 -0
  128. src/agents/idea_agent/utils/workflow/idea_contract.py +426 -0
  129. src/agents/idea_agent/utils/workflow/idea_debate.py +1238 -0
  130. src/agents/idea_agent/utils/workflow/idea_diversity.py +163 -0
  131. src/agents/idea_agent/utils/workflow/idea_fusion.py +889 -0
  132. src/agents/idea_agent/utils/workflow/idea_helpers.py +478 -0
  133. src/agents/idea_agent/utils/workflow/idea_portfolio.py +422 -0
  134. src/agents/idea_agent/utils/workflow/idea_synthesis.py +482 -0
  135. src/agents/idea_agent/utils/workflow/ligagent_flow.py +581 -0
  136. src/agents/idea_agent/utils/workflow/ligagent_handlers.py +1836 -0
  137. src/agents/idea_agent/utils/workflow/ligagent_helpers.py +1331 -0
  138. src/agents/idea_agent/utils/workflow/ligagent_utils.py +322 -0
  139. src/agents/idea_agent/utils/workflow/mature_idea_sources.py +648 -0
  140. src/agents/idea_agent/utils/workflow/multimodal_data_anchoring.py +578 -0
  141. src/agents/idea_agent/utils/workflow/result_persistence.py +774 -0
  142. src/agents/idea_agent/utils/workflow/stage_contract.py +76 -0
  143. src/agents/idea_agent/utils/workflow/workflow_runtime.py +288 -0
  144. src/agents/research_plan_author/__init__.py +50 -0
  145. src/agents/research_plan_author/artifacts.py +270 -0
  146. src/agents/research_plan_author/authoring_blueprint.py +1246 -0
  147. src/agents/research_plan_author/bibtex_renderer.py +252 -0
  148. src/agents/research_plan_author/contract_repair.py +131 -0
  149. src/agents/research_plan_author/contracts.py +745 -0
  150. src/agents/research_plan_author/cross_section_editor.py +232 -0
  151. src/agents/research_plan_author/document_quality.py +987 -0
  152. src/agents/research_plan_author/idea_evolution.py +223 -0
  153. src/agents/research_plan_author/input_loader.py +76 -0
  154. src/agents/research_plan_author/latex_compiler.py +260 -0
  155. src/agents/research_plan_author/latex_safety.py +159 -0
  156. src/agents/research_plan_author/llm_json.py +74 -0
  157. src/agents/research_plan_author/markdown_renderer.py +275 -0
  158. src/agents/research_plan_author/pdf_validator.py +149 -0
  159. src/agents/research_plan_author/render.py +166 -0
  160. src/agents/research_plan_author/render_artifacts.py +187 -0
  161. src/agents/research_plan_author/run.py +765 -0
  162. src/agents/research_plan_author/run_logging.py +209 -0
  163. src/agents/research_plan_author/section_cache.py +184 -0
  164. src/agents/research_plan_author/section_composer.py +1764 -0
  165. src/agents/research_plan_author/section_router.py +209 -0
  166. src/agents/research_plan_author/semantic_validator.py +254 -0
  167. src/agents/research_plan_author/source_bundle.py +46 -0
  168. src/agents/research_plan_author/source_registry.py +208 -0
  169. src/agents/research_plan_author/survey_source_loader.py +137 -0
  170. src/agents/research_plan_author/template_adapter.py +173 -0
  171. src/agents/research_plan_author/template_profile.py +204 -0
  172. src/agents/research_plan_author/tex_renderer.py +496 -0
  173. src/agents/research_plan_author/theory_presentation.py +134 -0
  174. src/agents/research_plan_author/theory_spine.py +703 -0
  175. src/agents/survey_agent/Dockerfile +54 -0
  176. src/agents/survey_agent/README.md +350 -0
  177. src/agents/survey_agent/config/deep_survey.yaml +217 -0
  178. src/agents/survey_agent/config/deep_survey_batch.yaml +176 -0
  179. src/agents/survey_agent/config/deep_survey_fast.yaml +183 -0
  180. src/agents/survey_agent/config/evaluate_baseline.yaml +197 -0
  181. src/agents/survey_agent/config/outcomeRAG.yaml +159 -0
  182. src/agents/survey_agent/env_report.md +348 -0
  183. src/agents/survey_agent/modules/__init__.py +0 -0
  184. src/agents/survey_agent/modules/code_collector.py +2194 -0
  185. src/agents/survey_agent/modules/code_report_generator.py +1282 -0
  186. src/agents/survey_agent/modules/data_manager.py +1713 -0
  187. src/agents/survey_agent/modules/database.py +167 -0
  188. src/agents/survey_agent/modules/fulltext_download_cache.py +462 -0
  189. src/agents/survey_agent/modules/fulltext_http.py +134 -0
  190. src/agents/survey_agent/modules/fulltext_resolution.py +749 -0
  191. src/agents/survey_agent/modules/judge.py +434 -0
  192. src/agents/survey_agent/modules/outcome_RAG.py +236 -0
  193. src/agents/survey_agent/modules/paper_graph_retriever.py +1075 -0
  194. src/agents/survey_agent/modules/pe.py +1729 -0
  195. src/agents/survey_agent/modules/pseudo_creator.py +782 -0
  196. src/agents/survey_agent/modules/pseudo_reviser.py +537 -0
  197. src/agents/survey_agent/modules/refine_agent.py +1952 -0
  198. src/agents/survey_agent/modules/survey_generator.py +5691 -0
  199. src/agents/survey_agent/modules/survey_visualizer.py +1817 -0
  200. src/agents/survey_agent/modules/visual_prompts.py +327 -0
  201. src/agents/survey_agent/modules/work_analyzer.py +2750 -0
  202. src/agents/survey_agent/modules/work_collector.py +5649 -0
  203. src/agents/survey_agent/scripts/baseline_evaluation.py +411 -0
  204. src/agents/survey_agent/scripts/run_deep_survey.py +224 -0
  205. src/agents/survey_agent/scripts/run_deep_survey_adapter.py +407 -0
  206. src/agents/survey_agent/scripts/run_deep_survey_adapter_with_input.py +409 -0
  207. src/agents/survey_agent/scripts/run_deep_survey_batch.py +228 -0
  208. src/agents/survey_agent/scripts/run_deep_survey_batch_arg.py +243 -0
  209. src/agents/survey_agent/scripts/run_deep_survey_batch_settings.py +218 -0
  210. src/agents/survey_agent/scripts/run_keynotes_gen.py +128 -0
  211. src/agents/survey_agent/scripts/run_work_clusters.py +140 -0
  212. src/agents/survey_agent/utils/__init__.py +0 -0
  213. src/agents/survey_agent/utils/ablation_utils.py +317 -0
  214. src/agents/survey_agent/utils/api_call.py +2518 -0
  215. src/agents/survey_agent/utils/chat_utils.py +82 -0
  216. src/agents/survey_agent/utils/config_utils.py +70 -0
  217. src/agents/survey_agent/utils/convert_to_md.py +29 -0
  218. src/agents/survey_agent/utils/err_info.py +24 -0
  219. src/agents/survey_agent/utils/file_utils.py +176 -0
  220. src/agents/survey_agent/utils/gpu_utils.py +145 -0
  221. src/agents/survey_agent/utils/graph_visualizer.py +279 -0
  222. src/agents/survey_agent/utils/html_utils.py +181 -0
  223. src/agents/survey_agent/utils/latex_generator_intra_cluster.py +529 -0
  224. src/agents/survey_agent/utils/latex_generator_relation_table.py +193 -0
  225. src/agents/survey_agent/utils/mineru_section_packer.py +334 -0
  226. src/agents/survey_agent/utils/mineru_utils.py +396 -0
  227. src/agents/survey_agent/utils/paper.py +144 -0
  228. src/agents/survey_agent/utils/read_yaml.py +15 -0
  229. src/agents/survey_agent/utils/repo_utils.py +167 -0
  230. src/agents/survey_agent/utils/rich_logger.py +77 -0
  231. src/agents/survey_agent/utils/step2v2.py +874 -0
  232. src/agents/survey_agent/utils/step2v2_extractor.py +856 -0
  233. src/agents/survey_agent/utils/topic_survey_storage.py +309 -0
  234. src/agents/survey_agent/utils/utils.py +118 -0
  235. src/agents/survey_agent/utils/work.py +2 -0
  236. src/cli.py +1611 -0
  237. src/config/__init__.py +213 -0
  238. src/config/default.yaml +1122 -0
  239. src/harness/LICENSE +21 -0
  240. src/harness/VENDORED.md +14 -0
  241. src/harness/src/__init__.py +0 -0
  242. src/harness/src/openharness/__init__.py +0 -0
  243. src/harness/src/openharness/__main__.py +6 -0
  244. src/harness/src/openharness/api/__init__.py +21 -0
  245. src/harness/src/openharness/api/client.py +281 -0
  246. src/harness/src/openharness/api/codex_client.py +407 -0
  247. src/harness/src/openharness/api/copilot_auth.py +240 -0
  248. src/harness/src/openharness/api/copilot_client.py +134 -0
  249. src/harness/src/openharness/api/errors.py +19 -0
  250. src/harness/src/openharness/api/openai_client.py +577 -0
  251. src/harness/src/openharness/api/provider.py +186 -0
  252. src/harness/src/openharness/api/registry.py +437 -0
  253. src/harness/src/openharness/api/usage.py +17 -0
  254. src/harness/src/openharness/auth/__init__.py +29 -0
  255. src/harness/src/openharness/auth/external.py +610 -0
  256. src/harness/src/openharness/auth/flows.py +189 -0
  257. src/harness/src/openharness/auth/manager.py +482 -0
  258. src/harness/src/openharness/auth/storage.py +269 -0
  259. src/harness/src/openharness/autopilot/__init__.py +23 -0
  260. src/harness/src/openharness/autopilot/service.py +2239 -0
  261. src/harness/src/openharness/autopilot/types.py +91 -0
  262. src/harness/src/openharness/bridge/__init__.py +20 -0
  263. src/harness/src/openharness/bridge/manager.py +106 -0
  264. src/harness/src/openharness/bridge/session_runner.py +46 -0
  265. src/harness/src/openharness/bridge/types.py +37 -0
  266. src/harness/src/openharness/bridge/work_secret.py +41 -0
  267. src/harness/src/openharness/channels/UPSTREAM +6 -0
  268. src/harness/src/openharness/channels/__init__.py +22 -0
  269. src/harness/src/openharness/channels/adapter.py +131 -0
  270. src/harness/src/openharness/channels/bus/__init__.py +6 -0
  271. src/harness/src/openharness/channels/bus/events.py +38 -0
  272. src/harness/src/openharness/channels/bus/queue.py +44 -0
  273. src/harness/src/openharness/channels/impl/__init__.py +6 -0
  274. src/harness/src/openharness/channels/impl/base.py +141 -0
  275. src/harness/src/openharness/channels/impl/dingtalk.py +445 -0
  276. src/harness/src/openharness/channels/impl/discord.py +311 -0
  277. src/harness/src/openharness/channels/impl/email.py +410 -0
  278. src/harness/src/openharness/channels/impl/feishu.py +1342 -0
  279. src/harness/src/openharness/channels/impl/manager.py +257 -0
  280. src/harness/src/openharness/channels/impl/matrix.py +700 -0
  281. src/harness/src/openharness/channels/impl/mochat.py +897 -0
  282. src/harness/src/openharness/channels/impl/qq.py +141 -0
  283. src/harness/src/openharness/channels/impl/slack.py +284 -0
  284. src/harness/src/openharness/channels/impl/telegram.py +525 -0
  285. src/harness/src/openharness/channels/impl/whatsapp.py +159 -0
  286. src/harness/src/openharness/cli.py +2551 -0
  287. src/harness/src/openharness/commands/__init__.py +21 -0
  288. src/harness/src/openharness/commands/registry.py +2783 -0
  289. src/harness/src/openharness/config/__init__.py +34 -0
  290. src/harness/src/openharness/config/paths.py +160 -0
  291. src/harness/src/openharness/config/schema.py +119 -0
  292. src/harness/src/openharness/config/settings.py +1104 -0
  293. src/harness/src/openharness/coordinator/__init__.py +12 -0
  294. src/harness/src/openharness/coordinator/agent_definitions.py +975 -0
  295. src/harness/src/openharness/coordinator/coordinator_mode.py +520 -0
  296. src/harness/src/openharness/engine/__init__.py +80 -0
  297. src/harness/src/openharness/engine/cost_tracker.py +24 -0
  298. src/harness/src/openharness/engine/messages.py +222 -0
  299. src/harness/src/openharness/engine/query.py +1188 -0
  300. src/harness/src/openharness/engine/query_engine.py +313 -0
  301. src/harness/src/openharness/engine/stream_events.py +90 -0
  302. src/harness/src/openharness/hooks/__init__.py +50 -0
  303. src/harness/src/openharness/hooks/events.py +20 -0
  304. src/harness/src/openharness/hooks/executor.py +242 -0
  305. src/harness/src/openharness/hooks/hot_reload.py +31 -0
  306. src/harness/src/openharness/hooks/loader.py +68 -0
  307. src/harness/src/openharness/hooks/schemas.py +66 -0
  308. src/harness/src/openharness/hooks/types.py +38 -0
  309. src/harness/src/openharness/keybindings/__init__.py +14 -0
  310. src/harness/src/openharness/keybindings/default_bindings.py +11 -0
  311. src/harness/src/openharness/keybindings/loader.py +22 -0
  312. src/harness/src/openharness/keybindings/parser.py +18 -0
  313. src/harness/src/openharness/keybindings/resolver.py +13 -0
  314. src/harness/src/openharness/mcp/__init__.py +79 -0
  315. src/harness/src/openharness/mcp/client.py +298 -0
  316. src/harness/src/openharness/mcp/config.py +16 -0
  317. src/harness/src/openharness/mcp/types.py +76 -0
  318. src/harness/src/openharness/memory/__init__.py +25 -0
  319. src/harness/src/openharness/memory/agent.py +91 -0
  320. src/harness/src/openharness/memory/manager.py +183 -0
  321. src/harness/src/openharness/memory/memdir.py +52 -0
  322. src/harness/src/openharness/memory/migrate.py +137 -0
  323. src/harness/src/openharness/memory/paths.py +22 -0
  324. src/harness/src/openharness/memory/relevance.py +145 -0
  325. src/harness/src/openharness/memory/scan.py +116 -0
  326. src/harness/src/openharness/memory/schema.py +443 -0
  327. src/harness/src/openharness/memory/search.py +71 -0
  328. src/harness/src/openharness/memory/team.py +90 -0
  329. src/harness/src/openharness/memory/types.py +33 -0
  330. src/harness/src/openharness/memory/usage.py +153 -0
  331. src/harness/src/openharness/output_styles/__init__.py +5 -0
  332. src/harness/src/openharness/output_styles/loader.py +42 -0
  333. src/harness/src/openharness/permissions/__init__.py +26 -0
  334. src/harness/src/openharness/permissions/checker.py +200 -0
  335. src/harness/src/openharness/permissions/modes.py +13 -0
  336. src/harness/src/openharness/personalization/__init__.py +6 -0
  337. src/harness/src/openharness/personalization/extractor.py +138 -0
  338. src/harness/src/openharness/personalization/rules.py +64 -0
  339. src/harness/src/openharness/personalization/session_hook.py +65 -0
  340. src/harness/src/openharness/platforms.py +86 -0
  341. src/harness/src/openharness/plugins/__init__.py +53 -0
  342. src/harness/src/openharness/plugins/bundled/__init__.py +0 -0
  343. src/harness/src/openharness/plugins/installer.py +39 -0
  344. src/harness/src/openharness/plugins/loader.py +730 -0
  345. src/harness/src/openharness/plugins/schemas.py +24 -0
  346. src/harness/src/openharness/plugins/types.py +59 -0
  347. src/harness/src/openharness/prompts/__init__.py +14 -0
  348. src/harness/src/openharness/prompts/claudemd.py +48 -0
  349. src/harness/src/openharness/prompts/context.py +187 -0
  350. src/harness/src/openharness/prompts/environment.py +135 -0
  351. src/harness/src/openharness/prompts/system_prompt.py +109 -0
  352. src/harness/src/openharness/sandbox/Dockerfile +6 -0
  353. src/harness/src/openharness/sandbox/__init__.py +33 -0
  354. src/harness/src/openharness/sandbox/adapter.py +148 -0
  355. src/harness/src/openharness/sandbox/docker_backend.py +232 -0
  356. src/harness/src/openharness/sandbox/docker_image.py +103 -0
  357. src/harness/src/openharness/sandbox/path_validator.py +37 -0
  358. src/harness/src/openharness/sandbox/session.py +63 -0
  359. src/harness/src/openharness/services/__init__.py +30 -0
  360. src/harness/src/openharness/services/autodream/__init__.py +34 -0
  361. src/harness/src/openharness/services/autodream/backup.py +104 -0
  362. src/harness/src/openharness/services/autodream/lock.py +138 -0
  363. src/harness/src/openharness/services/autodream/prompt.py +128 -0
  364. src/harness/src/openharness/services/autodream/service.py +313 -0
  365. src/harness/src/openharness/services/compact/__init__.py +1725 -0
  366. src/harness/src/openharness/services/cron.py +137 -0
  367. src/harness/src/openharness/services/cron_scheduler.py +595 -0
  368. src/harness/src/openharness/services/lsp/__init__.py +216 -0
  369. src/harness/src/openharness/services/memory_extract/__init__.py +261 -0
  370. src/harness/src/openharness/services/oauth/__init__.py +0 -0
  371. src/harness/src/openharness/services/session_backend.py +97 -0
  372. src/harness/src/openharness/services/session_memory/__init__.py +139 -0
  373. src/harness/src/openharness/services/session_storage.py +230 -0
  374. src/harness/src/openharness/services/token_estimation.py +15 -0
  375. src/harness/src/openharness/services/tool_outputs.py +55 -0
  376. src/harness/src/openharness/skills/__init__.py +44 -0
  377. src/harness/src/openharness/skills/_frontmatter.py +97 -0
  378. src/harness/src/openharness/skills/bundled/__init__.py +75 -0
  379. src/harness/src/openharness/skills/bundled/content/commit.md +25 -0
  380. src/harness/src/openharness/skills/bundled/content/debug.md +25 -0
  381. src/harness/src/openharness/skills/bundled/content/diagnose.md +35 -0
  382. src/harness/src/openharness/skills/bundled/content/plan.md +39 -0
  383. src/harness/src/openharness/skills/bundled/content/review.md +33 -0
  384. src/harness/src/openharness/skills/bundled/content/simplify.md +26 -0
  385. src/harness/src/openharness/skills/bundled/content/skill-creator.md +175 -0
  386. src/harness/src/openharness/skills/bundled/content/test.md +38 -0
  387. src/harness/src/openharness/skills/loader.py +229 -0
  388. src/harness/src/openharness/skills/registry.py +29 -0
  389. src/harness/src/openharness/skills/types.py +24 -0
  390. src/harness/src/openharness/state/__init__.py +6 -0
  391. src/harness/src/openharness/state/app_state.py +30 -0
  392. src/harness/src/openharness/state/store.py +40 -0
  393. src/harness/src/openharness/swarm/__init__.py +70 -0
  394. src/harness/src/openharness/swarm/in_process.py +693 -0
  395. src/harness/src/openharness/swarm/lockfile.py +24 -0
  396. src/harness/src/openharness/swarm/mailbox.py +522 -0
  397. src/harness/src/openharness/swarm/permission_sync.py +1168 -0
  398. src/harness/src/openharness/swarm/registry.py +410 -0
  399. src/harness/src/openharness/swarm/spawn_utils.py +229 -0
  400. src/harness/src/openharness/swarm/subprocess_backend.py +171 -0
  401. src/harness/src/openharness/swarm/team_lifecycle.py +910 -0
  402. src/harness/src/openharness/swarm/types.py +398 -0
  403. src/harness/src/openharness/swarm/worktree.py +315 -0
  404. src/harness/src/openharness/tasks/__init__.py +18 -0
  405. src/harness/src/openharness/tasks/local_agent_task.py +28 -0
  406. src/harness/src/openharness/tasks/local_shell_task.py +17 -0
  407. src/harness/src/openharness/tasks/manager.py +475 -0
  408. src/harness/src/openharness/tasks/stop_task.py +11 -0
  409. src/harness/src/openharness/tasks/types.py +32 -0
  410. src/harness/src/openharness/themes/__init__.py +21 -0
  411. src/harness/src/openharness/themes/builtin.py +89 -0
  412. src/harness/src/openharness/themes/loader.py +55 -0
  413. src/harness/src/openharness/themes/schema.py +54 -0
  414. src/harness/src/openharness/tools/__init__.py +107 -0
  415. src/harness/src/openharness/tools/agent_tool.py +141 -0
  416. src/harness/src/openharness/tools/ask_user_question_tool.py +46 -0
  417. src/harness/src/openharness/tools/base.py +80 -0
  418. src/harness/src/openharness/tools/bash_tool.py +259 -0
  419. src/harness/src/openharness/tools/brief_tool.py +33 -0
  420. src/harness/src/openharness/tools/config_tool.py +54 -0
  421. src/harness/src/openharness/tools/cron_create_tool.py +105 -0
  422. src/harness/src/openharness/tools/cron_delete_tool.py +32 -0
  423. src/harness/src/openharness/tools/cron_list_tool.py +69 -0
  424. src/harness/src/openharness/tools/cron_toggle_tool.py +37 -0
  425. src/harness/src/openharness/tools/enter_plan_mode_tool.py +28 -0
  426. src/harness/src/openharness/tools/enter_worktree_tool.py +80 -0
  427. src/harness/src/openharness/tools/exit_plan_mode_tool.py +28 -0
  428. src/harness/src/openharness/tools/exit_worktree_tool.py +42 -0
  429. src/harness/src/openharness/tools/file_edit_tool.py +95 -0
  430. src/harness/src/openharness/tools/file_read_tool.py +72 -0
  431. src/harness/src/openharness/tools/file_write_tool.py +87 -0
  432. src/harness/src/openharness/tools/glob_tool.py +175 -0
  433. src/harness/src/openharness/tools/grep_tool.py +374 -0
  434. src/harness/src/openharness/tools/image_generation_tool.py +447 -0
  435. src/harness/src/openharness/tools/image_to_text_tool.py +237 -0
  436. src/harness/src/openharness/tools/list_mcp_resources_tool.py +36 -0
  437. src/harness/src/openharness/tools/lsp_tool.py +154 -0
  438. src/harness/src/openharness/tools/mcp_auth_tool.py +71 -0
  439. src/harness/src/openharness/tools/mcp_tool.py +72 -0
  440. src/harness/src/openharness/tools/notebook_edit_tool.py +97 -0
  441. src/harness/src/openharness/tools/read_mcp_resource_tool.py +38 -0
  442. src/harness/src/openharness/tools/remote_trigger_tool.py +74 -0
  443. src/harness/src/openharness/tools/send_message_tool.py +58 -0
  444. src/harness/src/openharness/tools/skill_tool.py +43 -0
  445. src/harness/src/openharness/tools/sleep_tool.py +32 -0
  446. src/harness/src/openharness/tools/task_create_tool.py +56 -0
  447. src/harness/src/openharness/tools/task_get_tool.py +33 -0
  448. src/harness/src/openharness/tools/task_list_tool.py +35 -0
  449. src/harness/src/openharness/tools/task_output_tool.py +35 -0
  450. src/harness/src/openharness/tools/task_stop_tool.py +30 -0
  451. src/harness/src/openharness/tools/task_update_tool.py +50 -0
  452. src/harness/src/openharness/tools/team_create_tool.py +31 -0
  453. src/harness/src/openharness/tools/team_delete_tool.py +30 -0
  454. src/harness/src/openharness/tools/todo_write_tool.py +46 -0
  455. src/harness/src/openharness/tools/tool_search_tool.py +38 -0
  456. src/harness/src/openharness/tools/web_fetch_tool.py +117 -0
  457. src/harness/src/openharness/tools/web_search_tool.py +119 -0
  458. src/harness/src/openharness/ui/__init__.py +5 -0
  459. src/harness/src/openharness/ui/app.py +320 -0
  460. src/harness/src/openharness/ui/backend_host.py +941 -0
  461. src/harness/src/openharness/ui/coordinator_drain.py +198 -0
  462. src/harness/src/openharness/ui/input.py +32 -0
  463. src/harness/src/openharness/ui/output.py +265 -0
  464. src/harness/src/openharness/ui/permission_dialog.py +14 -0
  465. src/harness/src/openharness/ui/protocol.py +247 -0
  466. src/harness/src/openharness/ui/react_launcher.py +179 -0
  467. src/harness/src/openharness/ui/runtime.py +799 -0
  468. src/harness/src/openharness/ui/textual_app.py +495 -0
  469. src/harness/src/openharness/utils/__init__.py +0 -0
  470. src/harness/src/openharness/utils/file_lock.py +86 -0
  471. src/harness/src/openharness/utils/fs.py +98 -0
  472. src/harness/src/openharness/utils/helpers.py +77 -0
  473. src/harness/src/openharness/utils/network_guard.py +340 -0
  474. src/harness/src/openharness/utils/shell.py +147 -0
  475. src/harness/src/openharness/vim/__init__.py +5 -0
  476. src/harness/src/openharness/vim/transitions.py +8 -0
  477. src/harness/src/openharness/voice/__init__.py +7 -0
  478. src/harness/src/openharness/voice/keyterms.py +10 -0
  479. src/harness/src/openharness/voice/stream_stt.py +8 -0
  480. src/harness/src/openharness/voice/voice_mode.py +44 -0
  481. src/llm/__init__.py +29 -0
  482. src/llm/image_generation.py +402 -0
  483. src/llm/provider_registry.py +419 -0
  484. src/llm/runtime_env.py +64 -0
  485. src/llm/structured_output.py +131 -0
  486. src/llm/vision.py +198 -0
  487. src/memory/__init__.py +2 -0
  488. src/memory/api/__init__.py +1 -0
  489. src/memory/api/base_memory_system_api.py +98 -0
  490. src/memory/api/base_symbolic_memory_system_api.py +190 -0
  491. src/memory/api/base_vector_memory_system_api.py +116 -0
  492. src/memory/api/faiss_memory_system_api.py +228 -0
  493. src/memory/api/slot_process_api.py +827 -0
  494. src/memory/api/symbolic_memory_system_api.py +572 -0
  495. src/memory/memory_system/__init__.py +48 -0
  496. src/memory/memory_system/component_taxonomy.py +430 -0
  497. src/memory/memory_system/llm.py +437 -0
  498. src/memory/memory_system/models.py +168 -0
  499. src/memory/memory_system/user_prompt.py +418 -0
  500. src/memory/memory_system/utils.py +282 -0
  501. src/memory/memory_system/vectorstore.py +311 -0
  502. src/memory/memory_system/working_slot.py +99 -0
  503. src/pipeline/__init__.py +129 -0
  504. src/pipeline/contracts.py +531 -0
  505. src/pipeline/discipline_taxonomy.py +916 -0
  506. src/pipeline/evidence_coverage_ledger.py +1150 -0
  507. src/pipeline/evidence_refinement.py +620 -0
  508. src/pipeline/experiment_to_symbolic.py +137 -0
  509. src/pipeline/multimodal_evidence/__init__.py +28 -0
  510. src/pipeline/multimodal_evidence/capabilities.py +107 -0
  511. src/pipeline/multimodal_evidence/claims.py +135 -0
  512. src/pipeline/multimodal_evidence/contract.py +291 -0
  513. src/pipeline/multimodal_evidence/data_sh_compiler.py +505 -0
  514. src/pipeline/multimodal_evidence/handoff_binding.py +135 -0
  515. src/pipeline/multimodal_evidence/manifest.py +254 -0
  516. src/pipeline/multimodal_evidence/native_analysis.py +479 -0
  517. src/pipeline/multimodal_evidence/perception.py +277 -0
  518. src/pipeline/multimodal_evidence/query_binding.py +65 -0
  519. src/pipeline/multimodal_evidence/rendering.py +120 -0
  520. src/pipeline/multimodal_evidence/safety.py +42 -0
  521. src/pipeline/multimodal_evidence/sampling.py +69 -0
  522. src/pipeline/multimodal_evidence/service.py +148 -0
  523. src/pipeline/multimodal_evidence/survey_integration.py +460 -0
  524. src/pipeline/paper_identity.py +52 -0
  525. src/pipeline/research_design_inventory.py +338 -0
  526. src/pipeline/research_domain_resolution.py +669 -0
  527. src/pipeline/research_identity.py +548 -0
  528. src/pipeline/research_question_contract.py +646 -0
  529. src/pipeline/retrieval_lanes.py +889 -0
  530. src/pipeline/run_loop.py +862 -0
  531. src/pipeline/science_events.py +30 -0
  532. src/pipeline/science_manifests.py +602 -0
  533. src/pipeline/science_run.py +918 -0
  534. src/pipeline/science_stages.py +709 -0
  535. src/pipeline/science_workflow.py +807 -0
  536. src/pipeline/sh_cluster_projection.py +306 -0
  537. src/pipeline/sh_graph_provenance.py +648 -0
  538. src/pipeline/subhypothesis_decomposition.py +392 -0
  539. src/pipeline/subhypothesis_evidence.py +856 -0
  540. src/pipeline/survey_evidence_plan.py +858 -0
  541. src/pipeline/survey_gap_adjudication.py +647 -0
  542. src/pipeline/survey_gap_candidates.py +840 -0
  543. src/pipeline/survey_gap_ledger.py +683 -0
  544. src/pipeline/survey_gap_triage.py +401 -0
  545. src/pipeline/survey_handoff_persistence.py +481 -0
  546. src/pipeline/survey_handoff_projection.py +457 -0
  547. src/pipeline/survey_idea_handoff.py +1112 -0
  548. src/pipeline/survey_idea_loader.py +289 -0
  549. src/research_run_ids.py +11 -0
@@ -0,0 +1,987 @@
1
+ """Whole-document quality scoring and bounded revision for Research Plan Author."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Mapping
6
+ from concurrent.futures import ThreadPoolExecutor, as_completed
7
+ from copy import deepcopy
8
+ import json
9
+ import re
10
+ from typing import Any
11
+
12
+ from src.agents.survey_agent.modules.pe import EVAL_CRITERIA
13
+
14
+ from .latex_safety import LatexSafetyError, split_equation_content
15
+ from .llm_json import call_required_json
16
+ from .markdown_renderer import render_quality_review_markdown, render_research_plan_markdown
17
+ from .semantic_validator import validate_composed_research_plan
18
+
19
+
20
+ AUTHOR_DOCUMENT_QUALITY_SCHEMA_VERSION = "research_plan_author_document_quality_v1"
21
+ AUTHOR_DOCUMENT_REVISION_SCHEMA_VERSION = "research_plan_author_document_revision_v1"
22
+ _RUBRIC_DIMENSIONS = (
23
+ "Synthesis Quality",
24
+ "Organization",
25
+ "Readability",
26
+ "Academic Rigor",
27
+ "Clarity",
28
+ "Coherence",
29
+ "Comprehensiveness",
30
+ "Critical Analysis",
31
+ "Novelty and Insights",
32
+ "Future Directions",
33
+ )
34
+ _SPECIAL_RUBRICS = {
35
+ "mathematics_theory": (
36
+ (
37
+ "Theory Auditability",
38
+ "Assess whether the manuscript turns its central proposal into an auditable chain of labeled definitions, assumptions, lemmas, equations, proof obligations, and explicit dependency links. Reward a reader being able to identify what must be checked next; do not require a completed proof.",
39
+ 0.35,
40
+ ),
41
+ (
42
+ "Boundary and Status Discipline",
43
+ "Assess whether candidate, unverified, expected-not-observed, no-information, and review-required statuses are precise and consistently attached to the relevant mathematical claims. Reward disciplined scope management that still advances the argument rather than repeatedly restating uncertainty.",
44
+ 0.25,
45
+ ),
46
+ (
47
+ "Falsifiability and Decision Completeness",
48
+ "Assess whether falsifiers, counterexample criteria, proof-obligation outcomes, no-information branches, and next actions close the proposal's decision logic. Reward concrete pre-registered responses to each meaningful branch.",
49
+ 0.25,
50
+ ),
51
+ (
52
+ "Energy-Condition Defense",
53
+ "Assess whether the manuscript carefully distinguishes NEC, ANEC/AANEC, null convergence or Ricci contraction, SEC, and independently assumed focusing conditions, including the limits of their implications. Reward accurate boundary defense rather than decorative terminology.",
54
+ 0.15,
55
+ ),
56
+ ),
57
+ "computational_digital": (
58
+ (
59
+ "Computational Methodological Strength",
60
+ "Assess task definition, data split, baselines, ablations, robustness, metrics, and reproducibility as one coherent computational plan.",
61
+ 1.0,
62
+ ),
63
+ ),
64
+ "materials_chemical": (
65
+ (
66
+ "Mechanistic and Characterization Strength",
67
+ "Assess the connection among mechanism, material variables, characterization, controls, design of experiments, and reproducibility.",
68
+ 1.0,
69
+ ),
70
+ ),
71
+ "engineering_energy": (
72
+ (
73
+ "System-Boundary and Validation Strength",
74
+ "Assess system boundaries, constraints, metrics, failure modes, layered validation, and engineering implementability.",
75
+ 1.0,
76
+ ),
77
+ ),
78
+ "earth_environment_agro": (
79
+ (
80
+ "Spatiotemporal and Causal Design Strength",
81
+ "Assess spatial and temporal units, sampling frame, covariates, confounding, causal interpretation, and extrapolation limits.",
82
+ 1.0,
83
+ ),
84
+ ),
85
+ "life_veterinary": (
86
+ (
87
+ "Experimental Controls and Biological Validity",
88
+ "Assess model choice, controls, repeats, assays, bias control, and biological interpretation boundaries.",
89
+ 1.0,
90
+ ),
91
+ ),
92
+ "clinical_health": (
93
+ (
94
+ "Clinical Validity, Safety, and Governance Strength",
95
+ "Assess PICO, endpoints, bias control, clinical relevance, safety, ethics, governance, and implementability.",
96
+ 1.0,
97
+ ),
98
+ ),
99
+ }
100
+ _VISIBLE_CITATION_TOKEN = re.compile(r"\[@[^\]]+\]")
101
+ _EVALUATION_MARKER = re.compile(r"<!--\s*author:section=[^>]+-->", flags=re.IGNORECASE)
102
+ _REVIEW_PRESENTATION_PREFIX = re.compile(
103
+ r"\*\*(?:"
104
+ r"Definition\.|"
105
+ r"Proposition \((?:proposed|Candidate)\)\.|"
106
+ r"Planned protocol\.|"
107
+ r"Conditional outcome branch\.|"
108
+ r"Human-review checklist\.|"
109
+ r"Lemma(?: [^*]+)?|"
110
+ r"Proof Obligation(?: [^*]+)?|"
111
+ r"Equation \[[^*]+\]\.|"
112
+ r"Pre-registered Branch \([^*]+\)\.|"
113
+ r"Decision Status: No-information\."
114
+ r")\*\*\s*",
115
+ flags=re.IGNORECASE,
116
+ )
117
+
118
+
119
+ def _mapping(value: object) -> dict[str, Any]:
120
+ return dict(value) if isinstance(value, Mapping) else {}
121
+
122
+
123
+ def _text(value: object) -> str:
124
+ return str(value or "").strip()
125
+
126
+
127
+ def _as_bool(value: object, default: bool) -> bool:
128
+ if isinstance(value, bool):
129
+ return value
130
+ if value is None:
131
+ return default
132
+ return str(value).strip().casefold() in {"1", "true", "yes", "on"}
133
+
134
+
135
+ def _setting_or_default(settings: Mapping[str, Any], key: str, default: Any) -> Any:
136
+ """Preserve explicit falsy settings such as zero iterations or zero weight."""
137
+
138
+ value = settings.get(key)
139
+ return default if value is None else value
140
+
141
+
142
+ def _scorecard(
143
+ scores: Mapping[str, int],
144
+ special_scores: Mapping[str, int],
145
+ special_weight: float,
146
+ *,
147
+ special_dimension_weights: Mapping[str, float] | None = None,
148
+ ) -> dict[str, Any]:
149
+ core = (scores["Synthesis Quality"] + scores["Organization"]) / 2
150
+ writing = (scores["Readability"] + scores["Academic Rigor"] + scores["Clarity"] + scores["Coherence"]) / 4
151
+ depth = (scores["Comprehensiveness"] + scores["Critical Analysis"] + scores["Novelty and Insights"] + scores["Future Directions"]) / 4
152
+ total = 0.33 * core + 0.33 * writing + 0.34 * depth
153
+ normalized_special_weights = {
154
+ dimension: max(0.0, float((special_dimension_weights or {}).get(dimension, 1.0)))
155
+ for dimension in special_scores
156
+ }
157
+ special_weight_total = sum(normalized_special_weights.values())
158
+ special = (
159
+ sum(special_scores[dimension] * normalized_special_weights[dimension] for dimension in special_scores)
160
+ / special_weight_total
161
+ if special_scores and special_weight_total > 0
162
+ else total
163
+ )
164
+ weight = min(1.0, max(0.0, float(special_weight))) if special_scores else 0.0
165
+ return {
166
+ "dimension_scores": dict(scores),
167
+ "special_dimension_scores": dict(special_scores),
168
+ "special_dimension_weights": normalized_special_weights,
169
+ "core_quality": round(core, 4),
170
+ "writing_quality": round(writing, 4),
171
+ "content_depth": round(depth, 4),
172
+ "total_score": round(total, 4),
173
+ "final_100": round(total * 10, 2),
174
+ "special_score": round(special, 4),
175
+ "selection_score": round((1 - weight) * total + weight * special, 4),
176
+ "special_score_weight": weight,
177
+ }
178
+
179
+
180
+ def _block_lookup(document: Mapping[str, Any]) -> dict[tuple[str, str], str]:
181
+ result: dict[tuple[str, str], str] = {}
182
+ for section in [*(document.get("sections") or []), *(document.get("appendices") or [])]:
183
+ if not isinstance(section, Mapping):
184
+ continue
185
+ section_id = _text(section.get("section_id"))
186
+ for block in section.get("blocks") or []:
187
+ if isinstance(block, Mapping):
188
+ result[(section_id, _text(block.get("block_id")))] = _text(block.get("text"))
189
+ return result
190
+
191
+
192
+ def _claim_lookup(document: Mapping[str, Any]) -> dict[str, dict[str, Any]]:
193
+ return {
194
+ _text(claim.get("claim_id")): dict(claim)
195
+ for claim in document.get("claim_provenance") or []
196
+ if isinstance(claim, Mapping) and _text(claim.get("claim_id"))
197
+ }
198
+
199
+
200
+ def _normalise_evidence_text(value: object) -> str:
201
+ """Compare Judge excerpts with the raw block after removing review-only decoration."""
202
+
203
+ text = _EVALUATION_MARKER.sub("", _text(value))
204
+ text = _VISIBLE_CITATION_TOKEN.sub("", text)
205
+ text = _REVIEW_PRESENTATION_PREFIX.sub("", text)
206
+ return " ".join(text.replace("$$", " ").split())
207
+
208
+
209
+ def _valid_evidence(
210
+ items: object,
211
+ *,
212
+ blocks: Mapping[tuple[str, str], str],
213
+ ) -> tuple[list[dict[str, Any]] | None, list[str]]:
214
+ if not isinstance(items, list) or not items:
215
+ return None, ["evidence must be a non-empty list"]
216
+ normalized: list[dict[str, Any]] = []
217
+ warnings: list[str] = []
218
+ for index, item in enumerate(items, start=1):
219
+ if not isinstance(item, Mapping):
220
+ warnings.append(f"evidence[{index}] must be an object and was discarded")
221
+ continue
222
+ section_id = _text(item.get("section_id"))
223
+ block_id = _text(item.get("block_id"))
224
+ excerpt = _text(item.get("excerpt"))
225
+ assessment = _text(item.get("assessment"))
226
+ block_text = blocks.get((section_id, block_id), "")
227
+ normalized_excerpt = _normalise_evidence_text(excerpt)
228
+ normalized_block = _normalise_evidence_text(block_text)
229
+ missing = [
230
+ field
231
+ for field, value in {
232
+ "section_id": section_id,
233
+ "block_id": block_id,
234
+ "excerpt": excerpt,
235
+ "assessment": assessment,
236
+ }.items()
237
+ if not value
238
+ ]
239
+ if missing:
240
+ warnings.append(f"evidence[{index}] is missing {', '.join(missing)} and was discarded")
241
+ continue
242
+ if (section_id, block_id) not in blocks:
243
+ warnings.append(f"evidence[{index}] references an unknown section_id/block_id and was discarded")
244
+ continue
245
+ if not normalized_excerpt:
246
+ warnings.append(f"evidence[{index}].excerpt has no retained manuscript text and was discarded")
247
+ continue
248
+ if normalized_excerpt not in normalized_block:
249
+ warnings.append(f"evidence[{index}].excerpt is not found in its referenced block and was discarded")
250
+ continue
251
+ if len(normalized) < 3:
252
+ normalized.append({"section_id": section_id, "block_id": block_id, "excerpt": excerpt, "assessment": assessment})
253
+ if not normalized:
254
+ return None, [*warnings, "no evidence entry could be reliably located in the manuscript"]
255
+ return normalized, warnings
256
+
257
+
258
+ def _valid_references(
259
+ items: object,
260
+ *,
261
+ blocks: Mapping[tuple[str, str], str],
262
+ field_name: str,
263
+ ) -> tuple[list[dict[str, str]], list[str]]:
264
+ if not isinstance(items, list):
265
+ return [], [f"{field_name} must be a list; location is pending review"]
266
+ normalized: list[dict[str, str]] = []
267
+ warnings: list[str] = []
268
+ for index, item in enumerate(items, start=1):
269
+ if not isinstance(item, Mapping):
270
+ warnings.append(f"{field_name}[{index}] must be an object and was discarded")
271
+ continue
272
+ section_id = _text(item.get("section_id"))
273
+ block_id = _text(item.get("block_id"))
274
+ if (section_id, block_id) not in blocks:
275
+ warnings.append(f"{field_name}[{index}] references an unknown section_id/block_id and was discarded")
276
+ continue
277
+ if len(normalized) < 3:
278
+ normalized.append({"section_id": section_id, "block_id": block_id})
279
+ return normalized, warnings
280
+
281
+
282
+ def _response_summary(response: object) -> dict[str, Any]:
283
+ """Keep structural Judge diagnostics without persisting model prose or prompts."""
284
+
285
+ if not isinstance(response, Mapping):
286
+ return {"response_type": type(response).__name__}
287
+ strength = _mapping(response.get("maximum_strength"))
288
+ return {
289
+ "response_type": "object",
290
+ "top_level_keys": sorted(_text(key) for key in response.keys())[:20],
291
+ "dimension": _text(response.get("dimension")),
292
+ "score": response.get("score") if isinstance(response.get("score"), (str, int, float, bool)) else type(response.get("score")).__name__,
293
+ "rationale_present": bool(_text(response.get("rationale"))),
294
+ "evidence_count": len(response.get("evidence")) if isinstance(response.get("evidence"), list) else None,
295
+ "maximum_strength_present": strength.get("present") if isinstance(strength.get("present"), bool) else None,
296
+ "major_weakness_count": len(response.get("major_weaknesses")) if isinstance(response.get("major_weaknesses"), list) else None,
297
+ "polish_direction_count": len(response.get("polish_directions")) if isinstance(response.get("polish_directions"), list) else None,
298
+ }
299
+
300
+
301
+ def _invalid_report(category: str, *errors: str) -> tuple[None, dict[str, Any]]:
302
+ return None, {
303
+ "failure_category": category,
304
+ "validation_errors": [error for error in errors if error],
305
+ }
306
+
307
+
308
+ def _parse_dimension_report(
309
+ response: object,
310
+ *,
311
+ dimension: str,
312
+ document: Mapping[str, Any],
313
+ ) -> tuple[dict[str, Any] | None, dict[str, Any]]:
314
+ if not isinstance(response, Mapping):
315
+ return _invalid_report("response_shape", "Judge response must be a JSON object")
316
+ returned_dimension = _text(response.get("dimension"))
317
+ if returned_dimension != dimension:
318
+ return _invalid_report(
319
+ "dimension_mismatch",
320
+ f"response.dimension must equal the requested dimension '{dimension}'",
321
+ )
322
+ score = response.get("score")
323
+ if isinstance(score, bool) or not isinstance(score, int) or not 1 <= score <= 10:
324
+ return _invalid_report("score_contract", "response.score must be an integer from 1 through 10")
325
+ rationale = _text(response.get("rationale"))
326
+ if not rationale:
327
+ return _invalid_report("rationale_contract", "response.rationale must be non-empty")
328
+ blocks = _block_lookup(document)
329
+ evidence, evidence_errors = _valid_evidence(response.get("evidence"), blocks=blocks)
330
+ if evidence is None:
331
+ return _invalid_report("grounding_contract", *evidence_errors)
332
+ grounding_warnings = list(evidence_errors)
333
+ strength = _mapping(response.get("maximum_strength"))
334
+ if bool(strength.get("present")):
335
+ strength_refs, strength_errors = _valid_references(
336
+ strength.get("evidence_refs"),
337
+ blocks=blocks,
338
+ field_name="maximum_strength.evidence_refs",
339
+ )
340
+ if not _text(strength.get("description")):
341
+ return _invalid_report("strength_contract", "maximum_strength.description must be non-empty when present=true")
342
+ grounding_warnings.extend(strength_errors)
343
+ strength = {
344
+ "present": True,
345
+ "description": _text(strength.get("description")),
346
+ "evidence_refs": strength_refs,
347
+ "location_status": "located" if strength_refs else "pending_review",
348
+ }
349
+ else:
350
+ strength = {"present": False, "description": "", "evidence_refs": [], "location_status": "not_applicable"}
351
+ weaknesses: list[dict[str, Any]] = []
352
+ raw_weaknesses = response.get("major_weaknesses")
353
+ if not isinstance(raw_weaknesses, list):
354
+ return _invalid_report("weakness_contract", "major_weaknesses must be a list")
355
+ for index, weakness in enumerate(raw_weaknesses[:3], start=1):
356
+ if not isinstance(weakness, Mapping):
357
+ return _invalid_report("weakness_contract", f"major_weaknesses[{index}] must be an object")
358
+ refs, reference_errors = _valid_references(
359
+ weakness.get("evidence_refs"),
360
+ blocks=blocks,
361
+ field_name=f"major_weaknesses[{index}].evidence_refs",
362
+ )
363
+ missing = [
364
+ field
365
+ for field, value in {
366
+ "description": _text(weakness.get("description")),
367
+ "impact": _text(weakness.get("impact")),
368
+ "repair_direction": _text(weakness.get("repair_direction")),
369
+ }.items()
370
+ if not value
371
+ ]
372
+ if missing:
373
+ return _invalid_report("weakness_contract", f"major_weaknesses[{index}] is missing {', '.join(missing)}")
374
+ grounding_warnings.extend(reference_errors)
375
+ weaknesses.append({
376
+ "severity": _text(weakness.get("severity")) or "major",
377
+ "description": _text(weakness.get("description")),
378
+ "impact": _text(weakness.get("impact")),
379
+ "repair_direction": _text(weakness.get("repair_direction")),
380
+ "evidence_refs": refs,
381
+ "location_status": "located" if refs else "pending_review",
382
+ })
383
+ directions: list[dict[str, Any]] = []
384
+ raw_directions = response.get("polish_directions")
385
+ if not isinstance(raw_directions, list) or not raw_directions:
386
+ return _invalid_report("polish_contract", "polish_directions must be a non-empty list")
387
+ for index, direction in enumerate(raw_directions[:3], start=1):
388
+ if not isinstance(direction, Mapping) or not _text(direction.get("direction")):
389
+ return _invalid_report("polish_contract", f"polish_directions[{index}].direction must be non-empty")
390
+ try:
391
+ priority = int(direction.get("priority") or len(directions) + 1)
392
+ except (TypeError, ValueError):
393
+ return _invalid_report("polish_contract", f"polish_directions[{index}].priority must be an integer")
394
+ directions.append({
395
+ "priority": priority,
396
+ "direction": _text(direction.get("direction")),
397
+ "expected_gain": _text(direction.get("expected_gain")),
398
+ })
399
+ return {
400
+ "dimension": dimension,
401
+ "score": score,
402
+ "rationale": rationale,
403
+ "evidence": evidence,
404
+ "maximum_strength": strength,
405
+ "major_weaknesses": weaknesses,
406
+ "polish_directions": directions,
407
+ "grounding_warnings": grounding_warnings,
408
+ }, {"grounding_warnings": grounding_warnings}
409
+
410
+
411
+ def _judge_prompt(*, dimension: str, description: str, manuscript: str) -> str:
412
+ payload = {
413
+ "operation": "research_plan_document_quality_dimension",
414
+ "dimension": dimension,
415
+ "criterion_description": description,
416
+ "manuscript_markdown": manuscript,
417
+ "output_contract": {
418
+ "dimension": dimension,
419
+ "score": "integer 1..10",
420
+ "rationale": "why this score is warranted",
421
+ "evidence": [{"section_id": "existing marker", "block_id": "existing marker", "excerpt": "short phrase copied from the raw block text only", "assessment": "why it matters"}],
422
+ "maximum_strength": {"present": "boolean", "description": "if present", "evidence_refs": [{"section_id": "", "block_id": ""}]},
423
+ "major_weaknesses": [{"severity": "major or moderate", "description": "", "impact": "", "repair_direction": "", "evidence_refs": [{"section_id": "", "block_id": ""}]}],
424
+ "polish_directions": [{"priority": 1, "direction": "specific revision", "expected_gain": "affected quality"}],
425
+ },
426
+ }
427
+ return """You are a senior academic reviewer scoring one dimension of a complete research-plan manuscript. Read the full manuscript and score the requested dimension from 1 to 10 using the supplied criterion. Give a concrete, constructive assessment grounded in the manuscript itself. Identify its strongest contribution when one is present, identify major weaknesses when present, and give practical directions for a stronger next draft. The manuscript is a proposal: reward clear conditional reasoning, honest scope, and useful proof or validation obligations rather than demanding completed experiments or completed proofs. Use the invisible author markers only to locate evidence; never treat them as visible prose. For every evidence excerpt, copy a short phrase from the raw block text, excluding Markdown wrappers, generated citation suffixes, and marker comments. Return exactly one JSON object.\n\nINPUT_JSON:\n""" + json.dumps(payload, ensure_ascii=False)
428
+
429
+
430
+ def _revision_context(
431
+ preparation: Mapping[str, Any],
432
+ source_registry: Mapping[str, Any],
433
+ ) -> dict[str, Any]:
434
+ """Expose the complete frozen upstream knowledge base to the reviser.
435
+
436
+ This is an authoring context, not a section-local source permission list.
437
+ The original Author preparation already owns its provenance; retaining the
438
+ full context lets the reviser make substantive cross-agent connections.
439
+ """
440
+
441
+ knowledge_base = _mapping(source_registry.get("authoring_knowledge_base"))
442
+ if knowledge_base:
443
+ return deepcopy(knowledge_base)
444
+ context = _mapping(_mapping(preparation.get("source_bundle")).get("author_context"))
445
+ return {
446
+ "source_catalog": {
447
+ "citation_registry": deepcopy(list(source_registry.get("citation_registry") or [])),
448
+ "evidence_cards_by_id": deepcopy(_mapping(source_registry.get("evidence_cards_by_id"))),
449
+ },
450
+ "upstream_artifacts": {
451
+ "selected_direction": context.get("selected_direction"),
452
+ "research_design": context.get("research_design"),
453
+ "hypothesis_mapping": context.get("hypothesis_mapping"),
454
+ "formal_reasoning": context.get("formal_reasoning"),
455
+ "counterexample_analysis": context.get("counterexample_analysis"),
456
+ "outcome_branches": context.get("outcome_branches"),
457
+ "reasoning_context": context.get("reasoning_context"),
458
+ "variables_and_operationalization": context.get("variables_and_operationalization"),
459
+ "idea_evolution": _mapping(_mapping(preparation.get("source_bundle")).get("idea_evolution")),
460
+ "survey_binding": _mapping(_mapping(preparation.get("source_bundle")).get("survey_binding")),
461
+ },
462
+ "unknown_items": deepcopy(list(source_registry.get("unknown_items") or [])),
463
+ "review_items": deepcopy(list(source_registry.get("review_items") or [])),
464
+ }
465
+
466
+
467
+ def _editable_block_context(document: Mapping[str, Any]) -> list[dict[str, Any]]:
468
+ claims = _claim_lookup(document)
469
+ editable: list[dict[str, Any]] = []
470
+ for section in [*(document.get("sections") or []), *(document.get("appendices") or [])]:
471
+ if not isinstance(section, Mapping):
472
+ continue
473
+ section_id = _text(section.get("section_id"))
474
+ for block in section.get("blocks") or []:
475
+ if not isinstance(block, Mapping):
476
+ continue
477
+ claim_ids = [_text(claim_id) for claim_id in block.get("claim_ids") or [] if _text(claim_id)]
478
+ editable.append(
479
+ {
480
+ "section_id": section_id,
481
+ "block_id": _text(block.get("block_id")),
482
+ "claim_ids": claim_ids,
483
+ "supported_claims": [
484
+ {
485
+ "claim_id": claim_id,
486
+ "claim_kind": _text(claim.get("claim_kind")),
487
+ "statement": _text(claim.get("statement")),
488
+ "qualification": _text(claim.get("qualification")),
489
+ }
490
+ for claim_id in claim_ids
491
+ if isinstance((claim := claims.get(claim_id)), Mapping)
492
+ ],
493
+ }
494
+ )
495
+ return editable
496
+
497
+
498
+ def _revision_prompt(
499
+ *,
500
+ document: Mapping[str, Any],
501
+ reports: list[Mapping[str, Any]],
502
+ preparation: Mapping[str, Any],
503
+ source_registry: Mapping[str, Any],
504
+ max_edits: int,
505
+ partial_score_recovery: bool = False,
506
+ ) -> str:
507
+ editable_blocks = _editable_block_context(document)
508
+ abstract = _mapping(document.get("abstract"))
509
+ payload = {
510
+ "operation": "research_plan_document_quality_revision",
511
+ "manuscript_markdown": render_quality_review_markdown(document),
512
+ "quality_reports": reports,
513
+ "available_research_context": _revision_context(preparation, source_registry),
514
+ "editable_blocks": editable_blocks,
515
+ "abstract_claim_ids": [_text(claim_id) for claim_id in abstract.get("claim_ids") or [] if _text(claim_id)],
516
+ "max_block_edits": max_edits,
517
+ "output_contract": {
518
+ "schema_version": AUTHOR_DOCUMENT_REVISION_SCHEMA_VERSION,
519
+ "revision_summary": "short summary",
520
+ "abstract_text": "optional complete replacement",
521
+ "abstract_supporting_claim_ids": ["required only when abstract_text is supplied; choose existing abstract_claim_ids"],
522
+ "block_edits": [{"section_id": "existing", "block_id": "existing", "text": "complete replacement", "supporting_claim_ids": ["one or more claim_ids already attached to this block"], "heading": "optional replacement"}],
523
+ },
524
+ }
525
+ recovery_instruction = (
526
+ "A subset of judge reports was unavailable for this draft. Use the available reports to make one focused recovery revision; "
527
+ "the revised draft must earn a complete scorecard before it can compete for selection. "
528
+ if partial_score_recovery
529
+ else ""
530
+ )
531
+ return """You are revising a complete academic research plan after a detailed quality review. Strengthen the argument, depth, structure, mathematical explanation, transitions, and precision where the reports identify useful opportunities. Preserve the manuscript's strongest contributions. Work freely with the supplied research context, but keep the plan honest about what is proposed, conditional, or unresolved. For a mathematics-theory manuscript, make its reasoning auditable: use supplied candidates, proof obligations, falsifiers, no-information branches, and next actions to close the decision logic. Prefer a specific lemma, criterion, implication, or response over repeating that a dependency is undefined or needs confirmation. Do not invent missing definitions or completed proofs merely to improve a score. Return a compact set of high-value complete block replacements; do not rewrite blocks that do not benefit from revision. Existing citations, claim identifiers, and section structure are maintained by the Author backend, so focus on stronger scholarly prose and mathematical or methodological exposition. Every replacement must identify the already attached claims that support it; this preserves the proposal status of the underlying material while allowing deeper synthesis. Do not state a proposed or unverified result as completed, proved, observed, or universal. """ + recovery_instruction + """Return exactly one JSON object.\n\nINPUT_JSON:\n""" + json.dumps(payload, ensure_ascii=False)
532
+
533
+
534
+ def _apply_patch(document: Mapping[str, Any], patch: object, *, max_edits: int) -> tuple[dict[str, Any] | None, str]:
535
+ if not isinstance(patch, Mapping) or _text(patch.get("schema_version")) != AUTHOR_DOCUMENT_REVISION_SCHEMA_VERSION:
536
+ return None, "revision response does not match the document revision contract"
537
+ edits = patch.get("block_edits")
538
+ if not isinstance(edits, list) or len(edits) > max_edits:
539
+ return None, "revision has an invalid number of block edits"
540
+ candidate = deepcopy(dict(document))
541
+ block_lookup = {
542
+ (_text(section.get("section_id")), _text(block.get("block_id"))): block
543
+ for section in [*(candidate.get("sections") or []), *(candidate.get("appendices") or [])]
544
+ if isinstance(section, dict)
545
+ for block in section.get("blocks") or []
546
+ if isinstance(block, dict)
547
+ }
548
+ seen: set[tuple[str, str]] = set()
549
+ for edit in edits:
550
+ if not isinstance(edit, Mapping):
551
+ return None, "revision edit must be an object"
552
+ key = (_text(edit.get("section_id")), _text(edit.get("block_id")))
553
+ text = _text(edit.get("text"))
554
+ if key not in block_lookup or key in seen or not text:
555
+ return None, "revision edit targets an unknown, repeated, or empty block"
556
+ if _VISIBLE_CITATION_TOKEN.search(text):
557
+ return None, "revision may not add inline citation tokens; citations are derived by the Author backend"
558
+ seen.add(key)
559
+ block = block_lookup[key]
560
+ supporting_claim_ids = [_text(claim_id) for claim_id in edit.get("supporting_claim_ids") or [] if _text(claim_id)]
561
+ existing_claim_ids = {_text(claim_id) for claim_id in block.get("claim_ids") or [] if _text(claim_id)}
562
+ if not supporting_claim_ids or set(supporting_claim_ids) - existing_claim_ids:
563
+ return None, "revision edit must identify existing block claim_ids that support its replacement"
564
+ block["text"] = text
565
+ if "heading" in edit:
566
+ block["heading"] = _text(edit.get("heading"))
567
+ abstract_text = _text(patch.get("abstract_text"))
568
+ if abstract_text:
569
+ abstract = _mapping(candidate.get("abstract"))
570
+ supporting_claim_ids = [_text(claim_id) for claim_id in patch.get("abstract_supporting_claim_ids") or [] if _text(claim_id)]
571
+ abstract_claim_ids = {_text(claim_id) for claim_id in abstract.get("claim_ids") or [] if _text(claim_id)}
572
+ if not supporting_claim_ids or set(supporting_claim_ids) - abstract_claim_ids:
573
+ return None, "abstract revision must identify existing abstract claim_ids that support its replacement"
574
+ abstract["text"] = abstract_text
575
+ candidate["abstract"] = abstract
576
+ if not edits and not abstract_text:
577
+ return None, "revision contains no changes"
578
+ for block in block_lookup.values():
579
+ if _text(block.get("kind")) == "equation":
580
+ try:
581
+ fragments = split_equation_content(
582
+ block.get("text"),
583
+ label=f"quality revision block {_text(block.get('block_id'))}",
584
+ )
585
+ if not any(kind == "equation" for kind, _ in fragments):
586
+ raise LatexSafetyError(
587
+ f"quality revision block {_text(block.get('block_id'))} contains no valid mathematical expression"
588
+ )
589
+ except LatexSafetyError as error:
590
+ return None, str(error)
591
+ return candidate, _text(patch.get("revision_summary"))
592
+
593
+
594
+ def optimize_research_plan_document(
595
+ document: Mapping[str, Any],
596
+ *,
597
+ preparation: Mapping[str, Any],
598
+ routing: Mapping[str, Any],
599
+ source_registry: Mapping[str, Any],
600
+ quality_config: Mapping[str, Any] | None,
601
+ judge_llm_call: Callable[..., object] | None,
602
+ revision_llm_call: Callable[..., object] | None,
603
+ logger: Any | None = None,
604
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
605
+ """Score bounded document candidates and retain the best valid version."""
606
+
607
+ settings = _mapping(quality_config)
608
+ enabled = _as_bool(settings.get("enabled"), True)
609
+ special_weight = float(_setting_or_default(settings, "special_score_weight", 0.25))
610
+ partial_score_min_valid_dimensions = max(
611
+ 1,
612
+ int(_setting_or_default(settings, "partial_score_min_valid_dimensions", 8)),
613
+ )
614
+ max_iterations = max(0, int(_setting_or_default(settings, "max_iterations", 2)))
615
+ max_edits = max(1, int(_setting_or_default(settings, "max_revision_block_edits", 14)))
616
+ concurrency = max(1, int(_setting_or_default(settings, "score_concurrency", 3)))
617
+ judge_retries = max(1, int(_setting_or_default(settings, "judge_max_retries", 3)))
618
+ template_family = _text(routing.get("template_family"))
619
+ special = _SPECIAL_RUBRICS.get(template_family)
620
+ effective_special_weight = special_weight if special is not None else 0.0
621
+ special_dimension_weights = {
622
+ dimension: weight
623
+ for dimension, _description, weight in special or ()
624
+ }
625
+ audit: dict[str, Any] = {
626
+ "schema_version": AUTHOR_DOCUMENT_QUALITY_SCHEMA_VERSION,
627
+ "enabled": enabled,
628
+ "model": _text(settings.get("model")),
629
+ "template_family": template_family,
630
+ "general_score_weight": round(1 - effective_special_weight, 4),
631
+ "special_score_weight": round(effective_special_weight, 4),
632
+ "special_dimensions": [
633
+ {"dimension": dimension, "weight": weight}
634
+ for dimension, _description, weight in special or ()
635
+ ],
636
+ "partial_score_min_valid_dimensions": partial_score_min_valid_dimensions,
637
+ "candidates": [],
638
+ "selected_candidate_index": 0,
639
+ "winning_candidate_index": None,
640
+ "selection_status": "UNRESOLVED",
641
+ "warnings": [],
642
+ }
643
+
644
+ def score(candidate: Mapping[str, Any], index: int, parent_index: int | None, summary: str) -> dict[str, Any]:
645
+ record: dict[str, Any] = {
646
+ "candidate_index": index,
647
+ "parent_index": parent_index,
648
+ "revision_summary": summary,
649
+ "markdown": render_research_plan_markdown(candidate),
650
+ "status": "UNSCORED",
651
+ "dimension_reports": [],
652
+ "judge_attempts": [],
653
+ "failed_dimensions": [],
654
+ }
655
+ if not enabled or judge_llm_call is None:
656
+ record["status"] = "SKIPPED"
657
+ return record
658
+ manuscript = render_quality_review_markdown(candidate)
659
+ requested = [(dimension, _text(_mapping(EVAL_CRITERIA.get(dimension)).get("description"))) for dimension in _RUBRIC_DIMENSIONS]
660
+ if special is not None:
661
+ requested.extend(
662
+ (dimension, description)
663
+ for dimension, description, _weight in special
664
+ )
665
+ reports: dict[str, dict[str, Any]] = {}
666
+
667
+ def emit_attempt(attempt_record: Mapping[str, Any]) -> None:
668
+ if logger is None:
669
+ return
670
+ level = (
671
+ "WARNING"
672
+ if attempt_record.get("grounding_warnings")
673
+ else "INFO" if attempt_record.get("status") == "VALID" else "WARNING"
674
+ )
675
+ logger.emit(
676
+ "document_quality",
677
+ "dimension_score_attempt",
678
+ level=level,
679
+ status=str(attempt_record.get("status") or "WARNING"),
680
+ candidate_index=index,
681
+ dimension=attempt_record.get("dimension"),
682
+ attempt=attempt_record.get("attempt"),
683
+ failure_category=attempt_record.get("failure_category"),
684
+ validation_errors=attempt_record.get("validation_errors") or [],
685
+ grounding_warnings=attempt_record.get("grounding_warnings") or [],
686
+ response_summary=attempt_record.get("response_summary") or {},
687
+ error_type=attempt_record.get("error_type"),
688
+ failure_detail=attempt_record.get("failure_detail"),
689
+ )
690
+
691
+ def judge_dimension(dimension: str, description: str) -> tuple[dict[str, Any] | None, list[dict[str, Any]]]:
692
+ attempts: list[dict[str, Any]] = []
693
+ for attempt in range(judge_retries):
694
+ attempt_number = attempt + 1
695
+ try:
696
+ response = call_required_json(
697
+ judge_llm_call,
698
+ _judge_prompt(dimension=dimension, description=description, manuscript=manuscript),
699
+ stage=f"author_document_quality:{dimension}",
700
+ )
701
+ parsed, diagnostic = _parse_dimension_report(response, dimension=dimension, document=candidate)
702
+ attempt_record: dict[str, Any] = {
703
+ "dimension": dimension,
704
+ "attempt": attempt_number,
705
+ "response_summary": _response_summary(response),
706
+ }
707
+ if parsed is not None:
708
+ attempt_record["status"] = "VALID"
709
+ grounding_warnings = list(diagnostic.get("grounding_warnings") or [])
710
+ if grounding_warnings:
711
+ attempt_record["warning_category"] = "grounding_warning"
712
+ attempt_record["grounding_warnings"] = grounding_warnings
713
+ attempts.append(attempt_record)
714
+ emit_attempt(attempt_record)
715
+ return parsed, attempts
716
+ attempt_record.update(
717
+ {
718
+ "status": "INVALID",
719
+ "failure_category": diagnostic.get("failure_category") or "report_contract",
720
+ "validation_errors": list(diagnostic.get("validation_errors") or []),
721
+ }
722
+ )
723
+ except Exception as error:
724
+ attempt_record = {
725
+ "dimension": dimension,
726
+ "attempt": attempt_number,
727
+ "status": "ERROR",
728
+ "failure_category": "judge_call_error",
729
+ "error_type": type(error).__name__,
730
+ "failure_detail": _text(error)[:400],
731
+ }
732
+ attempts.append(attempt_record)
733
+ emit_attempt(attempt_record)
734
+ return None, attempts
735
+
736
+ judge_attempts: list[dict[str, Any]] = []
737
+ with ThreadPoolExecutor(max_workers=min(concurrency, len(requested))) as executor:
738
+ futures = {
739
+ executor.submit(judge_dimension, dimension, description): dimension
740
+ for dimension, description in requested
741
+ }
742
+ for future in as_completed(futures):
743
+ dimension = futures[future]
744
+ try:
745
+ parsed, dimension_attempts = future.result()
746
+ except Exception as error:
747
+ parsed = None
748
+ dimension_attempts = [{
749
+ "dimension": dimension,
750
+ "attempt": 0,
751
+ "status": "ERROR",
752
+ "failure_category": "judge_worker_error",
753
+ "error_type": type(error).__name__,
754
+ "failure_detail": _text(error)[:400],
755
+ }]
756
+ emit_attempt(dimension_attempts[0])
757
+ judge_attempts.extend(dimension_attempts)
758
+ if parsed is None:
759
+ record["failed_dimensions"].append(dimension)
760
+ last_attempt = dimension_attempts[-1] if dimension_attempts else {}
761
+ category = _text(last_attempt.get("failure_category")) or "unknown_failure"
762
+ audit["warnings"].append(
763
+ f"candidate {index} judge report for {dimension} was unavailable after "
764
+ f"{len(dimension_attempts)} attempt(s): {category}"
765
+ )
766
+ else:
767
+ reports[dimension] = parsed
768
+ requested_index = {dimension: position for position, (dimension, _description) in enumerate(requested)}
769
+ record["judge_attempts"] = sorted(
770
+ judge_attempts,
771
+ key=lambda attempt: (requested_index.get(_text(attempt.get("dimension")), len(requested)), int(attempt.get("attempt") or 0)),
772
+ )
773
+ record["failed_dimensions"] = [
774
+ dimension for dimension, _description in requested if dimension in set(record["failed_dimensions"])
775
+ ]
776
+ record["dimension_reports"] = [reports[key] for key, _description in requested if key in reports]
777
+ if len(reports) != len(requested):
778
+ record["partial_scorecard"] = {
779
+ "available_dimension_scores": {
780
+ dimension: reports[dimension]["score"]
781
+ for dimension, _description in requested
782
+ if dimension in reports
783
+ },
784
+ "missing_dimensions": list(record["failed_dimensions"]),
785
+ "valid_dimension_count": len(reports),
786
+ "requested_dimension_count": len(requested),
787
+ "complete": False,
788
+ }
789
+ record["status"] = (
790
+ "PARTIALLY_SCORED"
791
+ if len(reports) >= partial_score_min_valid_dimensions
792
+ else "JUDGE_FAILED"
793
+ )
794
+ return record
795
+ ordinary = {dimension: reports[dimension]["score"] for dimension in _RUBRIC_DIMENSIONS}
796
+ special_scores = {
797
+ dimension: reports[dimension]["score"]
798
+ for dimension, _description, _weight in special or ()
799
+ }
800
+ record["scorecard"] = _scorecard(
801
+ ordinary,
802
+ special_scores,
803
+ effective_special_weight,
804
+ special_dimension_weights=special_dimension_weights,
805
+ )
806
+ record["status"] = "SCORED"
807
+ return record
808
+
809
+ baseline = deepcopy(dict(document))
810
+ candidates = [score(baseline, 0, None, "Initial composed manuscript")]
811
+ candidate_documents = [baseline]
812
+ if logger is not None:
813
+ logger.emit(
814
+ "document_quality",
815
+ "candidate_scored",
816
+ status=candidates[0]["status"],
817
+ candidate_index=0,
818
+ total_score=_mapping(candidates[0].get("scorecard")).get("total_score"),
819
+ failed_dimension_count=len(candidates[0].get("failed_dimensions") or []),
820
+ failed_dimensions=candidates[0].get("failed_dimensions") or [],
821
+ )
822
+ current = baseline
823
+ current_index = 0
824
+ partial_score_recovery_used = False
825
+ for iteration in range(1, max_iterations + 1):
826
+ current_record = candidates[current_index]
827
+ current_status = _text(current_record.get("status"))
828
+ is_partial_recovery = current_status == "PARTIALLY_SCORED"
829
+ if current_status not in {"SCORED", "PARTIALLY_SCORED"} or revision_llm_call is None:
830
+ break
831
+ if is_partial_recovery and partial_score_recovery_used:
832
+ break
833
+ if is_partial_recovery:
834
+ current_record["partial_score_recovery_attempted"] = True
835
+ try:
836
+ patch = call_required_json(
837
+ revision_llm_call,
838
+ _revision_prompt(
839
+ document=current,
840
+ reports=list(current_record["dimension_reports"]),
841
+ preparation=preparation,
842
+ source_registry=source_registry,
843
+ max_edits=max_edits,
844
+ partial_score_recovery=is_partial_recovery,
845
+ ),
846
+ stage="author_document_quality_revision",
847
+ )
848
+ revised, summary = _apply_patch(current, patch, max_edits=max_edits)
849
+ except Exception as error:
850
+ revised, summary = None, f"revision call failed: {type(error).__name__}"
851
+ if revised is None:
852
+ audit["warnings"].append(f"quality iteration {iteration} discarded: {summary}")
853
+ break
854
+ validation_errors = validate_composed_research_plan(revised, preparation=preparation, routing=routing, source_registry=source_registry)
855
+ if validation_errors:
856
+ audit["warnings"].append(f"quality iteration {iteration} discarded by final validation: {'; '.join(validation_errors)}")
857
+ break
858
+ candidate_index = len(candidates)
859
+ candidate = score(revised, candidate_index, current_index, summary)
860
+ candidates.append(candidate)
861
+ candidate_documents.append(revised)
862
+ if logger is not None:
863
+ logger.emit(
864
+ "document_quality",
865
+ "candidate_scored",
866
+ status=candidate["status"],
867
+ candidate_index=candidate_index,
868
+ parent_index=current_index,
869
+ total_score=_mapping(candidate.get("scorecard")).get("total_score"),
870
+ failed_dimension_count=len(candidate.get("failed_dimensions") or []),
871
+ failed_dimensions=candidate.get("failed_dimensions") or [],
872
+ )
873
+ current = revised
874
+ current_index = candidate_index
875
+ if is_partial_recovery:
876
+ partial_score_recovery_used = True
877
+ break
878
+ scored = [candidate for candidate in candidates if candidate.get("status") == "SCORED"]
879
+ if scored:
880
+ selected = max(scored, key=lambda candidate: (_mapping(candidate.get("scorecard")).get("selection_score", -1), -int(candidate["candidate_index"])))
881
+ selected_index = int(selected["candidate_index"])
882
+ selected_document = candidate_documents[selected_index]
883
+ audit["winning_candidate_index"] = selected_index
884
+ audit["selection_status"] = "COMPLETE_SCORECARD_SELECTED"
885
+ else:
886
+ selected = candidates[0]
887
+ selected_index = 0
888
+ selected_document = baseline
889
+ audit["selection_status"] = (
890
+ "PARTIAL_SCORECARD_RETAINED_AS_FALLBACK"
891
+ if any(candidate.get("status") == "PARTIALLY_SCORED" for candidate in candidates)
892
+ else "NO_COMPLETE_SCORECARD_RETAINED_AS_FALLBACK"
893
+ )
894
+ audit["warnings"].append("no complete quality scorecard was available; retained the initial manuscript without selecting a quality winner")
895
+ audit["candidates"] = candidates
896
+ audit["selected_candidate_index"] = selected_index
897
+ return deepcopy(dict(selected_document)), audit
898
+
899
+
900
+ def render_document_quality_report(audit: Mapping[str, Any]) -> str:
901
+ """Render a human-readable score report without exposing prompts or sources."""
902
+
903
+ lines = ["# Research Plan Quality Report"]
904
+ winning_index = audit.get("winning_candidate_index")
905
+ if winning_index is None:
906
+ lines.append(f"- Retained manuscript candidate: {audit.get('selected_candidate_index', 0)} (no quality winner selected)")
907
+ else:
908
+ lines.append(f"- Selected candidate: {winning_index}")
909
+ for candidate in audit.get("candidates") or []:
910
+ if not isinstance(candidate, Mapping):
911
+ continue
912
+ lines.append(f"## Candidate {candidate.get('candidate_index')} — {candidate.get('status')}")
913
+ scorecard = _mapping(candidate.get("scorecard"))
914
+ if scorecard:
915
+ lines.append(f"- Total Score: {scorecard.get('total_score')} / 10")
916
+ lines.append(f"- Selection Score: {scorecard.get('selection_score')} / 10")
917
+ special_weights = _mapping(scorecard.get("special_dimension_weights"))
918
+ if special_weights:
919
+ lines.append(
920
+ "- Theory/domain score weights: "
921
+ + ", ".join(f"{dimension}={weight}" for dimension, weight in special_weights.items())
922
+ )
923
+ partial_scorecard = _mapping(candidate.get("partial_scorecard"))
924
+ if partial_scorecard:
925
+ lines.append(
926
+ "- Partial scorecard: "
927
+ + f"{partial_scorecard.get('valid_dimension_count')} of "
928
+ + f"{partial_scorecard.get('requested_dimension_count')} reports were valid; "
929
+ + "this candidate cannot be selected."
930
+ )
931
+ missing_dimensions = partial_scorecard.get("missing_dimensions") or []
932
+ if missing_dimensions:
933
+ lines.append("- Unavailable dimensions: " + ", ".join(_text(item) for item in missing_dimensions))
934
+ failed_attempts = [
935
+ attempt
936
+ for attempt in candidate.get("judge_attempts") or []
937
+ if isinstance(attempt, Mapping) and _text(attempt.get("status")) != "VALID"
938
+ ]
939
+ if failed_attempts:
940
+ lines.append("#### Judge Diagnostics")
941
+ for attempt in failed_attempts:
942
+ detail = "; ".join(_text(error) for error in attempt.get("validation_errors") or [] if _text(error))
943
+ if not detail:
944
+ detail = _text(attempt.get("failure_detail")) or _text(attempt.get("error_type"))
945
+ lines.append(
946
+ f"- {attempt.get('dimension')} attempt {attempt.get('attempt')}: "
947
+ f"{attempt.get('failure_category') or 'unknown_failure'}"
948
+ + (f" — {detail}" if detail else "")
949
+ )
950
+ for report in candidate.get("dimension_reports") or []:
951
+ if not isinstance(report, Mapping):
952
+ continue
953
+ lines.append(f"### {report.get('dimension')} — {report.get('score')} / 10")
954
+ lines.append(_text(report.get("rationale")))
955
+ for warning in report.get("grounding_warnings") or []:
956
+ if _text(warning):
957
+ lines.append("**Grounding warning:** " + _text(warning))
958
+ for evidence in report.get("evidence") or []:
959
+ if isinstance(evidence, Mapping):
960
+ lines.append(
961
+ "**Grounding:** "
962
+ + f"{_text(evidence.get('section_id'))}/{_text(evidence.get('block_id'))} — "
963
+ + _text(evidence.get("assessment"))
964
+ )
965
+ strength = _mapping(report.get("maximum_strength"))
966
+ if strength.get("present"):
967
+ lines.append("**Maximum strength:** " + _text(strength.get("description")))
968
+ if _text(strength.get("location_status")) == "pending_review":
969
+ lines.append("**Location:** pending review")
970
+ for weakness in report.get("major_weaknesses") or []:
971
+ if isinstance(weakness, Mapping):
972
+ lines.append("**Major weakness:** " + _text(weakness.get("description")))
973
+ if _text(weakness.get("location_status")) == "pending_review":
974
+ lines.append("**Location:** pending review")
975
+ lines.append("**Direction:** " + _text(weakness.get("repair_direction")))
976
+ for direction in report.get("polish_directions") or []:
977
+ if isinstance(direction, Mapping):
978
+ lines.append("**Polish direction:** " + _text(direction.get("direction")))
979
+ return "\n\n".join(line for line in lines if line.strip()) + "\n"
980
+
981
+
982
+ __all__ = [
983
+ "AUTHOR_DOCUMENT_QUALITY_SCHEMA_VERSION",
984
+ "AUTHOR_DOCUMENT_REVISION_SCHEMA_VERSION",
985
+ "optimize_research_plan_document",
986
+ "render_document_quality_report",
987
+ ]