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,3000 @@
1
+ """Runtime models and helper functions for memory-guided MCTS idea search."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import itertools
7
+ import json
8
+ import math
9
+ import random
10
+ import re
11
+ from copy import deepcopy
12
+ from dataclasses import dataclass, field, replace
13
+ from enum import Enum
14
+ from pathlib import Path
15
+ from typing import Any, Dict, List, Mapping, Optional, Sequence, Set, Tuple
16
+ from src.agents.idea_agent.utils.core.json_utils import (
17
+ pretty_json,
18
+ read_json_file,
19
+ )
20
+ from src.agents.idea_agent.utils.core.response_parsing import parse_json_response
21
+ from src.agents.idea_agent.utils.mcts.defect_registry import (
22
+ format_defect_registry,
23
+ profile_skill_defect_tags,
24
+ )
25
+ from src.agents.idea_agent.utils.mcts.idea_taste_presets import IdeaTastePreset
26
+ from src.agents.idea_agent.utils.mcts.skill_parsing import (
27
+ parse_blueprint_step,
28
+ parse_markdown_sections,
29
+ split_frontmatter,
30
+ )
31
+ from src.agents.idea_agent.utils.mcts.scientific_intervention_ontology import (
32
+ build_scientific_intervention_payload,
33
+ detect_profile_drift,
34
+ format_scientific_intervention_profile_for_prompt,
35
+ get_scientific_intervention_profile,
36
+ get_scientific_object_schema,
37
+ resolve_scientific_intervention_profile,
38
+ )
39
+ from src.agents.idea_agent.utils.mcts.scientific_rubric import (
40
+ SCIENTIFIC_INTERVENTION_PROFILE_VERSION,
41
+ SCIENTIFIC_RUBRIC_VERSION,
42
+ PROFILE_NOVELTY_AXES,
43
+ format_scientific_rubric_for_prompt,
44
+ profile_score_weights,
45
+ )
46
+ from src.memory.memory_system.component_taxonomy import extract_component_families
47
+ from src.agents.idea_agent.utils.mcts.mcts_helpers import (
48
+ _format_root_domains_for_prompt,
49
+ _clean_component_explanation,
50
+ _coerce_component_name,
51
+ _dedupe_keep_order_strings,
52
+ _filter_component_mapping_to_plan_keys,
53
+ _normalize_component_mapping,
54
+ build_structural_profile,
55
+ clip_text,
56
+ component_inventory_payload,
57
+ normalize_component_explanations,
58
+ parse_component_bundle_payload,
59
+ plan_to_experiment_text,
60
+ plan_to_method_text,
61
+ )
62
+ from src.agents.idea_agent.utils.prompting.prompt_views import (
63
+ format_evaluator_edit_plan_prompt_view,
64
+ format_evaluator_idea_prompt_view,
65
+ )
66
+ from src.agents.idea_agent.utils.workflow.idea_contract import (
67
+ normalize_idea_contract,
68
+ normalize_mature_idea,
69
+ )
70
+ from src.agents.idea_agent.utils.workflow.multimodal_data_anchoring import (
71
+ DATA_ANCHORED_PRIORITY,
72
+ is_data_anchored,
73
+ )
74
+
75
+
76
+ HYPOTHESIS_CONTRACT_FIELDS = (
77
+ "direction_mode",
78
+ "direction_summary",
79
+ "central_hypothesis",
80
+ "scientific_object",
81
+ "mechanism_or_relation",
82
+ "intervention_or_transformation",
83
+ "expected_mechanism",
84
+ "discriminating_observation",
85
+ "boundary_or_failure_condition",
86
+ "claim_scope",
87
+ "assumptions",
88
+ "target_gap_ids",
89
+ "gap_alignment",
90
+ "evidence_requirement",
91
+ "evidence_basis",
92
+ )
93
+
94
+ _ROUTE_REQUIRED_CONTRACT_FIELDS = {
95
+ "premise_inversion": ("central_hypothesis",),
96
+ "object_substitution": ("scientific_object",),
97
+ "mechanism_replacement": ("mechanism_or_relation", "expected_mechanism"),
98
+ "representation_shift": ("intervention_or_transformation",),
99
+ "verification_reversal": ("discriminating_observation",),
100
+ }
101
+
102
+
103
+ class AtomicEditOp(str, Enum):
104
+ ADD_COMPONENT = "ADD_COMPONENT"
105
+ REMOVE_COMPONENT = "REMOVE_COMPONENT"
106
+ REPLACE_COMPONENT = "REPLACE_COMPONENT"
107
+ REWIRE = "REWIRE"
108
+ ADD_PROTOCOL = "ADD_PROTOCOL"
109
+
110
+
111
+ # Descriptions for each atomic edit operation, used in prompt formatting and skill documentation.
112
+ ATOMIC_OP_DESCRIPTIONS: Dict[str, str] = {
113
+ AtomicEditOp.ADD_COMPONENT: "Introduce a new module or sub-module into the architecture. ",
114
+ AtomicEditOp.REMOVE_COMPONENT: "Delete an existing module from the architecture. ",
115
+ AtomicEditOp.REPLACE_COMPONENT: "Swap an existing module with a new implementation. ",
116
+ AtomicEditOp.REWIRE: "Change how two components are connected (data flow, gradient path, or API coupling). ",
117
+ AtomicEditOp.ADD_PROTOCOL: "Attach a validation protocol (regression, ablation, or stress test) to the plan. "
118
+ }
119
+
120
+
121
+ def format_op_descriptions(profile_id: Optional[str] = None) -> str:
122
+ """Return a human-readable reference block describing every atomic edit operation."""
123
+ if profile_id and str(profile_id).strip().lower() != "computational_algorithmic":
124
+ return "\n".join(
125
+ [
126
+ "Scientific transformation reference (use the selected profile's native objects directly):",
127
+ " - ADD_COMPONENT: introduce one object, condition, mechanism, relation, or observation named by the profile.",
128
+ " - REMOVE_COMPONENT: remove one unsupported or redundant object, assumption, process, or condition.",
129
+ " - REPLACE_COMPONENT: replace one weak object, assumption, process, or mechanism with a refined one.",
130
+ " - REWIRE: change a causal relation, derivation, observation mapping, or process relation.",
131
+ " - ADD_PROTOCOL: attach the evidence, measurement, proof, comparator, or boundary check required by the profile.",
132
+ ]
133
+ )
134
+ lines = ["Atomic edit operation reference:"]
135
+ for op in AtomicEditOp:
136
+ desc = ATOMIC_OP_DESCRIPTIONS.get(op, "No description.")
137
+ lines.append(f" - {op.value}: {desc}")
138
+ return "\n".join(lines)
139
+
140
+
141
+ def render_profile_evaluation_prompt(prompt: str, profile_id: str) -> str:
142
+ """Remove computational evaluation priors from non-computational prompts."""
143
+
144
+ if str(profile_id or "").strip().lower() == "computational_algorithmic":
145
+ return prompt
146
+ replacements = {
147
+ "- Evaluate the candidate using the scientific intervention profile and rubric above. The native contribution may be a material manipulation, biological or clinical causal intervention, environmental condition, engineering process, formal assumption/derivation/counterexample, measurement construct, or algorithmic mechanism.": "- Evaluate the candidate using the scientific intervention profile and rubric above. The native contribution must use the selected profile's object, process, mechanism, relation, observation, intervention, or formal claim.",
148
+ "- Do not treat training signal, loss, backbone, benchmark, or learned model components as universal requirements. For non-computational profiles, judge innovation through the profile's native mechanism, object, observation, comparator, proof, or boundary conditions; missing training machinery is not an innovation defect.": "- Judge innovation through the selected profile's native object, mechanism, relation, observation, comparator, proof, intervention, or boundary conditions.",
149
+ "- If the mature idea above is training-free or inference-time only, penalize candidates that add new training stages, learned controllers, auxiliary losses, or fine-tuning loops without a strong mechanism-level justification. Such drift should usually reduce alignment_score and increase complexity_penalty.": "- Penalize candidates that add unrelated optimization, control, or wrapper machinery without a strong profile-native mechanism justification.",
150
+ "generator, objective, representation, planner, or data path": "scientific mechanism, intervention, relation, observation, or process",
151
+ "- Do not speculate about compute/resource budgeting unless the candidate explicitly makes resource management part of its core mechanism.": "- Do not speculate about implementation resources unless the candidate explicitly makes resource constraints part of its core scientific claim.",
152
+ }
153
+ for source, target in replacements.items():
154
+ prompt = prompt.replace(source, target)
155
+ return prompt
156
+
157
+ def _synthesize_component_explanation_from_edit(component: str, edit: Optional["ComponentEdit"]) -> str:
158
+ if edit is None:
159
+ return _clean_component_explanation("", fallback_component=component)
160
+
161
+ if edit.reason:
162
+ return _clean_component_explanation(edit.reason, fallback_component=component)
163
+ if edit.op == AtomicEditOp.REPLACE_COMPONENT and edit.target:
164
+ return _clean_component_explanation(
165
+ f"Replaces {edit.target} with a stronger implementation for the updated idea.",
166
+ fallback_component=component,
167
+ )
168
+ if edit.op == AtomicEditOp.ADD_COMPONENT:
169
+ return _clean_component_explanation(
170
+ "Adds a targeted capability that was missing in the parent idea.",
171
+ fallback_component=component,
172
+ )
173
+ if edit.details:
174
+ return _clean_component_explanation(edit.details, fallback_component=component)
175
+ return _clean_component_explanation("", fallback_component=component)
176
+
177
+
178
+ def build_child_component_explanations(
179
+ parent_state: Any,
180
+ new_components: Sequence[str],
181
+ plan: "EditPlan",
182
+ instantiated: Optional[Dict[str, Any]] = None,
183
+ ) -> Dict[str, str]:
184
+ parent_lookup = normalize_component_explanations(
185
+ getattr(parent_state, "components", []),
186
+ getattr(parent_state, "component_explanations", {}),
187
+ )
188
+ payload_lookup = normalize_component_explanations(
189
+ list((instantiated or {}).get("component_role_explanations", {}).keys())
190
+ if isinstance((instantiated or {}).get("component_role_explanations"), dict)
191
+ else [],
192
+ (instantiated or {}).get("component_role_explanations", {}),
193
+ )
194
+ edit_lookup: Dict[str, ComponentEdit] = {}
195
+ for edit in getattr(plan, "component_edits", []) or []:
196
+ name = _coerce_component_name(getattr(edit, "component", ""))
197
+ if name:
198
+ edit_lookup[name] = edit
199
+
200
+ explanations: Dict[str, str] = {}
201
+ for component in new_components:
202
+ name = str(component).strip()
203
+ if not name:
204
+ continue
205
+ if name in parent_lookup:
206
+ explanations[name] = parent_lookup[name]
207
+ continue
208
+ if name in payload_lookup:
209
+ explanations[name] = payload_lookup[name]
210
+ continue
211
+ explanations[name] = _synthesize_component_explanation_from_edit(
212
+ name,
213
+ edit_lookup.get(name),
214
+ )
215
+ return explanations
216
+
217
+
218
+ @dataclass
219
+ class ComponentEdit:
220
+ op: AtomicEditOp
221
+ component: str
222
+ target: str = ""
223
+ condition: str = ""
224
+ details: str = ""
225
+ reason: str = ""
226
+
227
+ def to_dict(self) -> Dict[str, Any]:
228
+ return {
229
+ "op": self.op.value,
230
+ "component": _coerce_component_name(self.component),
231
+ "target": _coerce_component_name(self.target),
232
+ "condition": self.condition,
233
+ "details": self.details,
234
+ "reason": self.reason,
235
+ }
236
+
237
+
238
+ @dataclass
239
+ class ValidationProtocol:
240
+ regression_tests: List[str] = field(default_factory=list)
241
+ ablation_tests: List[str] = field(default_factory=list)
242
+ stress_tests: List[str] = field(default_factory=list)
243
+
244
+ def to_dict(self) -> Dict[str, Any]:
245
+ return {
246
+ "regression_tests": self.regression_tests,
247
+ "ablation_tests": self.ablation_tests,
248
+ "stress_tests": self.stress_tests,
249
+ }
250
+
251
+
252
+ @dataclass
253
+ class EditPlan:
254
+ skill_name: str
255
+ objective: str
256
+ target_defects: List[str]
257
+ component_edits: List[ComponentEdit]
258
+ validation: ValidationProtocol
259
+ guardrails: List[str]
260
+ memory_refs: List[str]
261
+ compile_notes: str
262
+ profile_id: str = ""
263
+ profile_rendering_id: str = ""
264
+ preferred_operations: List[str] = field(default_factory=list)
265
+ scientific_object_schema: Dict[str, Any] = field(default_factory=dict)
266
+
267
+ def to_dict(self) -> Dict[str, Any]:
268
+ return {
269
+ "skill_name": self.skill_name,
270
+ "objective": self.objective,
271
+ "target_defects": self.target_defects,
272
+ "component_edits": [edit.to_dict() for edit in self.component_edits],
273
+ "validation": self.validation.to_dict(),
274
+ "guardrails": self.guardrails,
275
+ "memory_refs": self.memory_refs,
276
+ "compile_notes": self.compile_notes,
277
+ "profile_id": self.profile_id,
278
+ "profile_rendering_id": self.profile_rendering_id,
279
+ "preferred_operations": self.preferred_operations,
280
+ "scientific_object_schema": self.scientific_object_schema,
281
+ }
282
+
283
+
284
+ @dataclass
285
+ class EditOperatorSkill:
286
+ name: str
287
+ description: str
288
+ structural_mode: str
289
+ scope_preference: str
290
+ requires_control_centered_parent: bool
291
+ defects: List[str] = field(default_factory=list)
292
+ guardrails: List[str] = field(default_factory=list)
293
+ atomic_blueprint: List[str] = field(default_factory=list)
294
+ required_protocols: List[str] = field(default_factory=list)
295
+ avoid_combinations: List[str] = field(default_factory=list)
296
+ execution_logic: List[str] = field(default_factory=list)
297
+ source_path: str = ""
298
+ profile_renderings: Dict[str, Dict[str, Any]] = field(default_factory=dict)
299
+ rendered_profile_id: str = ""
300
+ allowed_object_types: List[str] = field(default_factory=list)
301
+ preferred_operations: List[str] = field(default_factory=list)
302
+ profile_rendering_id: str = ""
303
+
304
+ def to_prompt_line(self) -> str:
305
+ defects = ", ".join(self.defects) if self.defects else "unspecified"
306
+ blueprint = ", ".join(self.atomic_blueprint) if self.atomic_blueprint else "none"
307
+ guardrails = ", ".join(self.guardrails) if self.guardrails else "none"
308
+ exec_logic = " | ".join(self.execution_logic) if self.execution_logic else "none"
309
+ profile = f" | profile={self.rendered_profile_id}" if self.rendered_profile_id else ""
310
+ native_ops = (
311
+ f" | native_operations={','.join(self.preferred_operations)}"
312
+ if self.preferred_operations
313
+ else ""
314
+ )
315
+ native_objects = (
316
+ f" | native_objects={','.join(self.allowed_object_types)}"
317
+ if self.allowed_object_types
318
+ else ""
319
+ )
320
+ return (
321
+ f"- {self.name}: {self.description} | defects={defects} "
322
+ f"| blueprint={blueprint} | guardrails={guardrails} "
323
+ f"| execution_logic={exec_logic}{profile}{native_ops}{native_objects}"
324
+ )
325
+
326
+
327
+ @dataclass
328
+ class SkillUsagePrior:
329
+ attempts: int = 0
330
+ successes: int = 0
331
+ reward_ema: float = 0.5
332
+ prior: float = 0.5
333
+ rule_constraints: List[str] = field(default_factory=list)
334
+
335
+ def to_dict(self) -> Dict[str, Any]:
336
+ return {
337
+ "attempts": self.attempts,
338
+ "successes": self.successes,
339
+ "reward_ema": self.reward_ema,
340
+ "prior": self.prior,
341
+ "rule_constraints": self.rule_constraints,
342
+ }
343
+
344
+
345
+ @dataclass
346
+ class SkillSelectionCandidate:
347
+ skill: EditOperatorSkill
348
+ defect_score: float
349
+ prior_score: float
350
+ preset_bias: float
351
+ structure_fit: float
352
+ structure_reason: str
353
+ selection_total: float
354
+ attempts: int = 0
355
+
356
+ def to_dict(self) -> Dict[str, Any]:
357
+ return {
358
+ "skill_name": self.skill.name,
359
+ "defect_score": self.defect_score,
360
+ "prior_score": self.prior_score,
361
+ "preset_bias": self.preset_bias,
362
+ "structure_fit": self.structure_fit,
363
+ "structure_reason": self.structure_reason,
364
+ "selection_total": self.selection_total,
365
+ "attempts": self.attempts,
366
+ }
367
+
368
+
369
+ @dataclass
370
+ class MemorySnippet:
371
+ identifier: str
372
+ title: str
373
+ detail: str
374
+ tags: List[str] = field(default_factory=list)
375
+
376
+ def to_prompt_line(self) -> str:
377
+ tags_str = f" tags={','.join(self.tags)}" if self.tags else ""
378
+ return f"[{self.identifier}] {self.title}{tags_str}: {self.detail}"
379
+
380
+
381
+ @dataclass
382
+ class MemoryBundle:
383
+ field_knowledge: List[MemorySnippet] = field(default_factory=list)
384
+ anti_patterns: List[MemorySnippet] = field(default_factory=list)
385
+ fix_recipes: List[MemorySnippet] = field(default_factory=list)
386
+
387
+ def to_prompt_block(self) -> str:
388
+ sections: List[str] = []
389
+ if self.field_knowledge:
390
+ sections.append("== Field Knowledge ==")
391
+ sections.extend(snippet.to_prompt_line() for snippet in self.field_knowledge)
392
+ if self.anti_patterns:
393
+ sections.append("== Anti-patterns ==")
394
+ sections.extend(snippet.to_prompt_line() for snippet in self.anti_patterns)
395
+ if self.fix_recipes:
396
+ sections.append("== Fix Recipes ==")
397
+ sections.extend(snippet.to_prompt_line() for snippet in self.fix_recipes)
398
+ if not sections:
399
+ return "No validated memory snippets matched. Rely on analysis context only."
400
+ return "\n".join(sections)
401
+
402
+ def referenced_ids(self) -> List[str]:
403
+ ids: List[str] = []
404
+ for bank in (self.field_knowledge, self.anti_patterns, self.fix_recipes):
405
+ ids.extend(snippet.identifier for snippet in bank)
406
+ return ids
407
+
408
+
409
+ DEFAULT_SKILL_TEMPLATES_PATH = (
410
+ Path(__file__).resolve().parents[2]
411
+ / "agent"
412
+ / "skills"
413
+ / "DEFAULT_SKILL_TEMPLATES.json"
414
+ )
415
+
416
+
417
+ def _load_default_skill_templates(path: Path) -> Dict[str, Dict[str, Any]]:
418
+ if not path.exists():
419
+ return {}
420
+ payload = read_json_file(path)
421
+ if not isinstance(payload, dict):
422
+ return {}
423
+ normalized: Dict[str, Dict[str, Any]] = {}
424
+ for key, value in payload.items():
425
+ name = str(key).strip()
426
+ if not name or not isinstance(value, dict):
427
+ continue
428
+ normalized[name] = value
429
+ return normalized
430
+
431
+
432
+ DEFAULT_SKILL_TEMPLATES: Dict[str, Dict[str, Any]] = _load_default_skill_templates(
433
+ DEFAULT_SKILL_TEMPLATES_PATH
434
+ )
435
+
436
+ ANTI_PATTERN_CONSTRAINTS: List[str] = [
437
+ "No feature dumping: every component edit must map to a measured defect.",
438
+ "Use the lightest validation suite that can falsify the core mechanism; do not let protocol bulk replace mechanism work.",
439
+ "Prefer mechanism clarity over loosely coupled add-ons.",
440
+ ]
441
+
442
+
443
+ _PROFILE_SKILL_TOKEN_MAPS: Dict[str, Dict[str, str]] = {
444
+ "physical_materials_chemical": {
445
+ "weak_internal_component": "weak_process_or_mechanism",
446
+ "refined_internal_component": "refined_process_or_mechanism",
447
+ "alternative_path_module": "competing_mechanism_or_phase",
448
+ "failure_regime_interface": "mechanism_discriminating_observation",
449
+ "weak_block": "weak_process_or_composition",
450
+ "modular_block": "localized_process_or_mechanism",
451
+ "downstream_interface": "property_or_performance_endpoint",
452
+ "scale_consistency_module": "structure_process_property_link",
453
+ "scale_interface": "cross_scale_characterization",
454
+ "flat_pipeline": "uncalibrated_process_structure_relation",
455
+ "hierarchical_pipeline": "structure_process_property_decomposition",
456
+ "execution_path": "process_observation_path",
457
+ "feedback_monitor": "characterization_feedback",
458
+ "adaptation_rule": "process_condition_update",
459
+ "theory_transfer_module": "transferred_material_mechanism",
460
+ "core_objective": "target_property_or_mechanism",
461
+ "speculative_executor": "exploratory_process_condition",
462
+ "repair_handler": "recovery_or_reprocessing_condition",
463
+ },
464
+ "life_molecular_mechanistic": {
465
+ "weak_internal_component": "weak_pathway_or_assay",
466
+ "refined_internal_component": "refined_mediator_or_perturbation",
467
+ "alternative_path_module": "competing_biological_pathway",
468
+ "failure_regime_interface": "mechanism_discriminating_assay",
469
+ "weak_block": "weak_intervention_or_mediator",
470
+ "modular_block": "localized_perturbation_pathway",
471
+ "downstream_interface": "phenotype_or_endpoint",
472
+ "scale_consistency_module": "mediator_phenotype_link",
473
+ "scale_interface": "multi_level_assay",
474
+ "flat_pipeline": "unresolved_perturbation_mediator_relation",
475
+ "hierarchical_pipeline": "system_pathway_phenotype_decomposition",
476
+ "execution_path": "perturbation_observation_path",
477
+ "feedback_monitor": "assay_response_observation",
478
+ "adaptation_rule": "dose_or_condition_update",
479
+ "theory_transfer_module": "transferred_biological_mechanism",
480
+ "core_objective": "target_phenotype_or_pathway",
481
+ "speculative_executor": "exploratory_perturbation",
482
+ "repair_handler": "recovery_or_control_condition",
483
+ },
484
+ "clinical_health": {
485
+ "weak_internal_component": "weak_intervention_or_mediator",
486
+ "refined_internal_component": "refined_intervention_or_mediator",
487
+ "alternative_path_module": "competing_causal_explanation",
488
+ "failure_regime_interface": "comparator_or_counterfactual",
489
+ "weak_block": "weak_intervention_or_measurement",
490
+ "modular_block": "targeted_intervention_or_mediator",
491
+ "downstream_interface": "clinical_endpoint",
492
+ "scale_consistency_module": "population_endpoint_mediation_link",
493
+ "scale_interface": "cohort_subgroup_boundary",
494
+ "flat_pipeline": "unresolved_intervention_outcome_relation",
495
+ "hierarchical_pipeline": "population_intervention_mediator_endpoint_decomposition",
496
+ "execution_path": "intervention_measurement_path",
497
+ "feedback_monitor": "outcome_measurement",
498
+ "adaptation_rule": "care_or_intervention_update",
499
+ "theory_transfer_module": "transferred_causal_mechanism",
500
+ "core_objective": "clinical_endpoint_or_utility",
501
+ "speculative_executor": "pilot_intervention_condition",
502
+ "repair_handler": "safety_or_escalation_condition",
503
+ },
504
+ "earth_environment_agro": {
505
+ "weak_internal_component": "weak_forcing_or_process",
506
+ "refined_internal_component": "refined_forcing_or_process",
507
+ "alternative_path_module": "competing_regime_or_process",
508
+ "failure_regime_interface": "spatiotemporal_regime_boundary",
509
+ "weak_block": "weak_observation_or_attribution",
510
+ "modular_block": "localized_process_or_observation",
511
+ "downstream_interface": "response_or_endpoint",
512
+ "scale_consistency_module": "spatiotemporal_scale_link",
513
+ "scale_interface": "observation_scale_alignment",
514
+ "flat_pipeline": "unresolved_forcing_process_response_relation",
515
+ "hierarchical_pipeline": "forcing_process_response_decomposition",
516
+ "execution_path": "forcing_observation_path",
517
+ "feedback_monitor": "observation_response_update",
518
+ "adaptation_rule": "scenario_or_management_update",
519
+ "theory_transfer_module": "transferred_earth_system_mechanism",
520
+ "core_objective": "response_or_attribution_claim",
521
+ "speculative_executor": "exploratory_scenario",
522
+ "repair_handler": "observation_or_scenario_recovery",
523
+ },
524
+ "energy_engineering_systems": {
525
+ "weak_internal_component": "weak_design_or_mechanism",
526
+ "refined_internal_component": "refined_design_or_mechanism",
527
+ "alternative_path_module": "alternate_operating_or_failure_regime",
528
+ "failure_regime_interface": "operating_state_boundary",
529
+ "weak_block": "weak_design_or_control_condition",
530
+ "modular_block": "localized_design_or_process",
531
+ "downstream_interface": "performance_or_safety_readout",
532
+ "scale_consistency_module": "component_system_link",
533
+ "scale_interface": "system_constraint_boundary",
534
+ "flat_pipeline": "uncalibrated_design_mechanism_relation",
535
+ "hierarchical_pipeline": "component_process_system_decomposition",
536
+ "execution_path": "operating_state_path",
537
+ "feedback_monitor": "sensor_or_performance_readout",
538
+ "adaptation_rule": "operating_condition_update",
539
+ "theory_transfer_module": "transferred_engineering_mechanism",
540
+ "core_objective": "design_or_safety_claim",
541
+ "speculative_executor": "recoverable_operating_condition",
542
+ "repair_handler": "fault_recovery_condition",
543
+ },
544
+ "formal_theoretical": {
545
+ "weak_internal_component": "weak_assumption_or_derivation",
546
+ "refined_internal_component": "refined_assumption_or_derivation",
547
+ "alternative_path_module": "counterexample_or_alternative_derivation",
548
+ "failure_regime_interface": "validity_domain_boundary",
549
+ "weak_block": "weak_proof_obligation",
550
+ "modular_block": "localized_proof_obligation",
551
+ "downstream_interface": "target_relation",
552
+ "scale_consistency_module": "multi_level_formal_relation",
553
+ "scale_interface": "validity_domain",
554
+ "flat_pipeline": "unresolved_assumption_derivation_relation",
555
+ "hierarchical_pipeline": "object_assumption_relation_decomposition",
556
+ "execution_path": "derivation_or_construction_path",
557
+ "feedback_monitor": "counterexample_check",
558
+ "adaptation_rule": "conjecture_refinement",
559
+ "theory_transfer_module": "transferred_formal_invariant",
560
+ "core_objective": "target_proposition_or_relation",
561
+ "speculative_executor": "candidate_construction",
562
+ "repair_handler": "counterexample_repair_obligation",
563
+ },
564
+ "generic_scientific": {
565
+ "weak_internal_component": "weak_candidate_mechanism",
566
+ "refined_internal_component": "refined_candidate_mechanism",
567
+ "alternative_path_module": "competing_explanation",
568
+ "failure_regime_interface": "boundary_condition",
569
+ "weak_block": "weak_manipulable_condition",
570
+ "modular_block": "localized_scientific_intervention",
571
+ "downstream_interface": "observable_or_endpoint",
572
+ "scale_consistency_module": "cross_scale_relation",
573
+ "scale_interface": "validity_domain",
574
+ "flat_pipeline": "unresolved_mechanism_relation",
575
+ "hierarchical_pipeline": "object_mechanism_observation_decomposition",
576
+ "execution_path": "intervention_observation_path",
577
+ "feedback_monitor": "measurement_observation",
578
+ "adaptation_rule": "condition_update",
579
+ "theory_transfer_module": "transferred_scientific_mechanism",
580
+ "core_objective": "target_scientific_relation",
581
+ "speculative_executor": "exploratory_intervention",
582
+ "repair_handler": "recovery_condition",
583
+ },
584
+ }
585
+
586
+
587
+ _PROFILE_SKILL_DESCRIPTION_PREFIXES: Dict[str, str] = {
588
+ "physical_materials_chemical": "materials/process-native",
589
+ "life_molecular_mechanistic": "biological/mechanistic",
590
+ "clinical_health": "clinical/causal",
591
+ "earth_environment_agro": "earth-system/observational",
592
+ "energy_engineering_systems": "engineering/system",
593
+ "formal_theoretical": "formal/theoretical",
594
+ "generic_scientific": "domain-neutral scientific",
595
+ }
596
+
597
+
598
+ _PROFILE_DISABLED_SKILLS: Dict[str, Set[str]] = {
599
+ "formal_theoretical": {"speculative-execution-with-repair"},
600
+ "clinical_health": {"speculative-execution-with-repair"},
601
+ "earth_environment_agro": {"speculative-execution-with-repair"},
602
+ }
603
+
604
+
605
+ _PROFILE_ALLOWED_SKILLS: Dict[str, Set[str]] = {
606
+ "physical_materials_chemical": {
607
+ "mechanism-commit-innovation",
608
+ "alternative-path-contrast",
609
+ "feedback-closed-loop",
610
+ "multi-scale-coordinator",
611
+ "theory-transfer-injection",
612
+ },
613
+ "life_molecular_mechanistic": {
614
+ "mechanism-commit-innovation",
615
+ "alternative-path-contrast",
616
+ "feedback-closed-loop",
617
+ "multi-scale-coordinator",
618
+ "theory-transfer-injection",
619
+ },
620
+ "clinical_health": {
621
+ "mechanism-commit-innovation",
622
+ "alternative-path-contrast",
623
+ "feedback-closed-loop",
624
+ "theory-transfer-injection",
625
+ },
626
+ "earth_environment_agro": {
627
+ "mechanism-commit-innovation",
628
+ "alternative-path-contrast",
629
+ "feedback-closed-loop",
630
+ "multi-scale-coordinator",
631
+ "theory-transfer-injection",
632
+ },
633
+ "energy_engineering_systems": {
634
+ "mechanism-commit-innovation",
635
+ "alternative-path-contrast",
636
+ "feedback-closed-loop",
637
+ "multi-scale-coordinator",
638
+ "theory-transfer-injection",
639
+ },
640
+ "formal_theoretical": {
641
+ "mechanism-commit-innovation",
642
+ "alternative-path-contrast",
643
+ "hierarchical-decomposition",
644
+ "theory-transfer-injection",
645
+ },
646
+ "generic_scientific": {
647
+ "mechanism-commit-innovation",
648
+ "alternative-path-contrast",
649
+ "feedback-closed-loop",
650
+ "multi-scale-coordinator",
651
+ "theory-transfer-injection",
652
+ },
653
+ }
654
+
655
+
656
+ def _profile_skill_description(skill_name: str, profile_id: str) -> str:
657
+ prefix = _PROFILE_SKILL_DESCRIPTION_PREFIXES.get(profile_id, "profile-native scientific")
658
+ descriptions = {
659
+ "mechanism-commit-innovation": "Commit to one concrete mechanism-level intervention in the selected scientific object.",
660
+ "alternative-path-contrast": "Contrast one competing mechanism, explanation, or validity regime.",
661
+ "surgical-modularity": "Perform one localized intervention on a process, relation, object, or mechanism.",
662
+ "multi-scale-coordinator": "Link the relevant scales, regimes, or levels of the scientific relation.",
663
+ "hierarchical-decomposition": "Decompose the scientific object, assumptions, process, or evidence obligation across explicit levels.",
664
+ "feedback-closed-loop": "Use observations or counterexamples to refine the intervention or scientific relation.",
665
+ "theory-transfer-injection": "Transfer one principle or invariant into the selected scientific mechanism or relation.",
666
+ "speculative-execution-with-repair": "Explore a recoverable alternative condition and state its repair or recovery boundary.",
667
+ }
668
+ return f"{prefix} {descriptions.get(skill_name, 'scientific intervention refinement')}"
669
+
670
+
671
+ class SkillCatalog:
672
+ def __init__(self, skill_root: Optional[Path] = None) -> None:
673
+ if skill_root is None:
674
+ skill_root = (
675
+ Path(__file__).resolve().parents[2]
676
+ / "agent"
677
+ / "skills"
678
+ / "edit_operator_skills"
679
+ )
680
+ self.skill_root = skill_root
681
+ self.skills: Dict[str, EditOperatorSkill] = {}
682
+ self.priors: Dict[str, SkillUsagePrior] = {}
683
+ self._load()
684
+
685
+ def _load(self) -> None:
686
+ loaded: Dict[str, EditOperatorSkill] = {}
687
+ if self.skill_root.exists():
688
+ for skill_file in sorted(self.skill_root.glob("*/SKILL.md")):
689
+ parsed = self._parse_skill_file(skill_file)
690
+ if parsed:
691
+ loaded[parsed.name] = parsed
692
+ for name, payload in DEFAULT_SKILL_TEMPLATES.items():
693
+ if name in loaded:
694
+ continue
695
+ loaded[name] = EditOperatorSkill(
696
+ name=name,
697
+ description=payload["description"],
698
+ structural_mode=payload["structural_mode"],
699
+ scope_preference=payload["scope_preference"],
700
+ requires_control_centered_parent=bool(payload["requires_control_centered_parent"]),
701
+ defects=list(payload.get("defects", [])),
702
+ guardrails=list(payload.get("guardrails", [])),
703
+ atomic_blueprint=list(payload.get("atomic_blueprint", [])),
704
+ required_protocols=list(payload.get("required_protocols", [])),
705
+ avoid_combinations=list(payload.get("avoid_combinations", [])),
706
+ execution_logic=list(payload.get("execution_logic", [])),
707
+ source_path="builtin-default",
708
+ profile_renderings=(
709
+ dict(payload.get("profile_renderings"))
710
+ if isinstance(payload.get("profile_renderings"), dict)
711
+ else {}
712
+ ),
713
+ )
714
+ self.skills = loaded
715
+ for name in self.skills:
716
+ self.priors.setdefault(name, SkillUsagePrior())
717
+
718
+ def _parse_skill_file(self, path: Path) -> Optional[EditOperatorSkill]:
719
+ text = path.read_text(encoding="utf-8")
720
+ frontmatter, body = split_frontmatter(text)
721
+ name = str(frontmatter.get("name", "")).strip() or path.parent.name
722
+ description = str(frontmatter.get("description", "")).strip()
723
+ sections = parse_markdown_sections(body)
724
+ template = DEFAULT_SKILL_TEMPLATES.get(name, {})
725
+ structural_mode_entries = sections.get("structural_mode", [])
726
+ scope_preference_entries = sections.get("scope_preference", [])
727
+ requires_control_centered_entries = sections.get("requires_control_centered_parent", [])
728
+ structural_mode = (
729
+ structural_mode_entries[0].strip()
730
+ if structural_mode_entries and str(structural_mode_entries[0]).strip()
731
+ else str(template.get("structural_mode", "local_refinement")).strip()
732
+ )
733
+ scope_preference = (
734
+ scope_preference_entries[0].strip()
735
+ if scope_preference_entries and str(scope_preference_entries[0]).strip()
736
+ else str(template.get("scope_preference", "existing_subsystem")).strip()
737
+ )
738
+ if (
739
+ requires_control_centered_entries
740
+ and str(requires_control_centered_entries[0]).strip()
741
+ ):
742
+ requires_control_centered_parent = (
743
+ requires_control_centered_entries[0].strip().lower() == "true"
744
+ )
745
+ else:
746
+ requires_control_centered_parent = bool(
747
+ template.get("requires_control_centered_parent", False)
748
+ )
749
+ defects = sections.get("defect_tags", []) or sections.get("defects", [])
750
+ guardrails = sections.get("guardrails", [])
751
+ atomic_blueprint = sections.get("atomic_blueprint", [])
752
+ required_protocols = sections.get("required_protocols", [])
753
+ avoid_combinations = sections.get("avoid_combinations", [])
754
+ execution_logic = sections.get("execution_logic", [])
755
+ if not description:
756
+ description = str(template.get("description", ""))
757
+ if not defects:
758
+ defects = list(template.get("defects", []))
759
+ if not guardrails:
760
+ guardrails = list(template.get("guardrails", []))
761
+ if not atomic_blueprint:
762
+ atomic_blueprint = list(template.get("atomic_blueprint", []))
763
+ if not required_protocols:
764
+ required_protocols = list(template.get("required_protocols", []))
765
+ if not execution_logic:
766
+ execution_logic = list(template.get("execution_logic", []))
767
+ if not description:
768
+ return None
769
+
770
+ return EditOperatorSkill(
771
+ name=name,
772
+ description=description,
773
+ structural_mode=structural_mode,
774
+ scope_preference=scope_preference,
775
+ requires_control_centered_parent=requires_control_centered_parent,
776
+ defects=defects,
777
+ guardrails=guardrails,
778
+ atomic_blueprint=atomic_blueprint,
779
+ required_protocols=required_protocols,
780
+ avoid_combinations=avoid_combinations,
781
+ execution_logic=execution_logic,
782
+ source_path=str(path),
783
+ profile_renderings=(
784
+ dict(template.get("profile_renderings"))
785
+ if isinstance(template.get("profile_renderings"), dict)
786
+ else {}
787
+ ),
788
+ )
789
+
790
+ def list_skills(self) -> List[EditOperatorSkill]:
791
+ return [self.skills[key] for key in sorted(self.skills.keys())]
792
+
793
+ def render_skill_for_profile(
794
+ self,
795
+ skill: EditOperatorSkill | str,
796
+ profile_id: str,
797
+ object_schema: Optional[Dict[str, Any]] = None,
798
+ ) -> EditOperatorSkill:
799
+ """Render a skill using the selected profile's native object vocabulary.
800
+
801
+ The atomic operation names remain legacy-compatible. Only their
802
+ placeholders, descriptions, and guardrails are rendered, so existing
803
+ parsers and persisted edit plans continue to work.
804
+ """
805
+
806
+ base = self.skills[skill] if isinstance(skill, str) else skill
807
+ normalized_profile = str(profile_id or "generic_scientific").strip().lower()
808
+ explicit_rendering = base.profile_renderings.get(normalized_profile)
809
+ if not isinstance(explicit_rendering, dict):
810
+ explicit_rendering = {}
811
+
812
+ def schema_payload(value: Any) -> Dict[str, Any]:
813
+ if isinstance(value, dict):
814
+ return value
815
+ if hasattr(value, "to_payload"):
816
+ rendered = value.to_payload()
817
+ return rendered if isinstance(rendered, dict) else {}
818
+ return {}
819
+
820
+ if normalized_profile == "computational_algorithmic":
821
+ schema = schema_payload(object_schema)
822
+ if not schema:
823
+ schema_spec = get_scientific_object_schema(normalized_profile)
824
+ schema = schema_spec.to_payload() if schema_spec is not None else {}
825
+ rendered = replace(
826
+ base,
827
+ rendered_profile_id=normalized_profile,
828
+ profile_rendering_id=f"{base.name}:{normalized_profile}:v1",
829
+ allowed_object_types=list(schema.get("object_types") or []),
830
+ preferred_operations=list(schema.get("allowed_operations") or []),
831
+ )
832
+ if explicit_rendering:
833
+ rendered = replace(rendered, **{
834
+ key: value for key, value in explicit_rendering.items()
835
+ if key in {"description", "structural_mode", "scope_preference", "guardrails", "atomic_blueprint", "required_protocols", "avoid_combinations", "execution_logic", "allowed_object_types", "preferred_operations"}
836
+ })
837
+ return rendered
838
+
839
+ if normalized_profile not in _PROFILE_SKILL_TOKEN_MAPS:
840
+ normalized_profile = "generic_scientific"
841
+ schema = schema_payload(object_schema)
842
+ if not schema:
843
+ schema_spec = get_scientific_object_schema(normalized_profile)
844
+ schema = schema_spec.to_payload() if schema_spec is not None else {}
845
+ token_map = _PROFILE_SKILL_TOKEN_MAPS[normalized_profile]
846
+
847
+ def render_text(value: str) -> str:
848
+ rendered = str(value or "")
849
+ for source, target in token_map.items():
850
+ rendered = rendered.replace(source, target)
851
+ return rendered
852
+
853
+ native_guardrails = [
854
+ "Use profile-native scientific objects and operations; do not recast the intervention as software architecture by default.",
855
+ "Keep the claim delta, mechanism delta, evidence obligation, and boundary condition explicit.",
856
+ ]
857
+ rendered = replace(
858
+ base,
859
+ description=_profile_skill_description(base.name, normalized_profile),
860
+ guardrails=[render_text(item) for item in base.guardrails] + native_guardrails,
861
+ atomic_blueprint=[render_text(item) for item in base.atomic_blueprint],
862
+ required_protocols=list(base.required_protocols),
863
+ execution_logic=[render_text(item) for item in base.execution_logic],
864
+ rendered_profile_id=normalized_profile,
865
+ profile_rendering_id=f"{base.name}:{normalized_profile}:v1",
866
+ allowed_object_types=list(schema.get("object_types") or []),
867
+ preferred_operations=list(schema.get("allowed_operations") or []),
868
+ )
869
+ if explicit_rendering:
870
+ rendered = replace(rendered, **{
871
+ key: value for key, value in explicit_rendering.items()
872
+ if key in {"description", "structural_mode", "scope_preference", "guardrails", "atomic_blueprint", "required_protocols", "avoid_combinations", "execution_logic", "allowed_object_types", "preferred_operations"}
873
+ })
874
+ return rendered
875
+
876
+ def format_for_prompt(
877
+ self,
878
+ skills: Optional[Sequence[EditOperatorSkill]] = None,
879
+ *,
880
+ profile_id: Optional[str] = None,
881
+ object_schema: Optional[Dict[str, Any]] = None,
882
+ ) -> str:
883
+ chosen = list(skills) if skills is not None else self.list_skills()
884
+ if profile_id:
885
+ normalized_profile = str(profile_id).strip().lower()
886
+ allowed_skills = _PROFILE_ALLOWED_SKILLS.get(normalized_profile)
887
+ if allowed_skills is not None:
888
+ chosen = [skill for skill in chosen if skill.name in allowed_skills]
889
+ chosen = [
890
+ self.render_skill_for_profile(skill, profile_id, object_schema)
891
+ for skill in chosen
892
+ ]
893
+ if not chosen:
894
+ return "No edit-operator skills available."
895
+ op_ref = format_op_descriptions(profile_id)
896
+ skill_lines = "\n".join(skill.to_prompt_line() for skill in chosen)
897
+ return f"{op_ref}\n\nAvailable edit-operator skills:\n{skill_lines}"
898
+
899
+ def render_for_profile(
900
+ self,
901
+ skill: EditOperatorSkill | str,
902
+ profile_id: str,
903
+ object_schema: Optional[Dict[str, Any]] = None,
904
+ ) -> EditOperatorSkill:
905
+ """Compatibility alias for callers using the shorter renderer name."""
906
+
907
+ return self.render_skill_for_profile(skill, profile_id, object_schema)
908
+
909
+ def render_references_for_prompt(self, skill_name: str) -> str:
910
+ skill = self.skills[skill_name]
911
+ if skill.source_path == "builtin-default":
912
+ return "None."
913
+ reference_dir = Path(skill.source_path).parent / "references"
914
+ if not reference_dir.exists():
915
+ return "None."
916
+ sections: List[str] = []
917
+ for reference_file in sorted(reference_dir.glob("*.md")):
918
+ content = reference_file.read_text(encoding="utf-8").strip()
919
+ if content:
920
+ sections.append(f"== {reference_file.name} ==\n{content}")
921
+ return "\n\n".join(sections) if sections else "None."
922
+
923
+ def select_skills(
924
+ self,
925
+ defect_tags: Sequence[str],
926
+ max_children: int,
927
+ structural_profile: Dict[str, Any],
928
+ preset: Optional[IdeaTastePreset] = None,
929
+ profile_id: Optional[str] = None,
930
+ ) -> List[SkillSelectionCandidate]:
931
+ defects = {str(tag).strip().lower() for tag in defect_tags if str(tag).strip()}
932
+ if not defects:
933
+ defects = {"unexplored_gap"}
934
+
935
+ preset_bias_map = dict(getattr(preset, "skill_bias", {}) or {})
936
+ scored: List[SkillSelectionCandidate] = []
937
+ disabled_skills = _PROFILE_DISABLED_SKILLS.get(
938
+ str(profile_id or "").strip().lower(),
939
+ set(),
940
+ )
941
+ normalized_profile = str(profile_id or "").strip().lower()
942
+ allowed_skills = _PROFILE_ALLOWED_SKILLS.get(normalized_profile)
943
+ for skill in self.skills.values():
944
+ if skill.name in disabled_skills or (
945
+ allowed_skills is not None and skill.name not in allowed_skills
946
+ ):
947
+ continue
948
+ skill_defects = {d.lower() for d in skill.defects}
949
+ skill_defects.update(
950
+ profile_skill_defect_tags(profile_id, skill.name)
951
+ )
952
+ overlap = len(defects & skill_defects)
953
+ defect_score = overlap / max(1, len(defects))
954
+ prior_state = self.priors.get(skill.name, SkillUsagePrior())
955
+ prior = prior_state.prior
956
+ attempts = max(0, int(prior_state.attempts))
957
+ raw_preset_bias = preset_bias_map.get(skill.name, 0.0)
958
+ try:
959
+ preset_bias = max(0.0, min(1.0, float(raw_preset_bias)))
960
+ except (TypeError, ValueError):
961
+ preset_bias = 0.0
962
+ structure_fit, structure_reason = self._structure_fit(skill, structural_profile)
963
+ if structure_fit == 0.0:
964
+ continue
965
+ total = (
966
+ 0.60 * defect_score
967
+ + 0.20 * prior
968
+ + 0.20 * preset_bias
969
+ ) * structure_fit
970
+ scored.append(
971
+ SkillSelectionCandidate(
972
+ skill=skill,
973
+ defect_score=defect_score,
974
+ prior_score=prior,
975
+ preset_bias=preset_bias,
976
+ structure_fit=structure_fit,
977
+ structure_reason=structure_reason,
978
+ selection_total=total,
979
+ attempts=attempts,
980
+ )
981
+ )
982
+
983
+ scored.sort(key=lambda item: (-item.selection_total, -item.defect_score, item.skill.name))
984
+ max_children = max(1, int(max_children))
985
+ if max_children == 1:
986
+ picked = [scored[0]] if scored else []
987
+ else:
988
+ exploit_count = min(len(scored), max(0, max_children - 1))
989
+ picked = list(scored[:exploit_count])
990
+ remaining = scored[exploit_count:]
991
+ if remaining and len(picked) < max_children:
992
+ eligible = [entry for entry in remaining if float(entry.defect_score) > 0.0]
993
+ if eligible:
994
+ weights = [
995
+ float(entry.defect_score)
996
+ * (1.0 + 1.0 / math.sqrt(float(entry.attempts) + 1.0))
997
+ for entry in eligible
998
+ ]
999
+ picked.append(random.choices(eligible, weights=weights, k=1)[0])
1000
+ else:
1001
+ picked.append(random.choice(remaining))
1002
+ if not picked:
1003
+ return scored[: max(1, max_children)]
1004
+ return picked
1005
+
1006
+ def _scope_fit(self, scope_preference: str, scope_kind: str) -> float:
1007
+ if scope_preference == scope_kind:
1008
+ return 1.0
1009
+ if {scope_preference, scope_kind} <= {"existing_component", "existing_subsystem"}:
1010
+ return 0.85
1011
+ if {scope_preference, scope_kind} <= {"execution_path", "broad_architecture"}:
1012
+ return 0.75
1013
+ if scope_preference == "core_objective" and scope_kind in {
1014
+ "existing_subsystem",
1015
+ "execution_path",
1016
+ "broad_architecture",
1017
+ }:
1018
+ return 0.7
1019
+ return 0.0
1020
+
1021
+ def _structure_fit(
1022
+ self,
1023
+ skill: EditOperatorSkill,
1024
+ structural_profile: Dict[str, Any],
1025
+ ) -> tuple[float, str]:
1026
+ if (
1027
+ skill.requires_control_centered_parent
1028
+ and not bool(structural_profile["control_centered"])
1029
+ and str(structural_profile.get("profile_id") or "").strip().lower()
1030
+ == "computational_algorithmic"
1031
+ ):
1032
+ return 0.0, "requires_control_centered_parent"
1033
+
1034
+ scope_kind = str(structural_profile["scope_kind"])
1035
+ fit = self._scope_fit(skill.scope_preference, scope_kind)
1036
+ profile_id = str(structural_profile.get("profile_id") or "").strip().lower()
1037
+ if (
1038
+ fit == 0.0
1039
+ and profile_id != "computational_algorithmic"
1040
+ and skill.structural_mode == "feedback_loop"
1041
+ and scope_kind == "existing_subsystem"
1042
+ ):
1043
+ fit = 0.85
1044
+ if fit == 0.0:
1045
+ return 0.0, "scope_mismatch"
1046
+
1047
+ if skill.structural_mode == "path_branching" and scope_kind == "existing_component":
1048
+ return 0.0, "path_branching_out_of_scope"
1049
+ if skill.structural_mode == "feedback_loop" and bool(structural_profile["training_free_like"]):
1050
+ return 0.0, "feedback_loop_out_of_scope"
1051
+ if skill.structural_mode == "path_branching" and not bool(structural_profile["has_multi_path_shape"]):
1052
+ return fit * 0.75, "scope_only"
1053
+ return fit, "aligned"
1054
+
1055
+ def compile_plan(
1056
+ self,
1057
+ skill: EditOperatorSkill,
1058
+ parent_title: str,
1059
+ parent_components: Sequence[str],
1060
+ target_defects: Sequence[str],
1061
+ memory_refs: Sequence[str],
1062
+ *,
1063
+ profile_id: str = "",
1064
+ object_schema: Optional[Dict[str, Any]] = None,
1065
+ ) -> EditPlan:
1066
+ if isinstance(object_schema, dict):
1067
+ normalized_object_schema = dict(object_schema)
1068
+ elif hasattr(object_schema, "to_payload"):
1069
+ rendered_schema = object_schema.to_payload()
1070
+ normalized_object_schema = (
1071
+ dict(rendered_schema) if isinstance(rendered_schema, dict) else {}
1072
+ )
1073
+ else:
1074
+ normalized_object_schema = {}
1075
+ parsed_steps = [parse_blueprint_step(step) for step in skill.atomic_blueprint]
1076
+ parsed_steps = [step for step in parsed_steps if step is not None]
1077
+
1078
+ component_edits: List[ComponentEdit] = []
1079
+ validation = ValidationProtocol()
1080
+
1081
+ required_protocols = {
1082
+ token.lower().strip()
1083
+ for token in skill.required_protocols
1084
+ if token and token.strip()
1085
+ }
1086
+
1087
+ for step in parsed_steps:
1088
+ op = step["op"]
1089
+ if op == AtomicEditOp.ADD_PROTOCOL.value:
1090
+ protocols = step.get("protocols", [])
1091
+ for protocol in protocols:
1092
+ required_protocols.add(protocol.lower().strip())
1093
+ continue
1094
+ component_edits.append(
1095
+ ComponentEdit(
1096
+ op=AtomicEditOp(op),
1097
+ component=step.get("component", ""),
1098
+ target=step.get("target", ""),
1099
+ condition=step.get("condition", ""),
1100
+ details=step.get("details", ""),
1101
+ )
1102
+ )
1103
+
1104
+ if not required_protocols:
1105
+ required_protocols = {"ablation"}
1106
+
1107
+ for protocol in sorted(required_protocols):
1108
+ test_text = _default_protocol_text(protocol, skill.name, parent_title, target_defects)
1109
+ if protocol == "regression":
1110
+ validation.regression_tests.append(test_text)
1111
+ elif protocol == "ablation":
1112
+ validation.ablation_tests.append(test_text)
1113
+ else:
1114
+ validation.stress_tests.append(test_text)
1115
+ component_edits.append(
1116
+ ComponentEdit(
1117
+ op=AtomicEditOp.ADD_PROTOCOL,
1118
+ component=protocol,
1119
+ details=test_text,
1120
+ )
1121
+ )
1122
+
1123
+ objective_defect = next(iter(target_defects), "unspecified_defect")
1124
+ plan = EditPlan(
1125
+ skill_name=skill.name,
1126
+ objective=f"Use {skill.name} to address {objective_defect}",
1127
+ target_defects=[str(tag) for tag in target_defects] or skill.defects[:1] or ["unspecified_defect"],
1128
+ component_edits=component_edits,
1129
+ validation=validation,
1130
+ guardrails=list(skill.guardrails),
1131
+ memory_refs=[str(ref) for ref in memory_refs][:6],
1132
+ compile_notes=(
1133
+ f"Compiled from skill '{skill.name}' with blueprint ops={len(skill.atomic_blueprint)}; "
1134
+ f"source={skill.source_path or 'builtin'}"
1135
+ ),
1136
+ profile_id=str(profile_id or skill.rendered_profile_id or ""),
1137
+ profile_rendering_id=str(skill.profile_rendering_id or ""),
1138
+ preferred_operations=list(skill.preferred_operations),
1139
+ scientific_object_schema=normalized_object_schema,
1140
+ )
1141
+ return plan
1142
+
1143
+ def update_prior(
1144
+ self,
1145
+ skill_name: str,
1146
+ reward: float,
1147
+ feedback: str,
1148
+ failure_modes: Sequence[str],
1149
+ success_threshold: float = 0.6,
1150
+ ) -> SkillUsagePrior:
1151
+ prior = self.priors.setdefault(skill_name, SkillUsagePrior())
1152
+ prior.attempts += 1
1153
+ clipped_reward = max(0.0, min(1.0, reward))
1154
+ if clipped_reward >= max(0.0, min(1.0, success_threshold)):
1155
+ prior.successes += 1
1156
+ prior.reward_ema = 0.8 * prior.reward_ema + 0.2 * clipped_reward
1157
+ beta_mean = (prior.successes + 1.0) / (prior.attempts + 2.0)
1158
+ prior.prior = 0.55 * beta_mean + 0.45 * prior.reward_ema
1159
+
1160
+ for failure in failure_modes:
1161
+ text = str(failure).strip()
1162
+ if not text:
1163
+ continue
1164
+ rule = f"Avoid failure mode: {text}"
1165
+ if rule not in prior.rule_constraints:
1166
+ prior.rule_constraints.append(rule)
1167
+ prior.rule_constraints = prior.rule_constraints[:8]
1168
+ return prior
1169
+
1170
+
1171
+
1172
+ def _default_protocol_text(
1173
+ protocol: str,
1174
+ skill_name: str,
1175
+ parent_title: str,
1176
+ target_defects: Sequence[str],
1177
+ ) -> str:
1178
+ defect = next((str(tag) for tag in target_defects if str(tag).strip()), "target defect")
1179
+ if protocol == "regression":
1180
+ return (
1181
+ f"Run regression against parent '{parent_title}' and verify no degradation on core metrics while fixing {defect}."
1182
+ )
1183
+ if protocol == "ablation":
1184
+ return (
1185
+ f"Ablate the {skill_name} delta to isolate contribution and confirm defect-level lift on {defect}."
1186
+ )
1187
+ return (
1188
+ f"Stress test {skill_name} under worst-case conditions tied to {defect} and record failure boundaries."
1189
+ )
1190
+
1191
+
1192
+ def apply_edit_plan_to_components(
1193
+ components: Sequence[str],
1194
+ edit_plan: EditPlan,
1195
+ ) -> List[str]:
1196
+ ordered = [str(comp) for comp in components if str(comp).strip()]
1197
+ existing = list(ordered)
1198
+
1199
+ def _contains(name: str) -> bool:
1200
+ return any(name == item for item in existing)
1201
+
1202
+ for edit in edit_plan.component_edits:
1203
+ component = _coerce_component_name(edit.component)
1204
+ target = _coerce_component_name(edit.target)
1205
+ if edit.op == AtomicEditOp.ADD_COMPONENT:
1206
+ if component and not _contains(component):
1207
+ existing.append(component)
1208
+ elif edit.op == AtomicEditOp.REMOVE_COMPONENT:
1209
+ if component:
1210
+ existing = [item for item in existing if item != component]
1211
+ elif edit.op == AtomicEditOp.REPLACE_COMPONENT:
1212
+ if target:
1213
+ replaced = False
1214
+ for idx, item in enumerate(existing):
1215
+ if item == target:
1216
+ existing[idx] = component or target
1217
+ replaced = True
1218
+ break
1219
+ if not replaced and component and not _contains(component):
1220
+ existing.append(component)
1221
+ elif component and not _contains(component):
1222
+ existing.append(component)
1223
+ elif edit.op == AtomicEditOp.REWIRE:
1224
+ # REWIRE affects topology, not component inventory.
1225
+ continue
1226
+ elif edit.op == AtomicEditOp.ADD_PROTOCOL:
1227
+ # Protocols are first-class edits but not structural components.
1228
+ continue
1229
+
1230
+ return existing
1231
+
1232
+
1233
+ def log_message(
1234
+ logger: Any,
1235
+ log_sink: Optional[Any],
1236
+ level: str,
1237
+ message: str,
1238
+ *args: Any,
1239
+ ) -> None:
1240
+ log_fn = getattr(logger, level, logger.info)
1241
+ try:
1242
+ log_fn(message, *args)
1243
+ except Exception:
1244
+ logger.exception("MCTS logging failure for message: %s", message)
1245
+ if log_sink:
1246
+ try:
1247
+ formatted = message % args if args else message
1248
+ except Exception:
1249
+ formatted = f"{message} | args={args}"
1250
+ try:
1251
+ log_sink(level, formatted)
1252
+ except Exception as exc:
1253
+ logger.debug("MCTS log sink failed: %s", exc)
1254
+
1255
+
1256
+ def compute_protocol_score_from_plan(plan: Optional[Dict[str, Any]]) -> float:
1257
+ if not plan:
1258
+ return 0.0
1259
+ validation = plan.get("validation") if isinstance(plan.get("validation"), dict) else {}
1260
+ score = 0.0
1261
+ if validation.get("regression_tests"):
1262
+ score += 0.9
1263
+ if validation.get("ablation_tests"):
1264
+ score += 1.2
1265
+ if validation.get("stress_tests"):
1266
+ score += 0.9
1267
+ return min(3.0, score)
1268
+
1269
+
1270
+ def memory_bundle_log_payload(bundle: Any) -> Dict[str, Any]:
1271
+ def _snippet_payload(snippet: Any) -> Dict[str, Any]:
1272
+ return {
1273
+ "id": getattr(snippet, "identifier", ""),
1274
+ "title": getattr(snippet, "title", ""),
1275
+ "detail": getattr(snippet, "detail", ""),
1276
+ "tags": list(getattr(snippet, "tags", []) or []),
1277
+ }
1278
+
1279
+ return {
1280
+ "field_knowledge": [_snippet_payload(s) for s in getattr(bundle, "field_knowledge", []) or []],
1281
+ "anti_patterns": [_snippet_payload(s) for s in getattr(bundle, "anti_patterns", []) or []],
1282
+ "fix_recipes": [_snippet_payload(s) for s in getattr(bundle, "fix_recipes", []) or []],
1283
+ }
1284
+
1285
+
1286
+ def simulate_log_payload(evaluation: Any) -> Dict[str, Any]:
1287
+ payload = {**evaluation.to_dict(), "composite": evaluation.composite}
1288
+ return payload
1289
+
1290
+
1291
+ def _direction_prompt_context(
1292
+ mcts: Any,
1293
+ state: Any,
1294
+ *,
1295
+ object_schema: Optional[Dict[str, Any]] = None,
1296
+ ) -> Dict[str, str]:
1297
+ intervention = getattr(state, "scientific_intervention", {})
1298
+ intervention = intervention if isinstance(intervention, Mapping) else {}
1299
+ profile_payload = getattr(mcts, "scientific_intervention_profile", {})
1300
+ profile_payload = profile_payload if isinstance(profile_payload, Mapping) else {}
1301
+ profile_id = str(
1302
+ intervention.get("profile_id")
1303
+ or profile_payload.get("profile_id")
1304
+ or "generic_scientific"
1305
+ ).strip().lower()
1306
+ preset = getattr(mcts, "idea_taste_preset", None)
1307
+ direction_mode = str(
1308
+ intervention.get("direction_mode")
1309
+ or getattr(preset, "mode", None)
1310
+ or "default"
1311
+ ).strip()
1312
+ direction_summary = str(
1313
+ intervention.get("direction_summary")
1314
+ or getattr(preset, "summary", None)
1315
+ or "Use the default scientific search balance."
1316
+ ).strip()
1317
+ taste_guidance = str(
1318
+ getattr(preset, "instantiation_guidance", None)
1319
+ or "No special taste guidance."
1320
+ ).strip()
1321
+ seed_records = [
1322
+ dict(seed)
1323
+ for seed in getattr(mcts, "gap_hypothesis_seeds", []) or []
1324
+ if isinstance(seed, Mapping)
1325
+ ]
1326
+ hypothesis_contract = intervention.get("hypothesis_contract")
1327
+ hypothesis_contract = (
1328
+ hypothesis_contract if isinstance(hypothesis_contract, Mapping) else {}
1329
+ )
1330
+ raw_target_gap_ids = hypothesis_contract.get("target_gap_ids")
1331
+ if isinstance(raw_target_gap_ids, (list, tuple, set, frozenset)):
1332
+ target_gap_ids = [
1333
+ str(gap_id).strip()
1334
+ for gap_id in raw_target_gap_ids
1335
+ if str(gap_id).strip()
1336
+ ]
1337
+ elif str(raw_target_gap_ids or "").strip():
1338
+ target_gap_ids = [str(raw_target_gap_ids).strip()]
1339
+ else:
1340
+ target_gap_ids = [
1341
+ str(seed.get("gap_id") or "").strip()
1342
+ for seed in seed_records
1343
+ if str(seed.get("gap_id") or "").strip()
1344
+ ]
1345
+ selected_schema = object_schema
1346
+ if not isinstance(selected_schema, Mapping):
1347
+ selected_schema = intervention.get("scientific_object_schema")
1348
+ if not isinstance(selected_schema, Mapping):
1349
+ selected_schema = profile_payload.get("scientific_object_schema")
1350
+ return {
1351
+ "direction_mode": direction_mode,
1352
+ "direction_summary": direction_summary,
1353
+ "taste_guidance": taste_guidance,
1354
+ "seed_id": str(getattr(mcts, "seed_id", "") or "None supplied"),
1355
+ "route_id": str(getattr(mcts, "route_id", "") or "None supplied"),
1356
+ "route_policy": pretty_json(getattr(mcts, "route_policy", {}) or {}),
1357
+ "target_gap_ids": ", ".join(dict.fromkeys(target_gap_ids)) or "None supplied",
1358
+ "profile_id": profile_id,
1359
+ "profile_native_object_schema": pretty_json(dict(selected_schema or {})),
1360
+ "gap_seed_context": str(getattr(mcts, "gap_seed_context", "") or "No gap seeds supplied."),
1361
+ }
1362
+
1363
+
1364
+ def _data_anchored_contract_prompt(state: Any) -> str:
1365
+ intervention = getattr(state, "scientific_intervention", {})
1366
+ intervention = intervention if isinstance(intervention, Mapping) else {}
1367
+ contract = intervention.get("data_anchored_contract")
1368
+ if not isinstance(contract, Mapping) or not contract.get("data_anchor_refs"):
1369
+ return ""
1370
+ return (
1371
+ "\n\n== Bounded supplied-data requirement ==\n"
1372
+ + pretty_json(dict(contract))
1373
+ + "\nThe data anchors are local observations, not paper citations. Keep their claim scope "
1374
+ "dataset-local and do not state that they prove a mechanism or a first discovery. "
1375
+ "The coverage pass must explicitly distinguish candidate_mechanism, "
1376
+ "alternative_explanation, and measurement_artifact branches. A measurement-at-risk "
1377
+ "anchor may support only measurement validity or workflow reasoning."
1378
+ )
1379
+
1380
+
1381
+ def _contract_from_instantiation(instantiated: Mapping[str, Any]) -> Dict[str, Any]:
1382
+ explicit_contract = instantiated.get("scientific_contract")
1383
+ if isinstance(explicit_contract, Mapping):
1384
+ contract = dict(explicit_contract)
1385
+ else:
1386
+ contract = {}
1387
+ for field_name in HYPOTHESIS_CONTRACT_FIELDS:
1388
+ value = instantiated.get(field_name)
1389
+ if value not in (None, "", [], {}):
1390
+ contract.setdefault(field_name, value)
1391
+ return contract
1392
+
1393
+
1394
+ def _nonempty_contract_value(value: Any) -> bool:
1395
+ return value not in (None, "", [], {})
1396
+
1397
+
1398
+ def _parent_hypothesis_contract(parent_state: Any) -> Dict[str, Any]:
1399
+ """Return the parent scientific contract without exposing mutable state."""
1400
+
1401
+ intervention = getattr(parent_state, "scientific_intervention", {}) or {}
1402
+ if not isinstance(intervention, Mapping):
1403
+ return {}
1404
+ parent_contract = intervention.get("hypothesis_contract")
1405
+ merged = (
1406
+ {
1407
+ field_name: deepcopy(value)
1408
+ for field_name, value in parent_contract.items()
1409
+ if field_name in HYPOTHESIS_CONTRACT_FIELDS
1410
+ and _nonempty_contract_value(value)
1411
+ }
1412
+ if isinstance(parent_contract, Mapping)
1413
+ else {}
1414
+ )
1415
+ for field_name in HYPOTHESIS_CONTRACT_FIELDS:
1416
+ value = intervention.get(field_name)
1417
+ if _nonempty_contract_value(value):
1418
+ merged.setdefault(field_name, deepcopy(value))
1419
+ return merged
1420
+
1421
+
1422
+ def merge_hypothesis_contract(
1423
+ parent_contract: Mapping[str, Any],
1424
+ instantiated_contract: Mapping[str, Any],
1425
+ ) -> Dict[str, Any]:
1426
+ """Merge a child instantiation onto its parent without treating blanks as clears."""
1427
+
1428
+ merged = {
1429
+ field_name: deepcopy(value)
1430
+ for field_name, value in parent_contract.items()
1431
+ if field_name in HYPOTHESIS_CONTRACT_FIELDS
1432
+ and _nonempty_contract_value(value)
1433
+ }
1434
+ for field_name, value in instantiated_contract.items():
1435
+ if field_name not in HYPOTHESIS_CONTRACT_FIELDS:
1436
+ continue
1437
+ if _nonempty_contract_value(value):
1438
+ merged[field_name] = deepcopy(value)
1439
+ return merged
1440
+
1441
+
1442
+ def _route_contract_incomplete_fields(
1443
+ route_id: str,
1444
+ parent_contract: Mapping[str, Any],
1445
+ instantiated_contract: Mapping[str, Any],
1446
+ ) -> List[str]:
1447
+ """Identify route-defining fields that a child omitted and would only inherit."""
1448
+
1449
+ required_fields = _ROUTE_REQUIRED_CONTRACT_FIELDS.get(route_id, ())
1450
+ if route_id == "mechanism_replacement":
1451
+ child_has_mechanism = any(
1452
+ _nonempty_contract_value(instantiated_contract.get(field_name))
1453
+ for field_name in required_fields
1454
+ )
1455
+ parent_has_mechanism = any(
1456
+ _nonempty_contract_value(parent_contract.get(field_name))
1457
+ for field_name in required_fields
1458
+ )
1459
+ return ["mechanism_or_relation"] if parent_has_mechanism and not child_has_mechanism else []
1460
+ incomplete: List[str] = []
1461
+ for field_name in required_fields:
1462
+ child_value = instantiated_contract.get(field_name)
1463
+ if _nonempty_contract_value(child_value):
1464
+ continue
1465
+ if _nonempty_contract_value(parent_contract.get(field_name)):
1466
+ incomplete.append(field_name)
1467
+ return incomplete
1468
+
1469
+
1470
+ def _contract_value_signature(value: Any) -> str:
1471
+ if isinstance(value, (Mapping, list, tuple, set, frozenset)):
1472
+ try:
1473
+ return json.dumps(value, sort_keys=True, ensure_ascii=False, default=str)
1474
+ except (TypeError, ValueError):
1475
+ pass
1476
+ return re.sub(r"\s+", " ", str(value or "")).strip().casefold()
1477
+
1478
+
1479
+ def _route_contract_noop_fields(
1480
+ route_id: str,
1481
+ parent_contract: Mapping[str, Any],
1482
+ instantiated_contract: Mapping[str, Any],
1483
+ ) -> List[str]:
1484
+ """Identify explicit route fields that only restate the parent contract."""
1485
+
1486
+ required_fields = _ROUTE_REQUIRED_CONTRACT_FIELDS.get(route_id, ())
1487
+ if route_id == "mechanism_replacement":
1488
+ child_values = [
1489
+ (field_name, instantiated_contract.get(field_name))
1490
+ for field_name in required_fields
1491
+ if _nonempty_contract_value(instantiated_contract.get(field_name))
1492
+ ]
1493
+ if child_values and all(
1494
+ _nonempty_contract_value(parent_contract.get(field_name))
1495
+ and _contract_value_signature(child_value)
1496
+ == _contract_value_signature(parent_contract.get(field_name))
1497
+ for field_name, child_value in child_values
1498
+ ):
1499
+ return ["mechanism_or_relation"]
1500
+ return []
1501
+ noops: List[str] = []
1502
+ for field_name in required_fields:
1503
+ child_value = instantiated_contract.get(field_name)
1504
+ parent_value = parent_contract.get(field_name)
1505
+ if (
1506
+ _nonempty_contract_value(child_value)
1507
+ and _nonempty_contract_value(parent_value)
1508
+ and _contract_value_signature(child_value)
1509
+ == _contract_value_signature(parent_value)
1510
+ ):
1511
+ noops.append(field_name)
1512
+ return noops
1513
+
1514
+
1515
+ def _route_parent_values(
1516
+ route_id: str,
1517
+ parent_contract: Mapping[str, Any],
1518
+ ) -> Dict[str, Any]:
1519
+ return {
1520
+ field_name: deepcopy(parent_contract[field_name])
1521
+ for field_name in _ROUTE_REQUIRED_CONTRACT_FIELDS.get(route_id, ())
1522
+ if _nonempty_contract_value(parent_contract.get(field_name))
1523
+ }
1524
+
1525
+
1526
+ def materialize_child_state(
1527
+ mcts: Any,
1528
+ parent_state: Any,
1529
+ plan: Any,
1530
+ instantiated: Optional[Dict[str, Any]] = None,
1531
+ selection_metadata: Optional[Dict[str, Any]] = None,
1532
+ *,
1533
+ idea_state_cls: Any,
1534
+ ) -> Any:
1535
+ new_components = apply_edit_plan_to_components(parent_state.components, plan)
1536
+ component_explanations = build_child_component_explanations(
1537
+ parent_state,
1538
+ new_components,
1539
+ plan,
1540
+ instantiated,
1541
+ )
1542
+
1543
+ inst = instantiated or {}
1544
+ title = inst.get("title") or f"{parent_state.title} | {plan.skill_name.replace('-', ' ').title()}"
1545
+ abstract = inst.get("abstract") or (
1546
+ f"Component-level macro action '{plan.skill_name}' targets defects "
1547
+ f"{', '.join(plan.target_defects)} via {len(plan.component_edits)} atomic edits."
1548
+ )
1549
+ core = inst.get("core_contribution") or plan.objective
1550
+ method = inst.get("method") or plan_to_method_text(plan)
1551
+ risks = inst.get("risks") or f"Guardrails: {'; '.join(plan.guardrails)}"
1552
+ rationale = inst.get("rationale") or plan.compile_notes
1553
+ tags = _dedupe_keep_order_strings(list(parent_state.tags) + [plan.skill_name] + list(plan.target_defects))
1554
+ selection_metadata = selection_metadata or {}
1555
+ skill_metrics = {
1556
+ "idea_taste_mode": str(selection_metadata.get("idea_taste_mode") or "none"),
1557
+ "skill_prior_before": mcts._skill_prior_for_prompt(plan.skill_name),
1558
+ "guardrails": plan.guardrails,
1559
+ "constraints": ANTI_PATTERN_CONSTRAINTS,
1560
+ "llm_instantiated": bool(inst),
1561
+ }
1562
+ skill_selection_breakdown = selection_metadata.get("skill_selection_breakdown")
1563
+ if isinstance(skill_selection_breakdown, dict) and skill_selection_breakdown:
1564
+ skill_metrics["skill_selection_breakdown"] = skill_selection_breakdown
1565
+ scientific_intervention = dict(
1566
+ getattr(parent_state, "scientific_intervention", {}) or {}
1567
+ )
1568
+ coverage_branch = selection_metadata.get("data_anchored_coverage_branch")
1569
+ if isinstance(coverage_branch, Mapping):
1570
+ coverage_branch = dict(coverage_branch)
1571
+ branch_id = str(coverage_branch.get("branch_id") or "").strip()
1572
+ if branch_id:
1573
+ tags = _dedupe_keep_order_strings(
1574
+ [*tags, f"data_coverage:{branch_id}"]
1575
+ )
1576
+ skill_metrics["data_anchored_coverage_branch"] = coverage_branch
1577
+ scientific_intervention["data_anchored_coverage_branch"] = coverage_branch
1578
+ inherited_data_contract = scientific_intervention.get("data_anchored_contract")
1579
+ if isinstance(inherited_data_contract, Mapping):
1580
+ inherited_data_contract = dict(inherited_data_contract)
1581
+ inherited_data_contract["active_coverage_branch"] = coverage_branch
1582
+ scientific_intervention["data_anchored_contract"] = inherited_data_contract
1583
+ parent_contract = _parent_hypothesis_contract(parent_state)
1584
+ instantiated_contract = _contract_from_instantiation(inst)
1585
+ scientific_contract = merge_hypothesis_contract(
1586
+ parent_contract,
1587
+ instantiated_contract,
1588
+ )
1589
+ if scientific_contract:
1590
+ scientific_intervention["hypothesis_contract"] = scientific_contract
1591
+ direction_mode = str(
1592
+ scientific_contract.get("direction_mode")
1593
+ or selection_metadata.get("idea_taste_mode")
1594
+ or getattr(getattr(mcts, "idea_taste_preset", None), "mode", None)
1595
+ or "default"
1596
+ ).strip()
1597
+ if direction_mode:
1598
+ scientific_intervention["direction_mode"] = direction_mode
1599
+ direction_summary = str(
1600
+ scientific_contract.get("direction_summary")
1601
+ or getattr(getattr(mcts, "idea_taste_preset", None), "summary", None)
1602
+ or ""
1603
+ ).strip()
1604
+ if direction_summary:
1605
+ scientific_intervention["direction_summary"] = direction_summary
1606
+ route_id = str(getattr(mcts, "route_id", "") or "").strip()
1607
+ incomplete_route_fields = _route_contract_incomplete_fields(
1608
+ route_id,
1609
+ parent_contract,
1610
+ instantiated_contract,
1611
+ )
1612
+ if incomplete_route_fields:
1613
+ scientific_intervention["route_contract_incomplete_fields"] = incomplete_route_fields
1614
+ else:
1615
+ scientific_intervention.pop("route_contract_incomplete_fields", None)
1616
+ noop_route_fields = _route_contract_noop_fields(
1617
+ route_id,
1618
+ parent_contract,
1619
+ instantiated_contract,
1620
+ )
1621
+ if noop_route_fields:
1622
+ scientific_intervention["route_contract_noop_fields"] = noop_route_fields
1623
+ else:
1624
+ scientific_intervention.pop("route_contract_noop_fields", None)
1625
+ if incomplete_route_fields or noop_route_fields:
1626
+ scientific_intervention["route_contract_parent_values"] = _route_parent_values(
1627
+ route_id,
1628
+ parent_contract,
1629
+ )
1630
+ else:
1631
+ scientific_intervention.pop("route_contract_parent_values", None)
1632
+ return idea_state_cls(
1633
+ title=title,
1634
+ abstract=abstract,
1635
+ core_contribution=core,
1636
+ method=method,
1637
+ risks=risks,
1638
+ tags=tags,
1639
+ operator=plan.skill_name,
1640
+ target_defects=plan.target_defects,
1641
+ rationale=rationale,
1642
+ memory_refs=plan.memory_refs,
1643
+ components=new_components,
1644
+ component_explanations=component_explanations,
1645
+ root_domains=list(getattr(parent_state, "root_domains", []) or []),
1646
+ discipline_resolution=dict(
1647
+ getattr(parent_state, "discipline_resolution", {}) or {}
1648
+ ),
1649
+ scientific_intervention=scientific_intervention,
1650
+ paper_graph_context=(
1651
+ str(inst.get("_paper_graph_context") or "")
1652
+ if isinstance(inst, dict) and str(inst.get("_paper_graph_context") or "").strip()
1653
+ else parent_state.paper_graph_context
1654
+ ),
1655
+ edit_plan=plan.to_dict(),
1656
+ skill_metrics=skill_metrics,
1657
+ )
1658
+
1659
+ def apply_instantiated_mapping_to_plan(
1660
+ plan: Any,
1661
+ instantiated: Optional[Dict[str, Any]],
1662
+ *,
1663
+ rewrite_components: bool = True,
1664
+ ) -> None:
1665
+ if not instantiated:
1666
+ return
1667
+ edit_reasons = instantiated.get("edit_reasons")
1668
+ if isinstance(edit_reasons, list):
1669
+ for reason_idx, edit in enumerate(plan.component_edits):
1670
+ if reason_idx < len(edit_reasons) and isinstance(edit_reasons[reason_idx], str):
1671
+ edit.reason = edit_reasons[reason_idx]
1672
+ if not rewrite_components or not isinstance(instantiated.get("component_mapping"), dict):
1673
+ return
1674
+ mapping = _normalize_component_mapping(instantiated.get("component_mapping"))
1675
+
1676
+ for edit in plan.component_edits:
1677
+ component_name = _coerce_component_name(edit.component)
1678
+ target_name = _coerce_component_name(edit.target)
1679
+ edit.component = mapping.get(component_name, component_name)
1680
+ edit.target = mapping.get(target_name, target_name) if target_name else ""
1681
+ if edit.op == AtomicEditOp.REWIRE:
1682
+ edit.details = f"Rewire {edit.component} -> {edit.target}"
1683
+ elif edit.op == AtomicEditOp.REPLACE_COMPONENT:
1684
+ edit.details = f"Replace {edit.target} with {edit.component}"
1685
+ elif edit.op == AtomicEditOp.ADD_COMPONENT:
1686
+ edit.details = f"ADD_COMPONENT on {edit.component}"
1687
+
1688
+
1689
+ def instantiate_compiled_plan_for_node(
1690
+ mcts: Any,
1691
+ plan: Any,
1692
+ parent_state: Any,
1693
+ bundle: Any,
1694
+ *,
1695
+ prompt_template: str,
1696
+ plan_name: str,
1697
+ plan_references: str = "None.",
1698
+ taste_guidance: str = "No special taste guidance.",
1699
+ root_domains_text: str = "Unspecified",
1700
+ additional_retrieval_context: str = "",
1701
+ stage: str = "mcts_expand",
1702
+ ) -> Optional[Dict[str, Any]]:
1703
+ component_edits_text = plan_to_method_text(plan)
1704
+ validation_text = plan_to_experiment_text(plan)
1705
+ schema = getattr(plan, "scientific_object_schema", {}) or {}
1706
+ native_guidance = list(plan.guardrails)
1707
+ if schema:
1708
+ native_guidance.extend(
1709
+ [
1710
+ "Profile rendering: " + str(getattr(plan, "profile_rendering_id", "") or "native"),
1711
+ "Allowed scientific object types: " + ", ".join(str(item) for item in schema.get("object_types", [])),
1712
+ "Preferred native operations: " + ", ".join(str(item) for item in schema.get("allowed_operations", [])),
1713
+ "Keep target_object, claim_delta, mechanism_delta, evidence_obligation, boundary_condition, and measurement_or_observation explicit.",
1714
+ ]
1715
+ )
1716
+
1717
+ prompt = prompt_template.format(
1718
+ topic=mcts.topic,
1719
+ root_domains=root_domains_text,
1720
+ scientific_intervention_profile=format_scientific_intervention_profile_for_prompt(
1721
+ getattr(parent_state, "scientific_intervention", None)
1722
+ ),
1723
+ refinement_scope=mcts.refinement_scope or "None",
1724
+ taste_guidance=taste_guidance,
1725
+ mature_idea=mcts.mature_idea or "None",
1726
+ parent_summary=parent_state.describe(),
1727
+ parent_components=", ".join(parent_state.components) if parent_state.components else "None",
1728
+ paper_context=mcts.paper_context,
1729
+ memory_bundle=bundle.to_prompt_block(),
1730
+ skill_references=plan_references,
1731
+ additional_retrieval_context=additional_retrieval_context,
1732
+ skill_name=plan_name,
1733
+ plan_objective=plan.objective,
1734
+ target_defects=", ".join(plan.target_defects),
1735
+ component_edits=component_edits_text,
1736
+ validation_protocols=validation_text,
1737
+ guardrails="; ".join(native_guidance) if native_guidance else "None",
1738
+ )
1739
+ direction_context = _direction_prompt_context(
1740
+ mcts,
1741
+ parent_state,
1742
+ object_schema=schema if isinstance(schema, dict) else None,
1743
+ )
1744
+ prompt += (
1745
+ "\n\n== Direction and Gap-to-Hypothesis Contract ==\n"
1746
+ f"Direction mode: {direction_context['direction_mode']}\n"
1747
+ f"Direction summary: {direction_context['direction_summary']}\n"
1748
+ f"Taste guidance: {direction_context['taste_guidance']}\n"
1749
+ f"Mature idea seed ID: {direction_context['seed_id']}\n"
1750
+ f"Search route ID: {direction_context['route_id']}\n"
1751
+ f"Search route policy: {direction_context['route_policy']}\n"
1752
+ f"Target gap IDs: {direction_context['target_gap_ids']}\n"
1753
+ f"Resolved profile ID: {direction_context['profile_id']}\n"
1754
+ "Profile-native scientific object schema:\n"
1755
+ f"{direction_context['profile_native_object_schema']}\n"
1756
+ "Gap-to-hypothesis seeds:\n"
1757
+ f"{direction_context['gap_seed_context']}\n"
1758
+ "\n== Required Directional Scientific Fields ==\n"
1759
+ "Return these fields in the JSON payload, either at the top level or mirrored inside scientific_contract: "
1760
+ "direction_mode, direction_summary, central_hypothesis, scientific_object, mechanism_or_relation, "
1761
+ "intervention_or_transformation, expected_mechanism, discriminating_observation, "
1762
+ "boundary_or_failure_condition, claim_scope, assumptions, target_gap_ids, gap_alignment, "
1763
+ "evidence_requirement, and evidence_basis.\n"
1764
+ "evidence_requirement must name the observation, contrast, proof, measurement, or boundary needed "
1765
+ "to distinguish the claim; it must not contain an experiment design. Do not return predicted results, "
1766
+ "sample sizes, statistical tests, instrument configurations, ablation plans, or failure-repair plans."
1767
+ )
1768
+ prompt += _data_anchored_contract_prompt(parent_state)
1769
+ survey_handoff = getattr(mcts, "survey_idea_handoff", {})
1770
+ if isinstance(survey_handoff, dict) and survey_handoff:
1771
+ prompt += "\n\n== Verified Survey -> Idea handoff ==\n" + pretty_json(survey_handoff)
1772
+ try:
1773
+ response = mcts.chat_fn(
1774
+ prompt,
1775
+ model=mcts.config.generation_model,
1776
+ stage=stage,
1777
+ temperature=mcts.config.generation_temperature,
1778
+ max_output_tokens=mcts.config.generation_max_tokens,
1779
+ )
1780
+ payload = parse_json_response(response)
1781
+ if isinstance(payload, list):
1782
+ payload = payload[0]
1783
+ if not isinstance(payload, dict):
1784
+ return None
1785
+ contract = payload.get("scientific_contract")
1786
+ if not isinstance(contract, dict):
1787
+ contract = {
1788
+ key: payload.get(key)
1789
+ for key in (
1790
+ "contribution_mode",
1791
+ "scientific_object",
1792
+ "central_hypothesis",
1793
+ "intervention_or_transformation",
1794
+ "expected_mechanism",
1795
+ "discriminating_observation",
1796
+ "boundary_or_failure_condition",
1797
+ "evidence_requirement",
1798
+ )
1799
+ if payload.get(key) not in (None, "", [], {})
1800
+ }
1801
+ if contract:
1802
+ payload["scientific_contract"] = contract
1803
+ payload["component_mapping"] = _filter_component_mapping_to_plan_keys(
1804
+ payload.get("component_mapping"),
1805
+ plan,
1806
+ )
1807
+ payload["component_role_explanations"] = normalize_component_explanations(
1808
+ list(payload["component_mapping"].values()),
1809
+ payload.get("component_role_explanations"),
1810
+ )
1811
+ return payload
1812
+ except Exception as exc:
1813
+ log_message(
1814
+ mcts.logger,
1815
+ mcts.log_sink,
1816
+ "warning",
1817
+ "⚠️ Plan instantiation failed for %s: %s",
1818
+ plan_name,
1819
+ exc,
1820
+ )
1821
+ return None
1822
+
1823
+
1824
+ def instantiate_skill_plan_for_node(
1825
+ mcts: Any,
1826
+ plan: Any,
1827
+ parent_state: Any,
1828
+ bundle: Any,
1829
+ *,
1830
+ prompt_template: str,
1831
+ root_domains_text: str = "Unspecified",
1832
+ additional_retrieval_context: str = "",
1833
+ ) -> Optional[Dict[str, Any]]:
1834
+ return instantiate_compiled_plan_for_node(
1835
+ mcts,
1836
+ plan,
1837
+ parent_state,
1838
+ bundle,
1839
+ prompt_template=prompt_template,
1840
+ plan_name=plan.skill_name,
1841
+ plan_references=mcts.skill_catalog.render_references_for_prompt(plan.skill_name),
1842
+ taste_guidance=(
1843
+ getattr(getattr(mcts, "idea_taste_preset", None), "instantiation_guidance", None)
1844
+ or "No special taste guidance."
1845
+ ),
1846
+ root_domains_text=root_domains_text,
1847
+ additional_retrieval_context=additional_retrieval_context,
1848
+ stage="mcts_expand",
1849
+ )
1850
+
1851
+
1852
+ def build_symbolic_eval_hints(mcts: Any, node: Any) -> str:
1853
+ if not getattr(mcts, "enable_symbolic_memory", True):
1854
+ return "No symbolic memory hints available."
1855
+ component_families = extract_component_families(node.state.components, node.state.method)
1856
+ if not component_families:
1857
+ return "No symbolic memory hints available."
1858
+
1859
+ retrieved_records: List[Tuple[str, str, float, Any]] = []
1860
+
1861
+ for cf in component_families:
1862
+ component_name = str(cf.get("component", "") or "").strip()
1863
+ family = cf.get("family", "")
1864
+ if not component_name and not family:
1865
+ continue
1866
+
1867
+ records = mcts.symbolic_memory.retrieve_hierarchical(
1868
+ target_component=component_name,
1869
+ target_family=family,
1870
+ limit=2,
1871
+ threshold=0.2,
1872
+ agent_id="idea_agent",
1873
+ query_context=str(getattr(node.state, "abstract", "") or "").strip(),
1874
+ )
1875
+ if not records:
1876
+ continue
1877
+
1878
+ for score, rec in records:
1879
+ retrieved_records.append((component_name, str(family or ""), float(score), rec))
1880
+
1881
+ if not retrieved_records:
1882
+ return "No symbolic memory hints available."
1883
+
1884
+ deduped_records: Dict[Tuple[str, ...], Tuple[str, str, float, Any]] = {}
1885
+ for query_component, query_family, score, rec in retrieved_records:
1886
+ dedupe_key = (
1887
+ str(getattr(rec, "component", "") or "").strip().lower(),
1888
+ str(getattr(rec, "component_family", "") or "").strip().lower(),
1889
+ str(getattr(rec, "result", "") or "").strip().lower(),
1890
+ str(getattr(rec, "metric", "") or "").strip().lower(),
1891
+ str(getattr(rec, "value", "") or "").strip().lower(),
1892
+ str(getattr(rec, "analysis", "") or "").strip().lower(),
1893
+ )
1894
+ existing = deduped_records.get(dedupe_key)
1895
+ if existing is None or score > existing[2]:
1896
+ deduped_records[dedupe_key] = (query_component, query_family, score, rec)
1897
+
1898
+ if not deduped_records:
1899
+ return "No symbolic memory hints available."
1900
+
1901
+ hints_parts: List[str] = []
1902
+ for query_component, query_family, score, rec in sorted(
1903
+ deduped_records.values(),
1904
+ key=lambda item: item[2],
1905
+ reverse=True,
1906
+ ):
1907
+ component = clip_text(getattr(rec, "component", "")) or "unknown_component"
1908
+ component_family = clip_text(getattr(rec, "component_family", "")) or "unknown_family"
1909
+ result = clip_text(getattr(rec, "result", "")) or "inconclusive"
1910
+ metric = clip_text(getattr(rec, "metric", "")) or "unspecified_metric"
1911
+ value = clip_text(getattr(rec, "value", "")) or "unspecified_value"
1912
+ confidence = float(getattr(rec, "confidence", 0.0))
1913
+ analysis = clip_text(getattr(rec, "analysis", ""))
1914
+ line = (
1915
+ f" - query_component={query_component or 'unknown_component'}"
1916
+ f" | query_family={query_family or 'unknown_family'}"
1917
+ f" | matched_family={component_family}"
1918
+ f" | component={component}"
1919
+ f" | result={result} | metric={metric} | value={value}"
1920
+ f" (conf={confidence:.2f}, score={score:.3f})"
1921
+ )
1922
+ if analysis:
1923
+ line += f" analysis: {analysis}"
1924
+ hints_parts.append(line)
1925
+
1926
+ header = (
1927
+ "Historical ablation records for the component families in this idea "
1928
+ "(positive result means removing the component helped, "
1929
+ "negative result means removing the component hurt):"
1930
+ )
1931
+ return header + "\n" + "\n".join(hints_parts)
1932
+
1933
+
1934
+ def update_skill_prior_from_evaluation(mcts: Any, node: Any, evaluation: Any) -> None:
1935
+ skill_name = node.state.operator
1936
+ if not skill_name or skill_name == "seed":
1937
+ return
1938
+
1939
+ normalized_reward = max(0.0, min(1.0, evaluation.composite / 5.0))
1940
+ prior = mcts.skill_catalog.update_prior(
1941
+ skill_name=skill_name,
1942
+ reward=normalized_reward,
1943
+ feedback=evaluation.feedback,
1944
+ failure_modes=evaluation.failure_modes,
1945
+ success_threshold=mcts.config.skill_prior_success_threshold,
1946
+ )
1947
+ node.state.skill_metrics["skill_prior_after"] = prior.to_dict()
1948
+
1949
+
1950
+ def extract_mature_idea_components_via_llm(
1951
+ mcts: Any,
1952
+ mature_idea: str,
1953
+ topic: str,
1954
+ *,
1955
+ prompt_template: str,
1956
+ max_components: int,
1957
+ prior_components: Optional[Sequence[str]] = None,
1958
+ prior_component_explanations: Optional[Any] = None,
1959
+ component_decisions: Optional[Sequence[Dict[str, Any]]] = None,
1960
+ ) -> Tuple[List[str], Dict[str, str]]:
1961
+ normalized_prior_components = [
1962
+ str(component).strip()
1963
+ for component in (prior_components or [])
1964
+ if str(component).strip()
1965
+ ]
1966
+ normalized_prior_explanations = normalize_component_explanations(
1967
+ normalized_prior_components,
1968
+ prior_component_explanations,
1969
+ )
1970
+ normalized_component_decisions = [
1971
+ decision for decision in (component_decisions or []) if isinstance(decision, dict)
1972
+ ]
1973
+ prompt = prompt_template.format(
1974
+ mature_idea=mature_idea,
1975
+ topic=topic,
1976
+ scientific_intervention_profile=format_scientific_intervention_profile_for_prompt(
1977
+ getattr(mcts, "scientific_intervention_profile", None)
1978
+ ),
1979
+ prior_components=pretty_json(
1980
+ component_inventory_payload(
1981
+ normalized_prior_components,
1982
+ normalized_prior_explanations,
1983
+ ),
1984
+ )
1985
+ if normalized_prior_components
1986
+ else "[]",
1987
+ component_decisions=pretty_json(normalized_component_decisions)
1988
+ if normalized_component_decisions
1989
+ else "[]",
1990
+ )
1991
+ try:
1992
+ response = mcts.chat_fn(
1993
+ prompt,
1994
+ model=mcts.config.generation_model,
1995
+ temperature=0.3,
1996
+ max_output_tokens=mcts.config.generation_max_tokens,
1997
+ )
1998
+ payload = parse_json_response(response)
1999
+ if isinstance(payload, list):
2000
+ payload = payload[0]
2001
+ components, explanations = parse_component_bundle_payload(
2002
+ payload,
2003
+ max_components=max_components,
2004
+ )
2005
+ if components:
2006
+ return components, explanations
2007
+ except Exception as exc:
2008
+ log_message(
2009
+ mcts.logger,
2010
+ mcts.log_sink,
2011
+ "warning",
2012
+ "⚠️ Component extraction from mature idea failed: %s",
2013
+ exc,
2014
+ )
2015
+ return [], {}
2016
+
2017
+
2018
+ def select_leaf_for_rollout(mcts: Any, node: Any) -> Tuple[Any, List[Any]]:
2019
+ current = node
2020
+ path = [node]
2021
+ while current.children and current.expanded:
2022
+ parent = current
2023
+ current = max(
2024
+ parent.children,
2025
+ key=lambda child: child.uct_value(
2026
+ parent_visits=parent.visits or 1,
2027
+ exploration_constant=mcts.config.exploration_constant,
2028
+ ),
2029
+ )
2030
+ path.append(current)
2031
+ return current, path
2032
+
2033
+
2034
+ def expand_node_with_skills(
2035
+ mcts: Any,
2036
+ node: Any,
2037
+ path: List[Any],
2038
+ *,
2039
+ min_components: int,
2040
+ max_components: int,
2041
+ ) -> Tuple[Optional[Any], List[Any]]:
2042
+ bundle = MemoryBundle()
2043
+ if getattr(mcts, "enable_vector_memory", True):
2044
+ bundle = mcts.memory_accessor.retrieve_bundle(
2045
+ query=(
2046
+ f"{mcts.topic}\n{node.state.title}\n"
2047
+ f"{node.state.core_contribution}\n"
2048
+ f"defects={','.join(node.state.target_defects)}"
2049
+ )
2050
+ )
2051
+ log_message(
2052
+ mcts.logger,
2053
+ mcts.log_sink,
2054
+ "info",
2055
+ "[MCTS] Expand: vector_memory\n%s",
2056
+ pretty_json(mcts._memory_bundle_log_payload(bundle)),
2057
+ )
2058
+ structural_profile = build_structural_profile(
2059
+ node.state,
2060
+ refinement_scope=mcts.refinement_scope,
2061
+ mature_idea=mcts.mature_idea,
2062
+ defect_tags=node.state.target_defects,
2063
+ profile_id=str(
2064
+ (getattr(node.state, "scientific_intervention", {}) or {}).get("profile_id")
2065
+ or "generic_scientific"
2066
+ ).strip().lower(),
2067
+ )
2068
+ log_message(
2069
+ mcts.logger,
2070
+ mcts.log_sink,
2071
+ "info",
2072
+ "[MCTS] Expand: structural_profile\n%s",
2073
+ pretty_json(structural_profile),
2074
+ )
2075
+ scientific_intervention = getattr(node.state, "scientific_intervention", {}) or {}
2076
+ profile_id = str(
2077
+ scientific_intervention.get("profile_id") or "generic_scientific"
2078
+ ).strip().lower()
2079
+ object_schema = scientific_intervention.get("scientific_object_schema")
2080
+ if not isinstance(object_schema, dict):
2081
+ schema_spec = get_scientific_object_schema(profile_id)
2082
+ object_schema = schema_spec.to_payload() if schema_spec is not None else {}
2083
+ selected_skill_candidates = mcts.skill_catalog.select_skills(
2084
+ defect_tags=node.state.target_defects,
2085
+ max_children=mcts.config.branching_factor,
2086
+ preset=getattr(mcts, "idea_taste_preset", None),
2087
+ structural_profile=structural_profile,
2088
+ profile_id=profile_id,
2089
+ )
2090
+ skill_candidates = list(selected_skill_candidates)
2091
+ data_contract = scientific_intervention.get("data_anchored_contract")
2092
+ data_contract = data_contract if isinstance(data_contract, Mapping) else {}
2093
+ coverage_assignment = data_contract.get("coverage_assignment")
2094
+ coverage_assignment = (
2095
+ coverage_assignment if isinstance(coverage_assignment, Mapping) else {}
2096
+ )
2097
+ coverage_branches = [
2098
+ dict(branch)
2099
+ for branch in data_contract.get("coverage_branches", [])
2100
+ if isinstance(branch, Mapping) and str(branch.get("branch_id") or "").strip()
2101
+ ]
2102
+ is_coverage_root = bool(coverage_assignment and coverage_branches) and int(
2103
+ getattr(node, "depth", len(path) - 1)
2104
+ ) == 0
2105
+ expansion_specs: list[tuple[Any, Mapping[str, Any] | None]]
2106
+ if is_coverage_root and skill_candidates:
2107
+ expansion_specs = [
2108
+ (skill_candidates[index % len(skill_candidates)], branch)
2109
+ for index, branch in enumerate(coverage_branches)
2110
+ ]
2111
+ else:
2112
+ expansion_specs = [(selection_candidate, None) for selection_candidate in skill_candidates]
2113
+ log_message(
2114
+ mcts.logger,
2115
+ mcts.log_sink,
2116
+ "info",
2117
+ "[MCTS] Expand: skill_prior\n%s",
2118
+ pretty_json(
2119
+ {candidate.skill.name: candidate.to_dict() for candidate in skill_candidates},
2120
+ ),
2121
+ )
2122
+ payload_count = len(skill_candidates)
2123
+ pre_children = len(node.children)
2124
+ new_child: Optional[Any] = None
2125
+
2126
+ idea_node_cls = type(node)
2127
+ operator_application_cls = type(node.transformation)
2128
+ for selection_candidate, coverage_branch in expansion_specs:
2129
+ skill = mcts.skill_catalog.render_skill_for_profile(
2130
+ selection_candidate.skill,
2131
+ profile_id,
2132
+ object_schema=object_schema,
2133
+ )
2134
+ plan = mcts.skill_catalog.compile_plan(
2135
+ skill=skill,
2136
+ parent_title=node.state.title,
2137
+ parent_components=node.state.components,
2138
+ target_defects=node.state.target_defects,
2139
+ memory_refs=bundle.referenced_ids(),
2140
+ profile_id=profile_id,
2141
+ object_schema=object_schema,
2142
+ )
2143
+ prior_constraints = mcts.skill_catalog.priors.get(skill.name, SkillUsagePrior()).rule_constraints
2144
+ if prior_constraints:
2145
+ plan.guardrails = _dedupe_keep_order_strings(plan.guardrails + list(prior_constraints))
2146
+ if coverage_branch is not None:
2147
+ branch_id = str(coverage_branch.get("branch_id") or "").strip()
2148
+ branch_instruction = str(coverage_branch.get("instruction") or "").strip()
2149
+ plan.objective = (
2150
+ f"{plan.objective} Data-coverage branch `{branch_id}`: {branch_instruction}"
2151
+ ).strip()
2152
+ plan.guardrails = _dedupe_keep_order_strings(
2153
+ [
2154
+ *plan.guardrails,
2155
+ f"Materialize the required data-coverage branch `{branch_id}`.",
2156
+ "Keep this branch dataset-local; it cannot establish a universal mechanism or novelty claim.",
2157
+ ]
2158
+ )
2159
+ plan.compile_notes = (
2160
+ f"{plan.compile_notes}\nRequired data-coverage branch: {branch_id}. "
2161
+ f"{branch_instruction}"
2162
+ ).strip()
2163
+
2164
+ current_count = len(node.state.components)
2165
+ filtered_edits: List[ComponentEdit] = []
2166
+ for edit in plan.component_edits:
2167
+ if current_count <= min_components and edit.op == AtomicEditOp.REMOVE_COMPONENT:
2168
+ continue
2169
+ if current_count >= max_components and edit.op == AtomicEditOp.ADD_COMPONENT:
2170
+ continue
2171
+ if edit.op == AtomicEditOp.ADD_COMPONENT:
2172
+ current_count += 1
2173
+ elif edit.op == AtomicEditOp.REMOVE_COMPONENT:
2174
+ current_count -= 1
2175
+ filtered_edits.append(edit)
2176
+ plan.component_edits = filtered_edits
2177
+
2178
+ instantiated = mcts._instantiate_skill_plan(plan, node.state, bundle)
2179
+ if isinstance(instantiated, dict) and instantiated.get("_skip_child_creation"):
2180
+ log_message(
2181
+ mcts.logger,
2182
+ mcts.log_sink,
2183
+ "info",
2184
+ "[MCTS] Expand: skipping skill=%s child creation (%s)",
2185
+ skill.name,
2186
+ instantiated.get("_skip_reason", "no reason provided"),
2187
+ )
2188
+ continue
2189
+ log_message(
2190
+ mcts.logger,
2191
+ mcts.log_sink,
2192
+ "info",
2193
+ "[MCTS] Expand: skill=%s instantiation_result\n%s",
2194
+ skill.name,
2195
+ pretty_json(
2196
+ instantiated
2197
+ if instantiated is not None
2198
+ else {"status": "empty", "message": "instantiation returned no output"},
2199
+ ),
2200
+ )
2201
+
2202
+ apply_instantiated_mapping_to_plan(plan, instantiated)
2203
+
2204
+ protocol_names: List[str] = []
2205
+ for edit_idx, edit in enumerate(plan.component_edits):
2206
+ if edit.op == AtomicEditOp.ADD_PROTOCOL:
2207
+ protocol_names.append(edit.component)
2208
+ continue
2209
+ op_dict = {
2210
+ "op": edit.op.value if hasattr(edit.op, "value") else edit.op,
2211
+ "component": edit.component,
2212
+ "target": edit.target,
2213
+ "condition": edit.condition,
2214
+ "details": edit.details or "",
2215
+ "reason": edit.reason or "",
2216
+ }
2217
+
2218
+ selection_metadata: Dict[str, Any] = {
2219
+ "idea_taste_mode": getattr(getattr(mcts, "idea_taste_preset", None), "mode", None) or "none",
2220
+ "skill_selection_breakdown": {
2221
+ "defect_score": selection_candidate.defect_score,
2222
+ "prior_score": selection_candidate.prior_score,
2223
+ "preset_bias": selection_candidate.preset_bias,
2224
+ "selection_total": selection_candidate.selection_total,
2225
+ },
2226
+ }
2227
+ if coverage_branch is not None:
2228
+ selection_metadata["data_anchored_coverage_branch"] = dict(coverage_branch)
2229
+ child_state = mcts._materialize_child_state(
2230
+ node.state,
2231
+ plan,
2232
+ instantiated,
2233
+ selection_metadata=selection_metadata,
2234
+ )
2235
+ child_node = attach_child(
2236
+ node,
2237
+ child_state,
2238
+ signature_nodes=mcts.signature_nodes,
2239
+ id_iter=mcts._id_iter,
2240
+ idea_node_cls=idea_node_cls,
2241
+ operator_application_cls=operator_application_cls,
2242
+ logger=mcts.logger,
2243
+ log_sink=mcts.log_sink,
2244
+ )
2245
+ if child_node is None:
2246
+ continue
2247
+ cached_eval = get_best_cached_evaluation(child_state.signature, mcts.evaluation_cache)
2248
+ if cached_eval:
2249
+ child_node.evaluation = cached_eval
2250
+ if new_child is None and child_node.visits == 0:
2251
+ new_child = child_node
2252
+
2253
+ node.expanded = True
2254
+
2255
+ if new_child is None and node.children:
2256
+ new_child = min(node.children, key=lambda c: c.visits)
2257
+ if new_child:
2258
+ return new_child, path + [new_child]
2259
+ return node, path
2260
+
2261
+
2262
+ def apply_profile_drift_to_evaluation(
2263
+ node: Any,
2264
+ evaluation: Any,
2265
+ profile_drift: Mapping[str, Any],
2266
+ ) -> None:
2267
+ """Apply a soft profile-drift penalty without rejecting the candidate."""
2268
+
2269
+ severity = str(profile_drift.get("drift_severity") or "none").strip().lower()
2270
+ if severity == "none":
2271
+ return
2272
+ intervention_payload = getattr(node.state, "scientific_intervention", None)
2273
+ if isinstance(intervention_payload, dict):
2274
+ intervention_payload["profile_drift"] = dict(profile_drift)
2275
+ detected_defects = getattr(evaluation, "detected_defects", None)
2276
+ if isinstance(detected_defects, list) and "profile_drift" not in detected_defects:
2277
+ detected_defects.append("profile_drift")
2278
+ target_defects = getattr(node.state, "target_defects", None)
2279
+ if isinstance(target_defects, list) and "profile_drift" not in target_defects:
2280
+ target_defects.append("profile_drift")
2281
+ if "profile_drift" in (detected_defects or []):
2282
+ penalty = 1.0 if severity == "material" else 0.5
2283
+ current_alignment = float(getattr(evaluation, "alignment_score", 0.0) or 0.0)
2284
+ if not getattr(evaluation, "_profile_drift_penalized", False):
2285
+ evaluation.alignment_score = max(0, round(current_alignment - penalty))
2286
+ setattr(evaluation, "_profile_drift_penalized", True)
2287
+ drift_terms = profile_drift.get("forbidden_primary_terms") or profile_drift.get("secondary_tool_mentions") or []
2288
+ drift_note = "Profile drift detected: " + ", ".join(str(term) for term in drift_terms)
2289
+ feedback = str(getattr(evaluation, "feedback", "") or "")
2290
+ if drift_note not in feedback:
2291
+ evaluation.feedback = f"{feedback}; {drift_note}" if feedback else drift_note
2292
+
2293
+
2294
+ def simulate_node_value(
2295
+ mcts: Any,
2296
+ node: Any,
2297
+ path: List[Any],
2298
+ experiences: List[Dict[str, Any]],
2299
+ *,
2300
+ idea_evaluation_cls: Any,
2301
+ ) -> Optional[Any]:
2302
+ path_summary_text = path_summary(path)
2303
+ scientific_intervention = getattr(node.state, "scientific_intervention", {})
2304
+ profile_id = str(
2305
+ scientific_intervention.get("profile_id") or "generic_scientific"
2306
+ ).strip().lower()
2307
+
2308
+ symbolic_hints = mcts._build_symbolic_eval_hints(node)
2309
+ log_message(
2310
+ mcts.logger,
2311
+ mcts.log_sink,
2312
+ "info",
2313
+ "[MCTS] Simulate: symbolic_memory\n%s",
2314
+ symbolic_hints,
2315
+ )
2316
+
2317
+ direction_context = _direction_prompt_context(mcts, node.state)
2318
+ prompt = mcts.evaluation_prompt.format(
2319
+ topic=mcts.topic,
2320
+ direction_mode=direction_context["direction_mode"],
2321
+ direction_summary=direction_context["direction_summary"],
2322
+ taste_guidance=direction_context["taste_guidance"],
2323
+ target_gap_ids=direction_context["target_gap_ids"],
2324
+ profile_id=direction_context["profile_id"],
2325
+ gap_seed_context=direction_context["gap_seed_context"],
2326
+ root_domains=_format_root_domains_for_prompt(getattr(node.state, "root_domains", [])),
2327
+ refinement_scope=mcts.refinement_scope or "None",
2328
+ mature_idea=mcts.mature_idea or "None",
2329
+ edit_plan=format_evaluator_edit_plan_prompt_view(node.state.edit_plan)
2330
+ if node.state.edit_plan
2331
+ else "No edit plan available.",
2332
+ idea=format_evaluator_idea_prompt_view(node.state, heading="Candidate Idea"),
2333
+ scientific_intervention_profile=format_scientific_intervention_profile_for_prompt(
2334
+ scientific_intervention
2335
+ ),
2336
+ profile_native_object_schema=direction_context["profile_native_object_schema"],
2337
+ scientific_rubric=format_scientific_rubric_for_prompt(scientific_intervention),
2338
+ defect_registry=format_defect_registry(profile_id),
2339
+ symbolic_memory_hints=symbolic_hints,
2340
+ )
2341
+ prompt += _data_anchored_contract_prompt(node.state)
2342
+ survey_handoff = getattr(mcts, "survey_idea_handoff", {})
2343
+ if isinstance(survey_handoff, dict) and survey_handoff:
2344
+ prompt += "\n\n== Verified Survey -> Idea handoff ==\n" + pretty_json(survey_handoff)
2345
+ prompt = render_profile_evaluation_prompt(prompt, profile_id)
2346
+ allowed_novelty_axes = PROFILE_NOVELTY_AXES.get(
2347
+ profile_id,
2348
+ PROFILE_NOVELTY_AXES["generic_scientific"],
2349
+ )
2350
+ prompt += (
2351
+ "\nFor `novelty_axes`, output only these profile-native keys: "
2352
+ + ", ".join(allowed_novelty_axes)
2353
+ + ". Do not add any other axis names.\n"
2354
+ )
2355
+ cache_key = evaluation_prompt_cache_key(
2356
+ prompt,
2357
+ rubric_version=SCIENTIFIC_RUBRIC_VERSION,
2358
+ profile_version=SCIENTIFIC_INTERVENTION_PROFILE_VERSION,
2359
+ profile_id=profile_id,
2360
+ )
2361
+
2362
+ survey_handoff = getattr(mcts, "survey_idea_handoff", {})
2363
+ profile_drift = detect_profile_drift(
2364
+ profile_id,
2365
+ candidate=node.state.to_payload(),
2366
+ survey_handoff=survey_handoff if isinstance(survey_handoff, Mapping) else None,
2367
+ research_object=getattr(node.state, "scientific_intervention", {}).get("scientific_object_schema")
2368
+ if isinstance(getattr(node.state, "scientific_intervention", {}), Mapping)
2369
+ else None,
2370
+ )
2371
+
2372
+ cached_evaluation = get_cached_evaluation(node.state.signature, cache_key, mcts.evaluation_cache)
2373
+ if cached_evaluation:
2374
+ apply_profile_drift_to_evaluation(node, cached_evaluation, profile_drift)
2375
+ node.evaluation = cached_evaluation
2376
+ node.latest_path_summary = path_summary_text
2377
+ log_message(
2378
+ mcts.logger,
2379
+ mcts.log_sink,
2380
+ "info",
2381
+ "[MCTS] Simulate (cache hit): node=%s\n[MCTS] Score: %.4f\n%s",
2382
+ node.state.title,
2383
+ cached_evaluation.composite,
2384
+ pretty_json(mcts._simulate_log_payload(cached_evaluation)),
2385
+ )
2386
+ maybe_record_experience(
2387
+ cache_key,
2388
+ node,
2389
+ cached_evaluation,
2390
+ path_summary_text,
2391
+ experiences,
2392
+ mcts.experience_cache,
2393
+ getattr(mcts, "enable_vector_memory", True),
2394
+ mcts.memory_accessor,
2395
+ mcts.config.min_confidence_for_memory,
2396
+ )
2397
+ return cached_evaluation
2398
+
2399
+ try:
2400
+ response = mcts.chat_fn(
2401
+ prompt,
2402
+ model=mcts.config.evaluation_model,
2403
+ temperature=mcts.config.evaluation_temperature,
2404
+ max_output_tokens=mcts.config.evaluation_max_tokens,
2405
+ )
2406
+ payload = parse_json_response(response)
2407
+ if isinstance(payload, list):
2408
+ payload = payload[0]
2409
+ legacy_weights = {
2410
+ "novelty_weight": mcts.config.novelty_weight,
2411
+ "surprise_weight": mcts.config.surprise_weight,
2412
+ "impact_weight": mcts.config.impact_weight,
2413
+ "feasibility_weight": mcts.config.feasibility_weight,
2414
+ "clarity_weight": mcts.config.clarity_weight,
2415
+ "conciseness_weight": mcts.config.conciseness_weight,
2416
+ "risk_weight": mcts.config.risk_weight,
2417
+ "alignment_weight": mcts.config.alignment_weight,
2418
+ "complexity_weight": mcts.config.complexity_weight,
2419
+ "protocol_weight": mcts.config.protocol_weight,
2420
+ "explanatory_power_weight": getattr(
2421
+ mcts.config, "explanatory_power_weight", 0.06
2422
+ ),
2423
+ "identifiability_weight": getattr(
2424
+ mcts.config, "identifiability_weight", 0.06
2425
+ ),
2426
+ "boundary_calibration_weight": getattr(
2427
+ mcts.config, "boundary_calibration_weight", 0.04
2428
+ ),
2429
+ "claim_overreach_weight": getattr(
2430
+ mcts.config, "claim_overreach_weight", 0.10
2431
+ ),
2432
+ }
2433
+ evaluation = idea_evaluation_cls.from_payload(
2434
+ payload,
2435
+ weights=profile_score_weights(profile_id, legacy_weights),
2436
+ profile_id=profile_id,
2437
+ )
2438
+ except Exception as exc:
2439
+ log_message(mcts.logger, mcts.log_sink, "warning", "⚠️ Simulation failed: %s", exc)
2440
+ return None
2441
+
2442
+ novelty_override = (
2443
+ mcts._score_component_novelty(node.state)
2444
+ if profile_id == "computational_algorithmic"
2445
+ else None
2446
+ )
2447
+ if novelty_override is not None:
2448
+ evaluation.novelty = round(novelty_override)
2449
+ evaluation.novelty_axes["scientific_novelty"] = float(evaluation.novelty)
2450
+ novelty_axes = getattr(mcts.component_novelty_scorer, "last_novelty_axes", {})
2451
+ if isinstance(novelty_axes, dict):
2452
+ evaluation.novelty_axes.update(novelty_axes)
2453
+
2454
+ if evaluation.protocol_score <= 0.0:
2455
+ evaluation.protocol_score = mcts._compute_protocol_score(node.state.edit_plan)
2456
+
2457
+ apply_profile_drift_to_evaluation(node, evaluation, profile_drift)
2458
+
2459
+ log_message(
2460
+ mcts.logger,
2461
+ mcts.log_sink,
2462
+ "info",
2463
+ "[MCTS] Simulate: node=%s\n[MCTS] Score: %.4f\n%s",
2464
+ node.state.title,
2465
+ evaluation.composite,
2466
+ pretty_json(mcts._simulate_log_payload(evaluation)),
2467
+ )
2468
+
2469
+ cache_evaluation(node.state.signature, cache_key, evaluation, mcts.evaluation_cache)
2470
+ node.evaluation = evaluation
2471
+ node.latest_path_summary = path_summary_text
2472
+
2473
+ maybe_record_experience(
2474
+ cache_key,
2475
+ node,
2476
+ evaluation,
2477
+ path_summary_text,
2478
+ experiences,
2479
+ mcts.experience_cache,
2480
+ getattr(mcts, "enable_vector_memory", True),
2481
+ mcts.memory_accessor,
2482
+ mcts.config.min_confidence_for_memory,
2483
+ )
2484
+
2485
+ return evaluation
2486
+
2487
+
2488
+ def backpropagate_rollout(path: List[Any], evaluation: Any) -> None:
2489
+ score = evaluation.composite
2490
+ for hop in reversed(path):
2491
+ hop.visits += 1
2492
+ hop.value_sum += score
2493
+
2494
+
2495
+ def reset_search_state(mcts: Any) -> None:
2496
+ mcts.signature_nodes = {}
2497
+ mcts.evaluation_cache = {}
2498
+ mcts.experience_cache.clear()
2499
+ mcts.trace = []
2500
+ mcts.retrieved_core_titles = []
2501
+ mcts._id_iter = itertools.count()
2502
+
2503
+
2504
+ def new_node(
2505
+ state: Any,
2506
+ depth: int,
2507
+ parent: Optional[Any],
2508
+ signature_nodes: Dict[str, Any],
2509
+ id_iter: Any,
2510
+ idea_node_cls: Any,
2511
+ operator_application_cls: Any,
2512
+ ) -> Any:
2513
+ existing = signature_nodes.get(state.signature)
2514
+ if existing:
2515
+ return existing
2516
+ node = idea_node_cls(
2517
+ node_id=next(id_iter),
2518
+ state=state,
2519
+ depth=depth,
2520
+ parent=parent,
2521
+ transformation=operator_application_cls(
2522
+ operator=state.operator,
2523
+ defects=state.target_defects,
2524
+ rationale=state.rationale,
2525
+ memory_refs=state.memory_refs,
2526
+ ),
2527
+ )
2528
+ signature_nodes[state.signature] = node
2529
+ if parent:
2530
+ parent.children.append(node)
2531
+ return node
2532
+
2533
+
2534
+ def attach_child(
2535
+ parent: Any,
2536
+ state: Any,
2537
+ signature_nodes: Dict[str, Any],
2538
+ id_iter: Any,
2539
+ idea_node_cls: Any,
2540
+ operator_application_cls: Any,
2541
+ logger: Any,
2542
+ log_sink: Optional[Any] = None,
2543
+ ) -> Optional[Any]:
2544
+ child = signature_nodes.get(state.signature)
2545
+ if child is None:
2546
+ return new_node(
2547
+ state,
2548
+ depth=parent.depth + 1,
2549
+ parent=parent,
2550
+ signature_nodes=signature_nodes,
2551
+ id_iter=id_iter,
2552
+ idea_node_cls=idea_node_cls,
2553
+ operator_application_cls=operator_application_cls,
2554
+ )
2555
+ if child is parent or is_ancestor(parent, child):
2556
+ return None
2557
+ if child not in parent.children:
2558
+ parent.children.append(child)
2559
+ return child
2560
+
2561
+
2562
+ def is_ancestor(node: Any, candidate: Any) -> bool:
2563
+ cursor: Optional[Any] = node
2564
+ while cursor is not None:
2565
+ if cursor is candidate:
2566
+ return True
2567
+ cursor = cursor.parent
2568
+ return False
2569
+
2570
+
2571
+ def path_summary(path: Sequence[Any], limit: int = 2048) -> str:
2572
+ steps: List[str] = []
2573
+ for hop in path:
2574
+ defects = hop.transformation.defects or ["unspecified"]
2575
+ steps.append(f"{hop.state.title} [{hop.transformation.operator}] -> defects {defects}")
2576
+ return clip_text(" | ".join(steps), limit)
2577
+
2578
+
2579
+ def evaluation_prompt_cache_key(
2580
+ prompt_text: str,
2581
+ *,
2582
+ rubric_version: str = SCIENTIFIC_RUBRIC_VERSION,
2583
+ profile_version: str = SCIENTIFIC_INTERVENTION_PROFILE_VERSION,
2584
+ profile_id: str = "generic_scientific",
2585
+ ) -> str:
2586
+ cache_material = "\n".join(
2587
+ [
2588
+ str(rubric_version),
2589
+ str(profile_version),
2590
+ str(profile_id),
2591
+ str(prompt_text),
2592
+ ]
2593
+ )
2594
+ return hashlib.sha256(cache_material.encode("utf-8")).hexdigest()
2595
+
2596
+
2597
+ def get_cached_evaluation(
2598
+ signature: str,
2599
+ path_key: str,
2600
+ evaluation_cache: Dict[str, Dict[str, Any]],
2601
+ ) -> Optional[Any]:
2602
+ sig_cache = evaluation_cache.get(signature)
2603
+ if not sig_cache:
2604
+ return None
2605
+ return sig_cache.get(path_key)
2606
+
2607
+
2608
+ def cache_evaluation(
2609
+ signature: str,
2610
+ path_key: str,
2611
+ evaluation: Any,
2612
+ evaluation_cache: Dict[str, Dict[str, Any]],
2613
+ ) -> None:
2614
+ evaluation_cache.setdefault(signature, {})[path_key] = evaluation
2615
+
2616
+
2617
+ def get_best_cached_evaluation(
2618
+ signature: str,
2619
+ evaluation_cache: Dict[str, Dict[str, Any]],
2620
+ ) -> Optional[Any]:
2621
+ sig_cache = evaluation_cache.get(signature)
2622
+ if not sig_cache:
2623
+ return None
2624
+ return max(sig_cache.values(), key=lambda ev: ev.composite)
2625
+
2626
+
2627
+ def maybe_record_experience(
2628
+ cache_key: str,
2629
+ node: Any,
2630
+ evaluation: Any,
2631
+ path_summary_text: str,
2632
+ experiences: List[Dict[str, Any]],
2633
+ experience_cache: Set[str],
2634
+ enable_vector_memory: bool,
2635
+ memory_accessor: Any,
2636
+ min_confidence_for_memory: float,
2637
+ ) -> None:
2638
+ if not enable_vector_memory:
2639
+ return
2640
+ if cache_key in experience_cache:
2641
+ return
2642
+ experience = harvest_experience(
2643
+ node,
2644
+ evaluation,
2645
+ path_summary_text,
2646
+ min_confidence_for_memory,
2647
+ )
2648
+ if not experience:
2649
+ return
2650
+ memory_accessor.persist_experience(experience)
2651
+ experiences.append(experience)
2652
+ experience_cache.add(cache_key)
2653
+
2654
+
2655
+ def build_root_state(
2656
+ topic: str,
2657
+ context: Dict[str, Any],
2658
+ idea_state_cls: Any,
2659
+ ) -> Any:
2660
+ latest_candidate = context.get("latest_candidate")
2661
+ root_idea = context.get("root_idea")
2662
+ mature_value = context.get("mature_idea")
2663
+ mature_record = context.get("mature_idea_record")
2664
+ if isinstance(mature_value, Mapping):
2665
+ mature_record = mature_value
2666
+ if isinstance(mature_record, Mapping):
2667
+ mature_record = normalize_mature_idea(mature_record)
2668
+ mature_idea = str(
2669
+ mature_record.get("hypothesis")
2670
+ or mature_record.get("abstract")
2671
+ or mature_record.get("title")
2672
+ or ""
2673
+ ).strip()
2674
+ else:
2675
+ mature_idea = str(mature_value or "").strip()
2676
+ background = context.get("background_knowledge") or []
2677
+ raw_defect_tags = context.get("defect_tags")
2678
+ if isinstance(raw_defect_tags, (list, tuple)):
2679
+ defect_tags = [str(tag).strip() for tag in raw_defect_tags if str(tag).strip()]
2680
+ elif str(raw_defect_tags or "").strip():
2681
+ defect_tags = [str(raw_defect_tags).strip()]
2682
+ else:
2683
+ defect_tags = []
2684
+ components = context.get("components") if isinstance(context.get("components"), list) else []
2685
+ context_component_explanations = (
2686
+ context.get("component_explanations")
2687
+ if isinstance(context.get("component_explanations"), (dict, list))
2688
+ else {}
2689
+ )
2690
+ root_domains = context.get("root_domains") if isinstance(context.get("root_domains"), list) else []
2691
+ discipline_resolution = (
2692
+ context.get("discipline_resolution")
2693
+ if isinstance(context.get("discipline_resolution"), dict)
2694
+ else {}
2695
+ )
2696
+ latest_payload: Dict[str, Any] = {}
2697
+
2698
+ if mature_idea:
2699
+ title = re.split(r"(?<=[.!?])\s+", mature_idea, maxsplit=1)[0].strip() or f"{topic} mature idea"
2700
+ abstract = mature_idea
2701
+ core = mature_idea
2702
+ method = mature_idea
2703
+ risks = "Primary risk is mechanism drift away from the mature idea during refinement."
2704
+ tags = ["seed", "mature_idea", "contract_root"]
2705
+ rationale = "Starting point anchored directly in the selected mature idea seed."
2706
+ if isinstance(mature_record, Mapping):
2707
+ title = str(mature_record.get("title") or title).strip()
2708
+ abstract = str(mature_record.get("abstract") or abstract).strip()
2709
+ core = str(
2710
+ mature_record.get("central_hypothesis")
2711
+ or mature_record.get("hypothesis")
2712
+ or core
2713
+ ).strip()
2714
+ method = str(
2715
+ mature_record.get("mechanism_or_relation")
2716
+ or mature_record.get("mechanism")
2717
+ or method
2718
+ ).strip()
2719
+ risks = "Primary risk is falsification of the mature idea mechanism or its stated assumptions."
2720
+ tags = ["seed", "mature_idea", "contract_root", str(mature_record.get("idea_id") or "")]
2721
+ tags = [tag for tag in tags if tag]
2722
+ elif isinstance(latest_candidate, dict) and latest_candidate:
2723
+ latest_payload = normalize_idea_contract(latest_candidate, keep_extra=True)
2724
+ title = latest_payload.get("title", f"{topic} seed idea")
2725
+ abstract = latest_payload.get("abstract", "")
2726
+ core = latest_payload.get("core_contribution", "")
2727
+ method = latest_payload.get("method", "")
2728
+ risks = latest_payload.get("risks", latest_payload.get("evaluation", ""))
2729
+ tags = latest_payload.get("tags")
2730
+ rationale = "Starting point from the latest candidate in the current run."
2731
+ if not components and isinstance(latest_payload.get("components"), list):
2732
+ components = [str(comp).strip() for comp in latest_payload.get("components", []) if str(comp).strip()]
2733
+ if not context_component_explanations:
2734
+ context_component_explanations = latest_payload.get("component_explanations", {})
2735
+ elif isinstance(root_idea, dict) and root_idea:
2736
+ latest_payload = normalize_idea_contract(root_idea, keep_extra=True)
2737
+ title = latest_payload.get("title", f"{topic} root idea")
2738
+ abstract = latest_payload.get("abstract", "")
2739
+ core = latest_payload.get("core_contribution", "")
2740
+ method = latest_payload.get("method", "")
2741
+ risks = latest_payload.get("risks", latest_payload.get("evaluation", ""))
2742
+ tags = latest_payload.get("tags")
2743
+ rationale = "Starting point from the explicit root idea produced by advanced analysis."
2744
+ if not components and isinstance(latest_payload.get("components"), list):
2745
+ components = [str(comp).strip() for comp in latest_payload.get("components", []) if str(comp).strip()]
2746
+ if not context_component_explanations:
2747
+ context_component_explanations = latest_payload.get("component_explanations", {})
2748
+ else:
2749
+ title = f"{topic} baseline"
2750
+ abstract = background[-1] if background else "Kick-off seed idea from analysis."
2751
+ core = "Seed idea derived from current analysis and background knowledge."
2752
+ method = "Synthesize referenced methods and expose unresolved bottlenecks."
2753
+ risks = "Need fairness checks and failure-mode surfacing."
2754
+ tags = ["seed"]
2755
+ rationale = "Starting point from existing analysis and background knowledge."
2756
+
2757
+ profile_payload = context.get("scientific_intervention_profile")
2758
+ existing_intervention = context.get("scientific_intervention")
2759
+ if not isinstance(existing_intervention, dict):
2760
+ existing_intervention = latest_payload.get("scientific_intervention")
2761
+ profile_resolution: Dict[str, Any] = {}
2762
+ if isinstance(profile_payload, dict):
2763
+ profile_resolution.update(profile_payload)
2764
+ if isinstance(existing_intervention, dict) and existing_intervention.get("profile_id"):
2765
+ profile_resolution.setdefault("profile_id", existing_intervention.get("profile_id"))
2766
+ profile_resolution.update(discipline_resolution)
2767
+ profile = resolve_scientific_intervention_profile(
2768
+ profile_resolution,
2769
+ root_domains=root_domains,
2770
+ project_context=(
2771
+ context.get("project_context")
2772
+ if isinstance(context.get("project_context"), dict)
2773
+ else None
2774
+ ),
2775
+ )
2776
+ if profile is None:
2777
+ profile = get_scientific_intervention_profile("generic_scientific")
2778
+ if profile is None: # pragma: no cover - registry invariant
2779
+ raise RuntimeError("generic_scientific intervention profile is unavailable")
2780
+ if profile.profile_id != "computational_algorithmic" and components:
2781
+ forbidden_algorithm_components = set(profile.forbidden_default_patterns)
2782
+ idea_text = " ".join(
2783
+ str(value or "")
2784
+ for value in (title, abstract, core, method)
2785
+ ).lower()
2786
+ high_precision_algorithm_terms = re.search(
2787
+ r"\b(?:algorithm(?:ic)?|machine\s+learning|deep\s+learning|neural\s+(?:network|model)|model\s+training|training\s+(?:a|the)\s+(?:model|network)|model\s+inference|inference[- ]time|trained\s+model|learned\s+(?:model|representation))\b",
2788
+ idea_text,
2789
+ )
2790
+ algorithmic_compound_terms = re.search(
2791
+ r"\b(?:algorithmic|machine[- ]learning|deep[- ]learning|neural|trained|learned)\s+(?:predictor|classifier|optimizer|surrogate\s+model|computational\s+(?:model|method))\b",
2792
+ idea_text,
2793
+ )
2794
+ algorithm_explicit = bool(high_precision_algorithm_terms or algorithmic_compound_terms)
2795
+ if not algorithm_explicit:
2796
+ components = [
2797
+ component
2798
+ for component in components
2799
+ if component not in forbidden_algorithm_components
2800
+ ]
2801
+ if not defect_tags:
2802
+ defect_tags = list(profile.defect_tags) or ["unexplored_gap"]
2803
+ if not components:
2804
+ components = profile.default_component_names()
2805
+ component_explanations = normalize_component_explanations(
2806
+ components,
2807
+ context_component_explanations,
2808
+ )
2809
+ scientific_intervention = build_scientific_intervention_payload(
2810
+ profile,
2811
+ components,
2812
+ component_explanations,
2813
+ existing_intervention,
2814
+ )
2815
+ seed_identity = str(
2816
+ context.get("seed_id")
2817
+ or context.get("idea_id")
2818
+ or (mature_record.get("idea_id") if isinstance(mature_record, Mapping) else "")
2819
+ or ""
2820
+ ).strip()
2821
+ route_identity = str(context.get("route_id") or "").strip()
2822
+ seed_gap_ids = context.get("target_gap_ids")
2823
+ if not isinstance(seed_gap_ids, list) and isinstance(mature_record, Mapping):
2824
+ seed_gap_ids = mature_record.get("target_gap_ids")
2825
+ seed_gap_ids = [str(item).strip() for item in (seed_gap_ids or []) if str(item).strip()]
2826
+ hypothesis_contract = dict(scientific_intervention.get("hypothesis_contract") or {})
2827
+ if seed_gap_ids:
2828
+ hypothesis_contract["target_gap_ids"] = seed_gap_ids
2829
+ if isinstance(mature_record, Mapping):
2830
+ for field_name, source_name in (
2831
+ ("central_hypothesis", "central_hypothesis"),
2832
+ ("scientific_object", "scientific_object"),
2833
+ ("mechanism_or_relation", "mechanism_or_relation"),
2834
+ ("assumptions", "assumptions"),
2835
+ ("evidence_basis", "evidence_basis"),
2836
+ ):
2837
+ value = mature_record.get(source_name)
2838
+ if value not in (None, "", [], {}):
2839
+ hypothesis_contract.setdefault(field_name, value)
2840
+ if hypothesis_contract:
2841
+ scientific_intervention["hypothesis_contract"] = hypothesis_contract
2842
+ if seed_identity:
2843
+ scientific_intervention["seed_id"] = seed_identity
2844
+ if route_identity:
2845
+ scientific_intervention["route_id"] = route_identity
2846
+ scientific_intervention["route_policy"] = context.get("route_policy") or {}
2847
+ routed_gap_context = {
2848
+ key: [dict(item) for item in context.get(key, []) if isinstance(item, dict)]
2849
+ for key in (
2850
+ "active_gaps",
2851
+ "provisional_gaps",
2852
+ "supporting_constraints",
2853
+ "verification_only_gaps",
2854
+ "future_work_seeds",
2855
+ )
2856
+ if isinstance(context.get(key), list) and context.get(key)
2857
+ }
2858
+ if routed_gap_context:
2859
+ scientific_intervention["gap_routing"] = routed_gap_context
2860
+ gap_hypothesis_seeds = [
2861
+ dict(seed)
2862
+ for seed in context.get("gap_hypothesis_seeds", [])
2863
+ if isinstance(seed, dict) and str(seed.get("seed_id") or "").strip()
2864
+ ]
2865
+ if gap_hypothesis_seeds:
2866
+ hypothesis_seed_refs = []
2867
+ for seed in gap_hypothesis_seeds:
2868
+ seed_ref = {
2869
+ "seed_id": str(seed.get("seed_id") or "").strip(),
2870
+ "gap_id": str(seed.get("gap_id") or "").strip(),
2871
+ "gap_route": str(seed.get("gap_route") or "").strip(),
2872
+ "seed_status": str(seed.get("seed_status") or "").strip(),
2873
+ "target_slot": str(seed.get("target_slot") or "").strip(),
2874
+ }
2875
+ if is_data_anchored(seed):
2876
+ seed_ref.update(
2877
+ {
2878
+ "analysis_priority": DATA_ANCHORED_PRIORITY,
2879
+ "data_anchor_refs": list(seed.get("data_anchor_refs") or []),
2880
+ "literature_reconciliation_status": str(
2881
+ seed.get("literature_reconciliation_status") or "unresolved"
2882
+ ),
2883
+ }
2884
+ )
2885
+ hypothesis_seed_refs.append(seed_ref)
2886
+ scientific_intervention["hypothesis_seed_refs"] = hypothesis_seed_refs
2887
+ scientific_intervention["gap_seed_status"] = (
2888
+ dict(context.get("gap_seed_status"))
2889
+ if isinstance(context.get("gap_seed_status"), dict)
2890
+ else {}
2891
+ )
2892
+ data_anchored_context = context.get("data_anchored_context")
2893
+ if not isinstance(data_anchored_context, Mapping) and isinstance(mature_record, Mapping):
2894
+ data_anchored_context = mature_record
2895
+ if is_data_anchored(data_anchored_context):
2896
+ scientific_intervention["data_anchored_contract"] = {
2897
+ "data_anchor_refs": list(data_anchored_context.get("data_anchor_refs") or []),
2898
+ "literature_reconciliation_status": str(
2899
+ data_anchored_context.get("literature_reconciliation_status") or "unresolved"
2900
+ ),
2901
+ "competing_explanations": list(
2902
+ data_anchored_context.get("competing_explanations") or []
2903
+ ),
2904
+ "measurement_needs": list(data_anchored_context.get("measurement_needs") or []),
2905
+ "claim_limits": list(data_anchored_context.get("claim_limits") or []),
2906
+ "mcts_depth_multiplier": float(
2907
+ data_anchored_context.get("mcts_depth_multiplier") or 1.75
2908
+ ),
2909
+ "coverage_branches": list(
2910
+ data_anchored_context.get("coverage_branches") or []
2911
+ ),
2912
+ }
2913
+ coverage_assignment = data_anchored_context.get(
2914
+ "data_anchored_coverage_assignment"
2915
+ )
2916
+ if isinstance(coverage_assignment, Mapping):
2917
+ scientific_intervention["data_anchored_contract"][
2918
+ "coverage_assignment"
2919
+ ] = dict(coverage_assignment)
2920
+
2921
+ return idea_state_cls(
2922
+ title=str(title),
2923
+ abstract=str(abstract),
2924
+ core_contribution=str(core),
2925
+ method=str(method),
2926
+ risks=str(risks),
2927
+ tags=[str(t) for t in tags] if isinstance(tags, list) else [str(tags)],
2928
+ operator="seed",
2929
+ target_defects=[str(tag) for tag in defect_tags],
2930
+ rationale=str(rationale),
2931
+ memory_refs=[],
2932
+ components=components,
2933
+ component_explanations=component_explanations,
2934
+ root_domains=[str(domain).strip() for domain in root_domains if str(domain).strip()][:2],
2935
+ discipline_resolution=discipline_resolution,
2936
+ scientific_intervention=scientific_intervention,
2937
+ paper_graph_context=str(context.get("paper_context") or ""),
2938
+ )
2939
+
2940
+
2941
+ def best_candidate(root: Any, candidate_cls: Any) -> Optional[Any]:
2942
+ candidates: List[Any] = []
2943
+ stack = [root]
2944
+ visited: Set[int] = set()
2945
+ while stack:
2946
+ node = stack.pop()
2947
+ if node.node_id in visited:
2948
+ continue
2949
+ visited.add(node.node_id)
2950
+ if node.evaluation:
2951
+ candidates.append(candidate_cls(node=node, evaluation=node.evaluation))
2952
+ stack.extend(node.children)
2953
+ if not candidates:
2954
+ return None
2955
+ return max(candidates, key=lambda c: c.evaluation.composite)
2956
+
2957
+
2958
+ def pareto_candidates(root: Any, candidate_cls: Any) -> Dict[str, Optional[Any]]:
2959
+ by_metric = {
2960
+ "novel": lambda ev: ev.novelty,
2961
+ "feasible": lambda ev: ev.feasibility,
2962
+ "concise": lambda ev: ev.conciseness,
2963
+ }
2964
+ pareto: Dict[str, Optional[Any]] = {k: None for k in by_metric}
2965
+ stack = [root]
2966
+ visited_ids: Set[int] = set()
2967
+ visited: List[Any] = []
2968
+ while stack:
2969
+ node = stack.pop()
2970
+ if node.node_id in visited_ids:
2971
+ continue
2972
+ visited_ids.add(node.node_id)
2973
+ if node.evaluation:
2974
+ visited.append(candidate_cls(node=node, evaluation=node.evaluation))
2975
+ stack.extend(node.children)
2976
+
2977
+ for label, scorer in by_metric.items():
2978
+ if visited:
2979
+ pareto[label] = max(visited, key=lambda c, s=scorer: s(c.evaluation))
2980
+ return pareto
2981
+
2982
+
2983
+ def harvest_experience(
2984
+ node: Any,
2985
+ evaluation: Any,
2986
+ path_summary_text: str,
2987
+ min_confidence_for_memory: float,
2988
+ ) -> Optional[Dict[str, Any]]:
2989
+ if evaluation.confidence > min_confidence_for_memory:
2990
+ experience = {
2991
+ "defect": ", ".join(node.state.target_defects) or evaluation.defect_fix_summary,
2992
+ "action": node.state.operator,
2993
+ "idea": node.state.title,
2994
+ "context": path_summary_text,
2995
+ "feedback": evaluation.feedback,
2996
+ "tags": node.state.tags + ["defect_fix"],
2997
+ "edit_plan": node.state.edit_plan,
2998
+ }
2999
+ return experience
3000
+ return None