@researai/deepscientist 1.5.0 → 1.5.2
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.
- package/AGENTS.md +26 -0
- package/README.md +47 -161
- package/assets/connectors/lingzhu/openclaw-bridge/README.md +124 -0
- package/assets/connectors/lingzhu/openclaw-bridge/index.ts +162 -0
- package/assets/connectors/lingzhu/openclaw-bridge/openclaw.plugin.json +145 -0
- package/assets/connectors/lingzhu/openclaw-bridge/package.json +35 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/cli.ts +180 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/config.ts +196 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/debug-log.ts +111 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/events.ts +4 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/http-handler.ts +1133 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/image-cache.ts +75 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/lingzhu-tools.ts +246 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/transform.ts +541 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/types.ts +131 -0
- package/assets/connectors/lingzhu/openclaw-bridge/tsconfig.json +14 -0
- package/assets/connectors/lingzhu/openclaw.lingzhu.config.template.json +39 -0
- package/bin/ds.js +2048 -166
- package/docs/en/00_QUICK_START.md +152 -0
- package/docs/en/01_SETTINGS_REFERENCE.md +1104 -0
- package/docs/en/02_START_RESEARCH_GUIDE.md +404 -0
- package/docs/en/03_QQ_CONNECTOR_GUIDE.md +325 -0
- package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +216 -0
- package/docs/en/05_TUI_GUIDE.md +141 -0
- package/docs/en/06_RUNTIME_AND_CANVAS.md +679 -0
- package/docs/en/07_MEMORY_AND_MCP.md +253 -0
- package/docs/en/08_FIGURE_STYLE_GUIDE.md +97 -0
- package/docs/en/09_DOCTOR.md +152 -0
- package/docs/en/90_ARCHITECTURE.md +247 -0
- package/docs/en/91_DEVELOPMENT.md +195 -0
- package/docs/en/99_ACKNOWLEDGEMENTS.md +29 -0
- package/docs/zh/00_QUICK_START.md +152 -0
- package/docs/zh/01_SETTINGS_REFERENCE.md +1137 -0
- package/docs/zh/02_START_RESEARCH_GUIDE.md +414 -0
- package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +324 -0
- package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +230 -0
- package/docs/zh/05_TUI_GUIDE.md +128 -0
- package/docs/zh/06_RUNTIME_AND_CANVAS.md +271 -0
- package/docs/zh/07_MEMORY_AND_MCP.md +235 -0
- package/docs/zh/08_FIGURE_STYLE_GUIDE.md +97 -0
- package/docs/zh/09_DOCTOR.md +154 -0
- package/docs/zh/99_ACKNOWLEDGEMENTS.md +29 -0
- package/install.sh +41 -16
- package/package.json +5 -2
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +6 -1
- package/src/deepscientist/artifact/guidance.py +9 -2
- package/src/deepscientist/artifact/service.py +1026 -39
- package/src/deepscientist/bash_exec/monitor.py +27 -5
- package/src/deepscientist/bash_exec/runtime.py +639 -0
- package/src/deepscientist/bash_exec/service.py +99 -16
- package/src/deepscientist/bridges/base.py +3 -0
- package/src/deepscientist/bridges/connectors.py +292 -13
- package/src/deepscientist/channels/qq.py +19 -2
- package/src/deepscientist/channels/relay.py +1 -0
- package/src/deepscientist/cli.py +32 -25
- package/src/deepscientist/config/models.py +28 -2
- package/src/deepscientist/config/service.py +202 -7
- package/src/deepscientist/connector_runtime.py +2 -0
- package/src/deepscientist/daemon/api/handlers.py +68 -6
- package/src/deepscientist/daemon/api/router.py +3 -0
- package/src/deepscientist/daemon/app.py +531 -15
- package/src/deepscientist/doctor.py +511 -0
- package/src/deepscientist/gitops/diff.py +3 -0
- package/src/deepscientist/home.py +26 -2
- package/src/deepscientist/latex_runtime.py +17 -4
- package/src/deepscientist/lingzhu_support.py +182 -0
- package/src/deepscientist/mcp/context.py +3 -1
- package/src/deepscientist/mcp/server.py +55 -2
- package/src/deepscientist/prompts/builder.py +222 -58
- package/src/deepscientist/quest/layout.py +2 -0
- package/src/deepscientist/quest/service.py +133 -14
- package/src/deepscientist/quest/stage_views.py +65 -1
- package/src/deepscientist/runners/codex.py +2 -0
- package/src/deepscientist/runtime_tools/__init__.py +16 -0
- package/src/deepscientist/runtime_tools/builtins.py +19 -0
- package/src/deepscientist/runtime_tools/models.py +29 -0
- package/src/deepscientist/runtime_tools/registry.py +40 -0
- package/src/deepscientist/runtime_tools/service.py +59 -0
- package/src/deepscientist/runtime_tools/tinytex.py +25 -0
- package/src/deepscientist/shared.py +44 -17
- package/src/deepscientist/tinytex.py +276 -0
- package/src/prompts/connectors/lingzhu.md +15 -0
- package/src/prompts/connectors/qq.md +121 -0
- package/src/prompts/system.md +214 -37
- package/src/skills/analysis-campaign/SKILL.md +46 -7
- package/src/skills/baseline/SKILL.md +12 -5
- package/src/skills/decision/SKILL.md +7 -5
- package/src/skills/experiment/SKILL.md +22 -5
- package/src/skills/finalize/SKILL.md +9 -5
- package/src/skills/idea/SKILL.md +6 -5
- package/src/skills/intake-audit/SKILL.md +277 -0
- package/src/skills/intake-audit/references/state-audit-template.md +41 -0
- package/src/skills/rebuttal/SKILL.md +409 -0
- package/src/skills/rebuttal/references/action-plan-template.md +63 -0
- package/src/skills/rebuttal/references/evidence-update-template.md +30 -0
- package/src/skills/rebuttal/references/response-letter-template.md +113 -0
- package/src/skills/rebuttal/references/review-matrix-template.md +55 -0
- package/src/skills/review/SKILL.md +295 -0
- package/src/skills/review/references/experiment-todo-template.md +29 -0
- package/src/skills/review/references/review-report-template.md +83 -0
- package/src/skills/review/references/revision-log-template.md +40 -0
- package/src/skills/scout/SKILL.md +6 -5
- package/src/skills/write/SKILL.md +8 -4
- package/src/tui/dist/components/WelcomePanel.js +17 -43
- package/src/tui/dist/components/messages/BashExecOperationMessage.js +3 -2
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-7v-dHngU.js → AiManusChatView-CZpg376x.js} +127 -597
- package/src/ui/dist/assets/{AnalysisPlugin-B_Xmz-KE.js → AnalysisPlugin-CtHA22g3.js} +1 -1
- package/src/ui/dist/assets/{AutoFigurePlugin-Cko-0tm1.js → AutoFigurePlugin-BSWmLMmF.js} +63 -8
- package/src/ui/dist/assets/{CliPlugin-BsU0ht7q.js → CliPlugin-CJ7jdm_s.js} +43 -609
- package/src/ui/dist/assets/{CodeEditorPlugin-DcMMP0Rt.js → CodeEditorPlugin-DhInVGFf.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-BqoQ5QyY.js → CodeViewerPlugin-D1n8S9r5.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-D7eHNhU6.js → DocViewerPlugin-C4XM_kqk.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-DLJN42T5.js → GitDiffViewerPlugin-W6kS9r6v.js} +1 -1
- package/src/ui/dist/assets/{ImageViewerPlugin-gJMV7MOu.js → ImageViewerPlugin-DPeUx_Oz.js} +5 -6
- package/src/ui/dist/assets/{LabCopilotPanel-B857sfxP.js → LabCopilotPanel-eAelUaub.js} +12 -15
- package/src/ui/dist/assets/LabPlugin-BbOrBxKY.js +2676 -0
- package/src/ui/dist/assets/{LatexPlugin-DWKEo-Wj.js → LatexPlugin-C-HhkVXY.js} +16 -16
- package/src/ui/dist/assets/{MarkdownViewerPlugin-DBzoEmhv.js → MarkdownViewerPlugin-BDIzIBfh.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-DoHc-8vo.js → MarketplacePlugin-DAOJphwr.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-CKjKH-yS.js → NotebookEditor-BsoMvDoU.js} +3 -3
- package/src/ui/dist/assets/{PdfLoader-zFoL0VPo.js → PdfLoader-fiC7RtHf.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-DXPaL9Nt.js → PdfMarkdownPlugin-C5OxZBFK.js} +3 -3
- package/src/ui/dist/assets/{PdfViewerPlugin-DhK8qCFp.js → PdfViewerPlugin-CAbxQebk.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-CdSi6krf.js → SearchPlugin-SE33Lb9B.js} +1 -1
- package/src/ui/dist/assets/{Stepper-V-WiDQJl.js → Stepper-0Av7GfV7.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-hIs1Efiu.js → TextViewerPlugin-Daf2gJDI.js} +4 -4
- package/src/ui/dist/assets/{VNCViewer-DG8b0q2X.js → VNCViewer-BKrMUIOX.js} +9 -10
- package/src/ui/dist/assets/{bibtex-HDac6fVW.js → bibtex-JBdOEe45.js} +1 -1
- package/src/ui/dist/assets/{code-BnBeNxBc.js → code-B0TDFCZz.js} +1 -1
- package/src/ui/dist/assets/{file-content-IRQ3jHb8.js → file-content-3YtrSacz.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-DZoQ9I6r.js → file-diff-panel-CJEg5OG1.js} +1 -1
- package/src/ui/dist/assets/{file-socket-BMCdLc-P.js → file-socket-CYQYdmB1.js} +1 -1
- package/src/ui/dist/assets/{file-utils-CltILB3w.js → file-utils-Cd1C9Ppl.js} +1 -1
- package/src/ui/dist/assets/{image-Boe6ffhu.js → image-B33ctrvC.js} +1 -1
- package/src/ui/dist/assets/{index-2Zf65FZt.js → index-9CLPVeZh.js} +1 -1
- package/src/ui/dist/assets/{index-DZqJ-qAM.js → index-BNQWqmJ2.js} +60 -2154
- package/src/ui/dist/assets/{index-DO43pFZP.js → index-BVXsmS7V.js} +84086 -84365
- package/src/ui/dist/assets/{index-BlplpvE1.js → index-Buw_N1VQ.js} +2 -2
- package/src/ui/dist/assets/{index-Bq2bvfkl.css → index-SwmFAld3.css} +2622 -2619
- package/src/ui/dist/assets/{message-square-mUHn_Ssb.js → message-square-D0cUJ9yU.js} +1 -1
- package/src/ui/dist/assets/{monaco-fe0arNEU.js → monaco-UZLYkp2n.js} +1 -1
- package/src/ui/dist/assets/{popover-D_7i19qU.js → popover-CTeiY-dK.js} +1 -1
- package/src/ui/dist/assets/{project-sync-DyVGrU7H.js → project-sync-Dbs01Xky.js} +2 -8
- package/src/ui/dist/assets/{sigma-BzazRyxQ.js → sigma-CM08S-xT.js} +1 -1
- package/src/ui/dist/assets/{tooltip-DN_yjHFH.js → tooltip-pDtzvU9p.js} +1 -1
- package/src/ui/dist/assets/trash-YvPCP-da.js +32 -0
- package/src/ui/dist/assets/{useCliAccess-DV2L2Qxy.js → useCliAccess-Bavi74Ac.js} +12 -42
- package/src/ui/dist/assets/{useFileDiffOverlay-DyTj-p_V.js → useFileDiffOverlay-CVXY6oeg.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-ozYHtUwq.js → wrap-text-Cf4flRW7.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-BN9MUyCQ.js → zoom-out-Hb0Z1YpT.js} +1 -1
- package/src/ui/dist/index.html +2 -2
- package/uv.lock +1155 -0
- package/assets/fonts/Inter-Variable.ttf +0 -0
- package/assets/fonts/NotoSerifSC-Regular-C94HN_ZN.ttf +0 -0
- package/assets/fonts/NunitoSans-Variable.ttf +0 -0
- package/assets/fonts/Satoshi-Medium-ByP-Zb-9.woff2 +0 -0
- package/assets/fonts/SourceSans3-Variable.ttf +0 -0
- package/assets/fonts/ds-fonts.css +0 -83
- package/src/ui/dist/assets/Inter-Variable-VF2RPR_K.ttf +0 -0
- package/src/ui/dist/assets/LabPlugin-bL7rpic8.js +0 -43
- package/src/ui/dist/assets/NotoSerifSC-Regular-C94HN_ZN-C94HN_ZN.ttf +0 -0
- package/src/ui/dist/assets/NunitoSans-Variable-B_ZymHAd.ttf +0 -0
- package/src/ui/dist/assets/Satoshi-Medium-ByP-Zb-9-GkA34YXu.woff2 +0 -0
- package/src/ui/dist/assets/SourceSans3-Variable-CD-WOsSK.ttf +0 -0
- package/src/ui/dist/assets/info-CcsK_htA.js +0 -18
- package/src/ui/dist/assets/user-plus-BusDx-hF.js +0 -79
|
@@ -91,6 +91,41 @@ class ArtifactService:
|
|
|
91
91
|
self.baselines = BaselineRegistry(home)
|
|
92
92
|
self.quest_service = QuestService(home)
|
|
93
93
|
|
|
94
|
+
def _normalize_evaluation_summary(self, payload: dict[str, Any] | None) -> dict[str, str] | None:
|
|
95
|
+
if not isinstance(payload, dict):
|
|
96
|
+
return None
|
|
97
|
+
normalized: dict[str, str] = {}
|
|
98
|
+
for key in (
|
|
99
|
+
"takeaway",
|
|
100
|
+
"claim_update",
|
|
101
|
+
"baseline_relation",
|
|
102
|
+
"comparability",
|
|
103
|
+
"failure_mode",
|
|
104
|
+
"next_action",
|
|
105
|
+
):
|
|
106
|
+
value = payload.get(key)
|
|
107
|
+
if value is None:
|
|
108
|
+
continue
|
|
109
|
+
text = str(value).strip()
|
|
110
|
+
if text:
|
|
111
|
+
normalized[key] = text
|
|
112
|
+
return normalized or None
|
|
113
|
+
|
|
114
|
+
def _evaluation_summary_markdown_lines(self, payload: dict[str, Any] | None) -> list[str]:
|
|
115
|
+
normalized = self._normalize_evaluation_summary(payload)
|
|
116
|
+
if not normalized:
|
|
117
|
+
return ["- Not recorded."]
|
|
118
|
+
labels = (
|
|
119
|
+
("takeaway", "Takeaway"),
|
|
120
|
+
("claim_update", "Claim Update"),
|
|
121
|
+
("baseline_relation", "Baseline Relation"),
|
|
122
|
+
("comparability", "Comparability"),
|
|
123
|
+
("failure_mode", "Failure Mode"),
|
|
124
|
+
("next_action", "Next Action"),
|
|
125
|
+
)
|
|
126
|
+
lines = [f"- {label}: {normalized[key]}" for key, label in labels if normalized.get(key)]
|
|
127
|
+
return lines or ["- Not recorded."]
|
|
128
|
+
|
|
94
129
|
def _workspace_root_for(self, quest_root: Path, workspace_root: Path | None = None) -> Path:
|
|
95
130
|
if workspace_root is not None:
|
|
96
131
|
return workspace_root
|
|
@@ -387,6 +422,207 @@ class ArtifactService:
|
|
|
387
422
|
write_json(path, normalized)
|
|
388
423
|
return normalized
|
|
389
424
|
|
|
425
|
+
def _analysis_baseline_inventory_path(self, quest_root: Path) -> Path:
|
|
426
|
+
return ensure_dir(quest_root / "artifacts" / "baselines") / "analysis_inventory.json"
|
|
427
|
+
|
|
428
|
+
def _read_analysis_baseline_inventory(self, quest_root: Path) -> dict[str, Any]:
|
|
429
|
+
path = self._analysis_baseline_inventory_path(quest_root)
|
|
430
|
+
payload = read_json(path, {})
|
|
431
|
+
if not isinstance(payload, dict):
|
|
432
|
+
payload = {}
|
|
433
|
+
entries = payload.get("entries") if isinstance(payload.get("entries"), list) else []
|
|
434
|
+
return {
|
|
435
|
+
"schema_version": 1,
|
|
436
|
+
"entries": [dict(item) for item in entries if isinstance(item, dict)],
|
|
437
|
+
"updated_at": payload.get("updated_at"),
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
def _write_analysis_baseline_inventory(self, quest_root: Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
441
|
+
path = self._analysis_baseline_inventory_path(quest_root)
|
|
442
|
+
normalized_entries = payload.get("entries") if isinstance(payload.get("entries"), list) else []
|
|
443
|
+
normalized = {
|
|
444
|
+
"schema_version": 1,
|
|
445
|
+
"entries": [dict(item) for item in normalized_entries if isinstance(item, dict)],
|
|
446
|
+
"updated_at": utc_now(),
|
|
447
|
+
}
|
|
448
|
+
write_json(path, normalized)
|
|
449
|
+
return normalized
|
|
450
|
+
|
|
451
|
+
def _normalize_baseline_root_rel_path(
|
|
452
|
+
self,
|
|
453
|
+
quest_root: Path,
|
|
454
|
+
baseline_root_rel_path: str | None,
|
|
455
|
+
*,
|
|
456
|
+
baseline_id: str | None = None,
|
|
457
|
+
) -> tuple[str | None, str | None]:
|
|
458
|
+
raw = str(baseline_root_rel_path or "").strip()
|
|
459
|
+
if not raw:
|
|
460
|
+
return None, None
|
|
461
|
+
candidate = Path(raw)
|
|
462
|
+
resolved = candidate.resolve() if candidate.is_absolute() else resolve_within(quest_root, raw)
|
|
463
|
+
if not resolved.exists():
|
|
464
|
+
raise FileNotFoundError(f"Baseline root does not exist: {resolved}")
|
|
465
|
+
try:
|
|
466
|
+
relative = resolved.relative_to(quest_root.resolve()).as_posix()
|
|
467
|
+
except ValueError as exc:
|
|
468
|
+
raise ValueError("`baseline_root_rel_path` must stay within quest_root.") from exc
|
|
469
|
+
parts = Path(relative).parts
|
|
470
|
+
if len(parts) < 3 or parts[0] != "baselines" or parts[1] not in {"local", "imported"}:
|
|
471
|
+
raise ValueError(
|
|
472
|
+
"`baseline_root_rel_path` must live under `baselines/local/<baseline_id>/...` or "
|
|
473
|
+
"`baselines/imported/<baseline_id>/...`."
|
|
474
|
+
)
|
|
475
|
+
normalized_baseline_id = str(baseline_id or parts[2]).strip() or None
|
|
476
|
+
if normalized_baseline_id and parts[2] != normalized_baseline_id:
|
|
477
|
+
raise ValueError(
|
|
478
|
+
f"`baseline_root_rel_path` points to baseline `{parts[2]}`, which does not match `{normalized_baseline_id}`."
|
|
479
|
+
)
|
|
480
|
+
return relative, parts[1]
|
|
481
|
+
|
|
482
|
+
@staticmethod
|
|
483
|
+
def _analysis_baseline_label(payload: dict[str, Any]) -> str:
|
|
484
|
+
baseline_id = str(payload.get("baseline_id") or "baseline").strip() or "baseline"
|
|
485
|
+
parts = [f"`{baseline_id}`"]
|
|
486
|
+
variant_id = str(payload.get("variant_id") or "").strip()
|
|
487
|
+
if variant_id:
|
|
488
|
+
parts.append(f"variant `{variant_id}`")
|
|
489
|
+
benchmark = str(payload.get("benchmark") or "").strip()
|
|
490
|
+
split = str(payload.get("split") or "").strip()
|
|
491
|
+
if benchmark and split:
|
|
492
|
+
parts.append(f"benchmark `{benchmark}` / split `{split}`")
|
|
493
|
+
elif benchmark:
|
|
494
|
+
parts.append(f"benchmark `{benchmark}`")
|
|
495
|
+
elif split:
|
|
496
|
+
parts.append(f"split `{split}`")
|
|
497
|
+
reason = str(payload.get("reason") or "").strip()
|
|
498
|
+
if reason:
|
|
499
|
+
parts.append(f"reason: {reason}")
|
|
500
|
+
return " · ".join(parts)
|
|
501
|
+
|
|
502
|
+
def _normalize_required_baselines(self, quest_root: Path, values: list[object] | None) -> list[dict[str, Any]]:
|
|
503
|
+
normalized: list[dict[str, Any]] = []
|
|
504
|
+
for raw in values or []:
|
|
505
|
+
if not isinstance(raw, dict):
|
|
506
|
+
continue
|
|
507
|
+
baseline_id = str(raw.get("baseline_id") or "").strip()
|
|
508
|
+
if not baseline_id:
|
|
509
|
+
continue
|
|
510
|
+
baseline_root_rel_path, storage_mode = self._normalize_baseline_root_rel_path(
|
|
511
|
+
quest_root,
|
|
512
|
+
raw.get("baseline_root_rel_path"),
|
|
513
|
+
baseline_id=baseline_id,
|
|
514
|
+
)
|
|
515
|
+
normalized.append(
|
|
516
|
+
{
|
|
517
|
+
"baseline_id": baseline_id,
|
|
518
|
+
"variant_id": str(raw.get("variant_id") or "").strip() or None,
|
|
519
|
+
"reason": str(raw.get("reason") or "").strip() or None,
|
|
520
|
+
"benchmark": str(raw.get("benchmark") or "").strip() or None,
|
|
521
|
+
"split": str(raw.get("split") or "").strip() or None,
|
|
522
|
+
"baseline_root_rel_path": baseline_root_rel_path,
|
|
523
|
+
"storage_mode": storage_mode or (str(raw.get("storage_mode") or "").strip() or None),
|
|
524
|
+
"usage_scope": "supplementary",
|
|
525
|
+
}
|
|
526
|
+
)
|
|
527
|
+
return normalized
|
|
528
|
+
|
|
529
|
+
def _normalize_comparison_baselines(self, quest_root: Path, values: list[object] | None) -> list[dict[str, Any]]:
|
|
530
|
+
normalized: list[dict[str, Any]] = []
|
|
531
|
+
for raw in values or []:
|
|
532
|
+
if not isinstance(raw, dict):
|
|
533
|
+
continue
|
|
534
|
+
baseline_id = str(raw.get("baseline_id") or "").strip()
|
|
535
|
+
if not baseline_id:
|
|
536
|
+
continue
|
|
537
|
+
baseline_root_rel_path, storage_mode = self._normalize_baseline_root_rel_path(
|
|
538
|
+
quest_root,
|
|
539
|
+
raw.get("baseline_root_rel_path"),
|
|
540
|
+
baseline_id=baseline_id,
|
|
541
|
+
)
|
|
542
|
+
metrics_summary = (
|
|
543
|
+
normalize_metrics_summary(raw.get("metrics_summary"))
|
|
544
|
+
if isinstance(raw.get("metrics_summary"), dict)
|
|
545
|
+
else {}
|
|
546
|
+
)
|
|
547
|
+
normalized.append(
|
|
548
|
+
{
|
|
549
|
+
"baseline_id": baseline_id,
|
|
550
|
+
"variant_id": str(raw.get("variant_id") or "").strip() or None,
|
|
551
|
+
"benchmark": str(raw.get("benchmark") or "").strip() or None,
|
|
552
|
+
"split": str(raw.get("split") or "").strip() or None,
|
|
553
|
+
"reason": str(raw.get("reason") or "").strip() or None,
|
|
554
|
+
"metrics_summary": metrics_summary,
|
|
555
|
+
"evidence_paths": [
|
|
556
|
+
str(item).strip() for item in (raw.get("evidence_paths") or []) if str(item).strip()
|
|
557
|
+
],
|
|
558
|
+
"baseline_root_rel_path": baseline_root_rel_path,
|
|
559
|
+
"storage_mode": storage_mode or (str(raw.get("storage_mode") or "").strip() or None),
|
|
560
|
+
"usage_scope": "supplementary",
|
|
561
|
+
"published": bool(raw.get("published", False)),
|
|
562
|
+
"published_entry_id": str(raw.get("published_entry_id") or "").strip() or None,
|
|
563
|
+
"status": str(raw.get("status") or "registered").strip() or "registered",
|
|
564
|
+
}
|
|
565
|
+
)
|
|
566
|
+
return normalized
|
|
567
|
+
|
|
568
|
+
@staticmethod
|
|
569
|
+
def _analysis_inventory_entry_key(payload: dict[str, Any]) -> tuple[str, str, str, str, str, str]:
|
|
570
|
+
origin = dict(payload.get("origin") or {}) if isinstance(payload.get("origin"), dict) else {}
|
|
571
|
+
return (
|
|
572
|
+
str(payload.get("baseline_id") or "").strip(),
|
|
573
|
+
str(payload.get("variant_id") or "").strip(),
|
|
574
|
+
str(origin.get("campaign_id") or "").strip(),
|
|
575
|
+
str(origin.get("slice_id") or "").strip(),
|
|
576
|
+
str(payload.get("benchmark") or "").strip(),
|
|
577
|
+
str(payload.get("split") or "").strip(),
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
@staticmethod
|
|
581
|
+
def _merge_analysis_inventory_entry(existing: dict[str, Any], incoming: dict[str, Any]) -> dict[str, Any]:
|
|
582
|
+
merged = dict(existing)
|
|
583
|
+
for key, value in incoming.items():
|
|
584
|
+
if value is None:
|
|
585
|
+
continue
|
|
586
|
+
if isinstance(value, str) and not value.strip():
|
|
587
|
+
continue
|
|
588
|
+
if isinstance(value, (list, dict)) and not value:
|
|
589
|
+
continue
|
|
590
|
+
merged[key] = value
|
|
591
|
+
merged["updated_at"] = utc_now()
|
|
592
|
+
merged.setdefault("created_at", existing.get("created_at") or incoming.get("created_at") or utc_now())
|
|
593
|
+
return merged
|
|
594
|
+
|
|
595
|
+
def _upsert_analysis_baseline_inventory(self, quest_root: Path, entries: list[dict[str, Any]]) -> dict[str, Any]:
|
|
596
|
+
inventory = self._read_analysis_baseline_inventory(quest_root)
|
|
597
|
+
existing_entries = [dict(item) for item in (inventory.get("entries") or []) if isinstance(item, dict)]
|
|
598
|
+
by_key = {
|
|
599
|
+
self._analysis_inventory_entry_key(item): dict(item)
|
|
600
|
+
for item in existing_entries
|
|
601
|
+
if str(item.get("baseline_id") or "").strip()
|
|
602
|
+
}
|
|
603
|
+
for raw in entries:
|
|
604
|
+
if not isinstance(raw, dict):
|
|
605
|
+
continue
|
|
606
|
+
entry = dict(raw)
|
|
607
|
+
if not str(entry.get("baseline_id") or "").strip():
|
|
608
|
+
continue
|
|
609
|
+
key = self._analysis_inventory_entry_key(entry)
|
|
610
|
+
current = by_key.get(key)
|
|
611
|
+
if current is None:
|
|
612
|
+
stamped = dict(entry)
|
|
613
|
+
stamped.setdefault("created_at", utc_now())
|
|
614
|
+
stamped["updated_at"] = utc_now()
|
|
615
|
+
by_key[key] = stamped
|
|
616
|
+
continue
|
|
617
|
+
by_key[key] = self._merge_analysis_inventory_entry(current, entry)
|
|
618
|
+
normalized = self._write_analysis_baseline_inventory(
|
|
619
|
+
quest_root,
|
|
620
|
+
{
|
|
621
|
+
"entries": list(by_key.values()),
|
|
622
|
+
},
|
|
623
|
+
)
|
|
624
|
+
return normalized
|
|
625
|
+
|
|
390
626
|
def _paper_root(self, quest_root: Path) -> Path:
|
|
391
627
|
return ensure_dir(quest_root / "paper")
|
|
392
628
|
|
|
@@ -405,6 +641,114 @@ class ArtifactService:
|
|
|
405
641
|
def _paper_bundle_manifest_path(self, quest_root: Path) -> Path:
|
|
406
642
|
return self._paper_root(quest_root) / "paper_bundle_manifest.json"
|
|
407
643
|
|
|
644
|
+
def _paper_baseline_inventory_path(self, quest_root: Path) -> Path:
|
|
645
|
+
return self._paper_root(quest_root) / "baseline_inventory.json"
|
|
646
|
+
|
|
647
|
+
def _open_source_root(self, quest_root: Path) -> Path:
|
|
648
|
+
return ensure_dir(quest_root / "release" / "open_source")
|
|
649
|
+
|
|
650
|
+
def _open_source_manifest_path(self, quest_root: Path) -> Path:
|
|
651
|
+
return self._open_source_root(quest_root) / "manifest.json"
|
|
652
|
+
|
|
653
|
+
def _open_source_cleanup_plan_path(self, quest_root: Path) -> Path:
|
|
654
|
+
return self._open_source_root(quest_root) / "cleanup_plan.md"
|
|
655
|
+
|
|
656
|
+
def _open_source_include_paths_path(self, quest_root: Path) -> Path:
|
|
657
|
+
return self._open_source_root(quest_root) / "include_paths.json"
|
|
658
|
+
|
|
659
|
+
def _open_source_exclude_paths_path(self, quest_root: Path) -> Path:
|
|
660
|
+
return self._open_source_root(quest_root) / "exclude_paths.json"
|
|
661
|
+
|
|
662
|
+
def _write_paper_baseline_inventory(self, quest_root: Path) -> dict[str, Any]:
|
|
663
|
+
quest_yaml = self.quest_service.read_quest_yaml(quest_root)
|
|
664
|
+
confirmed_baseline_ref = (
|
|
665
|
+
dict(quest_yaml.get("confirmed_baseline_ref") or {})
|
|
666
|
+
if isinstance(quest_yaml.get("confirmed_baseline_ref"), dict)
|
|
667
|
+
else None
|
|
668
|
+
)
|
|
669
|
+
analysis_inventory = self._read_analysis_baseline_inventory(quest_root)
|
|
670
|
+
payload = {
|
|
671
|
+
"schema_version": 1,
|
|
672
|
+
"canonical_baseline_ref": confirmed_baseline_ref,
|
|
673
|
+
"supplementary_baselines": [
|
|
674
|
+
dict(item) for item in (analysis_inventory.get("entries") or []) if isinstance(item, dict)
|
|
675
|
+
],
|
|
676
|
+
"updated_at": utc_now(),
|
|
677
|
+
}
|
|
678
|
+
write_json(self._paper_baseline_inventory_path(quest_root), payload)
|
|
679
|
+
return payload
|
|
680
|
+
|
|
681
|
+
def _ensure_open_source_prep(
|
|
682
|
+
self,
|
|
683
|
+
quest_root: Path,
|
|
684
|
+
*,
|
|
685
|
+
source_branch: str | None,
|
|
686
|
+
source_bundle_manifest_path: str,
|
|
687
|
+
baseline_inventory_path: str,
|
|
688
|
+
) -> dict[str, Any]:
|
|
689
|
+
root = self._open_source_root(quest_root)
|
|
690
|
+
cleanup_plan_path = self._open_source_cleanup_plan_path(quest_root)
|
|
691
|
+
include_paths_path = self._open_source_include_paths_path(quest_root)
|
|
692
|
+
exclude_paths_path = self._open_source_exclude_paths_path(quest_root)
|
|
693
|
+
manifest_path = self._open_source_manifest_path(quest_root)
|
|
694
|
+
if not cleanup_plan_path.exists():
|
|
695
|
+
write_text(
|
|
696
|
+
cleanup_plan_path,
|
|
697
|
+
"\n".join(
|
|
698
|
+
[
|
|
699
|
+
"# Open Source Cleanup Plan",
|
|
700
|
+
"",
|
|
701
|
+
"## Goal",
|
|
702
|
+
"",
|
|
703
|
+
"Prepare a clean public code branch from the finalized paper line.",
|
|
704
|
+
"",
|
|
705
|
+
"## Keep",
|
|
706
|
+
"",
|
|
707
|
+
"- Core training / evaluation code needed to reproduce the public results.",
|
|
708
|
+
"",
|
|
709
|
+
"## Remove Or Private",
|
|
710
|
+
"",
|
|
711
|
+
"- Temporary logs, scratch files, local secrets, and unrelated experimental debris.",
|
|
712
|
+
"",
|
|
713
|
+
"## Before Release",
|
|
714
|
+
"",
|
|
715
|
+
"- Confirm README, license, and benchmark instructions are complete.",
|
|
716
|
+
"- Confirm only necessary files remain in scope.",
|
|
717
|
+
"",
|
|
718
|
+
]
|
|
719
|
+
).rstrip()
|
|
720
|
+
+ "\n",
|
|
721
|
+
)
|
|
722
|
+
if not include_paths_path.exists():
|
|
723
|
+
write_json(include_paths_path, {"paths": []})
|
|
724
|
+
if not exclude_paths_path.exists():
|
|
725
|
+
write_json(exclude_paths_path, {"paths": []})
|
|
726
|
+
existing = read_json(manifest_path, {})
|
|
727
|
+
existing = existing if isinstance(existing, dict) else {}
|
|
728
|
+
manifest = {
|
|
729
|
+
**existing,
|
|
730
|
+
"schema_version": 1,
|
|
731
|
+
"status": str(existing.get("status") or "draft").strip() or "draft",
|
|
732
|
+
"source_branch": str(existing.get("source_branch") or source_branch or "").strip() or None,
|
|
733
|
+
"release_branch": str(existing.get("release_branch") or "").strip() or None,
|
|
734
|
+
"source_bundle_manifest_path": str(
|
|
735
|
+
existing.get("source_bundle_manifest_path") or source_bundle_manifest_path or ""
|
|
736
|
+
).strip()
|
|
737
|
+
or source_bundle_manifest_path,
|
|
738
|
+
"baseline_inventory_path": str(existing.get("baseline_inventory_path") or baseline_inventory_path or "").strip()
|
|
739
|
+
or baseline_inventory_path,
|
|
740
|
+
"cleanup_plan_path": str(existing.get("cleanup_plan_path") or "release/open_source/cleanup_plan.md").strip()
|
|
741
|
+
or "release/open_source/cleanup_plan.md",
|
|
742
|
+
"include_paths_path": str(existing.get("include_paths_path") or "release/open_source/include_paths.json").strip()
|
|
743
|
+
or "release/open_source/include_paths.json",
|
|
744
|
+
"exclude_paths_path": str(existing.get("exclude_paths_path") or "release/open_source/exclude_paths.json").strip()
|
|
745
|
+
or "release/open_source/exclude_paths.json",
|
|
746
|
+
"created_at": existing.get("created_at") or utc_now(),
|
|
747
|
+
"updated_at": utc_now(),
|
|
748
|
+
}
|
|
749
|
+
write_json(manifest_path, manifest)
|
|
750
|
+
return manifest
|
|
751
|
+
|
|
408
752
|
def _next_paper_outline_id(self, quest_root: Path) -> str:
|
|
409
753
|
max_index = 0
|
|
410
754
|
for root in (self._paper_outline_candidates_root(quest_root), self._paper_outline_revisions_root(quest_root)):
|
|
@@ -422,6 +766,45 @@ class ArtifactService:
|
|
|
422
766
|
def _normalize_string_list(values: list[object] | None) -> list[str]:
|
|
423
767
|
return [str(item).strip() for item in (values or []) if str(item).strip()]
|
|
424
768
|
|
|
769
|
+
def _normalize_campaign_origin(self, payload: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
770
|
+
if not isinstance(payload, dict):
|
|
771
|
+
return None
|
|
772
|
+
origin_kind = str(payload.get("kind") or "analysis").strip().lower() or "analysis"
|
|
773
|
+
normalized = {
|
|
774
|
+
"kind": origin_kind,
|
|
775
|
+
"reason": str(payload.get("reason") or "").strip() or None,
|
|
776
|
+
"source_artifact_id": str(payload.get("source_artifact_id") or "").strip() or None,
|
|
777
|
+
"source_outline_ref": str(payload.get("source_outline_ref") or "").strip() or None,
|
|
778
|
+
"source_review_round": str(payload.get("source_review_round") or "").strip() or None,
|
|
779
|
+
"reviewer_item_ids": self._normalize_string_list(payload.get("reviewer_item_ids")),
|
|
780
|
+
}
|
|
781
|
+
if not any(value for key, value in normalized.items() if key != "kind"):
|
|
782
|
+
normalized["reason"] = None
|
|
783
|
+
return normalized
|
|
784
|
+
|
|
785
|
+
def _normalize_campaign_todo_items(self, todo_items: list[dict[str, Any]] | None) -> list[dict[str, Any]]:
|
|
786
|
+
normalized_items: list[dict[str, Any]] = []
|
|
787
|
+
for raw in todo_items or []:
|
|
788
|
+
if not isinstance(raw, dict):
|
|
789
|
+
continue
|
|
790
|
+
normalized_items.append(
|
|
791
|
+
{
|
|
792
|
+
"todo_id": str(raw.get("todo_id") or raw.get("slice_id") or "").strip() or None,
|
|
793
|
+
"slice_id": str(raw.get("slice_id") or "").strip() or None,
|
|
794
|
+
"title": str(raw.get("title") or "").strip() or None,
|
|
795
|
+
"status": str(raw.get("status") or "pending").strip() or "pending",
|
|
796
|
+
"research_question": str(raw.get("research_question") or "").strip() or None,
|
|
797
|
+
"experimental_design": str(raw.get("experimental_design") or "").strip() or None,
|
|
798
|
+
"completion_condition": str(raw.get("completion_condition") or "").strip() or None,
|
|
799
|
+
"why_now": str(raw.get("why_now") or "").strip() or None,
|
|
800
|
+
"success_criteria": str(raw.get("success_criteria") or "").strip() or None,
|
|
801
|
+
"abandonment_criteria": str(raw.get("abandonment_criteria") or "").strip() or None,
|
|
802
|
+
"reviewer_item_ids": self._normalize_string_list(raw.get("reviewer_item_ids")),
|
|
803
|
+
"manuscript_targets": self._normalize_string_list(raw.get("manuscript_targets")),
|
|
804
|
+
}
|
|
805
|
+
)
|
|
806
|
+
return normalized_items
|
|
807
|
+
|
|
425
808
|
def _normalize_paper_outline_record(
|
|
426
809
|
self,
|
|
427
810
|
*,
|
|
@@ -1073,6 +1456,80 @@ class ArtifactService:
|
|
|
1073
1456
|
]
|
|
1074
1457
|
return candidates[-1] if candidates else None
|
|
1075
1458
|
|
|
1459
|
+
def _latest_branch_idea_id(self, quest_root: Path, branch_name: str) -> str | None:
|
|
1460
|
+
normalized_branch = str(branch_name or "").strip()
|
|
1461
|
+
if not normalized_branch:
|
|
1462
|
+
return None
|
|
1463
|
+
latest_idea = self._latest_idea_for_branch(quest_root, normalized_branch)
|
|
1464
|
+
if isinstance(latest_idea, dict):
|
|
1465
|
+
candidate = str(latest_idea.get("idea_id") or "").strip()
|
|
1466
|
+
if candidate:
|
|
1467
|
+
return candidate
|
|
1468
|
+
latest_main_run = self._latest_main_run_for_branch(quest_root, normalized_branch)
|
|
1469
|
+
if isinstance(latest_main_run, dict):
|
|
1470
|
+
candidate = str(latest_main_run.get("idea_id") or "").strip()
|
|
1471
|
+
if candidate:
|
|
1472
|
+
return candidate
|
|
1473
|
+
latest_match: tuple[str, int, str] | None = None
|
|
1474
|
+
latest_candidate: str | None = None
|
|
1475
|
+
for item in self.quest_service._collect_artifacts(quest_root):
|
|
1476
|
+
payload = dict(item.get("payload") or {}) if isinstance(item.get("payload"), dict) else {}
|
|
1477
|
+
if not payload:
|
|
1478
|
+
continue
|
|
1479
|
+
if str(payload.get("branch") or "").strip() != normalized_branch:
|
|
1480
|
+
continue
|
|
1481
|
+
candidate = str(payload.get("idea_id") or "").strip()
|
|
1482
|
+
if not candidate:
|
|
1483
|
+
continue
|
|
1484
|
+
artifact_path = str(item.get("path") or "")
|
|
1485
|
+
try:
|
|
1486
|
+
artifact_mtime_ns = Path(artifact_path).stat().st_mtime_ns if artifact_path else 0
|
|
1487
|
+
except OSError:
|
|
1488
|
+
artifact_mtime_ns = 0
|
|
1489
|
+
sort_key = (
|
|
1490
|
+
str(payload.get("updated_at") or payload.get("created_at") or ""),
|
|
1491
|
+
artifact_mtime_ns,
|
|
1492
|
+
artifact_path,
|
|
1493
|
+
)
|
|
1494
|
+
if latest_match is None or sort_key > latest_match:
|
|
1495
|
+
latest_match = sort_key
|
|
1496
|
+
latest_candidate = candidate
|
|
1497
|
+
if latest_match is not None and latest_candidate:
|
|
1498
|
+
return latest_candidate
|
|
1499
|
+
return None
|
|
1500
|
+
|
|
1501
|
+
def _resolve_analysis_parent_context(
|
|
1502
|
+
self,
|
|
1503
|
+
quest_root: Path,
|
|
1504
|
+
*,
|
|
1505
|
+
state: dict[str, Any],
|
|
1506
|
+
) -> tuple[str, Path, str | None]:
|
|
1507
|
+
current_root_raw = str(state.get("current_workspace_root") or "").strip()
|
|
1508
|
+
head_root_raw = str(state.get("research_head_worktree_root") or "").strip()
|
|
1509
|
+
parent_worktree_root: Path | None = None
|
|
1510
|
+
for raw in (current_root_raw, head_root_raw):
|
|
1511
|
+
if not raw:
|
|
1512
|
+
continue
|
|
1513
|
+
candidate = Path(raw)
|
|
1514
|
+
if candidate.exists():
|
|
1515
|
+
parent_worktree_root = candidate
|
|
1516
|
+
break
|
|
1517
|
+
if parent_worktree_root is None:
|
|
1518
|
+
parent_worktree_root = self._workspace_root_for(quest_root)
|
|
1519
|
+
|
|
1520
|
+
parent_branch = (
|
|
1521
|
+
str(state.get("current_workspace_branch") or "").strip()
|
|
1522
|
+
or str(state.get("research_head_branch") or "").strip()
|
|
1523
|
+
or current_branch(parent_worktree_root)
|
|
1524
|
+
or current_branch(self._workspace_root_for(quest_root))
|
|
1525
|
+
)
|
|
1526
|
+
parent_branch = str(parent_branch or "").strip()
|
|
1527
|
+
if not parent_branch:
|
|
1528
|
+
raise ValueError("Unable to resolve a parent branch for the analysis campaign.")
|
|
1529
|
+
|
|
1530
|
+
idea_id = self._latest_branch_idea_id(quest_root, parent_branch) or str(state.get("active_idea_id") or "").strip() or None
|
|
1531
|
+
return parent_branch, parent_worktree_root, idea_id
|
|
1532
|
+
|
|
1076
1533
|
def _idea_parent_branch(self, record: dict[str, Any] | None) -> str | None:
|
|
1077
1534
|
if not isinstance(record, dict) or not record:
|
|
1078
1535
|
return None
|
|
@@ -1357,6 +1814,111 @@ class ArtifactService:
|
|
|
1357
1814
|
"branches": branches,
|
|
1358
1815
|
}
|
|
1359
1816
|
|
|
1817
|
+
def resolve_runtime_refs(self, quest_root: Path) -> dict[str, Any]:
|
|
1818
|
+
state = self.quest_service.read_research_state(quest_root)
|
|
1819
|
+
snapshot = self.quest_service.snapshot(self._quest_id(quest_root))
|
|
1820
|
+
active_campaign_id = str(state.get("active_analysis_campaign_id") or "").strip() or None
|
|
1821
|
+
analysis_parent_branch = str(state.get("analysis_parent_branch") or "").strip() or None
|
|
1822
|
+
current_workspace_branch = str(state.get("current_workspace_branch") or "").strip() or None
|
|
1823
|
+
research_head_branch = str(state.get("research_head_branch") or "").strip() or None
|
|
1824
|
+
canonical_branch = analysis_parent_branch or current_workspace_branch or research_head_branch
|
|
1825
|
+
latest_main_run = self._latest_main_run_for_branch(quest_root, canonical_branch or "")
|
|
1826
|
+
selected_outline = read_json(self._paper_selected_outline_path(quest_root), {})
|
|
1827
|
+
selected_outline = selected_outline if isinstance(selected_outline, dict) else {}
|
|
1828
|
+
active_campaign = (
|
|
1829
|
+
self._read_analysis_manifest(quest_root, active_campaign_id)
|
|
1830
|
+
if active_campaign_id
|
|
1831
|
+
else {}
|
|
1832
|
+
)
|
|
1833
|
+
active_campaign = active_campaign if isinstance(active_campaign, dict) else {}
|
|
1834
|
+
latest_paths = (
|
|
1835
|
+
dict(latest_main_run.get("paths") or {})
|
|
1836
|
+
if isinstance(latest_main_run, dict) and isinstance(latest_main_run.get("paths"), dict)
|
|
1837
|
+
else {}
|
|
1838
|
+
)
|
|
1839
|
+
return {
|
|
1840
|
+
"ok": True,
|
|
1841
|
+
"active_idea_id": str(state.get("active_idea_id") or "").strip() or None,
|
|
1842
|
+
"research_head_branch": research_head_branch,
|
|
1843
|
+
"research_head_worktree_root": str(state.get("research_head_worktree_root") or "").strip() or None,
|
|
1844
|
+
"current_workspace_branch": current_workspace_branch,
|
|
1845
|
+
"current_workspace_root": str(state.get("current_workspace_root") or "").strip() or None,
|
|
1846
|
+
"analysis_parent_branch": analysis_parent_branch,
|
|
1847
|
+
"analysis_parent_worktree_root": str(state.get("analysis_parent_worktree_root") or "").strip() or None,
|
|
1848
|
+
"current_canonical_branch": canonical_branch,
|
|
1849
|
+
"active_analysis_campaign_id": active_campaign_id,
|
|
1850
|
+
"active_campaign_title": str(active_campaign.get("title") or "").strip() or None,
|
|
1851
|
+
"next_pending_slice_id": str(state.get("next_pending_slice_id") or "").strip() or None,
|
|
1852
|
+
"latest_main_run_id": str((latest_main_run or {}).get("run_id") or "").strip() or None,
|
|
1853
|
+
"latest_main_run_branch": str((latest_main_run or {}).get("branch") or "").strip() or None,
|
|
1854
|
+
"latest_main_result_json": str(latest_paths.get("result_json") or "").strip() or None,
|
|
1855
|
+
"selected_outline_ref": str(selected_outline.get("outline_id") or "").strip() or None,
|
|
1856
|
+
"default_reply_interaction_id": str(snapshot.get("default_reply_interaction_id") or "").strip() or None,
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
def get_analysis_campaign(self, quest_root: Path, campaign_id: str | None = None) -> dict[str, Any]:
|
|
1860
|
+
resolved_campaign_id = str(campaign_id or "").strip()
|
|
1861
|
+
if not resolved_campaign_id or resolved_campaign_id == "active":
|
|
1862
|
+
state = self.quest_service.read_research_state(quest_root)
|
|
1863
|
+
resolved_campaign_id = str(state.get("active_analysis_campaign_id") or "").strip()
|
|
1864
|
+
if not resolved_campaign_id:
|
|
1865
|
+
raise ValueError("No active analysis campaign is available.")
|
|
1866
|
+
manifest = self._read_analysis_manifest(quest_root, resolved_campaign_id)
|
|
1867
|
+
slices = [dict(item) for item in (manifest.get("slices") or []) if isinstance(item, dict)]
|
|
1868
|
+
pending_slices = [item for item in slices if str(item.get("status") or "pending").strip() == "pending"]
|
|
1869
|
+
completed_slices = [item for item in slices if str(item.get("status") or "").strip() != "pending"]
|
|
1870
|
+
next_pending_slice = pending_slices[0] if pending_slices else None
|
|
1871
|
+
return {
|
|
1872
|
+
"ok": True,
|
|
1873
|
+
"campaign_id": resolved_campaign_id,
|
|
1874
|
+
"title": str(manifest.get("title") or "").strip() or None,
|
|
1875
|
+
"goal": str(manifest.get("goal") or "").strip() or None,
|
|
1876
|
+
"active_idea_id": str(manifest.get("active_idea_id") or "").strip() or None,
|
|
1877
|
+
"parent_run_id": str(manifest.get("parent_run_id") or "").strip() or None,
|
|
1878
|
+
"parent_branch": str(manifest.get("parent_branch") or "").strip() or None,
|
|
1879
|
+
"parent_worktree_root": str(manifest.get("parent_worktree_root") or "").strip() or None,
|
|
1880
|
+
"selected_outline_ref": str(manifest.get("selected_outline_ref") or "").strip() or None,
|
|
1881
|
+
"campaign_origin": dict(manifest.get("campaign_origin") or {}) if isinstance(manifest.get("campaign_origin"), dict) else None,
|
|
1882
|
+
"todo_items": [dict(item) for item in (manifest.get("todo_items") or []) if isinstance(item, dict)],
|
|
1883
|
+
"slices": slices,
|
|
1884
|
+
"next_pending_slice_id": str((next_pending_slice or {}).get("slice_id") or "").strip() or None,
|
|
1885
|
+
"pending_slice_count": len(pending_slices),
|
|
1886
|
+
"completed_slice_count": len(completed_slices),
|
|
1887
|
+
"manifest": manifest,
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
def list_paper_outlines(self, quest_root: Path) -> dict[str, Any]:
|
|
1891
|
+
selected_outline = read_json(self._paper_selected_outline_path(quest_root), {})
|
|
1892
|
+
selected_outline = selected_outline if isinstance(selected_outline, dict) else {}
|
|
1893
|
+
outlines: list[dict[str, Any]] = []
|
|
1894
|
+
for status, root in (
|
|
1895
|
+
("candidate", self._paper_outline_candidates_root(quest_root)),
|
|
1896
|
+
("revised", self._paper_outline_revisions_root(quest_root)),
|
|
1897
|
+
):
|
|
1898
|
+
for path in sorted(root.glob("outline-*.json")):
|
|
1899
|
+
record = read_json(path, {})
|
|
1900
|
+
if not isinstance(record, dict) or not record:
|
|
1901
|
+
continue
|
|
1902
|
+
outline_id = str(record.get("outline_id") or path.stem).strip() or path.stem
|
|
1903
|
+
outlines.append(
|
|
1904
|
+
{
|
|
1905
|
+
"outline_id": outline_id,
|
|
1906
|
+
"title": str(record.get("title") or outline_id).strip() or outline_id,
|
|
1907
|
+
"status": str(record.get("status") or status).strip() or status,
|
|
1908
|
+
"review_result": str(record.get("review_result") or "").strip() or None,
|
|
1909
|
+
"path": str(path),
|
|
1910
|
+
"is_selected": outline_id == str(selected_outline.get("outline_id") or "").strip(),
|
|
1911
|
+
}
|
|
1912
|
+
)
|
|
1913
|
+
outlines.sort(key=lambda item: (str(item.get("outline_id") or ""), str(item.get("status") or "")))
|
|
1914
|
+
return {
|
|
1915
|
+
"ok": True,
|
|
1916
|
+
"selected_outline_ref": str(selected_outline.get("outline_id") or "").strip() or None,
|
|
1917
|
+
"selected_outline": selected_outline or None,
|
|
1918
|
+
"count": len(outlines),
|
|
1919
|
+
"outlines": outlines,
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1360
1922
|
def _previous_primary_best(
|
|
1361
1923
|
self,
|
|
1362
1924
|
quest_root: Path,
|
|
@@ -2149,6 +2711,7 @@ class ArtifactService:
|
|
|
2149
2711
|
status: str = "completed",
|
|
2150
2712
|
baseline_id: str | None = None,
|
|
2151
2713
|
baseline_variant_id: str | None = None,
|
|
2714
|
+
evaluation_summary: dict[str, Any] | None = None,
|
|
2152
2715
|
) -> dict[str, Any]:
|
|
2153
2716
|
self._require_baseline_gate_open(quest_root, action="record_main_experiment")
|
|
2154
2717
|
state = self.quest_service.read_research_state(quest_root)
|
|
@@ -2227,6 +2790,7 @@ class ArtifactService:
|
|
|
2227
2790
|
resolved_config_paths = [str(item).strip() for item in (config_paths or []) if str(item).strip()]
|
|
2228
2791
|
resolved_notes = [str(item).strip() for item in (notes or []) if str(item).strip()]
|
|
2229
2792
|
normalized_dataset_scope = str(dataset_scope or "full").strip().lower() or "full"
|
|
2793
|
+
normalized_evaluation_summary = self._normalize_evaluation_summary(evaluation_summary)
|
|
2230
2794
|
primary = comparisons.get("primary") if isinstance(comparisons, dict) else {}
|
|
2231
2795
|
primary_metric_id = str(progress_eval.get("primary_metric_id") or comparisons.get("primary_metric_id") or "").strip() or None
|
|
2232
2796
|
primary_value = primary.get("run_value") if isinstance(primary, dict) else None
|
|
@@ -2336,6 +2900,8 @@ class ArtifactService:
|
|
|
2336
2900
|
if resolved_notes:
|
|
2337
2901
|
run_lines.extend(["", "## Notes", ""])
|
|
2338
2902
|
run_lines.extend([f"- {item}" for item in resolved_notes])
|
|
2903
|
+
run_lines.extend(["", "## Evaluation Summary", ""])
|
|
2904
|
+
run_lines.extend(self._evaluation_summary_markdown_lines(normalized_evaluation_summary))
|
|
2339
2905
|
run_lines.extend(
|
|
2340
2906
|
[
|
|
2341
2907
|
"",
|
|
@@ -2384,6 +2950,7 @@ class ArtifactService:
|
|
|
2384
2950
|
key: value for key, value in comparisons.items() if key != "primary"
|
|
2385
2951
|
},
|
|
2386
2952
|
"progress_eval": progress_eval,
|
|
2953
|
+
"evaluation_summary": normalized_evaluation_summary,
|
|
2387
2954
|
"delivery_policy": delivery_policy,
|
|
2388
2955
|
"startup_contract": delivery_policy.get("startup_contract") or None,
|
|
2389
2956
|
"evidence_paths": resolved_evidence_paths,
|
|
@@ -2424,6 +2991,7 @@ class ArtifactService:
|
|
|
2424
2991
|
"recommended_next_route": delivery_policy.get("recommended_next_route"),
|
|
2425
2992
|
"changed_file_count": len(resolved_changed_files),
|
|
2426
2993
|
"evidence_count": len(resolved_evidence_paths),
|
|
2994
|
+
"evaluation_summary": normalized_evaluation_summary,
|
|
2427
2995
|
},
|
|
2428
2996
|
"delivery_policy": delivery_policy,
|
|
2429
2997
|
"startup_contract": delivery_policy.get("startup_contract") or None,
|
|
@@ -2439,6 +3007,7 @@ class ArtifactService:
|
|
|
2439
3007
|
key: value for key, value in comparisons.items() if key != "primary"
|
|
2440
3008
|
},
|
|
2441
3009
|
"progress_eval": progress_eval,
|
|
3010
|
+
"evaluation_summary": normalized_evaluation_summary,
|
|
2442
3011
|
"files_changed": resolved_changed_files,
|
|
2443
3012
|
"evidence_paths": resolved_evidence_paths,
|
|
2444
3013
|
"verdict": verdict,
|
|
@@ -2475,6 +3044,7 @@ class ArtifactService:
|
|
|
2475
3044
|
"breakthrough_level": progress_eval.get("breakthrough_level"),
|
|
2476
3045
|
"need_research_paper": delivery_policy.get("need_research_paper"),
|
|
2477
3046
|
"recommended_next_route": delivery_policy.get("recommended_next_route"),
|
|
3047
|
+
"evaluation_summary": normalized_evaluation_summary,
|
|
2478
3048
|
}
|
|
2479
3049
|
],
|
|
2480
3050
|
)
|
|
@@ -2497,6 +3067,7 @@ class ArtifactService:
|
|
|
2497
3067
|
key: value for key, value in comparisons.items() if key != "primary"
|
|
2498
3068
|
},
|
|
2499
3069
|
"progress_eval": progress_eval,
|
|
3070
|
+
"evaluation_summary": normalized_evaluation_summary,
|
|
2500
3071
|
"delivery_policy": delivery_policy,
|
|
2501
3072
|
}
|
|
2502
3073
|
|
|
@@ -2508,6 +3079,7 @@ class ArtifactService:
|
|
|
2508
3079
|
campaign_goal: str,
|
|
2509
3080
|
parent_run_id: str | None = None,
|
|
2510
3081
|
slices: list[dict[str, Any]],
|
|
3082
|
+
campaign_origin: dict[str, Any] | None = None,
|
|
2511
3083
|
selected_outline_ref: str | None = None,
|
|
2512
3084
|
research_questions: list[str] | None = None,
|
|
2513
3085
|
experimental_designs: list[str] | None = None,
|
|
@@ -2515,21 +3087,25 @@ class ArtifactService:
|
|
|
2515
3087
|
) -> dict[str, Any]:
|
|
2516
3088
|
self._require_baseline_gate_open(quest_root, action="create_analysis_campaign")
|
|
2517
3089
|
state = self.quest_service.read_research_state(quest_root)
|
|
2518
|
-
|
|
3090
|
+
parent_branch, parent_worktree_root, resolved_idea_id = self._resolve_analysis_parent_context(
|
|
3091
|
+
quest_root,
|
|
3092
|
+
state=state,
|
|
3093
|
+
)
|
|
3094
|
+
active_idea_id = str(resolved_idea_id or "").strip()
|
|
2519
3095
|
if not active_idea_id:
|
|
2520
3096
|
raise ValueError("An active idea is required before starting an analysis campaign.")
|
|
2521
3097
|
if not slices:
|
|
2522
3098
|
raise ValueError("At least one analysis slice is required.")
|
|
2523
|
-
parent_branch = str(state.get("research_head_branch") or current_branch(self._workspace_root_for(quest_root))).strip()
|
|
2524
|
-
parent_worktree_root = Path(str(state.get("research_head_worktree_root") or self._workspace_root_for(quest_root)))
|
|
2525
3099
|
campaign_id = generate_id("analysis")
|
|
2526
3100
|
charter_dir = ensure_dir(parent_worktree_root / "experiments" / "analysis-results" / campaign_id)
|
|
2527
3101
|
charter_path = charter_dir / "campaign.md"
|
|
3102
|
+
normalized_campaign_origin = self._normalize_campaign_origin(campaign_origin)
|
|
2528
3103
|
resolved_outline_ref = str(selected_outline_ref or "").strip() or None
|
|
2529
3104
|
normalized_research_questions = self._normalize_string_list(research_questions)
|
|
2530
3105
|
normalized_experimental_designs = self._normalize_string_list(experimental_designs)
|
|
2531
|
-
normalized_todo_items =
|
|
3106
|
+
normalized_todo_items = self._normalize_campaign_todo_items(todo_items)
|
|
2532
3107
|
slice_contexts: list[dict[str, Any]] = []
|
|
3108
|
+
inventory_entries: list[dict[str, Any]] = []
|
|
2533
3109
|
for index, raw in enumerate(slices, start=1):
|
|
2534
3110
|
slice_id = str(raw.get("slice_id") or generate_id("slice")).strip()
|
|
2535
3111
|
title = str(raw.get("title") or slice_id).strip() or slice_id
|
|
@@ -2550,6 +3126,21 @@ class ArtifactService:
|
|
|
2550
3126
|
worktree_root=worktree_root,
|
|
2551
3127
|
start_point=parent_branch,
|
|
2552
3128
|
)
|
|
3129
|
+
reviewer_item_ids = self._normalize_string_list(
|
|
3130
|
+
raw.get("reviewer_item_ids") or matched_todo.get("reviewer_item_ids")
|
|
3131
|
+
)
|
|
3132
|
+
manuscript_targets = self._normalize_string_list(
|
|
3133
|
+
raw.get("manuscript_targets") or matched_todo.get("manuscript_targets")
|
|
3134
|
+
)
|
|
3135
|
+
why_now = str(raw.get("why_now") or matched_todo.get("why_now") or "").strip()
|
|
3136
|
+
success_criteria = str(raw.get("success_criteria") or matched_todo.get("success_criteria") or "").strip()
|
|
3137
|
+
abandonment_criteria = str(
|
|
3138
|
+
raw.get("abandonment_criteria") or matched_todo.get("abandonment_criteria") or ""
|
|
3139
|
+
).strip()
|
|
3140
|
+
required_baselines = self._normalize_required_baselines(
|
|
3141
|
+
quest_root,
|
|
3142
|
+
raw.get("required_baselines") or matched_todo.get("required_baselines"),
|
|
3143
|
+
)
|
|
2553
3144
|
plan_dir = ensure_dir(worktree_root / "experiments" / "analysis" / campaign_id / slice_id)
|
|
2554
3145
|
plan_path = plan_dir / "plan.md"
|
|
2555
3146
|
requirement_lines = [
|
|
@@ -2567,6 +3158,10 @@ class ArtifactService:
|
|
|
2567
3158
|
"",
|
|
2568
3159
|
str(raw.get("experimental_design") or matched_todo.get("experimental_design") or "").strip() or "TBD",
|
|
2569
3160
|
"",
|
|
3161
|
+
"## Why Now",
|
|
3162
|
+
"",
|
|
3163
|
+
why_now or "TBD",
|
|
3164
|
+
"",
|
|
2570
3165
|
"## Hypothesis",
|
|
2571
3166
|
"",
|
|
2572
3167
|
str(raw.get("hypothesis") or "").strip() or "TBD",
|
|
@@ -2575,25 +3170,55 @@ class ArtifactService:
|
|
|
2575
3170
|
"",
|
|
2576
3171
|
str(raw.get("required_changes") or "").strip() or "TBD",
|
|
2577
3172
|
"",
|
|
2578
|
-
"##
|
|
2579
|
-
"",
|
|
2580
|
-
str(raw.get("metric_contract") or "").strip() or "TBD",
|
|
2581
|
-
"",
|
|
2582
|
-
"## Environment Notes",
|
|
2583
|
-
"",
|
|
2584
|
-
str(raw.get("environment_notes") or "").strip() or "TBD",
|
|
2585
|
-
"",
|
|
2586
|
-
"## Must Not Simplify",
|
|
2587
|
-
"",
|
|
2588
|
-
str(raw.get("must_not_simplify") or "").strip() or "Full dataset / full protocol only unless explicitly approved.",
|
|
2589
|
-
"",
|
|
2590
|
-
"## Completion Condition",
|
|
2591
|
-
"",
|
|
2592
|
-
str(raw.get("completion_condition") or matched_todo.get("completion_condition") or "").strip()
|
|
2593
|
-
or str(raw.get("must_not_simplify") or matched_todo.get("must_not_simplify") or "").strip()
|
|
2594
|
-
or "Complete the planned analysis slice and mirror the durable result back to the parent branch.",
|
|
3173
|
+
"## Required Baselines",
|
|
2595
3174
|
"",
|
|
2596
3175
|
]
|
|
3176
|
+
if required_baselines:
|
|
3177
|
+
requirement_lines.extend([f"- {self._analysis_baseline_label(item)}" for item in required_baselines])
|
|
3178
|
+
else:
|
|
3179
|
+
requirement_lines.append("- None recorded.")
|
|
3180
|
+
requirement_lines.extend(
|
|
3181
|
+
[
|
|
3182
|
+
"",
|
|
3183
|
+
"## Metric Contract",
|
|
3184
|
+
"",
|
|
3185
|
+
str(raw.get("metric_contract") or "").strip() or "TBD",
|
|
3186
|
+
"",
|
|
3187
|
+
"## Environment Notes",
|
|
3188
|
+
"",
|
|
3189
|
+
str(raw.get("environment_notes") or "").strip() or "TBD",
|
|
3190
|
+
"",
|
|
3191
|
+
"## Must Not Simplify",
|
|
3192
|
+
"",
|
|
3193
|
+
str(raw.get("must_not_simplify") or "").strip() or "Full dataset / full protocol only unless explicitly approved.",
|
|
3194
|
+
"",
|
|
3195
|
+
"## Success Criteria",
|
|
3196
|
+
"",
|
|
3197
|
+
success_criteria or "TBD",
|
|
3198
|
+
"",
|
|
3199
|
+
"## Abandonment Criteria",
|
|
3200
|
+
"",
|
|
3201
|
+
abandonment_criteria or "TBD",
|
|
3202
|
+
"",
|
|
3203
|
+
"## Completion Condition",
|
|
3204
|
+
"",
|
|
3205
|
+
str(raw.get("completion_condition") or matched_todo.get("completion_condition") or "").strip()
|
|
3206
|
+
or str(raw.get("must_not_simplify") or matched_todo.get("must_not_simplify") or "").strip()
|
|
3207
|
+
or "Complete the planned analysis slice and mirror the durable result back to the parent branch.",
|
|
3208
|
+
"",
|
|
3209
|
+
]
|
|
3210
|
+
)
|
|
3211
|
+
requirement_lines.extend(["## Reviewer Item IDs", ""])
|
|
3212
|
+
if reviewer_item_ids:
|
|
3213
|
+
requirement_lines.extend([f"- `{item}`" for item in reviewer_item_ids])
|
|
3214
|
+
else:
|
|
3215
|
+
requirement_lines.append("- None recorded.")
|
|
3216
|
+
requirement_lines.extend(["", "## Manuscript Targets", ""])
|
|
3217
|
+
if manuscript_targets:
|
|
3218
|
+
requirement_lines.extend([f"- {item}" for item in manuscript_targets])
|
|
3219
|
+
else:
|
|
3220
|
+
requirement_lines.append("- None recorded.")
|
|
3221
|
+
requirement_lines.append("")
|
|
2597
3222
|
write_text(plan_path, "\n".join(requirement_lines))
|
|
2598
3223
|
slice_contexts.append(
|
|
2599
3224
|
{
|
|
@@ -2612,20 +3237,48 @@ class ArtifactService:
|
|
|
2612
3237
|
"experimental_design": str(
|
|
2613
3238
|
raw.get("experimental_design") or matched_todo.get("experimental_design") or ""
|
|
2614
3239
|
).strip(),
|
|
3240
|
+
"why_now": why_now,
|
|
2615
3241
|
"hypothesis": str(raw.get("hypothesis") or "").strip(),
|
|
2616
3242
|
"required_changes": str(raw.get("required_changes") or "").strip(),
|
|
2617
3243
|
"metric_contract": str(raw.get("metric_contract") or "").strip(),
|
|
2618
3244
|
"environment_notes": str(raw.get("environment_notes") or "").strip(),
|
|
2619
3245
|
"must_not_simplify": str(raw.get("must_not_simplify") or "").strip(),
|
|
3246
|
+
"success_criteria": success_criteria,
|
|
3247
|
+
"abandonment_criteria": abandonment_criteria,
|
|
2620
3248
|
"completion_condition": str(
|
|
2621
3249
|
raw.get("completion_condition") or matched_todo.get("completion_condition") or ""
|
|
2622
3250
|
).strip(),
|
|
3251
|
+
"required_baselines": required_baselines,
|
|
3252
|
+
"reviewer_item_ids": reviewer_item_ids,
|
|
3253
|
+
"manuscript_targets": manuscript_targets,
|
|
2623
3254
|
}
|
|
3255
|
+
)
|
|
3256
|
+
inventory_entries.extend(
|
|
3257
|
+
[
|
|
3258
|
+
{
|
|
3259
|
+
"baseline_id": item.get("baseline_id"),
|
|
3260
|
+
"variant_id": item.get("variant_id"),
|
|
3261
|
+
"usage_scope": "supplementary",
|
|
3262
|
+
"status": "required",
|
|
3263
|
+
"reason": item.get("reason"),
|
|
3264
|
+
"benchmark": item.get("benchmark"),
|
|
3265
|
+
"split": item.get("split"),
|
|
3266
|
+
"baseline_root_rel_path": item.get("baseline_root_rel_path"),
|
|
3267
|
+
"storage_mode": item.get("storage_mode"),
|
|
3268
|
+
"origin": {
|
|
3269
|
+
"stage": "analysis_campaign",
|
|
3270
|
+
"campaign_id": campaign_id,
|
|
3271
|
+
"slice_id": slice_id,
|
|
3272
|
+
},
|
|
3273
|
+
}
|
|
3274
|
+
for item in required_baselines
|
|
3275
|
+
]
|
|
2624
3276
|
)
|
|
2625
3277
|
|
|
2626
3278
|
todo_manifest = {
|
|
2627
3279
|
"schema_version": 1,
|
|
2628
3280
|
"campaign_id": campaign_id,
|
|
3281
|
+
"campaign_origin": normalized_campaign_origin,
|
|
2629
3282
|
"selected_outline_ref": resolved_outline_ref,
|
|
2630
3283
|
"research_questions": normalized_research_questions,
|
|
2631
3284
|
"experimental_designs": normalized_experimental_designs,
|
|
@@ -2635,9 +3288,15 @@ class ArtifactService:
|
|
|
2635
3288
|
"slice_id": context["slice_id"],
|
|
2636
3289
|
"title": str(item.get("title") or context["title"]).strip() or context["title"],
|
|
2637
3290
|
"status": str(item.get("status") or "pending").strip() or "pending",
|
|
2638
|
-
"research_question":
|
|
2639
|
-
"experimental_design":
|
|
2640
|
-
"completion_condition":
|
|
3291
|
+
"research_question": item.get("research_question") or context.get("research_question"),
|
|
3292
|
+
"experimental_design": item.get("experimental_design") or context.get("experimental_design"),
|
|
3293
|
+
"completion_condition": item.get("completion_condition") or context.get("completion_condition") or context.get("must_not_simplify"),
|
|
3294
|
+
"why_now": item.get("why_now") or context.get("why_now"),
|
|
3295
|
+
"success_criteria": item.get("success_criteria") or context.get("success_criteria"),
|
|
3296
|
+
"abandonment_criteria": item.get("abandonment_criteria") or context.get("abandonment_criteria"),
|
|
3297
|
+
"required_baselines": item.get("required_baselines") or context.get("required_baselines") or [],
|
|
3298
|
+
"reviewer_item_ids": item.get("reviewer_item_ids") or context.get("reviewer_item_ids") or [],
|
|
3299
|
+
"manuscript_targets": item.get("manuscript_targets") or context.get("manuscript_targets") or [],
|
|
2641
3300
|
}
|
|
2642
3301
|
for context, item in zip(slice_contexts, normalized_todo_items + [{}] * max(0, len(slice_contexts) - len(normalized_todo_items)))
|
|
2643
3302
|
],
|
|
@@ -2666,6 +3325,14 @@ class ArtifactService:
|
|
|
2666
3325
|
"",
|
|
2667
3326
|
f"`{resolved_outline_ref or 'none'}`",
|
|
2668
3327
|
"",
|
|
3328
|
+
"## Campaign Origin",
|
|
3329
|
+
"",
|
|
3330
|
+
f"- Kind: `{(normalized_campaign_origin or {}).get('kind') or 'analysis'}`",
|
|
3331
|
+
f"- Reason: {str((normalized_campaign_origin or {}).get('reason') or 'Not recorded')}",
|
|
3332
|
+
f"- Source Artifact: `{str((normalized_campaign_origin or {}).get('source_artifact_id') or 'none')}`",
|
|
3333
|
+
f"- Source Outline: `{str((normalized_campaign_origin or {}).get('source_outline_ref') or 'none')}`",
|
|
3334
|
+
f"- Source Review Round: `{str((normalized_campaign_origin or {}).get('source_review_round') or 'none')}`",
|
|
3335
|
+
"",
|
|
2669
3336
|
"## Slices",
|
|
2670
3337
|
"",
|
|
2671
3338
|
]
|
|
@@ -2681,8 +3348,14 @@ class ArtifactService:
|
|
|
2681
3348
|
f"- Goal: {item['goal'] or 'TBD'}",
|
|
2682
3349
|
f"- Research question: {item['research_question'] or 'TBD'}",
|
|
2683
3350
|
f"- Experimental design: {item['experimental_design'] or 'TBD'}",
|
|
3351
|
+
f"- Why now: {item['why_now'] or 'TBD'}",
|
|
3352
|
+
f"- Required baselines: {', '.join(self._analysis_baseline_label(entry) for entry in item['required_baselines']) or 'none'}",
|
|
3353
|
+
f"- Success criteria: {item['success_criteria'] or 'TBD'}",
|
|
3354
|
+
f"- Abandonment criteria: {item['abandonment_criteria'] or 'TBD'}",
|
|
2684
3355
|
f"- Completion condition: {item['completion_condition'] or item['must_not_simplify'] or 'TBD'}",
|
|
2685
3356
|
f"- Requirement: {item['must_not_simplify'] or 'TBD'}",
|
|
3357
|
+
f"- Reviewer items: {', '.join(item['reviewer_item_ids']) or 'none'}",
|
|
3358
|
+
f"- Manuscript targets: {', '.join(item['manuscript_targets']) or 'none'}",
|
|
2686
3359
|
"",
|
|
2687
3360
|
]
|
|
2688
3361
|
)
|
|
@@ -2697,6 +3370,7 @@ class ArtifactService:
|
|
|
2697
3370
|
"active_idea_id": active_idea_id,
|
|
2698
3371
|
"parent_branch": parent_branch,
|
|
2699
3372
|
"parent_worktree_root": str(parent_worktree_root),
|
|
3373
|
+
"campaign_origin": normalized_campaign_origin,
|
|
2700
3374
|
"selected_outline_ref": resolved_outline_ref,
|
|
2701
3375
|
"research_questions": normalized_research_questions,
|
|
2702
3376
|
"experimental_designs": normalized_experimental_designs,
|
|
@@ -2707,6 +3381,45 @@ class ArtifactService:
|
|
|
2707
3381
|
"created_at": utc_now(),
|
|
2708
3382
|
},
|
|
2709
3383
|
)
|
|
3384
|
+
for item in slice_contexts:
|
|
3385
|
+
self.record(
|
|
3386
|
+
quest_root,
|
|
3387
|
+
{
|
|
3388
|
+
"kind": "milestone",
|
|
3389
|
+
"status": "prepared",
|
|
3390
|
+
"summary": f"Analysis slice `{item['slice_id']}` prepared as a child branch.",
|
|
3391
|
+
"reason": "Expose the pending follow-up branch durably so Canvas and Git lineage stay visible before execution.",
|
|
3392
|
+
"idea_id": active_idea_id,
|
|
3393
|
+
"campaign_id": campaign_id,
|
|
3394
|
+
"slice_id": item["slice_id"],
|
|
3395
|
+
"branch": item["branch"],
|
|
3396
|
+
"parent_branch": parent_branch,
|
|
3397
|
+
"worktree_root": item["worktree_root"],
|
|
3398
|
+
"worktree_rel_path": self._workspace_relative(quest_root, Path(item["worktree_root"])),
|
|
3399
|
+
"flow_type": "analysis_slice",
|
|
3400
|
+
"protocol_step": "prepare",
|
|
3401
|
+
"paths": {
|
|
3402
|
+
"plan_md": item["plan_path"],
|
|
3403
|
+
},
|
|
3404
|
+
"details": {
|
|
3405
|
+
"title": item["title"],
|
|
3406
|
+
"goal": item["goal"],
|
|
3407
|
+
"run_kind": item["run_kind"],
|
|
3408
|
+
"research_question": item["research_question"],
|
|
3409
|
+
"experimental_design": item["experimental_design"],
|
|
3410
|
+
"why_now": item["why_now"],
|
|
3411
|
+
"completion_condition": item["completion_condition"] or item["must_not_simplify"],
|
|
3412
|
+
"must_not_simplify": item["must_not_simplify"],
|
|
3413
|
+
"required_baselines": item["required_baselines"],
|
|
3414
|
+
"success_criteria": item["success_criteria"],
|
|
3415
|
+
"abandonment_criteria": item["abandonment_criteria"],
|
|
3416
|
+
"reviewer_item_ids": item["reviewer_item_ids"],
|
|
3417
|
+
"manuscript_targets": item["manuscript_targets"],
|
|
3418
|
+
},
|
|
3419
|
+
},
|
|
3420
|
+
checkpoint=False,
|
|
3421
|
+
workspace_root=Path(item["worktree_root"]),
|
|
3422
|
+
)
|
|
2710
3423
|
first_slice = slice_contexts[0]
|
|
2711
3424
|
artifact = self.record(
|
|
2712
3425
|
quest_root,
|
|
@@ -2729,6 +3442,7 @@ class ArtifactService:
|
|
|
2729
3442
|
"campaign_title": campaign_title,
|
|
2730
3443
|
"campaign_goal": campaign_goal,
|
|
2731
3444
|
"parent_run_id": parent_run_id,
|
|
3445
|
+
"campaign_origin": normalized_campaign_origin,
|
|
2732
3446
|
"selected_outline_ref": resolved_outline_ref,
|
|
2733
3447
|
"todo_manifest_path": str(todo_manifest_path),
|
|
2734
3448
|
"slice_count": len(slice_contexts),
|
|
@@ -2742,8 +3456,14 @@ class ArtifactService:
|
|
|
2742
3456
|
"goal": item["goal"],
|
|
2743
3457
|
"research_question": item["research_question"],
|
|
2744
3458
|
"experimental_design": item["experimental_design"],
|
|
3459
|
+
"why_now": item["why_now"],
|
|
2745
3460
|
"completion_condition": item["completion_condition"] or item["must_not_simplify"],
|
|
2746
3461
|
"must_not_simplify": item["must_not_simplify"],
|
|
3462
|
+
"required_baselines": item["required_baselines"],
|
|
3463
|
+
"success_criteria": item["success_criteria"],
|
|
3464
|
+
"abandonment_criteria": item["abandonment_criteria"],
|
|
3465
|
+
"reviewer_item_ids": item["reviewer_item_ids"],
|
|
3466
|
+
"manuscript_targets": item["manuscript_targets"],
|
|
2747
3467
|
}
|
|
2748
3468
|
for item in slice_contexts
|
|
2749
3469
|
],
|
|
@@ -2754,6 +3474,7 @@ class ArtifactService:
|
|
|
2754
3474
|
)
|
|
2755
3475
|
research_state = self.quest_service.update_research_state(
|
|
2756
3476
|
quest_root,
|
|
3477
|
+
active_idea_id=active_idea_id,
|
|
2757
3478
|
active_analysis_campaign_id=campaign_id,
|
|
2758
3479
|
analysis_parent_branch=parent_branch,
|
|
2759
3480
|
analysis_parent_worktree_root=str(parent_worktree_root),
|
|
@@ -2763,6 +3484,7 @@ class ArtifactService:
|
|
|
2763
3484
|
workspace_mode="analysis",
|
|
2764
3485
|
last_flow_type="analysis_campaign",
|
|
2765
3486
|
)
|
|
3487
|
+
baseline_inventory = self._upsert_analysis_baseline_inventory(quest_root, inventory_entries) if inventory_entries else None
|
|
2766
3488
|
self.quest_service.update_settings(self._quest_id(quest_root), active_anchor="analysis-campaign")
|
|
2767
3489
|
checkpoint_result = self._checkpoint_with_optional_push(
|
|
2768
3490
|
parent_worktree_root,
|
|
@@ -2785,16 +3507,17 @@ class ArtifactService:
|
|
|
2785
3507
|
attachments=[
|
|
2786
3508
|
{
|
|
2787
3509
|
"kind": "analysis_campaign",
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
3510
|
+
"campaign_id": campaign_id,
|
|
3511
|
+
"parent_branch": parent_branch,
|
|
3512
|
+
"parent_worktree_root": str(parent_worktree_root),
|
|
3513
|
+
"campaign_origin": normalized_campaign_origin,
|
|
3514
|
+
"selected_outline_ref": resolved_outline_ref,
|
|
3515
|
+
"todo_manifest_path": str(todo_manifest_path),
|
|
3516
|
+
"next_slice": first_slice,
|
|
3517
|
+
"todo_items": todo_manifest["todo_items"],
|
|
3518
|
+
"slices": slice_contexts,
|
|
3519
|
+
}
|
|
3520
|
+
],
|
|
2798
3521
|
)
|
|
2799
3522
|
return {
|
|
2800
3523
|
"ok": True,
|
|
@@ -2807,9 +3530,11 @@ class ArtifactService:
|
|
|
2807
3530
|
"campaign_id": campaign_id,
|
|
2808
3531
|
"parent_branch": parent_branch,
|
|
2809
3532
|
"parent_worktree_root": str(parent_worktree_root),
|
|
3533
|
+
"campaign_origin": normalized_campaign_origin,
|
|
2810
3534
|
"charter_path": str(charter_path),
|
|
2811
3535
|
"slices": slice_contexts,
|
|
2812
3536
|
"manifest": manifest,
|
|
3537
|
+
"analysis_baseline_inventory": baseline_inventory,
|
|
2813
3538
|
"todo_manifest_path": str(todo_manifest_path),
|
|
2814
3539
|
"artifact": artifact,
|
|
2815
3540
|
"checkpoint": checkpoint_result,
|
|
@@ -2992,6 +3717,18 @@ class ArtifactService:
|
|
|
2992
3717
|
raise ValueError("submit_paper_bundle requires a selected outline or explicit `outline_path`.")
|
|
2993
3718
|
|
|
2994
3719
|
manifest_path = self._paper_bundle_manifest_path(quest_root)
|
|
3720
|
+
baseline_inventory = self._write_paper_baseline_inventory(quest_root)
|
|
3721
|
+
baseline_inventory_path = self._paper_baseline_inventory_path(quest_root)
|
|
3722
|
+
source_branch = (
|
|
3723
|
+
str(self.quest_service.read_research_state(quest_root).get("current_workspace_branch") or "").strip()
|
|
3724
|
+
or current_branch(self._workspace_root_for(quest_root))
|
|
3725
|
+
)
|
|
3726
|
+
open_source_manifest = self._ensure_open_source_prep(
|
|
3727
|
+
quest_root,
|
|
3728
|
+
source_branch=source_branch,
|
|
3729
|
+
source_bundle_manifest_path="paper/paper_bundle_manifest.json",
|
|
3730
|
+
baseline_inventory_path="paper/baseline_inventory.json",
|
|
3731
|
+
)
|
|
2995
3732
|
manifest = {
|
|
2996
3733
|
"schema_version": 1,
|
|
2997
3734
|
"title": str(
|
|
@@ -3010,6 +3747,10 @@ class ArtifactService:
|
|
|
3010
3747
|
"compile_report_path": str(compile_report_path or "paper/build/compile_report.json").strip() or None,
|
|
3011
3748
|
"pdf_path": str(pdf_path or "").strip() or None,
|
|
3012
3749
|
"latex_root_path": str(latex_root_path or "").strip() or None,
|
|
3750
|
+
"baseline_inventory_path": "paper/baseline_inventory.json",
|
|
3751
|
+
"open_source_manifest_path": "release/open_source/manifest.json",
|
|
3752
|
+
"open_source_cleanup_plan_path": str(open_source_manifest.get("cleanup_plan_path") or "").strip()
|
|
3753
|
+
or "release/open_source/cleanup_plan.md",
|
|
3013
3754
|
"selected_outline_ref": str(selected_outline.get("outline_id") or "").strip() or None,
|
|
3014
3755
|
"created_at": utc_now(),
|
|
3015
3756
|
"updated_at": utc_now(),
|
|
@@ -3031,10 +3772,14 @@ class ArtifactService:
|
|
|
3031
3772
|
"outline_path": manifest.get("outline_path"),
|
|
3032
3773
|
"draft_path": manifest.get("draft_path"),
|
|
3033
3774
|
"pdf_path": manifest.get("pdf_path"),
|
|
3775
|
+
"baseline_inventory_path": str(baseline_inventory_path),
|
|
3776
|
+
"open_source_manifest_path": str(self._open_source_manifest_path(quest_root)),
|
|
3034
3777
|
},
|
|
3035
3778
|
"details": {
|
|
3036
3779
|
"title": manifest.get("title"),
|
|
3037
3780
|
"selected_outline_ref": manifest.get("selected_outline_ref"),
|
|
3781
|
+
"baseline_inventory_count": len(baseline_inventory.get("supplementary_baselines") or []),
|
|
3782
|
+
"open_source_status": open_source_manifest.get("status"),
|
|
3038
3783
|
},
|
|
3039
3784
|
},
|
|
3040
3785
|
checkpoint=False,
|
|
@@ -3044,6 +3789,8 @@ class ArtifactService:
|
|
|
3044
3789
|
"ok": True,
|
|
3045
3790
|
"manifest_path": str(manifest_path),
|
|
3046
3791
|
"manifest": manifest,
|
|
3792
|
+
"baseline_inventory_path": str(baseline_inventory_path),
|
|
3793
|
+
"open_source_manifest_path": str(self._open_source_manifest_path(quest_root)),
|
|
3047
3794
|
"artifact": artifact,
|
|
3048
3795
|
}
|
|
3049
3796
|
|
|
@@ -3060,8 +3807,14 @@ class ArtifactService:
|
|
|
3060
3807
|
evidence_paths: list[str] | None = None,
|
|
3061
3808
|
metric_rows: list[dict[str, Any]] | None = None,
|
|
3062
3809
|
deviations: list[str] | None = None,
|
|
3810
|
+
claim_impact: str | None = None,
|
|
3811
|
+
reviewer_resolution: str | None = None,
|
|
3812
|
+
manuscript_update_hint: str | None = None,
|
|
3813
|
+
next_recommendation: str | None = None,
|
|
3063
3814
|
dataset_scope: str = "full",
|
|
3064
3815
|
subset_approval_ref: str | None = None,
|
|
3816
|
+
comparison_baselines: list[dict[str, Any]] | None = None,
|
|
3817
|
+
evaluation_summary: dict[str, Any] | None = None,
|
|
3065
3818
|
) -> dict[str, Any]:
|
|
3066
3819
|
state = self.quest_service.read_research_state(quest_root)
|
|
3067
3820
|
manifest = self._read_analysis_manifest(quest_root, campaign_id)
|
|
@@ -3076,12 +3829,19 @@ class ArtifactService:
|
|
|
3076
3829
|
evidence_paths = [str(item).strip() for item in (evidence_paths or []) if str(item).strip()]
|
|
3077
3830
|
deviations = [str(item).strip() for item in (deviations or []) if str(item).strip()]
|
|
3078
3831
|
metric_rows = [item for item in (metric_rows or []) if isinstance(item, dict)]
|
|
3832
|
+
normalized_comparison_baselines = self._normalize_comparison_baselines(quest_root, comparison_baselines)
|
|
3833
|
+
normalized_claim_impact = str(claim_impact or "").strip() or None
|
|
3834
|
+
normalized_reviewer_resolution = str(reviewer_resolution or "").strip() or None
|
|
3835
|
+
normalized_manuscript_update_hint = str(manuscript_update_hint or "").strip() or None
|
|
3836
|
+
normalized_next_recommendation = str(next_recommendation or "").strip() or None
|
|
3837
|
+
normalized_evaluation_summary = self._normalize_evaluation_summary(evaluation_summary)
|
|
3079
3838
|
slice_worktree_root = Path(str(target.get("worktree_root") or ""))
|
|
3080
3839
|
parent_worktree_root = Path(str(manifest.get("parent_worktree_root") or ""))
|
|
3081
3840
|
parent_branch = str(manifest.get("parent_branch") or "")
|
|
3082
3841
|
|
|
3083
3842
|
result_dir = ensure_dir(slice_worktree_root / "experiments" / "analysis" / campaign_id / slice_id)
|
|
3084
3843
|
result_path = result_dir / "RESULT.md"
|
|
3844
|
+
result_json_path = result_dir / "RESULT.json"
|
|
3085
3845
|
result_lines = [
|
|
3086
3846
|
f"# {target.get('title') or slice_id}",
|
|
3087
3847
|
"",
|
|
@@ -3104,6 +3864,26 @@ class ArtifactService:
|
|
|
3104
3864
|
"",
|
|
3105
3865
|
results.strip() or "TBD",
|
|
3106
3866
|
"",
|
|
3867
|
+
"## Claim Impact",
|
|
3868
|
+
"",
|
|
3869
|
+
normalized_claim_impact or "Not recorded.",
|
|
3870
|
+
"",
|
|
3871
|
+
"## Reviewer Resolution",
|
|
3872
|
+
"",
|
|
3873
|
+
normalized_reviewer_resolution or "Not recorded.",
|
|
3874
|
+
"",
|
|
3875
|
+
"## Manuscript Update Hint",
|
|
3876
|
+
"",
|
|
3877
|
+
normalized_manuscript_update_hint or "Not recorded.",
|
|
3878
|
+
"",
|
|
3879
|
+
"## Next Recommendation",
|
|
3880
|
+
"",
|
|
3881
|
+
normalized_next_recommendation or "Not recorded.",
|
|
3882
|
+
"",
|
|
3883
|
+
"## Evaluation Summary",
|
|
3884
|
+
"",
|
|
3885
|
+
*self._evaluation_summary_markdown_lines(normalized_evaluation_summary),
|
|
3886
|
+
"",
|
|
3107
3887
|
"## Deviations",
|
|
3108
3888
|
"",
|
|
3109
3889
|
]
|
|
@@ -3120,6 +3900,20 @@ class ArtifactService:
|
|
|
3120
3900
|
result_lines.extend(["", "## Metric Rows", ""])
|
|
3121
3901
|
for row in metric_rows:
|
|
3122
3902
|
result_lines.append(f"- `{row}`")
|
|
3903
|
+
result_lines.extend(["", "## Comparison Baselines", ""])
|
|
3904
|
+
if normalized_comparison_baselines:
|
|
3905
|
+
for entry in normalized_comparison_baselines:
|
|
3906
|
+
result_lines.append(f"- {self._analysis_baseline_label(entry)}")
|
|
3907
|
+
if entry.get("baseline_root_rel_path"):
|
|
3908
|
+
result_lines.append(f" - Root: `{entry['baseline_root_rel_path']}`")
|
|
3909
|
+
if entry.get("metrics_summary"):
|
|
3910
|
+
result_lines.append(f" - Metrics: `{entry['metrics_summary']}`")
|
|
3911
|
+
if entry.get("published"):
|
|
3912
|
+
result_lines.append(
|
|
3913
|
+
f" - Published: `{entry.get('published_entry_id') or entry.get('baseline_id')}`"
|
|
3914
|
+
)
|
|
3915
|
+
else:
|
|
3916
|
+
result_lines.append("- None recorded.")
|
|
3123
3917
|
if subset_approval_ref:
|
|
3124
3918
|
result_lines.extend(["", "## Subset Approval", "", f"`{subset_approval_ref}`"])
|
|
3125
3919
|
write_text(result_path, "\n".join(result_lines).rstrip() + "\n")
|
|
@@ -3134,6 +3928,37 @@ class ArtifactService:
|
|
|
3134
3928
|
if len(keys) == 1:
|
|
3135
3929
|
metrics_summary[keys[0]] = row.get(keys[0])
|
|
3136
3930
|
|
|
3931
|
+
result_payload = {
|
|
3932
|
+
"schema_version": 1,
|
|
3933
|
+
"result_kind": "analysis_slice",
|
|
3934
|
+
"campaign_id": campaign_id,
|
|
3935
|
+
"slice_id": slice_id,
|
|
3936
|
+
"status": status,
|
|
3937
|
+
"title": target.get("title"),
|
|
3938
|
+
"goal": target.get("goal"),
|
|
3939
|
+
"run_kind": target.get("run_kind"),
|
|
3940
|
+
"required_baselines": target.get("required_baselines") or [],
|
|
3941
|
+
"comparison_baselines": normalized_comparison_baselines,
|
|
3942
|
+
"metrics_summary": metrics_summary,
|
|
3943
|
+
"metric_rows": metric_rows,
|
|
3944
|
+
"dataset_scope": normalized_scope,
|
|
3945
|
+
"subset_approval_ref": subset_approval_ref,
|
|
3946
|
+
"setup": setup.strip() or None,
|
|
3947
|
+
"execution": execution.strip() or None,
|
|
3948
|
+
"results": results.strip() or None,
|
|
3949
|
+
"claim_impact": normalized_claim_impact,
|
|
3950
|
+
"reviewer_resolution": normalized_reviewer_resolution,
|
|
3951
|
+
"manuscript_update_hint": normalized_manuscript_update_hint,
|
|
3952
|
+
"next_recommendation": normalized_next_recommendation,
|
|
3953
|
+
"evaluation_summary": normalized_evaluation_summary,
|
|
3954
|
+
"deviations": deviations,
|
|
3955
|
+
"evidence_paths": evidence_paths,
|
|
3956
|
+
"source_branch": str(target.get("branch") or ""),
|
|
3957
|
+
"source_worktree_root": str(slice_worktree_root),
|
|
3958
|
+
"updated_at": utc_now(),
|
|
3959
|
+
}
|
|
3960
|
+
write_json(result_json_path, result_payload)
|
|
3961
|
+
|
|
3137
3962
|
mirror_dir = ensure_dir(parent_worktree_root / "experiments" / "analysis-results" / campaign_id)
|
|
3138
3963
|
mirror_path = mirror_dir / f"{slice_id}.md"
|
|
3139
3964
|
mirror_lines = [
|
|
@@ -3164,7 +3989,25 @@ class ArtifactService:
|
|
|
3164
3989
|
"",
|
|
3165
3990
|
results.strip() or "TBD",
|
|
3166
3991
|
"",
|
|
3992
|
+
"## Claim Impact",
|
|
3993
|
+
"",
|
|
3994
|
+
normalized_claim_impact or "Not recorded.",
|
|
3995
|
+
"",
|
|
3996
|
+
"## Manuscript Update Hint",
|
|
3997
|
+
"",
|
|
3998
|
+
normalized_manuscript_update_hint or "Not recorded.",
|
|
3999
|
+
"",
|
|
4000
|
+
"## Evaluation Summary",
|
|
4001
|
+
"",
|
|
4002
|
+
*self._evaluation_summary_markdown_lines(normalized_evaluation_summary),
|
|
4003
|
+
"",
|
|
3167
4004
|
]
|
|
4005
|
+
mirror_lines.extend(["## Comparison Baselines", ""])
|
|
4006
|
+
if normalized_comparison_baselines:
|
|
4007
|
+
mirror_lines.extend([f"- {self._analysis_baseline_label(entry)}" for entry in normalized_comparison_baselines])
|
|
4008
|
+
else:
|
|
4009
|
+
mirror_lines.append("- None recorded.")
|
|
4010
|
+
mirror_lines.append("")
|
|
3168
4011
|
write_text(mirror_path, "\n".join(mirror_lines).rstrip() + "\n")
|
|
3169
4012
|
|
|
3170
4013
|
artifact = self.record(
|
|
@@ -3188,6 +4031,7 @@ class ArtifactService:
|
|
|
3188
4031
|
"protocol_step": "record",
|
|
3189
4032
|
"paths": {
|
|
3190
4033
|
"slice_result_md": str(result_path),
|
|
4034
|
+
"slice_result_json": str(result_json_path),
|
|
3191
4035
|
"parent_result_md": str(mirror_path),
|
|
3192
4036
|
},
|
|
3193
4037
|
"details": {
|
|
@@ -3197,9 +4041,17 @@ class ArtifactService:
|
|
|
3197
4041
|
"dataset_scope": normalized_scope,
|
|
3198
4042
|
"subset_approval_ref": subset_approval_ref,
|
|
3199
4043
|
"metric_rows": metric_rows,
|
|
4044
|
+
"claim_impact": normalized_claim_impact,
|
|
4045
|
+
"reviewer_resolution": normalized_reviewer_resolution,
|
|
4046
|
+
"manuscript_update_hint": normalized_manuscript_update_hint,
|
|
4047
|
+
"next_recommendation": normalized_next_recommendation,
|
|
3200
4048
|
"deviations": deviations,
|
|
3201
4049
|
"evidence_paths": evidence_paths,
|
|
4050
|
+
"required_baselines": target.get("required_baselines") or [],
|
|
4051
|
+
"comparison_baselines": normalized_comparison_baselines,
|
|
4052
|
+
"evaluation_summary": normalized_evaluation_summary,
|
|
3202
4053
|
},
|
|
4054
|
+
"evaluation_summary": normalized_evaluation_summary,
|
|
3203
4055
|
},
|
|
3204
4056
|
checkpoint=False,
|
|
3205
4057
|
workspace_root=slice_worktree_root,
|
|
@@ -3222,7 +4074,14 @@ class ArtifactService:
|
|
|
3222
4074
|
updated["status"] = status
|
|
3223
4075
|
updated["completed_at"] = utc_now()
|
|
3224
4076
|
updated["result_path"] = str(result_path)
|
|
4077
|
+
updated["result_json_path"] = str(result_json_path)
|
|
3225
4078
|
updated["mirror_path"] = str(mirror_path)
|
|
4079
|
+
updated["claim_impact"] = normalized_claim_impact
|
|
4080
|
+
updated["reviewer_resolution"] = normalized_reviewer_resolution
|
|
4081
|
+
updated["manuscript_update_hint"] = normalized_manuscript_update_hint
|
|
4082
|
+
updated["next_recommendation"] = normalized_next_recommendation
|
|
4083
|
+
updated["comparison_baselines"] = normalized_comparison_baselines
|
|
4084
|
+
updated["evaluation_summary"] = normalized_evaluation_summary
|
|
3226
4085
|
updated_slices.append(updated)
|
|
3227
4086
|
next_slice = next((item for item in updated_slices if str(item.get("status") or "") == "pending"), None)
|
|
3228
4087
|
manifest = self._write_analysis_manifest(
|
|
@@ -3233,6 +4092,36 @@ class ArtifactService:
|
|
|
3233
4092
|
"slices": updated_slices,
|
|
3234
4093
|
},
|
|
3235
4094
|
)
|
|
4095
|
+
baseline_inventory = (
|
|
4096
|
+
self._upsert_analysis_baseline_inventory(
|
|
4097
|
+
quest_root,
|
|
4098
|
+
[
|
|
4099
|
+
{
|
|
4100
|
+
"baseline_id": entry.get("baseline_id"),
|
|
4101
|
+
"variant_id": entry.get("variant_id"),
|
|
4102
|
+
"usage_scope": "supplementary",
|
|
4103
|
+
"status": "registered",
|
|
4104
|
+
"reason": entry.get("reason"),
|
|
4105
|
+
"benchmark": entry.get("benchmark"),
|
|
4106
|
+
"split": entry.get("split"),
|
|
4107
|
+
"baseline_root_rel_path": entry.get("baseline_root_rel_path"),
|
|
4108
|
+
"storage_mode": entry.get("storage_mode"),
|
|
4109
|
+
"metrics_summary": entry.get("metrics_summary"),
|
|
4110
|
+
"evidence_paths": entry.get("evidence_paths"),
|
|
4111
|
+
"published": entry.get("published"),
|
|
4112
|
+
"published_entry_id": entry.get("published_entry_id"),
|
|
4113
|
+
"origin": {
|
|
4114
|
+
"stage": "analysis_campaign",
|
|
4115
|
+
"campaign_id": campaign_id,
|
|
4116
|
+
"slice_id": slice_id,
|
|
4117
|
+
},
|
|
4118
|
+
}
|
|
4119
|
+
for entry in normalized_comparison_baselines
|
|
4120
|
+
],
|
|
4121
|
+
)
|
|
4122
|
+
if normalized_comparison_baselines
|
|
4123
|
+
else self._read_analysis_baseline_inventory(quest_root)
|
|
4124
|
+
)
|
|
3236
4125
|
|
|
3237
4126
|
if next_slice is not None:
|
|
3238
4127
|
research_state = self.quest_service.update_research_state(
|
|
@@ -3274,14 +4163,17 @@ class ArtifactService:
|
|
|
3274
4163
|
"slice_id": slice_id,
|
|
3275
4164
|
"status": status,
|
|
3276
4165
|
"result_path": str(result_path),
|
|
4166
|
+
"result_json_path": str(result_json_path),
|
|
3277
4167
|
"mirror_path": str(mirror_path),
|
|
3278
4168
|
"artifact": artifact,
|
|
3279
4169
|
"slice_checkpoint": slice_checkpoint,
|
|
3280
4170
|
"parent_checkpoint": parent_checkpoint,
|
|
3281
4171
|
"next_slice": next_slice,
|
|
3282
4172
|
"manifest": manifest,
|
|
4173
|
+
"analysis_baseline_inventory": baseline_inventory,
|
|
3283
4174
|
"interaction": interaction,
|
|
3284
4175
|
"research_state": research_state,
|
|
4176
|
+
"evaluation_summary": normalized_evaluation_summary,
|
|
3285
4177
|
"completed": False,
|
|
3286
4178
|
}
|
|
3287
4179
|
|
|
@@ -3336,12 +4228,14 @@ class ArtifactService:
|
|
|
3336
4228
|
parent_worktree_root,
|
|
3337
4229
|
message=f"analysis: summarize {campaign_id}",
|
|
3338
4230
|
)
|
|
4231
|
+
restored_idea_id = self._latest_branch_idea_id(quest_root, parent_branch) or str(manifest.get("active_idea_id") or "").strip() or None
|
|
3339
4232
|
research_state = self.quest_service.update_research_state(
|
|
3340
4233
|
quest_root,
|
|
4234
|
+
active_idea_id=restored_idea_id,
|
|
3341
4235
|
active_analysis_campaign_id=None,
|
|
3342
4236
|
next_pending_slice_id=None,
|
|
3343
|
-
current_workspace_branch=
|
|
3344
|
-
current_workspace_root=
|
|
4237
|
+
current_workspace_branch=parent_branch,
|
|
4238
|
+
current_workspace_root=str(parent_worktree_root),
|
|
3345
4239
|
workspace_mode="idea",
|
|
3346
4240
|
last_flow_type="analysis_campaign_complete",
|
|
3347
4241
|
)
|
|
@@ -3374,6 +4268,7 @@ class ArtifactService:
|
|
|
3374
4268
|
"slice_id": slice_id,
|
|
3375
4269
|
"status": status,
|
|
3376
4270
|
"result_path": str(result_path),
|
|
4271
|
+
"result_json_path": str(result_json_path),
|
|
3377
4272
|
"mirror_path": str(mirror_path),
|
|
3378
4273
|
"artifact": artifact,
|
|
3379
4274
|
"slice_checkpoint": slice_checkpoint,
|
|
@@ -3382,8 +4277,10 @@ class ArtifactService:
|
|
|
3382
4277
|
"summary_checkpoint": parent_summary_checkpoint,
|
|
3383
4278
|
"summary_path": str(summary_path),
|
|
3384
4279
|
"manifest": manifest,
|
|
4280
|
+
"analysis_baseline_inventory": baseline_inventory,
|
|
3385
4281
|
"interaction": interaction,
|
|
3386
4282
|
"research_state": research_state,
|
|
4283
|
+
"evaluation_summary": normalized_evaluation_summary,
|
|
3387
4284
|
"completed": True,
|
|
3388
4285
|
"returned_to_branch": parent_branch,
|
|
3389
4286
|
"returned_to_worktree_root": str(parent_worktree_root),
|
|
@@ -3789,6 +4686,8 @@ class ArtifactService:
|
|
|
3789
4686
|
expects_reply: bool | None = None,
|
|
3790
4687
|
reply_mode: str | None = None,
|
|
3791
4688
|
options: list[dict[str, Any]] | None = None,
|
|
4689
|
+
surface_actions: list[dict[str, Any]] | None = None,
|
|
4690
|
+
connector_hints: dict[str, Any] | None = None,
|
|
3792
4691
|
allow_free_text: bool = True,
|
|
3793
4692
|
reply_schema: dict[str, Any] | None = None,
|
|
3794
4693
|
reply_to_interaction_id: str | None = None,
|
|
@@ -3801,6 +4700,9 @@ class ArtifactService:
|
|
|
3801
4700
|
"approval_result": "approval",
|
|
3802
4701
|
}.get(kind, "progress")
|
|
3803
4702
|
options_resolved = options or []
|
|
4703
|
+
surface_actions_resolved = [dict(item) for item in (surface_actions or []) if isinstance(item, dict)]
|
|
4704
|
+
connector_hints_resolved = self._normalize_connector_hints(connector_hints)
|
|
4705
|
+
attachments_resolved, attachment_issues = self._normalize_interaction_attachments(quest_root, attachments)
|
|
3804
4706
|
reply_schema_resolved = reply_schema if isinstance(reply_schema, dict) else {}
|
|
3805
4707
|
reply_mode_resolved = str(
|
|
3806
4708
|
reply_mode
|
|
@@ -3860,10 +4762,14 @@ class ArtifactService:
|
|
|
3860
4762
|
"expects_reply": False,
|
|
3861
4763
|
"reply_mode": "none",
|
|
3862
4764
|
"delivered": False,
|
|
4765
|
+
"delivery_results": [],
|
|
3863
4766
|
"response_phase": response_phase,
|
|
3864
4767
|
"delivery_targets": [],
|
|
3865
4768
|
"delivery_policy": self._delivery_policy(self._connectors_config()),
|
|
3866
4769
|
"preferred_connector": self._preferred_connector(self._connectors_config()),
|
|
4770
|
+
"connector_hints": connector_hints_resolved,
|
|
4771
|
+
"normalized_attachments": attachments_resolved,
|
|
4772
|
+
"attachment_issues": attachment_issues,
|
|
3867
4773
|
"recent_inbound_messages": mailbox_payload.get("recent_inbound_messages") or [],
|
|
3868
4774
|
"delivery_batch": mailbox_payload.get("delivery_batch"),
|
|
3869
4775
|
"recent_interaction_records": mailbox_payload.get("recent_interaction_records") or [],
|
|
@@ -3889,11 +4795,13 @@ class ArtifactService:
|
|
|
3889
4795
|
"summary": message,
|
|
3890
4796
|
"interaction_phase": "request" if kind == "decision_request" else response_phase,
|
|
3891
4797
|
"importance": importance,
|
|
3892
|
-
"attachments":
|
|
4798
|
+
"attachments": attachments_resolved,
|
|
3893
4799
|
"interaction_id": resolved_interaction_id,
|
|
3894
4800
|
"expects_reply": expects_reply_resolved,
|
|
3895
4801
|
"reply_mode": reply_mode_resolved,
|
|
3896
4802
|
"options": options_resolved,
|
|
4803
|
+
"surface_actions": surface_actions_resolved,
|
|
4804
|
+
"connector_hints": connector_hints_resolved,
|
|
3897
4805
|
"allow_free_text": allow_free_text,
|
|
3898
4806
|
"reply_schema": reply_schema_resolved,
|
|
3899
4807
|
"reply_to_interaction_id": reply_to_interaction_id,
|
|
@@ -3929,6 +4837,7 @@ class ArtifactService:
|
|
|
3929
4837
|
)
|
|
3930
4838
|
delivery_targets: list[str] = []
|
|
3931
4839
|
delivered = False
|
|
4840
|
+
delivery_results: list[dict[str, Any]] = []
|
|
3932
4841
|
if deliver_to_bound_conversations:
|
|
3933
4842
|
connectors = self._connectors_config()
|
|
3934
4843
|
targets = self._select_delivery_targets(
|
|
@@ -3950,12 +4859,17 @@ class ArtifactService:
|
|
|
3950
4859
|
"expects_reply": expects_reply_resolved,
|
|
3951
4860
|
"reply_mode": reply_mode_resolved,
|
|
3952
4861
|
"options": options_resolved,
|
|
4862
|
+
"surface_actions": surface_actions_resolved,
|
|
4863
|
+
"connector_hints": connector_hints_resolved,
|
|
3953
4864
|
"allow_free_text": allow_free_text,
|
|
3954
4865
|
"reply_schema": reply_schema_resolved,
|
|
3955
4866
|
"reply_to_interaction_id": reply_to_interaction_id,
|
|
3956
|
-
"attachments":
|
|
4867
|
+
"attachments": attachments_resolved,
|
|
3957
4868
|
}
|
|
3958
|
-
|
|
4869
|
+
delivery_result = self._deliver_to_channel(channel_name, payload, connectors=connectors)
|
|
4870
|
+
delivery_result["conversation_id"] = target
|
|
4871
|
+
delivery_results.append(delivery_result)
|
|
4872
|
+
if delivery_result.get("ok", False) or delivery_result.get("queued", False):
|
|
3959
4873
|
delivery_targets.append(target)
|
|
3960
4874
|
delivered = True
|
|
3961
4875
|
|
|
@@ -3985,6 +4899,8 @@ class ArtifactService:
|
|
|
3985
4899
|
message=message,
|
|
3986
4900
|
response_phase=response_phase,
|
|
3987
4901
|
reply_mode=reply_mode_resolved,
|
|
4902
|
+
surface_actions=surface_actions_resolved,
|
|
4903
|
+
connector_hints=connector_hints_resolved,
|
|
3988
4904
|
created_at=(artifact.get("record") or {}).get("updated_at"),
|
|
3989
4905
|
)
|
|
3990
4906
|
|
|
@@ -3994,7 +4910,12 @@ class ArtifactService:
|
|
|
3994
4910
|
"interaction_id": request_state.get("interaction_id"),
|
|
3995
4911
|
"expects_reply": expects_reply_resolved,
|
|
3996
4912
|
"reply_mode": reply_mode_resolved,
|
|
4913
|
+
"surface_actions": surface_actions_resolved,
|
|
4914
|
+
"connector_hints": connector_hints_resolved,
|
|
4915
|
+
"normalized_attachments": attachments_resolved,
|
|
4916
|
+
"attachment_issues": attachment_issues,
|
|
3997
4917
|
"delivered": delivered,
|
|
4918
|
+
"delivery_results": delivery_results,
|
|
3998
4919
|
"response_phase": response_phase,
|
|
3999
4920
|
"delivery_targets": delivery_targets,
|
|
4000
4921
|
"delivery_policy": self._delivery_policy(self._connectors_config()),
|
|
@@ -4296,6 +5217,67 @@ class ArtifactService:
|
|
|
4296
5217
|
return enabled[0]
|
|
4297
5218
|
return None
|
|
4298
5219
|
|
|
5220
|
+
@staticmethod
|
|
5221
|
+
def _normalize_connector_hints(connector_hints: dict[str, Any] | None) -> dict[str, Any]:
|
|
5222
|
+
if not isinstance(connector_hints, dict):
|
|
5223
|
+
return {}
|
|
5224
|
+
normalized: dict[str, Any] = {}
|
|
5225
|
+
for key, value in connector_hints.items():
|
|
5226
|
+
name = str(key or "").strip().lower()
|
|
5227
|
+
if not name or not isinstance(value, dict):
|
|
5228
|
+
continue
|
|
5229
|
+
normalized[name] = dict(value)
|
|
5230
|
+
return normalized
|
|
5231
|
+
|
|
5232
|
+
def _normalize_interaction_attachments(
|
|
5233
|
+
self,
|
|
5234
|
+
quest_root: Path,
|
|
5235
|
+
attachments: list[dict[str, Any]] | None,
|
|
5236
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
5237
|
+
normalized: list[dict[str, Any]] = []
|
|
5238
|
+
issues: list[dict[str, Any]] = []
|
|
5239
|
+
for index, raw_item in enumerate(attachments or [], start=1):
|
|
5240
|
+
if not isinstance(raw_item, dict):
|
|
5241
|
+
issues.append(
|
|
5242
|
+
{
|
|
5243
|
+
"attachment_index": index,
|
|
5244
|
+
"error": "attachment must be an object",
|
|
5245
|
+
}
|
|
5246
|
+
)
|
|
5247
|
+
continue
|
|
5248
|
+
item = dict(raw_item)
|
|
5249
|
+
path_value = str(item.get("path") or "").strip()
|
|
5250
|
+
if path_value:
|
|
5251
|
+
resolved_path = Path(path_value).expanduser()
|
|
5252
|
+
if not resolved_path.is_absolute():
|
|
5253
|
+
resolved_path = (quest_root / resolved_path).resolve()
|
|
5254
|
+
else:
|
|
5255
|
+
resolved_path = resolved_path.resolve()
|
|
5256
|
+
item["path"] = str(resolved_path)
|
|
5257
|
+
if not resolved_path.exists():
|
|
5258
|
+
item["path_error"] = "path_not_found"
|
|
5259
|
+
issues.append(
|
|
5260
|
+
{
|
|
5261
|
+
"attachment_index": index,
|
|
5262
|
+
"path": str(resolved_path),
|
|
5263
|
+
"error": "attachment path does not exist",
|
|
5264
|
+
}
|
|
5265
|
+
)
|
|
5266
|
+
connector_delivery = item.get("connector_delivery")
|
|
5267
|
+
if isinstance(connector_delivery, dict):
|
|
5268
|
+
normalized_delivery: dict[str, Any] = {}
|
|
5269
|
+
for key, value in connector_delivery.items():
|
|
5270
|
+
name = str(key or "").strip().lower()
|
|
5271
|
+
if not name or not isinstance(value, dict):
|
|
5272
|
+
continue
|
|
5273
|
+
normalized_delivery[name] = dict(value)
|
|
5274
|
+
if normalized_delivery:
|
|
5275
|
+
item["connector_delivery"] = normalized_delivery
|
|
5276
|
+
else:
|
|
5277
|
+
item.pop("connector_delivery", None)
|
|
5278
|
+
normalized.append(item)
|
|
5279
|
+
return normalized, issues
|
|
5280
|
+
|
|
4299
5281
|
def _select_delivery_targets(self, targets: list[str], *, connectors: dict[str, Any]) -> list[str]:
|
|
4300
5282
|
if not targets:
|
|
4301
5283
|
return ["local:default"]
|
|
@@ -4445,6 +5427,11 @@ class ArtifactService:
|
|
|
4445
5427
|
"transport": transport,
|
|
4446
5428
|
"response_phase": str(payload.get("response_phase") or "").strip() or None,
|
|
4447
5429
|
"importance": str(payload.get("importance") or "").strip() or None,
|
|
5430
|
+
"artifact_id": str(payload.get("artifact_id") or "").strip() or None,
|
|
5431
|
+
"interaction_id": str(payload.get("interaction_id") or "").strip() or None,
|
|
5432
|
+
"surface_actions": payload.get("surface_actions") if isinstance(payload.get("surface_actions"), list) else [],
|
|
5433
|
+
"connector_hints": payload.get("connector_hints") if isinstance(payload.get("connector_hints"), dict) else {},
|
|
5434
|
+
"delivery_parts": delivery.get("parts") if isinstance(delivery.get("parts"), list) else [],
|
|
4448
5435
|
"error": str(result.get("error") or delivery.get("error") or "").strip() or None,
|
|
4449
5436
|
"created_at": utc_now(),
|
|
4450
5437
|
},
|