@researai/deepscientist 1.5.13 → 1.5.15
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/README.md +8 -0
- package/assets/branding/logo-raster.png +0 -0
- package/bin/ds.js +134 -49
- package/docs/en/00_QUICK_START.md +2 -2
- package/docs/en/01_SETTINGS_REFERENCE.md +20 -4
- package/docs/en/03_QQ_CONNECTOR_GUIDE.md +19 -0
- package/docs/en/05_TUI_GUIDE.md +466 -96
- package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
- package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +2 -0
- package/docs/en/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
- package/docs/en/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
- package/docs/en/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
- package/docs/en/README.md +8 -0
- package/docs/zh/00_QUICK_START.md +2 -2
- package/docs/zh/01_SETTINGS_REFERENCE.md +20 -4
- package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +19 -0
- package/docs/zh/05_TUI_GUIDE.md +465 -82
- package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
- package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +2 -0
- package/docs/zh/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
- package/docs/zh/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
- package/docs/zh/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
- package/docs/zh/README.md +8 -0
- package/install.sh +2 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/artifact/charts.py +567 -0
- package/src/deepscientist/artifact/guidance.py +50 -10
- package/src/deepscientist/artifact/metrics.py +228 -5
- package/src/deepscientist/artifact/schemas.py +3 -0
- package/src/deepscientist/artifact/service.py +4004 -538
- package/src/deepscientist/bash_exec/models.py +23 -0
- package/src/deepscientist/bash_exec/monitor.py +147 -67
- package/src/deepscientist/bash_exec/runtime.py +218 -156
- package/src/deepscientist/bash_exec/service.py +79 -64
- package/src/deepscientist/bash_exec/shells.py +87 -0
- package/src/deepscientist/bridges/connectors.py +51 -2
- package/src/deepscientist/config/models.py +6 -3
- package/src/deepscientist/config/service.py +7 -2
- package/src/deepscientist/connector/lingzhu_support.py +23 -4
- package/src/deepscientist/connector/weixin_support.py +122 -1
- package/src/deepscientist/daemon/api/handlers.py +75 -4
- package/src/deepscientist/daemon/api/router.py +1 -0
- package/src/deepscientist/daemon/app.py +869 -236
- package/src/deepscientist/doctor.py +51 -0
- package/src/deepscientist/file_lock.py +48 -0
- package/src/deepscientist/gitops/diff.py +167 -1
- package/src/deepscientist/mcp/server.py +331 -21
- package/src/deepscientist/process_control.py +161 -0
- package/src/deepscientist/prompts/builder.py +275 -491
- package/src/deepscientist/quest/service.py +2336 -145
- package/src/deepscientist/quest/stage_views.py +305 -29
- package/src/deepscientist/runners/base.py +2 -0
- package/src/deepscientist/runners/codex.py +88 -5
- package/src/deepscientist/runners/runtime_overrides.py +17 -1
- package/src/deepscientist/shared.py +6 -1
- package/src/prompts/contracts/shared_interaction.md +13 -4
- package/src/prompts/system.md +984 -1985
- package/src/skills/analysis-campaign/SKILL.md +31 -2
- package/src/skills/analysis-campaign/references/artifact-orchestration.md +1 -1
- package/src/skills/analysis-campaign/references/writing-facing-slice-examples.md +65 -0
- package/src/skills/baseline/SKILL.md +267 -994
- package/src/skills/baseline/references/baseline-checklist-template.md +21 -32
- package/src/skills/baseline/references/baseline-plan-template.md +41 -57
- package/src/skills/decision/SKILL.md +19 -2
- package/src/skills/experiment/SKILL.md +8 -2
- package/src/skills/finalize/SKILL.md +18 -0
- package/src/skills/idea/SKILL.md +78 -0
- package/src/skills/idea/references/idea-generation-playbook.md +100 -0
- package/src/skills/idea/references/outline-seeding-example.md +60 -0
- package/src/skills/intake-audit/SKILL.md +1 -1
- package/src/skills/optimize/SKILL.md +1644 -0
- package/src/skills/rebuttal/SKILL.md +2 -1
- package/src/skills/review/SKILL.md +2 -1
- package/src/skills/write/SKILL.md +80 -12
- package/src/skills/write/references/outline-evidence-contract-example.md +107 -0
- package/src/tui/dist/app/AppContainer.js +1445 -52
- package/src/tui/dist/components/Composer.js +1 -1
- package/src/tui/dist/components/ConfigScreen.js +190 -36
- package/src/tui/dist/components/GradientStatusText.js +1 -20
- package/src/tui/dist/components/InputPrompt.js +41 -32
- package/src/tui/dist/components/LoadingIndicator.js +1 -1
- package/src/tui/dist/components/Logo.js +61 -38
- package/src/tui/dist/components/MainContent.js +10 -3
- package/src/tui/dist/components/WelcomePanel.js +4 -12
- package/src/tui/dist/components/messages/AssistantMessage.js +1 -1
- package/src/tui/dist/components/messages/BashExecOperationMessage.js +3 -3
- package/src/tui/dist/components/messages/OperationMessage.js +1 -1
- package/src/tui/dist/index.js +28 -1
- package/src/tui/dist/layouts/DefaultAppLayout.js +3 -3
- package/src/tui/dist/lib/api.js +17 -0
- package/src/tui/dist/lib/connectors.js +261 -0
- package/src/tui/dist/semantic-colors.js +29 -19
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-CnJcXynW.js → AiManusChatView-DDjbFnbt.js} +12 -12
- package/src/ui/dist/assets/{AnalysisPlugin-DeyzPEhV.js → AnalysisPlugin-Yb5IdmaU.js} +1 -1
- package/src/ui/dist/assets/CliPlugin-e64sreyu.js +31037 -0
- package/src/ui/dist/assets/{CodeEditorPlugin-B-xicq1e.js → CodeEditorPlugin-C4D2TIkU.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-DT54ysXa.js → CodeViewerPlugin-BVoNZIvC.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-DQtKT-VD.js → DocViewerPlugin-CLChbllo.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-hqHbCfnv.js → GitDiffViewerPlugin-C4xeFyFQ.js} +20 -20
- package/src/ui/dist/assets/{ImageViewerPlugin-OcVo33jV.js → ImageViewerPlugin-OiMUAcLi.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-DdGwhEUV.js → LabCopilotPanel-BjD2ThQF.js} +11 -11
- package/src/ui/dist/assets/{LabPlugin-Ciz1gDaX.js → LabPlugin-DQPg-NrB.js} +2 -2
- package/src/ui/dist/assets/{LatexPlugin-BhmjNQRC.js → LatexPlugin-CI05XAV9.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-BzdVH9Bx.js → MarkdownViewerPlugin-DpeBLYZf.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-DmyHspXt.js → MarketplacePlugin-DolE58Q2.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-BTVYRGkm.js → NotebookEditor-7Qm2rSWD.js} +11 -11
- package/src/ui/dist/assets/{NotebookEditor-BMXKrDRk.js → NotebookEditor-C1kWaxKi.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-CvcjJHXv.js → PdfLoader-BfOHw8Zw.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-DW2ej8Vk.js → PdfMarkdownPlugin-BulDREv1.js} +2 -2
- package/src/ui/dist/assets/{PdfViewerPlugin-CmlDxbhU.js → PdfViewerPlugin-C-daaOaL.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-DAjQZPSv.js → SearchPlugin-CjpaiJ3A.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-C-nVAZb_.js → TextViewerPlugin-BxIyqPQC.js} +5 -5
- package/src/ui/dist/assets/{VNCViewer-D7-dIYon.js → VNCViewer-HAg9mF7M.js} +10 -10
- package/src/ui/dist/assets/{bot-C_G4WtNI.js → bot-0DYntytV.js} +1 -1
- package/src/ui/dist/assets/{code-Cd7WfiWq.js → code-B20Slj_w.js} +1 -1
- package/src/ui/dist/assets/{file-content-B57zsL9y.js → file-content-DT24KFma.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-DVoheLFq.js → file-diff-panel-DK13YPql.js} +1 -1
- package/src/ui/dist/assets/{file-socket-B5kXFxZP.js → file-socket-B4T2o4nR.js} +1 -1
- package/src/ui/dist/assets/{image-LLOjkMHF.js → image-DSeR_sDS.js} +1 -1
- package/src/ui/dist/assets/{index-hOUOWbW2.js → index-BrFje2Uk.js} +2 -2
- package/src/ui/dist/assets/{index-Dxa2eYMY.js → index-BwRJaoTl.js} +1 -1
- package/src/ui/dist/assets/{index-CLQauncb.js → index-D_E4281X.js} +5418 -28620
- package/src/ui/dist/assets/{index-C3r2iGrp.js → index-DnYB3xb1.js} +12 -12
- package/src/ui/dist/assets/{index-BQG-1s2o.css → index-G7AcWcMu.css} +43 -2
- package/src/ui/dist/assets/{monaco-BGGAEii3.js → monaco-LExaAN3Y.js} +1 -1
- package/src/ui/dist/assets/{pdf-effect-queue-DlEr1_y5.js → pdf-effect-queue-BJk5okWJ.js} +1 -1
- package/src/ui/dist/assets/{popover-CWJbJuYY.js → popover-D3Gg_FoV.js} +1 -1
- package/src/ui/dist/assets/{project-sync-CRJiucYO.js → project-sync-C_ygLlVU.js} +1 -1
- package/src/ui/dist/assets/{select-CoHB7pvH.js → select-CpAK6uWm.js} +2 -2
- package/src/ui/dist/assets/{sigma-D5aJWR8J.js → sigma-DEccaSgk.js} +1 -1
- package/src/ui/dist/assets/{square-check-big-DUK_mnkS.js → square-check-big-uUfyVsbD.js} +1 -1
- package/src/ui/dist/assets/{trash-ChU3SEE3.js → trash-CXvwwSe8.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-BrJBV3tY.js → useCliAccess-Bnop4mgR.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-C2OQaVWc.js → useFileDiffOverlay-B8eUAX0I.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-C7Qqh-om.js → wrap-text-9vbOBpkW.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-rtX0FKya.js → zoom-out-BgVMmOW4.js} +1 -1
- package/src/ui/dist/index.html +2 -2
- package/uv.lock +1 -1
- package/src/ui/dist/assets/CliPlugin-CB1YODQn.js +0 -5905
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import math
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
MORANDI_BG = "#F6F1EA"
|
|
12
|
+
MORANDI_PANEL = "#FFFDF8"
|
|
13
|
+
MORANDI_GRID = "#E2D8CB"
|
|
14
|
+
MORANDI_AXIS = "#A99A8A"
|
|
15
|
+
MORANDI_TEXT = "#4F4942"
|
|
16
|
+
MORANDI_TEXT_MUTED = "#7A736A"
|
|
17
|
+
MORANDI_BLUE = "#445F7D"
|
|
18
|
+
MORANDI_RED = "#8F5C62"
|
|
19
|
+
MORANDI_GOLD = "#C2A15C"
|
|
20
|
+
MORANDI_GOLD_STROKE = "#A78549"
|
|
21
|
+
MORANDI_BORDER = "#DED1C0"
|
|
22
|
+
MORANDI_SOFT_BLUE = "#D7E0E6"
|
|
23
|
+
MORANDI_SOFT_RED = "#E7D5D8"
|
|
24
|
+
MORANDI_CARD = "#FBF7F1"
|
|
25
|
+
MORANDI_PLOT_BG = "#FFFCF8"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def render_main_experiment_metric_timeline_chart(
|
|
29
|
+
*,
|
|
30
|
+
series: dict[str, Any],
|
|
31
|
+
output_path: Path,
|
|
32
|
+
style: str = "branded",
|
|
33
|
+
) -> dict[str, Any]:
|
|
34
|
+
normalized_style = "branded"
|
|
35
|
+
baseline = _select_baseline(series)
|
|
36
|
+
points = [dict(item) for item in (series.get("points") or []) if isinstance(item, dict)]
|
|
37
|
+
direction = _normalize_direction(series.get("direction"))
|
|
38
|
+
label = str(series.get("label") or series.get("metric_id") or "Metric").strip() or "Metric"
|
|
39
|
+
metric_id = str(series.get("metric_id") or label).strip() or label
|
|
40
|
+
decimals = series.get("decimals") if isinstance(series.get("decimals"), int) else None
|
|
41
|
+
unit = str(series.get("unit") or "").strip() or None
|
|
42
|
+
human_label = _humanize_metric_label(label)
|
|
43
|
+
|
|
44
|
+
width = 1360
|
|
45
|
+
height = 820
|
|
46
|
+
header_height = 178
|
|
47
|
+
footer_height = 132
|
|
48
|
+
summary_width = 0
|
|
49
|
+
padding = {"left": 92, "right": 56, "top": 38, "bottom": 42}
|
|
50
|
+
plot_left = padding["left"]
|
|
51
|
+
plot_top = header_height
|
|
52
|
+
plot_right = width - 54
|
|
53
|
+
plot_bottom = height - footer_height
|
|
54
|
+
plot_width = plot_right - plot_left
|
|
55
|
+
plot_height = plot_bottom - plot_top
|
|
56
|
+
|
|
57
|
+
values = [
|
|
58
|
+
float(item["value"])
|
|
59
|
+
for item in points
|
|
60
|
+
if isinstance(item.get("value"), (int, float)) and math.isfinite(float(item["value"]))
|
|
61
|
+
]
|
|
62
|
+
if isinstance(baseline.get("value"), (int, float)) and math.isfinite(float(baseline["value"])):
|
|
63
|
+
values.append(float(baseline["value"]))
|
|
64
|
+
if not values:
|
|
65
|
+
values = [0.0, 1.0]
|
|
66
|
+
min_value = min(values)
|
|
67
|
+
max_value = max(values)
|
|
68
|
+
if math.isclose(min_value, max_value):
|
|
69
|
+
min_value -= 1.0
|
|
70
|
+
max_value += 1.0
|
|
71
|
+
padding_value = (max_value - min_value) * 0.12
|
|
72
|
+
min_value -= padding_value
|
|
73
|
+
max_value += padding_value
|
|
74
|
+
value_range = max(max_value - min_value, 1e-9)
|
|
75
|
+
|
|
76
|
+
image = Image.new("RGBA", (width, height), MORANDI_BG)
|
|
77
|
+
draw = ImageDraw.Draw(image)
|
|
78
|
+
draw.rounded_rectangle(
|
|
79
|
+
(18, 18, width - 18, height - 18),
|
|
80
|
+
radius=30,
|
|
81
|
+
fill=MORANDI_PANEL,
|
|
82
|
+
outline=MORANDI_GRID,
|
|
83
|
+
width=1,
|
|
84
|
+
)
|
|
85
|
+
draw.rounded_rectangle(
|
|
86
|
+
(plot_left - 18, plot_top - 18, plot_right + 18, plot_bottom + 18),
|
|
87
|
+
radius=26,
|
|
88
|
+
fill=MORANDI_PLOT_BG,
|
|
89
|
+
outline=MORANDI_BORDER,
|
|
90
|
+
width=1,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
title_font = _load_font(38, bold=True)
|
|
94
|
+
subtitle_font = _load_font(18, bold=False)
|
|
95
|
+
axis_font = _load_font(16, bold=False)
|
|
96
|
+
badge_font = _load_font(17, bold=True)
|
|
97
|
+
card_label_font = _load_font(15, bold=True)
|
|
98
|
+
_draw_brand_lockup(draw, image, width=width)
|
|
99
|
+
|
|
100
|
+
title = human_label
|
|
101
|
+
subtitle_parts = [metric_id]
|
|
102
|
+
subtitle_parts.append("higher is better" if direction == "maximize" else "lower is better")
|
|
103
|
+
if unit:
|
|
104
|
+
subtitle_parts.append(unit)
|
|
105
|
+
subtitle = " · ".join(subtitle_parts)
|
|
106
|
+
|
|
107
|
+
draw.text((52, 44), title, fill=MORANDI_TEXT, font=title_font)
|
|
108
|
+
draw.text((52, 92), subtitle, fill=MORANDI_TEXT_MUTED, font=subtitle_font)
|
|
109
|
+
_draw_header_badges(
|
|
110
|
+
draw,
|
|
111
|
+
x=52,
|
|
112
|
+
y=122,
|
|
113
|
+
labels=[
|
|
114
|
+
"Main Experiment",
|
|
115
|
+
f"{len(points)} runs",
|
|
116
|
+
"Baseline reference" if isinstance(baseline.get("value"), (int, float)) else "No baseline line",
|
|
117
|
+
],
|
|
118
|
+
font=badge_font,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
for step in range(5):
|
|
122
|
+
ratio = step / 4 if 4 else 0
|
|
123
|
+
y = plot_top + plot_height - ratio * plot_height
|
|
124
|
+
draw.line((plot_left, y, plot_right, y), fill=MORANDI_GRID, width=1)
|
|
125
|
+
value = min_value + ratio * value_range
|
|
126
|
+
label_text = _format_metric_value(value, decimals)
|
|
127
|
+
bbox = draw.textbbox((0, 0), label_text, font=axis_font)
|
|
128
|
+
draw.text(
|
|
129
|
+
(plot_left - 16 - (bbox[2] - bbox[0]), y - (bbox[3] - bbox[1]) / 2),
|
|
130
|
+
label_text,
|
|
131
|
+
fill=MORANDI_TEXT_MUTED,
|
|
132
|
+
font=axis_font,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
axis_color = MORANDI_AXIS
|
|
136
|
+
draw.line((plot_left, plot_bottom, plot_right, plot_bottom), fill=axis_color, width=2)
|
|
137
|
+
draw.line((plot_left, plot_top, plot_left, plot_bottom), fill=axis_color, width=2)
|
|
138
|
+
|
|
139
|
+
if isinstance(baseline.get("value"), (int, float)) and math.isfinite(float(baseline["value"])):
|
|
140
|
+
baseline_y = _value_to_y(float(baseline["value"]), plot_top, plot_height, min_value, value_range)
|
|
141
|
+
_draw_dashed_line(
|
|
142
|
+
draw,
|
|
143
|
+
(plot_left, baseline_y),
|
|
144
|
+
(plot_right, baseline_y),
|
|
145
|
+
fill=MORANDI_GOLD,
|
|
146
|
+
width=3,
|
|
147
|
+
dash=12,
|
|
148
|
+
gap=8,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
point_positions: list[tuple[float, float, dict[str, Any], bool, bool]] = []
|
|
152
|
+
point_slots: list[dict[str, Any]] = []
|
|
153
|
+
if isinstance(baseline.get("value"), (int, float)) and math.isfinite(float(baseline["value"])):
|
|
154
|
+
point_slots.append(
|
|
155
|
+
{
|
|
156
|
+
"slot_key": "baseline",
|
|
157
|
+
"display_label": "Base",
|
|
158
|
+
"value": float(baseline["value"]),
|
|
159
|
+
"delta_vs_baseline": 0.0,
|
|
160
|
+
"baseline_slot": True,
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
for point in points:
|
|
164
|
+
point_slots.append(
|
|
165
|
+
{
|
|
166
|
+
"slot_key": point.get("seq"),
|
|
167
|
+
"display_label": f"R{point.get('seq') or len(point_slots)}",
|
|
168
|
+
"value": point.get("value"),
|
|
169
|
+
"delta_vs_baseline": point.get("delta_vs_baseline"),
|
|
170
|
+
"baseline_slot": False,
|
|
171
|
+
**point,
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
x_count = max(1, len(point_slots))
|
|
175
|
+
for index, point in enumerate(point_slots):
|
|
176
|
+
value = point.get("value")
|
|
177
|
+
if not isinstance(value, (int, float)) or not math.isfinite(float(value)):
|
|
178
|
+
continue
|
|
179
|
+
x = plot_left if x_count == 1 else plot_left + (index / (x_count - 1)) * plot_width
|
|
180
|
+
y = _value_to_y(float(value), plot_top, plot_height, min_value, value_range)
|
|
181
|
+
beats_baseline = _beats_baseline(
|
|
182
|
+
value=float(value),
|
|
183
|
+
baseline_value=float(baseline["value"]) if isinstance(baseline.get("value"), (int, float)) else None,
|
|
184
|
+
delta=point.get("delta_vs_baseline"),
|
|
185
|
+
direction=direction,
|
|
186
|
+
) if not bool(point.get("baseline_slot")) else False
|
|
187
|
+
point_positions.append((x, y, point, beats_baseline, bool(point.get("baseline_slot"))))
|
|
188
|
+
|
|
189
|
+
for index in range(1, len(point_positions)):
|
|
190
|
+
x0, y0, *_ = point_positions[index - 1]
|
|
191
|
+
x1, y1, *_ = point_positions[index]
|
|
192
|
+
draw.line((x0, y0, x1, y1), fill=MORANDI_BLUE, width=4)
|
|
193
|
+
|
|
194
|
+
latest_index = len(point_positions) - 1
|
|
195
|
+
for index, (x, y, point, beats_baseline, baseline_slot) in enumerate(point_positions):
|
|
196
|
+
fill = MORANDI_GOLD if baseline_slot else MORANDI_RED if index == latest_index else MORANDI_BLUE
|
|
197
|
+
radius = 8 if baseline_slot else 9 if index == latest_index else 7
|
|
198
|
+
draw.ellipse((x - radius, y - radius, x + radius, y + radius), fill=fill, outline=MORANDI_PANEL, width=3)
|
|
199
|
+
if beats_baseline:
|
|
200
|
+
_draw_star(draw, x, y - 18, outer_radius=10, inner_radius=4.2, fill=MORANDI_GOLD, outline=MORANDI_GOLD_STROKE)
|
|
201
|
+
tick_label = str(point.get("display_label") or f"R{index + 1}")
|
|
202
|
+
bbox = draw.textbbox((0, 0), tick_label, font=axis_font)
|
|
203
|
+
draw.text((x - (bbox[2] - bbox[0]) / 2, plot_bottom + 12), tick_label, fill=MORANDI_TEXT_MUTED, font=axis_font)
|
|
204
|
+
|
|
205
|
+
_draw_plot_legend(
|
|
206
|
+
draw,
|
|
207
|
+
x=plot_right - 4,
|
|
208
|
+
y=plot_top - 50,
|
|
209
|
+
baseline_value=float(baseline["value"]) if isinstance(baseline.get("value"), (int, float)) else None,
|
|
210
|
+
decimals=decimals,
|
|
211
|
+
font=badge_font,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
latest_value = point_positions[-1][2].get("value") if point_positions else None
|
|
215
|
+
latest_delta = point_positions[-1][2].get("delta_vs_baseline") if point_positions else None
|
|
216
|
+
latest_beats_baseline = point_positions[-1][3] if point_positions else False
|
|
217
|
+
_draw_footer_summary(
|
|
218
|
+
draw,
|
|
219
|
+
left=plot_left,
|
|
220
|
+
top=plot_bottom + 36,
|
|
221
|
+
width=plot_right - plot_left,
|
|
222
|
+
label_font=card_label_font,
|
|
223
|
+
badge_font=badge_font,
|
|
224
|
+
baseline_label=str(baseline.get("label") or "Baseline").strip() or "Baseline",
|
|
225
|
+
baseline_value=baseline.get("value"),
|
|
226
|
+
latest_value=latest_value,
|
|
227
|
+
delta_value=latest_delta,
|
|
228
|
+
decimals=decimals,
|
|
229
|
+
direction=direction,
|
|
230
|
+
beats_baseline=latest_beats_baseline,
|
|
231
|
+
unit=unit,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
if point_positions:
|
|
235
|
+
latest_x, latest_y, latest_point, _, latest_is_baseline = point_positions[-1]
|
|
236
|
+
latest_value = latest_point.get("value")
|
|
237
|
+
if not latest_is_baseline:
|
|
238
|
+
latest_text = _format_metric_value(latest_value, decimals)
|
|
239
|
+
_draw_latest_value_callout(
|
|
240
|
+
draw,
|
|
241
|
+
x=latest_x,
|
|
242
|
+
y=latest_y,
|
|
243
|
+
value_text=latest_text,
|
|
244
|
+
font=badge_font,
|
|
245
|
+
fill=MORANDI_SOFT_RED if latest_beats_baseline else MORANDI_CARD,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
ensure_parent(output_path)
|
|
249
|
+
image.save(output_path, format="PNG")
|
|
250
|
+
return {
|
|
251
|
+
"metric_id": metric_id,
|
|
252
|
+
"label": human_label,
|
|
253
|
+
"path": str(output_path),
|
|
254
|
+
"baseline_value": baseline.get("value"),
|
|
255
|
+
"latest_value": next((point[2].get("value") for point in reversed(point_positions) if not point[4]), None),
|
|
256
|
+
"point_count": len(point_positions),
|
|
257
|
+
"direction": direction,
|
|
258
|
+
"style": "branded",
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def ensure_parent(path: Path) -> None:
|
|
263
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _select_baseline(series: dict[str, Any]) -> dict[str, Any]:
|
|
267
|
+
baselines = [dict(item) for item in (series.get("baselines") or []) if isinstance(item, dict)]
|
|
268
|
+
selected = next(
|
|
269
|
+
(
|
|
270
|
+
item
|
|
271
|
+
for item in baselines
|
|
272
|
+
if bool(item.get("selected"))
|
|
273
|
+
and isinstance(item.get("value"), (int, float))
|
|
274
|
+
and math.isfinite(float(item["value"]))
|
|
275
|
+
),
|
|
276
|
+
None,
|
|
277
|
+
)
|
|
278
|
+
if selected is not None:
|
|
279
|
+
return selected
|
|
280
|
+
fallback = next(
|
|
281
|
+
(
|
|
282
|
+
item
|
|
283
|
+
for item in baselines
|
|
284
|
+
if isinstance(item.get("value"), (int, float))
|
|
285
|
+
and math.isfinite(float(item["value"]))
|
|
286
|
+
),
|
|
287
|
+
None,
|
|
288
|
+
)
|
|
289
|
+
return fallback or {}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _normalize_direction(value: object) -> str:
|
|
293
|
+
text = str(value or "").strip().lower().replace("-", "_").replace(" ", "_")
|
|
294
|
+
if text in {"lower", "minimize", "lower_better", "less_is_better"}:
|
|
295
|
+
return "minimize"
|
|
296
|
+
return "maximize"
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _format_metric_value(value: object, decimals: int | None) -> str:
|
|
300
|
+
if not isinstance(value, (int, float)) or not math.isfinite(float(value)):
|
|
301
|
+
return "—"
|
|
302
|
+
number = float(value)
|
|
303
|
+
if isinstance(decimals, int):
|
|
304
|
+
return f"{number:.{decimals}f}"
|
|
305
|
+
rendered = f"{number:.4f}"
|
|
306
|
+
return rendered.rstrip("0").rstrip(".")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _humanize_metric_label(value: str) -> str:
|
|
310
|
+
parts = [part for part in str(value or "").replace("-", "_").split("_") if part]
|
|
311
|
+
if not parts:
|
|
312
|
+
return "Metric"
|
|
313
|
+
return " ".join(part.upper() if len(part) <= 3 else part.capitalize() for part in parts)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _beats_baseline(*, value: float, baseline_value: float | None, delta: object, direction: str) -> bool:
|
|
317
|
+
if baseline_value is not None and math.isfinite(baseline_value):
|
|
318
|
+
return value < baseline_value if direction == "minimize" else value > baseline_value
|
|
319
|
+
if isinstance(delta, (int, float)) and math.isfinite(float(delta)):
|
|
320
|
+
return float(delta) < 0 if direction == "minimize" else float(delta) > 0
|
|
321
|
+
return False
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _value_to_y(value: float, plot_top: float, plot_height: float, min_value: float, value_range: float) -> float:
|
|
325
|
+
return plot_top + plot_height - ((value - min_value) / value_range) * plot_height
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _draw_dashed_line(
|
|
329
|
+
draw: ImageDraw.ImageDraw,
|
|
330
|
+
start: tuple[float, float],
|
|
331
|
+
end: tuple[float, float],
|
|
332
|
+
*,
|
|
333
|
+
fill: str,
|
|
334
|
+
width: int,
|
|
335
|
+
dash: int,
|
|
336
|
+
gap: int,
|
|
337
|
+
) -> None:
|
|
338
|
+
x0, y0 = start
|
|
339
|
+
x1, y1 = end
|
|
340
|
+
total = math.dist((x0, y0), (x1, y1))
|
|
341
|
+
if total <= 0:
|
|
342
|
+
return
|
|
343
|
+
dx = (x1 - x0) / total
|
|
344
|
+
dy = (y1 - y0) / total
|
|
345
|
+
progress = 0.0
|
|
346
|
+
while progress < total:
|
|
347
|
+
segment = min(progress + dash, total)
|
|
348
|
+
draw.line(
|
|
349
|
+
(
|
|
350
|
+
x0 + dx * progress,
|
|
351
|
+
y0 + dy * progress,
|
|
352
|
+
x0 + dx * segment,
|
|
353
|
+
y0 + dy * segment,
|
|
354
|
+
),
|
|
355
|
+
fill=fill,
|
|
356
|
+
width=width,
|
|
357
|
+
)
|
|
358
|
+
progress += dash + gap
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _draw_star(
|
|
362
|
+
draw: ImageDraw.ImageDraw,
|
|
363
|
+
cx: float,
|
|
364
|
+
cy: float,
|
|
365
|
+
*,
|
|
366
|
+
outer_radius: float,
|
|
367
|
+
inner_radius: float,
|
|
368
|
+
fill: str,
|
|
369
|
+
outline: str,
|
|
370
|
+
) -> None:
|
|
371
|
+
points: list[tuple[float, float]] = []
|
|
372
|
+
for index in range(10):
|
|
373
|
+
angle = -math.pi / 2 + (index * math.pi) / 5
|
|
374
|
+
radius = outer_radius if index % 2 == 0 else inner_radius
|
|
375
|
+
points.append((cx + math.cos(angle) * radius, cy + math.sin(angle) * radius))
|
|
376
|
+
draw.polygon(points, fill=fill, outline=outline)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _draw_brand_lockup(draw: ImageDraw.ImageDraw, image: Image.Image, *, width: int) -> None:
|
|
380
|
+
mark = _load_brand_mark()
|
|
381
|
+
text_right = width - 52
|
|
382
|
+
brand_font = _load_font(22, bold=True)
|
|
383
|
+
caption_font = _load_font(13, bold=False)
|
|
384
|
+
text_bbox = draw.textbbox((0, 0), "DeepScientist", font=brand_font)
|
|
385
|
+
caption_bbox = draw.textbbox((0, 0), "Autonomous Research Update", font=caption_font)
|
|
386
|
+
content_width = max(text_bbox[2] - text_bbox[0], caption_bbox[2] - caption_bbox[0])
|
|
387
|
+
mark_width = mark.width if mark is not None else 0
|
|
388
|
+
total_width = content_width + (mark_width + 12 if mark is not None else 0)
|
|
389
|
+
left = max(52, text_right - total_width)
|
|
390
|
+
if mark is not None:
|
|
391
|
+
image.alpha_composite(mark, (left, 42))
|
|
392
|
+
text_left = left + (mark_width + 12 if mark is not None else 0)
|
|
393
|
+
draw.text((text_left, 46), "DeepScientist", fill=MORANDI_TEXT, font=brand_font)
|
|
394
|
+
draw.text((text_left, 74), "Autonomous Research Update", fill=MORANDI_TEXT_MUTED, font=caption_font)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _draw_header_badges(
|
|
398
|
+
draw: ImageDraw.ImageDraw,
|
|
399
|
+
*,
|
|
400
|
+
x: int,
|
|
401
|
+
y: int,
|
|
402
|
+
labels: list[str],
|
|
403
|
+
font: ImageFont.ImageFont,
|
|
404
|
+
) -> None:
|
|
405
|
+
cursor_x = x
|
|
406
|
+
for label in labels:
|
|
407
|
+
if not label:
|
|
408
|
+
continue
|
|
409
|
+
text = str(label).strip()
|
|
410
|
+
bbox = draw.textbbox((0, 0), text, font=font)
|
|
411
|
+
width = (bbox[2] - bbox[0]) + 24
|
|
412
|
+
draw.rounded_rectangle(
|
|
413
|
+
(cursor_x, y, cursor_x + width, y + 30),
|
|
414
|
+
radius=14,
|
|
415
|
+
fill=MORANDI_CARD,
|
|
416
|
+
outline=MORANDI_BORDER,
|
|
417
|
+
width=1,
|
|
418
|
+
)
|
|
419
|
+
draw.text((cursor_x + 12, y + 6), text, fill=MORANDI_TEXT_MUTED, font=font)
|
|
420
|
+
cursor_x += width + 10
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _draw_plot_legend(
|
|
424
|
+
draw: ImageDraw.ImageDraw,
|
|
425
|
+
*,
|
|
426
|
+
x: int,
|
|
427
|
+
y: int,
|
|
428
|
+
baseline_value: float | None,
|
|
429
|
+
decimals: int | None,
|
|
430
|
+
font: ImageFont.ImageFont,
|
|
431
|
+
) -> None:
|
|
432
|
+
if baseline_value is None:
|
|
433
|
+
return
|
|
434
|
+
label = f"Baseline · {_format_metric_value(baseline_value, decimals)}"
|
|
435
|
+
bbox = draw.textbbox((0, 0), label, font=font)
|
|
436
|
+
width = (bbox[2] - bbox[0]) + 48
|
|
437
|
+
left = x - width
|
|
438
|
+
draw.rounded_rectangle(
|
|
439
|
+
(left, y, x, y + 30),
|
|
440
|
+
radius=15,
|
|
441
|
+
fill="#FFF7E9",
|
|
442
|
+
outline="#E7D2A7",
|
|
443
|
+
width=1,
|
|
444
|
+
)
|
|
445
|
+
_draw_dashed_line(draw, (left + 12, y + 15), (left + 34, y + 15), fill=MORANDI_GOLD, width=3, dash=7, gap=5)
|
|
446
|
+
draw.text((left + 42, y + 6), label, fill=MORANDI_GOLD_STROKE, font=font)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _draw_footer_summary(
|
|
450
|
+
draw: ImageDraw.ImageDraw,
|
|
451
|
+
*,
|
|
452
|
+
left: int,
|
|
453
|
+
top: int,
|
|
454
|
+
width: int,
|
|
455
|
+
label_font: ImageFont.ImageFont,
|
|
456
|
+
badge_font: ImageFont.ImageFont,
|
|
457
|
+
baseline_label: str,
|
|
458
|
+
baseline_value: object,
|
|
459
|
+
latest_value: object,
|
|
460
|
+
delta_value: object,
|
|
461
|
+
decimals: int | None,
|
|
462
|
+
direction: str,
|
|
463
|
+
beats_baseline: bool,
|
|
464
|
+
unit: str | None,
|
|
465
|
+
) -> None:
|
|
466
|
+
card_height = 54
|
|
467
|
+
gap = 14
|
|
468
|
+
value_font = _load_font(24, bold=True)
|
|
469
|
+
cards = [
|
|
470
|
+
("Latest", _format_metric_value(latest_value, decimals), MORANDI_SOFT_RED if beats_baseline else MORANDI_CARD),
|
|
471
|
+
("Baseline", _format_metric_value(baseline_value, decimals), "#FFF7E9"),
|
|
472
|
+
("Delta", _format_metric_value(delta_value, decimals) if isinstance(delta_value, (int, float)) else "—", MORANDI_SOFT_BLUE),
|
|
473
|
+
]
|
|
474
|
+
card_width = int((width - gap * 2) / 3)
|
|
475
|
+
for index, (title, value, fill) in enumerate(cards):
|
|
476
|
+
x = left + index * (card_width + gap)
|
|
477
|
+
y = top
|
|
478
|
+
draw.rounded_rectangle(
|
|
479
|
+
(x, y, x + card_width, y + card_height),
|
|
480
|
+
radius=18,
|
|
481
|
+
fill=fill,
|
|
482
|
+
outline=MORANDI_BORDER,
|
|
483
|
+
width=1,
|
|
484
|
+
)
|
|
485
|
+
draw.text((x + 16, y + 12), title, fill=MORANDI_TEXT_MUTED, font=label_font)
|
|
486
|
+
draw.text((x + 102, y + 8), value, fill=MORANDI_TEXT, font=value_font)
|
|
487
|
+
if title == "Baseline":
|
|
488
|
+
draw.text((x + 16, y + 36), _truncate_text(baseline_label, 24), fill=MORANDI_TEXT_MUTED, font=badge_font)
|
|
489
|
+
elif title == "Delta":
|
|
490
|
+
hint = "lower is better" if direction == "minimize" else "higher is better"
|
|
491
|
+
draw.text((x + 16, y + 36), hint, fill=MORANDI_TEXT_MUTED, font=badge_font)
|
|
492
|
+
elif title == "Latest":
|
|
493
|
+
latest_hint = "beats baseline" if beats_baseline else "latest recorded point"
|
|
494
|
+
if unit:
|
|
495
|
+
latest_hint = f"{latest_hint} · {unit}"
|
|
496
|
+
draw.text((x + 16, y + 36), latest_hint, fill=MORANDI_TEXT_MUTED, font=badge_font)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _draw_latest_value_callout(
|
|
500
|
+
draw: ImageDraw.ImageDraw,
|
|
501
|
+
*,
|
|
502
|
+
x: float,
|
|
503
|
+
y: float,
|
|
504
|
+
value_text: str,
|
|
505
|
+
font: ImageFont.ImageFont,
|
|
506
|
+
fill: str,
|
|
507
|
+
) -> None:
|
|
508
|
+
label = f"Latest {value_text}"
|
|
509
|
+
bbox = draw.textbbox((0, 0), label, font=font)
|
|
510
|
+
width = (bbox[2] - bbox[0]) + 24
|
|
511
|
+
height = 28
|
|
512
|
+
left = x - width / 2
|
|
513
|
+
top = y - 42
|
|
514
|
+
draw.rounded_rectangle(
|
|
515
|
+
(left, top, left + width, top + height),
|
|
516
|
+
radius=14,
|
|
517
|
+
fill=fill,
|
|
518
|
+
outline=MORANDI_BORDER,
|
|
519
|
+
width=1,
|
|
520
|
+
)
|
|
521
|
+
draw.text((left + 12, top + 5), label, fill=MORANDI_TEXT, font=font)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _truncate_text(value: str, limit: int) -> str:
|
|
525
|
+
glyphs = list(value)
|
|
526
|
+
if len(glyphs) <= limit:
|
|
527
|
+
return value
|
|
528
|
+
return "".join(glyphs[: max(0, limit - 1)]).rstrip() + "…"
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
@functools.lru_cache(maxsize=1)
|
|
532
|
+
def _load_brand_mark() -> Image.Image | None:
|
|
533
|
+
candidates = [
|
|
534
|
+
Path(__file__).resolve().parents[3] / "assets" / "branding" / "logo-raster.png",
|
|
535
|
+
Path(__file__).resolve().parents[3] / "assets" / "branding" / "deepscientist-mark.png",
|
|
536
|
+
]
|
|
537
|
+
for path in candidates:
|
|
538
|
+
if not path.exists():
|
|
539
|
+
continue
|
|
540
|
+
try:
|
|
541
|
+
image = Image.open(path).convert("RGBA")
|
|
542
|
+
alpha = image.getchannel("A")
|
|
543
|
+
bbox = alpha.getbbox()
|
|
544
|
+
if bbox:
|
|
545
|
+
image = image.crop(bbox)
|
|
546
|
+
image.thumbnail((42, 42))
|
|
547
|
+
return image
|
|
548
|
+
except Exception:
|
|
549
|
+
continue
|
|
550
|
+
return None
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _load_font(size: int, *, bold: bool) -> ImageFont.ImageFont:
|
|
554
|
+
candidates = [
|
|
555
|
+
"/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf" if bold else "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf",
|
|
556
|
+
"/usr/share/fonts/opentype/noto/NotoSans-Bold.ttf" if bold else "/usr/share/fonts/opentype/noto/NotoSans-Regular.ttf",
|
|
557
|
+
"/usr/share/fonts/truetype/liberation2/LiberationSans-Bold.ttf" if bold else "/usr/share/fonts/truetype/liberation2/LiberationSans-Regular.ttf",
|
|
558
|
+
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" if bold else "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
|
559
|
+
"/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf" if bold else "/usr/share/fonts/dejavu/DejaVuSans.ttf",
|
|
560
|
+
]
|
|
561
|
+
for path in candidates:
|
|
562
|
+
if Path(path).exists():
|
|
563
|
+
try:
|
|
564
|
+
return ImageFont.truetype(path, size=size)
|
|
565
|
+
except Exception:
|
|
566
|
+
continue
|
|
567
|
+
return ImageFont.load_default()
|
|
@@ -114,12 +114,13 @@ def build_guidance_for_record(record: dict[str, Any]) -> dict[str, Any]:
|
|
|
114
114
|
flow_type = str(record.get("flow_type") or "").strip().lower()
|
|
115
115
|
protocol_step = str(record.get("protocol_step") or "").strip().lower()
|
|
116
116
|
if flow_type == "baseline_gate" and protocol_step == "confirm":
|
|
117
|
+
next_skill = "idea" if _need_research_paper_from_record(record) else "optimize"
|
|
117
118
|
return _guidance(
|
|
118
119
|
current_anchor="baseline",
|
|
119
|
-
recommended_skill=
|
|
120
|
+
recommended_skill=next_skill,
|
|
120
121
|
recommended_action="continue",
|
|
121
|
-
summary="Baseline gate confirmed. Move into
|
|
122
|
-
why_now="The accepted baseline is now explicitly available as the downstream comparison anchor, so
|
|
122
|
+
summary="Baseline gate confirmed. Move into the next algorithmic route-selection stage relative to the accepted baseline.",
|
|
123
|
+
why_now="The accepted baseline is now explicitly available as the downstream comparison anchor, so the next leverage point is to choose and promote the strongest next direction.",
|
|
123
124
|
complete_when=[
|
|
124
125
|
"At least one candidate idea is recorded durably.",
|
|
125
126
|
"A decision artifact selects, rejects, or branches the current direction.",
|
|
@@ -165,23 +166,62 @@ def build_guidance_for_record(record: dict[str, Any]) -> dict[str, Any]:
|
|
|
165
166
|
if kind == "idea":
|
|
166
167
|
flow_type = str(record.get("flow_type") or "").strip().lower()
|
|
167
168
|
protocol_step = str(record.get("protocol_step") or "").strip().lower()
|
|
169
|
+
if flow_type == "idea_submission" and protocol_step == "candidate":
|
|
170
|
+
return _guidance(
|
|
171
|
+
current_anchor="idea",
|
|
172
|
+
recommended_skill="optimize",
|
|
173
|
+
recommended_action="continue",
|
|
174
|
+
summary="Candidate idea recorded. Compare it against the other candidate briefs before promoting a durable branch.",
|
|
175
|
+
why_now="This candidate is a lightweight optimization brief rather than a committed research line. Rank or refine the candidate pool first, then promote only the strongest directions into durable branches.",
|
|
176
|
+
complete_when=[
|
|
177
|
+
"The candidate pool is narrowed to the strongest 1 to 3 directions.",
|
|
178
|
+
"Any promoted direction is resubmitted as a durable line with `submission_mode='line'`.",
|
|
179
|
+
],
|
|
180
|
+
alternative_routes=[
|
|
181
|
+
_route("continue", "Refine candidate pool", "Several candidate briefs still overlap or lack a clear winner.", "Improves selection quality, but delays implementation."),
|
|
182
|
+
_route("launch_experiment", "Promote immediately", "This candidate is already clearly stronger than the alternatives.", "Moves faster, but risks under-explored alternatives."),
|
|
183
|
+
],
|
|
184
|
+
suggested_artifact_calls=[
|
|
185
|
+
_artifact_call(
|
|
186
|
+
"artifact.submit_idea(mode='create', submission_mode='line', source_candidate_id=..., lineage_intent='continue_line'|'branch_alternative', ...)",
|
|
187
|
+
"Promote the chosen candidate brief into a durable optimization line.",
|
|
188
|
+
),
|
|
189
|
+
_artifact_call("artifact.record(kind='decision', ...)", "Record why a candidate was promoted, deferred, or rejected."),
|
|
190
|
+
],
|
|
191
|
+
source_artifact_kind=kind,
|
|
192
|
+
source_artifact_id=artifact_id,
|
|
193
|
+
related_paths=[str(path) for path in related_paths],
|
|
194
|
+
)
|
|
168
195
|
if flow_type == "idea_submission" and protocol_step in {"create", "revise"}:
|
|
196
|
+
details = dict(record.get("details") or {}) if isinstance(record.get("details"), dict) else {}
|
|
197
|
+
next_target = _normalize_anchor(details.get("next_target") or record.get("next_target") or "experiment")
|
|
198
|
+
recommended_skill = next_target if next_target in {
|
|
199
|
+
"scout",
|
|
200
|
+
"baseline",
|
|
201
|
+
"idea",
|
|
202
|
+
"optimize",
|
|
203
|
+
"experiment",
|
|
204
|
+
"analysis-campaign",
|
|
205
|
+
"write",
|
|
206
|
+
"finalize",
|
|
207
|
+
"decision",
|
|
208
|
+
} else "experiment"
|
|
169
209
|
return _guidance(
|
|
170
210
|
current_anchor="idea",
|
|
171
|
-
recommended_skill=
|
|
172
|
-
recommended_action="launch_experiment",
|
|
173
|
-
summary="Idea branch is ready. Continue with the
|
|
174
|
-
why_now="The accepted idea already has its durable branch/worktree, so the next leverage point is
|
|
211
|
+
recommended_skill=recommended_skill,
|
|
212
|
+
recommended_action="continue" if recommended_skill == "optimize" else "launch_experiment",
|
|
213
|
+
summary="Idea branch is ready. Continue with the next active optimization stage on this durable research node.",
|
|
214
|
+
why_now="The accepted idea already has its durable branch/worktree, so the next leverage point is the configured next stage rather than another route-selection loop.",
|
|
175
215
|
complete_when=[
|
|
176
|
-
"
|
|
177
|
-
"
|
|
216
|
+
"The next configured stage starts from this branch.",
|
|
217
|
+
"The resulting evidence or decision is written durably.",
|
|
178
218
|
],
|
|
179
219
|
alternative_routes=[
|
|
180
220
|
_route("continue", "Inspect the branch once", "A quick branch sanity check is still needed before running.", "Adds caution, but should stay short."),
|
|
181
221
|
_route("launch_analysis_campaign", "Analyze first", "The idea package still has unresolved setup ambiguity that needs clarification.", "Can reduce wasted runs, but is unusual before a first main result."),
|
|
182
222
|
],
|
|
183
223
|
suggested_artifact_calls=[
|
|
184
|
-
_artifact_call("artifact.record_main_experiment(...)", "Record the first real main result on this branch."),
|
|
224
|
+
_artifact_call("artifact.record_main_experiment(...)", "Record the first real main result on this branch when the next stage is experiment-oriented."),
|
|
185
225
|
],
|
|
186
226
|
source_artifact_kind=kind,
|
|
187
227
|
source_artifact_id=artifact_id,
|