@researai/deepscientist 1.5.14 → 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/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 +6 -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/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 +6 -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 +3534 -191
- 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/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 +758 -206
- 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 +173 -5
- package/src/deepscientist/process_control.py +161 -0
- package/src/deepscientist/prompts/builder.py +267 -442
- package/src/deepscientist/quest/service.py +2255 -163
- package/src/deepscientist/quest/stage_views.py +171 -0
- 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/prompts/contracts/shared_interaction.md +13 -4
- package/src/prompts/system.md +916 -72
- 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 +2 -0
- 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 +3 -0
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-DaF9Nge_.js → AiManusChatView-DDjbFnbt.js} +12 -12
- package/src/ui/dist/assets/{AnalysisPlugin-BSVx6dXE.js → AnalysisPlugin-Yb5IdmaU.js} +1 -1
- package/src/ui/dist/assets/CliPlugin-e64sreyu.js +31037 -0
- package/src/ui/dist/assets/{CodeEditorPlugin-DU9G0Tox.js → CodeEditorPlugin-C4D2TIkU.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-DoX_fI9l.js → CodeViewerPlugin-BVoNZIvC.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-C4FWIXuU.js → DocViewerPlugin-CLChbllo.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-BgfFMgtf.js → GitDiffViewerPlugin-C4xeFyFQ.js} +20 -20
- package/src/ui/dist/assets/{ImageViewerPlugin-tcPkfY_x.js → ImageViewerPlugin-OiMUAcLi.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-_dKV60Bf.js → LabCopilotPanel-BjD2ThQF.js} +11 -11
- package/src/ui/dist/assets/{LabPlugin-Bje0ayoC.js → LabPlugin-DQPg-NrB.js} +2 -2
- package/src/ui/dist/assets/{LatexPlugin-CVsBzAln.js → LatexPlugin-CI05XAV9.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-xjmrqv_8.js → MarkdownViewerPlugin-DpeBLYZf.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-mMM2A8wP.js → MarketplacePlugin-DolE58Q2.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-3kVDSOBo.js → NotebookEditor-7Qm2rSWD.js} +11 -11
- package/src/ui/dist/assets/{NotebookEditor-SoJ8X-MO.js → NotebookEditor-C1kWaxKi.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-DElVuHl9.js → PdfLoader-BfOHw8Zw.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-Bq88XT4G.js → PdfMarkdownPlugin-BulDREv1.js} +2 -2
- package/src/ui/dist/assets/{PdfViewerPlugin-CsCXMo9S.js → PdfViewerPlugin-C-daaOaL.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-oUPvy19k.js → SearchPlugin-CjpaiJ3A.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-CRkT9yNy.js → TextViewerPlugin-BxIyqPQC.js} +5 -5
- package/src/ui/dist/assets/{VNCViewer-BgbuvWhR.js → VNCViewer-HAg9mF7M.js} +10 -10
- package/src/ui/dist/assets/{bot-v_RASACv.js → bot-0DYntytV.js} +1 -1
- package/src/ui/dist/assets/{code-5hC9d0VH.js → code-B20Slj_w.js} +1 -1
- package/src/ui/dist/assets/{file-content-D1PxfOrp.js → file-content-DT24KFma.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-DG1oT_Hj.js → file-diff-panel-DK13YPql.js} +1 -1
- package/src/ui/dist/assets/{file-socket-BmdFYQlk.js → file-socket-B4T2o4nR.js} +1 -1
- package/src/ui/dist/assets/{image-Dqe2X2tW.js → image-DSeR_sDS.js} +1 -1
- package/src/ui/dist/assets/{index-RDlNXXx1.js → index-BrFje2Uk.js} +2 -2
- package/src/ui/dist/assets/{index-DVsMKK_y.js → index-BwRJaoTl.js} +1 -1
- package/src/ui/dist/assets/{index-Nt9hS4ck.js → index-D_E4281X.js} +5007 -28514
- package/src/ui/dist/assets/{index-Duvz8Ip0.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-DIXge1CP.js → monaco-LExaAN3Y.js} +1 -1
- package/src/ui/dist/assets/{pdf-effect-queue-BBTTQaO-.js → pdf-effect-queue-BJk5okWJ.js} +1 -1
- package/src/ui/dist/assets/{popover-BWlolyxo.js → popover-D3Gg_FoV.js} +1 -1
- package/src/ui/dist/assets/{project-sync-BM5PkFH4.js → project-sync-C_ygLlVU.js} +1 -1
- package/src/ui/dist/assets/{select-D4dAtrA8.js → select-CpAK6uWm.js} +2 -2
- package/src/ui/dist/assets/{sigma-CKbE5jJT.js → sigma-DEccaSgk.js} +1 -1
- package/src/ui/dist/assets/{square-check-big-CZNGMgiB.js → square-check-big-uUfyVsbD.js} +1 -1
- package/src/ui/dist/assets/{trash-DaB37xAz.js → trash-CXvwwSe8.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-C2OmAcWe.js → useCliAccess-Bnop4mgR.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-Dowd1Ij4.js → useFileDiffOverlay-B8eUAX0I.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-BGjAhAUq.js → wrap-text-9vbOBpkW.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-dMZQMXzc.js → zoom-out-BgVMmOW4.js} +1 -1
- package/src/ui/dist/index.html +2 -2
- package/src/ui/dist/assets/CliPlugin-C9gzJX41.js +0 -5905
|
@@ -13,7 +13,7 @@ from urllib.request import Request
|
|
|
13
13
|
|
|
14
14
|
from .. import __version__ as DEEPSCIENTIST_VERSION
|
|
15
15
|
from ..network import urlopen_with_proxy as urlopen
|
|
16
|
-
from ..shared import ensure_dir, read_json, write_json
|
|
16
|
+
from ..shared import ensure_dir, read_json, utc_now, write_json
|
|
17
17
|
|
|
18
18
|
DEFAULT_WEIXIN_BASE_URL = "https://ilinkai.weixin.qq.com"
|
|
19
19
|
DEFAULT_WEIXIN_CDN_BASE_URL = "https://novac2c.cdn.weixin.qq.com/c2c"
|
|
@@ -611,6 +611,22 @@ def save_weixin_context_tokens(root: Path, items: dict[str, dict[str, Any]]) ->
|
|
|
611
611
|
write_json(weixin_context_tokens_path(root), {"tokens": items})
|
|
612
612
|
|
|
613
613
|
|
|
614
|
+
def _weixin_replay_cursor(value: Any) -> int:
|
|
615
|
+
try:
|
|
616
|
+
return max(0, int(value or 0))
|
|
617
|
+
except (TypeError, ValueError):
|
|
618
|
+
return 0
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def get_weixin_context_entry(root: Path, user_id: str) -> dict[str, Any]:
|
|
622
|
+
normalized_user_id = str(user_id or "").strip()
|
|
623
|
+
if not normalized_user_id:
|
|
624
|
+
return {}
|
|
625
|
+
items = load_weixin_context_tokens(root)
|
|
626
|
+
current = items.get(normalized_user_id)
|
|
627
|
+
return dict(current) if isinstance(current, dict) else {}
|
|
628
|
+
|
|
629
|
+
|
|
614
630
|
def remember_weixin_context_token(
|
|
615
631
|
root: Path,
|
|
616
632
|
*,
|
|
@@ -635,6 +651,16 @@ def remember_weixin_context_token(
|
|
|
635
651
|
"conversation_id": str(conversation_id or current.get("conversation_id") or "").strip() or None,
|
|
636
652
|
"message_id": str(message_id or current.get("message_id") or "").strip() or None,
|
|
637
653
|
"updated_at": str(updated_at or current.get("updated_at") or "").strip() or None,
|
|
654
|
+
"stale_context": False,
|
|
655
|
+
"stale_since": None,
|
|
656
|
+
"last_ret_minus_2_at": None,
|
|
657
|
+
"last_outbound_error": None,
|
|
658
|
+
"last_outbound_kind": None,
|
|
659
|
+
"queued_replay_cursor": _weixin_replay_cursor(current.get("queued_replay_cursor")),
|
|
660
|
+
"last_replay_at": str(current.get("last_replay_at") or "").strip() or None,
|
|
661
|
+
"last_replay_trigger_message_id": str(current.get("last_replay_trigger_message_id") or "").strip() or None,
|
|
662
|
+
"last_replayed_count": _weixin_replay_cursor(current.get("last_replayed_count")) or None,
|
|
663
|
+
"last_replay_dropped_count": _weixin_replay_cursor(current.get("last_replay_dropped_count")) or None,
|
|
638
664
|
}
|
|
639
665
|
save_weixin_context_tokens(root, items)
|
|
640
666
|
|
|
@@ -648,6 +674,101 @@ def get_weixin_context_token(root: Path, user_id: str) -> str | None:
|
|
|
648
674
|
return token or None
|
|
649
675
|
|
|
650
676
|
|
|
677
|
+
def get_weixin_replay_cursor(root: Path, user_id: str) -> int:
|
|
678
|
+
entry = get_weixin_context_entry(root, user_id)
|
|
679
|
+
return _weixin_replay_cursor(entry.get("queued_replay_cursor"))
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def update_weixin_replay_cursor(
|
|
683
|
+
root: Path,
|
|
684
|
+
*,
|
|
685
|
+
user_id: str,
|
|
686
|
+
queued_replay_cursor: int,
|
|
687
|
+
last_replay_at: str | None = None,
|
|
688
|
+
last_replay_trigger_message_id: str | None = None,
|
|
689
|
+
last_replayed_count: int | None = None,
|
|
690
|
+
last_replay_dropped_count: int | None = None,
|
|
691
|
+
) -> dict[str, Any]:
|
|
692
|
+
normalized_user_id = str(user_id or "").strip()
|
|
693
|
+
if not normalized_user_id:
|
|
694
|
+
return {}
|
|
695
|
+
items = load_weixin_context_tokens(root)
|
|
696
|
+
current = dict(items.get(normalized_user_id) or {})
|
|
697
|
+
current.update(
|
|
698
|
+
{
|
|
699
|
+
"user_id": normalized_user_id,
|
|
700
|
+
"queued_replay_cursor": _weixin_replay_cursor(queued_replay_cursor),
|
|
701
|
+
"last_replay_at": str(last_replay_at or utc_now()).strip() or utc_now(),
|
|
702
|
+
"last_replay_trigger_message_id": str(last_replay_trigger_message_id or "").strip() or None,
|
|
703
|
+
"last_replayed_count": _weixin_replay_cursor(last_replayed_count) if last_replayed_count is not None else None,
|
|
704
|
+
"last_replay_dropped_count": _weixin_replay_cursor(last_replay_dropped_count)
|
|
705
|
+
if last_replay_dropped_count is not None
|
|
706
|
+
else None,
|
|
707
|
+
}
|
|
708
|
+
)
|
|
709
|
+
items[normalized_user_id] = current
|
|
710
|
+
save_weixin_context_tokens(root, items)
|
|
711
|
+
return current
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def mark_weixin_context_stale(
|
|
715
|
+
root: Path,
|
|
716
|
+
*,
|
|
717
|
+
user_id: str,
|
|
718
|
+
error: str,
|
|
719
|
+
kind: str | None = None,
|
|
720
|
+
updated_at: str | None = None,
|
|
721
|
+
) -> dict[str, Any]:
|
|
722
|
+
normalized_user_id = str(user_id or "").strip()
|
|
723
|
+
if not normalized_user_id:
|
|
724
|
+
return {}
|
|
725
|
+
timestamp = str(updated_at or utc_now()).strip() or utc_now()
|
|
726
|
+
items = load_weixin_context_tokens(root)
|
|
727
|
+
current = dict(items.get(normalized_user_id) or {})
|
|
728
|
+
current.update(
|
|
729
|
+
{
|
|
730
|
+
"user_id": normalized_user_id,
|
|
731
|
+
"stale_context": True,
|
|
732
|
+
"stale_since": str(current.get("stale_since") or "").strip() or timestamp,
|
|
733
|
+
"last_ret_minus_2_at": timestamp,
|
|
734
|
+
"last_outbound_error": str(error or "").strip() or None,
|
|
735
|
+
"last_outbound_kind": str(kind or "").strip() or None,
|
|
736
|
+
}
|
|
737
|
+
)
|
|
738
|
+
items[normalized_user_id] = current
|
|
739
|
+
save_weixin_context_tokens(root, items)
|
|
740
|
+
return current
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def clear_weixin_context_send_state(
|
|
744
|
+
root: Path,
|
|
745
|
+
*,
|
|
746
|
+
user_id: str,
|
|
747
|
+
kind: str | None = None,
|
|
748
|
+
updated_at: str | None = None,
|
|
749
|
+
) -> dict[str, Any]:
|
|
750
|
+
normalized_user_id = str(user_id or "").strip()
|
|
751
|
+
if not normalized_user_id:
|
|
752
|
+
return {}
|
|
753
|
+
timestamp = str(updated_at or utc_now()).strip() or utc_now()
|
|
754
|
+
items = load_weixin_context_tokens(root)
|
|
755
|
+
current = dict(items.get(normalized_user_id) or {})
|
|
756
|
+
current.update(
|
|
757
|
+
{
|
|
758
|
+
"user_id": normalized_user_id,
|
|
759
|
+
"stale_context": False,
|
|
760
|
+
"stale_since": None,
|
|
761
|
+
"last_ret_minus_2_at": None,
|
|
762
|
+
"last_outbound_error": None,
|
|
763
|
+
"last_outbound_kind": str(kind or "").strip() or None,
|
|
764
|
+
"last_success_at": timestamp,
|
|
765
|
+
}
|
|
766
|
+
)
|
|
767
|
+
items[normalized_user_id] = current
|
|
768
|
+
save_weixin_context_tokens(root, items)
|
|
769
|
+
return current
|
|
770
|
+
|
|
771
|
+
|
|
651
772
|
def weixin_sync_state_path(root: Path) -> Path:
|
|
652
773
|
return ensure_dir(root) / "sync_state.json"
|
|
653
774
|
|
|
@@ -11,7 +11,7 @@ from urllib.parse import parse_qs, unquote
|
|
|
11
11
|
from ...acp import OptionalACPBridge, build_session_descriptor, build_session_update, get_acp_bridge_status
|
|
12
12
|
from ...bash_exec.service import DEFAULT_TERMINAL_SESSION_ID
|
|
13
13
|
from ... import __version__ as DEEPSCIENTIST_VERSION
|
|
14
|
-
from ...gitops import commit_detail, compare_refs, diff_file_between_refs, diff_file_for_commit, export_git_graph,
|
|
14
|
+
from ...gitops import commit_detail, compare_refs, diff_file_between_refs, diff_file_for_commit, export_git_graph, log_ref_history
|
|
15
15
|
from ...memory import MemoryService
|
|
16
16
|
from ...quest import QuestService
|
|
17
17
|
from ...shared import generate_id, read_json, read_text, resolve_within, run_command, sha256_text, utc_now
|
|
@@ -474,6 +474,11 @@ npm --prefix src/ui run build</pre>
|
|
|
474
474
|
|
|
475
475
|
def quest_session(self, quest_id: str) -> dict:
|
|
476
476
|
snapshot = self.app.quest_service.snapshot_fast(quest_id)
|
|
477
|
+
for kind in ("details", "canvas"):
|
|
478
|
+
try:
|
|
479
|
+
self.app.quest_service.prime_projection(quest_id, kind)
|
|
480
|
+
except Exception:
|
|
481
|
+
continue
|
|
477
482
|
return {
|
|
478
483
|
"ok": True,
|
|
479
484
|
"quest_id": quest_id,
|
|
@@ -768,7 +773,20 @@ npm --prefix src/ui run build</pre>
|
|
|
768
773
|
return self.app.control_quest(quest_id, action=action, source=source)
|
|
769
774
|
|
|
770
775
|
def workflow(self, quest_id: str) -> dict:
|
|
771
|
-
|
|
776
|
+
payload = self.app.quest_service.workflow(quest_id)
|
|
777
|
+
projection_state = str(((payload or {}).get("projection_status") or {}).get("state") or "").strip().lower()
|
|
778
|
+
if projection_state and projection_state != "ready":
|
|
779
|
+
if isinstance(payload, dict):
|
|
780
|
+
payload["optimization_frontier"] = None
|
|
781
|
+
return payload
|
|
782
|
+
quest_root = self._fresh_quest_service()._quest_root(quest_id)
|
|
783
|
+
try:
|
|
784
|
+
frontier = self.app.artifact_service.get_optimization_frontier(quest_root)
|
|
785
|
+
except Exception:
|
|
786
|
+
frontier = {"ok": False}
|
|
787
|
+
if isinstance(payload, dict):
|
|
788
|
+
payload["optimization_frontier"] = frontier.get("optimization_frontier") if isinstance(frontier, dict) else None
|
|
789
|
+
return payload
|
|
772
790
|
|
|
773
791
|
def quest_layout(self, quest_id: str) -> dict:
|
|
774
792
|
quest_root = self._fresh_quest_service()._quest_root(quest_id)
|
|
@@ -816,15 +834,21 @@ npm --prefix src/ui run build</pre>
|
|
|
816
834
|
def metrics_timeline(self, quest_id: str) -> dict:
|
|
817
835
|
return self.app.quest_service.metrics_timeline(quest_id)
|
|
818
836
|
|
|
837
|
+
def baseline_compare(self, quest_id: str) -> dict:
|
|
838
|
+
return self.app.quest_service.baseline_compare(quest_id)
|
|
839
|
+
|
|
819
840
|
def git_branches(self, quest_id: str) -> dict:
|
|
820
841
|
quest_root = self._fresh_quest_service()._quest_root(quest_id)
|
|
821
|
-
payload =
|
|
842
|
+
payload = self.app.quest_service.git_branch_canvas(quest_id)
|
|
822
843
|
research_state = self.app.quest_service.read_research_state(quest_root)
|
|
823
844
|
active_workspace_branch = str(research_state.get("current_workspace_branch") or "").strip() or None
|
|
824
845
|
research_head_branch = str(research_state.get("research_head_branch") or "").strip() or None
|
|
825
846
|
payload["active_workspace_ref"] = active_workspace_branch
|
|
826
847
|
payload["research_head_ref"] = research_head_branch
|
|
827
848
|
payload["workspace_mode"] = str(research_state.get("workspace_mode") or "quest").strip() or "quest"
|
|
849
|
+
projection_state = str(((payload or {}).get("projection_status") or {}).get("state") or "").strip().lower()
|
|
850
|
+
if projection_state and projection_state != "ready" and not (payload.get("nodes") or []):
|
|
851
|
+
return payload
|
|
828
852
|
quest_data = self.app.quest_service.read_quest_yaml(quest_root)
|
|
829
853
|
active_anchor = str(quest_data.get("active_anchor") or "").strip().lower()
|
|
830
854
|
active_analysis_campaign_id = str(research_state.get("active_analysis_campaign_id") or "").strip() or None
|
|
@@ -837,11 +861,40 @@ npm --prefix src/ui run build</pre>
|
|
|
837
861
|
branch_summary = self.app.artifact_service.list_research_branches(quest_root)
|
|
838
862
|
except Exception:
|
|
839
863
|
branch_summary = {"branches": []}
|
|
864
|
+
try:
|
|
865
|
+
optimization_frontier = self.app.artifact_service.get_optimization_frontier(quest_root)
|
|
866
|
+
except Exception:
|
|
867
|
+
optimization_frontier = {"ok": False}
|
|
840
868
|
branch_summary_by_name = {
|
|
841
869
|
str(item.get("branch_name") or "").strip(): item
|
|
842
870
|
for item in (branch_summary.get("branches") or [])
|
|
843
871
|
if str(item.get("branch_name") or "").strip()
|
|
844
872
|
}
|
|
873
|
+
frontier_payload = (
|
|
874
|
+
dict(optimization_frontier.get("optimization_frontier") or {})
|
|
875
|
+
if isinstance(optimization_frontier, dict)
|
|
876
|
+
and isinstance(optimization_frontier.get("optimization_frontier"), dict)
|
|
877
|
+
else {}
|
|
878
|
+
)
|
|
879
|
+
best_branch_name = str(((frontier_payload.get("best_branch") or {}) if isinstance(frontier_payload.get("best_branch"), dict) else {}).get("branch_name") or "").strip() or None
|
|
880
|
+
stagnant_branch_names = {
|
|
881
|
+
str(item.get("branch_name") or "").strip()
|
|
882
|
+
for item in (frontier_payload.get("stagnant_branches") or [])
|
|
883
|
+
if isinstance(item, dict) and str(item.get("branch_name") or "").strip()
|
|
884
|
+
}
|
|
885
|
+
fusion_candidate_names = {
|
|
886
|
+
str(item.get("branch_name") or "").strip()
|
|
887
|
+
for item in (frontier_payload.get("fusion_candidates") or [])
|
|
888
|
+
if isinstance(item, dict) and str(item.get("branch_name") or "").strip()
|
|
889
|
+
}
|
|
890
|
+
candidate_count_by_branch: dict[str, int] = {}
|
|
891
|
+
for item in frontier_payload.get("implementation_candidates") or []:
|
|
892
|
+
if not isinstance(item, dict):
|
|
893
|
+
continue
|
|
894
|
+
branch_name = str(item.get("branch") or "").strip()
|
|
895
|
+
if not branch_name:
|
|
896
|
+
continue
|
|
897
|
+
candidate_count_by_branch[branch_name] = candidate_count_by_branch.get(branch_name, 0) + 1
|
|
845
898
|
active_campaign = {}
|
|
846
899
|
if active_analysis_campaign_id:
|
|
847
900
|
try:
|
|
@@ -856,6 +909,11 @@ npm --prefix src/ui run build</pre>
|
|
|
856
909
|
if isinstance(active_campaign, dict)
|
|
857
910
|
else None
|
|
858
911
|
)
|
|
912
|
+
campaign_paper_line_branch = (
|
|
913
|
+
str(active_campaign.get("paper_line_branch") or "").strip() or None
|
|
914
|
+
if isinstance(active_campaign, dict)
|
|
915
|
+
else None
|
|
916
|
+
)
|
|
859
917
|
campaign_slices = [
|
|
860
918
|
dict(item)
|
|
861
919
|
for item in ((active_campaign or {}).get("slices") or [])
|
|
@@ -903,6 +961,14 @@ npm --prefix src/ui run build</pre>
|
|
|
903
961
|
workflow_state["status_reason"] = "Analysis slice pending."
|
|
904
962
|
return workflow_state
|
|
905
963
|
if branch_kind == "paper":
|
|
964
|
+
if campaign_paper_line_branch and ref == campaign_paper_line_branch and next_pending_slice_id is not None:
|
|
965
|
+
workflow_state["analysis_state"] = "active"
|
|
966
|
+
workflow_state["writing_state"] = "blocked_by_analysis"
|
|
967
|
+
workflow_state["status_reason"] = (
|
|
968
|
+
f"Analysis {campaign_completed_slices}/{campaign_total_slices} done"
|
|
969
|
+
+ (f" · next: {next_pending_slice_id}" if next_pending_slice_id else "")
|
|
970
|
+
)
|
|
971
|
+
return workflow_state
|
|
906
972
|
if ref == current_workspace_branch and workspace_mode == "paper":
|
|
907
973
|
workflow_state["writing_state"] = "completed" if active_anchor == "finalize" else "active"
|
|
908
974
|
workflow_state["status_reason"] = (
|
|
@@ -912,7 +978,7 @@ npm --prefix src/ui run build</pre>
|
|
|
912
978
|
workflow_state["writing_state"] = "ready"
|
|
913
979
|
workflow_state["status_reason"] = "Writing workspace prepared."
|
|
914
980
|
return workflow_state
|
|
915
|
-
if campaign_parent_branch and ref == campaign_parent_branch:
|
|
981
|
+
if campaign_parent_branch and not campaign_paper_line_branch and ref == campaign_parent_branch:
|
|
916
982
|
workflow_state["analysis_state"] = "completed" if next_pending_slice_id is None else "active"
|
|
917
983
|
if has_main_result:
|
|
918
984
|
workflow_state["writing_state"] = "ready" if next_pending_slice_id is None else "blocked_by_analysis"
|
|
@@ -955,6 +1021,11 @@ npm --prefix src/ui run build</pre>
|
|
|
955
1021
|
node["latest_main_experiment"] = summary.get("latest_main_experiment")
|
|
956
1022
|
node["experiment_count"] = summary.get("experiment_count")
|
|
957
1023
|
node["has_main_result"] = summary.get("has_main_result")
|
|
1024
|
+
node["optimization_mode"] = frontier_payload.get("mode")
|
|
1025
|
+
node["optimization_best"] = ref == best_branch_name
|
|
1026
|
+
node["optimization_stagnant"] = ref in stagnant_branch_names
|
|
1027
|
+
node["optimization_fusion_candidate"] = ref in fusion_candidate_names
|
|
1028
|
+
node["optimization_candidate_count"] = candidate_count_by_branch.get(ref, 0)
|
|
958
1029
|
return payload
|
|
959
1030
|
|
|
960
1031
|
def git_log(self, quest_id: str, path: str) -> dict:
|
|
@@ -61,6 +61,7 @@ ROUTES: list[tuple[str, re.Pattern[str], str]] = [
|
|
|
61
61
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/graph$"), "graph"),
|
|
62
62
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/graph/(?P<kind>svg|png|json)$"), "graph_asset"),
|
|
63
63
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/metrics/timeline$"), "metrics_timeline"),
|
|
64
|
+
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/baselines/compare$"), "baseline_compare"),
|
|
64
65
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/branches$"), "git_branches"),
|
|
65
66
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/log$"), "git_log"),
|
|
66
67
|
("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/compare$"), "git_compare"),
|