@researai/deepscientist 1.5.8 → 1.5.11
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/LICENSE +186 -21
- package/README.md +108 -95
- package/assets/branding/connector-qq.png +0 -0
- package/assets/branding/connector-rokid.png +0 -0
- package/assets/branding/connector-weixin.png +0 -0
- package/assets/branding/projects.png +0 -0
- package/bin/ds.js +172 -13
- package/docs/assets/branding/projects.png +0 -0
- package/docs/en/00_QUICK_START.md +308 -70
- package/docs/en/01_SETTINGS_REFERENCE.md +3 -0
- package/docs/en/02_START_RESEARCH_GUIDE.md +112 -0
- package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +62 -179
- package/docs/en/09_DOCTOR.md +41 -5
- package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +137 -0
- package/docs/en/11_LICENSE_AND_RISK.md +256 -0
- package/docs/en/12_GUIDED_WORKFLOW_TOUR.md +427 -0
- package/docs/en/13_CORE_ARCHITECTURE_GUIDE.md +297 -0
- package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
- package/docs/en/99_ACKNOWLEDGEMENTS.md +4 -1
- package/docs/en/README.md +79 -0
- package/docs/images/lingzhu/rokid-agent-platform-create.png +0 -0
- package/docs/images/weixin/weixin-plugin-entry.png +0 -0
- package/docs/images/weixin/weixin-plugin-entry.svg +33 -0
- package/docs/images/weixin/weixin-qr-confirm.svg +30 -0
- package/docs/images/weixin/weixin-quest-media-flow.svg +44 -0
- package/docs/images/weixin/weixin-settings-bind.svg +57 -0
- package/docs/zh/00_QUICK_START.md +315 -74
- package/docs/zh/01_SETTINGS_REFERENCE.md +3 -0
- package/docs/zh/02_START_RESEARCH_GUIDE.md +112 -0
- package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +62 -193
- package/docs/zh/09_DOCTOR.md +41 -5
- package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +144 -0
- package/docs/zh/11_LICENSE_AND_RISK.md +256 -0
- package/docs/zh/12_GUIDED_WORKFLOW_TOUR.md +423 -0
- package/docs/zh/13_CORE_ARCHITECTURE_GUIDE.md +296 -0
- package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
- package/docs/zh/99_ACKNOWLEDGEMENTS.md +4 -1
- package/docs/zh/README.md +126 -0
- package/install.sh +0 -34
- package/package.json +3 -3
- package/pyproject.toml +2 -2
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/annotations.py +343 -0
- package/src/deepscientist/artifact/arxiv.py +484 -37
- package/src/deepscientist/artifact/metrics.py +1 -3
- package/src/deepscientist/artifact/service.py +1347 -111
- package/src/deepscientist/arxiv_library.py +275 -0
- package/src/deepscientist/bash_exec/service.py +9 -0
- package/src/deepscientist/bridges/builtins.py +2 -0
- package/src/deepscientist/bridges/connectors.py +447 -0
- package/src/deepscientist/channels/__init__.py +2 -0
- package/src/deepscientist/channels/builtins.py +3 -1
- package/src/deepscientist/channels/qq.py +1 -1
- package/src/deepscientist/channels/qq_gateway.py +1 -1
- package/src/deepscientist/channels/relay.py +7 -1
- package/src/deepscientist/channels/weixin.py +59 -0
- package/src/deepscientist/channels/weixin_ilink.py +317 -0
- package/src/deepscientist/config/models.py +22 -2
- package/src/deepscientist/config/service.py +431 -60
- package/src/deepscientist/connector/__init__.py +4 -0
- package/src/deepscientist/connector/connector_profiles.py +481 -0
- package/src/deepscientist/connector/lingzhu_support.py +668 -0
- package/src/deepscientist/connector/qq_profiles.py +206 -0
- package/src/deepscientist/connector/weixin_support.py +663 -0
- package/src/deepscientist/connector_profiles.py +1 -374
- package/src/deepscientist/connector_runtime.py +2 -0
- package/src/deepscientist/daemon/api/handlers.py +295 -5
- package/src/deepscientist/daemon/api/router.py +16 -1
- package/src/deepscientist/daemon/app.py +1130 -61
- package/src/deepscientist/doctor.py +5 -2
- package/src/deepscientist/gitops/diff.py +120 -29
- package/src/deepscientist/lingzhu_support.py +1 -182
- package/src/deepscientist/mcp/server.py +14 -5
- package/src/deepscientist/prompts/builder.py +29 -1
- package/src/deepscientist/qq_profiles.py +1 -196
- package/src/deepscientist/quest/node_traces.py +152 -2
- package/src/deepscientist/quest/service.py +169 -43
- package/src/deepscientist/quest/stage_views.py +172 -9
- package/src/deepscientist/registries/baseline.py +56 -4
- package/src/deepscientist/runners/codex.py +55 -3
- package/src/deepscientist/weixin_support.py +1 -0
- package/src/prompts/connectors/lingzhu.md +3 -1
- package/src/prompts/connectors/weixin.md +230 -0
- package/src/prompts/system.md +9 -0
- package/src/skills/idea/SKILL.md +16 -0
- package/src/skills/idea/references/literature-survey-template.md +24 -0
- package/src/skills/idea/references/related-work-playbook.md +4 -0
- package/src/skills/idea/references/selection-gate.md +9 -0
- package/src/skills/write/SKILL.md +1 -1
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-m2FNtwbn.js → AiManusChatView-D0mTXG4-.js} +156 -48
- package/src/ui/dist/assets/{AnalysisPlugin-BMTF8EGL.js → AnalysisPlugin-Db0cTXxm.js} +1 -1
- package/src/ui/dist/assets/{CliPlugin-BEOWgxCI.js → CliPlugin-DrV8je02.js} +164 -9
- package/src/ui/dist/assets/{CodeEditorPlugin-BCXvjqmb.js → CodeEditorPlugin-QXMSCH71.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-DaJcy3nD.js → CodeViewerPlugin-7hhtWj_E.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-ByfeIq4K.js → DocViewerPlugin-BWMSnRJe.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-Cksf3VZ-.js → GitDiffViewerPlugin-7J9h9Vy_.js} +20 -21
- package/src/ui/dist/assets/{ImageViewerPlugin-CFz-OsTS.js → ImageViewerPlugin-CHJl_0lr.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-CJ1cJzoX.js → LabCopilotPanel-1qSow1es.js} +11 -11
- package/src/ui/dist/assets/{LabPlugin-BF3dVJwa.js → LabPlugin-eQpPPCEp.js} +2 -1
- package/src/ui/dist/assets/{LatexPlugin-DDkwZ6Sj.js → LatexPlugin-BwRfi89Z.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-HAuvurcT.js → MarkdownViewerPlugin-836PVQWV.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-BtoTYy2C.js → MarketplacePlugin-C2y_556i.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-CSJYx7b-.js → NotebookEditor-BRzJbGsn.js} +12 -12
- package/src/ui/dist/assets/{NotebookEditor-DQgRezm_.js → NotebookEditor-DIX7Mlzu.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-DPa_-fv6.js → PdfLoader-DzRaTAlq.js} +14 -7
- package/src/ui/dist/assets/{PdfMarkdownPlugin-BZpXOEjm.js → PdfMarkdownPlugin-DZUfIUnp.js} +73 -6
- package/src/ui/dist/assets/{PdfViewerPlugin-BT8a6wGR.js → PdfViewerPlugin-BwtICzue.js} +103 -34
- package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +3627 -0
- package/src/ui/dist/assets/{SearchPlugin-D_blveZi.js → SearchPlugin-DHeIAMsx.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-Btx0M3hX.js → TextViewerPlugin-C3tCmFox.js} +5 -4
- package/src/ui/dist/assets/{VNCViewer-DImJO4rO.js → VNCViewer-CQsKVm3t.js} +10 -10
- package/src/ui/dist/assets/bot-BEA2vWuK.js +21 -0
- package/src/ui/dist/assets/branding/logo-rokid.png +0 -0
- package/src/ui/dist/assets/browser-BAcuE0Xj.js +2895 -0
- package/src/ui/dist/assets/{code-BUfXGJSl.js → code-XfbSR8K2.js} +1 -1
- package/src/ui/dist/assets/{file-content-VqamwI3X.js → file-content-BjxNaIfy.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-C_wOoS7a.js → file-diff-panel-D_lLVQk0.js} +1 -1
- package/src/ui/dist/assets/{file-socket-D2bTuMVP.js → file-socket-D9x_5vlY.js} +1 -1
- package/src/ui/dist/assets/{image-BZkGJ4mM.js → image-BhWT33W1.js} +1 -1
- package/src/ui/dist/assets/{index-DdRW6RMJ.js → index--c4iXtuy.js} +12 -12
- package/src/ui/dist/assets/{index-CxkvSeKw.js → index-BDxipwrC.js} +2 -2
- package/src/ui/dist/assets/{index-DjggJovS.js → index-DZTZ8mWP.js} +14934 -9613
- package/src/ui/dist/assets/{index-DXZ1daiJ.css → index-Dqj-Mjb4.css} +2 -13
- package/src/ui/dist/assets/index-PJbSbPTy.js +25 -0
- package/src/ui/dist/assets/{monaco-DHMc7kKM.js → monaco-K8izTGgo.js} +1 -1
- package/src/ui/dist/assets/{pdf-effect-queue-DSw_D3RV.js → pdf-effect-queue-DfBors6y.js} +16 -1
- package/src/ui/dist/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
- package/src/ui/dist/assets/{popover-B85oCgCS.js → popover-yFK1J4fL.js} +1 -1
- package/src/ui/dist/assets/{project-sync-DOMCcPac.js → project-sync-PENr2zcz.js} +1 -74
- package/src/ui/dist/assets/select-CAbJDfYv.js +1690 -0
- package/src/ui/dist/assets/{sigma-BO2rQrl3.js → sigma-DEuYJqTl.js} +1 -1
- package/src/ui/dist/assets/{index-D9QIGcmc.js → square-check-big-omoSUmcd.js} +2 -13
- package/src/ui/dist/assets/{trash-BsVEH_dV.js → trash--F119N47.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-b8L6JuZm.js → useCliAccess-D31UR23I.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-BY7uA9hV.js → useFileDiffOverlay-BH6KcMzq.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-BwyVuUIK.js → wrap-text-CZ613PM5.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-RDpLugQP.js → zoom-out-BgDLAv3z.js} +1 -1
- package/src/ui/dist/index.html +2 -2
- package/src/ui/dist/assets/AutoFigurePlugin-BGxN8Umr.css +0 -3056
- package/src/ui/dist/assets/AutoFigurePlugin-DxPdMUNb.js +0 -8149
- package/src/ui/dist/assets/PdfViewerPlugin-BJXtIwj_.css +0 -260
- package/src/ui/dist/assets/Stepper-DH2k75Vo.js +0 -158
- package/src/ui/dist/assets/bibtex-B-Hqu0Sg.js +0 -189
- package/src/ui/dist/assets/file-utils--zJCPN1i.js +0 -109
- package/src/ui/dist/assets/message-square-FUIPIhU2.js +0 -16
- package/src/ui/dist/assets/pdfjs-DU1YE8WO.js +0 -3
- package/src/ui/dist/assets/tooltip-B1OspAkx.js +0 -108
|
@@ -5,6 +5,7 @@ import shutil
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
+
from ..arxiv_library import ArxivLibraryService
|
|
8
9
|
from ..bridges import register_builtin_connector_bridges
|
|
9
10
|
from ..channels import get_channel_factory, register_builtin_channels
|
|
10
11
|
from ..config import ConfigManager
|
|
@@ -38,7 +39,7 @@ from ..shared import (
|
|
|
38
39
|
)
|
|
39
40
|
from ..quest import QuestService
|
|
40
41
|
from ..memory.frontmatter import dump_markdown_document, load_markdown_document
|
|
41
|
-
from .arxiv import read_arxiv_content
|
|
42
|
+
from .arxiv import fetch_arxiv_metadata, read_arxiv_content
|
|
42
43
|
from .guidance import build_guidance_for_record, guidance_summary
|
|
43
44
|
from .metrics import (
|
|
44
45
|
baseline_metric_lines,
|
|
@@ -48,6 +49,7 @@ from .metrics import (
|
|
|
48
49
|
compute_progress_eval,
|
|
49
50
|
MetricContractValidationError,
|
|
50
51
|
normalize_metric_contract,
|
|
52
|
+
normalize_metric_direction,
|
|
51
53
|
normalize_metric_rows,
|
|
52
54
|
normalize_metrics_summary,
|
|
53
55
|
selected_baseline_metrics,
|
|
@@ -95,6 +97,77 @@ class ArtifactService:
|
|
|
95
97
|
self.home = home
|
|
96
98
|
self.baselines = BaselineRegistry(home)
|
|
97
99
|
self.quest_service = QuestService(home)
|
|
100
|
+
self.arxiv_library = ArxivLibraryService()
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def _notification_text(value: object) -> str | None:
|
|
104
|
+
text = str(value or "")
|
|
105
|
+
if not text.strip():
|
|
106
|
+
return None
|
|
107
|
+
normalized_lines: list[str] = []
|
|
108
|
+
for raw_line in text.replace("\r\n", "\n").replace("\r", "\n").split("\n"):
|
|
109
|
+
cleaned = re.sub(r"[ \t]+", " ", raw_line).strip()
|
|
110
|
+
if not cleaned:
|
|
111
|
+
if normalized_lines and normalized_lines[-1] != "":
|
|
112
|
+
normalized_lines.append("")
|
|
113
|
+
continue
|
|
114
|
+
normalized_lines.append(cleaned)
|
|
115
|
+
while normalized_lines and normalized_lines[-1] == "":
|
|
116
|
+
normalized_lines.pop()
|
|
117
|
+
rendered = "\n".join(normalized_lines).strip()
|
|
118
|
+
return rendered or None
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
def _notification_block(cls, value: object) -> str | None:
|
|
122
|
+
if value is None:
|
|
123
|
+
return None
|
|
124
|
+
if isinstance(value, dict):
|
|
125
|
+
lines: list[str] = []
|
|
126
|
+
for key, item in value.items():
|
|
127
|
+
label = cls._format_route_label(key) or str(key).strip()
|
|
128
|
+
block = cls._notification_block(item)
|
|
129
|
+
if not label or not block:
|
|
130
|
+
continue
|
|
131
|
+
block_lines = block.splitlines()
|
|
132
|
+
if len(block_lines) == 1:
|
|
133
|
+
lines.append(f"- {label}: {block_lines[0]}")
|
|
134
|
+
continue
|
|
135
|
+
lines.append(f"- {label}:")
|
|
136
|
+
lines.extend(f" {line}" if line else "" for line in block_lines)
|
|
137
|
+
return "\n".join(lines).strip() or None
|
|
138
|
+
if isinstance(value, (list, tuple, set)):
|
|
139
|
+
lines = []
|
|
140
|
+
for item in value:
|
|
141
|
+
block = cls._notification_block(item)
|
|
142
|
+
if not block:
|
|
143
|
+
continue
|
|
144
|
+
block_lines = block.splitlines()
|
|
145
|
+
if not block_lines:
|
|
146
|
+
continue
|
|
147
|
+
lines.append(f"- {block_lines[0]}")
|
|
148
|
+
lines.extend(f" {line}" if line else "" for line in block_lines[1:])
|
|
149
|
+
return "\n".join(lines).strip() or None
|
|
150
|
+
return cls._notification_text(value)
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def _append_notification_section(cls, lines: list[str], label: str, value: object) -> None:
|
|
154
|
+
block = cls._notification_block(value)
|
|
155
|
+
if not block:
|
|
156
|
+
return
|
|
157
|
+
lines.extend(["", f"{label}:", block])
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def _append_notification_file_section(lines: list[str], entries: list[tuple[str, str | None]]) -> None:
|
|
161
|
+
normalized = [
|
|
162
|
+
(label, str(path).strip())
|
|
163
|
+
for label, path in entries
|
|
164
|
+
if str(path or "").strip()
|
|
165
|
+
]
|
|
166
|
+
if not normalized:
|
|
167
|
+
return
|
|
168
|
+
lines.extend(["", "Files:"])
|
|
169
|
+
for label, path in normalized:
|
|
170
|
+
lines.append(f"- {label}: `{path}`")
|
|
98
171
|
|
|
99
172
|
def _normalize_evaluation_summary(self, payload: dict[str, Any] | None) -> dict[str, str] | None:
|
|
100
173
|
if not isinstance(payload, dict):
|
|
@@ -131,6 +204,302 @@ class ArtifactService:
|
|
|
131
204
|
lines = [f"- {label}: {normalized[key]}" for key, label in labels if normalized.get(key)]
|
|
132
205
|
return lines or ["- Not recorded."]
|
|
133
206
|
|
|
207
|
+
@staticmethod
|
|
208
|
+
def _format_route_label(value: object) -> str | None:
|
|
209
|
+
normalized = str(value or "").strip().replace("_", " ").replace("-", " ")
|
|
210
|
+
if not normalized:
|
|
211
|
+
return None
|
|
212
|
+
return " ".join(part.capitalize() for part in normalized.split())
|
|
213
|
+
|
|
214
|
+
def _format_foundation_label(self, foundation_ref: dict[str, Any] | None, *, fallback: str | None = None) -> str:
|
|
215
|
+
payload = dict(foundation_ref or {})
|
|
216
|
+
label = self._notification_text(payload.get("label"))
|
|
217
|
+
if label:
|
|
218
|
+
return label
|
|
219
|
+
kind = self._notification_text(payload.get("kind"))
|
|
220
|
+
ref = self._notification_text(payload.get("ref"))
|
|
221
|
+
branch = self._notification_text(payload.get("branch"))
|
|
222
|
+
if kind and ref:
|
|
223
|
+
return f"{kind} {ref}"
|
|
224
|
+
if branch:
|
|
225
|
+
return branch
|
|
226
|
+
return fallback or "current head"
|
|
227
|
+
|
|
228
|
+
def _build_idea_interaction_message(
|
|
229
|
+
self,
|
|
230
|
+
*,
|
|
231
|
+
action: str,
|
|
232
|
+
idea_id: str,
|
|
233
|
+
title: str | None,
|
|
234
|
+
problem: str | None,
|
|
235
|
+
hypothesis: str | None,
|
|
236
|
+
mechanism: str | None,
|
|
237
|
+
foundation_label: str | None,
|
|
238
|
+
branch_name: str,
|
|
239
|
+
next_target: str | None,
|
|
240
|
+
idea_md_rel_path: str | None,
|
|
241
|
+
draft_md_rel_path: str | None,
|
|
242
|
+
) -> str:
|
|
243
|
+
lead = "is now active" if action == "create" else "was revised"
|
|
244
|
+
lines = [f"Idea `{idea_id}` {lead} on branch `{branch_name}`."]
|
|
245
|
+
self._append_notification_section(lines, "Title", title)
|
|
246
|
+
self._append_notification_section(lines, "Problem", problem)
|
|
247
|
+
self._append_notification_section(lines, "Hypothesis", hypothesis)
|
|
248
|
+
self._append_notification_section(lines, "Mechanism", mechanism)
|
|
249
|
+
if foundation_label:
|
|
250
|
+
self._append_notification_section(lines, "Foundation", foundation_label)
|
|
251
|
+
if next_target:
|
|
252
|
+
self._append_notification_section(lines, "Next route", self._format_route_label(next_target) or next_target)
|
|
253
|
+
self._append_notification_file_section(
|
|
254
|
+
lines,
|
|
255
|
+
[
|
|
256
|
+
("Idea doc", idea_md_rel_path),
|
|
257
|
+
("Draft", draft_md_rel_path),
|
|
258
|
+
],
|
|
259
|
+
)
|
|
260
|
+
return "\n".join(lines)
|
|
261
|
+
|
|
262
|
+
def _build_main_experiment_interaction_message(
|
|
263
|
+
self,
|
|
264
|
+
*,
|
|
265
|
+
run_id: str,
|
|
266
|
+
branch_name: str,
|
|
267
|
+
verdict: str,
|
|
268
|
+
primary_metric_id: str | None,
|
|
269
|
+
primary_value: object,
|
|
270
|
+
primary_baseline: object,
|
|
271
|
+
primary_delta: object,
|
|
272
|
+
decimals: int | None,
|
|
273
|
+
conclusion: str | None,
|
|
274
|
+
evaluation_summary: dict[str, str] | None,
|
|
275
|
+
breakthrough_level: str | None,
|
|
276
|
+
recommended_next_route: str | None,
|
|
277
|
+
run_md_rel_path: str | None,
|
|
278
|
+
result_json_rel_path: str | None,
|
|
279
|
+
) -> str:
|
|
280
|
+
lines = [f"Main experiment `{run_id}` finished on branch `{branch_name}`."]
|
|
281
|
+
outcome_lines: list[str] = []
|
|
282
|
+
if primary_metric_id and primary_value is not None:
|
|
283
|
+
metric_text = f"{primary_metric_id}={self._format_metric_value(primary_value, decimals)}"
|
|
284
|
+
if primary_baseline is not None and primary_delta is not None:
|
|
285
|
+
metric_text += (
|
|
286
|
+
f", baseline={self._format_metric_value(primary_baseline, decimals)}, "
|
|
287
|
+
f"delta={self._format_metric_value(primary_delta, decimals)}"
|
|
288
|
+
)
|
|
289
|
+
outcome_lines.append(f"- Metric: {metric_text}")
|
|
290
|
+
outcome_lines.append(f"- Verdict: {self._format_route_label(verdict) or verdict}")
|
|
291
|
+
if self._notification_text(breakthrough_level):
|
|
292
|
+
outcome_lines.append(f"- Breakthrough level: {self._notification_text(breakthrough_level)}")
|
|
293
|
+
if recommended_next_route:
|
|
294
|
+
outcome_lines.append(
|
|
295
|
+
f"- Recommended next route: {self._format_route_label(recommended_next_route) or recommended_next_route}"
|
|
296
|
+
)
|
|
297
|
+
if outcome_lines:
|
|
298
|
+
lines.extend(["", "Outcome:", *outcome_lines])
|
|
299
|
+
self._append_notification_section(
|
|
300
|
+
lines,
|
|
301
|
+
"Conclusion",
|
|
302
|
+
self._notification_text(conclusion) or (evaluation_summary or {}).get("takeaway"),
|
|
303
|
+
)
|
|
304
|
+
normalized_evaluation_summary = self._normalize_evaluation_summary(evaluation_summary)
|
|
305
|
+
if normalized_evaluation_summary:
|
|
306
|
+
lines.extend(["", "Evaluation summary:", *self._evaluation_summary_markdown_lines(normalized_evaluation_summary)])
|
|
307
|
+
self._append_notification_file_section(
|
|
308
|
+
lines,
|
|
309
|
+
[
|
|
310
|
+
("Run log", run_md_rel_path),
|
|
311
|
+
("Result", result_json_rel_path),
|
|
312
|
+
],
|
|
313
|
+
)
|
|
314
|
+
return "\n".join(lines)
|
|
315
|
+
|
|
316
|
+
def _build_outline_interaction_message(
|
|
317
|
+
self,
|
|
318
|
+
*,
|
|
319
|
+
action: str,
|
|
320
|
+
outline_id: str,
|
|
321
|
+
title: str | None,
|
|
322
|
+
selected_reason: str | None,
|
|
323
|
+
story: str | None,
|
|
324
|
+
research_questions: object,
|
|
325
|
+
experimental_designs: object,
|
|
326
|
+
selected_outline_rel_path: str | None,
|
|
327
|
+
outline_selection_rel_path: str | None,
|
|
328
|
+
revised_outline_rel_path: str | None = None,
|
|
329
|
+
) -> str:
|
|
330
|
+
verb = "selected" if action == "select" else "revised"
|
|
331
|
+
lines = [f"Paper outline `{outline_id}` was {verb} and promoted into the writing stage."]
|
|
332
|
+
self._append_notification_section(lines, "Title", title)
|
|
333
|
+
self._append_notification_section(lines, "Reason", selected_reason)
|
|
334
|
+
self._append_notification_section(lines, "Story", story)
|
|
335
|
+
self._append_notification_section(lines, "Research questions", research_questions)
|
|
336
|
+
self._append_notification_section(lines, "Experimental designs", experimental_designs)
|
|
337
|
+
self._append_notification_section(
|
|
338
|
+
lines,
|
|
339
|
+
"Next route",
|
|
340
|
+
"Continue writing on the paper branch, or launch outline-bound analysis if evidence is still missing.",
|
|
341
|
+
)
|
|
342
|
+
self._append_notification_file_section(
|
|
343
|
+
lines,
|
|
344
|
+
[
|
|
345
|
+
("Selected outline", selected_outline_rel_path),
|
|
346
|
+
("Selection note", outline_selection_rel_path),
|
|
347
|
+
("Revision record", revised_outline_rel_path),
|
|
348
|
+
],
|
|
349
|
+
)
|
|
350
|
+
return "\n".join(lines)
|
|
351
|
+
|
|
352
|
+
def _build_analysis_campaign_interaction_message(
|
|
353
|
+
self,
|
|
354
|
+
*,
|
|
355
|
+
campaign_id: str,
|
|
356
|
+
goal: str | None,
|
|
357
|
+
parent_branch: str,
|
|
358
|
+
selected_outline_ref: str | None,
|
|
359
|
+
first_slice: dict[str, Any],
|
|
360
|
+
todo_manifest_rel_path: str | None,
|
|
361
|
+
) -> str:
|
|
362
|
+
lines = [f"Analysis campaign `{campaign_id}` is ready from parent branch `{parent_branch}`."]
|
|
363
|
+
self._append_notification_section(lines, "Goal", goal)
|
|
364
|
+
if selected_outline_ref:
|
|
365
|
+
self._append_notification_section(lines, "Selected outline", f"`{selected_outline_ref}`")
|
|
366
|
+
next_slice_lines = [
|
|
367
|
+
f"- Slice: `{first_slice.get('slice_id')}`",
|
|
368
|
+
f"- Branch: `{first_slice.get('branch')}`",
|
|
369
|
+
]
|
|
370
|
+
if self._notification_text(first_slice.get("title")):
|
|
371
|
+
next_slice_lines.append(f"- Focus: {self._notification_text(first_slice.get('title'))}")
|
|
372
|
+
lines.extend(["", "Next slice:", *next_slice_lines])
|
|
373
|
+
requirement = self._notification_text(first_slice.get("must_not_simplify") or first_slice.get("goal"))
|
|
374
|
+
if requirement:
|
|
375
|
+
self._append_notification_section(lines, "Core requirement", requirement)
|
|
376
|
+
self._append_notification_file_section(lines, [("Todo manifest", todo_manifest_rel_path)])
|
|
377
|
+
return "\n".join(lines)
|
|
378
|
+
|
|
379
|
+
def _build_analysis_slice_interaction_message(
|
|
380
|
+
self,
|
|
381
|
+
*,
|
|
382
|
+
campaign_id: str,
|
|
383
|
+
slice_id: str,
|
|
384
|
+
evaluation_summary: dict[str, str] | None,
|
|
385
|
+
claim_impact: str | None,
|
|
386
|
+
next_slice: dict[str, Any],
|
|
387
|
+
mirror_rel_path: str | None,
|
|
388
|
+
) -> str:
|
|
389
|
+
lines = [f"Analysis slice `{slice_id}` from campaign `{campaign_id}` is complete."]
|
|
390
|
+
normalized_evaluation_summary = self._normalize_evaluation_summary(evaluation_summary)
|
|
391
|
+
if normalized_evaluation_summary:
|
|
392
|
+
lines.extend(["", "Evaluation summary:", *self._evaluation_summary_markdown_lines(normalized_evaluation_summary)])
|
|
393
|
+
self._append_notification_section(lines, "Claim impact", claim_impact)
|
|
394
|
+
lines.extend(
|
|
395
|
+
[
|
|
396
|
+
"",
|
|
397
|
+
"Next slice:",
|
|
398
|
+
f"- Slice: `{next_slice.get('slice_id')}`",
|
|
399
|
+
f"- Branch: `{next_slice.get('branch')}`",
|
|
400
|
+
]
|
|
401
|
+
)
|
|
402
|
+
requirement = self._notification_text(next_slice.get("must_not_simplify") or next_slice.get("goal"))
|
|
403
|
+
if requirement:
|
|
404
|
+
self._append_notification_section(lines, "Core requirement", requirement)
|
|
405
|
+
self._append_notification_file_section(lines, [("Parent mirror", mirror_rel_path)])
|
|
406
|
+
return "\n".join(lines)
|
|
407
|
+
|
|
408
|
+
def _build_analysis_complete_interaction_message(
|
|
409
|
+
self,
|
|
410
|
+
*,
|
|
411
|
+
campaign_id: str,
|
|
412
|
+
completed_slices: list[dict[str, Any]],
|
|
413
|
+
summary_rel_path: str | None,
|
|
414
|
+
writing_branch: str | None,
|
|
415
|
+
writing_worktree_rel_path: str | None,
|
|
416
|
+
) -> str:
|
|
417
|
+
lines = [f"Analysis campaign `{campaign_id}` is complete."]
|
|
418
|
+
overview_lines = [f"- Completed slices: {len(completed_slices)}"]
|
|
419
|
+
if writing_branch:
|
|
420
|
+
overview_lines.append(f"- Next route: writing is active on branch `{writing_branch}`")
|
|
421
|
+
if writing_worktree_rel_path:
|
|
422
|
+
overview_lines.append(f"- Writing workspace: `{writing_worktree_rel_path}`")
|
|
423
|
+
else:
|
|
424
|
+
overview_lines.append("- Next route: make the next durable decision from the merged analysis evidence.")
|
|
425
|
+
lines.extend(["", "Overview:", *overview_lines])
|
|
426
|
+
completed_slice_lines: list[str] = []
|
|
427
|
+
for item in completed_slices:
|
|
428
|
+
slice_id = str(item.get("slice_id") or "").strip() or "unknown"
|
|
429
|
+
title = self._notification_text(item.get("title"))
|
|
430
|
+
lead = f"- `{slice_id}`"
|
|
431
|
+
if title:
|
|
432
|
+
lead += f": {title}"
|
|
433
|
+
completed_slice_lines.append(lead)
|
|
434
|
+
takeaway = self._notification_text(
|
|
435
|
+
((item.get("evaluation_summary") or {}) if isinstance(item.get("evaluation_summary"), dict) else {}).get(
|
|
436
|
+
"takeaway"
|
|
437
|
+
)
|
|
438
|
+
)
|
|
439
|
+
if takeaway:
|
|
440
|
+
completed_slice_lines.append(f" Takeaway: {takeaway}")
|
|
441
|
+
claim_impact = self._notification_text(item.get("claim_impact"))
|
|
442
|
+
if claim_impact:
|
|
443
|
+
completed_slice_lines.append(f" Claim impact: {claim_impact}")
|
|
444
|
+
if completed_slice_lines:
|
|
445
|
+
lines.extend(["", "Completed slices:", *completed_slice_lines])
|
|
446
|
+
self._append_notification_file_section(lines, [("Summary", summary_rel_path)])
|
|
447
|
+
return "\n".join(lines)
|
|
448
|
+
|
|
449
|
+
def _build_paper_bundle_interaction_message(
|
|
450
|
+
self,
|
|
451
|
+
*,
|
|
452
|
+
title: str | None,
|
|
453
|
+
summary: str | None,
|
|
454
|
+
paper_branch: str | None,
|
|
455
|
+
source_branch: str | None,
|
|
456
|
+
source_run_id: str | None,
|
|
457
|
+
selected_outline_ref: str | None,
|
|
458
|
+
manifest_rel_path: str | None,
|
|
459
|
+
draft_rel_path: str | None,
|
|
460
|
+
writing_plan_rel_path: str | None,
|
|
461
|
+
references_rel_path: str | None,
|
|
462
|
+
claim_evidence_map_rel_path: str | None,
|
|
463
|
+
compile_report_rel_path: str | None,
|
|
464
|
+
pdf_rel_path: str | None,
|
|
465
|
+
latex_root_rel_path: str | None,
|
|
466
|
+
baseline_inventory_rel_path: str | None,
|
|
467
|
+
open_source_manifest_rel_path: str | None,
|
|
468
|
+
) -> str:
|
|
469
|
+
bundle_label = self._notification_text(title) or "paper"
|
|
470
|
+
lines = [f"Paper bundle `{bundle_label}` is ready on branch `{paper_branch or 'paper'}`."]
|
|
471
|
+
overview_lines: list[str] = []
|
|
472
|
+
if source_branch:
|
|
473
|
+
overview_lines.append(f"- Source branch: `{source_branch}`")
|
|
474
|
+
if source_run_id:
|
|
475
|
+
overview_lines.append(f"- Source run: `{source_run_id}`")
|
|
476
|
+
if selected_outline_ref:
|
|
477
|
+
overview_lines.append(f"- Selected outline: `{selected_outline_ref}`")
|
|
478
|
+
if overview_lines:
|
|
479
|
+
lines.extend(["", "Overview:", *overview_lines])
|
|
480
|
+
self._append_notification_section(lines, "Summary", summary)
|
|
481
|
+
self._append_notification_file_section(
|
|
482
|
+
lines,
|
|
483
|
+
[
|
|
484
|
+
("Bundle manifest", manifest_rel_path),
|
|
485
|
+
("Draft", draft_rel_path),
|
|
486
|
+
("Writing plan", writing_plan_rel_path),
|
|
487
|
+
("References", references_rel_path),
|
|
488
|
+
("Claim-evidence map", claim_evidence_map_rel_path),
|
|
489
|
+
("Compile report", compile_report_rel_path),
|
|
490
|
+
("PDF", pdf_rel_path),
|
|
491
|
+
("LaTeX root", latex_root_rel_path),
|
|
492
|
+
("Baseline inventory", baseline_inventory_rel_path),
|
|
493
|
+
("Open-source manifest", open_source_manifest_rel_path),
|
|
494
|
+
],
|
|
495
|
+
)
|
|
496
|
+
self._append_notification_section(
|
|
497
|
+
lines,
|
|
498
|
+
"Next route",
|
|
499
|
+
"Finalize the paper package, review the bundle artifacts, and publish or close the quest when ready.",
|
|
500
|
+
)
|
|
501
|
+
return "\n".join(lines)
|
|
502
|
+
|
|
134
503
|
def _load_metric_contract_payload(self, quest_root: Path, metric_contract_json_rel_path: str | None) -> dict[str, Any] | None:
|
|
135
504
|
rel_path = str(metric_contract_json_rel_path or "").strip()
|
|
136
505
|
if not rel_path:
|
|
@@ -144,6 +513,173 @@ class ArtifactService:
|
|
|
144
513
|
payload = read_json(resolved_path, {})
|
|
145
514
|
return payload if isinstance(payload, dict) and payload else None
|
|
146
515
|
|
|
516
|
+
def _normalize_metric_directions(self, metric_directions: object) -> dict[str, str]:
|
|
517
|
+
if not isinstance(metric_directions, dict):
|
|
518
|
+
return {}
|
|
519
|
+
normalized: dict[str, str] = {}
|
|
520
|
+
for raw_metric_id, raw_direction in metric_directions.items():
|
|
521
|
+
metric_id = str(raw_metric_id or "").strip()
|
|
522
|
+
if not metric_id:
|
|
523
|
+
continue
|
|
524
|
+
normalized[metric_id] = normalize_metric_direction(raw_direction, metric_id=metric_id)
|
|
525
|
+
return normalized
|
|
526
|
+
|
|
527
|
+
def _apply_metric_directions_to_contract(
|
|
528
|
+
self,
|
|
529
|
+
*,
|
|
530
|
+
metric_contract: object,
|
|
531
|
+
metric_directions: object,
|
|
532
|
+
baseline_id: str | None = None,
|
|
533
|
+
metrics_summary: object = None,
|
|
534
|
+
metric_rows: object = None,
|
|
535
|
+
primary_metric: object = None,
|
|
536
|
+
baseline_variants: object = None,
|
|
537
|
+
) -> tuple[dict[str, Any], dict[str, Any] | None]:
|
|
538
|
+
normalized_contract = normalize_metric_contract(
|
|
539
|
+
metric_contract,
|
|
540
|
+
baseline_id=baseline_id,
|
|
541
|
+
metrics_summary=metrics_summary,
|
|
542
|
+
metric_rows=metric_rows,
|
|
543
|
+
primary_metric=primary_metric,
|
|
544
|
+
baseline_variants=baseline_variants,
|
|
545
|
+
)
|
|
546
|
+
normalized_primary_metric = dict(primary_metric or {}) if isinstance(primary_metric, dict) else None
|
|
547
|
+
overrides = self._normalize_metric_directions(metric_directions)
|
|
548
|
+
if not overrides:
|
|
549
|
+
return normalized_contract, normalized_primary_metric
|
|
550
|
+
|
|
551
|
+
metrics_by_id: dict[str, dict[str, Any]] = {}
|
|
552
|
+
ordered_metric_ids: list[str] = []
|
|
553
|
+
for raw_metric in normalized_contract.get("metrics", []):
|
|
554
|
+
if not isinstance(raw_metric, dict):
|
|
555
|
+
continue
|
|
556
|
+
metric_id = str(raw_metric.get("metric_id") or "").strip()
|
|
557
|
+
if not metric_id:
|
|
558
|
+
continue
|
|
559
|
+
metrics_by_id[metric_id] = dict(raw_metric)
|
|
560
|
+
ordered_metric_ids.append(metric_id)
|
|
561
|
+
for metric_id, direction in overrides.items():
|
|
562
|
+
current = metrics_by_id.get(metric_id)
|
|
563
|
+
if current is None:
|
|
564
|
+
current = {
|
|
565
|
+
"metric_id": metric_id,
|
|
566
|
+
"label": metric_id,
|
|
567
|
+
"direction": direction,
|
|
568
|
+
"unit": None,
|
|
569
|
+
"decimals": None,
|
|
570
|
+
"chart_group": "default",
|
|
571
|
+
}
|
|
572
|
+
ordered_metric_ids.append(metric_id)
|
|
573
|
+
else:
|
|
574
|
+
current = {
|
|
575
|
+
**current,
|
|
576
|
+
"direction": direction,
|
|
577
|
+
}
|
|
578
|
+
metrics_by_id[metric_id] = current
|
|
579
|
+
|
|
580
|
+
primary_metric_id = str(
|
|
581
|
+
(normalized_primary_metric or {}).get("metric_id")
|
|
582
|
+
or (normalized_primary_metric or {}).get("name")
|
|
583
|
+
or (normalized_primary_metric or {}).get("id")
|
|
584
|
+
or normalized_contract.get("primary_metric_id")
|
|
585
|
+
or ""
|
|
586
|
+
).strip()
|
|
587
|
+
if normalized_primary_metric and primary_metric_id in overrides:
|
|
588
|
+
normalized_primary_metric = {
|
|
589
|
+
**normalized_primary_metric,
|
|
590
|
+
"direction": overrides[primary_metric_id],
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return {
|
|
594
|
+
**normalized_contract,
|
|
595
|
+
"metrics": [metrics_by_id[metric_id] for metric_id in ordered_metric_ids if metric_id in metrics_by_id],
|
|
596
|
+
}, normalized_primary_metric
|
|
597
|
+
|
|
598
|
+
def _merge_run_metric_contract(
|
|
599
|
+
self,
|
|
600
|
+
*,
|
|
601
|
+
baseline_metric_contract: object,
|
|
602
|
+
baseline_primary_metric: object,
|
|
603
|
+
baseline_variants: object,
|
|
604
|
+
run_metric_contract: object,
|
|
605
|
+
metrics_summary: object,
|
|
606
|
+
metric_rows: object,
|
|
607
|
+
baseline_id: str | None = None,
|
|
608
|
+
) -> dict[str, Any]:
|
|
609
|
+
baseline_contract = normalize_metric_contract(
|
|
610
|
+
baseline_metric_contract,
|
|
611
|
+
baseline_id=baseline_id,
|
|
612
|
+
metrics_summary=metrics_summary,
|
|
613
|
+
metric_rows=metric_rows,
|
|
614
|
+
primary_metric=baseline_primary_metric,
|
|
615
|
+
baseline_variants=baseline_variants,
|
|
616
|
+
)
|
|
617
|
+
if not isinstance(run_metric_contract, dict) or not run_metric_contract:
|
|
618
|
+
return baseline_contract
|
|
619
|
+
|
|
620
|
+
overlay_contract = normalize_metric_contract(
|
|
621
|
+
run_metric_contract,
|
|
622
|
+
baseline_id=baseline_id,
|
|
623
|
+
metrics_summary=metrics_summary,
|
|
624
|
+
metric_rows=metric_rows,
|
|
625
|
+
primary_metric=baseline_contract.get("primary_metric_id"),
|
|
626
|
+
)
|
|
627
|
+
overlay_metrics: dict[str, dict[str, Any]] = {}
|
|
628
|
+
for raw_metric in overlay_contract.get("metrics", []):
|
|
629
|
+
if not isinstance(raw_metric, dict):
|
|
630
|
+
continue
|
|
631
|
+
metric_id = str(raw_metric.get("metric_id") or "").strip()
|
|
632
|
+
if metric_id:
|
|
633
|
+
overlay_metrics[metric_id] = raw_metric
|
|
634
|
+
|
|
635
|
+
merged_metrics: list[dict[str, Any]] = []
|
|
636
|
+
seen_metric_ids: set[str] = set()
|
|
637
|
+
for raw_metric in baseline_contract.get("metrics", []):
|
|
638
|
+
if not isinstance(raw_metric, dict):
|
|
639
|
+
continue
|
|
640
|
+
metric_id = str(raw_metric.get("metric_id") or "").strip()
|
|
641
|
+
if not metric_id:
|
|
642
|
+
continue
|
|
643
|
+
patch = overlay_metrics.get(metric_id) or {}
|
|
644
|
+
merged = dict(raw_metric)
|
|
645
|
+
for field in (
|
|
646
|
+
"label",
|
|
647
|
+
"unit",
|
|
648
|
+
"decimals",
|
|
649
|
+
"chart_group",
|
|
650
|
+
"description",
|
|
651
|
+
"derivation",
|
|
652
|
+
"source_ref",
|
|
653
|
+
"required",
|
|
654
|
+
"origin_path",
|
|
655
|
+
):
|
|
656
|
+
value = patch.get(field)
|
|
657
|
+
if value is None:
|
|
658
|
+
continue
|
|
659
|
+
if isinstance(value, str) and not value.strip():
|
|
660
|
+
continue
|
|
661
|
+
merged[field] = value
|
|
662
|
+
merged_metrics.append(merged)
|
|
663
|
+
seen_metric_ids.add(metric_id)
|
|
664
|
+
|
|
665
|
+
for metric_id, raw_metric in overlay_metrics.items():
|
|
666
|
+
if metric_id in seen_metric_ids:
|
|
667
|
+
continue
|
|
668
|
+
merged_metrics.append(dict(raw_metric))
|
|
669
|
+
|
|
670
|
+
merged_contract = {
|
|
671
|
+
**baseline_contract,
|
|
672
|
+
"metrics": merged_metrics,
|
|
673
|
+
}
|
|
674
|
+
if not merged_contract.get("evaluation_protocol") and overlay_contract.get("evaluation_protocol") is not None:
|
|
675
|
+
merged_contract["evaluation_protocol"] = overlay_contract.get("evaluation_protocol")
|
|
676
|
+
for key, value in overlay_contract.items():
|
|
677
|
+
if key in {"contract_id", "primary_metric_id", "metrics", "evaluation_protocol"}:
|
|
678
|
+
continue
|
|
679
|
+
if key not in merged_contract and value is not None:
|
|
680
|
+
merged_contract[key] = value
|
|
681
|
+
return merged_contract
|
|
682
|
+
|
|
147
683
|
def _workspace_root_for(self, quest_root: Path, workspace_root: Path | None = None) -> Path:
|
|
148
684
|
if workspace_root is not None:
|
|
149
685
|
return workspace_root
|
|
@@ -999,6 +1535,9 @@ class ArtifactService:
|
|
|
999
1535
|
continue
|
|
1000
1536
|
seen_paths.add(key)
|
|
1001
1537
|
payload = read_yaml(path, {})
|
|
1538
|
+
baseline_id = str(payload.get("source_baseline_id") or "").strip() if isinstance(payload, dict) else ""
|
|
1539
|
+
if baseline_id and self.baselines.is_deleted(baseline_id):
|
|
1540
|
+
continue
|
|
1002
1541
|
if isinstance(payload, dict) and payload:
|
|
1003
1542
|
attachments.append(payload)
|
|
1004
1543
|
if not attachments:
|
|
@@ -1011,6 +1550,48 @@ class ArtifactService:
|
|
|
1011
1550
|
),
|
|
1012
1551
|
)
|
|
1013
1552
|
|
|
1553
|
+
def _baseline_workspace_roots(self, quest_root: Path) -> list[Path]:
|
|
1554
|
+
roots: list[Path] = [quest_root]
|
|
1555
|
+
research_state = read_json(quest_root / ".ds" / "research_state.json", {})
|
|
1556
|
+
if isinstance(research_state, dict):
|
|
1557
|
+
for key in (
|
|
1558
|
+
"research_head_worktree_root",
|
|
1559
|
+
"current_workspace_root",
|
|
1560
|
+
"analysis_parent_worktree_root",
|
|
1561
|
+
"paper_parent_worktree_root",
|
|
1562
|
+
):
|
|
1563
|
+
raw = str(research_state.get(key) or "").strip()
|
|
1564
|
+
if raw:
|
|
1565
|
+
roots.append(Path(raw))
|
|
1566
|
+
worktrees_root = quest_root / ".ds" / "worktrees"
|
|
1567
|
+
if worktrees_root.exists():
|
|
1568
|
+
roots.extend(path for path in sorted(worktrees_root.iterdir()) if path.is_dir())
|
|
1569
|
+
deduped: list[Path] = []
|
|
1570
|
+
seen: set[str] = set()
|
|
1571
|
+
for root in roots:
|
|
1572
|
+
key = str(root.resolve(strict=False))
|
|
1573
|
+
if key in seen:
|
|
1574
|
+
continue
|
|
1575
|
+
seen.add(key)
|
|
1576
|
+
deduped.append(root)
|
|
1577
|
+
return deduped
|
|
1578
|
+
|
|
1579
|
+
@staticmethod
|
|
1580
|
+
def _remove_baseline_materialization(root: Path, baseline_id: str) -> list[str]:
|
|
1581
|
+
deleted_paths: list[str] = []
|
|
1582
|
+
for candidate in (
|
|
1583
|
+
root / "baselines" / "imported" / baseline_id,
|
|
1584
|
+
root / "baselines" / "local" / baseline_id,
|
|
1585
|
+
):
|
|
1586
|
+
if not candidate.exists():
|
|
1587
|
+
continue
|
|
1588
|
+
if candidate.is_dir():
|
|
1589
|
+
shutil.rmtree(candidate)
|
|
1590
|
+
else:
|
|
1591
|
+
candidate.unlink()
|
|
1592
|
+
deleted_paths.append(str(candidate))
|
|
1593
|
+
return deleted_paths
|
|
1594
|
+
|
|
1014
1595
|
def _resolve_baseline_path(
|
|
1015
1596
|
self,
|
|
1016
1597
|
quest_root: Path,
|
|
@@ -1867,8 +2448,19 @@ class ArtifactService:
|
|
|
1867
2448
|
) -> tuple[str, Path, str | None]:
|
|
1868
2449
|
current_root_raw = str(state.get("current_workspace_root") or "").strip()
|
|
1869
2450
|
head_root_raw = str(state.get("research_head_worktree_root") or "").strip()
|
|
2451
|
+
paper_parent_root_raw = str(state.get("paper_parent_worktree_root") or "").strip()
|
|
2452
|
+
current_branch_raw = str(state.get("current_workspace_branch") or "").strip()
|
|
2453
|
+
research_head_branch_raw = str(state.get("research_head_branch") or "").strip()
|
|
2454
|
+
paper_parent_branch_raw = str(state.get("paper_parent_branch") or "").strip()
|
|
2455
|
+
workspace_mode = str(state.get("workspace_mode") or "").strip().lower()
|
|
2456
|
+
prefer_paper_parent = workspace_mode == "paper" or self._branch_kind_from_name(current_branch_raw) == "paper"
|
|
1870
2457
|
parent_worktree_root: Path | None = None
|
|
1871
|
-
|
|
2458
|
+
root_candidates = (
|
|
2459
|
+
(paper_parent_root_raw, head_root_raw, current_root_raw)
|
|
2460
|
+
if prefer_paper_parent
|
|
2461
|
+
else (current_root_raw, head_root_raw, paper_parent_root_raw)
|
|
2462
|
+
)
|
|
2463
|
+
for raw in root_candidates:
|
|
1872
2464
|
if not raw:
|
|
1873
2465
|
continue
|
|
1874
2466
|
candidate = Path(raw)
|
|
@@ -1879,10 +2471,21 @@ class ArtifactService:
|
|
|
1879
2471
|
parent_worktree_root = self._workspace_root_for(quest_root)
|
|
1880
2472
|
|
|
1881
2473
|
parent_branch = (
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
2474
|
+
(
|
|
2475
|
+
paper_parent_branch_raw
|
|
2476
|
+
or research_head_branch_raw
|
|
2477
|
+
or current_branch_raw
|
|
2478
|
+
or current_branch(parent_worktree_root)
|
|
2479
|
+
or current_branch(self._workspace_root_for(quest_root))
|
|
2480
|
+
)
|
|
2481
|
+
if prefer_paper_parent
|
|
2482
|
+
else (
|
|
2483
|
+
current_branch_raw
|
|
2484
|
+
or research_head_branch_raw
|
|
2485
|
+
or paper_parent_branch_raw
|
|
2486
|
+
or current_branch(parent_worktree_root)
|
|
2487
|
+
or current_branch(self._workspace_root_for(quest_root))
|
|
2488
|
+
)
|
|
1886
2489
|
)
|
|
1887
2490
|
parent_branch = str(parent_branch or "").strip()
|
|
1888
2491
|
if not parent_branch:
|
|
@@ -2278,32 +2881,82 @@ class ArtifactService:
|
|
|
2278
2881
|
}
|
|
2279
2882
|
|
|
2280
2883
|
def list_paper_outlines(self, quest_root: Path) -> dict[str, Any]:
|
|
2281
|
-
|
|
2884
|
+
selected_outline_path = self._paper_selected_outline_path(quest_root)
|
|
2885
|
+
selected_outline = read_json(selected_outline_path, {})
|
|
2282
2886
|
selected_outline = selected_outline if isinstance(selected_outline, dict) else {}
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
(
|
|
2286
|
-
(
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2887
|
+
if not selected_outline:
|
|
2888
|
+
fallback_selected_outline_path = quest_root / "paper" / "selected_outline.json"
|
|
2889
|
+
fallback_selected_outline = read_json(fallback_selected_outline_path, {})
|
|
2890
|
+
if isinstance(fallback_selected_outline, dict) and fallback_selected_outline:
|
|
2891
|
+
selected_outline = fallback_selected_outline
|
|
2892
|
+
selected_outline_path = fallback_selected_outline_path
|
|
2893
|
+
|
|
2894
|
+
selected_outline_id = str(selected_outline.get("outline_id") or "").strip()
|
|
2895
|
+
status_rank = {"candidate": 1, "revised": 2, "selected": 3}
|
|
2896
|
+
outlines_by_id: dict[str, dict[str, Any]] = {}
|
|
2897
|
+
seen_paper_roots: set[str] = set()
|
|
2898
|
+
paper_roots: list[Path] = []
|
|
2899
|
+
for root in (self._paper_root(quest_root), quest_root / "paper"):
|
|
2900
|
+
try:
|
|
2901
|
+
key = str(root.resolve())
|
|
2902
|
+
except FileNotFoundError:
|
|
2903
|
+
key = str(root)
|
|
2904
|
+
if key in seen_paper_roots:
|
|
2905
|
+
continue
|
|
2906
|
+
seen_paper_roots.add(key)
|
|
2907
|
+
paper_roots.append(root)
|
|
2908
|
+
|
|
2909
|
+
for paper_root in paper_roots:
|
|
2910
|
+
for default_status, relative_parts in (
|
|
2911
|
+
("candidate", ("outlines", "candidates")),
|
|
2912
|
+
("revised", ("outlines", "revisions")),
|
|
2913
|
+
):
|
|
2914
|
+
root = paper_root.joinpath(*relative_parts)
|
|
2915
|
+
if not root.exists():
|
|
2291
2916
|
continue
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2917
|
+
for path in sorted(root.glob("outline-*.json")):
|
|
2918
|
+
record = read_json(path, {})
|
|
2919
|
+
if not isinstance(record, dict) or not record:
|
|
2920
|
+
continue
|
|
2921
|
+
outline_id = str(record.get("outline_id") or path.stem).strip() or path.stem
|
|
2922
|
+
item = {
|
|
2295
2923
|
"outline_id": outline_id,
|
|
2296
2924
|
"title": str(record.get("title") or outline_id).strip() or outline_id,
|
|
2297
|
-
"status": str(record.get("status") or
|
|
2925
|
+
"status": str(record.get("status") or default_status).strip() or default_status,
|
|
2298
2926
|
"review_result": str(record.get("review_result") or "").strip() or None,
|
|
2299
2927
|
"path": str(path),
|
|
2300
|
-
"is_selected": outline_id ==
|
|
2928
|
+
"is_selected": outline_id == selected_outline_id,
|
|
2301
2929
|
}
|
|
2302
|
-
|
|
2930
|
+
current = outlines_by_id.get(outline_id)
|
|
2931
|
+
if current is None or status_rank.get(str(item.get("status") or ""), 0) >= status_rank.get(
|
|
2932
|
+
str(current.get("status") or ""),
|
|
2933
|
+
0,
|
|
2934
|
+
):
|
|
2935
|
+
outlines_by_id[outline_id] = item
|
|
2936
|
+
|
|
2937
|
+
if selected_outline_id:
|
|
2938
|
+
selected_item = {
|
|
2939
|
+
"outline_id": selected_outline_id,
|
|
2940
|
+
"title": str(selected_outline.get("title") or selected_outline_id).strip() or selected_outline_id,
|
|
2941
|
+
"status": str(selected_outline.get("status") or "selected").strip() or "selected",
|
|
2942
|
+
"review_result": str(selected_outline.get("review_result") or "").strip() or None,
|
|
2943
|
+
"path": str(selected_outline_path),
|
|
2944
|
+
"is_selected": True,
|
|
2945
|
+
}
|
|
2946
|
+
current = outlines_by_id.get(selected_outline_id)
|
|
2947
|
+
if current is None or status_rank.get(str(selected_item.get("status") or ""), 0) >= status_rank.get(
|
|
2948
|
+
str(current.get("status") or ""),
|
|
2949
|
+
0,
|
|
2950
|
+
):
|
|
2951
|
+
outlines_by_id[selected_outline_id] = selected_item
|
|
2952
|
+
else:
|
|
2953
|
+
current["is_selected"] = True
|
|
2954
|
+
|
|
2955
|
+
outlines = list(outlines_by_id.values())
|
|
2303
2956
|
outlines.sort(key=lambda item: (str(item.get("outline_id") or ""), str(item.get("status") or "")))
|
|
2304
2957
|
return {
|
|
2305
2958
|
"ok": True,
|
|
2306
|
-
"selected_outline_ref":
|
|
2959
|
+
"selected_outline_ref": selected_outline_id or None,
|
|
2307
2960
|
"selected_outline": selected_outline or None,
|
|
2308
2961
|
"count": len(outlines),
|
|
2309
2962
|
"outlines": outlines,
|
|
@@ -2366,8 +3019,296 @@ class ArtifactService:
|
|
|
2366
3019
|
deduped.append(path)
|
|
2367
3020
|
return deduped
|
|
2368
3021
|
|
|
2369
|
-
|
|
2370
|
-
|
|
3022
|
+
@staticmethod
|
|
3023
|
+
def _arxiv_content_from_item(item: dict[str, Any]) -> str:
|
|
3024
|
+
title = str(item.get("title") or item.get("display_name") or item.get("arxiv_id") or "arXiv paper").strip()
|
|
3025
|
+
authors = [str(author).strip() for author in (item.get("authors") or []) if str(author).strip()]
|
|
3026
|
+
categories = [str(category).strip() for category in (item.get("categories") or []) if str(category).strip()]
|
|
3027
|
+
abstract = str(item.get("abstract") or "").strip() or "Abstract unavailable."
|
|
3028
|
+
overview = str(item.get("overview") or "").strip()
|
|
3029
|
+
lines = [f"# {title}", "", f"- paper_id: {str(item.get('arxiv_id') or '').strip()}"]
|
|
3030
|
+
if item.get("metadata_source"):
|
|
3031
|
+
lines.append(f"- metadata_source: {item['metadata_source']}")
|
|
3032
|
+
if item.get("summary_source"):
|
|
3033
|
+
lines.append(f"- summary_source: {item['summary_source']}")
|
|
3034
|
+
if authors:
|
|
3035
|
+
lines.append(f"- authors: {', '.join(authors)}")
|
|
3036
|
+
if categories:
|
|
3037
|
+
lines.append(f"- categories: {', '.join(categories)}")
|
|
3038
|
+
if item.get("published_at"):
|
|
3039
|
+
lines.append(f"- published_at: {item['published_at']}")
|
|
3040
|
+
if item.get("version") is not None:
|
|
3041
|
+
lines.append(f"- version: v{item['version']}")
|
|
3042
|
+
if overview:
|
|
3043
|
+
lines.extend(["", "## Summary", "", overview])
|
|
3044
|
+
if abstract and abstract != overview:
|
|
3045
|
+
lines.extend(["", "## Abstract", "", abstract])
|
|
3046
|
+
else:
|
|
3047
|
+
lines.extend(["", "## Abstract", "", abstract])
|
|
3048
|
+
return "\n".join(lines).strip()
|
|
3049
|
+
|
|
3050
|
+
@staticmethod
|
|
3051
|
+
def _arxiv_item_needs_refresh(item: dict[str, Any] | None) -> bool:
|
|
3052
|
+
if not isinstance(item, dict):
|
|
3053
|
+
return False
|
|
3054
|
+
title = str(item.get("title") or "").strip()
|
|
3055
|
+
lowered_title = title.lower()
|
|
3056
|
+
authors = item.get("authors") or []
|
|
3057
|
+
categories = item.get("categories") or []
|
|
3058
|
+
published_at = str(item.get("published_at") or "").strip()
|
|
3059
|
+
metadata_source = str(item.get("metadata_source") or "").strip()
|
|
3060
|
+
bibtex = str(item.get("bibtex") or "").strip()
|
|
3061
|
+
overview = str(item.get("overview") or "").strip()
|
|
3062
|
+
overview_markdown = str(item.get("overview_markdown") or "").strip()
|
|
3063
|
+
overview_source = str(item.get("overview_source") or "").strip()
|
|
3064
|
+
return (
|
|
3065
|
+
not title
|
|
3066
|
+
or title.startswith("#")
|
|
3067
|
+
or lowered_title.startswith("research paper analysis")
|
|
3068
|
+
or lowered_title.startswith("## research paper analysis")
|
|
3069
|
+
or not authors
|
|
3070
|
+
or not categories
|
|
3071
|
+
or not published_at
|
|
3072
|
+
or not metadata_source
|
|
3073
|
+
or not bibtex
|
|
3074
|
+
or (not overview and not overview_markdown and not overview_source)
|
|
3075
|
+
)
|
|
3076
|
+
|
|
3077
|
+
def _refresh_arxiv_item_metadata(self, quest_root: Path, item: dict[str, Any]) -> dict[str, Any]:
|
|
3078
|
+
arxiv_id = str(item.get("arxiv_id") or "").strip()
|
|
3079
|
+
if not arxiv_id:
|
|
3080
|
+
return item
|
|
3081
|
+
metadata = fetch_arxiv_metadata(arxiv_id)
|
|
3082
|
+
if not metadata.get("ok"):
|
|
3083
|
+
return item
|
|
3084
|
+
summary = read_arxiv_content(arxiv_id, full_text=False)
|
|
3085
|
+
summary_source = summary.get("summary_source") if summary.get("ok") else None
|
|
3086
|
+
overview_source = summary.get("overview_source") if summary.get("ok") else None
|
|
3087
|
+
return self.arxiv_library.upsert_item(
|
|
3088
|
+
quest_root,
|
|
3089
|
+
{
|
|
3090
|
+
**item,
|
|
3091
|
+
"arxiv_id": metadata.get("paper_id") or arxiv_id,
|
|
3092
|
+
"title": metadata.get("title") or item.get("title") or arxiv_id,
|
|
3093
|
+
"display_name": metadata.get("title") or item.get("display_name") or arxiv_id,
|
|
3094
|
+
"authors": metadata.get("authors") or item.get("authors") or [],
|
|
3095
|
+
"categories": metadata.get("categories") or item.get("categories") or [],
|
|
3096
|
+
"abstract": metadata.get("abstract") or item.get("abstract") or "",
|
|
3097
|
+
"published_at": metadata.get("published_at") or item.get("published_at") or "",
|
|
3098
|
+
"version": metadata.get("version") if metadata.get("version") is not None else item.get("version"),
|
|
3099
|
+
"primary_class": metadata.get("primary_class") or item.get("primary_class") or "",
|
|
3100
|
+
"metadata_source": metadata.get("metadata_source") or item.get("metadata_source"),
|
|
3101
|
+
"metadata_status": "ready",
|
|
3102
|
+
"overview": summary.get("overview") or item.get("overview") or "",
|
|
3103
|
+
"overview_markdown": summary.get("overview_markdown") or item.get("overview_markdown") or "",
|
|
3104
|
+
"summary_source": summary_source or item.get("summary_source"),
|
|
3105
|
+
"overview_source": overview_source or summary_source or item.get("overview_source"),
|
|
3106
|
+
"bibtex": metadata.get("bibtex") or item.get("bibtex"),
|
|
3107
|
+
"abs_url": metadata.get("abs_url") or item.get("abs_url"),
|
|
3108
|
+
"pdf_url": metadata.get("pdf_url") or item.get("pdf_url"),
|
|
3109
|
+
},
|
|
3110
|
+
)
|
|
3111
|
+
|
|
3112
|
+
@staticmethod
|
|
3113
|
+
def _arxiv_file_payload(quest_root: Path, item: dict[str, Any]) -> dict[str, Any]:
|
|
3114
|
+
relative = str(item.get("path") or "").strip()
|
|
3115
|
+
if not relative:
|
|
3116
|
+
return {}
|
|
3117
|
+
document_id = str(item.get("document_id") or f"questpath::{relative}").strip()
|
|
3118
|
+
return {
|
|
3119
|
+
"path": relative,
|
|
3120
|
+
"document_id": document_id,
|
|
3121
|
+
"pdf_rel_path": relative,
|
|
3122
|
+
"pdf_url": f"/api/quests/{quest_root.name}/documents/asset?document_id={document_id}",
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
def arxiv(
|
|
3126
|
+
self,
|
|
3127
|
+
paper_id: str | None = None,
|
|
3128
|
+
*,
|
|
3129
|
+
full_text: bool = False,
|
|
3130
|
+
mode: str = "read",
|
|
3131
|
+
quest_root: Path | None = None,
|
|
3132
|
+
) -> dict[str, Any]:
|
|
3133
|
+
normalized_mode = str(mode or "read").strip().lower() or "read"
|
|
3134
|
+
if normalized_mode == "list":
|
|
3135
|
+
if quest_root is None:
|
|
3136
|
+
return {
|
|
3137
|
+
"ok": False,
|
|
3138
|
+
"mode": "list",
|
|
3139
|
+
"error": "`quest_root` is required for `artifact.arxiv(mode='list')`.",
|
|
3140
|
+
}
|
|
3141
|
+
items = self.arxiv_library.list_items(quest_root)
|
|
3142
|
+
refreshed_any = False
|
|
3143
|
+
for item in items[:]:
|
|
3144
|
+
if not self._arxiv_item_needs_refresh(item):
|
|
3145
|
+
continue
|
|
3146
|
+
refreshed = self._refresh_arxiv_item_metadata(quest_root, item)
|
|
3147
|
+
if refreshed != item:
|
|
3148
|
+
refreshed_any = True
|
|
3149
|
+
if refreshed_any:
|
|
3150
|
+
items = self.arxiv_library.list_items(quest_root)
|
|
3151
|
+
return {
|
|
3152
|
+
"ok": True,
|
|
3153
|
+
"mode": "list",
|
|
3154
|
+
"items": items,
|
|
3155
|
+
"count": len(items),
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3158
|
+
if paper_id is None:
|
|
3159
|
+
return {
|
|
3160
|
+
"ok": False,
|
|
3161
|
+
"mode": normalized_mode,
|
|
3162
|
+
"error": "`paper_id` is required for `artifact.arxiv(mode='read')`.",
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
if quest_root is None:
|
|
3166
|
+
return {
|
|
3167
|
+
**read_arxiv_content(paper_id, full_text=full_text),
|
|
3168
|
+
"mode": normalized_mode,
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3171
|
+
entry = self.arxiv_library.mark_processing(quest_root, paper_id)
|
|
3172
|
+
cached_entry = self.arxiv_library.get_item(quest_root, paper_id)
|
|
3173
|
+
if cached_entry and self._arxiv_item_needs_refresh(cached_entry):
|
|
3174
|
+
refreshed = self._refresh_arxiv_item_metadata(quest_root, cached_entry)
|
|
3175
|
+
if refreshed:
|
|
3176
|
+
cached_entry = refreshed
|
|
3177
|
+
if (
|
|
3178
|
+
cached_entry
|
|
3179
|
+
and not full_text
|
|
3180
|
+
and cached_entry.get("abstract")
|
|
3181
|
+
and (cached_entry.get("summary_source") or cached_entry.get("metadata_status") == "pending")
|
|
3182
|
+
):
|
|
3183
|
+
paper_ref = str(cached_entry.get("arxiv_id") or paper_id).strip()
|
|
3184
|
+
self.arxiv_library.queue_pdf_download(quest_root, str(cached_entry.get("arxiv_id") or paper_id))
|
|
3185
|
+
return {
|
|
3186
|
+
"ok": True,
|
|
3187
|
+
"mode": normalized_mode,
|
|
3188
|
+
"paper_id": paper_ref,
|
|
3189
|
+
"requested_full_text": full_text,
|
|
3190
|
+
"content_mode": "abstract",
|
|
3191
|
+
"source": "quest_arxiv_library",
|
|
3192
|
+
"source_url": f"https://arxiv.org/abs/{paper_ref}",
|
|
3193
|
+
"title": cached_entry.get("title"),
|
|
3194
|
+
"authors": cached_entry.get("authors") or [],
|
|
3195
|
+
"categories": cached_entry.get("categories") or [],
|
|
3196
|
+
"abstract": cached_entry.get("abstract") or "",
|
|
3197
|
+
"overview": cached_entry.get("overview") or "",
|
|
3198
|
+
"overview_markdown": cached_entry.get("overview_markdown") or "",
|
|
3199
|
+
"summary_source": cached_entry.get("summary_source"),
|
|
3200
|
+
"overview_source": cached_entry.get("overview_source"),
|
|
3201
|
+
"metadata_source": cached_entry.get("metadata_source"),
|
|
3202
|
+
"published_at": cached_entry.get("published_at") or "",
|
|
3203
|
+
"version": cached_entry.get("version"),
|
|
3204
|
+
"primary_class": cached_entry.get("primary_class") or "",
|
|
3205
|
+
"bibtex": cached_entry.get("bibtex") or "",
|
|
3206
|
+
"status": cached_entry.get("status"),
|
|
3207
|
+
"metadata_status": cached_entry.get("metadata_status"),
|
|
3208
|
+
"abs_url": f"https://arxiv.org/abs/{paper_ref}",
|
|
3209
|
+
"pdf_url": f"https://arxiv.org/pdf/{paper_ref}.pdf",
|
|
3210
|
+
"content": self._arxiv_content_from_item(cached_entry),
|
|
3211
|
+
"attempts": [],
|
|
3212
|
+
**self._arxiv_file_payload(quest_root, cached_entry),
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
fetched = read_arxiv_content(str(entry.get("arxiv_id") or paper_id), full_text=full_text)
|
|
3216
|
+
if not fetched.get("ok"):
|
|
3217
|
+
normalized_id = str(entry.get("arxiv_id") or paper_id).strip()
|
|
3218
|
+
placeholder = self.arxiv_library.upsert_item(
|
|
3219
|
+
quest_root,
|
|
3220
|
+
{
|
|
3221
|
+
**(cached_entry or {}),
|
|
3222
|
+
"arxiv_id": normalized_id,
|
|
3223
|
+
"title": str((cached_entry or {}).get("title") or normalized_id).strip(),
|
|
3224
|
+
"display_name": str((cached_entry or {}).get("display_name") or normalized_id).strip(),
|
|
3225
|
+
"status": str((cached_entry or {}).get("status") or "processing").strip() or "processing",
|
|
3226
|
+
"metadata_status": "pending",
|
|
3227
|
+
"error": None,
|
|
3228
|
+
"pdf_rel_path": self.arxiv_library.pdf_relative_path(normalized_id),
|
|
3229
|
+
"abs_url": str((cached_entry or {}).get("abs_url") or f"https://arxiv.org/abs/{normalized_id}"),
|
|
3230
|
+
"pdf_url": str((cached_entry or {}).get("pdf_url") or f"https://arxiv.org/pdf/{normalized_id}.pdf"),
|
|
3231
|
+
},
|
|
3232
|
+
)
|
|
3233
|
+
self.arxiv_library.queue_pdf_download(
|
|
3234
|
+
quest_root,
|
|
3235
|
+
normalized_id,
|
|
3236
|
+
pdf_url=str(placeholder.get("pdf_url") or "").strip() or None,
|
|
3237
|
+
)
|
|
3238
|
+
latest = self.arxiv_library.get_item(quest_root, normalized_id) or placeholder
|
|
3239
|
+
return {
|
|
3240
|
+
"ok": True,
|
|
3241
|
+
"mode": normalized_mode,
|
|
3242
|
+
"paper_id": normalized_id,
|
|
3243
|
+
"requested_full_text": full_text,
|
|
3244
|
+
"content_mode": "pending",
|
|
3245
|
+
"source": "quest_arxiv_library_partial",
|
|
3246
|
+
"source_url": latest.get("abs_url") or f"https://arxiv.org/abs/{normalized_id}",
|
|
3247
|
+
"title": latest.get("title"),
|
|
3248
|
+
"authors": latest.get("authors") or [],
|
|
3249
|
+
"categories": latest.get("categories") or [],
|
|
3250
|
+
"abstract": latest.get("abstract") or "",
|
|
3251
|
+
"overview": latest.get("overview") or "",
|
|
3252
|
+
"overview_markdown": latest.get("overview_markdown") or "",
|
|
3253
|
+
"summary_source": latest.get("summary_source"),
|
|
3254
|
+
"overview_source": latest.get("overview_source"),
|
|
3255
|
+
"metadata_source": latest.get("metadata_source"),
|
|
3256
|
+
"published_at": latest.get("published_at") or "",
|
|
3257
|
+
"version": latest.get("version"),
|
|
3258
|
+
"primary_class": latest.get("primary_class") or "",
|
|
3259
|
+
"bibtex": latest.get("bibtex") or "",
|
|
3260
|
+
"status": latest.get("status"),
|
|
3261
|
+
"metadata_status": "pending",
|
|
3262
|
+
"metadata_pending": True,
|
|
3263
|
+
"message": "Metadata is temporarily unavailable. Open the arXiv link directly while DeepScientist retries later.",
|
|
3264
|
+
"abs_url": latest.get("abs_url") or f"https://arxiv.org/abs/{normalized_id}",
|
|
3265
|
+
"pdf_url": latest.get("pdf_url") or f"https://arxiv.org/pdf/{normalized_id}.pdf",
|
|
3266
|
+
"content": self._arxiv_content_from_item(latest),
|
|
3267
|
+
"attempts": fetched.get("attempts") or [],
|
|
3268
|
+
"guidance": fetched.get("guidance"),
|
|
3269
|
+
**self._arxiv_file_payload(quest_root, latest),
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
saved = self.arxiv_library.upsert_item(
|
|
3273
|
+
quest_root,
|
|
3274
|
+
{
|
|
3275
|
+
**(cached_entry or {}),
|
|
3276
|
+
"arxiv_id": fetched.get("paper_id") or str(entry.get("arxiv_id") or paper_id),
|
|
3277
|
+
"title": fetched.get("title") or cached_entry.get("title") if cached_entry else fetched.get("title"),
|
|
3278
|
+
"authors": fetched.get("authors") or (cached_entry.get("authors") if cached_entry else []),
|
|
3279
|
+
"categories": fetched.get("categories") or (cached_entry.get("categories") if cached_entry else []),
|
|
3280
|
+
"abstract": fetched.get("abstract") or (cached_entry.get("abstract") if cached_entry else ""),
|
|
3281
|
+
"overview": fetched.get("overview") or (cached_entry.get("overview") if cached_entry else ""),
|
|
3282
|
+
"overview_markdown": fetched.get("overview_markdown") or (cached_entry.get("overview_markdown") if cached_entry else ""),
|
|
3283
|
+
"summary_source": fetched.get("summary_source") or (cached_entry.get("summary_source") if cached_entry else None),
|
|
3284
|
+
"overview_source": fetched.get("overview_source") or (cached_entry.get("overview_source") if cached_entry else None),
|
|
3285
|
+
"metadata_source": fetched.get("metadata_source") or (cached_entry.get("metadata_source") if cached_entry else None),
|
|
3286
|
+
"published_at": fetched.get("published_at") or (cached_entry.get("published_at") if cached_entry else ""),
|
|
3287
|
+
"version": fetched.get("version") if fetched.get("version") is not None else (cached_entry.get("version") if cached_entry else None),
|
|
3288
|
+
"primary_class": fetched.get("primary_class") or (cached_entry.get("primary_class") if cached_entry else ""),
|
|
3289
|
+
"bibtex": fetched.get("bibtex") or (cached_entry.get("bibtex") if cached_entry else None),
|
|
3290
|
+
"abs_url": fetched.get("abs_url") or (cached_entry.get("abs_url") if cached_entry else None),
|
|
3291
|
+
"pdf_url": fetched.get("pdf_url") or (cached_entry.get("pdf_url") if cached_entry else None),
|
|
3292
|
+
"display_name": fetched.get("title") or fetched.get("paper_id") or str(entry.get("arxiv_id") or paper_id),
|
|
3293
|
+
"pdf_rel_path": self.arxiv_library.pdf_relative_path(str(fetched.get("paper_id") or entry.get("arxiv_id") or paper_id)),
|
|
3294
|
+
"status": "processing",
|
|
3295
|
+
"metadata_status": "ready",
|
|
3296
|
+
"error": None,
|
|
3297
|
+
},
|
|
3298
|
+
)
|
|
3299
|
+
self.arxiv_library.queue_pdf_download(
|
|
3300
|
+
quest_root,
|
|
3301
|
+
str(saved.get("arxiv_id") or paper_id),
|
|
3302
|
+
pdf_url=str(fetched.get("pdf_url") or "").strip() or None,
|
|
3303
|
+
)
|
|
3304
|
+
latest = self.arxiv_library.get_item(quest_root, str(saved.get("arxiv_id") or paper_id)) or saved
|
|
3305
|
+
return {
|
|
3306
|
+
**fetched,
|
|
3307
|
+
"mode": normalized_mode,
|
|
3308
|
+
"status": latest.get("status"),
|
|
3309
|
+
"metadata_status": latest.get("metadata_status"),
|
|
3310
|
+
**self._arxiv_file_payload(quest_root, latest),
|
|
3311
|
+
}
|
|
2371
3312
|
|
|
2372
3313
|
def record(
|
|
2373
3314
|
self,
|
|
@@ -3139,19 +4080,26 @@ class ArtifactService:
|
|
|
3139
4080
|
worktree_root,
|
|
3140
4081
|
message=f"idea: create {resolved_idea_id}",
|
|
3141
4082
|
)
|
|
4083
|
+
idea_md_rel_path = self._workspace_relative(quest_root, idea_md_path)
|
|
4084
|
+
idea_draft_rel_path = self._workspace_relative(quest_root, idea_draft_path)
|
|
3142
4085
|
interaction = self.interact(
|
|
3143
4086
|
quest_root,
|
|
3144
4087
|
kind="milestone",
|
|
3145
|
-
message=(
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
4088
|
+
message=self._build_idea_interaction_message(
|
|
4089
|
+
action="create",
|
|
4090
|
+
idea_id=resolved_idea_id,
|
|
4091
|
+
title=title,
|
|
4092
|
+
problem=problem,
|
|
4093
|
+
hypothesis=hypothesis,
|
|
4094
|
+
mechanism=mechanism,
|
|
4095
|
+
foundation_label=self._format_foundation_label(
|
|
4096
|
+
foundation,
|
|
4097
|
+
fallback=foundation.get("branch") or "current head",
|
|
4098
|
+
),
|
|
4099
|
+
branch_name=branch_name,
|
|
4100
|
+
next_target=next_target,
|
|
4101
|
+
idea_md_rel_path=idea_md_rel_path,
|
|
4102
|
+
draft_md_rel_path=idea_draft_rel_path,
|
|
3155
4103
|
),
|
|
3156
4104
|
deliver_to_bound_conversations=True,
|
|
3157
4105
|
include_recent_inbound_messages=False,
|
|
@@ -3338,17 +4286,26 @@ class ArtifactService:
|
|
|
3338
4286
|
worktree_root,
|
|
3339
4287
|
message=f"idea: revise {resolved_idea_id}",
|
|
3340
4288
|
)
|
|
4289
|
+
idea_md_rel_path = self._workspace_relative(quest_root, idea_md_path)
|
|
4290
|
+
idea_draft_rel_path = self._workspace_relative(quest_root, idea_draft_path)
|
|
3341
4291
|
interaction = self.interact(
|
|
3342
4292
|
quest_root,
|
|
3343
4293
|
kind="progress",
|
|
3344
|
-
message=(
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
4294
|
+
message=self._build_idea_interaction_message(
|
|
4295
|
+
action="revise",
|
|
4296
|
+
idea_id=resolved_idea_id,
|
|
4297
|
+
title=title,
|
|
4298
|
+
problem=problem,
|
|
4299
|
+
hypothesis=hypothesis,
|
|
4300
|
+
mechanism=mechanism,
|
|
4301
|
+
foundation_label=self._format_foundation_label(
|
|
4302
|
+
existing_foundation_ref,
|
|
4303
|
+
fallback=(existing_foundation_ref or {}).get("branch") or "current head",
|
|
4304
|
+
),
|
|
4305
|
+
branch_name=branch_name,
|
|
4306
|
+
next_target=next_target,
|
|
4307
|
+
idea_md_rel_path=idea_md_rel_path,
|
|
4308
|
+
draft_md_rel_path=idea_draft_rel_path,
|
|
3352
4309
|
),
|
|
3353
4310
|
deliver_to_bound_conversations=True,
|
|
3354
4311
|
include_recent_inbound_messages=False,
|
|
@@ -3551,15 +4508,36 @@ class ArtifactService:
|
|
|
3551
4508
|
for item in normalized_metric_rows
|
|
3552
4509
|
if str(item.get("metric_id") or "").strip()
|
|
3553
4510
|
}
|
|
3554
|
-
effective_metric_contract = normalize_metric_contract(
|
|
3555
|
-
metric_contract or baseline_entry.get("metric_contract"),
|
|
3556
|
-
baseline_id=resolved_baseline_id,
|
|
3557
|
-
metrics_summary=normalized_metrics_summary,
|
|
3558
|
-
metric_rows=normalized_metric_rows,
|
|
3559
|
-
primary_metric=baseline_entry.get("primary_metric"),
|
|
3560
|
-
baseline_variants=baseline_entry.get("baseline_variants"),
|
|
3561
|
-
)
|
|
3562
4511
|
baseline_contract_payload = self._load_metric_contract_payload(quest_root, metric_contract_json_rel_path)
|
|
4512
|
+
baseline_metric_contract = baseline_entry.get("metric_contract")
|
|
4513
|
+
baseline_primary_metric = baseline_entry.get("primary_metric")
|
|
4514
|
+
if isinstance(baseline_contract_payload, dict) and baseline_contract_payload:
|
|
4515
|
+
payload_metric_contract = baseline_contract_payload.get("metric_contract")
|
|
4516
|
+
if isinstance(payload_metric_contract, dict) and payload_metric_contract:
|
|
4517
|
+
baseline_metric_contract = payload_metric_contract
|
|
4518
|
+
payload_primary_metric = baseline_contract_payload.get("primary_metric")
|
|
4519
|
+
if isinstance(payload_primary_metric, dict) and payload_primary_metric:
|
|
4520
|
+
baseline_primary_metric = payload_primary_metric
|
|
4521
|
+
effective_metric_contract = (
|
|
4522
|
+
self._merge_run_metric_contract(
|
|
4523
|
+
baseline_metric_contract=baseline_metric_contract,
|
|
4524
|
+
baseline_primary_metric=baseline_primary_metric,
|
|
4525
|
+
baseline_variants=baseline_entry.get("baseline_variants"),
|
|
4526
|
+
run_metric_contract=metric_contract,
|
|
4527
|
+
metrics_summary=normalized_metrics_summary,
|
|
4528
|
+
metric_rows=normalized_metric_rows,
|
|
4529
|
+
baseline_id=resolved_baseline_id,
|
|
4530
|
+
)
|
|
4531
|
+
if isinstance(baseline_metric_contract, dict) and baseline_metric_contract
|
|
4532
|
+
else normalize_metric_contract(
|
|
4533
|
+
metric_contract or baseline_entry.get("metric_contract"),
|
|
4534
|
+
baseline_id=resolved_baseline_id,
|
|
4535
|
+
metrics_summary=normalized_metrics_summary,
|
|
4536
|
+
metric_rows=normalized_metric_rows,
|
|
4537
|
+
primary_metric=baseline_primary_metric,
|
|
4538
|
+
baseline_variants=baseline_entry.get("baseline_variants"),
|
|
4539
|
+
)
|
|
4540
|
+
)
|
|
3563
4541
|
metric_validation: dict[str, Any] | None = None
|
|
3564
4542
|
if strict_metric_contract:
|
|
3565
4543
|
metric_validation = validate_main_experiment_against_baseline_contract(
|
|
@@ -3830,14 +4808,21 @@ class ArtifactService:
|
|
|
3830
4808
|
interaction = self.interact(
|
|
3831
4809
|
quest_root,
|
|
3832
4810
|
kind="milestone",
|
|
3833
|
-
message=(
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
4811
|
+
message=self._build_main_experiment_interaction_message(
|
|
4812
|
+
run_id=run_identifier,
|
|
4813
|
+
branch_name=branch_name,
|
|
4814
|
+
verdict=verdict,
|
|
4815
|
+
primary_metric_id=primary_metric_id,
|
|
4816
|
+
primary_value=primary_value,
|
|
4817
|
+
primary_baseline=primary_baseline,
|
|
4818
|
+
primary_delta=primary_delta,
|
|
4819
|
+
decimals=decimals if isinstance(decimals, int) else None,
|
|
4820
|
+
conclusion=conclusion.strip() or progress_eval.get("reason"),
|
|
4821
|
+
evaluation_summary=normalized_evaluation_summary,
|
|
4822
|
+
breakthrough_level=str(progress_eval.get("breakthrough_level") or "").strip() or None,
|
|
4823
|
+
recommended_next_route=str(delivery_policy.get("recommended_next_route") or "").strip() or None,
|
|
4824
|
+
run_md_rel_path=self._workspace_relative(quest_root, run_md_path),
|
|
4825
|
+
result_json_rel_path=self._workspace_relative(quest_root, result_json_path),
|
|
3841
4826
|
),
|
|
3842
4827
|
deliver_to_bound_conversations=True,
|
|
3843
4828
|
include_recent_inbound_messages=False,
|
|
@@ -3924,6 +4909,14 @@ class ArtifactService:
|
|
|
3924
4909
|
quest_root,
|
|
3925
4910
|
state=state,
|
|
3926
4911
|
)
|
|
4912
|
+
runtime_refs = self.resolve_runtime_refs(quest_root)
|
|
4913
|
+
resolved_parent_run_id = (
|
|
4914
|
+
str(parent_run_id or "").strip()
|
|
4915
|
+
or str(state.get("paper_parent_run_id") or "").strip()
|
|
4916
|
+
or str((self._latest_main_run_for_branch(quest_root, parent_branch) or {}).get("run_id") or "").strip()
|
|
4917
|
+
or str(runtime_refs.get("latest_main_run_id") or "").strip()
|
|
4918
|
+
or None
|
|
4919
|
+
)
|
|
3927
4920
|
active_idea_id = str(resolved_idea_id or "").strip()
|
|
3928
4921
|
if not active_idea_id:
|
|
3929
4922
|
raise ValueError("An active idea is required before starting an analysis campaign.")
|
|
@@ -3937,6 +4930,54 @@ class ArtifactService:
|
|
|
3937
4930
|
normalized_research_questions = self._normalize_string_list(research_questions)
|
|
3938
4931
|
normalized_experimental_designs = self._normalize_string_list(experimental_designs)
|
|
3939
4932
|
normalized_todo_items = self._normalize_campaign_todo_items(todo_items)
|
|
4933
|
+
quest_data = self.quest_service.read_quest_yaml(quest_root)
|
|
4934
|
+
active_anchor = str(quest_data.get("active_anchor") or "").strip().lower()
|
|
4935
|
+
campaign_origin_kind = (
|
|
4936
|
+
str(normalized_campaign_origin.get("kind") or "").strip().lower()
|
|
4937
|
+
if isinstance(normalized_campaign_origin, dict)
|
|
4938
|
+
else ""
|
|
4939
|
+
)
|
|
4940
|
+
writing_facing = bool(
|
|
4941
|
+
resolved_outline_ref
|
|
4942
|
+
or normalized_research_questions
|
|
4943
|
+
or normalized_experimental_designs
|
|
4944
|
+
or normalized_todo_items
|
|
4945
|
+
or str(state.get("workspace_mode") or "").strip().lower() == "paper"
|
|
4946
|
+
or active_anchor == "write"
|
|
4947
|
+
or campaign_origin_kind in {"write", "paper", "rebuttal", "revision"}
|
|
4948
|
+
)
|
|
4949
|
+
if writing_facing:
|
|
4950
|
+
if not resolved_outline_ref:
|
|
4951
|
+
raise ValueError(
|
|
4952
|
+
"Writing-facing analysis campaigns require `selected_outline_ref` before slices can be launched."
|
|
4953
|
+
)
|
|
4954
|
+
if not normalized_research_questions:
|
|
4955
|
+
raise ValueError(
|
|
4956
|
+
"Writing-facing analysis campaigns require non-empty `research_questions`."
|
|
4957
|
+
)
|
|
4958
|
+
if not normalized_experimental_designs:
|
|
4959
|
+
raise ValueError(
|
|
4960
|
+
"Writing-facing analysis campaigns require non-empty `experimental_designs`."
|
|
4961
|
+
)
|
|
4962
|
+
if not normalized_todo_items:
|
|
4963
|
+
raise ValueError(
|
|
4964
|
+
"Writing-facing analysis campaigns require non-empty `todo_items`."
|
|
4965
|
+
)
|
|
4966
|
+
todo_slice_ids = {
|
|
4967
|
+
str(item.get("slice_id") or "").strip()
|
|
4968
|
+
for item in normalized_todo_items
|
|
4969
|
+
if str(item.get("slice_id") or "").strip()
|
|
4970
|
+
}
|
|
4971
|
+
missing_slice_ids = [
|
|
4972
|
+
str(raw.get("slice_id") or "").strip()
|
|
4973
|
+
for raw in slices
|
|
4974
|
+
if str(raw.get("slice_id") or "").strip() and str(raw.get("slice_id") or "").strip() not in todo_slice_ids
|
|
4975
|
+
]
|
|
4976
|
+
if missing_slice_ids:
|
|
4977
|
+
raise ValueError(
|
|
4978
|
+
"Writing-facing analysis campaigns require one todo item per slice. "
|
|
4979
|
+
f"Missing todo items for: {', '.join(missing_slice_ids)}."
|
|
4980
|
+
)
|
|
3940
4981
|
slice_contexts: list[dict[str, Any]] = []
|
|
3941
4982
|
inventory_entries: list[dict[str, Any]] = []
|
|
3942
4983
|
for index, raw in enumerate(slices, start=1):
|
|
@@ -4199,7 +5240,7 @@ class ArtifactService:
|
|
|
4199
5240
|
{
|
|
4200
5241
|
"title": campaign_title,
|
|
4201
5242
|
"goal": campaign_goal,
|
|
4202
|
-
"parent_run_id":
|
|
5243
|
+
"parent_run_id": resolved_parent_run_id,
|
|
4203
5244
|
"active_idea_id": active_idea_id,
|
|
4204
5245
|
"parent_branch": parent_branch,
|
|
4205
5246
|
"parent_worktree_root": str(parent_worktree_root),
|
|
@@ -4274,7 +5315,7 @@ class ArtifactService:
|
|
|
4274
5315
|
"details": {
|
|
4275
5316
|
"campaign_title": campaign_title,
|
|
4276
5317
|
"campaign_goal": campaign_goal,
|
|
4277
|
-
"parent_run_id":
|
|
5318
|
+
"parent_run_id": resolved_parent_run_id,
|
|
4278
5319
|
"campaign_origin": normalized_campaign_origin,
|
|
4279
5320
|
"selected_outline_ref": resolved_outline_ref,
|
|
4280
5321
|
"todo_manifest_path": str(todo_manifest_path),
|
|
@@ -4326,14 +5367,13 @@ class ArtifactService:
|
|
|
4326
5367
|
interaction = self.interact(
|
|
4327
5368
|
quest_root,
|
|
4328
5369
|
kind="milestone",
|
|
4329
|
-
message=(
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
f"- Core requirement: {first_slice['must_not_simplify'] or 'Follow the full evaluation protocol.'}"
|
|
5370
|
+
message=self._build_analysis_campaign_interaction_message(
|
|
5371
|
+
campaign_id=campaign_id,
|
|
5372
|
+
goal=campaign_goal,
|
|
5373
|
+
parent_branch=parent_branch,
|
|
5374
|
+
selected_outline_ref=resolved_outline_ref,
|
|
5375
|
+
first_slice=first_slice,
|
|
5376
|
+
todo_manifest_rel_path=self._workspace_relative(quest_root, todo_manifest_path),
|
|
4337
5377
|
),
|
|
4338
5378
|
deliver_to_bound_conversations=True,
|
|
4339
5379
|
include_recent_inbound_messages=False,
|
|
@@ -4418,6 +5458,7 @@ class ArtifactService:
|
|
|
4418
5458
|
if normalized_mode == "candidate":
|
|
4419
5459
|
resolved_outline_id = str(outline_id or self._next_paper_outline_id(quest_root)).strip()
|
|
4420
5460
|
candidate_path = self._paper_outline_candidates_root(quest_root, workspace_root=workspace_root) / f"{resolved_outline_id}.json"
|
|
5461
|
+
canonical_candidate_path = quest_root / "paper" / "outlines" / "candidates" / f"{resolved_outline_id}.json"
|
|
4421
5462
|
existing = read_json(candidate_path, {})
|
|
4422
5463
|
existing = existing if isinstance(existing, dict) else {}
|
|
4423
5464
|
record = self._normalize_paper_outline_record(
|
|
@@ -4432,6 +5473,8 @@ class ArtifactService:
|
|
|
4432
5473
|
created_at=str(existing.get("created_at") or "") or None,
|
|
4433
5474
|
)
|
|
4434
5475
|
write_json(candidate_path, record)
|
|
5476
|
+
if canonical_candidate_path.resolve() != candidate_path.resolve():
|
|
5477
|
+
write_json(canonical_candidate_path, record)
|
|
4435
5478
|
artifact = self.record(
|
|
4436
5479
|
quest_root,
|
|
4437
5480
|
{
|
|
@@ -4491,6 +5534,9 @@ class ArtifactService:
|
|
|
4491
5534
|
)
|
|
4492
5535
|
|
|
4493
5536
|
write_json(selected_outline_path, resolved_record)
|
|
5537
|
+
canonical_selected_outline_path = quest_root / "paper" / "selected_outline.json"
|
|
5538
|
+
if canonical_selected_outline_path.resolve() != selected_outline_path.resolve():
|
|
5539
|
+
write_json(canonical_selected_outline_path, resolved_record)
|
|
4494
5540
|
if source_candidate_path.exists():
|
|
4495
5541
|
source_record["status"] = "selected" if normalized_mode == "select" else "revised"
|
|
4496
5542
|
source_record["updated_at"] = utc_now()
|
|
@@ -4499,6 +5545,9 @@ class ArtifactService:
|
|
|
4499
5545
|
if normalized_mode == "revise":
|
|
4500
5546
|
revised_outline_path = ensure_dir(paper_root / "outlines" / "revisions") / f"{source_outline_id}.json"
|
|
4501
5547
|
write_json(revised_outline_path, resolved_record)
|
|
5548
|
+
canonical_revised_outline_path = quest_root / "paper" / "outlines" / "revisions" / f"{source_outline_id}.json"
|
|
5549
|
+
if canonical_revised_outline_path.resolve() != revised_outline_path.resolve():
|
|
5550
|
+
write_json(canonical_revised_outline_path, resolved_record)
|
|
4502
5551
|
|
|
4503
5552
|
outline_selection_path = paper_root / "outline_selection.md"
|
|
4504
5553
|
action_label = "selected" if normalized_mode == "select" else "revised"
|
|
@@ -4541,6 +5590,46 @@ class ArtifactService:
|
|
|
4541
5590
|
checkpoint=False,
|
|
4542
5591
|
workspace_root=workspace_root,
|
|
4543
5592
|
)
|
|
5593
|
+
selected_outline_rel_path = self._workspace_relative(quest_root, selected_outline_path)
|
|
5594
|
+
outline_selection_rel_path = self._workspace_relative(quest_root, outline_selection_path)
|
|
5595
|
+
revised_outline_rel_path = self._workspace_relative(quest_root, revised_outline_path) if revised_outline_path else None
|
|
5596
|
+
interaction = self.interact(
|
|
5597
|
+
quest_root,
|
|
5598
|
+
kind="milestone" if normalized_mode == "select" else "progress",
|
|
5599
|
+
message=self._build_outline_interaction_message(
|
|
5600
|
+
action=normalized_mode,
|
|
5601
|
+
outline_id=source_outline_id,
|
|
5602
|
+
title=str(resolved_record.get("title") or "").strip() or source_outline_id,
|
|
5603
|
+
selected_reason=selected_reason or note,
|
|
5604
|
+
story=str(resolved_record.get("story") or "").strip() or None,
|
|
5605
|
+
research_questions=(
|
|
5606
|
+
(resolved_record.get("detailed_outline") or {})
|
|
5607
|
+
if isinstance(resolved_record.get("detailed_outline"), dict)
|
|
5608
|
+
else {}
|
|
5609
|
+
).get("research_questions"),
|
|
5610
|
+
experimental_designs=(
|
|
5611
|
+
(resolved_record.get("detailed_outline") or {})
|
|
5612
|
+
if isinstance(resolved_record.get("detailed_outline"), dict)
|
|
5613
|
+
else {}
|
|
5614
|
+
).get("experimental_designs"),
|
|
5615
|
+
selected_outline_rel_path=selected_outline_rel_path,
|
|
5616
|
+
outline_selection_rel_path=outline_selection_rel_path,
|
|
5617
|
+
revised_outline_rel_path=revised_outline_rel_path,
|
|
5618
|
+
),
|
|
5619
|
+
deliver_to_bound_conversations=True,
|
|
5620
|
+
include_recent_inbound_messages=False,
|
|
5621
|
+
attachments=[
|
|
5622
|
+
{
|
|
5623
|
+
"kind": "paper_outline_selected" if normalized_mode == "select" else "paper_outline_revised",
|
|
5624
|
+
"outline_id": source_outline_id,
|
|
5625
|
+
"title": resolved_record.get("title"),
|
|
5626
|
+
"selected_reason": selected_reason,
|
|
5627
|
+
"selected_outline_path": str(selected_outline_path),
|
|
5628
|
+
"outline_selection_path": str(outline_selection_path),
|
|
5629
|
+
"revised_outline_path": str(revised_outline_path) if revised_outline_path else None,
|
|
5630
|
+
}
|
|
5631
|
+
],
|
|
5632
|
+
)
|
|
4544
5633
|
return {
|
|
4545
5634
|
"ok": True,
|
|
4546
5635
|
"mode": normalized_mode,
|
|
@@ -4550,6 +5639,7 @@ class ArtifactService:
|
|
|
4550
5639
|
"revised_outline_path": str(revised_outline_path) if revised_outline_path else None,
|
|
4551
5640
|
"record": resolved_record,
|
|
4552
5641
|
"artifact": artifact,
|
|
5642
|
+
"interaction": interaction,
|
|
4553
5643
|
}
|
|
4554
5644
|
|
|
4555
5645
|
def submit_paper_bundle(
|
|
@@ -4673,6 +5763,50 @@ class ArtifactService:
|
|
|
4673
5763
|
checkpoint=False,
|
|
4674
5764
|
workspace_root=workspace_root,
|
|
4675
5765
|
)
|
|
5766
|
+
interaction = self.interact(
|
|
5767
|
+
quest_root,
|
|
5768
|
+
kind="milestone",
|
|
5769
|
+
message=self._build_paper_bundle_interaction_message(
|
|
5770
|
+
title=str(manifest.get("title") or "").strip() or None,
|
|
5771
|
+
summary=str(manifest.get("summary") or "").strip() or None,
|
|
5772
|
+
paper_branch=paper_branch,
|
|
5773
|
+
source_branch=source_branch,
|
|
5774
|
+
source_run_id=source_run_id,
|
|
5775
|
+
selected_outline_ref=str(manifest.get("selected_outline_ref") or "").strip() or None,
|
|
5776
|
+
manifest_rel_path=self._workspace_relative(quest_root, manifest_path),
|
|
5777
|
+
draft_rel_path=str(manifest.get("draft_path") or "").strip() or None,
|
|
5778
|
+
writing_plan_rel_path=str(manifest.get("writing_plan_path") or "").strip() or None,
|
|
5779
|
+
references_rel_path=str(manifest.get("references_path") or "").strip() or None,
|
|
5780
|
+
claim_evidence_map_rel_path=str(manifest.get("claim_evidence_map_path") or "").strip() or None,
|
|
5781
|
+
compile_report_rel_path=str(manifest.get("compile_report_path") or "").strip() or None,
|
|
5782
|
+
pdf_rel_path=str(manifest.get("pdf_path") or "").strip() or None,
|
|
5783
|
+
latex_root_rel_path=str(manifest.get("latex_root_path") or "").strip() or None,
|
|
5784
|
+
baseline_inventory_rel_path=paper_inventory_rel,
|
|
5785
|
+
open_source_manifest_rel_path=str(manifest.get("open_source_manifest_path") or "").strip() or None,
|
|
5786
|
+
),
|
|
5787
|
+
deliver_to_bound_conversations=True,
|
|
5788
|
+
include_recent_inbound_messages=False,
|
|
5789
|
+
attachments=[
|
|
5790
|
+
{
|
|
5791
|
+
"kind": "paper_bundle",
|
|
5792
|
+
"title": manifest.get("title"),
|
|
5793
|
+
"paper_branch": paper_branch,
|
|
5794
|
+
"source_branch": source_branch,
|
|
5795
|
+
"source_run_id": source_run_id,
|
|
5796
|
+
"selected_outline_ref": manifest.get("selected_outline_ref"),
|
|
5797
|
+
"manifest_path": str(manifest_path),
|
|
5798
|
+
"draft_path": manifest.get("draft_path"),
|
|
5799
|
+
"writing_plan_path": manifest.get("writing_plan_path"),
|
|
5800
|
+
"references_path": manifest.get("references_path"),
|
|
5801
|
+
"claim_evidence_map_path": manifest.get("claim_evidence_map_path"),
|
|
5802
|
+
"compile_report_path": manifest.get("compile_report_path"),
|
|
5803
|
+
"pdf_path": manifest.get("pdf_path"),
|
|
5804
|
+
"latex_root_path": manifest.get("latex_root_path"),
|
|
5805
|
+
"baseline_inventory_path": str(baseline_inventory_path),
|
|
5806
|
+
"open_source_manifest_path": str(self._open_source_manifest_path(quest_root, workspace_root=workspace_root)),
|
|
5807
|
+
}
|
|
5808
|
+
],
|
|
5809
|
+
)
|
|
4676
5810
|
return {
|
|
4677
5811
|
"ok": True,
|
|
4678
5812
|
"manifest_path": str(manifest_path),
|
|
@@ -4680,6 +5814,7 @@ class ArtifactService:
|
|
|
4680
5814
|
"baseline_inventory_path": str(baseline_inventory_path),
|
|
4681
5815
|
"open_source_manifest_path": str(self._open_source_manifest_path(quest_root, workspace_root=workspace_root)),
|
|
4682
5816
|
"artifact": artifact,
|
|
5817
|
+
"interaction": interaction,
|
|
4683
5818
|
}
|
|
4684
5819
|
|
|
4685
5820
|
def record_analysis_slice(
|
|
@@ -4716,7 +5851,17 @@ class ArtifactService:
|
|
|
4716
5851
|
|
|
4717
5852
|
evidence_paths = [str(item).strip() for item in (evidence_paths or []) if str(item).strip()]
|
|
4718
5853
|
deviations = [str(item).strip() for item in (deviations or []) if str(item).strip()]
|
|
4719
|
-
|
|
5854
|
+
normalized_metric_rows = normalize_metric_rows(metric_rows or [])
|
|
5855
|
+
normalized_metrics_summary = {
|
|
5856
|
+
str(item.get("metric_id") or "").strip(): item.get("value")
|
|
5857
|
+
for item in normalized_metric_rows
|
|
5858
|
+
if str(item.get("metric_id") or "").strip()
|
|
5859
|
+
}
|
|
5860
|
+
normalized_metric_contract = normalize_metric_contract(
|
|
5861
|
+
{},
|
|
5862
|
+
metrics_summary=normalized_metrics_summary,
|
|
5863
|
+
metric_rows=normalized_metric_rows,
|
|
5864
|
+
)
|
|
4720
5865
|
normalized_comparison_baselines = self._normalize_comparison_baselines(quest_root, comparison_baselines)
|
|
4721
5866
|
normalized_claim_impact = str(claim_impact or "").strip() or None
|
|
4722
5867
|
normalized_reviewer_resolution = str(reviewer_resolution or "").strip() or None
|
|
@@ -4784,9 +5929,9 @@ class ArtifactService:
|
|
|
4784
5929
|
result_lines.extend([f"- `{item}`" for item in evidence_paths])
|
|
4785
5930
|
else:
|
|
4786
5931
|
result_lines.append("- None recorded.")
|
|
4787
|
-
if
|
|
5932
|
+
if normalized_metric_rows:
|
|
4788
5933
|
result_lines.extend(["", "## Metric Rows", ""])
|
|
4789
|
-
for row in
|
|
5934
|
+
for row in normalized_metric_rows:
|
|
4790
5935
|
result_lines.append(f"- `{row}`")
|
|
4791
5936
|
result_lines.extend(["", "## Comparison Baselines", ""])
|
|
4792
5937
|
if normalized_comparison_baselines:
|
|
@@ -4806,16 +5951,6 @@ class ArtifactService:
|
|
|
4806
5951
|
result_lines.extend(["", "## Subset Approval", "", f"`{subset_approval_ref}`"])
|
|
4807
5952
|
write_text(result_path, "\n".join(result_lines).rstrip() + "\n")
|
|
4808
5953
|
|
|
4809
|
-
metrics_summary: dict[str, Any] = {}
|
|
4810
|
-
for row in metric_rows:
|
|
4811
|
-
name = str(row.get("name") or row.get("metric") or "").strip()
|
|
4812
|
-
if name:
|
|
4813
|
-
metrics_summary[name] = row.get("value")
|
|
4814
|
-
continue
|
|
4815
|
-
keys = [key for key in row.keys() if key not in {"split", "seed", "note", "notes"}]
|
|
4816
|
-
if len(keys) == 1:
|
|
4817
|
-
metrics_summary[keys[0]] = row.get(keys[0])
|
|
4818
|
-
|
|
4819
5954
|
result_payload = {
|
|
4820
5955
|
"schema_version": 1,
|
|
4821
5956
|
"result_kind": "analysis_slice",
|
|
@@ -4827,8 +5962,9 @@ class ArtifactService:
|
|
|
4827
5962
|
"run_kind": target.get("run_kind"),
|
|
4828
5963
|
"required_baselines": target.get("required_baselines") or [],
|
|
4829
5964
|
"comparison_baselines": normalized_comparison_baselines,
|
|
4830
|
-
"metrics_summary":
|
|
4831
|
-
"metric_rows":
|
|
5965
|
+
"metrics_summary": normalized_metrics_summary,
|
|
5966
|
+
"metric_rows": normalized_metric_rows,
|
|
5967
|
+
"metric_contract": normalized_metric_contract,
|
|
4832
5968
|
"dataset_scope": normalized_scope,
|
|
4833
5969
|
"subset_approval_ref": subset_approval_ref,
|
|
4834
5970
|
"setup": setup.strip() or None,
|
|
@@ -4914,7 +6050,11 @@ class ArtifactService:
|
|
|
4914
6050
|
"parent_branch": parent_branch,
|
|
4915
6051
|
"worktree_root": str(slice_worktree_root),
|
|
4916
6052
|
"worktree_rel_path": self._workspace_relative(quest_root, slice_worktree_root),
|
|
4917
|
-
"metrics_summary":
|
|
6053
|
+
"metrics_summary": normalized_metrics_summary,
|
|
6054
|
+
"metric_rows": normalized_metric_rows,
|
|
6055
|
+
"metric_contract": normalized_metric_contract,
|
|
6056
|
+
"comparison_baselines": normalized_comparison_baselines,
|
|
6057
|
+
"evidence_paths": evidence_paths,
|
|
4918
6058
|
"flow_type": "analysis_slice",
|
|
4919
6059
|
"protocol_step": "record",
|
|
4920
6060
|
"paths": {
|
|
@@ -4928,7 +6068,7 @@ class ArtifactService:
|
|
|
4928
6068
|
"must_not_simplify": target.get("must_not_simplify"),
|
|
4929
6069
|
"dataset_scope": normalized_scope,
|
|
4930
6070
|
"subset_approval_ref": subset_approval_ref,
|
|
4931
|
-
"metric_rows":
|
|
6071
|
+
"metric_rows": normalized_metric_rows,
|
|
4932
6072
|
"claim_impact": normalized_claim_impact,
|
|
4933
6073
|
"reviewer_resolution": normalized_reviewer_resolution,
|
|
4934
6074
|
"manuscript_update_hint": normalized_manuscript_update_hint,
|
|
@@ -4968,6 +6108,8 @@ class ArtifactService:
|
|
|
4968
6108
|
updated["reviewer_resolution"] = normalized_reviewer_resolution
|
|
4969
6109
|
updated["manuscript_update_hint"] = normalized_manuscript_update_hint
|
|
4970
6110
|
updated["next_recommendation"] = normalized_next_recommendation
|
|
6111
|
+
updated["metrics_summary"] = normalized_metrics_summary
|
|
6112
|
+
updated["metric_rows"] = normalized_metric_rows
|
|
4971
6113
|
updated["comparison_baselines"] = normalized_comparison_baselines
|
|
4972
6114
|
updated["evaluation_summary"] = normalized_evaluation_summary
|
|
4973
6115
|
updated_slices.append(updated)
|
|
@@ -5025,13 +6167,13 @@ class ArtifactService:
|
|
|
5025
6167
|
interaction = self.interact(
|
|
5026
6168
|
quest_root,
|
|
5027
6169
|
kind="progress",
|
|
5028
|
-
message=(
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
6170
|
+
message=self._build_analysis_slice_interaction_message(
|
|
6171
|
+
campaign_id=campaign_id,
|
|
6172
|
+
slice_id=slice_id,
|
|
6173
|
+
evaluation_summary=normalized_evaluation_summary,
|
|
6174
|
+
claim_impact=normalized_claim_impact,
|
|
6175
|
+
next_slice=next_slice,
|
|
6176
|
+
mirror_rel_path=self._workspace_relative(quest_root, mirror_path),
|
|
5035
6177
|
),
|
|
5036
6178
|
deliver_to_bound_conversations=True,
|
|
5037
6179
|
include_recent_inbound_messages=False,
|
|
@@ -5156,20 +6298,16 @@ class ArtifactService:
|
|
|
5156
6298
|
interaction = self.interact(
|
|
5157
6299
|
quest_root,
|
|
5158
6300
|
kind="milestone",
|
|
5159
|
-
message=(
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
(
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
)
|
|
5170
|
-
if writing_workspace
|
|
5171
|
-
else "Use the completed analysis evidence to make the next durable route decision."
|
|
5172
|
-
)
|
|
6301
|
+
message=self._build_analysis_complete_interaction_message(
|
|
6302
|
+
campaign_id=campaign_id,
|
|
6303
|
+
completed_slices=updated_slices,
|
|
6304
|
+
summary_rel_path=self._workspace_relative(quest_root, summary_path),
|
|
6305
|
+
writing_branch=writing_workspace.get("branch") if writing_workspace else None,
|
|
6306
|
+
writing_worktree_rel_path=(
|
|
6307
|
+
self._workspace_relative(quest_root, Path(str(writing_workspace.get("worktree_root"))))
|
|
6308
|
+
if writing_workspace and str(writing_workspace.get("worktree_root") or "").strip()
|
|
6309
|
+
else None
|
|
6310
|
+
),
|
|
5173
6311
|
),
|
|
5174
6312
|
deliver_to_bound_conversations=True,
|
|
5175
6313
|
include_recent_inbound_messages=False,
|
|
@@ -5259,6 +6397,81 @@ class ArtifactService:
|
|
|
5259
6397
|
"guidance": "The selected baseline is now attached under baselines/imported. Reuse it before considering a fresh reproduction.",
|
|
5260
6398
|
}
|
|
5261
6399
|
|
|
6400
|
+
def delete_baseline(self, baseline_id: str) -> dict[str, Any]:
|
|
6401
|
+
existing = self.baselines.get(baseline_id, include_deleted=True)
|
|
6402
|
+
if existing is None:
|
|
6403
|
+
raise FileNotFoundError(f"Unknown baseline: {baseline_id}")
|
|
6404
|
+
|
|
6405
|
+
normalized_baseline_id = str(existing.get("baseline_id") or existing.get("entry_id") or baseline_id).strip()
|
|
6406
|
+
already_deleted = self.baselines.is_deleted(normalized_baseline_id)
|
|
6407
|
+
deleted_entry = self.baselines.delete(normalized_baseline_id) if not already_deleted else dict(existing)
|
|
6408
|
+
|
|
6409
|
+
affected_quest_ids: list[str] = []
|
|
6410
|
+
cleared_requested_refs = 0
|
|
6411
|
+
cleared_confirmed_refs = 0
|
|
6412
|
+
deleted_paths: list[str] = []
|
|
6413
|
+
warnings: list[str] = []
|
|
6414
|
+
quests_root = self.home / "quests"
|
|
6415
|
+
|
|
6416
|
+
for quest_yaml in sorted(quests_root.glob("*/quest.yaml")):
|
|
6417
|
+
quest_root = quest_yaml.parent
|
|
6418
|
+
quest_id = quest_root.name
|
|
6419
|
+
quest_touched = False
|
|
6420
|
+
quest_payload = self.quest_service.read_quest_yaml(quest_root)
|
|
6421
|
+
|
|
6422
|
+
requested_ref = (
|
|
6423
|
+
dict(quest_payload.get("requested_baseline_ref") or {})
|
|
6424
|
+
if isinstance(quest_payload.get("requested_baseline_ref"), dict)
|
|
6425
|
+
else {}
|
|
6426
|
+
)
|
|
6427
|
+
if str(requested_ref.get("baseline_id") or "").strip() == normalized_baseline_id:
|
|
6428
|
+
self.quest_service.update_startup_context(quest_root, requested_baseline_ref=None)
|
|
6429
|
+
cleared_requested_refs += 1
|
|
6430
|
+
quest_touched = True
|
|
6431
|
+
|
|
6432
|
+
confirmed_ref = (
|
|
6433
|
+
dict(quest_payload.get("confirmed_baseline_ref") or {})
|
|
6434
|
+
if isinstance(quest_payload.get("confirmed_baseline_ref"), dict)
|
|
6435
|
+
else {}
|
|
6436
|
+
)
|
|
6437
|
+
if str(confirmed_ref.get("baseline_id") or "").strip() == normalized_baseline_id:
|
|
6438
|
+
self.quest_service.update_baseline_state(
|
|
6439
|
+
quest_root,
|
|
6440
|
+
baseline_gate="pending",
|
|
6441
|
+
confirmed_baseline_ref=None,
|
|
6442
|
+
active_anchor="baseline",
|
|
6443
|
+
)
|
|
6444
|
+
cleared_confirmed_refs += 1
|
|
6445
|
+
quest_touched = True
|
|
6446
|
+
|
|
6447
|
+
for root in self._baseline_workspace_roots(quest_root):
|
|
6448
|
+
try:
|
|
6449
|
+
removed = self._remove_baseline_materialization(root, normalized_baseline_id)
|
|
6450
|
+
except OSError as exc:
|
|
6451
|
+
warnings.append(
|
|
6452
|
+
f"Unable to remove baseline materialization under `{root}` for quest `{quest_id}`: {exc}"
|
|
6453
|
+
)
|
|
6454
|
+
continue
|
|
6455
|
+
if removed:
|
|
6456
|
+
deleted_paths.extend(removed)
|
|
6457
|
+
quest_touched = True
|
|
6458
|
+
|
|
6459
|
+
if quest_touched:
|
|
6460
|
+
affected_quest_ids.append(quest_id)
|
|
6461
|
+
|
|
6462
|
+
return {
|
|
6463
|
+
"ok": True,
|
|
6464
|
+
"baseline_id": normalized_baseline_id,
|
|
6465
|
+
"deleted": not already_deleted,
|
|
6466
|
+
"already_deleted": already_deleted,
|
|
6467
|
+
"baseline_registry_entry": deleted_entry,
|
|
6468
|
+
"affected_quest_ids": affected_quest_ids,
|
|
6469
|
+
"cleared_requested_refs": cleared_requested_refs,
|
|
6470
|
+
"cleared_confirmed_refs": cleared_confirmed_refs,
|
|
6471
|
+
"deleted_paths": deleted_paths,
|
|
6472
|
+
"warnings": warnings,
|
|
6473
|
+
}
|
|
6474
|
+
|
|
5262
6475
|
def confirm_baseline(
|
|
5263
6476
|
self,
|
|
5264
6477
|
quest_root: Path,
|
|
@@ -5270,6 +6483,7 @@ class ArtifactService:
|
|
|
5270
6483
|
summary: str | None = None,
|
|
5271
6484
|
baseline_kind: str | None = None,
|
|
5272
6485
|
metric_contract: dict[str, Any] | None = None,
|
|
6486
|
+
metric_directions: dict[str, str] | None = None,
|
|
5273
6487
|
metrics_summary: dict[str, Any] | None = None,
|
|
5274
6488
|
primary_metric: dict[str, Any] | None = None,
|
|
5275
6489
|
auto_advance: bool = True,
|
|
@@ -5370,6 +6584,19 @@ class ArtifactService:
|
|
|
5370
6584
|
if isinstance(selected_variant, dict) and selected_variant.get("metrics_summary") is not None
|
|
5371
6585
|
else entry.get("metrics_summary")
|
|
5372
6586
|
)
|
|
6587
|
+
entry_metric_contract, entry_primary_metric = self._apply_metric_directions_to_contract(
|
|
6588
|
+
metric_contract=entry.get("metric_contract"),
|
|
6589
|
+
metric_directions=metric_directions,
|
|
6590
|
+
baseline_id=resolved_baseline_id,
|
|
6591
|
+
metrics_summary=source_metrics_summary,
|
|
6592
|
+
primary_metric=entry.get("primary_metric"),
|
|
6593
|
+
baseline_variants=entry.get("baseline_variants"),
|
|
6594
|
+
)
|
|
6595
|
+
entry = {
|
|
6596
|
+
**entry,
|
|
6597
|
+
"metric_contract": entry_metric_contract,
|
|
6598
|
+
"primary_metric": entry_primary_metric or entry.get("primary_metric"),
|
|
6599
|
+
}
|
|
5373
6600
|
canonical_baseline = (
|
|
5374
6601
|
validate_baseline_metric_contract_submission(
|
|
5375
6602
|
metric_contract=entry.get("metric_contract"),
|
|
@@ -5534,6 +6761,8 @@ class ArtifactService:
|
|
|
5534
6761
|
"snapshot": self.quest_service.snapshot(self._quest_id(quest_root)),
|
|
5535
6762
|
"metric_details": canonical_baseline["metric_details"],
|
|
5536
6763
|
"legacy_guidance": "Baseline gate confirmed. Idea selection is now the default next anchor.",
|
|
6764
|
+
"metric_contract_json_path": str(metric_contract_json.get("path") or ""),
|
|
6765
|
+
"metric_contract_json_rel_path": str(metric_contract_json.get("rel_path") or ""),
|
|
5537
6766
|
}
|
|
5538
6767
|
|
|
5539
6768
|
def waive_baseline(
|
|
@@ -6157,11 +7386,18 @@ class ArtifactService:
|
|
|
6157
7386
|
return f"run/{run_id or generate_id('run')}"
|
|
6158
7387
|
|
|
6159
7388
|
def _bound_conversations(self, quest_root: Path) -> list[str]:
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
7389
|
+
quest_id = self._quest_id(quest_root)
|
|
7390
|
+
sources = [
|
|
7391
|
+
self._normalize_conversation_id(str(item))
|
|
7392
|
+
for item in self.quest_service.binding_sources(quest_id)
|
|
7393
|
+
]
|
|
7394
|
+
authoritative_keys = {conversation_identity_key(item) for item in sources}
|
|
7395
|
+
connector_sources = [
|
|
7396
|
+
item
|
|
7397
|
+
for item in self._connector_bound_conversations(quest_id)
|
|
7398
|
+
if conversation_identity_key(item) in authoritative_keys
|
|
7399
|
+
]
|
|
7400
|
+
return self._dedupe_targets([*sources, *connector_sources])
|
|
6165
7401
|
|
|
6166
7402
|
def _connector_bound_conversations(self, quest_id: str) -> list[str]:
|
|
6167
7403
|
root = self.home / "logs" / "connectors"
|