@researai/deepscientist 1.5.14 → 1.5.16

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 (225) hide show
  1. package/README.md +336 -90
  2. package/assets/branding/logo-raster.png +0 -0
  3. package/bin/ds.js +816 -131
  4. package/docs/en/00_QUICK_START.md +36 -15
  5. package/docs/en/01_SETTINGS_REFERENCE.md +53 -4
  6. package/docs/en/02_START_RESEARCH_GUIDE.md +7 -0
  7. package/docs/en/03_QQ_CONNECTOR_GUIDE.md +19 -0
  8. package/docs/en/05_TUI_GUIDE.md +6 -0
  9. package/docs/en/06_RUNTIME_AND_CANVAS.md +4 -3
  10. package/docs/en/09_DOCTOR.md +11 -5
  11. package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
  12. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +65 -13
  13. package/docs/en/15_CODEX_PROVIDER_SETUP.md +25 -8
  14. package/docs/en/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
  15. package/docs/en/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
  16. package/docs/en/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
  17. package/docs/en/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  18. package/docs/en/19_LOCAL_BROWSER_AUTH.md +70 -0
  19. package/docs/en/20_WORKSPACE_MODES_GUIDE.md +250 -0
  20. package/docs/en/README.md +24 -0
  21. package/docs/zh/00_QUICK_START.md +36 -15
  22. package/docs/zh/01_SETTINGS_REFERENCE.md +53 -4
  23. package/docs/zh/02_START_RESEARCH_GUIDE.md +7 -0
  24. package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +19 -0
  25. package/docs/zh/05_TUI_GUIDE.md +6 -0
  26. package/docs/zh/09_DOCTOR.md +11 -5
  27. package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
  28. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +65 -13
  29. package/docs/zh/15_CODEX_PROVIDER_SETUP.md +25 -8
  30. package/docs/zh/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
  31. package/docs/zh/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
  32. package/docs/zh/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
  33. package/docs/zh/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  34. package/docs/zh/19_LOCAL_BROWSER_AUTH.md +68 -0
  35. package/docs/zh/20_WORKSPACE_MODES_GUIDE.md +251 -0
  36. package/docs/zh/README.md +24 -0
  37. package/install.sh +2 -0
  38. package/package.json +1 -1
  39. package/pyproject.toml +1 -1
  40. package/src/deepscientist/__init__.py +1 -1
  41. package/src/deepscientist/acp/envelope.py +6 -0
  42. package/src/deepscientist/artifact/charts.py +567 -0
  43. package/src/deepscientist/artifact/guidance.py +50 -10
  44. package/src/deepscientist/artifact/metrics.py +228 -5
  45. package/src/deepscientist/artifact/schemas.py +3 -0
  46. package/src/deepscientist/artifact/service.py +4276 -308
  47. package/src/deepscientist/bash_exec/models.py +23 -0
  48. package/src/deepscientist/bash_exec/monitor.py +147 -67
  49. package/src/deepscientist/bash_exec/runtime.py +218 -156
  50. package/src/deepscientist/bash_exec/service.py +309 -69
  51. package/src/deepscientist/bash_exec/shells.py +87 -0
  52. package/src/deepscientist/bridges/connectors.py +51 -2
  53. package/src/deepscientist/cli.py +115 -19
  54. package/src/deepscientist/codex_cli_compat.py +232 -0
  55. package/src/deepscientist/config/models.py +8 -4
  56. package/src/deepscientist/config/service.py +38 -11
  57. package/src/deepscientist/connector/weixin_support.py +122 -1
  58. package/src/deepscientist/daemon/api/handlers.py +199 -9
  59. package/src/deepscientist/daemon/api/router.py +5 -0
  60. package/src/deepscientist/daemon/app.py +1458 -289
  61. package/src/deepscientist/doctor.py +51 -0
  62. package/src/deepscientist/file_lock.py +48 -0
  63. package/src/deepscientist/gitops/__init__.py +10 -1
  64. package/src/deepscientist/gitops/diff.py +296 -1
  65. package/src/deepscientist/gitops/service.py +4 -1
  66. package/src/deepscientist/mcp/server.py +212 -5
  67. package/src/deepscientist/process_control.py +161 -0
  68. package/src/deepscientist/prompts/builder.py +501 -453
  69. package/src/deepscientist/quest/layout.py +15 -2
  70. package/src/deepscientist/quest/service.py +2539 -195
  71. package/src/deepscientist/quest/stage_views.py +177 -1
  72. package/src/deepscientist/runners/base.py +2 -0
  73. package/src/deepscientist/runners/codex.py +169 -31
  74. package/src/deepscientist/runners/runtime_overrides.py +17 -1
  75. package/src/deepscientist/skills/__init__.py +2 -2
  76. package/src/deepscientist/skills/installer.py +196 -5
  77. package/src/deepscientist/skills/registry.py +66 -0
  78. package/src/prompts/connectors/qq.md +18 -8
  79. package/src/prompts/connectors/weixin.md +16 -6
  80. package/src/prompts/contracts/shared_interaction.md +24 -4
  81. package/src/prompts/system.md +921 -72
  82. package/src/prompts/system_copilot.md +43 -0
  83. package/src/skills/analysis-campaign/SKILL.md +32 -2
  84. package/src/skills/analysis-campaign/references/artifact-orchestration.md +1 -1
  85. package/src/skills/analysis-campaign/references/writing-facing-slice-examples.md +65 -0
  86. package/src/skills/baseline/SKILL.md +10 -0
  87. package/src/skills/decision/SKILL.md +27 -2
  88. package/src/skills/experiment/SKILL.md +16 -2
  89. package/src/skills/figure-polish/SKILL.md +1 -0
  90. package/src/skills/finalize/SKILL.md +19 -0
  91. package/src/skills/idea/SKILL.md +79 -0
  92. package/src/skills/idea/references/idea-generation-playbook.md +100 -0
  93. package/src/skills/idea/references/outline-seeding-example.md +60 -0
  94. package/src/skills/intake-audit/SKILL.md +9 -1
  95. package/src/skills/mentor/SKILL.md +217 -0
  96. package/src/skills/mentor/references/correction-rules.md +210 -0
  97. package/src/skills/mentor/references/knowledge-profile.md +91 -0
  98. package/src/skills/mentor/references/persona-profile.md +138 -0
  99. package/src/skills/mentor/references/taste-profile.md +128 -0
  100. package/src/skills/mentor/references/thought-style-profile.md +138 -0
  101. package/src/skills/mentor/references/work-profile.md +289 -0
  102. package/src/skills/mentor/references/workflow-profile.md +240 -0
  103. package/src/skills/optimize/SKILL.md +1645 -0
  104. package/src/skills/rebuttal/SKILL.md +3 -1
  105. package/src/skills/review/SKILL.md +3 -1
  106. package/src/skills/scout/SKILL.md +8 -0
  107. package/src/skills/write/SKILL.md +81 -12
  108. package/src/skills/write/references/outline-evidence-contract-example.md +107 -0
  109. package/src/tui/dist/app/AppContainer.js +22 -11
  110. package/src/tui/dist/index.js +4 -1
  111. package/src/tui/dist/lib/api.js +33 -3
  112. package/src/tui/package.json +1 -1
  113. package/src/ui/dist/assets/AiManusChatView-COFACy7V.js +204 -0
  114. package/src/ui/dist/assets/AnalysisPlugin-DnSm0GZn.js +1 -0
  115. package/src/ui/dist/assets/CliPlugin-CvwCmDQ5.js +109 -0
  116. package/src/ui/dist/assets/CodeEditorPlugin-cOqSa0xq.js +2 -0
  117. package/src/ui/dist/assets/CodeViewerPlugin-itb0tltR.js +270 -0
  118. package/src/ui/dist/assets/DocViewerPlugin-DqKkiCI6.js +7 -0
  119. package/src/ui/dist/assets/GitCommitViewerPlugin-DVgNHBCS.js +1 -0
  120. package/src/ui/dist/assets/GitDiffViewerPlugin-DxL2ezFG.js +6 -0
  121. package/src/ui/dist/assets/GitSnapshotViewer-B_RQm1YZ.js +30 -0
  122. package/src/ui/dist/assets/ImageViewerPlugin-tHqlXY3n.js +26 -0
  123. package/src/ui/dist/assets/LabCopilotPanel-ClMbq5Yu.js +14 -0
  124. package/src/ui/dist/assets/LabPlugin-L_SuE8ow.js +22 -0
  125. package/src/ui/dist/assets/LatexPlugin-B495DTXC.js +25 -0
  126. package/src/ui/dist/assets/MarkdownViewerPlugin-DG28-61B.js +128 -0
  127. package/src/ui/dist/assets/MarketplacePlugin-BiOGT-Kj.js +13 -0
  128. package/src/ui/dist/assets/{NotebookEditor-CccQYZjX.css → NotebookEditor-BHH8rdGj.css} +1 -1
  129. package/src/ui/dist/assets/NotebookEditor-BOr3x3Ej.css +1 -0
  130. package/src/ui/dist/assets/NotebookEditor-C-4Kt1p9.js +81 -0
  131. package/src/ui/dist/assets/NotebookEditor-CVsj8h_T.js +361 -0
  132. package/src/ui/dist/assets/PdfLoader-CASDQmxJ.js +16 -0
  133. package/src/ui/dist/assets/PdfLoader-Cy5jtWrr.css +1 -0
  134. package/src/ui/dist/assets/PdfMarkdownPlugin-BFhwoKsY.js +1 -0
  135. package/src/ui/dist/assets/PdfViewerPlugin-DcOzU9vd.js +17 -0
  136. package/src/ui/dist/assets/PdfViewerPlugin-nwwE-fjJ.css +1 -0
  137. package/src/ui/dist/assets/SearchPlugin-CHj7M58O.js +16 -0
  138. package/src/ui/dist/assets/SearchPlugin-DA4en4hK.css +1 -0
  139. package/src/ui/dist/assets/TextViewerPlugin-CB4DYfWO.js +54 -0
  140. package/src/ui/dist/assets/VNCViewer-CjlbyCB3.js +11 -0
  141. package/src/ui/dist/assets/bot-CFkZY-JP.js +6 -0
  142. package/src/ui/dist/assets/browser-CTB2jwNe.js +8 -0
  143. package/src/ui/dist/assets/chevron-up-Dq5ofbht.js +6 -0
  144. package/src/ui/dist/assets/code-DLC6G24T.js +6 -0
  145. package/src/ui/dist/assets/file-content-Dv4LoZec.js +1 -0
  146. package/src/ui/dist/assets/file-diff-panel-Denq-lC3.js +1 -0
  147. package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +1 -0
  148. package/src/ui/dist/assets/file-socket-Cu4Qln7Y.js +1 -0
  149. package/src/ui/dist/assets/git-commit-horizontal-BUh6G52n.js +6 -0
  150. package/src/ui/dist/assets/image-B9HUUddG.js +6 -0
  151. package/src/ui/dist/assets/index-B2B1sg-M.js +1 -0
  152. package/src/ui/dist/assets/index-Cgla8biy.css +33 -0
  153. package/src/ui/dist/assets/index-DRyx7vAc.js +1 -0
  154. package/src/ui/dist/assets/index-Gbl53BNp.js +2496 -0
  155. package/src/ui/dist/assets/index-wQ7RIIRd.js +11 -0
  156. package/src/ui/dist/assets/monaco-CiHMMNH_.js +1 -0
  157. package/src/ui/dist/assets/pdf-effect-queue-ZtnHFCAi.js +6 -0
  158. package/src/ui/dist/assets/plugin-monaco-C8UgLomw.js +19 -0
  159. package/src/ui/dist/assets/plugin-notebook-HbW2K-1c.js +169 -0
  160. package/src/ui/dist/assets/plugin-pdf-CR8hgQBV.js +357 -0
  161. package/src/ui/dist/assets/plugin-terminal-MXFIPun8.js +227 -0
  162. package/src/ui/dist/assets/popover-DL6h35vr.js +1 -0
  163. package/src/ui/dist/assets/project-sync-CsX08Qno.js +1 -0
  164. package/src/ui/dist/assets/select-DvmXt1yY.js +11 -0
  165. package/src/ui/dist/assets/sigma-7jpXazui.js +6 -0
  166. package/src/ui/dist/assets/trash-xA7kFt8i.js +11 -0
  167. package/src/ui/dist/assets/useCliAccess-DsMwDjOp.js +1 -0
  168. package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +1 -0
  169. package/src/ui/dist/assets/wrap-text-CwMn-iqb.js +11 -0
  170. package/src/ui/dist/assets/zoom-out-R-GWEhzS.js +11 -0
  171. package/src/ui/dist/index.html +5 -2
  172. package/src/ui/dist/assets/AiManusChatView-DaF9Nge_.js +0 -26597
  173. package/src/ui/dist/assets/AnalysisPlugin-BSVx6dXE.js +0 -123
  174. package/src/ui/dist/assets/CliPlugin-C9gzJX41.js +0 -5905
  175. package/src/ui/dist/assets/CodeEditorPlugin-DU9G0Tox.js +0 -427
  176. package/src/ui/dist/assets/CodeViewerPlugin-DoX_fI9l.js +0 -905
  177. package/src/ui/dist/assets/DocViewerPlugin-C4FWIXuU.js +0 -278
  178. package/src/ui/dist/assets/GitDiffViewerPlugin-BgfFMgtf.js +0 -2661
  179. package/src/ui/dist/assets/ImageViewerPlugin-tcPkfY_x.js +0 -500
  180. package/src/ui/dist/assets/LabCopilotPanel-_dKV60Bf.js +0 -4104
  181. package/src/ui/dist/assets/LabPlugin-Bje0ayoC.js +0 -2677
  182. package/src/ui/dist/assets/LatexPlugin-CVsBzAln.js +0 -1792
  183. package/src/ui/dist/assets/MarkdownViewerPlugin-xjmrqv_8.js +0 -308
  184. package/src/ui/dist/assets/MarketplacePlugin-mMM2A8wP.js +0 -413
  185. package/src/ui/dist/assets/NotebookEditor-3kVDSOBo.js +0 -4214
  186. package/src/ui/dist/assets/NotebookEditor-C3VQ7ylN.css +0 -1405
  187. package/src/ui/dist/assets/NotebookEditor-SoJ8X-MO.js +0 -84873
  188. package/src/ui/dist/assets/PdfLoader-C-Y707R3.css +0 -49
  189. package/src/ui/dist/assets/PdfLoader-DElVuHl9.js +0 -25468
  190. package/src/ui/dist/assets/PdfMarkdownPlugin-Bq88XT4G.js +0 -409
  191. package/src/ui/dist/assets/PdfViewerPlugin-CsCXMo9S.js +0 -3095
  192. package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +0 -3627
  193. package/src/ui/dist/assets/SearchPlugin-DDMrGDkh.css +0 -379
  194. package/src/ui/dist/assets/SearchPlugin-oUPvy19k.js +0 -741
  195. package/src/ui/dist/assets/TextViewerPlugin-CRkT9yNy.js +0 -472
  196. package/src/ui/dist/assets/VNCViewer-BgbuvWhR.js +0 -18821
  197. package/src/ui/dist/assets/awareness-C0NPR2Dj.js +0 -292
  198. package/src/ui/dist/assets/bot-v_RASACv.js +0 -21
  199. package/src/ui/dist/assets/browser-BAcuE0Xj.js +0 -2895
  200. package/src/ui/dist/assets/code-5hC9d0VH.js +0 -17
  201. package/src/ui/dist/assets/file-content-D1PxfOrp.js +0 -377
  202. package/src/ui/dist/assets/file-diff-panel-DG1oT_Hj.js +0 -92
  203. package/src/ui/dist/assets/file-jump-queue-r5XKgJEV.js +0 -16
  204. package/src/ui/dist/assets/file-socket-BmdFYQlk.js +0 -58
  205. package/src/ui/dist/assets/function-B5QZkkHC.js +0 -1895
  206. package/src/ui/dist/assets/image-Dqe2X2tW.js +0 -18
  207. package/src/ui/dist/assets/index-BQG-1s2o.css +0 -12553
  208. package/src/ui/dist/assets/index-DVsMKK_y.js +0 -25
  209. package/src/ui/dist/assets/index-Duvz8Ip0.js +0 -159
  210. package/src/ui/dist/assets/index-Nt9hS4ck.js +0 -244829
  211. package/src/ui/dist/assets/index-RDlNXXx1.js +0 -120
  212. package/src/ui/dist/assets/monaco-DIXge1CP.js +0 -623
  213. package/src/ui/dist/assets/pdf-effect-queue-BBTTQaO-.js +0 -47
  214. package/src/ui/dist/assets/pdf_viewer-e0g1is2C.js +0 -8206
  215. package/src/ui/dist/assets/popover-BWlolyxo.js +0 -476
  216. package/src/ui/dist/assets/project-sync-BM5PkFH4.js +0 -297
  217. package/src/ui/dist/assets/select-D4dAtrA8.js +0 -1690
  218. package/src/ui/dist/assets/sigma-CKbE5jJT.js +0 -22
  219. package/src/ui/dist/assets/square-check-big-CZNGMgiB.js +0 -17
  220. package/src/ui/dist/assets/trash-DaB37xAz.js +0 -32
  221. package/src/ui/dist/assets/useCliAccess-C2OmAcWe.js +0 -957
  222. package/src/ui/dist/assets/useFileDiffOverlay-Dowd1Ij4.js +0 -53
  223. package/src/ui/dist/assets/wrap-text-BGjAhAUq.js +0 -35
  224. package/src/ui/dist/assets/yjs-DncrqiZ8.js +0 -11243
  225. package/src/ui/dist/assets/zoom-out-dMZQMXzc.js +0 -34
