clearai-dsh 0.1.0
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/CHANGELOG.md +26 -0
- package/LICENSE +201 -0
- package/README.md +138 -0
- package/README.zh-CN.md +138 -0
- package/bin/clearai.mjs +224 -0
- package/brand/README.md +41 -0
- package/brand/logo-512-dark.png +0 -0
- package/brand/logo-512.png +0 -0
- package/brand/logo-lockup-dark.png +0 -0
- package/brand/logo-lockup.png +0 -0
- package/brand/logo-lockup.svg +12 -0
- package/brand/logo-wordmark.svg +6 -0
- package/brand/logo.svg +19 -0
- package/cordis.patch.yml +39 -0
- package/lib/client.js +3071 -0
- package/lib/fold.js +1576 -0
- package/lib/host.js +605 -0
- package/package.json +65 -0
- package/presets/clearai/agent.cordis.yml +226 -0
- package/presets/clearai/plugins/brain.js +547 -0
- package/presets/clearai/plugins/clearai-kernel.js +5485 -0
- package/presets/clearai/plugins/ontology.js +306 -0
- package/presets/clearai/plugins/prompts.js +312 -0
- package/presets/clearai/preset.yml +5 -0
- package/presets/clearai/skills/clearai-loop/SKILL.md +89 -0
- package/presets/clearai/template/knowledge/README.md +25 -0
- package/presets/clearai/template/memory/README.md +34 -0
- package/presets/clearai/template/project.md +49 -0
- package/presets/clearai/template/skills/README.md +37 -0
- package/presets/clearai/template/skills/chart-diagram-qa/SKILL.md +43 -0
- package/presets/clearai/template/skills/citation-management/SKILL.md +73 -0
- package/presets/clearai/template/skills/citation-management/references/bibtex_formatting.md +908 -0
- package/presets/clearai/template/skills/citation-management/references/citation_validation.md +794 -0
- package/presets/clearai/template/skills/citation-management/references/google_scholar_search.md +725 -0
- package/presets/clearai/template/skills/citation-management/references/metadata_extraction.md +870 -0
- package/presets/clearai/template/skills/citation-management/references/pubmed_search.md +839 -0
- package/presets/clearai/template/skills/citation-management/scripts/doi_to_bibtex.py +204 -0
- package/presets/clearai/template/skills/citation-management/scripts/extract_metadata.py +569 -0
- package/presets/clearai/template/skills/citation-management/scripts/format_bibtex.py +349 -0
- package/presets/clearai/template/skills/citation-management/scripts/generate_schematic.py +139 -0
- package/presets/clearai/template/skills/citation-management/scripts/generate_schematic_ai.py +817 -0
- package/presets/clearai/template/skills/citation-management/scripts/search_google_scholar.py +282 -0
- package/presets/clearai/template/skills/citation-management/scripts/search_pubmed.py +398 -0
- package/presets/clearai/template/skills/citation-management/scripts/validate_citations.py +497 -0
- package/presets/clearai/template/skills/data-analysis/SKILL.md +92 -0
- package/presets/clearai/template/skills/data-analysis/checklists/readiness_check.md +23 -0
- package/presets/clearai/template/skills/data-analysis/templates/analysis_report.md.tpl +63 -0
- package/presets/clearai/template/skills/data-analysis/templates/cleaning_rules_draft.yaml.tpl +32 -0
- package/presets/clearai/template/skills/data-analysis/templates/data_dictionary.md.tpl +12 -0
- package/presets/clearai/template/skills/data-analysis/templates/domain_knowledge_template.md.tpl +316 -0
- package/presets/clearai/template/skills/data-analysis/templates/feature_candidates.json.tpl +20 -0
- package/presets/clearai/template/skills/data-analysis/templates/quality_scorecard.md.tpl +30 -0
- package/presets/clearai/template/skills/data-analysis/workflows/01-data-profiling.md +42 -0
- package/presets/clearai/template/skills/data-analysis/workflows/02-quality-audit.md +36 -0
- package/presets/clearai/template/skills/data-analysis/workflows/03-physical-correlation.md +25 -0
- package/presets/clearai/template/skills/data-analysis/workflows/04-unstructured-mining.md +26 -0
- package/presets/clearai/template/skills/data-qa-analysis/SKILL.md +102 -0
- package/presets/clearai/template/skills/data-qa-analysis/checklists/readiness_check.md +62 -0
- package/presets/clearai/template/skills/data-qa-analysis/templates/best_in_class_report.md.tpl +56 -0
- package/presets/clearai/template/skills/data-qa-analysis/templates/cleaning_rules_draft.yaml.tpl +56 -0
- package/presets/clearai/template/skills/data-qa-analysis/templates/data_dictionary.md.tpl +13 -0
- package/presets/clearai/template/skills/data-qa-analysis/templates/data_source_inventory_and_lineage.md.tpl +146 -0
- package/presets/clearai/template/skills/data-qa-analysis/templates/data_status_report.md.tpl +60 -0
- package/presets/clearai/template/skills/data-qa-analysis/templates/steady_state_rules.yaml.tpl +41 -0
- package/presets/clearai/template/skills/data-qa-analysis/templates/subsystem_registry.md.tpl +101 -0
- package/presets/clearai/template/skills/data-qa-analysis/templates/unified_execution_plan.md.tpl +100 -0
- package/presets/clearai/template/skills/data-qa-analysis/workflows/01-data-source-inventory-and-lineage.md +194 -0
- package/presets/clearai/template/skills/data-qa-analysis/workflows/02-data-alignment-and-tag-semantics.md +122 -0
- package/presets/clearai/template/skills/data-qa-analysis/workflows/03-steady-state-identification.md +126 -0
- package/presets/clearai/template/skills/data-qa-analysis/workflows/04-consumption-analysis.md +152 -0
- package/presets/clearai/template/skills/data-qa-analysis/workflows/05-best-in-class-and-optimization-space.md +78 -0
- package/presets/clearai/template/skills/domain-presearch/SKILL.md +131 -0
- package/presets/clearai/template/skills/domain-presearch/checklists/domain_checklist.md +24 -0
- package/presets/clearai/template/skills/domain-presearch/references/figure_code.md +78 -0
- package/presets/clearai/template/skills/domain-presearch/references/strategic_frameworks.md +38 -0
- package/presets/clearai/template/skills/exploration-loop/SKILL.md +81 -0
- package/presets/clearai/template/skills/exploratory-data-analysis/SKILL.md +77 -0
- package/presets/clearai/template/skills/exploratory-data-analysis/references/bioinformatics_genomics_formats.md +664 -0
- package/presets/clearai/template/skills/exploratory-data-analysis/references/chemistry_molecular_formats.md +664 -0
- package/presets/clearai/template/skills/exploratory-data-analysis/references/general_scientific_formats.md +518 -0
- package/presets/clearai/template/skills/exploratory-data-analysis/references/microscopy_imaging_formats.md +620 -0
- package/presets/clearai/template/skills/exploratory-data-analysis/references/proteomics_metabolomics_formats.md +517 -0
- package/presets/clearai/template/skills/exploratory-data-analysis/references/spectroscopy_analytical_formats.md +633 -0
- package/presets/clearai/template/skills/exploratory-data-analysis/scripts/eda_analyzer.py +547 -0
- package/presets/clearai/template/skills/hypothesis-generation/SKILL.md +73 -0
- package/presets/clearai/template/skills/hypothesis-generation/references/experimental_design_patterns.md +329 -0
- package/presets/clearai/template/skills/hypothesis-generation/references/hypothesis_quality_criteria.md +198 -0
- package/presets/clearai/template/skills/hypothesis-generation/references/literature_search_strategies.md +622 -0
- package/presets/clearai/template/skills/hypothesis-generation/scripts/generate_schematic.py +139 -0
- package/presets/clearai/template/skills/hypothesis-generation/scripts/generate_schematic_ai.py +817 -0
- package/presets/clearai/template/skills/literature-review/SKILL.md +72 -0
- package/presets/clearai/template/skills/literature-review/references/citation_styles.md +166 -0
- package/presets/clearai/template/skills/literature-review/references/database_strategies.md +455 -0
- package/presets/clearai/template/skills/literature-review/scripts/generate_pdf.py +176 -0
- package/presets/clearai/template/skills/literature-review/scripts/generate_schematic.py +139 -0
- package/presets/clearai/template/skills/literature-review/scripts/generate_schematic_ai.py +817 -0
- package/presets/clearai/template/skills/literature-review/scripts/search_databases.py +303 -0
- package/presets/clearai/template/skills/literature-review/scripts/verify_citations.py +221 -0
- package/presets/clearai/template/skills/paper-lookup/SKILL.md +59 -0
- package/presets/clearai/template/skills/paper-lookup/references/arxiv.md +161 -0
- package/presets/clearai/template/skills/paper-lookup/references/biorxiv.md +118 -0
- package/presets/clearai/template/skills/paper-lookup/references/core.md +150 -0
- package/presets/clearai/template/skills/paper-lookup/references/crossref.md +181 -0
- package/presets/clearai/template/skills/paper-lookup/references/medrxiv.md +104 -0
- package/presets/clearai/template/skills/paper-lookup/references/openalex.md +174 -0
- package/presets/clearai/template/skills/paper-lookup/references/pmc.md +152 -0
- package/presets/clearai/template/skills/paper-lookup/references/pubmed.md +124 -0
- package/presets/clearai/template/skills/paper-lookup/references/semantic-scholar.md +203 -0
- package/presets/clearai/template/skills/paper-lookup/references/unpaywall.md +127 -0
- package/presets/clearai/template/skills/process-presearch/SKILL.md +196 -0
- package/presets/clearai/template/skills/process-presearch/checklists/process_checklist.md +18 -0
- package/presets/clearai/template/skills/process-presearch/references/figure_code.md +107 -0
- package/presets/clearai/template/skills/process-presearch/references/source_attribution_example.md +22 -0
- package/presets/clearai/template/skills/process-understanding-extraction/SKILL.md +69 -0
- package/presets/clearai/template/skills/process-understanding-extraction/checklists/readiness_check.md +34 -0
- package/presets/clearai/template/skills/process-understanding-extraction/templates/docx_raw_dump_extractor.py.tpl +132 -0
- package/presets/clearai/template/skills/process-understanding-extraction/templates/entity_map_unit_topology.json.tpl +86 -0
- package/presets/clearai/template/skills/process-understanding-extraction/templates/process_brief.md.tpl +89 -0
- package/presets/clearai/template/skills/process-understanding-extraction/templates/process_brief_builder_from_raw_dump.py.tpl +203 -0
- package/presets/clearai/template/skills/process-understanding-extraction/templates/process_flow_mermaid.md.tpl +41 -0
- package/presets/clearai/template/skills/process-understanding-extraction/templates/unified_execution_plan.md.tpl +53 -0
- package/presets/clearai/template/skills/process-understanding-extraction/workflows/01-process-doc-discovery.md +173 -0
- package/presets/clearai/template/skills/process-understanding-extraction/workflows/02-process-understanding-and-diagramming.md +106 -0
- package/presets/clearai/template/skills/scientific-brainstorming/SKILL.md +64 -0
- package/presets/clearai/template/skills/scientific-brainstorming/references/brainstorming_methods.md +326 -0
- package/presets/clearai/template/skills/scientific-critical-thinking/SKILL.md +72 -0
- package/presets/clearai/template/skills/scientific-critical-thinking/references/common_biases.md +364 -0
- package/presets/clearai/template/skills/scientific-critical-thinking/references/evidence_hierarchy.md +485 -0
- package/presets/clearai/template/skills/scientific-critical-thinking/references/experimental_design.md +496 -0
- package/presets/clearai/template/skills/scientific-critical-thinking/references/logical_fallacies.md +478 -0
- package/presets/clearai/template/skills/scientific-critical-thinking/references/scientific_method.md +169 -0
- package/presets/clearai/template/skills/scientific-critical-thinking/references/statistical_pitfalls.md +506 -0
- package/presets/clearai/template/skills/skill-creator/SKILL.md +109 -0
- package/presets/clearai/template/skills/skill-creator/references/authoring-guide.md +89 -0
- package/presets/clearai/template/skills/statistical-analysis/SKILL.md +79 -0
- package/presets/clearai/template/skills/statistical-analysis/references/assumptions_and_diagnostics.md +369 -0
- package/presets/clearai/template/skills/statistical-analysis/references/bayesian_statistics.md +653 -0
- package/presets/clearai/template/skills/statistical-analysis/references/effect_sizes_and_power.md +578 -0
- package/presets/clearai/template/skills/statistical-analysis/references/reporting_standards.md +469 -0
- package/presets/clearai/template/skills/statistical-analysis/references/test_selection_guide.md +129 -0
- package/presets/clearai/template/skills/statistical-analysis/scripts/assumption_checks.py +538 -0
- package/presets/clearai/template/skills/web-artifact/SKILL.md +165 -0
- package/presets/clearai/template/skills/web-artifact/assets/renderer/renderer.css +229 -0
- package/presets/clearai/template/skills/web-artifact/assets/renderer/renderer.js +373 -0
- package/presets/clearai/template/skills/web-artifact/assets/vendor/elkjs/LICENSE +263 -0
- package/presets/clearai/template/skills/web-artifact/assets/vendor/elkjs/UPSTREAM.md +26 -0
- package/presets/clearai/template/skills/web-artifact/assets/vendor/elkjs/elk.bundled.js +6605 -0
- package/presets/clearai/template/skills/web-artifact/references/when-drawing-a-topology.md +150 -0
- package/presets/clearai/template/skills/web-artifact/references/when-the-page-must-work-offline.md +62 -0
- package/presets/clearai/template/skills/web-artifact/scripts/check_artifact.py +167 -0
- package/presets/clearai/template/skills/web-artifact/scripts/render_topology.js +272 -0
- package/presets/clearai/template/skills/what-if-oracle/LICENSE.txt +5 -0
- package/presets/clearai/template/skills/what-if-oracle/SKILL.md +72 -0
- package/presets/clearai/template/skills/what-if-oracle/references/scenario-templates.md +154 -0
package/lib/fold.js
ADDED
|
@@ -0,0 +1,1576 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClearAI 的状态机(宿主半)。
|
|
3
|
+
*
|
|
4
|
+
* 这是 ClearAI 认识论的**事实侧**:状态不是被存下来的,而是**会话日志的投影**——
|
|
5
|
+
* 与 DSH 原生一致(`sessionProjections` 的纯同步 fold + 水印缓存 + fork 感知),
|
|
6
|
+
* 也与 ClearAI 自己的原则一致:「系统的状态永远可以被重新计算,因此永远不会与事实不一致。
|
|
7
|
+
* 凡是被存储的状态,都是潜在的谎言。」
|
|
8
|
+
*
|
|
9
|
+
* 两条不变量:
|
|
10
|
+
* 1. **本文件只解释事实,不做判断。** 它吃的全是工具已经算完并记进日志的结论
|
|
11
|
+
* (`tool/result.meta.mutation`)——准入过没过、评估者裁了什么、算术选了谁。
|
|
12
|
+
* 判断(读盘核产物、派评估者、算术排序)在预设侧的工具里,因为它们需要 I/O 与异步。
|
|
13
|
+
* 2. **纯函数。** fold 不许有 I/O、不许读时钟、不许调服务。状态必须是纯 JSON
|
|
14
|
+
* (投影要求,也是它可以被缓存与重放的前提)。
|
|
15
|
+
*
|
|
16
|
+
* 与预设内核的契约:`meta = { kind: 'clearai', v: 1, mutation: { t, ... } }`。
|
|
17
|
+
* 词汇表由本文件的 `applyMutation` 定义;预设侧只负责产出,不负责解释。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** 世界线分支状态的秩:`_BRANCH_RANK`。秩只增;一旦出现 adopted,整个分叉的分支状态冻结。 */
|
|
21
|
+
const BRANCH_RANK = { exploring: 0, evaluated: 1, adopted: 2, pruned: 2 }
|
|
22
|
+
|
|
23
|
+
export const MUTATION_KIND = 'clearai'
|
|
24
|
+
/**
|
|
25
|
+
* 状态版本:形状一变就 +1。
|
|
26
|
+
* v2 → v3:多了「合并技能目录」与「本会话用了几次技能」两块(技能面)。
|
|
27
|
+
* v3 → v4:多了「运行档」(人在场 / 无人值守)这块。
|
|
28
|
+
* (§34 更正:它**不是**"可被人切换的事实"而是部署预设的初值——面板那个开关已删。)
|
|
29
|
+
* v4 → v5:多了「项目章程」的读数(占位还有几条 / §1 当前阶段填没填 / 最后改动)。
|
|
30
|
+
* v5 → v6:多了「续跑窗口」的账(`continuation`):我们**对宿主说过的话**——布防、暂停、收兵、
|
|
31
|
+
* 以及「它不在了,而那不是我们干的」。它以前记在内核的进程内存里,重启或分叉就说不清了。
|
|
32
|
+
* v6 → v7:那条账多一个 `label`:我们最后一次在平台对象上写下的那句身份文本。
|
|
33
|
+
* 它的用途只有一个:**分清「身份变了」与「人改写过了」**(§17.6)——前者我们换窗口,
|
|
34
|
+
* 后者以人为准、一个字都不动。没有它,这两种情形在外部长得一模一样。
|
|
35
|
+
* 投影缓存按版本判定,所以旧缓存会被丢弃、从日志重折一遍——用量是**从日志折出来的**,重折才完整。
|
|
36
|
+
*/
|
|
37
|
+
export const STATE_VERSION = 7
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* **只留台账、不折进视图**的变更类型(词汇表的另一半)。
|
|
41
|
+
*
|
|
42
|
+
* 为什么要有这份清单:`applyMutation` 对不认识的类型**原样返回**(前向兼容,必须如此),
|
|
43
|
+
* 于是「故意不折」与「忘了折」在代码里长得一模一样。把它显式写下来,两件事就分开了:
|
|
44
|
+
* · 发行自检按它比对内核实际落的每一类事实 —— 既没折、又不在清单里的,是**真的缺口**;
|
|
45
|
+
* · 读代码的人不必逐条 grep 才知道哪些事实不上界面。
|
|
46
|
+
*
|
|
47
|
+
* 这四类都是**账本事实**:它们记「系统/账本做了什么」,不改变任何派生量
|
|
48
|
+
* (准入检查、提交流水、快照、恢复),面板只展示当前可读取的证据与记录,
|
|
49
|
+
* 而不是从投影里再长一份。
|
|
50
|
+
*/
|
|
51
|
+
export const LEDGER_ONLY_MUTATIONS = ['admission/checked', 'git/committed', 'git/restored', 'git/snapshot']
|
|
52
|
+
|
|
53
|
+
/** 空状态。`init` 与「日志里还没有任何变更」都必须给出同一个形状。 */
|
|
54
|
+
export function emptyState() {
|
|
55
|
+
return {
|
|
56
|
+
goal: null,
|
|
57
|
+
hypotheses: [],
|
|
58
|
+
plans: [],
|
|
59
|
+
evidence: [],
|
|
60
|
+
audits: [],
|
|
61
|
+
materials: [],
|
|
62
|
+
facts: [],
|
|
63
|
+
forks: [],
|
|
64
|
+
/** 系统按触发派出去的侦察(子角色不是模型自由委派):结论进资料面,这里留一条记录 */
|
|
65
|
+
scouts: [],
|
|
66
|
+
/** 工作区里等人采纳的候选技能(内核扫描后落的事实)与已采纳的记录 */
|
|
67
|
+
brainCandidates: [],
|
|
68
|
+
skillPromotions: [],
|
|
69
|
+
/**
|
|
70
|
+
* 外脑全景(技能清单 + 记忆索引)。**由内核扫描后随投影下发**——面板在浏览器里读不了盘,
|
|
71
|
+
* 而内核每个 pre-step 都在扫;清单走投影(变了才发),正文才走宿主路由(点开时才读)。
|
|
72
|
+
*/
|
|
73
|
+
brain: null,
|
|
74
|
+
/**
|
|
75
|
+
* 宿主原生的**合并技能目录**(预设自带 / 项目 / 用户 / 我们投影的 `clear/skills` 合并后的表)。
|
|
76
|
+
* 内核每个 pre-step 调一次 `ctx.skills.snapshot`,变了才发——于是面板与模型看的是**同一张表**。
|
|
77
|
+
*/
|
|
78
|
+
skillCatalog: null,
|
|
79
|
+
/**
|
|
80
|
+
* 本会话的技能加载记录。**纯派生,零新账**:
|
|
81
|
+
* 模型加载 = 日志里的 `tool/call name='skill'`;人引用 = 一条含 `/名字` 的用户消息。
|
|
82
|
+
*/
|
|
83
|
+
skillUsage: {},
|
|
84
|
+
blocks: {},
|
|
85
|
+
releases: [],
|
|
86
|
+
/**
|
|
87
|
+
* 运行档(人在场 / 无人值守)。**它是人的事实,不是部署的常量**:
|
|
88
|
+
* · `effective` = 内核随投影下发的那一份(只有内核知道组合配置),面板据此画开关;
|
|
89
|
+
* · `override` = 人在面板上切过的那一份(人门 `set_autonomy`,署名是人)。
|
|
90
|
+
* 两者分开摆,才说得清「这是部署给我的默认」还是「这是我此刻要的」。
|
|
91
|
+
*/
|
|
92
|
+
autonomy: { override: null, effective: null },
|
|
93
|
+
/**
|
|
94
|
+
* 项目章程(`PROJECT.md`)的读数(内核扫描后随投影下发)。
|
|
95
|
+
* 为什么要有它:章程的**读**侧早就接在原生指令文件上了,但**写**侧只有一句提示词——
|
|
96
|
+
* 结果是跑完几轮、交付了产物,章程还是铺工作区那天的模板,谁也没发现(2026-09-11 实测)。
|
|
97
|
+
* 事实摆在这里,人与模型都看得见「它还是不是空壳」。
|
|
98
|
+
*/
|
|
99
|
+
constitution: null,
|
|
100
|
+
/**
|
|
101
|
+
* **本体形状**(§23):内核随投影下发的那份声明(对象/状态/边/等级)。
|
|
102
|
+
* 为什么进投影而不是在界面里手抄:面板那一格的页眉要从**声明**生成——
|
|
103
|
+
* 抄一份就会漂移,而漂移的界面比没有界面更坏。它与目录同一条纪律:变了才发。
|
|
104
|
+
*/
|
|
105
|
+
ontology: null,
|
|
106
|
+
/**
|
|
107
|
+
* **续跑窗口的账**(§17):我们向平台说过的那句话——「这个窗口归我布防 / 我按了暂停 /
|
|
108
|
+
* 我收兵了 / 它不在了而那不是我们干的」。形状见 `applyMutation` 的 `continuation/set`。
|
|
109
|
+
*
|
|
110
|
+
* 为什么它必须在投影里而不是内核内存里:宿主的 `paused` 相位分不清「人按的」与
|
|
111
|
+
* 「策略按的」,而这两者的处置正好相反(人按的绝不覆盖,自己按的要能恢复)。
|
|
112
|
+
* 只记在内存里,一次重启就把我们自己的暂停误报成人的暂停——那是一句**不实的话**;
|
|
113
|
+
* 而「我们说过的话要有账」本来就是这套设计的纪律(意图工具不能断言事实)。
|
|
114
|
+
*/
|
|
115
|
+
continuation: null,
|
|
116
|
+
/** 正在飞的工具调用(来自 tool/call):让「评估者在裁决」成为一条可看见的事实 */
|
|
117
|
+
inFlight: null,
|
|
118
|
+
/** 本会话自己写过的路径(来自 tool/call 的 write/edit):L4 来源分离的判据 */
|
|
119
|
+
written: [],
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 等裁决的三个工具:它们的调用在飞时,派生阶段是 `auditing`。
|
|
125
|
+
* 其余工具不等外部裁决,不该让面板在每次调用时跳一下。
|
|
126
|
+
*/
|
|
127
|
+
const VERDICT_WAITERS = new Set(['AdvancePlan', 'AdvanceWorldline', 'CloseGoal'])
|
|
128
|
+
|
|
129
|
+
/** 记下来的自写路径有上限:它是判据,不是档案(档案在会话日志里)。 */
|
|
130
|
+
const MAX_WRITTEN = 512
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 技能引用手势:与原生 `dsh-tool-skill` 的 `SKILL_GESTURE` **同一条正则**。
|
|
134
|
+
*
|
|
135
|
+
* 为什么不各写一条:这条正则是「技能正文会不会被注入」的**充要条件**
|
|
136
|
+
* (`dsh-tool-skill/lib/index.js` 的 `invokedSkillNames`:只认 `source.kind === 'user'` 的
|
|
137
|
+
* 文本块)。面板数出来的用法与原生真的做了什么必须是同一件事,否则读数就是编的。
|
|
138
|
+
*/
|
|
139
|
+
const SKILL_GESTURE = /(^|\s)\/([a-z0-9]+(?:-[a-z0-9]+)*)(?=\s|$)/g
|
|
140
|
+
/** 每条技能留最近几次记录(用量是判据不是档案;要全量去会话日志里查)。 */
|
|
141
|
+
const MAX_SKILL_EVENTS = 8
|
|
142
|
+
/** 最多跟踪多少条技能,免得一个失控的会话把投影撑大。 */
|
|
143
|
+
const MAX_SKILLS_TRACKED = 64
|
|
144
|
+
|
|
145
|
+
/** 从一条消息的文本块里取出被引用的技能名(与原生同一条正则、同一套去重)。 */
|
|
146
|
+
function invokedSkillNames(message) {
|
|
147
|
+
const names = []
|
|
148
|
+
for (const block of Array.isArray(message?.content) ? message.content : []) {
|
|
149
|
+
if (block?.type !== 'text' || typeof block.text !== 'string') continue
|
|
150
|
+
for (const match of block.text.matchAll(SKILL_GESTURE)) if (match[2] !== undefined && !names.includes(match[2])) names.push(match[2])
|
|
151
|
+
}
|
|
152
|
+
return names
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** 这一次加载落在哪一步上(「在第 3 步」这个指针的由来)。 */
|
|
156
|
+
function stepPointer(state) {
|
|
157
|
+
const plan = state.plans.find((item) => item.status === 'active') ?? null
|
|
158
|
+
if (plan === null) return { plan: null, step: null, ordinal: null }
|
|
159
|
+
const step = plan.steps.find((item) => item.status === 'open') ?? null
|
|
160
|
+
return { plan: plan.id, step: step === null ? null : step.id, ordinal: step === null ? null : step.ordinal }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function recordSkillUse(state, name, by, at, pointer) {
|
|
164
|
+
const usage = state.skillUsage ?? (state.skillUsage = {})
|
|
165
|
+
if (usage[name] === undefined && Object.keys(usage).length >= MAX_SKILLS_TRACKED) return
|
|
166
|
+
const entry = usage[name] ?? { name, model: 0, human: 0, events: [], lastAt: null }
|
|
167
|
+
if (by === 'model') entry.model += 1
|
|
168
|
+
else entry.human += 1
|
|
169
|
+
entry.events.push({ by, at, plan: pointer.plan, step: pointer.step, ordinal: pointer.ordinal })
|
|
170
|
+
if (entry.events.length > MAX_SKILL_EVENTS) entry.events.splice(0, entry.events.length - MAX_SKILL_EVENTS)
|
|
171
|
+
entry.lastAt = at
|
|
172
|
+
usage[name] = entry
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** 深拷贝(状态是纯 JSON,这是唯一需要的工具)。 */
|
|
176
|
+
function clone(value) {
|
|
177
|
+
return value === undefined ? undefined : JSON.parse(JSON.stringify(value))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function appendSteps(plan, rawSteps) {
|
|
181
|
+
for (const raw of rawSteps) {
|
|
182
|
+
plan.steps.push({
|
|
183
|
+
id: raw.id,
|
|
184
|
+
ordinal: plan.steps.length + 1,
|
|
185
|
+
do: raw.do,
|
|
186
|
+
artifacts: raw.artifacts ?? [],
|
|
187
|
+
done_criteria: raw.done_criteria,
|
|
188
|
+
tests: raw.tests ?? null,
|
|
189
|
+
status: 'open',
|
|
190
|
+
evidence: null,
|
|
191
|
+
advancedAt: null,
|
|
192
|
+
voidReason: null,
|
|
193
|
+
criteria_versions: [raw.done_criteria],
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 把一条变更记录折进状态,返回**新状态**(不改传入的那个)。
|
|
200
|
+
* 认不出来的 `t` 原样返回旧状态(投影的「不感兴趣就返回同一引用」语义)。
|
|
201
|
+
*
|
|
202
|
+
* @param state - 当前状态(纯 JSON)
|
|
203
|
+
* @param mutation - 预设侧写进 `tool/result.meta.mutation` 的变更记录
|
|
204
|
+
* @returns 新状态,或原状态(不关心这条)
|
|
205
|
+
*/
|
|
206
|
+
export function applyMutation(state, mutation) {
|
|
207
|
+
if (mutation === null || typeof mutation !== 'object' || typeof mutation.t !== 'string') return state
|
|
208
|
+
const at = typeof mutation.at === 'number' ? mutation.at : 0
|
|
209
|
+
const next = clone(state)
|
|
210
|
+
const RANK = { open: 0, blocked: 0, advanced: 1, void: 1 }
|
|
211
|
+
const settle = (step, status) => {
|
|
212
|
+
if (step === undefined || step === null) return
|
|
213
|
+
if ((RANK[status] ?? 0) < (RANK[step.status] ?? 0)) return // 降级不可表示(事实棘轮)
|
|
214
|
+
step.status = status
|
|
215
|
+
}
|
|
216
|
+
const planOf = (id) => next.plans.find((plan) => plan.id === id)
|
|
217
|
+
const stepOf = (planId, stepId) => planOf(planId)?.steps.find((step) => step.id === stepId)
|
|
218
|
+
|
|
219
|
+
switch (mutation.t) {
|
|
220
|
+
case 'goal/set': {
|
|
221
|
+
const previous = next.goal
|
|
222
|
+
if (previous !== null && previous.id === mutation.id && mutation.revision > previous.revision) {
|
|
223
|
+
previous.revision = mutation.revision
|
|
224
|
+
previous.claim = mutation.claim
|
|
225
|
+
previous.done_criteria = mutation.done_criteria
|
|
226
|
+
previous.promote_at_level = mutation.promote_at_level
|
|
227
|
+
previous.reasons.push(mutation.reason ?? null)
|
|
228
|
+
} else {
|
|
229
|
+
if (previous !== null && previous.status === 'open') previous.status = 'superseded'
|
|
230
|
+
next.goal = {
|
|
231
|
+
id: mutation.id,
|
|
232
|
+
claim: mutation.claim,
|
|
233
|
+
done_criteria: mutation.done_criteria,
|
|
234
|
+
promote_at_level: mutation.promote_at_level,
|
|
235
|
+
status: 'open',
|
|
236
|
+
revision: mutation.revision ?? 1,
|
|
237
|
+
reasons: [mutation.reason ?? null],
|
|
238
|
+
openedAt: at,
|
|
239
|
+
closedAt: null,
|
|
240
|
+
closeVerdict: null,
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
for (const hypothesis of mutation.hypotheses ?? []) {
|
|
244
|
+
next.hypotheses.push({
|
|
245
|
+
id: hypothesis.id,
|
|
246
|
+
goal: mutation.id,
|
|
247
|
+
claim: hypothesis.claim,
|
|
248
|
+
refute_when: hypothesis.refute_when,
|
|
249
|
+
status: 'proposed',
|
|
250
|
+
version: hypothesis.version ?? 1,
|
|
251
|
+
at,
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
break
|
|
255
|
+
}
|
|
256
|
+
case 'goal/closed': {
|
|
257
|
+
if (next.goal !== null && next.goal.id === mutation.id) {
|
|
258
|
+
next.goal.status = mutation.status
|
|
259
|
+
next.goal.closedAt = at
|
|
260
|
+
next.goal.closeVerdict = mutation.verdict ?? null
|
|
261
|
+
}
|
|
262
|
+
break
|
|
263
|
+
}
|
|
264
|
+
case 'hypothesis/superseded': {
|
|
265
|
+
/**
|
|
266
|
+
* **终态黏住**(§22,与 ClearAI 本体 P3 同一个病、同一个修法):
|
|
267
|
+
* 旧写法把任何还没被替代的行都改成 `superseded` —— 于是一条**已被推翻**的假设,
|
|
268
|
+
* 会因为下一版假设清单里没再列它而变成「已被替代」。而「被推翻」与「被替代」是两件事:
|
|
269
|
+
* 前者说这条猜想错了,后者说这条猜想不再被提。后者不该改写前者。
|
|
270
|
+
* 已经升格成事实的(`claim` 出现在 facts 里)同理:事实在,假设就不能被悄悄换掉。
|
|
271
|
+
*/
|
|
272
|
+
const promoted = next.facts.some((fact) => fact.goal === mutation.goal && fact.text === mutation.claim)
|
|
273
|
+
const row = next.hypotheses.find((item) => item.id === mutation.id && item.status !== 'superseded' && item.status !== 'refuted' && !promoted)
|
|
274
|
+
if (row !== undefined) {
|
|
275
|
+
row.status = 'superseded'
|
|
276
|
+
row.supersededBy = mutation.by
|
|
277
|
+
}
|
|
278
|
+
break
|
|
279
|
+
}
|
|
280
|
+
case 'plan/created': {
|
|
281
|
+
// 重复 id 是上游 bug(比如两毫秒内建了两份计划)。忽略比污染旧计划好:
|
|
282
|
+
// 忽略会让下一次调用明确报「没有活动计划」,污染则是静默地错。
|
|
283
|
+
if (next.plans.some((plan) => plan.id === mutation.id)) break
|
|
284
|
+
const plan = {
|
|
285
|
+
id: mutation.id,
|
|
286
|
+
goal: mutation.goal ?? null,
|
|
287
|
+
phase_id: mutation.phase_id ?? null,
|
|
288
|
+
brief: mutation.brief ?? '',
|
|
289
|
+
status: 'active',
|
|
290
|
+
at,
|
|
291
|
+
closedAt: null,
|
|
292
|
+
summary: null,
|
|
293
|
+
blocked: undefined,
|
|
294
|
+
/**
|
|
295
|
+
* 授权记号(ClearAI `plan.confirmed_at` / `confirmed_by`)。
|
|
296
|
+
* 两种取得方式,都可考:**显式动作**(人在面板上按确认 → `by:'user'`)与
|
|
297
|
+
* **行为**(交付过一步 → `by:'progress'`);无人值守那一档在立约时由档自动确认
|
|
298
|
+
* (`by:'autonomy'`——ClearAI 的原话是「选择 Goal 模式就是用户的无人值守授权」)。
|
|
299
|
+
* 注意这是一个**记号**,不是闸门:没确认的计划照样能被交付推起来,那一刻记号按事实补写。
|
|
300
|
+
*/
|
|
301
|
+
confirmed_at: mutation.confirmed_at ?? null,
|
|
302
|
+
confirmed_by: mutation.confirmed_by ?? null,
|
|
303
|
+
steps: [],
|
|
304
|
+
}
|
|
305
|
+
next.plans.push(plan)
|
|
306
|
+
appendSteps(plan, mutation.steps ?? [])
|
|
307
|
+
break
|
|
308
|
+
}
|
|
309
|
+
case 'plan/confirmed': {
|
|
310
|
+
// 第一次授权为准(`stamp_confirmed_by_progress` 同款幂等:已确认就不再改写)。
|
|
311
|
+
const plan = planOf(mutation.plan)
|
|
312
|
+
if (plan === undefined || plan.confirmed_at !== null) break
|
|
313
|
+
plan.confirmed_at = mutation.at ?? new Date(at).toISOString()
|
|
314
|
+
plan.confirmed_by = mutation.by ?? 'user'
|
|
315
|
+
break
|
|
316
|
+
}
|
|
317
|
+
case 'plan/amended': {
|
|
318
|
+
const plan = planOf(mutation.plan)
|
|
319
|
+
if (plan === undefined || plan.status !== 'active') break
|
|
320
|
+
appendSteps(plan, [mutation.step])
|
|
321
|
+
break
|
|
322
|
+
}
|
|
323
|
+
case 'plan/refined': {
|
|
324
|
+
const step = stepOf(mutation.plan, mutation.step)
|
|
325
|
+
if (step === undefined || step.status !== 'open') break
|
|
326
|
+
step.done_criteria = mutation.new_criteria
|
|
327
|
+
step.criteria_versions.push(mutation.new_criteria) // 旧版本留着,什么都不删
|
|
328
|
+
break
|
|
329
|
+
}
|
|
330
|
+
case 'plan/voided': {
|
|
331
|
+
const step = stepOf(mutation.plan, mutation.step)
|
|
332
|
+
if (step === undefined) break
|
|
333
|
+
settle(step, 'void')
|
|
334
|
+
if (step.status === 'void') step.voidReason = mutation.reason
|
|
335
|
+
break
|
|
336
|
+
}
|
|
337
|
+
case 'plan/closed': {
|
|
338
|
+
const plan = planOf(mutation.plan)
|
|
339
|
+
if (plan !== undefined) {
|
|
340
|
+
plan.status = 'closed'
|
|
341
|
+
plan.closedAt = at
|
|
342
|
+
plan.summary = mutation.summary ?? null
|
|
343
|
+
plan.blocked = undefined
|
|
344
|
+
}
|
|
345
|
+
break
|
|
346
|
+
}
|
|
347
|
+
case 'plan/blocked': {
|
|
348
|
+
const plan = planOf(mutation.plan)
|
|
349
|
+
if (plan !== undefined) plan.blocked = { step: mutation.step, attempts: mutation.attempts, reason: mutation.reason }
|
|
350
|
+
break
|
|
351
|
+
}
|
|
352
|
+
case 'block/counted': {
|
|
353
|
+
next.blocks[`${mutation.plan}:${mutation.step}`] = mutation.count
|
|
354
|
+
break
|
|
355
|
+
}
|
|
356
|
+
case 'block/cleared': {
|
|
357
|
+
delete next.blocks[`${mutation.plan}:${mutation.step}`]
|
|
358
|
+
break
|
|
359
|
+
}
|
|
360
|
+
case 'observation/recorded': {
|
|
361
|
+
next.materials.push({
|
|
362
|
+
id: mutation.id,
|
|
363
|
+
ref: mutation.ref,
|
|
364
|
+
source: mutation.source ?? 'self',
|
|
365
|
+
digest: mutation.digest ?? null,
|
|
366
|
+
bytes: mutation.bytes ?? null,
|
|
367
|
+
note: mutation.note ?? null,
|
|
368
|
+
step: mutation.step ?? null,
|
|
369
|
+
branch: mutation.branch ?? null,
|
|
370
|
+
at,
|
|
371
|
+
})
|
|
372
|
+
break
|
|
373
|
+
}
|
|
374
|
+
case 'audit/dispatched': {
|
|
375
|
+
next.audits.push({
|
|
376
|
+
id: mutation.id,
|
|
377
|
+
step: mutation.step,
|
|
378
|
+
plan: mutation.plan,
|
|
379
|
+
kind: mutation.kind ?? 'evidence_audit',
|
|
380
|
+
verdict: null,
|
|
381
|
+
evaluator: 'independent',
|
|
382
|
+
child: mutation.evaluator_session ?? null,
|
|
383
|
+
capability: mutation.capability ?? null,
|
|
384
|
+
shortfalls: [],
|
|
385
|
+
basis: null,
|
|
386
|
+
card_path: null,
|
|
387
|
+
at,
|
|
388
|
+
})
|
|
389
|
+
break
|
|
390
|
+
}
|
|
391
|
+
case 'audit/settled': {
|
|
392
|
+
const audit = next.audits.find((item) => item.id === mutation.id)
|
|
393
|
+
if (audit !== undefined) {
|
|
394
|
+
audit.verdict = mutation.verdict
|
|
395
|
+
audit.shortfalls = mutation.shortfalls ?? []
|
|
396
|
+
audit.basis = mutation.basis ?? null
|
|
397
|
+
audit.card_path = mutation.card_path ?? null
|
|
398
|
+
}
|
|
399
|
+
break
|
|
400
|
+
}
|
|
401
|
+
case 'evidence/recorded': {
|
|
402
|
+
next.evidence.push({
|
|
403
|
+
id: mutation.id,
|
|
404
|
+
step: mutation.step,
|
|
405
|
+
plan: mutation.plan,
|
|
406
|
+
branch: mutation.branch ?? null,
|
|
407
|
+
verdict: mutation.verdict,
|
|
408
|
+
level: mutation.level,
|
|
409
|
+
evaluator: mutation.evaluator,
|
|
410
|
+
basis: mutation.basis ?? null,
|
|
411
|
+
refs: mutation.refs ?? [],
|
|
412
|
+
/**
|
|
413
|
+
* **出处**(§27b):记账那一刻解析好的四类入口
|
|
414
|
+
* (`artifact` / `audit-card` / `evaluator-session` / `approval-record`)。
|
|
415
|
+
* 界面只渲染它,不再自己猜「这一串是路径还是材料 id」——
|
|
416
|
+
* 正是那种猜法让整排出处点不开(真数据实测)。
|
|
417
|
+
*/
|
|
418
|
+
origins: Array.isArray(mutation.origins) ? mutation.origins : [],
|
|
419
|
+
anchor: mutation.anchor ?? 'artifact',
|
|
420
|
+
basis_reviewable: mutation.basis_reviewable !== false,
|
|
421
|
+
at,
|
|
422
|
+
})
|
|
423
|
+
break
|
|
424
|
+
}
|
|
425
|
+
case 'step/advanced': {
|
|
426
|
+
const step = stepOf(mutation.plan, mutation.step)
|
|
427
|
+
if (step !== undefined) {
|
|
428
|
+
settle(step, 'advanced')
|
|
429
|
+
step.evidence = mutation.evidence
|
|
430
|
+
step.advancedAt = at
|
|
431
|
+
}
|
|
432
|
+
break
|
|
433
|
+
}
|
|
434
|
+
case 'human/released': {
|
|
435
|
+
/**
|
|
436
|
+
* 放行绑在**哪条轴**上要说清(2026-09-11,AUDIT §14-A):主线绑步骤、世界线绑分支。
|
|
437
|
+
* `via` 记它凭什么算数——现在只有 `approval`(原生审批栈的权威记录),不推断。
|
|
438
|
+
*/
|
|
439
|
+
next.releases.push({
|
|
440
|
+
step: mutation.step ?? null,
|
|
441
|
+
plan: mutation.plan ?? null,
|
|
442
|
+
fork: mutation.fork ?? null,
|
|
443
|
+
branch: mutation.branch ?? null,
|
|
444
|
+
via: mutation.via ?? null,
|
|
445
|
+
at,
|
|
446
|
+
call: mutation.call ?? null,
|
|
447
|
+
})
|
|
448
|
+
break
|
|
449
|
+
}
|
|
450
|
+
case 'fact/promoted': {
|
|
451
|
+
/**
|
|
452
|
+
* 事实的字段(§23):`scope` 是它的**边界**(推翻条件)——没有边界的事实下一轮没人敢用;
|
|
453
|
+
* `level` 是它被支持到哪一级(判者可查)。两者都来自升格那一刻的假设,不事后补。
|
|
454
|
+
*/
|
|
455
|
+
next.facts.push({
|
|
456
|
+
id: mutation.id,
|
|
457
|
+
goal: mutation.goal,
|
|
458
|
+
text: mutation.text,
|
|
459
|
+
scope: mutation.scope ?? null,
|
|
460
|
+
level: mutation.level ?? null,
|
|
461
|
+
evidence: mutation.evidence ?? [],
|
|
462
|
+
path: mutation.path ?? null,
|
|
463
|
+
at,
|
|
464
|
+
})
|
|
465
|
+
break
|
|
466
|
+
}
|
|
467
|
+
case 'worldline/prepared': {
|
|
468
|
+
const fork = next.forks.find((item) => item.id === mutation.fork)
|
|
469
|
+
if (fork === undefined) break
|
|
470
|
+
fork.tier = mutation.tier
|
|
471
|
+
fork.base = mutation.base ?? null
|
|
472
|
+
fork.degradedReason = mutation.reason ?? null
|
|
473
|
+
for (const entry of mutation.branches ?? []) {
|
|
474
|
+
const branch = fork.branches.find((item) => item.id === entry.id)
|
|
475
|
+
if (branch !== undefined) {
|
|
476
|
+
branch.worktree_path = entry.path
|
|
477
|
+
branch.git_branch = entry.branch
|
|
478
|
+
branch.worktree_removed = false
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
break
|
|
482
|
+
}
|
|
483
|
+
case 'fork/merge_skipped': {
|
|
484
|
+
const fork = next.forks.find((item) => item.id === mutation.fork)
|
|
485
|
+
if (fork === undefined) break
|
|
486
|
+
fork.mergeSkipped = { branch: mutation.branch ?? null, reason: mutation.reason ?? null }
|
|
487
|
+
break
|
|
488
|
+
}
|
|
489
|
+
case 'worldline/executing': {
|
|
490
|
+
const fork = next.forks.find((item) => item.id === mutation.fork)
|
|
491
|
+
const branch = fork?.branches.find((item) => item.id === mutation.branch)
|
|
492
|
+
if (branch === undefined) break
|
|
493
|
+
branch.execution = { child: mutation.child ?? null, capability: mutation.capability ?? null, ok: null, conclusion: null }
|
|
494
|
+
break
|
|
495
|
+
}
|
|
496
|
+
case 'worldline/executed': {
|
|
497
|
+
const fork = next.forks.find((item) => item.id === mutation.fork)
|
|
498
|
+
const branch = fork?.branches.find((item) => item.id === mutation.branch)
|
|
499
|
+
if (branch === undefined) break
|
|
500
|
+
branch.execution = { ...(branch.execution ?? { child: mutation.child ?? null, capability: null }), ok: mutation.ok === true, conclusion: mutation.conclusion ?? null, note: mutation.note ?? null }
|
|
501
|
+
break
|
|
502
|
+
}
|
|
503
|
+
case 'worldline/removed': {
|
|
504
|
+
const fork = next.forks.find((item) => item.id === mutation.fork)
|
|
505
|
+
const branch = fork?.branches.find((item) => item.id === mutation.branch)
|
|
506
|
+
if (branch === undefined) break
|
|
507
|
+
branch.worktree_removed = true
|
|
508
|
+
branch.kept_ref = mutation.kept_ref !== false
|
|
509
|
+
break
|
|
510
|
+
}
|
|
511
|
+
case 'fork/merged': {
|
|
512
|
+
const fork = next.forks.find((item) => item.id === mutation.fork)
|
|
513
|
+
if (fork === undefined) break
|
|
514
|
+
fork.merge = {
|
|
515
|
+
branch: mutation.branch,
|
|
516
|
+
commit: mutation.commit ?? null,
|
|
517
|
+
mode: mutation.mode ?? 'merged',
|
|
518
|
+
snapshotCommit: mutation.snapshot_commit ?? null,
|
|
519
|
+
// 临时采纳 = 分差不足以称结论(或由判断而来):它不是结论,是待复核的决定。
|
|
520
|
+
provisional: mutation.provisional === true,
|
|
521
|
+
by: mutation.by ?? 'metric',
|
|
522
|
+
decisionNote: mutation.decision_note ?? null,
|
|
523
|
+
margin: mutation.margin ?? null,
|
|
524
|
+
}
|
|
525
|
+
fork.mergeConflict = null
|
|
526
|
+
break
|
|
527
|
+
}
|
|
528
|
+
case 'fork/merge_conflict': {
|
|
529
|
+
const fork = next.forks.find((item) => item.id === mutation.fork)
|
|
530
|
+
if (fork === undefined) break
|
|
531
|
+
fork.mergeConflict = { branch: mutation.branch, winner: mutation.winner ?? null, margin: mutation.margin ?? null, detail: mutation.detail ?? null }
|
|
532
|
+
break
|
|
533
|
+
}
|
|
534
|
+
case 'scout/dispatched': {
|
|
535
|
+
next.scouts.push({
|
|
536
|
+
id: mutation.id,
|
|
537
|
+
step: mutation.step,
|
|
538
|
+
plan: mutation.plan ?? null,
|
|
539
|
+
trigger: mutation.trigger ?? null,
|
|
540
|
+
child: mutation.child ?? null,
|
|
541
|
+
capability: mutation.capability ?? null,
|
|
542
|
+
// 任务身份:同一个身份已经正常回灌过的,再派时会被复用而不是重跑(幂等派遣)。
|
|
543
|
+
digest: mutation.digest ?? null,
|
|
544
|
+
conclusion: null,
|
|
545
|
+
note: null,
|
|
546
|
+
at,
|
|
547
|
+
})
|
|
548
|
+
break
|
|
549
|
+
}
|
|
550
|
+
case 'scout/settled': {
|
|
551
|
+
const scout = next.scouts.find((item) => item.id === mutation.id)
|
|
552
|
+
if (scout !== undefined) {
|
|
553
|
+
scout.conclusion = mutation.conclusion ?? null
|
|
554
|
+
scout.note = mutation.note ?? null
|
|
555
|
+
}
|
|
556
|
+
break
|
|
557
|
+
}
|
|
558
|
+
case 'fork/created': {
|
|
559
|
+
if (next.forks.some((fork) => fork.step === mutation.step)) break // 一步只分叉一次
|
|
560
|
+
next.forks.push({
|
|
561
|
+
id: mutation.id,
|
|
562
|
+
step: mutation.step,
|
|
563
|
+
plan: mutation.plan,
|
|
564
|
+
question: mutation.question,
|
|
565
|
+
decide_by: mutation.decide_by,
|
|
566
|
+
settled: false,
|
|
567
|
+
abandoned: false,
|
|
568
|
+
abandonReason: null,
|
|
569
|
+
/** 横评仲裁的判决(`fork/arbitrated`)与它的会话 id(面板可旁观)。 */
|
|
570
|
+
arbitration: null,
|
|
571
|
+
arbitrationSession: null,
|
|
572
|
+
/** 人在面板上的裁决(结构化记录,by:'user');null = 还没裁。 */
|
|
573
|
+
humanDecision: null,
|
|
574
|
+
verdict: null,
|
|
575
|
+
undecidable: null,
|
|
576
|
+
at,
|
|
577
|
+
branches: (mutation.options ?? []).map((option) => ({
|
|
578
|
+
id: option.id,
|
|
579
|
+
label: option.label,
|
|
580
|
+
approach: option.approach,
|
|
581
|
+
done_criteria: option.done_criteria,
|
|
582
|
+
workspace: option.workspace ?? null,
|
|
583
|
+
artifacts: option.artifacts ?? [],
|
|
584
|
+
level: option.level ?? 'L0',
|
|
585
|
+
status: 'exploring',
|
|
586
|
+
card_path: null,
|
|
587
|
+
evaluator_session: null,
|
|
588
|
+
humanDecision: null,
|
|
589
|
+
reading: null,
|
|
590
|
+
validity: null,
|
|
591
|
+
verdict: null,
|
|
592
|
+
basis: null,
|
|
593
|
+
evaluator: null,
|
|
594
|
+
evidence: null,
|
|
595
|
+
at: null,
|
|
596
|
+
})),
|
|
597
|
+
})
|
|
598
|
+
break
|
|
599
|
+
}
|
|
600
|
+
case 'branch/delivered': {
|
|
601
|
+
const fork = next.forks.find((item) => item.id === mutation.fork)
|
|
602
|
+
const branch = fork?.branches.find((item) => item.id === mutation.branch)
|
|
603
|
+
if (branch === undefined) break
|
|
604
|
+
if ((BRANCH_RANK[branch.status] ?? 0) < BRANCH_RANK.evaluated) branch.status = 'evaluated'
|
|
605
|
+
branch.reading = mutation.reading ?? null
|
|
606
|
+
branch.validity = mutation.validity ?? null
|
|
607
|
+
branch.verdict = mutation.verdict ?? null
|
|
608
|
+
branch.basis = mutation.basis ?? null
|
|
609
|
+
branch.evaluator = mutation.evaluator ?? null
|
|
610
|
+
branch.evidence = mutation.evidence ?? null
|
|
611
|
+
// 评估卡与评估者会话:仲裁用卡(注入),面板用会话(旁观入口)。
|
|
612
|
+
branch.card_path = mutation.card_path ?? null
|
|
613
|
+
branch.evaluator_session = mutation.evaluator_session ?? null
|
|
614
|
+
branch.at = at
|
|
615
|
+
break
|
|
616
|
+
}
|
|
617
|
+
case 'fork/converged': {
|
|
618
|
+
const fork = next.forks.find((item) => item.id === mutation.fork)
|
|
619
|
+
if (fork === undefined) break
|
|
620
|
+
fork.settled = true
|
|
621
|
+
fork.undecidable = null
|
|
622
|
+
fork.verdict = { winner: mutation.winner, margin: mutation.margin ?? null, tie: mutation.tie === true, metric: mutation.metric ?? null, direction: mutation.direction ?? null }
|
|
623
|
+
for (const branch of fork.branches) {
|
|
624
|
+
if ((BRANCH_RANK[branch.status] ?? 0) >= BRANCH_RANK.adopted) continue // 一旦采纳,状态冻结
|
|
625
|
+
branch.status = branch.id === mutation.winner ? 'adopted' : 'pruned'
|
|
626
|
+
}
|
|
627
|
+
break
|
|
628
|
+
}
|
|
629
|
+
case 'brain/candidates': {
|
|
630
|
+
// 内核扫描工作区后落的**事实**:当前有哪些候选技能在等人采纳。
|
|
631
|
+
// 收件箱条目据此派生(状态锚:候选被采纳或文件消失,条目自然消失)。
|
|
632
|
+
next.brainCandidates = Array.isArray(mutation.items) ? mutation.items : []
|
|
633
|
+
break
|
|
634
|
+
}
|
|
635
|
+
case 'skill/promoted': {
|
|
636
|
+
next.skillPromotions = [...(next.skillPromotions ?? []), { name: mutation.name, by: mutation.by ?? 'user', at }]
|
|
637
|
+
break
|
|
638
|
+
}
|
|
639
|
+
case 'fork/arbitration_dispatched': {
|
|
640
|
+
const fork = next.forks.find((item) => item.id === mutation.fork)
|
|
641
|
+
if (fork === undefined) break
|
|
642
|
+
fork.arbitrationSession = mutation.arbiter_session ?? null
|
|
643
|
+
break
|
|
644
|
+
}
|
|
645
|
+
case 'fork/arbitrated': {
|
|
646
|
+
const fork = next.forks.find((item) => item.id === mutation.fork)
|
|
647
|
+
if (fork === undefined) break
|
|
648
|
+
// 判决写进节点(唯一真相仍是投影):采纳与否由内核重判一次决定,这里只记事实。
|
|
649
|
+
fork.arbitration = {
|
|
650
|
+
winner: mutation.winner ?? null,
|
|
651
|
+
ranking: mutation.ranking ?? [],
|
|
652
|
+
reason: mutation.reason ?? '',
|
|
653
|
+
confidence: mutation.confidence ?? 'low',
|
|
654
|
+
arbiter_session: mutation.arbiter_run_id ?? null,
|
|
655
|
+
}
|
|
656
|
+
break
|
|
657
|
+
}
|
|
658
|
+
case 'fork/undecidable': {
|
|
659
|
+
const fork = next.forks.find((item) => item.id === mutation.fork)
|
|
660
|
+
if (fork === undefined) break
|
|
661
|
+
fork.undecidable = { code: mutation.code, reason: mutation.reason, readings: mutation.readings ?? [] }
|
|
662
|
+
break
|
|
663
|
+
}
|
|
664
|
+
case 'fork/abandoned': {
|
|
665
|
+
const fork = next.forks.find((item) => item.id === mutation.fork)
|
|
666
|
+
if (fork === undefined) break
|
|
667
|
+
fork.abandoned = true
|
|
668
|
+
fork.abandonReason = mutation.reason ?? null
|
|
669
|
+
break
|
|
670
|
+
}
|
|
671
|
+
case 'continuation/set': {
|
|
672
|
+
/**
|
|
673
|
+
* 续跑窗口的账(§17)。四种状态就是「我们说过的话」的全部:
|
|
674
|
+
* · `armed` —— 我们布了防(或按策略恢复了它):此刻它在为我们跑。
|
|
675
|
+
* · `paused` —— **我们**按下的暂停,`why` 是真实理由(等裁决 / 等人门 / 阶段边界)。
|
|
676
|
+
* · `stopped` —— 我们收的兵(目标达成 / 如实放弃 / 计划触礁)。
|
|
677
|
+
* · `withdrawn` —— 它不在了,而**不是我们清的**(我们的清除永远与建同拍,见内核 §17.2):
|
|
678
|
+
* 这是平台上唯一说得出口的解释,所以它也只说这些,不说「谁」干的。
|
|
679
|
+
* 一条记录:**窗口在会话里是单数**,所以后一条覆盖前一条,不留历史(历史在会话日志里)。
|
|
680
|
+
*/
|
|
681
|
+
next.continuation = {
|
|
682
|
+
state: typeof mutation.state === 'string' ? mutation.state : 'unknown',
|
|
683
|
+
goal: mutation.goal ?? null,
|
|
684
|
+
target: mutation.target ?? null,
|
|
685
|
+
why: mutation.why ?? null,
|
|
686
|
+
/**
|
|
687
|
+
* 平台对象上那句身份文本的**最后一次观测值**(§17.6):我们写下它时记我们的,
|
|
688
|
+
* 别人改写之后重记他的。判据是「平台上的文本 === 这条记录」⇒ 还是我们的。
|
|
689
|
+
*/
|
|
690
|
+
label: mutation.label ?? null,
|
|
691
|
+
at,
|
|
692
|
+
}
|
|
693
|
+
break
|
|
694
|
+
}
|
|
695
|
+
default: {
|
|
696
|
+
return state // 不认识:不感兴趣,原样返回
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return next
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/** 一次调用可能落多条事实(观测 → 准入 → 审计 → 证据 → 推进):按序折进去。 */
|
|
703
|
+
export function applyMutations(state, mutations) {
|
|
704
|
+
if (!Array.isArray(mutations) || mutations.length === 0) return state
|
|
705
|
+
let next = state
|
|
706
|
+
for (const mutation of mutations) next = applyMutation(next, mutation)
|
|
707
|
+
return next
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* 会话日志事件 → 状态。这是投影的入口:除了工具结果里的变更记录,
|
|
712
|
+
* 还吃三条**关于过程本身的事实**——
|
|
713
|
+
* · `tool/call` 在飞 → 「评估者在裁决」这个阶段(不用等结果就能看见)
|
|
714
|
+
* · `write`/`edit` 的自写路径 → L4 的「做的人自己写的不算观测」
|
|
715
|
+
* · `skill` 的加载与人的 `/名字` 引用 → 本会话的技能用量(没有第二本账)
|
|
716
|
+
* 三者都来自日志,所以重挂载、重放、fork 之后都还对。
|
|
717
|
+
*/
|
|
718
|
+
export function applyEvent(state, event) {
|
|
719
|
+
if (event === null || typeof event !== 'object') return state
|
|
720
|
+
if (event.type === 'user/message') {
|
|
721
|
+
// 内核观察到的事实(候选技能、采纳记录、合并目录)走**插件消息的结构化 section**——
|
|
722
|
+
// 记在会话日志里,所以状态仍然可以从日志重放出来,而不是靠一句散文。
|
|
723
|
+
const source = event.data?.source
|
|
724
|
+
if (source !== null && typeof source === 'object' && source.kind === 'plugin' && Array.isArray(source.sections)) {
|
|
725
|
+
/**
|
|
726
|
+
* 一条插件消息里可能**同时**带好几件事实(内核一次 pre-step 把目录、当档、候选一起发)。
|
|
727
|
+
* 所以这里是「逐件折」而不是「找到一件就 return」——2026-09-11 实测踩过:
|
|
728
|
+
* 原来找到目录就 return,于是同一条消息里的**当档被吃掉**,
|
|
729
|
+
* 表现是面板上的档位开关永远停在「还没定档」,而机制那边早就按新档跑了。
|
|
730
|
+
*/
|
|
731
|
+
const brain = source.sections.find((section) => section?.name === 'clearai/brain')
|
|
732
|
+
const tier = source.sections.find((section) => section?.name === 'clearai/autonomy')
|
|
733
|
+
/**
|
|
734
|
+
* 内核在**回合之间**观察到的事实(目前:世界线执行者跑完)。
|
|
735
|
+
* 与工具结果里的 `meta.mutations` **同形**,所以直接走同一个 `applyMutations`——
|
|
736
|
+
* 事实只有一个折法,不管它是从工具结果来的还是从插件消息来的。
|
|
737
|
+
*
|
|
738
|
+
* 注意它必须**在同一道门里**折:一条插件消息可能同时带目录、当档与事实变更,
|
|
739
|
+
* 而 `next` 是这道门里才 clone 出来的。第一版把这段写在门**外**并引用了 `next`,
|
|
740
|
+
* 抛出的 ReferenceError 被自己的 try/catch 吞掉——于是它一直是空操作
|
|
741
|
+
* (2026-09-11 长测:世界线结论回灌「落账了」但投影里看不见,查了一轮才找到)。
|
|
742
|
+
*/
|
|
743
|
+
const ontologySection = source.sections.find((section) => section?.name === 'clearai/ontology')
|
|
744
|
+
if (ontologySection !== undefined && typeof ontologySection.text === 'string') {
|
|
745
|
+
try {
|
|
746
|
+
const payload = JSON.parse(ontologySection.text)
|
|
747
|
+
if (payload !== null && typeof payload === 'object' && Array.isArray(payload.objects)) next.ontology = payload
|
|
748
|
+
} catch {
|
|
749
|
+
/* 坏 payload:不折(与目录同一条纪律,不猜) */
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const facts = source.sections.find((section) => section?.name === 'clearai/mutations')
|
|
753
|
+
if (brain !== undefined || tier !== undefined || facts !== undefined) {
|
|
754
|
+
let next = clone(state)
|
|
755
|
+
let touched = false
|
|
756
|
+
if (brain !== undefined && typeof brain.text === 'string') {
|
|
757
|
+
try {
|
|
758
|
+
const payload = JSON.parse(brain.text)
|
|
759
|
+
if (Array.isArray(payload?.candidates)) next.brainCandidates = payload.candidates
|
|
760
|
+
if (payload?.overview !== undefined) next.brain = payload.overview
|
|
761
|
+
// 章程读数:只有真的变了才会出现在 payload 里(内核那侧按文件系统指纹去重)。
|
|
762
|
+
if (payload?.constitution !== undefined) next.constitution = payload.constitution
|
|
763
|
+
// 合并目录:内核从宿主的 skills 服务取的那张表(变了才发)。
|
|
764
|
+
if (payload?.catalog !== null && typeof payload?.catalog === 'object' && Array.isArray(payload.catalog.entries)) {
|
|
765
|
+
next.skillCatalog = { complete: payload.catalog.complete === true, entries: payload.catalog.entries, at: typeof event.time === 'number' ? event.time : null }
|
|
766
|
+
}
|
|
767
|
+
for (const name of Array.isArray(payload?.promoted) ? payload.promoted : []) {
|
|
768
|
+
next.skillPromotions = [...(next.skillPromotions ?? []), { name, by: 'user', at: typeof event.time === 'number' ? event.time : Date.now() }]
|
|
769
|
+
}
|
|
770
|
+
touched = true
|
|
771
|
+
} catch {
|
|
772
|
+
// 坏 payload:如实少一条外脑事实,但不吞掉同一条消息里别的事实。
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (facts !== undefined && typeof facts.text === 'string') {
|
|
776
|
+
try {
|
|
777
|
+
const payload = JSON.parse(facts.text)
|
|
778
|
+
if (Array.isArray(payload?.mutations) && payload.mutations.length > 0) {
|
|
779
|
+
next = applyMutations(next, payload.mutations)
|
|
780
|
+
touched = true
|
|
781
|
+
}
|
|
782
|
+
} catch {
|
|
783
|
+
// 同上:坏 payload 不当事实,也不影响别的。
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (tier !== undefined && typeof tier.text === 'string') {
|
|
787
|
+
try {
|
|
788
|
+
const payload = JSON.parse(tier.text)
|
|
789
|
+
const value = String(payload?.value ?? '')
|
|
790
|
+
if (value === 'attended' || value === 'unattended') {
|
|
791
|
+
next.autonomy = {
|
|
792
|
+
...(next.autonomy ?? {}),
|
|
793
|
+
effective: { value, preset: String(payload?.preset ?? value), source: payload?.source === 'session' ? 'session' : 'preset', at: typeof event.time === 'number' ? event.time : null },
|
|
794
|
+
}
|
|
795
|
+
touched = true
|
|
796
|
+
}
|
|
797
|
+
} catch {
|
|
798
|
+
// 同上:坏 payload 不当事实,也不影响别的。
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (touched) return next
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
// 人在面板上按下的人门动作 → 落成**事实**(不是请求):确认计划就地生效;
|
|
805
|
+
// 世界线的裁决记在分叉上,由内核的 ConvergeFork/AbandonFork 落实(合并与清理是内核的活)。
|
|
806
|
+
const gate = parseHumanGate(event.data)
|
|
807
|
+
// 人引用技能:一条含 `/名字` 的**用户消息**。这就是原生 pre-step 注入正文的判据,
|
|
808
|
+
// 所以「人用过它」不是我们的推测,而是这条消息本身。插件与注入消息都不算(署名必须是人)。
|
|
809
|
+
const quoted = source !== null && typeof source === 'object' && source.kind === 'user' ? invokedSkillNames(event.data) : []
|
|
810
|
+
if (gate === null && quoted.length === 0) return state
|
|
811
|
+
const at = typeof event.time === 'number' ? event.time : Date.now()
|
|
812
|
+
const next = clone(state)
|
|
813
|
+
for (const name of quoted) recordSkillUse(next, name, 'human', at, stepPointer(state))
|
|
814
|
+
if (gate === null) return next
|
|
815
|
+
// 人切运行档:记成一条**人的事实**,当值立刻生效(机制在内核那一侧,见 effectiveAutonomy)。
|
|
816
|
+
if (gate.action === 'set_autonomy') {
|
|
817
|
+
const value = String(gate.value ?? '')
|
|
818
|
+
if (!AUTONOMY_VALUES.includes(value)) return next
|
|
819
|
+
next.autonomy = { ...(next.autonomy ?? {}), override: { value, at } }
|
|
820
|
+
return next
|
|
821
|
+
}
|
|
822
|
+
const fork = next.forks.find((item) => item.id === (gate.fork ?? null))
|
|
823
|
+
if (fork === undefined) return next
|
|
824
|
+
fork.humanDecision = {
|
|
825
|
+
action: gate.action,
|
|
826
|
+
branch: gate.branch ?? null,
|
|
827
|
+
note: gate.note ?? null,
|
|
828
|
+
at,
|
|
829
|
+
}
|
|
830
|
+
return next
|
|
831
|
+
}
|
|
832
|
+
if (event.type === 'tool/call') {
|
|
833
|
+
const data = event.data ?? {}
|
|
834
|
+
if (typeof data.name !== 'string') return state
|
|
835
|
+
const next = clone(state)
|
|
836
|
+
if (data.name === 'skill') {
|
|
837
|
+
// 模型加载技能 = 原生 `skill` 工具的一次调用。工具名与参数形状照原样认,不猜别的写法。
|
|
838
|
+
let args = null
|
|
839
|
+
try {
|
|
840
|
+
args = typeof data.arguments === 'string' ? JSON.parse(data.arguments) : data.arguments
|
|
841
|
+
} catch {
|
|
842
|
+
args = null
|
|
843
|
+
}
|
|
844
|
+
const name = args === null || typeof args !== 'object' || typeof args.name !== 'string' ? '' : args.name.trim()
|
|
845
|
+
if (name !== '') recordSkillUse(next, name, 'model', typeof event.time === 'number' ? event.time : 0, stepPointer(state))
|
|
846
|
+
}
|
|
847
|
+
if (data.name === 'write' || data.name === 'edit') {
|
|
848
|
+
let args = null
|
|
849
|
+
try {
|
|
850
|
+
args = typeof data.arguments === 'string' ? JSON.parse(data.arguments) : data.arguments
|
|
851
|
+
} catch {
|
|
852
|
+
args = null
|
|
853
|
+
}
|
|
854
|
+
const path = args === null || typeof args !== 'object' ? undefined : args.file_path
|
|
855
|
+
if (typeof path === 'string' && path !== '' && !next.written.includes(path)) {
|
|
856
|
+
next.written.push(path)
|
|
857
|
+
if (next.written.length > MAX_WRITTEN) next.written.splice(0, next.written.length - MAX_WRITTEN)
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
next.inFlight = VERDICT_WAITERS.has(data.name) ? { name: data.name, callId: data.callId ?? null, at: typeof event.time === 'number' ? event.time : 0 } : null
|
|
861
|
+
return next
|
|
862
|
+
}
|
|
863
|
+
if (event.type === 'tool/result') {
|
|
864
|
+
const meta = event.data === undefined || event.data === null ? undefined : event.data.meta
|
|
865
|
+
let next = state
|
|
866
|
+
if (meta !== null && meta !== undefined && typeof meta === 'object' && meta.kind === MUTATION_KIND) {
|
|
867
|
+
next = applyMutations(next, Array.isArray(meta.mutations) ? meta.mutations : meta.mutation === undefined ? [] : [meta.mutation])
|
|
868
|
+
}
|
|
869
|
+
if (next.inFlight !== null) {
|
|
870
|
+
const cleared = next === state ? clone(state) : next
|
|
871
|
+
cleared.inFlight = null
|
|
872
|
+
return cleared
|
|
873
|
+
}
|
|
874
|
+
return next
|
|
875
|
+
}
|
|
876
|
+
return state
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// ── 派生:阶段 / 完成度 / 假设状态 / 世界线阶段,全部现算,状态里不存 ──────────
|
|
880
|
+
|
|
881
|
+
export function derive(state) {
|
|
882
|
+
const activePlan = state.plans.find((plan) => plan.status === 'active') ?? null
|
|
883
|
+
const closedPlans = state.plans.filter((plan) => plan.status === 'closed')
|
|
884
|
+
const pendingAudit = state.audits.some((audit) => audit.verdict === null)
|
|
885
|
+
const stepOf = (planId, stepId) => state.plans.find((plan) => plan.id === planId)?.steps.find((step) => step.id === stepId) ?? null
|
|
886
|
+
const levelIndex = (level) => ['L0', 'L1', 'L2', 'L3', 'L4'].indexOf(String(level ?? '').toUpperCase())
|
|
887
|
+
|
|
888
|
+
const hypotheses = state.hypotheses.map((hypothesis) => {
|
|
889
|
+
const rows = []
|
|
890
|
+
for (const item of state.evidence) {
|
|
891
|
+
const step = stepOf(item.plan, item.step)
|
|
892
|
+
if (step?.tests?.hypothesis === hypothesis.id) rows.push(item)
|
|
893
|
+
}
|
|
894
|
+
const refutations = rows.filter((item) => item.verdict === 'refute').length
|
|
895
|
+
const inconclusive = rows.filter((item) => item.verdict === 'inconclusive').length
|
|
896
|
+
const supportedLevel = rows
|
|
897
|
+
.filter((item) => item.verdict === 'support')
|
|
898
|
+
.reduce((best, item) => Math.max(best, levelIndex(item.level)), -1)
|
|
899
|
+
let status = hypothesis.status
|
|
900
|
+
if (status === 'proposed' && rows.length > 0) status = 'alive'
|
|
901
|
+
// 被推翻是黏性终态(§22):「已被替代」不该改写「已被推翻」——两件事。
|
|
902
|
+
if (status !== 'superseded' && status !== 'refuted' && refutations > 0) status = 'refuted'
|
|
903
|
+
return { ...hypothesis, status, supportedLevel: supportedLevel < 0 ? null : `L${supportedLevel}`, refutations, inconclusive }
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* `plan_is_authorized`:有记号 **或** 已经真的推进过(行为即授权)。
|
|
908
|
+
* 第二个分支在 ClearAI 那边是给存量文档用的;这里同样留着——事实与意图冲突时以事实为准。
|
|
909
|
+
*/
|
|
910
|
+
const planIsAuthorized = (plan) =>
|
|
911
|
+
plan !== null && (plan.confirmed_at !== null || plan.steps.some((step) => step.status !== 'open'))
|
|
912
|
+
const planConfirmationPending = activePlan !== null && activePlan.status === 'active' && !planIsAuthorized(activePlan)
|
|
913
|
+
let phase = null
|
|
914
|
+
let progress = null
|
|
915
|
+
if (state.goal !== null) {
|
|
916
|
+
if (state.goal.status === 'achieved' || state.goal.status === 'abandoned') phase = state.goal.status
|
|
917
|
+
else if (state.inFlight !== null) phase = 'auditing' // 交付/结案在飞:正在等裁决
|
|
918
|
+
else if (pendingAudit) phase = 'auditing' // 上一次派出去的裁决还没回来(超时留痕)
|
|
919
|
+
else if (activePlan !== null && activePlan.blocked !== undefined) phase = 'stalled'
|
|
920
|
+
else if (activePlan === null && closedPlans.length === 0) phase = 'planning'
|
|
921
|
+
else if (activePlan !== null && activePlan.steps.every((step) => step.status !== 'open')) phase = 'stage_boundary'
|
|
922
|
+
else if (activePlan !== null) phase = 'executing'
|
|
923
|
+
else phase = 'stage_boundary'
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* 完成度(派生,不存)。
|
|
927
|
+
*
|
|
928
|
+
* 2026-09-11 长测抓到的自相矛盾:目标已经 achieved、两条事实都升格了,卡片却写「完成度 0%」。
|
|
929
|
+
* 两个原因:① 计划一关,活跃计划为 null,口径回落到假设;② 而**升格**(`fact/promoted`)
|
|
930
|
+
* 是一条独立路径,它不改假设状态,于是分子是 0。
|
|
931
|
+
*
|
|
932
|
+
* 两条都修,而且顺序有意义:
|
|
933
|
+
* · **终局优先**:目标已经有终局(achieved/abandoned)时,完成度就是终局该有的样子——
|
|
934
|
+
* 达成 = 1,如实放弃 = 0。中途口径不该覆盖终局,那是拿尺子量已经封箱的东西。
|
|
935
|
+
* · **升格算数**:回落口径里,被升格成事实的假设也算「已确认」——升格本来就是它最硬的确认。
|
|
936
|
+
*/
|
|
937
|
+
const promotedClaims = new Set((state.facts ?? []).map((item) => String(item.text ?? '')))
|
|
938
|
+
const live = activePlan?.steps.filter((step) => step.status !== 'void') ?? []
|
|
939
|
+
if (state.goal?.status === 'achieved') progress = 1
|
|
940
|
+
else if (state.goal?.status === 'abandoned') progress = 0
|
|
941
|
+
else if (live.length > 0) progress = live.filter((step) => step.status === 'advanced').length / live.length
|
|
942
|
+
else {
|
|
943
|
+
const effective = hypotheses.filter((item) => item.status !== 'superseded' && item.status !== 'retracted')
|
|
944
|
+
if (effective.length > 0) {
|
|
945
|
+
const done = effective.filter((item) => item.status === 'confirmed' || promotedClaims.has(String(item.claim ?? ''))).length
|
|
946
|
+
progress = done / effective.length
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* 世界线的**派生状态**(2026-09-11 定的语义,见 recon 的「四条结束方式」):
|
|
953
|
+
*
|
|
954
|
+
* · `failed` —— 执行者没跑成(`worldline/executed{ok:false}`)。**它不是落选**:
|
|
955
|
+
* 落选意味着它跑完了、被尺子排到了后面;失败是「世界没给它机会」。原来的实现在这两件事上
|
|
956
|
+
* 共用一个状态(都还是 exploring),于是界面会一直为一条死掉的线呼吸——那是说假话。
|
|
957
|
+
* · `orphaned` —— 承载它的**步骤被作废**了,而它还没收口。它既不是被人裁掉(没人做过这个决定),
|
|
958
|
+
* 也不是被算术排掉(没有尺子排过它):它是「随承诺撤回而终止」。这是两个已存在事实的**推论**,
|
|
959
|
+
* 不是第五种存储状态——零新账,也**不改写历史**(绝不把它写成「已放弃」)。
|
|
960
|
+
*/
|
|
961
|
+
const forks = state.forks.map((fork) => {
|
|
962
|
+
const owner = (activePlan ?? closedPlans[closedPlans.length - 1] ?? null)?.steps.find((step) => step.id === fork.step) ?? null
|
|
963
|
+
const ownerVoided = owner !== null && owner.status === 'void'
|
|
964
|
+
return {
|
|
965
|
+
...fork,
|
|
966
|
+
ownerStatus: owner === null ? null : owner.status,
|
|
967
|
+
ownerVoidReason: ownerVoided ? (owner.voidReason ?? null) : null,
|
|
968
|
+
orphaned: ownerVoided && !fork.settled && !fork.abandoned,
|
|
969
|
+
phase: fork.abandoned
|
|
970
|
+
? 'abandoned'
|
|
971
|
+
: fork.settled
|
|
972
|
+
? 'settled'
|
|
973
|
+
: ownerVoided
|
|
974
|
+
? 'orphaned'
|
|
975
|
+
: fork.branches.every((branch) => (BRANCH_RANK[branch.status] ?? 0) >= BRANCH_RANK.evaluated)
|
|
976
|
+
? 'deciding'
|
|
977
|
+
: 'exploring',
|
|
978
|
+
branches: fork.branches.map((branch) => ({
|
|
979
|
+
...branch,
|
|
980
|
+
// 派生,不落库:执行没跑成,而状态还停在探索中。
|
|
981
|
+
failed: branch.status === 'exploring' && branch.execution !== null && branch.execution !== undefined && branch.execution.ok === false,
|
|
982
|
+
orphaned: ownerVoided && !fork.settled && !fork.abandoned && branch.status === 'exploring',
|
|
983
|
+
/**
|
|
984
|
+
* **执行者未归**(2026-09-11 长测抓到的第二句假话,与「失败」「孤儿」同一族):
|
|
985
|
+
*
|
|
986
|
+
* 执行者是异步派的,所以「它在跑」是一条会过期的状态。分叉一旦收口(收敛或放弃),
|
|
987
|
+
* 那条世界线就**没有归宿**了——回灌也进不了任何决定。可 `execution.ok` 仍是 `null`,
|
|
988
|
+
* 于是卡片会永远写「(执行中,结论会自动回灌)」:**一句实现了不了的承诺**。
|
|
989
|
+
*
|
|
990
|
+
* 两条已有事实的推论:分叉是终局 + 执行者没报过。零新账,也不改写 `execution` 本身
|
|
991
|
+
* (它仍然是「没报过」这个已发生的事实)。
|
|
992
|
+
*/
|
|
993
|
+
unreturned: (fork.settled === true || fork.abandoned === true) && branch.execution !== null && branch.execution !== undefined && branch.execution.ok === null,
|
|
994
|
+
})),
|
|
995
|
+
}
|
|
996
|
+
})
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* 收件箱:每一道门都是**状态锚**(由派生事实算出,不依赖中断记录)——门一解决,条目自然消失,
|
|
1000
|
+
* 不可能残留成僵尸。
|
|
1001
|
+
* 条目只带**分诊信息**(标题/摘要/指向),不带全部正文:收件箱是分诊,不是问诊。
|
|
1002
|
+
*
|
|
1003
|
+
* 2026-09-11 起这里**只放真门**:不拍板就真的推不动的那种。计划确认不再是条目(它是记号,
|
|
1004
|
+
* 不是闸门;见 HUMAN_GATE_ACTIONS 的收敛记录)——收件箱里的每一条都经得起「等人是必须的吗」。
|
|
1005
|
+
*/
|
|
1006
|
+
const inbox = []
|
|
1007
|
+
for (const fork of forks) {
|
|
1008
|
+
// 人已经裁决过的不再等他(ClearAI 的条目是状态锚:状态一变,条目自然消失)。
|
|
1009
|
+
if (fork.phase !== 'deciding' || (fork.humanDecision ?? null) !== null) continue
|
|
1010
|
+
const label = fork.branches.find((branch) => branch.id === fork.recommended)?.label ?? null
|
|
1011
|
+
inbox.push({
|
|
1012
|
+
kind: 'fork_adopt',
|
|
1013
|
+
title: '世界线裁决',
|
|
1014
|
+
summary: `${fork.question} · ${fork.branches.length} 条世界线探索完毕,待采纳一条${label === null ? '' : ` · 推荐★${label}`}`,
|
|
1015
|
+
plan: fork.plan ?? null,
|
|
1016
|
+
step: fork.step,
|
|
1017
|
+
human_action: 'adopt_branch',
|
|
1018
|
+
/** 门要什么:**点击**(有白名单动词)还是**一句话**(语义判断归模型)。 */
|
|
1019
|
+
needs: 'click',
|
|
1020
|
+
})
|
|
1021
|
+
}
|
|
1022
|
+
for (const fork of forks) {
|
|
1023
|
+
if (fork.merge?.provisional !== true) continue
|
|
1024
|
+
inbox.push({
|
|
1025
|
+
kind: 'provisional_review',
|
|
1026
|
+
title: '临时采纳待复核',
|
|
1027
|
+
summary: `${fork.question} · 已**临时**采纳「${fork.branches.find((branch) => branch.id === fork.merge.branch)?.label ?? fork.merge.branch}」:${fork.merge.decisionNote ?? '分差不足以称结论'}`,
|
|
1028
|
+
plan: fork.plan ?? null,
|
|
1029
|
+
step: fork.step,
|
|
1030
|
+
// 复核不是一次点击:要么认可(什么都不用做),要么让人/模型改判据重做一条。
|
|
1031
|
+
human_action: null,
|
|
1032
|
+
/**
|
|
1033
|
+
* §37:它是**要一句话**的门 ✗ —— 原先"认可就什么都不用做"的写法有个洞:
|
|
1034
|
+
* 门开着 ⇒ 续跑停着(`hasOpenGate`)⇒ 什么都不做的话,系统就一直等 ✗。
|
|
1035
|
+
* 所以人得**说一声**;界面上必须把这件事说出来(以前既没按钮也没提示 ✗)。
|
|
1036
|
+
*/
|
|
1037
|
+
needs: 'word',
|
|
1038
|
+
ask: '认可就说一句话(比如「认可,继续」);要改判据就重做一条',
|
|
1039
|
+
})
|
|
1040
|
+
}
|
|
1041
|
+
for (const candidate of state.brainCandidates ?? []) {
|
|
1042
|
+
inbox.push({
|
|
1043
|
+
kind: 'skill_candidate',
|
|
1044
|
+
title: `候选技能:${candidate.name}`,
|
|
1045
|
+
summary: `${String(candidate.description ?? '').slice(0, 120)}——模型自己写的 SOP,采纳后才进它的技能目录。`,
|
|
1046
|
+
plan: null,
|
|
1047
|
+
step: null,
|
|
1048
|
+
human_action: 'promote_skill',
|
|
1049
|
+
skill: candidate.name,
|
|
1050
|
+
needs: 'click',
|
|
1051
|
+
})
|
|
1052
|
+
}
|
|
1053
|
+
if (activePlan !== null && activePlan.blocked !== undefined) {
|
|
1054
|
+
inbox.push({
|
|
1055
|
+
kind: 'plan_blocked',
|
|
1056
|
+
title: '计划被拦',
|
|
1057
|
+
summary: `连续 ${activePlan.blocked.attempts} 次未过观测准入:${activePlan.blocked.reason}`,
|
|
1058
|
+
plan: activePlan.id,
|
|
1059
|
+
step: activePlan.blocked.step ?? null,
|
|
1060
|
+
// 「解除阻塞」不是一次点击能表达的事(要改计划或改判据),所以它没有人门动作:
|
|
1061
|
+
// 人说一句「按 X 改」,语义判断归模型(ClearAI 删掉 NL 白名单的同一条理由)。
|
|
1062
|
+
human_action: null,
|
|
1063
|
+
needs: 'word',
|
|
1064
|
+
ask: '说一句怎么改(改计划 / 补判据),语义判断归模型',
|
|
1065
|
+
})
|
|
1066
|
+
}
|
|
1067
|
+
// `has_open_gate`:有一道门开着——调度侧据此不驱动(ClearAI:两个谓词同集)。
|
|
1068
|
+
const hasOpenGate = inbox.length > 0
|
|
1069
|
+
|
|
1070
|
+
const settlement = state.evidence.map((item) => {
|
|
1071
|
+
const step = stepOf(item.plan, item.step)
|
|
1072
|
+
const refs = state.materials.filter((material) => item.refs.includes(material.id)).map((material) => material.ref)
|
|
1073
|
+
const anchors = item.refs.filter((ref) => !ref.startsWith('m-'))
|
|
1074
|
+
return {
|
|
1075
|
+
stepId: item.step,
|
|
1076
|
+
intent: step?.done_criteria ?? '—',
|
|
1077
|
+
fact: [...refs, ...anchors].join(', ') || '—',
|
|
1078
|
+
evaluator: item.evaluator,
|
|
1079
|
+
delta: item.verdict === 'support' ? (item.basis ?? '—') : `${item.verdict}:${item.basis ?? '—'}`,
|
|
1080
|
+
}
|
|
1081
|
+
})
|
|
1082
|
+
|
|
1083
|
+
return { phase, progress, hypotheses, activePlan, closedPlans, pendingAudit, forks, settlement, stepOf, inbox, hasOpenGate, planConfirmationPending, planIsAuthorized }
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* 人门动作的标记。
|
|
1088
|
+
*
|
|
1089
|
+
* 人在面板上按的每一个动作,都经宿主平面的路由变成**一条用户消息**进会话日志——
|
|
1090
|
+
* 内容是一行结构化标记,不是自然语言。为什么要这样:
|
|
1091
|
+
* · **可审计**:谁在什么时候做了什么(采纳哪条世界线、扶正哪个技能、把档切成什么),
|
|
1092
|
+
* 就在日志里(`source.kind==='user'`);
|
|
1093
|
+
* · **不做 NLU**:ClearAI 把确认短语白名单整体删掉了,理由是「语义判断只归模型,
|
|
1094
|
+
* harness 只做顺序可判定的题」。标记进得来、自然语言进不来,是同一条纪律;
|
|
1095
|
+
* · **agent 不可达**:这些动词**没有工具 schema**——模型能调的工具面里不存在它们,
|
|
1096
|
+
* 它只能看见「人做了什么」这条事实。
|
|
1097
|
+
*
|
|
1098
|
+
* 2026-09-11 收敛(奥卡姆,用户拍板):动词表从 5 个缩到 4 个,砍的两个都是**重复**:
|
|
1099
|
+
* · `confirm_plan` —— 原生 `dsh-plan-mode` 就是「用户复核的出口」;而我们自己的
|
|
1100
|
+
* `planIsAuthorized` 本来就承认「交付第一步即授权」。计划确认从来不是闸门(它是记号),
|
|
1101
|
+
* 少一个假装成闸门的按钮,界面就不再暗示一条不存在的约束。需要人拍板时用原生
|
|
1102
|
+
* `ask_user_question`(它同样把人的答复留在日志里,署名一样是人)。
|
|
1103
|
+
* · `invoke_skill` —— 原生 `/` 技能触发器(`dsh-client-ui-skill` 注册 trigger `/`,
|
|
1104
|
+
* `dsh-client-ui-input-trigger` 出候选菜单)做的正是同一件事,而且带候选菜单。
|
|
1105
|
+
* 留下的四个各有原生没有的职责:
|
|
1106
|
+
* · `adopt_branch`/`abandon_fork` —— `ConvergeFork` 只认「算术」或「人门」两条路,
|
|
1107
|
+
* 算不出来时这是唯一的结构化人裁决通道(不做 NLU 是纪律,不是懒);
|
|
1108
|
+
* · `promote_skill` —— 候选技能扶正是**只有人能触发**的写动作(模型不能自举);
|
|
1109
|
+
* · `set_autonomy` —— 「我现在在不在场」只有人知道,档是人写的事实,不是系统猜的。
|
|
1110
|
+
*/
|
|
1111
|
+
export const HUMAN_GATE_MARK = '[clearai·人门]'
|
|
1112
|
+
/** 面板上允许出现的动词。表外的动词一律拒(与贡献表同一套「表外的名字不许出现」)。 */
|
|
1113
|
+
export const HUMAN_GATE_ACTIONS = ['adopt_branch', 'abandon_fork', 'promote_skill']
|
|
1114
|
+
/** 运行档的两个取值(`set_autonomy` 的合法参数)。 */
|
|
1115
|
+
export const AUTONOMY_VALUES = ['attended', 'unattended']
|
|
1116
|
+
|
|
1117
|
+
/** 从一条用户消息里认出人门标记;不是标记就返回 null。 */
|
|
1118
|
+
export function parseHumanGate(message) {
|
|
1119
|
+
if (message === null || typeof message !== 'object') return null
|
|
1120
|
+
// 必须**署名是人**:插件/模型来源的同名标记不算人门动作。这条不是洁癖——
|
|
1121
|
+
// 内核也会往会话里写带 section 的插件消息,不区分来源就等于给了它一条伪造人意的路。
|
|
1122
|
+
if (message.source === null || typeof message.source !== 'object' || message.source.kind !== 'user') return null
|
|
1123
|
+
const blocks = Array.isArray(message.content) ? message.content : []
|
|
1124
|
+
const first = blocks.find((block) => block?.type === 'text' && typeof block.text === 'string')
|
|
1125
|
+
if (first === undefined || !first.text.startsWith(HUMAN_GATE_MARK)) return null
|
|
1126
|
+
const rest = first.text.slice(HUMAN_GATE_MARK.length).trim()
|
|
1127
|
+
const line = rest.split('\n')[0].trim()
|
|
1128
|
+
try {
|
|
1129
|
+
const parsed = JSON.parse(line)
|
|
1130
|
+
if (parsed === null || typeof parsed !== 'object') return null
|
|
1131
|
+
if (!HUMAN_GATE_ACTIONS.includes(String(parsed.action))) return null
|
|
1132
|
+
return parsed
|
|
1133
|
+
} catch {
|
|
1134
|
+
return null
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* 技能面:合并目录(内核发的)+ 本会话用量(从日志折的)。
|
|
1140
|
+
*
|
|
1141
|
+
* 用量**不做跨会话统计**(第一性原理:判据混杂、没有决策者据此行动,
|
|
1142
|
+
* 更好的原语是「一次具体实例 + 可点开的指针」)。这里给的正是那个指针:谁在哪一步加载的、
|
|
1143
|
+
* 那一步**现在**是什么结果——现算,所以技能用完那一步后来又交付了,这里也跟着变。
|
|
1144
|
+
*/
|
|
1145
|
+
function skillUsageView(state) {
|
|
1146
|
+
const usage = state.skillUsage ?? {}
|
|
1147
|
+
return Object.values(usage)
|
|
1148
|
+
.map((entry) => {
|
|
1149
|
+
const events = Array.isArray(entry.events) ? entry.events : []
|
|
1150
|
+
const last = events.length === 0 ? null : events[events.length - 1]
|
|
1151
|
+
const plan = last === null ? null : (state.plans.find((item) => item.id === last.plan) ?? null)
|
|
1152
|
+
const step = plan === null || last.step === null ? null : (plan.steps.find((item) => item.id === last.step) ?? null)
|
|
1153
|
+
return {
|
|
1154
|
+
name: entry.name,
|
|
1155
|
+
model: entry.model ?? 0,
|
|
1156
|
+
human: entry.human ?? 0,
|
|
1157
|
+
lastAt: entry.lastAt ?? null,
|
|
1158
|
+
last:
|
|
1159
|
+
last === null
|
|
1160
|
+
? null
|
|
1161
|
+
: {
|
|
1162
|
+
by: last.by,
|
|
1163
|
+
at: last.at ?? null,
|
|
1164
|
+
plan: last.plan ?? null,
|
|
1165
|
+
step: last.step ?? null,
|
|
1166
|
+
ordinal: last.ordinal ?? null,
|
|
1167
|
+
// 指针落在的这一步现在的结果(现算):advanced / open / void
|
|
1168
|
+
outcome: step === null ? null : step.status,
|
|
1169
|
+
evidence: step === null ? null : (step.evidence ?? null),
|
|
1170
|
+
stepDo: step === null ? null : step.do,
|
|
1171
|
+
},
|
|
1172
|
+
}
|
|
1173
|
+
})
|
|
1174
|
+
.sort((left, right) => String(left.name).localeCompare(String(right.name)))
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/** 面板契约(DESIGN.md §3.2)。浏览器读的就是这个,一字不改。 */
|
|
1178
|
+
export function view(state, sessionId) {
|
|
1179
|
+
const derived = derive(state)
|
|
1180
|
+
const plan = derived.activePlan ?? derived.closedPlans[derived.closedPlans.length - 1] ?? null
|
|
1181
|
+
const sid = sessionId === undefined || sessionId === null ? (state.sessionId ?? null) : String(sessionId)
|
|
1182
|
+
return {
|
|
1183
|
+
ok: true,
|
|
1184
|
+
mounted: true,
|
|
1185
|
+
sessionId: sid,
|
|
1186
|
+
/** 收件箱(面板「需要你 N」的数据面):只有分诊信息,正文在各自的视图里。 */
|
|
1187
|
+
inbox: derived.inbox,
|
|
1188
|
+
hasOpenGate: derived.hasOpenGate,
|
|
1189
|
+
/**
|
|
1190
|
+
* 运行档:`value` = 当档(内核下发的那份)、`source` 说明它从哪来(部署预设 / 人在面板上切的)。
|
|
1191
|
+
* 面板画开关与卡片写事实都读这里 —— 投影是唯一真相。
|
|
1192
|
+
*/
|
|
1193
|
+
/** 项目章程的读数(面板「技能 · 记忆」页签顶部那一行;点开走原生预览)。 */
|
|
1194
|
+
constitution: state.constitution ?? null,
|
|
1195
|
+
/**
|
|
1196
|
+
* 被闸门裁断过几次(`block/counted`):世界树的**分段通道**画的就是它——
|
|
1197
|
+
* 这一步磨了几轮、其中几次被驳回。事实在投影里,面板只负责画。
|
|
1198
|
+
*/
|
|
1199
|
+
blocks: state.blocks ?? {},
|
|
1200
|
+
/**
|
|
1201
|
+
* **人放行**(`human/released`)的痕迹:每一条都带它绑在哪条轴上
|
|
1202
|
+
* (`step` / `branch`)以及凭据(`via:'approval'`,原生审批栈的权威记录)。
|
|
1203
|
+
* 面板与测试都读这里 —— 推断出来的放行不该有痕迹,所以这份读数本身就是判据。
|
|
1204
|
+
*/
|
|
1205
|
+
releases: (state.releases ?? []).map((item) => ({ step: item.step ?? null, plan: item.plan ?? null, fork: item.fork ?? null, branch: item.branch ?? null, via: item.via ?? null, call: item.call ?? null, at: item.at ?? null })),
|
|
1206
|
+
autonomy: {
|
|
1207
|
+
value: state.autonomy?.effective?.value ?? null,
|
|
1208
|
+
preset: state.autonomy?.effective?.preset ?? null,
|
|
1209
|
+
source: state.autonomy?.effective?.source ?? null,
|
|
1210
|
+
override: state.autonomy?.override ?? null,
|
|
1211
|
+
},
|
|
1212
|
+
brain: state.brain ?? null,
|
|
1213
|
+
/** 本体形状(面板页眉据此生成,不手抄)。 */
|
|
1214
|
+
ontology: state.ontology ?? null,
|
|
1215
|
+
/**
|
|
1216
|
+
* 续跑窗口的账(§17):面板读它,于是「续跑停着——等裁决」这种**平台说不出来的话**
|
|
1217
|
+
* 有地方说。轮数与相位仍然只在原生 dock 上出现(一个事实只在**一块**面上说),
|
|
1218
|
+
* 这里交出去的只有:状态、它服务的事实对象、以及停下来的真实理由。
|
|
1219
|
+
*/
|
|
1220
|
+
continuation:
|
|
1221
|
+
state.continuation === null || state.continuation === undefined
|
|
1222
|
+
? null
|
|
1223
|
+
: {
|
|
1224
|
+
state: state.continuation.state,
|
|
1225
|
+
goal: state.continuation.goal ?? null,
|
|
1226
|
+
target: state.continuation.target ?? null,
|
|
1227
|
+
why: state.continuation.why ?? null,
|
|
1228
|
+
label: state.continuation.label ?? null,
|
|
1229
|
+
},
|
|
1230
|
+
/** 技能面:合并目录 + 本会话用量(「技能 · 记忆」页签的数据面)。 */
|
|
1231
|
+
skills: { catalog: state.skillCatalog ?? null, usage: skillUsageView(state) },
|
|
1232
|
+
inFlight: state.inFlight === undefined ? null : state.inFlight,
|
|
1233
|
+
written: Array.isArray(state.written) ? state.written.length : 0,
|
|
1234
|
+
goal:
|
|
1235
|
+
state.goal === null
|
|
1236
|
+
? null
|
|
1237
|
+
: {
|
|
1238
|
+
id: state.goal.id,
|
|
1239
|
+
claim: state.goal.claim,
|
|
1240
|
+
doneCriteria: state.goal.done_criteria,
|
|
1241
|
+
promoteAtLevel: state.goal.promote_at_level,
|
|
1242
|
+
status: state.goal.status,
|
|
1243
|
+
phase: derived.phase,
|
|
1244
|
+
progress: derived.progress,
|
|
1245
|
+
revision: state.goal.revision,
|
|
1246
|
+
hypotheses: derived.hypotheses.map((hypothesis) => ({
|
|
1247
|
+
id: hypothesis.id,
|
|
1248
|
+
claim: hypothesis.claim,
|
|
1249
|
+
refuteWhen: hypothesis.refute_when,
|
|
1250
|
+
status: hypothesis.status,
|
|
1251
|
+
supportedLevel: hypothesis.supportedLevel,
|
|
1252
|
+
refutations: hypothesis.refutations,
|
|
1253
|
+
inconclusive: hypothesis.inconclusive,
|
|
1254
|
+
version: hypothesis.version,
|
|
1255
|
+
})),
|
|
1256
|
+
},
|
|
1257
|
+
/**
|
|
1258
|
+
* **全部计划**(§32):一个对话会有多个世界树。活动的那份照旧在 `plan` 里,
|
|
1259
|
+
* 已收尾的留在这里 —— 它们没有被删(文档也归档在 `clear/goals/plans/<id>.md`),
|
|
1260
|
+
* 只是原先**没有入口**切回去看 ✗。
|
|
1261
|
+
*/
|
|
1262
|
+
plans: state.plans.map((item) => ({
|
|
1263
|
+
id: item.id,
|
|
1264
|
+
status: item.status,
|
|
1265
|
+
brief: item.brief ?? '',
|
|
1266
|
+
closedAt: item.closedAt ?? null,
|
|
1267
|
+
stepCount: (item.steps ?? []).length,
|
|
1268
|
+
steps: (item.steps ?? []).map((step) => ({
|
|
1269
|
+
id: step.id,
|
|
1270
|
+
ordinal: step.ordinal,
|
|
1271
|
+
do: step.do,
|
|
1272
|
+
artifacts: (step.artifacts ?? []).map((artifact) => (typeof artifact === 'string' ? { path: artifact, exists: null } : artifact)),
|
|
1273
|
+
doneCriteria: step.done_criteria,
|
|
1274
|
+
tests: step.tests,
|
|
1275
|
+
status: step.status,
|
|
1276
|
+
evidenceId: step.evidence,
|
|
1277
|
+
advancedAt: step.advancedAt,
|
|
1278
|
+
voidReason: step.voidReason,
|
|
1279
|
+
})),
|
|
1280
|
+
})),
|
|
1281
|
+
plan:
|
|
1282
|
+
plan === null
|
|
1283
|
+
? null
|
|
1284
|
+
: {
|
|
1285
|
+
id: plan.id,
|
|
1286
|
+
status: plan.status,
|
|
1287
|
+
/** 计划自己的一句话(模型建计划时写的)。**给人看的名字**用它,内部 id 退回 tooltip(§18)。 */
|
|
1288
|
+
brief: plan.brief ?? '',
|
|
1289
|
+
blocked: plan.blocked ?? null,
|
|
1290
|
+
// 授权记号(ClearAI `plan.confirmed_at` / `confirmed_by`):面板据此显示计划门。
|
|
1291
|
+
confirmedAt: plan.confirmed_at ?? null,
|
|
1292
|
+
confirmedBy: plan.confirmed_by ?? null,
|
|
1293
|
+
confirmationPending: derived.planConfirmationPending && plan === derived.activePlan,
|
|
1294
|
+
steps: plan.steps.map((step) => ({
|
|
1295
|
+
id: step.id,
|
|
1296
|
+
ordinal: step.ordinal,
|
|
1297
|
+
do: step.do,
|
|
1298
|
+
/**
|
|
1299
|
+
* **形状归一**:声明里 `artifacts` 是字符串(计划里写下的路径),
|
|
1300
|
+
* 而面板按对象读 ⇒ 真数据里树详情显示 `undefined(缺)` ✗。
|
|
1301
|
+
* `exists: null` 的意思是**没查过**(折法不碰盘);只有宿主路由 stat 过才会是 true/false。
|
|
1302
|
+
*/
|
|
1303
|
+
artifacts: (step.artifacts ?? []).map((artifact) => (typeof artifact === 'string' ? { path: artifact, exists: null } : artifact)),
|
|
1304
|
+
doneCriteria: step.done_criteria,
|
|
1305
|
+
tests: step.tests,
|
|
1306
|
+
status: step.status,
|
|
1307
|
+
evidenceId: step.evidence,
|
|
1308
|
+
advancedAt: step.advancedAt,
|
|
1309
|
+
voidReason: step.voidReason,
|
|
1310
|
+
})),
|
|
1311
|
+
advancedCount: plan.steps.filter((step) => step.status === 'advanced').length,
|
|
1312
|
+
totalCount: plan.steps.filter((step) => step.status !== 'void').length,
|
|
1313
|
+
},
|
|
1314
|
+
/**
|
|
1315
|
+
* **跨计划的步骤索引**(§25):步骤 id → 它的判据/归属。
|
|
1316
|
+
*
|
|
1317
|
+
* 为什么必须有它(真数据踩出来的):计划会改版、收尾、重开——**命题的验证步常常留在旧计划里**
|
|
1318
|
+
* (实测:一场里两条计划,命题的 `tests` 全在已收尾的那条上,而 `view.plan` 只交当前活动的那条)
|
|
1319
|
+
* ⇒ 只查活动计划时,证据→步骤→命题这条链**全断**,面板上五个命题的证据与判者全显示「—」。
|
|
1320
|
+
* 索引是纯派生,不新增存储。
|
|
1321
|
+
*/
|
|
1322
|
+
stepIndex: Object.fromEntries(
|
|
1323
|
+
state.plans.flatMap((item) =>
|
|
1324
|
+
item.steps.map((step) => [
|
|
1325
|
+
step.id,
|
|
1326
|
+
{ plan: item.id, planStatus: item.status, tests: step.tests ?? null, do: step.do, status: step.status },
|
|
1327
|
+
]),
|
|
1328
|
+
),
|
|
1329
|
+
),
|
|
1330
|
+
evidence: state.evidence.map((item) => ({
|
|
1331
|
+
id: item.id,
|
|
1332
|
+
stepId: item.step,
|
|
1333
|
+
planId: item.plan,
|
|
1334
|
+
/**
|
|
1335
|
+
* `branch` 与 `anchor` **必须交出去**(§24 修):面板要给每条证据指一个**出处**——
|
|
1336
|
+
* 独立证据指评估卡(文件)、自判证据指它锚定的产物。少了这两个字段,
|
|
1337
|
+
* 「评估卡」那一项永远出不来(实测:世界线证据的评估卡按 `分支` 归属,
|
|
1338
|
+
* 与证据的 `stepId` 不是同一个 id,只看 stepId 会对不上)。
|
|
1339
|
+
*/
|
|
1340
|
+
branch: item.branch ?? null,
|
|
1341
|
+
anchor: item.anchor ?? 'artifact',
|
|
1342
|
+
/** 记账时定下的出处(§27b)。旧日志没有这个字段 ⇒ 客户端走只读回退。 */
|
|
1343
|
+
origins: item.origins ?? [],
|
|
1344
|
+
basisReviewable: item.basis_reviewable !== false,
|
|
1345
|
+
verdict: item.verdict,
|
|
1346
|
+
level: item.level,
|
|
1347
|
+
evaluator: item.evaluator,
|
|
1348
|
+
basis: item.basis,
|
|
1349
|
+
refs: item.refs,
|
|
1350
|
+
at: item.at,
|
|
1351
|
+
})),
|
|
1352
|
+
audits: state.audits.map((item) => ({
|
|
1353
|
+
id: item.id,
|
|
1354
|
+
stepId: item.step,
|
|
1355
|
+
verdict: item.verdict,
|
|
1356
|
+
cardPath: item.card_path,
|
|
1357
|
+
evaluator: item.evaluator,
|
|
1358
|
+
// 评估者子会话:面板的**旁观入口**(只读旁观,不是接管)。
|
|
1359
|
+
evaluatorSession: item.child ?? null,
|
|
1360
|
+
capability: item.capability ?? null,
|
|
1361
|
+
at: item.at,
|
|
1362
|
+
})),
|
|
1363
|
+
materials: state.materials.map((item) => ({ id: item.id, ref: item.ref, source: item.source, digest: item.digest, note: item.note ?? null, at: item.at })),
|
|
1364
|
+
facts: state.facts.map((item) => ({
|
|
1365
|
+
id: item.id,
|
|
1366
|
+
text: item.text,
|
|
1367
|
+
/** 边界(推翻条件)与支持等级:面板与货架都要显示它——「已知」必须带边界。 */
|
|
1368
|
+
scope: item.scope ?? null,
|
|
1369
|
+
level: item.level ?? null,
|
|
1370
|
+
evidenceIds: item.evidence,
|
|
1371
|
+
path: item.path,
|
|
1372
|
+
at: item.at,
|
|
1373
|
+
})),
|
|
1374
|
+
/**
|
|
1375
|
+
* 侦察记录:**结局要能看出来**。
|
|
1376
|
+
*
|
|
1377
|
+
* 2026-09-11 用户实测:中断一次跑动之后「状态上全部显示执行完成」,而 DSH 其实报了失败。
|
|
1378
|
+
* 两处都修了:内核改成按 `stopReason` 落账(aborted/error 不算完成),这里把 `note` 交出去
|
|
1379
|
+
* 并派生成三态——跑着 / 正常回灌 / 没正常结束。面板与资料面据此分诊,而不是一律当"完成"。
|
|
1380
|
+
*/
|
|
1381
|
+
scouts: (state.scouts ?? []).map((item) => ({
|
|
1382
|
+
id: item.id,
|
|
1383
|
+
stepId: item.step,
|
|
1384
|
+
trigger: item.trigger,
|
|
1385
|
+
child: item.child,
|
|
1386
|
+
capability: item.capability,
|
|
1387
|
+
digest: item.digest ?? null,
|
|
1388
|
+
conclusion: item.conclusion,
|
|
1389
|
+
at: item.at,
|
|
1390
|
+
note: item.note ?? null,
|
|
1391
|
+
status: item.conclusion === null || item.conclusion === undefined ? 'running' : item.note === null || item.note === undefined ? 'settled' : 'failed',
|
|
1392
|
+
})),
|
|
1393
|
+
settlement: derived.settlement,
|
|
1394
|
+
forks: derived.forks.map((fork) => ({
|
|
1395
|
+
tier: fork.tier ?? null,
|
|
1396
|
+
degradedReason: fork.degradedReason ?? null,
|
|
1397
|
+
merge: fork.merge ?? null,
|
|
1398
|
+
mergeConflict: fork.mergeConflict ?? null,
|
|
1399
|
+
/** 临时采纳:分差不足以称结论(或由判断而来)→ 面板要给「待复核」标记。 */
|
|
1400
|
+
provisional: fork.merge?.provisional === true,
|
|
1401
|
+
decisionNote: fork.merge?.decisionNote ?? null,
|
|
1402
|
+
/** 人在面板上的裁决(结构化记录,by:'user'):内核的工具据此落实它。 */
|
|
1403
|
+
humanDecision: fork.humanDecision ?? null,
|
|
1404
|
+
id: fork.id,
|
|
1405
|
+
stepId: fork.step,
|
|
1406
|
+
question: fork.question,
|
|
1407
|
+
phase: fork.phase,
|
|
1408
|
+
decided: fork.settled,
|
|
1409
|
+
abandonReason: fork.abandonReason,
|
|
1410
|
+
/** 承载它的步骤现在是什么状态(void = 承诺已撤回)——面板据此说「随步骤作废而终止」。 */
|
|
1411
|
+
ownerStatus: fork.ownerStatus ?? null,
|
|
1412
|
+
ownerVoidReason: fork.ownerVoidReason ?? null,
|
|
1413
|
+
/** 派生:步骤作废 + 还没收口。不是新状态,是两个事实的推论(见 derive 里的说明)。 */
|
|
1414
|
+
orphaned: fork.orphaned === true,
|
|
1415
|
+
decideBy: { metric: fork.decide_by?.metric ?? null, direction: fork.decide_by?.direction ?? null },
|
|
1416
|
+
verdict: fork.verdict,
|
|
1417
|
+
undecidable: fork.undecidable,
|
|
1418
|
+
arbitration: fork.arbitration ?? null,
|
|
1419
|
+
arbitrationSession: fork.arbitrationSession ?? null,
|
|
1420
|
+
branches: fork.branches.map((branch) => ({
|
|
1421
|
+
id: branch.id,
|
|
1422
|
+
label: branch.label,
|
|
1423
|
+
approach: branch.approach,
|
|
1424
|
+
doneCriteria: branch.done_criteria,
|
|
1425
|
+
workspace: branch.workspace,
|
|
1426
|
+
level: branch.level,
|
|
1427
|
+
status: branch.status,
|
|
1428
|
+
reading: branch.reading,
|
|
1429
|
+
validity: branch.validity,
|
|
1430
|
+
/** 派生:执行没跑成(`execution.ok === false`)。**它不是落选**——面板要分开说。 */
|
|
1431
|
+
failed: branch.failed === true,
|
|
1432
|
+
orphaned: branch.orphaned === true,
|
|
1433
|
+
/** 派生:分叉已收口而执行者没报过 ⇒ 那句「结论会自动回灌」作废(见 derive 里的说明)。 */
|
|
1434
|
+
unreturned: branch.unreturned === true,
|
|
1435
|
+
execution: branch.execution ?? null,
|
|
1436
|
+
verdict: branch.verdict,
|
|
1437
|
+
basis: branch.basis,
|
|
1438
|
+
evaluator: branch.evaluator,
|
|
1439
|
+
execution: branch.execution ?? null,
|
|
1440
|
+
cardPath: branch.card_path ?? null,
|
|
1441
|
+
evaluatorSession: branch.evaluator_session ?? null,
|
|
1442
|
+
gitBranch: branch.git_branch ?? null,
|
|
1443
|
+
worktreePath: branch.worktree_path ?? null,
|
|
1444
|
+
worktreeRemoved: branch.worktree_removed === true,
|
|
1445
|
+
keptRef: branch.kept_ref !== false,
|
|
1446
|
+
at: branch.at,
|
|
1447
|
+
})),
|
|
1448
|
+
})),
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/** 运行态卡:每回合由系统重算,注给模型的**事实**。 */
|
|
1453
|
+
export function renderCard(state) {
|
|
1454
|
+
const derived = derive(state)
|
|
1455
|
+
const goal = state.goal
|
|
1456
|
+
const plan = derived.activePlan
|
|
1457
|
+
const time = (() => {
|
|
1458
|
+
const d = new Date()
|
|
1459
|
+
const pad = (n) => String(n).padStart(2, '0')
|
|
1460
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
|
1461
|
+
})()
|
|
1462
|
+
const lines = ['【运行态卡 · Harness-owned state】', `- 时间:${time}(秒级对决策没有信息量,却会让缓存断裂——真需要精确时间用 bash date)`]
|
|
1463
|
+
/**
|
|
1464
|
+
* 运行档:它是**人的处境**,不是模型的判断对象。写清楚它从哪来,模型就不必猜
|
|
1465
|
+
* 「现在有没有人在旁边」——这一档决定续跑是驱动还是等人、立约是否即授权。
|
|
1466
|
+
*/
|
|
1467
|
+
const autonomy = state.autonomy?.effective ?? null
|
|
1468
|
+
if (autonomy !== null) {
|
|
1469
|
+
lines.push(
|
|
1470
|
+
/**
|
|
1471
|
+
* §34 之后档位**不再是面板上的开关**:它只是部署预设写的初值。
|
|
1472
|
+
* 所以卡片如实说"这件事是怎么配的",不再承诺"你可以在面板上切"(那是已删的能力 ✗)。
|
|
1473
|
+
*/
|
|
1474
|
+
`- 运行档:${autonomy.value === 'unattended' ? '无人值守' : '人在场'}(部署预设写的;它不是"要不要人参与"的开关——要不要人由**门**决定:计划待确认/等裁决/有人在等)`,
|
|
1475
|
+
)
|
|
1476
|
+
}
|
|
1477
|
+
if (goal === null) {
|
|
1478
|
+
lines.push('- 当前目标:未立(SetGoal 需要一份「怎样算回答了」的判据)')
|
|
1479
|
+
} else {
|
|
1480
|
+
const progress = derived.progress === null ? '无法计算' : `${Math.round(derived.progress * 100)}%`
|
|
1481
|
+
lines.push(`- 当前目标(${goal.id} · rev${goal.revision} · ${goal.status}):${goal.claim}`)
|
|
1482
|
+
lines.push(`- 判据:${goal.done_criteria}`)
|
|
1483
|
+
lines.push(`- 阶段(派生):${derived.phase} · 完成度(派生):${progress}`)
|
|
1484
|
+
}
|
|
1485
|
+
if (derived.hypotheses.length > 0) {
|
|
1486
|
+
lines.push('- 假设状态(由证据算出):')
|
|
1487
|
+
for (const hypothesis of derived.hypotheses) {
|
|
1488
|
+
lines.push(
|
|
1489
|
+
` · ${hypothesis.id} [${hypothesis.status}] ${hypothesis.claim} — 推翻条件:${hypothesis.refute_when}` +
|
|
1490
|
+
`(支持到 ${hypothesis.supportedLevel ?? '—'} · 推翻 ${hypothesis.refutations} · 无法判定 ${hypothesis.inconclusive})`,
|
|
1491
|
+
)
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
if (plan === null) {
|
|
1495
|
+
lines.push('- 当前计划:无活动计划')
|
|
1496
|
+
} else {
|
|
1497
|
+
if (derived.planConfirmationPending) {
|
|
1498
|
+
/**
|
|
1499
|
+
* 2026-09-11 起这里只说**事实**,不再带「先引导确认」那句劝告。
|
|
1500
|
+
*
|
|
1501
|
+
* 为什么:确认计划那道门砍了(见 HUMAN_GATE_ACTIONS 的收敛记录)——它本来就不是闸门,
|
|
1502
|
+
* 而那句劝告会让人(和模型)以为有一条必须走的路。现在这句只交代机理:
|
|
1503
|
+
* 记号没落账,但**授权不会因此缺席**——交付第一步就按事实补写。
|
|
1504
|
+
*/
|
|
1505
|
+
lines.push('- plan_confirmation_pending: true')
|
|
1506
|
+
lines.push('- confirmed_at:(null — 授权记号未落账;第一次交付会按事实补写(行为即授权)。不需要任何人先按什么)')
|
|
1507
|
+
} else if (plan.confirmed_at === null) {
|
|
1508
|
+
// 记号没落账,但计划事实上已经推进过:授权已经发生。照旧写「等人确认」会与上一行
|
|
1509
|
+
// 当场矛盾,而同一段里两句打架的话比一句错话更糟(ClearAI 的原话)。
|
|
1510
|
+
lines.push('- plan_confirmation_pending: false')
|
|
1511
|
+
lines.push('- confirmed_at:(null,但本计划已推进过——授权已经发生,继续执行)')
|
|
1512
|
+
} else {
|
|
1513
|
+
lines.push(`- confirmed_at:${plan.confirmed_at}(${plan.confirmed_by === 'user' ? '人显式确认' : plan.confirmed_by === 'progress' ? '据推进事实补写' : String(plan.confirmed_by)})`)
|
|
1514
|
+
}
|
|
1515
|
+
lines.push(`- 当前计划(${plan.id}${plan.goal === null ? '' : ` · 目标 ${plan.goal} 的一个阶段`})步骤:`)
|
|
1516
|
+
for (const step of plan.steps) {
|
|
1517
|
+
const tests = step.tests === null ? '' : ` 【验 ${step.tests.hypothesis} · ${step.tests.level}】`
|
|
1518
|
+
lines.push(` ${step.ordinal}. [${step.status}] ${step.do}${tests} → 物证:${step.artifacts.join(', ') || '(未声明)'}`)
|
|
1519
|
+
}
|
|
1520
|
+
const first = plan.steps.find((step) => step.status === 'open')
|
|
1521
|
+
if (first !== undefined) lines.push(`- 下一个可交付步:${first.id}(交付只能落在第一个未落定步)`)
|
|
1522
|
+
if (plan.blocked !== undefined) lines.push(`- 计划被拦:${plan.blocked.reason}(连续 ${plan.blocked.attempts} 次未过闸,停下等人)`)
|
|
1523
|
+
}
|
|
1524
|
+
if (derived.hasOpenGate) {
|
|
1525
|
+
lines.push(`- 门(等人,${derived.inbox.length} 件):${derived.inbox.map((item) => `${item.kind}·${item.title}`).join(' / ')}`)
|
|
1526
|
+
}
|
|
1527
|
+
for (const fork of derived.forks) {
|
|
1528
|
+
const ruler = fork.decide_by === null || fork.decide_by === undefined ? '(无尺子)' : `${fork.decide_by.metric}(${fork.decide_by.direction === 'min' ? '越小越好' : '越大越好'})`
|
|
1529
|
+
lines.push(`- 世界线(步 ${fork.step} · ${fork.phase}):${fork.question} — 裁决指标 ${ruler}`)
|
|
1530
|
+
for (const branch of fork.branches) {
|
|
1531
|
+
const reading = branch.reading === null || branch.reading === undefined ? '未报读数' : `读数 ${branch.reading}${branch.validity === 'usable' ? '' : '(不可用)'}`
|
|
1532
|
+
/**
|
|
1533
|
+
* 执行者是**异步**派的(2026-09-11:ForkPlan 不再空等),所以「它跑完了没有」是一条
|
|
1534
|
+
* 独立的事实——不写出来,模型会以为分支在闲着,或者以为结论已经到手。
|
|
1535
|
+
*/
|
|
1536
|
+
const executing = branch.execution ?? null
|
|
1537
|
+
const runner =
|
|
1538
|
+
executing === null
|
|
1539
|
+
? '(执行者未派出)'
|
|
1540
|
+
: branch.unreturned === true
|
|
1541
|
+
? '(执行者未归 · 分叉已收口,结论不再回灌)'
|
|
1542
|
+
: executing.ok === null
|
|
1543
|
+
? '(执行中,结论会自动回灌)'
|
|
1544
|
+
: executing.ok === true
|
|
1545
|
+
? '(已回灌)'
|
|
1546
|
+
: `(执行没跑成:${executing.note ?? 'unknown'})`
|
|
1547
|
+
lines.push(` · [${branch.status}] ${branch.label}:${branch.approach}(${reading})${runner}`)
|
|
1548
|
+
}
|
|
1549
|
+
if (fork.verdict !== null && fork.verdict !== undefined) {
|
|
1550
|
+
lines.push(` → 已采纳 ${fork.verdict.winner},差额 ${fork.verdict.margin ?? '—'}${fork.verdict.tie === true ? ' · 并列' : ''}(算术裁决,不是谁说得响)`)
|
|
1551
|
+
}
|
|
1552
|
+
if (fork.undecidable !== null && fork.undecidable !== undefined) {
|
|
1553
|
+
lines.push(` → 算不出来(${fork.undecidable.code}):${fork.undecidable.reason} 停下问人,不许退化成随便挑一条。`)
|
|
1554
|
+
}
|
|
1555
|
+
if (fork.abandoned === true) lines.push(` → 已放弃探索:${fork.abandonReason ?? ''}(留痕)`)
|
|
1556
|
+
if (fork.phase === 'exploring' || fork.phase === 'deciding') lines.push(' · 这一步长着未收敛的分叉:先交付每条世界线,再 ConvergeFork;普通交付不能越过它。')
|
|
1557
|
+
if (fork.humanDecision !== null && fork.humanDecision !== undefined && !fork.settled) {
|
|
1558
|
+
const decision = fork.humanDecision
|
|
1559
|
+
lines.push(
|
|
1560
|
+
decision.action === 'adopt_branch'
|
|
1561
|
+
? ` · **人已裁决**:采纳「${fork.branches.find((branch) => branch.id === decision.branch)?.label ?? decision.branch}」——跑 ConvergeFork 落实它(合并是内核的活)。`
|
|
1562
|
+
: ' · **人已裁决**:放弃这条分叉——跑 AbandonFork 落实它(清理工作副本是内核的活)。',
|
|
1563
|
+
)
|
|
1564
|
+
}
|
|
1565
|
+
if (fork.merge?.provisional === true) {
|
|
1566
|
+
lines.push(` · **临时采纳**(待复核):${fork.merge.decisionNote ?? '分差不足以称结论'}——它不是结论,是一个待复核的决定。`)
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
if (state.evidence.length > 0) {
|
|
1570
|
+
const last = state.evidence[state.evidence.length - 1]
|
|
1571
|
+
lines.push(`- 最近一条证据:${last.id} ${last.verdict}(${last.evaluator} · ${last.level})`)
|
|
1572
|
+
}
|
|
1573
|
+
if (state.facts.length > 0) lines.push(`- 已升格事实:${state.facts.length} 条`)
|
|
1574
|
+
lines.push('- 提醒:进度、阶段、假设状态都是系统算出来的;你不能声明它们,只能通过交付与裁决推进。')
|
|
1575
|
+
return lines.join('\n')
|
|
1576
|
+
}
|