@@ -581,6 +581,18 @@ def normalize_metric_contract(
581
581
  def selected_baseline_metrics(entry: dict[str, Any] | None, selected_variant_id: str | None = None) -> dict[str, Any]:
582
582
  if not isinstance(entry, dict) or not entry:
583
583
  return {}
584
+
585
+ def with_primary_metric(summary: dict[str, float], primary_metric: object) -> dict[str, float]:
586
+ resolved = OrderedDict(summary)
587
+ if isinstance(primary_metric, dict):
588
+ metric_id = str(
589
+ primary_metric.get("metric_id") or primary_metric.get("name") or primary_metric.get("id") or ""
590
+ ).strip()
591
+ value = to_number(primary_metric.get("value"))
592
+ if metric_id and value is not None and metric_id not in resolved:
593
+ resolved[metric_id] = value
594
+ return dict(resolved)
595
+
584
596
  variants = entry.get("baseline_variants") if isinstance(entry.get("baseline_variants"), list) else []
585
597
  target_id = str(selected_variant_id or entry.get("default_variant_id") or "").strip()
586
598
  selected_variant = None
@@ -592,15 +604,33 @@ def selected_baseline_metrics(entry: dict[str, Any] | None, selected_variant_id:
592
604
  if selected_variant is None and variants:
593
605
  selected_variant = next((item for item in variants if isinstance(item, dict)), None)
594
606
  if isinstance(selected_variant, dict):
595
- summary = extract_numeric_metric_map(metrics_summary=selected_variant.get("metrics_summary"))
607
+ summary = with_primary_metric(
608
+ extract_numeric_metric_map(metrics_summary=selected_variant.get("metrics_summary")),
609
+ selected_variant.get("primary_metric"),
610
+ )
596
611
  if summary:
597
612
  return summary
598
- return extract_numeric_metric_map(metrics_summary=entry.get("metrics_summary"))
613
+ return with_primary_metric(
614
+ extract_numeric_metric_map(metrics_summary=entry.get("metrics_summary")),
615
+ entry.get("primary_metric"),
616
+ )
599
617
 
600
618
 
601
619
  def baseline_metric_lines(entry: dict[str, Any] | None, selected_variant_id: str | None = None) -> list[dict[str, Any]]:
602
620
  if not isinstance(entry, dict) or not entry:
603
621
  return []
622
+
623
+ def metrics_with_primary(summary: object, primary_metric: object) -> dict[str, float]:
624
+ resolved = OrderedDict(extract_numeric_metric_map(metrics_summary=summary))
625
+ if isinstance(primary_metric, dict):
626
+ metric_id = str(
627
+ primary_metric.get("metric_id") or primary_metric.get("name") or primary_metric.get("id") or ""
628
+ ).strip()
629
+ value = to_number(primary_metric.get("value"))
630
+ if metric_id and value is not None and metric_id not in resolved:
631
+ resolved[metric_id] = value
632
+ return dict(resolved)
633
+
604
634
  baseline_id = str(entry.get("baseline_id") or entry.get("entry_id") or "").strip() or None
605
635
  selected_id = str(selected_variant_id or entry.get("default_variant_id") or "").strip() or None
606
636
  lines: list[dict[str, Any]] = []
@@ -609,12 +639,13 @@ def baseline_metric_lines(entry: dict[str, Any] | None, selected_variant_id: str
609
639
  if not isinstance(variant, dict):
610
640
  continue
611
641
  variant_id = str(variant.get("variant_id") or "").strip() or None
612
- metrics_summary = extract_numeric_metric_map(metrics_summary=variant.get("metrics_summary"))
642
+ variant_label = str(variant.get("label") or variant_id or "variant").strip() or "variant"
643
+ metrics_summary = metrics_with_primary(variant.get("metrics_summary"), variant.get("primary_metric"))
613
644
  for metric_id, value in metrics_summary.items():
614
645
  lines.append(
615
646
  {
616
647
  "metric_id": metric_id,
617
- "label": f"{baseline_id or 'baseline'}:{variant_id or 'variant'}",
648
+ "label": f"{baseline_id or 'baseline'}:{variant_label}",
618
649
  "baseline_id": baseline_id,
619
650
  "variant_id": variant_id,
620
651
  "selected": bool(selected_id and variant_id == selected_id),
@@ -624,7 +655,7 @@ def baseline_metric_lines(entry: dict[str, Any] | None, selected_variant_id: str
624
655
  )
625
656
  if lines:
626
657
  return lines
627
- for metric_id, value in extract_numeric_metric_map(metrics_summary=entry.get("metrics_summary")).items():
658
+ for metric_id, value in metrics_with_primary(entry.get("metrics_summary"), entry.get("primary_metric")).items():
628
659
  lines.append(
629
660
  {
630
661
  "metric_id": metric_id,
@@ -639,6 +670,198 @@ def baseline_metric_lines(entry: dict[str, Any] | None, selected_variant_id: str
639
670
  return lines
640
671
 
641
672
 
673
+ def build_baseline_compare_payload(
674
+ *,
675
+ quest_id: str,
676
+ baseline_entries: list[dict[str, Any]],
677
+ active_baseline_id: str | None = None,
678
+ active_variant_id: str | None = None,
679
+ ) -> dict[str, Any]:
680
+ series_map: OrderedDict[str, dict[str, Any]] = OrderedDict()
681
+ baseline_meta_map: dict[str, dict[str, Any]] = {}
682
+ deduped_entries: OrderedDict[str, dict[str, Any]] = OrderedDict()
683
+ ordered_baseline_entries: list[dict[str, Any]] = []
684
+ primary_metric_id: str | None = None
685
+ active_baseline_text = str(active_baseline_id or "").strip() or None
686
+ active_variant_text = str(active_variant_id or "").strip() or None
687
+
688
+ def entry_variant_groups(entry: dict[str, Any]) -> list[tuple[str | None, dict[str, Any] | None]]:
689
+ variants = entry.get("baseline_variants") if isinstance(entry.get("baseline_variants"), list) else []
690
+ if variants:
691
+ groups: list[tuple[str | None, dict[str, Any] | None]] = []
692
+ for variant in variants:
693
+ if not isinstance(variant, dict):
694
+ continue
695
+ groups.append((str(variant.get("variant_id") or "").strip() or None, variant))
696
+ if groups:
697
+ return groups
698
+ return [(None, None)]
699
+
700
+ def entry_key(entry: dict[str, Any], *, variant_id: str | None) -> str:
701
+ baseline_id = str(entry.get("baseline_id") or entry.get("entry_id") or "").strip() or "baseline"
702
+ variant_text = (
703
+ str(variant_id or entry.get("default_variant_id") or "").strip()
704
+ or "default"
705
+ )
706
+ return f"{baseline_id}::{variant_text}"
707
+
708
+ def is_selected(entry: dict[str, Any], *, variant_id: str | None) -> bool:
709
+ baseline_id = str(entry.get("baseline_id") or entry.get("entry_id") or "").strip() or None
710
+ if not baseline_id or baseline_id != active_baseline_text:
711
+ return False
712
+ resolved_variant_id = str(variant_id or entry.get("default_variant_id") or "").strip() or None
713
+ if active_variant_text:
714
+ return resolved_variant_id == active_variant_text
715
+ if resolved_variant_id:
716
+ default_variant_id = str(entry.get("default_variant_id") or "").strip() or None
717
+ return resolved_variant_id == (default_variant_id or resolved_variant_id)
718
+ return True
719
+
720
+ def ensure_series(metric_id: str, meta: dict[str, Any] | None = None) -> dict[str, Any]:
721
+ resolved_meta = meta or baseline_meta_map.get(metric_id) or _normalize_metric_entry({}, fallback_id=metric_id)
722
+ if metric_id not in series_map:
723
+ series_map[metric_id] = {
724
+ "metric_id": metric_id,
725
+ "label": resolved_meta.get("label") or metric_id,
726
+ "direction": normalize_metric_direction(resolved_meta.get("direction"), metric_id=metric_id),
727
+ "unit": resolved_meta.get("unit"),
728
+ "decimals": resolved_meta.get("decimals"),
729
+ "chart_group": resolved_meta.get("chart_group"),
730
+ "values": [],
731
+ }
732
+ else:
733
+ series_map[metric_id]["label"] = resolved_meta.get("label") or series_map[metric_id]["label"]
734
+ series_map[metric_id]["direction"] = normalize_metric_direction(
735
+ resolved_meta.get("direction") or series_map[metric_id]["direction"],
736
+ metric_id=metric_id,
737
+ )
738
+ series_map[metric_id]["unit"] = resolved_meta.get("unit") or series_map[metric_id]["unit"]
739
+ if resolved_meta.get("decimals") is not None:
740
+ series_map[metric_id]["decimals"] = resolved_meta.get("decimals")
741
+ series_map[metric_id]["chart_group"] = (
742
+ resolved_meta.get("chart_group") or series_map[metric_id]["chart_group"]
743
+ )
744
+ return series_map[metric_id]
745
+
746
+ for entry in baseline_entries:
747
+ if not isinstance(entry, dict):
748
+ continue
749
+ baseline_id = str(entry.get("baseline_id") or entry.get("entry_id") or "").strip() or None
750
+ if not baseline_id:
751
+ continue
752
+ for variant_id, _variant in entry_variant_groups(entry):
753
+ deduped_entries[entry_key(entry, variant_id=variant_id)] = {
754
+ **entry,
755
+ "_compare_variant_id": variant_id,
756
+ }
757
+
758
+ for normalized_entry in deduped_entries.values():
759
+ variant_id = str(normalized_entry.get("_compare_variant_id") or "").strip() or None
760
+ contract = normalize_metric_contract(
761
+ normalized_entry.get("metric_contract"),
762
+ baseline_id=str(normalized_entry.get("baseline_id") or normalized_entry.get("entry_id") or ""),
763
+ metrics_summary=selected_baseline_metrics(normalized_entry, variant_id),
764
+ primary_metric=normalized_entry.get("primary_metric"),
765
+ baseline_variants=normalized_entry.get("baseline_variants"),
766
+ )
767
+ if primary_metric_id is None:
768
+ candidate_primary = str(contract.get("primary_metric_id") or "").strip() or None
769
+ if candidate_primary:
770
+ primary_metric_id = candidate_primary
771
+ metric_meta = extract_metric_meta_map(
772
+ metric_contract=normalized_entry.get("metric_contract"),
773
+ metrics_summary=selected_baseline_metrics(normalized_entry, variant_id),
774
+ )
775
+ baseline_meta_map.update(metric_meta)
776
+ compare_key = entry_key(normalized_entry, variant_id=variant_id)
777
+ selected = is_selected(normalized_entry, variant_id=variant_id)
778
+ ordered_baseline_entries.append(
779
+ {
780
+ "entry_key": compare_key,
781
+ "baseline_id": str(normalized_entry.get("baseline_id") or normalized_entry.get("entry_id") or "").strip() or None,
782
+ "variant_id": variant_id,
783
+ "label": next(
784
+ (
785
+ str(item.get("label") or item.get("variant_id") or "").strip()
786
+ for item in (normalized_entry.get("baseline_variants") or [])
787
+ if isinstance(item, dict) and str(item.get("variant_id") or "").strip() == str(variant_id or "").strip()
788
+ ),
789
+ None,
790
+ )
791
+ or (variant_id or str(normalized_entry.get("baseline_id") or "").strip() or "baseline"),
792
+ "baseline_kind": str(normalized_entry.get("baseline_kind") or "").strip() or None,
793
+ "summary": str(normalized_entry.get("summary") or "").strip() or None,
794
+ "selected": selected,
795
+ "updated_at": normalized_entry.get("updated_at") or normalized_entry.get("created_at"),
796
+ "metric_count": len(selected_baseline_metrics(normalized_entry, variant_id)),
797
+ }
798
+ )
799
+ for line in baseline_metric_lines(normalized_entry, variant_id):
800
+ metric_id = str(line.get("metric_id") or "").strip()
801
+ if not metric_id:
802
+ continue
803
+ line_variant_id = str(line.get("variant_id") or "").strip() or None
804
+ if line_variant_id != variant_id:
805
+ if not (line_variant_id is None and variant_id is None):
806
+ continue
807
+ ensure_series(metric_id, metric_meta.get(metric_id))
808
+ series_map[metric_id]["values"].append(
809
+ {
810
+ "entry_key": compare_key,
811
+ "label": line.get("label"),
812
+ "baseline_id": line.get("baseline_id"),
813
+ "variant_id": line.get("variant_id"),
814
+ "selected": selected,
815
+ "value": line.get("value"),
816
+ "raw_value": line.get("raw_value"),
817
+ "baseline_kind": str(normalized_entry.get("baseline_kind") or "").strip() or None,
818
+ "summary": str(normalized_entry.get("summary") or "").strip() or None,
819
+ "updated_at": normalized_entry.get("updated_at") or normalized_entry.get("created_at"),
820
+ }
821
+ )
822
+
823
+ def sort_metric_values(series: dict[str, Any]) -> None:
824
+ direction = normalize_metric_direction(series.get("direction"), metric_id=str(series.get("metric_id") or ""))
825
+
826
+ def sort_key(item: dict[str, Any]) -> tuple[int, float, str]:
827
+ value = to_number(item.get("value"))
828
+ if value is None:
829
+ metric_rank = float("inf")
830
+ elif direction == "minimize":
831
+ metric_rank = value
832
+ else:
833
+ metric_rank = -value
834
+ return (0 if item.get("selected") else 1, metric_rank, str(item.get("label") or ""))
835
+
836
+ series["values"].sort(key=sort_key)
837
+
838
+ for series in series_map.values():
839
+ sort_metric_values(series)
840
+
841
+ ordered_baseline_entries.sort(
842
+ key=lambda item: (
843
+ 0 if item.get("selected") else 1,
844
+ str(item.get("updated_at") or ""),
845
+ str(item.get("baseline_id") or ""),
846
+ str(item.get("variant_id") or ""),
847
+ )
848
+ )
849
+
850
+ return {
851
+ "quest_id": quest_id,
852
+ "primary_metric_id": primary_metric_id,
853
+ "total_entries": len(ordered_baseline_entries),
854
+ "baseline_ref": {
855
+ "baseline_id": active_baseline_text,
856
+ "variant_id": active_variant_text,
857
+ }
858
+ if active_baseline_text
859
+ else None,
860
+ "entries": ordered_baseline_entries,
861
+ "series": [item for item in series_map.values() if item["values"]],
862
+ }
863
+
864
+
642
865
  def normalize_metric_rows(
643
866
  metric_rows: object,
644
867
  *,
@@ -5,6 +5,7 @@ ARTIFACT_DIRS = {
5
5
  "idea": "ideas",
6
6
  "decision": "decisions",
7
7
  "progress": "progress",
8
+ "answer": "answers",
8
9
  "milestone": "milestones",
9
10
  "run": "runs",
10
11
  "report": "reports",
@@ -61,6 +62,8 @@ def guidance_for_kind(kind: str) -> str:
61
62
  return "Run recorded. Compare metrics, then decide whether to continue, branch, or stop."
62
63
  if kind == "milestone":
63
64
  return "Milestone recorded. Send a concise progress update to the active surface."
65
+ if kind == "answer":
66
+ return "Answer stored. This was a direct user-facing reply, not a long-running progress checkpoint."
64
67
  if kind == "report":
65
68
  return "Report saved. Use it to update SUMMARY.md and the next planning step."
66
69
  if kind == "approval":