clearai-dsh 0.3.1 → 0.4.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.
Files changed (148) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/README.md +28 -28
  3. package/README.zh-CN.md +28 -28
  4. package/lib/client.js +861 -2555
  5. package/lib/domain-language.js +552 -48
  6. package/lib/fold.js +349 -1015
  7. package/lib/host.js +47 -557
  8. package/lib/invariant.js +9 -12
  9. package/lib/knowledge-view.js +363 -226
  10. package/lib/lang.js +81 -0
  11. package/package.json +1 -1
  12. package/presets/clearai/agent.cordis.yml +54 -82
  13. package/presets/clearai/clearai.patch.yml +54 -82
  14. package/presets/clearai/plugins/clearai-kernel.js +1584 -5456
  15. package/presets/clearai/plugins/ontology.js +56 -14
  16. package/presets/clearai/plugins/prompts.js +73 -363
  17. package/presets/clearai/skills/clearai-loop/SKILL.md +73 -59
  18. package/presets/clearai/plugins/brain.js +0 -547
  19. package/presets/clearai/plugins/commands.js +0 -199
  20. package/presets/clearai/template/knowledge/README.md +0 -25
  21. package/presets/clearai/template/memory/README.md +0 -34
  22. package/presets/clearai/template/project.md +0 -49
  23. package/presets/clearai/template/skills/README.md +0 -37
  24. package/presets/clearai/template/skills/chart-diagram-qa/SKILL.md +0 -43
  25. package/presets/clearai/template/skills/citation-management/SKILL.md +0 -73
  26. package/presets/clearai/template/skills/citation-management/references/bibtex_formatting.md +0 -908
  27. package/presets/clearai/template/skills/citation-management/references/citation_validation.md +0 -794
  28. package/presets/clearai/template/skills/citation-management/references/google_scholar_search.md +0 -725
  29. package/presets/clearai/template/skills/citation-management/references/metadata_extraction.md +0 -870
  30. package/presets/clearai/template/skills/citation-management/references/pubmed_search.md +0 -839
  31. package/presets/clearai/template/skills/citation-management/scripts/doi_to_bibtex.py +0 -204
  32. package/presets/clearai/template/skills/citation-management/scripts/extract_metadata.py +0 -569
  33. package/presets/clearai/template/skills/citation-management/scripts/format_bibtex.py +0 -349
  34. package/presets/clearai/template/skills/citation-management/scripts/generate_schematic.py +0 -139
  35. package/presets/clearai/template/skills/citation-management/scripts/generate_schematic_ai.py +0 -817
  36. package/presets/clearai/template/skills/citation-management/scripts/search_google_scholar.py +0 -282
  37. package/presets/clearai/template/skills/citation-management/scripts/search_pubmed.py +0 -398
  38. package/presets/clearai/template/skills/citation-management/scripts/validate_citations.py +0 -497
  39. package/presets/clearai/template/skills/data-analysis/SKILL.md +0 -92
  40. package/presets/clearai/template/skills/data-analysis/checklists/readiness_check.md +0 -23
  41. package/presets/clearai/template/skills/data-analysis/templates/analysis_report.md.tpl +0 -63
  42. package/presets/clearai/template/skills/data-analysis/templates/cleaning_rules_draft.yaml.tpl +0 -32
  43. package/presets/clearai/template/skills/data-analysis/templates/data_dictionary.md.tpl +0 -12
  44. package/presets/clearai/template/skills/data-analysis/templates/domain_knowledge_template.md.tpl +0 -316
  45. package/presets/clearai/template/skills/data-analysis/templates/feature_candidates.json.tpl +0 -20
  46. package/presets/clearai/template/skills/data-analysis/templates/quality_scorecard.md.tpl +0 -30
  47. package/presets/clearai/template/skills/data-analysis/workflows/01-data-profiling.md +0 -42
  48. package/presets/clearai/template/skills/data-analysis/workflows/02-quality-audit.md +0 -36
  49. package/presets/clearai/template/skills/data-analysis/workflows/03-physical-correlation.md +0 -25
  50. package/presets/clearai/template/skills/data-analysis/workflows/04-unstructured-mining.md +0 -26
  51. package/presets/clearai/template/skills/data-qa-analysis/SKILL.md +0 -102
  52. package/presets/clearai/template/skills/data-qa-analysis/checklists/readiness_check.md +0 -62
  53. package/presets/clearai/template/skills/data-qa-analysis/templates/best_in_class_report.md.tpl +0 -56
  54. package/presets/clearai/template/skills/data-qa-analysis/templates/cleaning_rules_draft.yaml.tpl +0 -56
  55. package/presets/clearai/template/skills/data-qa-analysis/templates/data_dictionary.md.tpl +0 -13
  56. package/presets/clearai/template/skills/data-qa-analysis/templates/data_source_inventory_and_lineage.md.tpl +0 -146
  57. package/presets/clearai/template/skills/data-qa-analysis/templates/data_status_report.md.tpl +0 -60
  58. package/presets/clearai/template/skills/data-qa-analysis/templates/steady_state_rules.yaml.tpl +0 -41
  59. package/presets/clearai/template/skills/data-qa-analysis/templates/subsystem_registry.md.tpl +0 -101
  60. package/presets/clearai/template/skills/data-qa-analysis/templates/unified_execution_plan.md.tpl +0 -100
  61. package/presets/clearai/template/skills/data-qa-analysis/workflows/01-data-source-inventory-and-lineage.md +0 -194
  62. package/presets/clearai/template/skills/data-qa-analysis/workflows/02-data-alignment-and-tag-semantics.md +0 -122
  63. package/presets/clearai/template/skills/data-qa-analysis/workflows/03-steady-state-identification.md +0 -126
  64. package/presets/clearai/template/skills/data-qa-analysis/workflows/04-consumption-analysis.md +0 -152
  65. package/presets/clearai/template/skills/data-qa-analysis/workflows/05-best-in-class-and-optimization-space.md +0 -78
  66. package/presets/clearai/template/skills/domain-presearch/SKILL.md +0 -131
  67. package/presets/clearai/template/skills/domain-presearch/checklists/domain_checklist.md +0 -24
  68. package/presets/clearai/template/skills/domain-presearch/references/figure_code.md +0 -78
  69. package/presets/clearai/template/skills/domain-presearch/references/strategic_frameworks.md +0 -38
  70. package/presets/clearai/template/skills/exploration-loop/SKILL.md +0 -81
  71. package/presets/clearai/template/skills/exploratory-data-analysis/SKILL.md +0 -77
  72. package/presets/clearai/template/skills/exploratory-data-analysis/references/bioinformatics_genomics_formats.md +0 -664
  73. package/presets/clearai/template/skills/exploratory-data-analysis/references/chemistry_molecular_formats.md +0 -664
  74. package/presets/clearai/template/skills/exploratory-data-analysis/references/general_scientific_formats.md +0 -518
  75. package/presets/clearai/template/skills/exploratory-data-analysis/references/microscopy_imaging_formats.md +0 -620
  76. package/presets/clearai/template/skills/exploratory-data-analysis/references/proteomics_metabolomics_formats.md +0 -517
  77. package/presets/clearai/template/skills/exploratory-data-analysis/references/spectroscopy_analytical_formats.md +0 -633
  78. package/presets/clearai/template/skills/exploratory-data-analysis/scripts/eda_analyzer.py +0 -547
  79. package/presets/clearai/template/skills/hypothesis-generation/SKILL.md +0 -73
  80. package/presets/clearai/template/skills/hypothesis-generation/references/experimental_design_patterns.md +0 -329
  81. package/presets/clearai/template/skills/hypothesis-generation/references/hypothesis_quality_criteria.md +0 -198
  82. package/presets/clearai/template/skills/hypothesis-generation/references/literature_search_strategies.md +0 -622
  83. package/presets/clearai/template/skills/hypothesis-generation/scripts/generate_schematic.py +0 -139
  84. package/presets/clearai/template/skills/hypothesis-generation/scripts/generate_schematic_ai.py +0 -817
  85. package/presets/clearai/template/skills/literature-review/SKILL.md +0 -72
  86. package/presets/clearai/template/skills/literature-review/references/citation_styles.md +0 -166
  87. package/presets/clearai/template/skills/literature-review/references/database_strategies.md +0 -455
  88. package/presets/clearai/template/skills/literature-review/scripts/generate_pdf.py +0 -176
  89. package/presets/clearai/template/skills/literature-review/scripts/generate_schematic.py +0 -139
  90. package/presets/clearai/template/skills/literature-review/scripts/generate_schematic_ai.py +0 -817
  91. package/presets/clearai/template/skills/literature-review/scripts/search_databases.py +0 -303
  92. package/presets/clearai/template/skills/literature-review/scripts/verify_citations.py +0 -221
  93. package/presets/clearai/template/skills/paper-lookup/SKILL.md +0 -59
  94. package/presets/clearai/template/skills/paper-lookup/references/arxiv.md +0 -161
  95. package/presets/clearai/template/skills/paper-lookup/references/biorxiv.md +0 -118
  96. package/presets/clearai/template/skills/paper-lookup/references/core.md +0 -150
  97. package/presets/clearai/template/skills/paper-lookup/references/crossref.md +0 -181
  98. package/presets/clearai/template/skills/paper-lookup/references/medrxiv.md +0 -104
  99. package/presets/clearai/template/skills/paper-lookup/references/openalex.md +0 -174
  100. package/presets/clearai/template/skills/paper-lookup/references/pmc.md +0 -152
  101. package/presets/clearai/template/skills/paper-lookup/references/pubmed.md +0 -124
  102. package/presets/clearai/template/skills/paper-lookup/references/semantic-scholar.md +0 -203
  103. package/presets/clearai/template/skills/paper-lookup/references/unpaywall.md +0 -127
  104. package/presets/clearai/template/skills/process-presearch/SKILL.md +0 -196
  105. package/presets/clearai/template/skills/process-presearch/checklists/process_checklist.md +0 -18
  106. package/presets/clearai/template/skills/process-presearch/references/figure_code.md +0 -107
  107. package/presets/clearai/template/skills/process-presearch/references/source_attribution_example.md +0 -22
  108. package/presets/clearai/template/skills/process-understanding-extraction/SKILL.md +0 -69
  109. package/presets/clearai/template/skills/process-understanding-extraction/checklists/readiness_check.md +0 -34
  110. package/presets/clearai/template/skills/process-understanding-extraction/templates/docx_raw_dump_extractor.py.tpl +0 -132
  111. package/presets/clearai/template/skills/process-understanding-extraction/templates/entity_map_unit_topology.json.tpl +0 -86
  112. package/presets/clearai/template/skills/process-understanding-extraction/templates/process_brief.md.tpl +0 -89
  113. package/presets/clearai/template/skills/process-understanding-extraction/templates/process_brief_builder_from_raw_dump.py.tpl +0 -203
  114. package/presets/clearai/template/skills/process-understanding-extraction/templates/process_flow_mermaid.md.tpl +0 -41
  115. package/presets/clearai/template/skills/process-understanding-extraction/templates/unified_execution_plan.md.tpl +0 -53
  116. package/presets/clearai/template/skills/process-understanding-extraction/workflows/01-process-doc-discovery.md +0 -173
  117. package/presets/clearai/template/skills/process-understanding-extraction/workflows/02-process-understanding-and-diagramming.md +0 -106
  118. package/presets/clearai/template/skills/scientific-brainstorming/SKILL.md +0 -64
  119. package/presets/clearai/template/skills/scientific-brainstorming/references/brainstorming_methods.md +0 -326
  120. package/presets/clearai/template/skills/scientific-critical-thinking/SKILL.md +0 -72
  121. package/presets/clearai/template/skills/scientific-critical-thinking/references/common_biases.md +0 -364
  122. package/presets/clearai/template/skills/scientific-critical-thinking/references/evidence_hierarchy.md +0 -485
  123. package/presets/clearai/template/skills/scientific-critical-thinking/references/experimental_design.md +0 -496
  124. package/presets/clearai/template/skills/scientific-critical-thinking/references/logical_fallacies.md +0 -478
  125. package/presets/clearai/template/skills/scientific-critical-thinking/references/scientific_method.md +0 -169
  126. package/presets/clearai/template/skills/scientific-critical-thinking/references/statistical_pitfalls.md +0 -506
  127. package/presets/clearai/template/skills/skill-creator/SKILL.md +0 -109
  128. package/presets/clearai/template/skills/skill-creator/references/authoring-guide.md +0 -89
  129. package/presets/clearai/template/skills/statistical-analysis/SKILL.md +0 -79
  130. package/presets/clearai/template/skills/statistical-analysis/references/assumptions_and_diagnostics.md +0 -369
  131. package/presets/clearai/template/skills/statistical-analysis/references/bayesian_statistics.md +0 -653
  132. package/presets/clearai/template/skills/statistical-analysis/references/effect_sizes_and_power.md +0 -578
  133. package/presets/clearai/template/skills/statistical-analysis/references/reporting_standards.md +0 -469
  134. package/presets/clearai/template/skills/statistical-analysis/references/test_selection_guide.md +0 -129
  135. package/presets/clearai/template/skills/statistical-analysis/scripts/assumption_checks.py +0 -538
  136. package/presets/clearai/template/skills/web-artifact/SKILL.md +0 -165
  137. package/presets/clearai/template/skills/web-artifact/assets/renderer/renderer.css +0 -229
  138. package/presets/clearai/template/skills/web-artifact/assets/renderer/renderer.js +0 -373
  139. package/presets/clearai/template/skills/web-artifact/assets/vendor/elkjs/LICENSE +0 -263
  140. package/presets/clearai/template/skills/web-artifact/assets/vendor/elkjs/UPSTREAM.md +0 -26
  141. package/presets/clearai/template/skills/web-artifact/assets/vendor/elkjs/elk.bundled.js +0 -6605
  142. package/presets/clearai/template/skills/web-artifact/references/when-drawing-a-topology.md +0 -150
  143. package/presets/clearai/template/skills/web-artifact/references/when-the-page-must-work-offline.md +0 -62
  144. package/presets/clearai/template/skills/web-artifact/scripts/check_artifact.py +0 -167
  145. package/presets/clearai/template/skills/web-artifact/scripts/render_topology.js +0 -272
  146. package/presets/clearai/template/skills/what-if-oracle/LICENSE.txt +0 -5
  147. package/presets/clearai/template/skills/what-if-oracle/SKILL.md +0 -72
  148. package/presets/clearai/template/skills/what-if-oracle/references/scenario-templates.md +0 -154
package/lib/fold.js CHANGED
@@ -16,11 +16,9 @@
16
16
  * 与预设内核的契约:`meta = { kind: 'clearai', v: 1, mutation: { t, ... } }`。
17
17
  * 词汇表由本文件的 `applyMutation` 定义;预设侧只负责产出,不负责解释。
18
18
  */
19
- import { applyLexiconMutation, deriveConflicts, emptyLexicon, formatAssertion, graphProjection, lexiconHealth, normalizeLexicon, objectKey, termUsage, VALUE_FORMS } from './domain-language.js'
20
- import { knowledgeView } from './knowledge-view.js'
21
-
22
- /** 世界线分支状态的秩。秩只增;一旦出现 adopted,整个分叉的分支状态冻结。 */
23
- const BRANCH_RANK = { exploring: 0, evaluated: 1, adopted: 2, pruned: 2 }
19
+ import { applyLexiconMutation, changedDefinitions, materializeOntology, classifyWorkspacePath, deriveConflicts, emptyLexicon, factFromFile, formatAssertion, graphProjection, lexiconHealth, normalizeLexicon, objectKey, termUsage, VALUE_FORMS } from './domain-language.js'
20
+ import { handleOf, knowledgeView, trustOf } from './knowledge-view.js'
21
+ import { bilingual, detectLanguage, messageText, tr, withLanguage } from './lang.js'
24
22
 
25
23
  /** 五个等级,由低到高。等级是「这条证据有多大程度只能靠信任做的人」的刻度(见 docs/verification-loop.md 的等级表)。 */
26
24
  const LEVELS = ['L0', 'L1', 'L2', 'L3', 'L4']
@@ -42,11 +40,29 @@ export const MUTATION_KIND = 'clearai'
42
40
  * 没有断言的事实照旧可读——形状变了才 +1,不是语义变了才 +1。
43
41
  * v10 → v11:**实体账本**独立于目标裁决:`entities`(登记的实例)、`entityAssertions`
44
42
  * (登记那一刻就成立的边)、`hostHealth`(宿主读面降级的账),以及
45
- * `hypotheses[].skips`(跳级理由)与 `goal.criteriaHistory`(判据修订)。
43
+ * `hypotheses[].skips`(跳级理由,v14 删)与 `goal.criteriaHistory`(判据修订)。
46
44
  * 旧日志这三块都是空表:实体图仍只从 `facts[].assertions` 长出来,逐字节不变。
47
- * 投影缓存按版本判定,所以旧缓存会被丢弃、从日志重折一遍——用量是**从日志折出来的**,重折才完整。
45
+ * v11 → v12:**宿主已有的交还宿主**:分叉 / 世界线(`forks`)、侦察(`scouts`)、外脑与技能目录
46
+ * (`brain`、`brainCandidates`、`skillPromotions`、`skillCatalog`、`skillUsage`)、章程读数
47
+ * (`constitution`)与写入计数(`writeCalls`)都从状态里删了。旧日志里的对应事件类型
48
+ * (`fork/*`、`worldline/*`、`scout/*`、`branch/*`、`git/*`、`clearai/brain` 段、相关人门动作)
49
+ * 不认识就原样跳过,其余部分照常折出来。
50
+ * v12 → v13:**步骤完成与结果分开**:步骤的 `tests` 折成 `{hypotheses: id[], level}`(旧账的单条
51
+ * `hypothesis` 照旧读);证据带上它针对的判断(`hypothesis`);`step/advanced` 带上交付本身
52
+ * (`delivery`:谁判的、凭什么、出处),`step.evidence` 变成 id 数组;结算事实带上两项裁决
53
+ * (`holds` / `results`)。
54
+ * v13 → v14:**缺口与关口收窄**:跳级理由整套删除——假设上不再有 `skips`,派生里不再有
55
+ * `untouchedLevels`;旧日志里的 `level/skipped` 不认识就原样跳过。缺口只留三种
56
+ * (`untouched_claims`、`prose_only_claims`、`entities_unlanded`)。
57
+ * v14 → v15:**攒下来的东西住在文件里**:多了 `workspace.files`(工作区里事实文件与本体文件
58
+ * 的内容快照,由 `workspace/synced` 折进来);事实带上 `definitions`(升格那一刻用到的
59
+ * 词条含义指纹)。派生的事实行合并其它会话留下的事实(`foreign`),并给出
60
+ * `definitionsChanged`。本体(词汇、实体、实体关系)改为从本体文件折出来,跨文件问题记在
61
+ * `ontologyProblems`。旧日志里没有这些,折出来就是空的。
62
+ * v15 → v16:多了 `language`(人说话用的语言,从人的消息折出来):系统写给人的话、面板里的读数都跟着它。
63
+ * 投影缓存按版本判定,所以旧缓存会被丢弃、从日志重折一遍。
48
64
  */
49
- export const STATE_VERSION = 11
65
+ export const STATE_VERSION = 16
50
66
 
51
67
  /**
52
68
  * **只留台账、不折进视图**的变更类型(词汇表的另一半)。
@@ -56,11 +72,10 @@ export const STATE_VERSION = 11
56
72
  * · 发行自检按它比对内核实际落的每一类事实 —— 既没折、又不在清单里的,是**真的缺口**;
57
73
  * · 读代码的人不必逐条 grep 才知道哪些事实不上界面。
58
74
  *
59
- * 这四类都是**账本事实**:它们记「系统/账本做了什么」,不改变任何派生量
60
- * (准入检查、提交流水、快照、恢复),面板只展示当前可读取的证据与记录,
61
- * 而不是从投影里再长一份。
75
+ * 它们记「系统做了什么」(准入检查),不改变任何派生量。
76
+ * 旧日志里的 `git/*`(已删除的账本)不再列出:不认识的类型原样返回,本来就会被安静跳过。
62
77
  */
63
- export const LEDGER_ONLY_MUTATIONS = ['admission/checked', 'git/committed', 'git/restored', 'git/snapshot']
78
+ export const LEDGER_ONLY_MUTATIONS = ['admission/checked']
64
79
 
65
80
  /** 空状态。`init` 与「日志里还没有任何变更」都必须给出同一个形状。 */
66
81
  export function emptyState() {
@@ -72,45 +87,8 @@ export function emptyState() {
72
87
  audits: [],
73
88
  materials: [],
74
89
  facts: [],
75
- forks: [],
76
- /** 系统按触发派出去的侦察(子角色不是模型自由委派):结论进资料面,这里留一条记录 */
77
- scouts: [],
78
- /** 工作区里等人采纳的候选技能(内核扫描后落的事实)与已采纳的记录 */
79
- brainCandidates: [],
80
- skillPromotions: [],
81
- /**
82
- * 外脑全景(技能清单 + 记忆索引)。**由内核扫描后随投影下发**——面板在浏览器里读不了盘,
83
- * 而内核每个 pre-step 都在扫;清单走投影(变了才发),正文才走宿主路由(点开时才读)。
84
- */
85
- brain: null,
86
- /**
87
- * 宿主原生的**合并技能目录**(预设自带 / 项目 / 用户 / 我们投影的 `clear/skills` 合并后的表)。
88
- * 内核每个 pre-step 调一次 `ctx.skills.snapshot`,变了才发——于是面板与模型看的是**同一张表**。
89
- */
90
- skillCatalog: null,
91
- /**
92
- * 本会话的技能加载记录。**纯派生,零新账**:
93
- * 模型加载 = 日志里的 `tool/call name='skill'`;人引用 = 一条含 `/名字` 的用户消息。
94
- */
95
- skillUsage: {},
96
90
  blocks: {},
97
91
  releases: [],
98
- /**
99
- * 运行档(人在场 / 无人值守)。**它是部署预设的初值,不是可切换的开关**:
100
- * · 人门动词 `set_autonomy` 已摘除,面板上那个开关已经不存在;
101
- * · `effective` = 内核随投影下发的那一份(只有内核知道组合配置),**只用于展示**;
102
- * · `override` = 历史日志里可能留下的人门记录。读取侧保留它只是为了旧会话仍然读得通,
103
- * 当前**没有任何写入者**——不要据此认为现在还能切换档位。
104
- * · 它也不影响续跑:要不要继续由门状态算出来(见内核 turnDemand)。
105
- */
106
- autonomy: { override: null, effective: null },
107
- /**
108
- * 项目章程(`PROJECT.md`)的读数(内核扫描后随投影下发)。
109
- * 为什么要有它:章程的**读**侧早就接在原生指令文件上了,但**写**侧只有一句提示词——
110
- * 结果是跑完几轮、交付了产物,章程还是铺工作区那天的模板,谁也没发现——不提醒它就会一直漂着。
111
- * 事实摆在这里,人与模型都看得见「它还是不是空壳」。
112
- */
113
- constitution: null,
114
92
  /**
115
93
  * **本体形状**:内核随投影下发的那份声明(对象/状态/边/等级)。
116
94
  * 为什么进投影而不是在界面里手抄:面板那一格的页眉要从**声明**生成——
@@ -149,87 +127,38 @@ export function emptyState() {
149
127
  */
150
128
  hostHealth: [],
151
129
  /**
152
- * **续跑窗口的账**:我们向平台说过的那句话——「这个窗口归我布防 / 我按了暂停 /
153
- * 我收兵了 / 它不在了而那不是我们干的」。形状见 `applyMutation` 的 `continuation/set`。
130
+ * **工作区文件快照**(`workspace/synced`):路径 → `{ digest, data }` 或 `{ digest, error }`。
154
131
  *
155
- * 为什么它必须在投影里而不是内核内存里:宿主的 `paused` 相位分不清「人按的」与
156
- * 「策略按的」,而这两者的处置正好相反(人按的绝不覆盖,自己按的要能恢复)。
157
- * 只记在内存里,一次重启就把我们自己的暂停误报成人的暂停——那是一句**不实的话**;
158
- * 而「我们说过的话要有账」本来就是这套设计的纪律(意图工具不能断言事实)。
132
+ * 攒下来的东西(事实、本体)住在项目文件里,跨会话活着;而投影只吃账本。内核每一拍把
133
+ * 这两处的**变化**折成一条变更,于是文件内容也是账本里一条可重放的事实——重放、分叉、
134
+ * 离线复判读到的都是那一刻的文件,而不是此刻盘上的。
135
+ */
136
+ workspace: { files: {} },
137
+ /**
138
+ * **本体文件的跨文件问题**(第二道校验,只提示):引用断了、类型对不上、单值关系两个取值……
139
+ * 有问题的节点或边不进图,问题列在这里,卡片与本体页都读它。
140
+ */
141
+ ontologyProblems: [],
142
+ /**
143
+ * **人说话用的语言**('zh' / 'en';还没听到人说话时为 null)。从人的消息里折出来:
144
+ * 工具结果、运行态卡、面板读数都跟着它(见 `lang.js`)。
159
145
  */
160
- continuation: null,
146
+ language: null,
161
147
  /** 正在飞的工具调用(来自 tool/call):让「评估者在裁决」成为一条可看见的事实 */
162
148
  inFlight: null,
163
149
  /** 本会话自己写过的路径(来自 tool/call 的 write/edit):L4 来源分离的判据 */
164
150
  written: [],
165
- /**
166
- * **会改工作区的工具调用**的一次计数(来自 tool/call 的 write/edit/bash/pwsh)。
167
- *
168
- * 为什么要有它:`written` 只认 write/edit,而探索最常走 bash(脚本自己产出文件),
169
- * 于是「这个会话到底动过工作区没有」用 `written` 答不全。内核在**回合边界**上要问的
170
- * 正是这一句(是否值得记一次工作区快照),所以这里记一个只增的计数——
171
- * 它是日志折出来的,可重放,不是第二本账。
172
- */
173
- writeCalls: 0,
174
151
  }
175
152
  }
176
153
 
177
154
  /**
178
- * 等裁决的三个工具:它们的调用在飞时,派生阶段是 `auditing`。
155
+ * 等裁决的工具:它们的调用在飞时,派生阶段是 `auditing`。`CloseGoal` 是 `Conclude` 的旧名,留着让旧日志照样折。
179
156
  * 其余工具不等外部裁决,不该让面板在每次调用时跳一下。
180
157
  */
181
- const VERDICT_WAITERS = new Set(['AdvancePlan', 'AdvanceWorldline', 'CloseGoal'])
158
+ const VERDICT_WAITERS = new Set(['AdvancePlan', 'Conclude', 'CloseGoal'])
182
159
 
183
160
  /** 记下来的自写路径有上限:它是判据,不是档案(档案在会话日志里)。 */
184
161
  const MAX_WRITTEN = 512
185
- /**
186
- * 会**改工作区**的工具名。`bash`/`pwsh` 也算:脚本自己产出文件,内核分辨不了只读与写入,
187
- * 所以宁可偏保守——多记一笔快照,也不漏掉探索产出。
188
- */
189
- const WRITE_CAPABLE_TOOLS = new Set(['write', 'edit', 'bash', 'pwsh'])
190
-
191
- /**
192
- * 技能引用手势:与原生 `dsh-tool-skill` 的 `SKILL_GESTURE` **同一条正则**。
193
- *
194
- * 为什么不各写一条:这条正则是「技能正文会不会被注入」的**充要条件**
195
- * (`dsh-tool-skill/lib/index.js` 的 `invokedSkillNames`:只认 `source.kind === 'user'` 的
196
- * 文本块)。面板数出来的用法与原生真的做了什么必须是同一件事,否则读数就是编的。
197
- */
198
- const SKILL_GESTURE = /(^|\s)\/([a-z0-9]+(?:-[a-z0-9]+)*)(?=\s|$)/g
199
- /** 每条技能留最近几次记录(用量是判据不是档案;要全量去会话日志里查)。 */
200
- const MAX_SKILL_EVENTS = 8
201
- /** 最多跟踪多少条技能,免得一个失控的会话把投影撑大。 */
202
- const MAX_SKILLS_TRACKED = 64
203
-
204
- /** 从一条消息的文本块里取出被引用的技能名(与原生同一条正则、同一套去重)。 */
205
- function invokedSkillNames(message) {
206
- const names = []
207
- for (const block of Array.isArray(message?.content) ? message.content : []) {
208
- if (block?.type !== 'text' || typeof block.text !== 'string') continue
209
- for (const match of block.text.matchAll(SKILL_GESTURE)) if (match[2] !== undefined && !names.includes(match[2])) names.push(match[2])
210
- }
211
- return names
212
- }
213
-
214
- /** 这一次加载落在哪一步上(「在第 3 步」这个指针的由来)。 */
215
- function stepPointer(state) {
216
- const plan = state.plans.find((item) => item.status === 'active') ?? null
217
- if (plan === null) return { plan: null, step: null, ordinal: null }
218
- const step = plan.steps.find((item) => item.status === 'open') ?? null
219
- return { plan: plan.id, step: step === null ? null : step.id, ordinal: step === null ? null : step.ordinal }
220
- }
221
-
222
- function recordSkillUse(state, name, by, at, pointer) {
223
- const usage = state.skillUsage ?? (state.skillUsage = {})
224
- if (usage[name] === undefined && Object.keys(usage).length >= MAX_SKILLS_TRACKED) return
225
- const entry = usage[name] ?? { name, model: 0, human: 0, events: [], lastAt: null }
226
- if (by === 'model') entry.model += 1
227
- else entry.human += 1
228
- entry.events.push({ by, at, plan: pointer.plan, step: pointer.step, ordinal: pointer.ordinal })
229
- if (entry.events.length > MAX_SKILL_EVENTS) entry.events.splice(0, entry.events.length - MAX_SKILL_EVENTS)
230
- entry.lastAt = at
231
- usage[name] = entry
232
- }
233
162
 
234
163
  /** 深拷贝(状态是纯 JSON,这是唯一需要的工具)。 */
235
164
  function clone(value) {
@@ -257,6 +186,16 @@ function factFromHypothesis(facts, goalId, hypothesisId, claim) {
257
186
  })
258
187
  }
259
188
 
189
+ /**
190
+ * 步骤检验的判断一律折成 `{hypotheses: id[], level}`:新账写 `hypotheses`(可多条),
191
+ * 旧账只有单条 `hypothesis`。读面只认这一种形状。
192
+ */
193
+ export function normalizeTests(tests) {
194
+ if (tests === undefined || tests === null || typeof tests !== 'object') return null
195
+ const hypotheses = Array.isArray(tests.hypotheses) ? tests.hypotheses.map(String) : typeof tests.hypothesis === 'string' && tests.hypothesis !== '' ? [tests.hypothesis] : []
196
+ return { hypotheses, level: tests.level ?? null }
197
+ }
198
+
260
199
  function appendSteps(plan, rawSteps) {
261
200
  for (const raw of rawSteps) {
262
201
  plan.steps.push({
@@ -265,7 +204,7 @@ function appendSteps(plan, rawSteps) {
265
204
  do: raw.do,
266
205
  artifacts: raw.artifacts ?? [],
267
206
  done_criteria: raw.done_criteria,
268
- tests: raw.tests ?? null,
207
+ tests: normalizeTests(raw.tests),
269
208
  status: 'open',
270
209
  evidence: null,
271
210
  advancedAt: null,
@@ -357,12 +296,15 @@ export function applyMutation(state, mutation) {
357
296
  if (String(known.claim ?? '') !== claim) continue
358
297
  known.refute_when = hypothesis.refute_when
359
298
  known.version = hypothesis.version ?? known.version
299
+ if (typeof hypothesis.name === 'string' && hypothesis.name !== '') known.name = hypothesis.name
360
300
  if (Array.isArray(hypothesis.assertions)) known.assertions = clone(hypothesis.assertions)
361
301
  continue
362
302
  }
363
303
  next.hypotheses.push({
364
304
  id: hypothesis.id,
365
305
  goal: mutation.id,
306
+ /** 短名(第六阶段起,模型自己起);旧日志没有它,读的一侧取主张开头。 */
307
+ ...(typeof hypothesis.name === 'string' && hypothesis.name !== '' ? { name: hypothesis.name } : {}),
366
308
  claim,
367
309
  refute_when: hypothesis.refute_when,
368
310
  status: 'proposed',
@@ -372,12 +314,6 @@ export function applyMutation(state, mutation) {
372
314
  * 不写断言照旧成立(宽松+校验);写了就是「这条主张用这门语言怎么说」。
373
315
  */
374
316
  assertions: Array.isArray(hypothesis.assertions) ? clone(hypothesis.assertions) : null,
375
- /**
376
- * **跳级理由**(`level/skipped` 折进来)。跳级本身不违规(首次测量没有廉价路
377
- * 可走),但「我直接上了 L3、下面几级从没走过」必须能说清为什么不适用——
378
- * 理由落在这里,`derive()` 会把被理由覆盖的层从 `untouchedLevels` 里减掉。
379
- */
380
- skips: [],
381
317
  at,
382
318
  })
383
319
  }
@@ -423,31 +359,12 @@ export function applyMutation(state, mutation) {
423
359
  closedAt: null,
424
360
  summary: null,
425
361
  blocked: undefined,
426
- /**
427
- * 授权记号(ClearAI `plan.confirmed_at` / `confirmed_by`)。
428
- * 两种取得方式,都可考:**显式动作**(人在原生审阅卡上批准 → `by:'user'`)与
429
- * **行为**(交付过一步 → `by:'progress'`,见内核 AdvancePlan)。
430
- * 第三种来源 `by:'autonomy'`(无人值守档在立约时自动确认)**已删除**:
431
- * 那会让「计划经人确认」这条证据变成系统自己签的。读取侧不需要兼容它,
432
- * 因为删除发生在写入侧,历史日志里最多出现 `user` 与 `progress`。
433
- * 注意这是一个**记号**,不是闸门:没确认的计划照样能被交付推起来,那一刻记号按事实补写。
434
- */
435
- confirmed_at: mutation.confirmed_at ?? null,
436
- confirmed_by: mutation.confirmed_by ?? null,
437
362
  steps: [],
438
363
  }
439
364
  next.plans.push(plan)
440
365
  appendSteps(plan, mutation.steps ?? [])
441
366
  break
442
367
  }
443
- case 'plan/confirmed': {
444
- // 第一次授权为准(`stamp_confirmed_by_progress` 同款幂等:已确认就不再改写)。
445
- const plan = planOf(mutation.plan)
446
- if (plan === undefined || plan.confirmed_at !== null) break
447
- plan.confirmed_at = mutation.at ?? new Date(at).toISOString()
448
- plan.confirmed_by = mutation.by ?? 'user'
449
- break
450
- }
451
368
  case 'plan/amended': {
452
369
  const plan = planOf(mutation.plan)
453
370
  if (plan === undefined || plan.status !== 'active') break
@@ -503,7 +420,6 @@ export function applyMutation(state, mutation) {
503
420
  note: mutation.note ?? null,
504
421
  path: mutation.path ?? null,
505
422
  step: mutation.step ?? null,
506
- branch: mutation.branch ?? null,
507
423
  at,
508
424
  })
509
425
  break
@@ -560,6 +476,9 @@ export function applyMutation(state, mutation) {
560
476
  const audit = next.audits.find((item) => item.id === mutation.id)
561
477
  if (audit !== undefined) {
562
478
  audit.verdict = mutation.verdict
479
+ /** 两项裁决:交付成立吗 + 每条判断的结果。旧账没有这两格,`verdict` 那时说的就是交付成立吗。 */
480
+ audit.holds = mutation.holds ?? null
481
+ audit.results = Array.isArray(mutation.results) ? mutation.results : []
563
482
  audit.shortfalls = mutation.shortfalls ?? []
564
483
  audit.basis = mutation.basis ?? null
565
484
  audit.card_path = mutation.card_path ?? null
@@ -572,7 +491,8 @@ export function applyMutation(state, mutation) {
572
491
  id: mutation.id,
573
492
  step: mutation.step,
574
493
  plan: mutation.plan,
575
- branch: mutation.branch ?? null,
494
+ /** 针对哪条判断;旧账没有这一格(`undefined`),按步骤检验的判断找回来。 */
495
+ hypothesis: mutation.hypothesis,
576
496
  verdict: mutation.verdict,
577
497
  level: mutation.level,
578
498
  evaluator: mutation.evaluator,
@@ -595,21 +515,22 @@ export function applyMutation(state, mutation) {
595
515
  const step = stepOf(mutation.plan, mutation.step)
596
516
  if (step !== undefined) {
597
517
  settle(step, 'advanced')
598
- step.evidence = mutation.evidence
518
+ /** 新账是证据 id 的数组(每条判断一份,不检验判断就是空数组);旧账是单个 id。 */
519
+ step.evidence = Array.isArray(mutation.evidence) ? mutation.evidence : mutation.evidence === undefined || mutation.evidence === null ? [] : [mutation.evidence]
520
+ /** 交付本身:谁判的、凭什么、出处。旧账没有,读面退回到证据。 */
521
+ step.delivery =
522
+ mutation.evaluator === undefined
523
+ ? null
524
+ : { evaluator: mutation.evaluator, basis: mutation.basis ?? null, refs: Array.isArray(mutation.refs) ? mutation.refs : [], origins: Array.isArray(mutation.origins) ? mutation.origins : [] }
599
525
  step.advancedAt = at
600
526
  }
601
527
  break
602
528
  }
603
529
  case 'human/released': {
604
- /**
605
- * 放行绑在**哪条轴**上要说清:主线绑步骤、世界线绑分支。
606
- * `via` 记它凭什么算数——现在只有 `approval`(原生审批栈的权威记录),不推断。
607
- */
530
+ /** `via` 记它凭什么算数——现在只有 `approval`(原生审批栈的权威记录),不推断。 */
608
531
  next.releases.push({
609
532
  step: mutation.step ?? null,
610
533
  plan: mutation.plan ?? null,
611
- fork: mutation.fork ?? null,
612
- branch: mutation.branch ?? null,
613
534
  via: mutation.via ?? null,
614
535
  at,
615
536
  call: mutation.call ?? null,
@@ -633,10 +554,47 @@ export function applyMutation(state, mutation) {
633
554
  evidence: mutation.evidence ?? [],
634
555
  path: mutation.path ?? null,
635
556
  assertions: Array.isArray(mutation.assertions) ? clone(mutation.assertions) : null,
557
+ /** 升格那一刻,断言用到的词条各自的含义指纹:之后定义改了,这条事实要复核。 */
558
+ definitions: mutation.definitions !== null && typeof mutation.definitions === 'object' && !Array.isArray(mutation.definitions) ? clone(mutation.definitions) : null,
636
559
  at,
637
560
  })
638
561
  break
639
562
  }
563
+ /**
564
+ * **工作区同步**(`workspace/synced`,内核发):事实文件与本体文件这一拍的变化。
565
+ * 每条 change 是一个文件的新样子(`data` 或读不成时的 `error`),或 `removed`。
566
+ * 按路径覆盖,所以同一批变化折两遍结果一样。
567
+ */
568
+ case 'workspace/synced': {
569
+ const files = next.workspace !== null && typeof next.workspace === 'object' && next.workspace.files !== null && typeof next.workspace.files === 'object' ? next.workspace.files : {}
570
+ next.workspace = { ...(next.workspace ?? {}), files }
571
+ for (const change of Array.isArray(mutation.changes) ? mutation.changes : []) {
572
+ const path = typeof change?.path === 'string' ? change.path : ''
573
+ if (path === '') continue
574
+ if (change.removed === true) {
575
+ delete files[path]
576
+ continue
577
+ }
578
+ files[path] = change.error !== undefined && change.error !== null ? { digest: change.digest ?? null, error: String(change.error) } : { digest: change.digest ?? null, data: change.data ?? null }
579
+ }
580
+ /**
581
+ * **本体从文件折出来**:词汇、实体、实体断言整份按文件重算——文件就是权威,
582
+ * 账本里只记「那一刻文件是什么样」。旧会话里工具写下的本体事件仍折得出来,
583
+ * 但一旦这个会话同步过本体文件,就以文件为准(旧会话不迁移)。
584
+ */
585
+ const touchedOntology = (mutation.changes ?? []).some((change) => {
586
+ const kind = classifyWorkspacePath(change?.path)?.kind
587
+ return kind !== undefined && kind !== 'fact'
588
+ })
589
+ if (touchedOntology || Object.keys(files).some((path) => classifyWorkspacePath(path)?.kind !== 'fact')) {
590
+ const ontology = materializeOntology(files)
591
+ next.lexicon = ontology.lexicon
592
+ next.entities = ontology.entities
593
+ next.entityAssertions = ontology.entityAssertions
594
+ next.ontologyProblems = ontology.problems
595
+ }
596
+ break
597
+ }
640
598
  case 'fact/reviewed': {
641
599
  /**
642
600
  * **人审查过一条被推翻标记的事实**:撤回它,或判证据不可靠、维持原事实。
@@ -650,260 +608,6 @@ export function applyMutation(state, mutation) {
650
608
  fact.review = { decision: mutation.decision === 'retracted' ? 'retracted' : 'kept', reason: mutation.reason ?? null, at, by: mutation.by ?? 'user' }
651
609
  break
652
610
  }
653
- case 'worldline/prepared': {
654
- const fork = next.forks.find((item) => item.id === mutation.fork)
655
- if (fork === undefined) break
656
- fork.tier = mutation.tier
657
- fork.base = mutation.base ?? null
658
- fork.degradedReason = mutation.reason ?? null
659
- for (const entry of mutation.branches ?? []) {
660
- const branch = fork.branches.find((item) => item.id === entry.id)
661
- if (branch !== undefined) {
662
- branch.worktree_path = entry.path
663
- branch.git_branch = entry.branch
664
- branch.worktree_removed = false
665
- }
666
- }
667
- break
668
- }
669
- case 'fork/merge_skipped': {
670
- const fork = next.forks.find((item) => item.id === mutation.fork)
671
- if (fork === undefined) break
672
- fork.mergeSkipped = { branch: mutation.branch ?? null, reason: mutation.reason ?? null }
673
- break
674
- }
675
- case 'worldline/executing': {
676
- const fork = next.forks.find((item) => item.id === mutation.fork)
677
- const branch = fork?.branches.find((item) => item.id === mutation.branch)
678
- if (branch === undefined) break
679
- branch.execution = { child: mutation.child ?? null, capability: mutation.capability ?? null, ok: null, conclusion: null }
680
- break
681
- }
682
- case 'worldline/executed': {
683
- const fork = next.forks.find((item) => item.id === mutation.fork)
684
- const branch = fork?.branches.find((item) => item.id === mutation.branch)
685
- if (branch === undefined) break
686
- branch.execution = { ...(branch.execution ?? { child: mutation.child ?? null, capability: null }), ok: mutation.ok === true, conclusion: mutation.conclusion ?? null, note: mutation.note ?? null }
687
- break
688
- }
689
- case 'worldline/removed': {
690
- const fork = next.forks.find((item) => item.id === mutation.fork)
691
- const branch = fork?.branches.find((item) => item.id === mutation.branch)
692
- if (branch === undefined) break
693
- branch.worktree_removed = true
694
- branch.kept_ref = mutation.kept_ref !== false
695
- break
696
- }
697
- case 'fork/merged': {
698
- const fork = next.forks.find((item) => item.id === mutation.fork)
699
- if (fork === undefined) break
700
- fork.merge = {
701
- branch: mutation.branch,
702
- commit: mutation.commit ?? null,
703
- mode: mutation.mode ?? 'merged',
704
- snapshotCommit: mutation.snapshot_commit ?? null,
705
- // 临时采纳 = 分差不足以称结论(或由判断而来):它不是结论,是待复核的决定。
706
- provisional: mutation.provisional === true,
707
- by: mutation.by ?? 'metric',
708
- decisionNote: mutation.decision_note ?? null,
709
- margin: mutation.margin ?? null,
710
- }
711
- fork.mergeConflict = null
712
- break
713
- }
714
- case 'fork/merge_conflict': {
715
- const fork = next.forks.find((item) => item.id === mutation.fork)
716
- if (fork === undefined) break
717
- fork.mergeConflict = { branch: mutation.branch, winner: mutation.winner ?? null, margin: mutation.margin ?? null, detail: mutation.detail ?? null }
718
- break
719
- }
720
- case 'scout/dispatched': {
721
- next.scouts.push({
722
- id: mutation.id,
723
- step: mutation.step,
724
- plan: mutation.plan ?? null,
725
- trigger: mutation.trigger ?? null,
726
- child: mutation.child ?? null,
727
- capability: mutation.capability ?? null,
728
- // 任务身份:同一个身份已经正常回灌过的,再派时会被复用而不是重跑(幂等派遣)。
729
- digest: mutation.digest ?? null,
730
- conclusion: null,
731
- note: null,
732
- path: null,
733
- at,
734
- })
735
- break
736
- }
737
- case 'scout/settled': {
738
- const scout = next.scouts.find((item) => item.id === mutation.id)
739
- if (scout !== undefined) {
740
- scout.conclusion = mutation.conclusion ?? null
741
- scout.note = mutation.note ?? null
742
- scout.path = mutation.path ?? null
743
- }
744
- break
745
- }
746
- case 'fork/created': {
747
- if (next.forks.some((fork) => fork.step === mutation.step)) break // 一步只分叉一次
748
- next.forks.push({
749
- id: mutation.id,
750
- step: mutation.step,
751
- plan: mutation.plan,
752
- question: mutation.question,
753
- decide_by: mutation.decide_by,
754
- settled: false,
755
- abandoned: false,
756
- abandonReason: null,
757
- /** 横评仲裁的判决(`fork/arbitrated`)与它的会话 id(面板可旁观)。 */
758
- arbitration: null,
759
- arbitrationSession: null,
760
- /** 人在面板上的裁决(结构化记录,by:'user');null = 还没裁。 */
761
- humanDecision: null,
762
- verdict: null,
763
- undecidable: null,
764
- /**
765
- * 算术算出来的推荐(`fork/recommended`):人裁决那条路上**唯一的输入**。
766
- * 它是事实(系统算过),不是状态——分支秩不动,收敛仍然只走 ConvergeFork。
767
- */
768
- recommended: null,
769
- recommendMargin: null,
770
- recommendProvisional: false,
771
- recommendBy: null,
772
- /**
773
- * 采纳时**没能合并**的原因(`fork/merge_skipped`):面板据此说清「决定登记了,
774
- * 赢家的产物要靠一次普通交付落位」。不转发它,这句话就永远说不出来。
775
- */
776
- mergeSkipped: null,
777
- at,
778
- branches: (mutation.options ?? []).map((option) => ({
779
- id: option.id,
780
- label: option.label,
781
- approach: option.approach,
782
- done_criteria: option.done_criteria,
783
- workspace: option.workspace ?? null,
784
- artifacts: option.artifacts ?? [],
785
- level: option.level ?? 'L0',
786
- status: 'exploring',
787
- card_path: null,
788
- evaluator_session: null,
789
- humanDecision: null,
790
- reading: null,
791
- validity: null,
792
- verdict: null,
793
- basis: null,
794
- evaluator: null,
795
- evidence: null,
796
- at: null,
797
- })),
798
- })
799
- break
800
- }
801
- case 'branch/delivered': {
802
- const fork = next.forks.find((item) => item.id === mutation.fork)
803
- const branch = fork?.branches.find((item) => item.id === mutation.branch)
804
- if (branch === undefined) break
805
- if ((BRANCH_RANK[branch.status] ?? 0) < BRANCH_RANK.evaluated) branch.status = 'evaluated'
806
- branch.reading = mutation.reading ?? null
807
- branch.validity = mutation.validity ?? null
808
- branch.verdict = mutation.verdict ?? null
809
- branch.basis = mutation.basis ?? null
810
- branch.evaluator = mutation.evaluator ?? null
811
- branch.evidence = mutation.evidence ?? null
812
- // 评估卡与评估者会话:仲裁用卡(注入),面板用会话(旁观入口)。
813
- branch.card_path = mutation.card_path ?? null
814
- branch.evaluator_session = mutation.evaluator_session ?? null
815
- branch.at = at
816
- break
817
- }
818
- case 'fork/recommended': {
819
- /**
820
- * 算术算出来的推荐。**只记事实,不动状态**:分支秩不变、`settled` 不变——
821
- * 采纳永远还是要走 `ConvergeFork`(或人的一次裁决)。它存在的理由只有一个:
822
- * 人门卡与面板读的是投影里的这个字段,不落账它们就只能写「推荐:无」。
823
- */
824
- const fork = next.forks.find((item) => item.id === mutation.fork)
825
- if (fork === undefined) break
826
- fork.recommended = mutation.branch ?? null
827
- fork.recommendMargin = mutation.margin ?? null
828
- fork.recommendProvisional = mutation.provisional === true
829
- fork.recommendBy = mutation.by ?? null
830
- break
831
- }
832
- case 'fork/converged': {
833
- const fork = next.forks.find((item) => item.id === mutation.fork)
834
- if (fork === undefined) break
835
- fork.settled = true
836
- fork.undecidable = null
837
- fork.verdict = { winner: mutation.winner, margin: mutation.margin ?? null, tie: mutation.tie === true, metric: mutation.metric ?? null, direction: mutation.direction ?? null }
838
- for (const branch of fork.branches) {
839
- if ((BRANCH_RANK[branch.status] ?? 0) >= BRANCH_RANK.adopted) continue // 一旦采纳,状态冻结
840
- branch.status = branch.id === mutation.winner ? 'adopted' : 'pruned'
841
- }
842
- break
843
- }
844
- case 'fork/arbitration_dispatched': {
845
- const fork = next.forks.find((item) => item.id === mutation.fork)
846
- if (fork === undefined) break
847
- fork.arbitrationSession = mutation.arbiter_session ?? null
848
- break
849
- }
850
- case 'fork/arbitrated': {
851
- const fork = next.forks.find((item) => item.id === mutation.fork)
852
- if (fork === undefined) break
853
- // 判决写进节点(唯一真相仍是投影):采纳与否由内核重判一次决定,这里只记事实。
854
- fork.arbitration = {
855
- winner: mutation.winner ?? null,
856
- ranking: mutation.ranking ?? [],
857
- reason: mutation.reason ?? '',
858
- confidence: mutation.confidence ?? 'low',
859
- arbiter_session: mutation.arbiter_run_id ?? null,
860
- }
861
- break
862
- }
863
- case 'fork/undecidable': {
864
- const fork = next.forks.find((item) => item.id === mutation.fork)
865
- if (fork === undefined) break
866
- fork.undecidable = { code: mutation.code, reason: mutation.reason, readings: mutation.readings ?? [] }
867
- break
868
- }
869
- case 'fork/abandoned': {
870
- const fork = next.forks.find((item) => item.id === mutation.fork)
871
- if (fork === undefined) break
872
- fork.abandoned = true
873
- fork.abandonReason = mutation.reason ?? null
874
- break
875
- }
876
- case 'continuation/set': {
877
- /**
878
- * 续跑窗口的账。四种状态就是「我们说过的话」的全部:
879
- * · `armed` —— 我们布了防(或按策略恢复了它):此刻它在为我们跑。
880
- * · `paused` —— **我们**按下的暂停,`why` 是真实理由(等裁决 / 等人门 / 阶段边界)。
881
- * · `stopped` —— 我们收的兵(目标达成 / 如实放弃 / 计划触礁)。
882
- * · `withdrawn` —— 它不在了,而**不是我们清的**(我们的清除永远与建同拍):
883
- * 这是平台上唯一说得出口的解释,所以它也只说这些,不说「谁」干的。
884
- * 一条记录:**窗口在会话里是单数**,所以后一条覆盖前一条,不留历史(历史在会话日志里)。
885
- */
886
- next.continuation = {
887
- state: typeof mutation.state === 'string' ? mutation.state : 'unknown',
888
- goal: mutation.goal ?? null,
889
- target: mutation.target ?? null,
890
- why: mutation.why ?? null,
891
- /**
892
- * 平台对象上那句身份文本的**最后一次观测值**:我们写下它时记我们的,
893
- * 别人改写之后重记他的。判据是「平台上的文本 === 这条记录」⇒ 还是我们的。
894
- */
895
- label: mutation.label ?? null,
896
- at,
897
- }
898
- break
899
- }
900
- /**
901
- * **领域词汇的六个事件**:接纳 / 版本化修订 / 黏性废止(概念与谓词各三条)。
902
- *
903
- * 折法在这里只做解释:把事件折成 `lexicon`。校验(引用是否存在、形状对不对、
904
- * 语义变化有没有偷偷走修订)全部发生在**落账之前**——预设侧的工具与宿主路由
905
- * 用的是 `domain-language.js` 的同一份判据;折法不重复判一遍,否则两份判据必然漂。
906
- */
907
611
  case 'ontology/term_added':
908
612
  case 'ontology/predicate_added':
909
613
  case 'ontology/term_revised':
@@ -972,19 +676,6 @@ export function applyMutation(state, mutation) {
972
676
  })
973
677
  break
974
678
  }
975
- /**
976
- * **跳级理由**(`level/skipped`):这一级为什么在本项目里不适用。
977
- *
978
- * 折法只记事实,不改派生:`derive()` 把被理由覆盖的层从 `untouchedLevels` 里减掉,
979
- * 于是「跳级没理由」这条缺口会自动消失——理由本身就是它的出口。
980
- */
981
- case 'level/skipped': {
982
- const hypothesis = next.hypotheses.find((item) => item.id === mutation.hypothesis)
983
- if (hypothesis === undefined) break
984
- hypothesis.skips = Array.isArray(hypothesis.skips) ? hypothesis.skips : []
985
- hypothesis.skips.push({ levels: Array.isArray(mutation.levels) ? mutation.levels.slice() : [], reason: mutation.reason ?? null, at })
986
- break
987
- }
988
679
  /**
989
680
  * **判据修订**(`criteria/revised`):改「怎样算完成」这件事本身。
990
681
  *
@@ -1063,6 +754,16 @@ function stampAt(mutations, time) {
1063
754
  * 宿主读已发布 V3 日志时把旧形状抬升成 `plugin:clearai`;但事件被**直接**喂进这个纯函数时
1064
755
  * (测试、旧导出、重放工具)仍带着旧形状,所以两侧都认。
1065
756
  */
757
+ /** 人写的一条消息是哪种语言(插件消息、续跑提示不算)。 */
758
+ function humanLanguage(message) {
759
+ if (message === null || typeof message !== 'object' || message.role !== 'user') return null
760
+ const kind = message.source?.kind
761
+ if (kind !== undefined && kind !== 'user') return null
762
+ const text = messageText(message)
763
+ if (text.startsWith(HUMAN_GATE_MARK)) return null
764
+ return detectLanguage(text)
765
+ }
766
+
1066
767
  function isClearaiSource(source) {
1067
768
  if (source === null || typeof source !== 'object' || !Array.isArray(source.sections)) return false
1068
769
  return source.kind === 'plugin:clearai' || (source.kind === 'plugin' && source.plugin === 'clearai')
@@ -1078,6 +779,13 @@ function isClearaiSource(source) {
1078
779
  */
1079
780
  export function applyEvent(state, event) {
1080
781
  if (event === null || typeof event !== 'object') return state
782
+ // 人说话用的语言:每一条人写的消息都可能改判(没有信号就不改)。这一拍折出来的文字(本体问题等)也用它。
783
+ const spoken = event.type === 'user/message' ? humanLanguage(event.data) : null
784
+ const base = spoken !== null && spoken !== state?.language ? { ...state, language: spoken } : state
785
+ return withLanguage(base?.language, () => foldEvent(base, event))
786
+ }
787
+
788
+ function foldEvent(state, event) {
1081
789
  if (event.type === 'user/message') {
1082
790
  // 内核观察到的事实(候选技能、采纳记录、合并目录)走**插件消息的结构化 section**——
1083
791
  // 记在会话日志里,所以状态仍然可以从日志重放出来,而不是靠一句散文。
@@ -1100,8 +808,6 @@ export function applyEvent(state, event) {
1100
808
  * 原来找到目录就 return,于是同一条消息里的**运行档被吃掉**,
1101
809
  * 表现是投影里的 effective 档永远停在「还没定档」,而机制那边早就按新档跑了。
1102
810
  */
1103
- const brain = source.sections.find((section) => section?.name === 'clearai/brain')
1104
- const tier = source.sections.find((section) => section?.name === 'clearai/autonomy')
1105
811
  /**
1106
812
  * 内核在**回合之间**观察到的事实(目前:世界线执行者跑完)。
1107
813
  * 与工具结果里的 `meta.mutations` **同形**,所以直接走同一个 `applyMutations`——
@@ -1122,28 +828,9 @@ export function applyEvent(state, event) {
1122
828
  }
1123
829
  }
1124
830
  const facts = source.sections.find((section) => section?.name === 'clearai/mutations')
1125
- if (brain !== undefined || tier !== undefined || facts !== undefined) {
831
+ if (facts !== undefined) {
1126
832
  let next = clone(state)
1127
833
  let touched = false
1128
- if (brain !== undefined && typeof brain.text === 'string') {
1129
- try {
1130
- const payload = JSON.parse(brain.text)
1131
- if (Array.isArray(payload?.candidates)) next.brainCandidates = payload.candidates
1132
- if (payload?.overview !== undefined) next.brain = payload.overview
1133
- // 章程读数:只有真的变了才会出现在 payload 里(内核那侧按文件系统指纹去重)。
1134
- if (payload?.constitution !== undefined) next.constitution = payload.constitution
1135
- // 合并目录:内核从宿主的 skills 服务取的那张表(变了才发)。
1136
- if (payload?.catalog !== null && typeof payload?.catalog === 'object' && Array.isArray(payload.catalog.entries)) {
1137
- next.skillCatalog = { complete: payload.catalog.complete === true, entries: payload.catalog.entries, at: typeof event.time === 'number' ? event.time : null }
1138
- }
1139
- for (const name of Array.isArray(payload?.promoted) ? payload.promoted : []) {
1140
- next.skillPromotions = [...(next.skillPromotions ?? []), { name, by: 'user', at: typeof event.time === 'number' ? event.time : Date.now() }]
1141
- }
1142
- touched = true
1143
- } catch {
1144
- // 坏 payload:如实少一条外脑事实,但不吞掉同一条消息里别的事实。
1145
- }
1146
- }
1147
834
  if (facts !== undefined && typeof facts.text === 'string') {
1148
835
  try {
1149
836
  const payload = JSON.parse(facts.text)
@@ -1155,61 +842,18 @@ export function applyEvent(state, event) {
1155
842
  // 同上:坏 payload 不当事实,也不影响别的。
1156
843
  }
1157
844
  }
1158
- if (tier !== undefined && typeof tier.text === 'string') {
1159
- try {
1160
- const payload = JSON.parse(tier.text)
1161
- const value = String(payload?.value ?? '')
1162
- if (value === 'attended' || value === 'unattended') {
1163
- next.autonomy = {
1164
- ...(next.autonomy ?? {}),
1165
- effective: { value, preset: String(payload?.preset ?? value), source: payload?.source === 'session' ? 'session' : 'preset', at: typeof event.time === 'number' ? event.time : null },
1166
- }
1167
- touched = true
1168
- }
1169
- } catch {
1170
- // 同上:坏 payload 不当事实,也不影响别的。
1171
- }
1172
- }
1173
845
  if (touched) return next
1174
846
  }
1175
847
  }
1176
- // 人在面板上按下的人门动作 → 落成**事实**(不是请求):确认计划就地生效;
1177
- // 世界线的裁决记在分叉上,由内核的 ConvergeFork/AbandonFork 落实(合并与清理是内核的活)。
848
+ // 旧日志里人在面板上按下的人门动作 → 照旧落成**事实**(面板已不再发)。
1178
849
  const gate = parseHumanGate(event.data)
1179
- // 人引用技能:一条含 `/名字` 的**用户消息**。这就是原生 pre-step 注入正文的判据,
1180
- // 所以「人用过它」不是我们的推测,而是这条消息本身。插件与注入消息都不算(署名必须是人)。
1181
- const quoted = source !== null && typeof source === 'object' && source.kind === 'user' ? invokedSkillNames(event.data) : []
1182
- if (gate === null && quoted.length === 0) return state
850
+ if (gate === null) return state
1183
851
  const at = typeof event.time === 'number' ? event.time : Date.now()
1184
852
  const next = clone(state)
1185
- for (const name of quoted) recordSkillUse(next, name, 'human', at, stepPointer(state))
1186
- if (gate === null) return next
1187
- // **旧日志容忍分支**(写入者已摘除,只有旧日志可能带着):人切运行档曾经是一条人门动作,旧会话里可能留着。
1188
- // 读到它就照旧记成一条**人的事实**,让历史会话仍然读得通;当前面板上已经没有这个开关,
1189
- // 新的会话不会再产生这一条。不要据此认为"现在还能切档"——要删这个分支得先确认没有旧日志。
1190
- if (gate.action === 'set_autonomy') {
1191
- const value = String(gate.value ?? '')
1192
- if (!AUTONOMY_VALUES.includes(value)) return next
1193
- next.autonomy = { ...(next.autonomy ?? {}), override: { value, at } }
1194
- return next
1195
- }
1196
853
  /**
1197
854
  * 人审查一条事实:标的在 `value`(面板送出的是事实 id),缘由在 `note`。
1198
855
  * 已经审过的不再改(第一次决定为准,与计划授权那条同一条纪律)。
1199
856
  */
1200
- /**
1201
- * 人认可一次**临时采纳**:它是那道门唯一的机械出口。
1202
- *
1203
- * 「临时采纳」的语义是「分差不足以称结论」;原来它只能靠人说话,而门开着会按住续跑
1204
- * ⇒ 什么都不做的话系统一直等。认可是**人的决定**,落一条 `by:'user'` 的确认留着痕迹;
1205
- * 「要改判据」那条路仍然在(说一句话,模型重做一条世界线)。
1206
- */
1207
- if (gate.action === 'confirm_provisional') {
1208
- const provisionalFork = next.forks.find((item) => item.id === (gate.fork ?? null))
1209
- if (provisionalFork?.merge?.provisional !== true || provisionalFork.merge.confirmed !== undefined) return next
1210
- provisionalFork.merge = { ...provisionalFork.merge, confirmed: { at, by: 'user' } }
1211
- return next
1212
- }
1213
857
  if (gate.action === 'retract_fact' || gate.action === 'keep_fact') {
1214
858
  const fact = next.facts.find((item) => item.id === (gate.value ?? null))
1215
859
  if (fact === undefined || fact.review !== undefined) return next
@@ -1239,31 +883,12 @@ export function applyEvent(state, event) {
1239
883
  next.lexicon = applyLexiconMutation(next.lexicon, { t: kind, ...entry, by: 'user' }, at)
1240
884
  return next
1241
885
  }
1242
- const fork = next.forks.find((item) => item.id === (gate.fork ?? null))
1243
- if (fork === undefined) return next
1244
- fork.humanDecision = {
1245
- action: gate.action,
1246
- branch: gate.branch ?? null,
1247
- note: gate.note ?? null,
1248
- at,
1249
- }
1250
886
  return next
1251
887
  }
1252
888
  if (event.type === 'tool/call') {
1253
889
  const data = event.data ?? {}
1254
890
  if (typeof data.name !== 'string') return state
1255
891
  const next = clone(state)
1256
- if (data.name === 'skill') {
1257
- // 模型加载技能 = 原生 `skill` 工具的一次调用。工具名与参数形状照原样认,不猜别的写法。
1258
- let args = null
1259
- try {
1260
- args = typeof data.arguments === 'string' ? JSON.parse(data.arguments) : data.arguments
1261
- } catch {
1262
- args = null
1263
- }
1264
- const name = args === null || typeof args !== 'object' || typeof args.name !== 'string' ? '' : args.name.trim()
1265
- if (name !== '') recordSkillUse(next, name, 'model', typeof event.time === 'number' ? event.time : 0, stepPointer(state))
1266
- }
1267
892
  if (data.name === 'write' || data.name === 'edit') {
1268
893
  let args = null
1269
894
  try {
@@ -1277,11 +902,6 @@ export function applyEvent(state, event) {
1277
902
  if (next.written.length > MAX_WRITTEN) next.written.splice(0, next.written.length - MAX_WRITTEN)
1278
903
  }
1279
904
  }
1280
- /**
1281
- * 会改工作区的那几件工具都记一笔:`write`/`edit` 是直接的,`bash`/`pwsh` 是间接的
1282
- * (脚本自己产出文件,内核看不见)。计数只增,判据留给用它的地方。
1283
- */
1284
- if (WRITE_CAPABLE_TOOLS.has(data.name)) next.writeCalls = (next.writeCalls ?? 0) + 1
1285
905
  next.inFlight = VERDICT_WAITERS.has(data.name) ? { name: data.name, callId: data.callId ?? null, at: typeof event.time === 'number' ? event.time : 0 } : null
1286
906
  return next
1287
907
  }
@@ -1316,10 +936,30 @@ export function applyEvent(state, event) {
1316
936
  /** 假设的终态:到了这里就不再是「还要继续做的活」。 */
1317
937
  const TERMINAL_HYPOTHESIS = new Set(['refuted', 'superseded', 'retracted'])
1318
938
 
939
+ /**
940
+ * **断言主体落图了吗**:一条判断的断言里,主体还不是实体图节点的那些(去重)。
941
+ *
942
+ * 节点有几种来路,都带出处或独立裁决:实体文件(`clear/ontology/entities/`)里的实体与它们的关系、升格事实里的主体,
943
+ * 以及它们以 instance 形态引出的宾语。`derive()` 把读数挂在每条判断上(`unlanded`),
944
+ * 缺口 ③ 与 `Conclude` 的实体门读的都是它——「卡上说落了」与「门说没落」不会出现两种读数。
945
+ */
946
+ function subjectsOffGraph(hypothesis, nodes) {
947
+ const seen = new Map()
948
+ for (const assertion of Array.isArray(hypothesis?.assertions) ? hypothesis.assertions : []) {
949
+ const id = String(assertion?.subject?.id ?? '').trim()
950
+ const type = String(assertion?.subject?.type ?? '').trim()
951
+ if (id === '' || type === '') continue
952
+ const key = `${type}|${id}`
953
+ if (seen.has(key) || nodes.has(key)) continue
954
+ seen.set(key, { id, type })
955
+ }
956
+ return [...seen.values()]
957
+ }
958
+
1319
959
  /**
1320
960
  * **知识模式(分诊)与缺口读数。**
1321
961
  *
1322
- * 判据是**结构的,不是词法的**:这里不猜「这句话像不像研究任务」。立约(`SetGoal`)并用
962
+ * 判据是**结构的,不是词法的**:这里不猜「这句话像不像研究任务」。立约(`Frame`)并用
1323
963
  * 相互竞争的假设登记它,是模型自己已经做出的那次承诺——它意味着这件事要跨多轮、要有依据、
1324
964
  * 要有可复核的结论。日常问答从不立约,于是从不进这一档。用词面启发式去猜任务类型,
1325
965
  * 正是真跑里评估者抓到的那类「硬编码比例」的老路:猜错了没人能复核,而结构判据可以。
@@ -1344,48 +984,11 @@ export function deriveKnowledge(state, hypotheses, factRows, lexicon) {
1344
984
  const gaps = []
1345
985
  if (mode === 'knowledge') {
1346
986
  /**
1347
- * ① 语言还没立起来:跨轮复用与「按概念取用已知」都要求先有词汇。
1348
- * 判据是**两个都空**——只有概念没有谓词时,关系还说不出来,但语言已经开张了,
1349
- * 那是进展不是缺口(否则每注册一个概念就多一条永远擦不掉的抱怨)。
1350
- */
1351
- if (terms.length === 0 && predicates.length === 0) {
1352
- gaps.push({
1353
- code: 'no_language',
1354
- count: registered.length,
1355
- detail: `${registered.length} 条在验命题,但还没有任何概念与谓词:换一轮只能靠重读散文取用它们`,
1356
- nextAction: '先 RegisterTerm 立词,再 RegisterPredicate 说清它们之间是什么关系',
1357
- })
1358
- }
1359
- /**
1360
- * ② 只有散文主张的命题:断言是可选的(「加法,不是门槛」),所以这里**不是违规**,
1361
- * 而是「这条主张还不能被机器比对」。只算非终态的:被推翻/被替代的不再欠这一笔。
1362
- */
1363
- const proseOnly = registered.filter((item) => !Array.isArray(item.assertions) || item.assertions.length === 0)
1364
- if (proseOnly.length > 0) {
1365
- gaps.push({
1366
- code: 'prose_only_claims',
1367
- count: proseOnly.length,
1368
- detail: `${proseOnly.length} 条在验命题只有散文主张:两条结论是不是在说同一件事,只能靠重读判断`,
1369
- nextAction: '用 SetGoal 的修订把这条主张写成断言(主词–谓词–宾语),引用已登记的 id',
1370
- })
1371
- }
1372
- /**
1373
- * ③ **升格时没带断言**。只算 0.2.0 那条路走出来的事实(`hypothesis` 已关联)——
1374
- * 更早的事实补不上断言(见已知缺口),把它们算成欠账就是一条永远还不掉的抱怨。
1375
- */
1376
- const unstructured = factRows.filter((fact) => typeof fact.hypothesis === 'string' && fact.hypothesis !== '' && (!Array.isArray(fact.assertions) || fact.assertions.length === 0))
1377
- if (unstructured.length > 0) {
1378
- gaps.push({
1379
- code: 'unstructured_facts',
1380
- count: unstructured.length,
1381
- detail: `${unstructured.length} 条已升格事实没有断言:它们进不了实体图,也不能按概念取用`,
1382
- nextAction: '下次升格时带上 assertions;这条老事实的形态靠新一次升格补',
1383
- })
1384
- }
1385
- /**
1386
- * ④ **从没被证据碰过的命题**。只在这次会话已经真的跑出过证据之后才报——计划刚立、
1387
- * 一步都还没走时,所有假设都是「没碰过」,那是正常的起点而不是缺口。
1388
- * 它和结案时那条 `unjudged` 是同一条先例:**不逼裁决,但不许把「没看过」写成「没问题」**。
987
+ * 缺口只留三种,都是「这一轮的结论还不能被复用」的直接读数,每种都指得到今天就能补的动作。
988
+ *
989
+ * ① **从没被证据碰过的判断**。只在这次会话已经真的跑出过证据之后才报——计划刚立、
990
+ * 一步都还没走时,所有判断都是「没碰过」,那是正常的起点而不是缺口。
991
+ * 不逼裁决,但不许把「没看过」写成「没问题」。
1389
992
  */
1390
993
  if (Array.isArray(state?.evidence) && state.evidence.length > 0) {
1391
994
  const untouched = registered.filter((item) => item.refutations + item.inconclusive === 0 && item.supportedLevel === null)
@@ -1393,89 +996,57 @@ export function deriveKnowledge(state, hypotheses, factRows, lexicon) {
1393
996
  gaps.push({
1394
997
  code: 'untouched_claims',
1395
998
  count: untouched.length,
1396
- detail: `${untouched.length} 条在验命题还没有任何证据碰过(支持 / 推翻 / 无法判定都算碰过)`,
1397
- nextAction: '给它派一个带 tests 的步骤并交付:支持 / 推翻 / 无法判定都算碰过',
999
+ detail: tr(`${untouched.length} 条验证中的判断还没有任何证据碰过(支持 / 推翻 / 不确定都算碰过)`, `${untouched.length} judgments under test have not been touched by any evidence (support, refute and inconclusive all count)`),
1000
+ nextAction: tr('给它派一个带 tests 的步骤并交付:支持 / 推翻 / 无法判定都算碰过', 'Give it a step with tests and deliver it: support, refute and inconclusive all count'),
1398
1001
  })
1399
1002
  }
1400
1003
  }
1401
1004
  /**
1402
- * ⑤ **实体层空着**。判据是整数可清点的:非终态命题的断言主体去重数(wanted)与实体层
1403
- * 节点数。两个数都从账本现算,没有一个字是猜的。
1404
- *
1405
- * 为什么必须有这一条:实体层从前是升格的副产品,于是「断言齐备、门槛也放行」却
1406
- * 因为另一条无关判据被卡住时,卡上四项全绿而**实体图是空的**——「看得见的边界」
1407
- * 在这里漏过一整层。诚实出口是两条:登记并落断言,或如实说清不值得留下形态。
1408
- */
1409
- const assertions = registered.flatMap((item) => (Array.isArray(item.assertions) ? item.assertions : []))
1410
- const wantedKeys = new Set(assertions.map((assertion) => `${String(assertion?.subject?.type ?? '')}|${String(assertion?.subject?.id ?? '')}`).filter((key) => key !== '|'))
1411
- /**
1412
- * **判据是"每一个主体都落地了吗",不是"图上有没有东西"。**
1413
- *
1414
- * 弱判据(实体层节点数 === 0)有一个便宜的绕法:随便登记一个**无关**节点,
1415
- * 缺口就消失,而真正该落地的那几个主体仍然只在命题上。于是这道门形同虚设。
1416
- * 所以这里逐个主体判它有没有落成实体层节点,`count` 就是**还没落地的个数**。
1005
+ * ② 只有散文主张的判断:断言是可选的,所以这里**不是违规**,也不拦结案,
1006
+ * 而是「这条主张还不能被机器比对、下一轮也按概念取不到」。只算非终态的。
1417
1007
  */
1008
+ const proseOnly = registered.filter((item) => !Array.isArray(item.assertions) || item.assertions.length === 0)
1009
+ if (proseOnly.length > 0) {
1010
+ gaps.push({
1011
+ code: 'prose_only_claims',
1012
+ count: proseOnly.length,
1013
+ detail: tr(`${proseOnly.length} 条验证中的判断只有散文主张:两条结论是不是在说同一件事,只能靠重读判断`, `${proseOnly.length} judgments under test are prose only: whether two conclusions say the same thing can only be judged by rereading them`),
1014
+ nextAction: tr('先在 clear/ontology/ 下写概念、关系与主体的实体文件,再用 Frame 修订把主张写成断言(主词–谓词–宾语)', 'First write concept, relation and subject entity files under clear/ontology/, then revise with Frame to turn the claim into assertions (subject–predicate–object)'),
1015
+ })
1016
+ }
1418
1017
  /**
1419
- * **"落地"的判据是"这句断言在图上有边",不是"图上有这个节点"。**
1018
+ * ③ **断言主体没落图**。判据与 `Conclude` 的实体门是同一条(每条判断上的 `unlanded`):
1019
+ * 断言的主体是不是实体图上带出处的节点。逐个主体判,`count` 就是还没落地的个数——
1020
+ * 随便登记一个无关节点不会让它消失。
1420
1021
  *
1421
- * 只数节点会漏掉最要命的那种形态:实例登记了(图上有节点),而关于它的那句话还挂在命题上
1422
- * ——图有节点、没有边,"实体图长出来了"仍然是一句空话。所以这里取**断言边的起点集合**:
1423
- * 一条边要么来自带出处的实体断言(`Assert`),要么来自过了独立裁决的升格事实。
1022
+ * 为什么看节点、不看边:边由升格本身落下(事实带着它的断言进图),
1023
+ * 要求在升格之前另在实体文件里把同一句话再写一遍,只是让模型重复劳动。
1424
1024
  */
1425
- const graph = graphProjection(state)
1426
- const landedKeys = new Set(graph.edges.filter((edge) => edge.kind === 'assertion').map((edge) => String(edge.from)))
1427
- const unlanded = [...wantedKeys].filter((key) => !landedKeys.has(`term:${key}`) && !landedKeys.has(key))
1025
+ const unlanded = [...new Map(registered.flatMap((item) => item.unlanded ?? []).map((item) => [`${item.type}|${item.id}`, item])).values()]
1428
1026
  if (unlanded.length > 0) {
1429
1027
  gaps.push({
1430
1028
  code: 'entities_unlanded',
1431
1029
  count: unlanded.length,
1432
- detail: `${unlanded.length} 个断言主体还没有落到实体图(共 ${wantedKeys.size} 个):断言只挂在命题上,不构成「已知」`,
1433
- nextAction: '先 RegisterInstance 把实例连出处登记下来;确实不值得留下形态就如实说清',
1030
+ detail: tr(
1031
+ `${unlanded.length} 个断言主体还没有落到实体图(${unlanded.map((item) => `${item.type}|${item.id}`).join('、')}):断言只挂在命题上,不构成「已知」`,
1032
+ `${unlanded.length} assertion subjects are not on the entity graph yet (${unlanded.map((item) => `${item.type}|${item.id}`).join(', ')}): the assertions hang on the proposition only and are not "known"`,
1033
+ ),
1034
+ nextAction: tr(
1035
+ '给这些主体各写一个实体文件(clear/ontology/entities/<id>.json,带类型与出处);确实不值得留下形态就把断言从判断上拿掉(Frame 修订)',
1036
+ 'Write an entity file for each subject (clear/ontology/entities/<id>.json, with type and source); if one is really not worth keeping, drop the assertion from the judgment (Frame revision)',
1037
+ ),
1434
1038
  })
1435
1039
  }
1436
- /**
1437
- * ⑥ **跳级没有理由**。`derive()` 已经把写明理由的层从 `untouchedLevels` 里减掉,
1438
- * 所以这里数出来的每一个都是真的欠一句解释的层——出口只有一条:
1439
- * 用 `ExplainLevelSkip` 写清「为什么这一级在本项目里不适用」。
1440
- */
1441
- const skipped = registered
1442
- .map((item) => ({ id: item.id, levels: Array.isArray(item.untouchedLevels) ? item.untouchedLevels : [] }))
1443
- .filter((item) => item.levels.length > 0)
1444
- const skippedCount = skipped.reduce((sum, item) => sum + item.levels.length, 0)
1445
- if (skippedCount > 0) {
1446
- gaps.push({
1447
- code: 'levels_skipped',
1448
- count: skippedCount,
1449
- detail: `${skippedCount} 处跳级没有理由(${skipped.map((item) => `命题 ${item.id} 缺 ${item.levels.join('/')}`).join(';')})`,
1450
- nextAction: '用 ExplainLevelSkip 写明「为什么这一级在本项目里不适用」',
1451
- })
1452
- }
1453
- /**
1454
- * ⑦ **注册了却没人用的概念**。它和货架上「零引用的概念」那一节读**同一份**引用面
1455
- * (`termUsage`),所以两处不会出现「货架说没人用、缺口说用了」这种两种读数。
1456
- */
1457
- if (terms.length > 0 && registered.length > 0) {
1458
- const usage = termUsage(lexicon, factRows, Array.isArray(state?.entityAssertions) ? state.entityAssertions : [])
1459
- const orphans = terms.filter((term) => term.status !== 'deprecated' && (usage.get(term.id) ?? 0) === 0)
1460
- if (orphans.length > 0) {
1461
- gaps.push({
1462
- code: 'orphan_terms',
1463
- count: orphans.length,
1464
- detail: `${orphans.length} 个概念没有任何结论引用它们(它们还只是约定,不是已知)`,
1465
- nextAction: '要么在断言里用起来,要么在货架上如实标出「未被引用」',
1466
- })
1467
- }
1468
- }
1469
1040
  }
1470
1041
  return {
1471
1042
  mode,
1472
1043
  /** 一句人话:为什么进了这一档(或为什么没进)。它进卡,所以不许写成术语。 */
1473
1044
  why:
1474
1045
  mode === 'knowledge'
1475
- ? `目标还开着,带着 ${registered.length} 条登记过的命题——这是跨轮、要依据、要可复核结论的活`
1046
+ ? tr(`目标还开着,带着 ${registered.length} 条登记过的命题——这是跨轮、要依据、要可复核结论的活`, `The goal is open with ${registered.length} registered propositions: work across turns that needs evidence and checkable conclusions`)
1476
1047
  : open
1477
- ? '目标还开着,但没有登记命题:按普通任务推进'
1478
- : '没有开着的目标:按普通任务推进',
1048
+ ? tr('目标还开着,但没有登记命题:按普通任务推进', 'The goal is open but has no registered propositions: proceed as an ordinary task')
1049
+ : tr('没有开着的目标:按普通任务推进', 'No open goal: proceed as an ordinary task'),
1479
1050
  gaps,
1480
1051
  }
1481
1052
  }
@@ -1483,10 +1054,10 @@ export function deriveKnowledge(state, hypotheses, factRows, lexicon) {
1483
1054
  /**
1484
1055
  * **知识预检(preflight)**:进入知识模式那一刻,把「已知」主动送到模型面前。
1485
1056
  *
1486
- * 解决的问题:真跑里模型不查就开工——不是因为不知道有 \`QueryKnowledge\`,
1057
+ * 解决的问题:真跑里模型不查就开工——不是因为没有查询的入口,
1487
1058
  * 而是因为**没人提醒它此刻该查**。提示词会被读成建议;卡里的读数不会。
1488
1059
  * 于是这里把相关性判断做成投影:从当前目标与命题的文本出发,圈出**有界**的一组
1489
- * 已有词汇、事实与冲突,随运行态卡注入。模型只在这份摘要不够用时才需要精确查询。
1060
+ * 已有词汇、事实与冲突,随运行态卡注入。模型只在这份摘要不够用时才需要去读全文。
1490
1061
  *
1491
1062
  * 三条纪律(与缺口读数同一套):
1492
1063
  * · **只读**:不产生变更,不新增状态——它就是 \`derive\` 的另一个读面;
@@ -1707,12 +1278,12 @@ function factChain(context, fact) {
1707
1278
  */
1708
1279
  function factHistory(fact, evidence) {
1709
1280
  const events = []
1710
- if (fact?.at !== undefined && fact?.at !== null) events.push({ kind: 'fact/promoted', at: fact.at, summary: `升格为事实(支持到 ${fact.level ?? '—'})` })
1281
+ if (fact?.at !== undefined && fact?.at !== null) events.push({ kind: 'fact/promoted', at: fact.at, summary: tr(`升格为事实(支持到 ${fact.level ?? '—'})`, `Promoted to fact (supported to ${fact.level ?? '—'})`) })
1711
1282
  if (fact?.review !== undefined && fact?.review !== null) {
1712
1283
  events.push({
1713
1284
  kind: 'fact/reviewed',
1714
1285
  at: fact.review.at ?? null,
1715
- summary: fact.review.decision === 'retracted' ? '人审查后**撤回**(记录保留)' : '人审查后**维持**(判证据不可靠)',
1286
+ summary: fact.review.decision === 'retracted' ? tr('人审查后**撤回**(记录保留)', 'A person reviewed it and **retracted** it (the record is kept)') : tr('人审查后**维持**(判证据不可靠)', 'A person reviewed it and **kept** it (the evidence was judged unreliable)'),
1716
1287
  reason: fact.review.reason ?? null,
1717
1288
  })
1718
1289
  }
@@ -1723,12 +1294,12 @@ function factHistory(fact, evidence) {
1723
1294
  /** 词汇条目的留痕:登记 / 历次修订 / 废止。登记与修订本来就存在词条里(折法保留的)。 */
1724
1295
  function entryHistory(entry, label) {
1725
1296
  const events = []
1726
- if (entry?.at !== undefined && entry?.at !== null) events.push({ kind: `${label}_added`, at: entry.at, summary: `登记(依据:${entry.basis ?? '—'})`, by: entry.by ?? null })
1297
+ if (entry?.at !== undefined && entry?.at !== null) events.push({ kind: `${label}_added`, at: entry.at, summary: tr(`登记(依据:${entry.basis ?? '—'})`, `Registered (basis: ${entry.basis ?? '—'})`), by: entry.by ?? null })
1727
1298
  for (const revision of Array.isArray(entry?.revisions) ? entry.revisions : []) {
1728
- events.push({ kind: `${label}_revised`, at: revision.at ?? null, summary: `修订到 v${revision.version ?? '?'}`, reason: revision.reason ?? null, by: revision.by ?? null })
1299
+ events.push({ kind: `${label}_revised`, at: revision.at ?? null, summary: tr(`修订到 v${revision.version ?? '?'}`, `Revised to v${revision.version ?? '?'}`), reason: revision.reason ?? null, by: revision.by ?? null })
1729
1300
  }
1730
1301
  if (entry?.deprecated !== undefined && entry?.deprecated !== null) {
1731
- events.push({ kind: `${label}_deprecated`, at: entry.deprecated.at ?? null, summary: '废止(黏性终态,没有复活)', reason: entry.deprecated.reason ?? null, by: entry.deprecated.by ?? null })
1302
+ events.push({ kind: `${label}_deprecated`, at: entry.deprecated.at ?? null, summary: tr('废止(黏性终态,没有复活)', 'Deprecated (final; it does not come back)'), reason: entry.deprecated.reason ?? null, by: entry.deprecated.by ?? null })
1732
1303
  }
1733
1304
  return events.filter((event) => event.at !== null || event.kind.endsWith('_deprecated')).sort((left, right) => (left.at ?? 0) - (right.at ?? 0))
1734
1305
  }
@@ -1786,7 +1357,7 @@ function inspectConcept(context, termId) {
1786
1357
  history: entryHistory(term, 'term'),
1787
1358
  actions: { canFilter: true, canExpand: true, canEdit: true },
1788
1359
  /** 词汇是**约定**,不需要证据等级——把它说清楚,免得读的人以为它「没验」。 */
1789
- note: '概念是约定,不是主张:它不带证据等级。用这个词写下的句子才需要。',
1360
+ note: tr('概念是约定,不是主张:它不带证据等级。用这个词写下的句子才需要。', 'A concept is a convention, not a claim: it carries no evidence level. Only sentences written with it do.'),
1790
1361
  }
1791
1362
  }
1792
1363
 
@@ -1807,7 +1378,7 @@ function inspectValueType(context, form) {
1807
1378
  /** 值形态由系统固定,不进领域本体、也没有版本史——如实说空,不编一段。 */
1808
1379
  history: [],
1809
1380
  actions: { canFilter: true, canExpand: false, canEdit: false },
1810
- note: '值形态由系统固定(statement / quantity / formula / code / reference),不独立治理,所以没有版本史。',
1381
+ note: tr('值形态由系统固定(statement / quantity / formula / code / reference),不独立治理,所以没有版本史。', 'Value forms are fixed by the system (statement / quantity / formula / code / reference); they are not governed separately, so they have no version history.'),
1811
1382
  }
1812
1383
  }
1813
1384
 
@@ -1851,7 +1422,7 @@ function inspectPredicate(context, predicateId) {
1851
1422
  .map((conflict) => ({ predicate: conflict.predicate, subject: conflict.subject, sides: conflict.sides.map((side) => ({ fact: side.fact ?? null, value: side.value ?? null })) })),
1852
1423
  history: entryHistory(predicate, 'predicate'),
1853
1424
  actions: { canFilter: true, canExpand: true, canEdit: true },
1854
- note: predicate.functional === true ? '单值谓词:同一主词上两条未撤回的确认事实取值不同时,系统给出一对冲突读数(只暴露,不裁决)。' : null,
1425
+ note: predicate.functional === true ? tr('单值谓词:同一主词上两条未撤回的确认事实取值不同时,系统给出一对冲突读数(只暴露,不裁决)。', 'Single-valued predicate: when two unretracted confirmed facts give different values for the same subject, the system reports a conflict pair (surfaced, not resolved).') : null,
1855
1426
  }
1856
1427
  }
1857
1428
 
@@ -1903,7 +1474,7 @@ function inspectInstance(context, key) {
1903
1474
  .map((conflict) => ({ predicate: conflict.predicate, subject: conflict.subject, sides: conflict.sides.map((side) => ({ fact: side.fact ?? null, value: side.value ?? null })) })),
1904
1475
  history: [],
1905
1476
  actions: { canFilter: true, canExpand: true, canEdit: false },
1906
- note: '实例由断言投影出来,不单独注册、也不做实体消解(同名即同节点);它没有自己的版本史。',
1477
+ note: tr('实例由断言投影出来,不单独注册、也不做实体消解(同名即同节点);它没有自己的版本史。', 'Instances are projected from assertions; they are not registered separately and not resolved (same name means same node); they have no version history of their own.'),
1907
1478
  }
1908
1479
  }
1909
1480
 
@@ -1944,7 +1515,7 @@ function inspectLiteral(context, key) {
1944
1515
  conflicts: [],
1945
1516
  history: [],
1946
1517
  actions: { canFilter: true, canExpand: true, canEdit: false },
1947
- note: '字面值是断言里的客体,由事实投影出来;它没有独立生命周期。',
1518
+ note: tr('字面值是断言里的客体,由事实投影出来;它没有独立生命周期。', 'A literal value is the object of an assertion, projected from facts; it has no lifecycle of its own.'),
1948
1519
  }
1949
1520
  }
1950
1521
 
@@ -1980,18 +1551,42 @@ function inspectEdge(context, edgeId) {
1980
1551
  conflicts: chain.conflicts,
1981
1552
  history: chain.history,
1982
1553
  actions: { canFilter: true, canExpand: true, canEdit: false },
1983
- note: predicate?.functional === true ? '这条边落在单值谓词上:同一主词出现第二个不同取值时会产生冲突(只暴露,不裁决)。' : null,
1554
+ note: predicate?.functional === true ? tr('这条边落在单值谓词上:同一主词出现第二个不同取值时会产生冲突(只暴露,不裁决)。', 'This edge uses a single-valued predicate: a second, different value for the same subject produces a conflict (surfaced, not resolved).') : null,
1555
+ }
1556
+ }
1557
+
1558
+
1559
+ /**
1560
+ * **可信度怎么变的**:按证据落账的先后重放一遍,每一笔记下「之前 → 之后」。
1561
+ * 面板「点开一条」的第二段读的就是它;分组判据仍只有 `trustOf` 一处,这里只是逐笔调用。
1562
+ * 升格(写进长期知识)和撤回不在这里——它们由读的一侧接在末尾(见 `view`)。
1563
+ */
1564
+ function trustHistory(hypothesis, rows) {
1565
+ const history = [{ kind: 'proposed', at: hypothesis.at ?? null, to: 'testing' }]
1566
+ let replay = { status: 'proposed', supportedLevel: null, refutations: 0, inconclusive: 0 }
1567
+ const ordered = rows
1568
+ .map((item, index) => ({ item, index }))
1569
+ .sort((left, right) => (left.item.at ?? 0) - (right.item.at ?? 0) || left.index - right.index)
1570
+ .map(({ item }) => item)
1571
+ for (const item of ordered) {
1572
+ const from = trustOf(replay)
1573
+ if (item.verdict === 'refute') replay = { ...replay, refutations: replay.refutations + 1 }
1574
+ else if (item.verdict === 'inconclusive') replay = { ...replay, inconclusive: replay.inconclusive + 1 }
1575
+ else if (item.verdict === 'support' && LEVELS.indexOf(String(item.level ?? '').toUpperCase()) > LEVELS.indexOf(String(replay.supportedLevel ?? '').toUpperCase())) replay = { ...replay, supportedLevel: item.level }
1576
+ history.push({ kind: 'evidence', id: item.id ?? null, at: item.at ?? null, plan: item.plan ?? null, step: item.step ?? null, verdict: item.verdict, level: item.level ?? null, evaluator: item.evaluator ?? null, basis: item.basis ?? null, from, to: trustOf(replay) })
1984
1577
  }
1578
+ return history
1985
1579
  }
1986
1580
 
1987
1581
  /** 五种值形态的名字与一句话解释(名字取自 `domain-language` 的枚举,这里只加给人读的说明)。 */
1988
- const VALUE_FORM_GLOSS = {
1989
- statement: '短陈述字符串',
1990
- quantity: '数值 + 单位',
1991
- formula: '公式源码(LaTeX;本版不做语义解析)',
1992
- code: '指向工作区里真有的文件路径',
1993
- reference: '外部引用(文献 / URL / 编号)',
1582
+ const VALUE_FORM_GLOSS_TEXT = {
1583
+ statement: ['短陈述字符串', 'A short statement string'],
1584
+ quantity: ['数值 + 单位', 'A number plus a unit'],
1585
+ formula: ['公式源码(LaTeX;本版不做语义解析)', 'Formula source (LaTeX; its meaning is not parsed)'],
1586
+ code: ['指向工作区里真有的文件路径', 'A path to a file that exists in the workspace'],
1587
+ reference: ['外部引用(文献 / URL / 编号)', 'An external reference (paper / URL / identifier)'],
1994
1588
  }
1589
+ const VALUE_FORM_GLOSS = bilingual(VALUE_FORM_GLOSS_TEXT)
1995
1590
 
1996
1591
  export function derive(state) {
1997
1592
  const activePlan = state.plans.find((plan) => plan.status === 'active') ?? null
@@ -2012,11 +1607,16 @@ export function derive(state) {
2012
1607
  .map((item) => item.hypothesis)
2013
1608
  .filter((id) => typeof id === 'string' && id !== ''),
2014
1609
  )
1610
+ const instanceNodes = new Set(graphProjection(state).nodes.filter((node) => node.kind === 'instance').map((node) => String(node.id)))
2015
1611
  const hypotheses = state.hypotheses.map((hypothesis) => {
2016
1612
  const rows = []
2017
1613
  for (const item of state.evidence) {
1614
+ if (item.hypothesis !== undefined) {
1615
+ if (item.hypothesis === hypothesis.id) rows.push(item)
1616
+ continue
1617
+ }
2018
1618
  const step = stepOf(item.plan, item.step)
2019
- if (step?.tests?.hypothesis === hypothesis.id) rows.push(item)
1619
+ if ((normalizeTests(step?.tests)?.hypotheses ?? []).includes(hypothesis.id)) rows.push(item)
2020
1620
  }
2021
1621
  const refutations = rows.filter((item) => item.verdict === 'refute').length
2022
1622
  const inconclusive = rows.filter((item) => item.verdict === 'inconclusive').length
@@ -2029,40 +1629,13 @@ export function derive(state) {
2029
1629
  else if (status === 'proposed' && rows.length > 0) status = 'alive'
2030
1630
  // 被推翻是黏性终态:「已被替代」不该改写「已被推翻」——两件事。
2031
1631
  if (status !== 'superseded' && status !== 'refuted' && status !== 'retracted' && refutations > 0) status = 'refuted'
2032
- /**
2033
- * **从没被走过的等级**(派生,零新账)。
2034
- *
2035
- * 等级衡量的是「这条结论在多大程度上只能靠信任做的人」,而它逐级上升的补偿是
2036
- * 独立裁决与人放行。所以「我直接在 L3 上交付、L0/L1/L2 从没走过」本身不是违规
2037
- * (首次测量没有廉价路可走),但**它必须看得见**——与「假设从没被证据碰过」记成
2038
- * `unjudged` 是同一条先例:不逼裁决,但不许把「没看过」写成「没问题」。
2039
- *
2040
- * 只列**低于已用到过的最高等级**、且一条证据都没有的那些级;没走过任何等级时为空
2041
- * (还没有声明可谈)。
2042
- */
2043
- const used = new Set(rows.map((item) => String(item.level ?? '').toUpperCase()))
2044
- const top = [...used].reduce((best, level) => Math.max(best, LEVELS.indexOf(level)), -1)
2045
- /**
2046
- * **写明理由的跳级不算「没走过」**:`level/skipped` 记的就是「这一级在本项目里
2047
- * 为什么不适用」。所以减掉它——于是 `levels_skipped` 缺口与卡上那一行读的都是
2048
- * 减完的结果:「写明理由」是那条缺口唯一的出口,而且它真的会让缺口消失。
2049
- */
2050
- const skipCovered = new Set(
2051
- (Array.isArray(hypothesis.skips) ? hypothesis.skips : [])
2052
- .flatMap((skip) => (Array.isArray(skip?.levels) ? skip.levels : []))
2053
- .map((level) => String(level).toUpperCase()),
2054
- )
2055
- const untouchedLevels = top <= 0 ? [] : LEVELS.slice(0, top).filter((level) => !used.has(level) && !skipCovered.has(level))
2056
- return { ...hypothesis, status, supportedLevel: supportedLevel < 0 ? null : `L${supportedLevel}`, refutations, inconclusive, untouchedLevels }
1632
+ return { ...hypothesis, status, supportedLevel: supportedLevel < 0 ? null : `L${supportedLevel}`, refutations, inconclusive, unlanded: subjectsOffGraph(hypothesis, instanceNodes), history: trustHistory(hypothesis, rows) }
2057
1633
  })
2058
1634
 
2059
1635
  /**
2060
1636
  * `plan_is_authorized`:有记号 **或** 已经真的推进过(行为即授权)。
2061
1637
  * 第二个分支在 ClearAI 那边是给存量文档用的;这里同样留着——事实与意图冲突时以事实为准。
2062
1638
  */
2063
- const planIsAuthorized = (plan) =>
2064
- plan !== null && (plan.confirmed_at !== null || plan.steps.some((step) => step.status !== 'open'))
2065
- const planConfirmationPending = activePlan !== null && activePlan.status === 'active' && !planIsAuthorized(activePlan)
2066
1639
  let phase = null
2067
1640
  let progress = null
2068
1641
  if (state.goal !== null) {
@@ -2106,140 +1679,41 @@ export function derive(state) {
2106
1679
  }
2107
1680
  }
2108
1681
 
2109
- /**
2110
- * 世界线的**派生状态**(语义:见 recon 的「四条结束方式」):
2111
- *
2112
- * · `failed` —— 执行者没跑成(`worldline/executed{ok:false}`)。**它不是落选**:
2113
- * 落选意味着它跑完了、被尺子排到了后面;失败是「世界没给它机会」。原来的实现在这两件事上
2114
- * 共用一个状态(都还是 exploring),于是界面会一直为一条死掉的线呼吸——那是说假话。
2115
- * · `orphaned` —— 承载它的**步骤被作废**了,而它还没收口。它既不是被人裁掉(没人做过这个决定),
2116
- * 也不是被算术排掉(没有尺子排过它):它是「随承诺撤回而终止」。这是两个已存在事实的**推论**,
2117
- * 不是第五种存储状态——零新账,也**不改写历史**(绝不把它写成「已放弃」)。
2118
- */
2119
- const forks = state.forks.map((fork) => {
2120
- const owner = (activePlan ?? closedPlans[closedPlans.length - 1] ?? null)?.steps.find((step) => step.id === fork.step) ?? null
2121
- const ownerVoided = owner !== null && owner.status === 'void'
2122
- return {
2123
- ...fork,
2124
- ownerStatus: owner === null ? null : owner.status,
2125
- ownerVoidReason: ownerVoided ? (owner.voidReason ?? null) : null,
2126
- orphaned: ownerVoided && !fork.settled && !fork.abandoned,
2127
- phase: fork.abandoned
2128
- ? 'abandoned'
2129
- : fork.settled
2130
- ? 'settled'
2131
- : ownerVoided
2132
- ? 'orphaned'
2133
- : fork.branches.every((branch) => (BRANCH_RANK[branch.status] ?? 0) >= BRANCH_RANK.evaluated)
2134
- ? 'deciding'
2135
- : 'exploring',
2136
- branches: fork.branches.map((branch) => ({
2137
- ...branch,
2138
- // 派生,不落库:执行没跑成,而状态还停在探索中。
2139
- failed: branch.status === 'exploring' && branch.execution !== null && branch.execution !== undefined && branch.execution.ok === false,
2140
- orphaned: ownerVoided && !fork.settled && !fork.abandoned && branch.status === 'exploring',
2141
- /**
2142
- * **执行者未归**(与「失败」「孤儿」同一族的假话——不说破就会被当成还在跑):
2143
- *
2144
- * 执行者是异步派的,所以「它在跑」是一条会过期的状态。分叉一旦收口(收敛或放弃),
2145
- * 那条世界线就**没有归宿**了——回灌也进不了任何决定。可 `execution.ok` 仍是 `null`,
2146
- * 于是卡片会永远写「(执行中,结论会自动回灌)」:**一句实现了不了的承诺**。
2147
- *
2148
- * 两条已有事实的推论:分叉是终局 + 执行者没报过。零新账,也不改写 `execution` 本身
2149
- * (它仍然是「没报过」这个已发生的事实)。
2150
- */
2151
- unreturned: (fork.settled === true || fork.abandoned === true) && branch.execution !== null && branch.execution !== undefined && branch.execution.ok === null,
2152
- })),
2153
- }
2154
- })
2155
-
2156
- /**
2157
- * 收件箱:每一道门都是**状态锚**(由派生事实算出,不依赖中断记录)——门一解决,条目自然消失,
2158
- * 不可能残留成僵尸。
2159
- * 条目只带**分诊信息**(标题/摘要/指向),不带全部正文:收件箱是分诊,不是问诊。
2160
- *
2161
- * 这里**只放真门**:不拍板就真的推不动的那种。计划确认不再是条目(它是记号,
2162
- * 不是闸门;见 HUMAN_GATE_ACTIONS 的收敛记录)——收件箱里的每一条都经得起「等人是必须的吗」。
2163
- */
2164
- const inbox = []
2165
- for (const fork of forks) {
2166
- // 人已经裁决过的不再等他(ClearAI 的条目是状态锚:状态一变,条目自然消失)。
2167
- if (fork.phase !== 'deciding' || (fork.humanDecision ?? null) !== null) continue
2168
- const label = fork.branches.find((branch) => branch.id === fork.recommended)?.label ?? null
2169
- inbox.push({
2170
- kind: 'fork_adopt',
2171
- title: '世界线裁决',
2172
- summary: `${fork.question} · ${fork.branches.length} 条世界线探索完毕,待采纳一条${label === null ? '' : ` · 推荐★${label}`}`,
2173
- plan: fork.plan ?? null,
2174
- step: fork.step,
2175
- /** 条目要指得出**是哪一盘分叉**:界面那条「用提问卡决定」的手势按它去取题目与选项。 */
2176
- fork: fork.id,
2177
- human_action: 'adopt_branch',
2178
- /** 门要什么:**点击**(有白名单动词)还是**一句话**(语义判断归模型)。 */
2179
- needs: 'click',
2180
- })
2181
- }
2182
- for (const fork of forks) {
2183
- // 人已经认可过就不再等他(状态锚:状态一变,条目自然消失)。
2184
- if (fork.merge?.provisional !== true || (fork.merge.confirmed ?? null) !== null) continue
2185
- inbox.push({
2186
- kind: 'provisional_review',
2187
- title: '临时采纳待复核',
2188
- summary: `${fork.question} · 已**临时**采纳「${fork.branches.find((branch) => branch.id === fork.merge.branch)?.label ?? fork.merge.branch}」:${fork.merge.decisionNote ?? '分差不足以称结论'}`,
2189
- plan: fork.plan ?? null,
2190
- step: fork.step,
2191
- /** 标的:认可是对**哪一盘分叉**的认可。 */
2192
- fork: fork.id,
2193
- /** 认可是一个动作(一键);「要改判据」那条路仍然靠说一句话,由模型重做一条世界线。 */
2194
- human_action: 'confirm_provisional',
2195
- /**
2196
- * **一键认可**:它曾经是「要一句话」的门,而「认可就什么都不用做」有个洞——
2197
- * 门开着 ⇒ 续跑停着 ⇒ 什么都不做的话系统一直等,等一个永远不会来的动作。
2198
- * 认可是一次决定,决定就该有按钮;要改判据那条路仍然在(说一句话,模型重做一条世界线)。
2199
- */
2200
- needs: 'click',
2201
- ask: '要改判据就重做一条世界线(说一句即可)',
2202
- })
2203
- }
2204
- for (const candidate of state.brainCandidates ?? []) {
2205
- inbox.push({
2206
- kind: 'skill_candidate',
2207
- title: `候选技能:${candidate.name}`,
2208
- summary: `${String(candidate.description ?? '').slice(0, 120)}——模型自己写的 SOP,采纳后才进它的技能目录。`,
2209
- plan: null,
2210
- step: null,
2211
- human_action: 'promote_skill',
2212
- skill: candidate.name,
2213
- needs: 'click',
2214
- })
2215
- }
2216
- if (activePlan !== null && activePlan.blocked !== undefined) {
2217
- inbox.push({
2218
- kind: 'plan_blocked',
2219
- title: '计划被拦',
2220
- summary: `连续 ${activePlan.blocked.attempts} 次未过观测准入:${activePlan.blocked.reason}`,
2221
- plan: activePlan.id,
2222
- step: activePlan.blocked.step ?? null,
2223
- // 「解除阻塞」不是一次点击能表达的事(要改计划或改判据),所以它没有人门动作:
2224
- // 人说一句「按 X 改」,语义判断归模型(ClearAI 删掉 NL 白名单的同一条理由)。
2225
- human_action: null,
2226
- needs: 'word',
2227
- ask: '说一句怎么改(改计划 / 补判据),语义判断归模型',
2228
- })
2229
- }
2230
- // `has_open_gate`:有一道门开着——调度侧据此不驱动(ClearAI:两个谓词同集)。
2231
- const hasOpenGate = inbox.length > 0
2232
-
2233
1682
  /**
2234
1683
  * 事实那一行的**两个读数**(都派生,不另存):
2235
1684
  * · `refuted`——它的假设收到过推翻证据 ⇒ 这条事实要复核;
2236
1685
  * · `review` ——人已经审查过(撤回 / 维持),决定连缘由一起留着。
2237
1686
  */
2238
- const factRows = (state.facts ?? []).map((fact) => {
1687
+ const lexicon = normalizeLexicon(state.lexicon)
1688
+ const ownFacts = (state.facts ?? []).map((fact) => {
2239
1689
  const linked = typeof fact.hypothesis === 'string' && fact.hypothesis !== ''
2240
1690
  const owner = hypotheses.find((item) => (linked ? item.id === fact.hypothesis : String(item.claim ?? '') === String(fact.text ?? '')))
2241
- return { ...fact, refuted: (owner?.refutations ?? 0) > 0, review: fact.review ?? null }
1691
+ return { ...fact, refuted: (owner?.refutations ?? 0) > 0, review: fact.review ?? null, foreign: false }
2242
1692
  })
1693
+ /**
1694
+ * **别的会话留下的事实**(住在 `clear/knowledge/facts/<id>.json`,经 `workspace/synced` 进账)。
1695
+ * 同一个 id 本会话账上也有就以账上那条为准(它带着推翻读数);文件上的复核结论照样认。
1696
+ * 它们的假设不在本会话,所以 `refuted` 只能是 false——推翻要在这里重新检验一次才算。
1697
+ */
1698
+ const ownIds = new Set(ownFacts.map((fact) => fact.id))
1699
+ const foreignFacts = []
1700
+ for (const [path, file] of Object.entries(state.workspace?.files ?? {})) {
1701
+ if (classifyWorkspacePath(path)?.kind !== 'fact' || file?.data === undefined) continue
1702
+ const row = factFromFile(file.data, path)
1703
+ if (row === null) continue
1704
+ if (ownIds.has(row.id)) {
1705
+ const own = ownFacts.find((fact) => fact.id === row.id)
1706
+ if (own.review === null && row.review !== null) own.review = row.review
1707
+ continue
1708
+ }
1709
+ foreignFacts.push({ ...row, refuted: false, foreign: true })
1710
+ }
1711
+ foreignFacts.sort((a, b) => (a.at ?? 0) - (b.at ?? 0))
1712
+ /**
1713
+ * `definitionsChanged`:升格那一刻用到的词条,**含义**此后改过或已不在的那些 id。
1714
+ * 只是读数,不撤回事实:改了定义的人知道为什么改,复核交给人与证据。
1715
+ */
1716
+ const factRows = [...foreignFacts, ...ownFacts].map((fact) => ({ ...fact, definitionsChanged: changedDefinitions(lexicon, fact.definitions) }))
2243
1717
 
2244
1718
  /**
2245
1719
  * **领域词汇的派生读数**(两条,都不新存东西):
@@ -2250,28 +1724,31 @@ export function derive(state) {
2250
1724
  * 两者都吃 `factRows` 而不是 `state.facts`:事实的复核态与推翻标记是派生的,
2251
1725
  * 在这里重算一遍就等于第二份判据。
2252
1726
  */
2253
- const lexicon = normalizeLexicon(state.lexicon)
2254
1727
  const conflicts = deriveConflicts(factRows, lexicon)
2255
1728
  const lexiconIssues = lexiconHealth(lexicon, factRows)
2256
- for (const fact of factRows) {
2257
- /**
2258
- * **推翻证据只标记事实,撤不撤由人定**(数据本身也可能是错的)。
2259
- * 两种结局都要能一键落地,否则这道门没有出口:维持也是一次决定,而且它必须落账,
2260
- * 不然「没决定」与「决定维持」在门的状态上长得一模一样,系统会一直等。
2261
- */
2262
- if (fact.refuted !== true || fact.review !== null) continue
2263
- inbox.push({
2264
- kind: 'fact_refutation',
2265
- title: '事实被推翻,等你决定',
2266
- summary: `「${String(fact.text ?? '').slice(0, 90)}」出现了推翻证据:撤回它,或判证据不可靠、维持原事实。`,
2267
- plan: null,
2268
- step: null,
2269
- /** 标的:面板把 `value` 原样写进人门动作,宿主据此核标的存在、fold 据此落账。 */
2270
- value: fact.id,
2271
- human_action: 'retract_fact',
2272
- needs: 'click',
1729
+ /**
1730
+ * **待处理**(第六阶段,取代空了的收件箱):只陈述、不放按钮——决定在对话里说,
1731
+ * 或由开门的那次调用用原生提问卡问。两类:计划连拦停下了;已写进长期知识的结论互相矛盾。
1732
+ * 都是派生读数:计划一解封、矛盾一方被推翻,条目自然消失。
1733
+ */
1734
+ const needYou = []
1735
+ if (activePlan !== null && activePlan.blocked !== undefined) needYou.push({ kind: 'plan_blocked', text: tr(`计划停下了:${String(activePlan.blocked.reason ?? '连续没过')},怎么改?`, `The plan stopped: ${String(activePlan.blocked.reason ?? 'it kept failing')}. How should it change?`) })
1736
+ for (const conflict of conflicts) {
1737
+ const values = (conflict.sides ?? []).map((side) => String(side.value ?? '?'))
1738
+ needYou.push({
1739
+ kind: 'conflict',
1740
+ text: tr(
1741
+ `${String(conflict.subject ?? '')}的${String(conflict.predicate ?? '')}:${values.map((value) => `「${value}」`).join('和')}矛盾,以哪个为准?`,
1742
+ `${String(conflict.predicate ?? '')} of ${String(conflict.subject ?? '')}: ${values.map((value) => `"${value}"`).join(' and ')} conflict. Which one holds?`,
1743
+ ),
2273
1744
  })
2274
1745
  }
1746
+ for (const fact of factRows) {
1747
+ if (fact.definitionsChanged.length === 0 || fact.review?.decision === 'retracted') continue
1748
+ const head = String(fact.text ?? '').replace(/\s+/g, ' ').trim()
1749
+ needYou.push({ kind: 'definition_changed', fact: fact.id, text: tr(`「${head.length > 40 ? `${head.slice(0, 39)}…` : head}」用到的 ${fact.definitionsChanged.join('、')} 定义已变,这条结论还成立吗?`, `The definitions of ${fact.definitionsChanged.join(', ')} used by "${head.length > 40 ? `${head.slice(0, 39)}…` : head}" changed. Does this conclusion still hold?`) })
1750
+ }
1751
+
2275
1752
 
2276
1753
  const settlement = state.evidence.map((item) => {
2277
1754
  const step = stepOf(item.plan, item.step)
@@ -2293,14 +1770,10 @@ export function derive(state) {
2293
1770
  activePlan,
2294
1771
  closedPlans,
2295
1772
  pendingAudit,
2296
- forks,
2297
1773
  factRows,
2298
1774
  settlement,
2299
1775
  stepOf,
2300
- inbox,
2301
- hasOpenGate,
2302
- planConfirmationPending,
2303
- planIsAuthorized,
1776
+ needYou,
2304
1777
  /** 领域词汇与它的两条派生读数(见上面那段:冲突与健康度都只是读数)。 */
2305
1778
  lexicon,
2306
1779
  conflicts,
@@ -2315,52 +1788,36 @@ export function derive(state) {
2315
1788
  *
2316
1789
  * 人在面板上按的每一个动作,都经宿主平面的路由变成**一条用户消息**进会话日志——
2317
1790
  * 内容是一行结构化标记,不是自然语言。为什么要这样:
2318
- * · **可审计**:谁在什么时候做了什么(采纳哪条世界线、扶正哪个技能、把档切成什么),
1791
+ * · **可审计**:谁在什么时候做了什么(撤回或维持哪条事实、改了哪个词条),
2319
1792
  * 就在日志里(`source.kind==='user'`);
2320
1793
  * · **不做 NLU**:ClearAI 把确认短语白名单整体删掉了,理由是「语义判断只归模型,
2321
1794
  * harness 只做顺序可判定的题」。标记进得来、自然语言进不来,是同一条纪律;
2322
1795
  * · **agent 不可达**:这些动词**没有工具 schema**——模型能调的工具面里不存在它们,
2323
1796
  * 它只能看见「人做了什么」这条事实。
2324
1797
  *
2325
- * 动词表只有 3 个(奥卡姆:另外砍掉的两个都是**重复**):
1798
+ * 砍掉的动词(奥卡姆:都是**重复**,或者它服务的机制已经删除):
2326
1799
  * · `confirm_plan` —— 原生 `dsh-plan-mode` 就是「用户复核的出口」;而我们自己的
2327
1800
  * `planIsAuthorized` 本来就承认「交付第一步即授权」。计划确认从来不是闸门(它是记号),
2328
1801
  * 少一个假装成闸门的按钮,界面就不再暗示一条不存在的约束。需要人拍板时用原生
2329
1802
  * `ask_user_question`(它同样把人的答复留在日志里,署名一样是人)。
2330
1803
  * · `invoke_skill` —— 原生 `/` 技能触发器(`dsh-client-ui-skill` 注册 trigger `/`,
2331
1804
  * `dsh-client-ui-input-trigger` 出候选菜单)做的正是同一件事,而且带候选菜单。
2332
- * 留下的三个各有原生没有的职责:
2333
- * · `adopt_branch`/`abandon_fork` —— `ConvergeFork` 只认「算术」或「人门」两条路,
2334
- * 算不出来时这是唯一的结构化人裁决通道(不做 NLU 是纪律,不是懒);
2335
- * · `promote_skill` —— 候选技能扶正是**只有人能触发**的写动作(模型不能自举)。
1805
+ * · `adopt_branch` / `abandon_fork` / `confirm_provisional` —— 世界线已删除,并行探索交给原生子任务;
1806
+ * · `promote_skill` —— 外脑已删除,技能走原生技能目录。
1807
+ * 旧日志里的这些动作不再折:`parseHumanGate` 按动词表拒收,原样跳过。
2336
1808
  *
2337
- * 已摘除:`set_autonomy`。「在场与否」是**运行时状态**(有没有门开着、有没有裁决在飞),
2338
- * 不是人在面板上按的一个开关;那个档位还顺手把「计划经人确认」变成系统自己签的。
2339
- * 折法里仍留一条**只读**容忍分支(见 `applyEvent` 的注释):旧会话日志里可能有一条这样的记录,
2340
- * 而历史必须继续读得通——但**当前没有任何写入者**,面板上也没有这个开关。
1809
+ * 已摘除:`set_autonomy`(运行档整个删了,旧日志里的这条记录原样跳过);
1810
+ * `retract_fact` / `keep_fact`(这道门改由内核当场问人,见下面的 `LEGACY_GATE_ACTIONS`)。
2341
1811
  */
2342
1812
  export const HUMAN_GATE_MARK = '[clearai·人门]'
2343
- /** 面板上允许出现的动词。表外的动词一律拒(与贡献表同一套「表外的名字不许出现」)。 */
2344
- export const HUMAN_GATE_ACTIONS = [
2345
- 'adopt_branch',
2346
- 'abandon_fork',
2347
- 'promote_skill',
2348
- 'retract_fact',
2349
- 'keep_fact',
2350
- 'confirm_provisional',
2351
- /**
2352
- * **本体四动词(人的通道)**:面板抽屉发的就是它们;模型有同名语义的工具
2353
- * (`RegisterTerm` 等),但这两个面落的是**同一套判据与同一本账**——判据在宿主半的
2354
- * `domain` 门里,词汇事件只有一种折法。这个数组在内核那一侧有一份**逐字镜像**
2355
- * (预设面不能 import 这一层),两边相等由 authority-boundary 套件钉死。
2356
- */
2357
- 'register_term',
2358
- 'register_predicate',
2359
- 'revise_term',
2360
- 'deprecate_entry',
2361
- ]
2362
- /** 运行档的两个取值。**只用于读取旧日志**里的 `set_autonomy` 记录;当前没有写入口。 */
2363
- export const AUTONOMY_VALUES = ['attended', 'unattended']
1813
+ /**
1814
+ * **只在读旧日志时认**的动词。面板早已没有写入口(第六阶段把 `/api/clearai/gate` 整条拿掉):
1815
+ * 撤回 / 维持事实从第三阶段起由内核在那次交付里当场问人;本体四动词随编辑抽屉一起删,
1816
+ * 要改词汇就在对话里说,模型去改 `clear/ontology/` 下的文件。
1817
+ * 但旧会话里人按过的决定必须照旧折出来,否则重放时一条审过的事实会变回「待复核」、
1818
+ * 人登记的词会凭空消失。
1819
+ */
1820
+ const LEGACY_GATE_ACTIONS = ['retract_fact', 'keep_fact', 'register_term', 'register_predicate', 'revise_term', 'deprecate_entry']
2364
1821
 
2365
1822
  /** 从一条用户消息里认出人门标记;不是标记就返回 null。 */
2366
1823
  export function parseHumanGate(message) {
@@ -2376,90 +1833,60 @@ export function parseHumanGate(message) {
2376
1833
  try {
2377
1834
  const parsed = JSON.parse(line)
2378
1835
  if (parsed === null || typeof parsed !== 'object') return null
2379
- if (!HUMAN_GATE_ACTIONS.includes(String(parsed.action))) return null
1836
+ if (!LEGACY_GATE_ACTIONS.includes(String(parsed.action))) return null
2380
1837
  return parsed
2381
1838
  } catch {
2382
1839
  return null
2383
1840
  }
2384
1841
  }
2385
1842
 
2386
- /**
2387
- * 技能面:合并目录(内核发的)+ 本会话用量(从日志折的)。
2388
- *
2389
- * 用量**不做跨会话统计**(第一性原理:判据混杂、没有决策者据此行动,
2390
- * 更好的原语是「一次具体实例 + 可点开的指针」)。这里给的正是那个指针:谁在哪一步加载的、
2391
- * 那一步**现在**是什么结果——现算,所以技能用完那一步后来又交付了,这里也跟着变。
2392
- */
2393
- function skillUsageView(state) {
2394
- const usage = state.skillUsage ?? {}
2395
- return Object.values(usage)
2396
- .map((entry) => {
2397
- const events = Array.isArray(entry.events) ? entry.events : []
2398
- const last = events.length === 0 ? null : events[events.length - 1]
2399
- const plan = last === null ? null : (state.plans.find((item) => item.id === last.plan) ?? null)
2400
- const step = plan === null || last.step === null ? null : (plan.steps.find((item) => item.id === last.step) ?? null)
2401
- return {
2402
- name: entry.name,
2403
- model: entry.model ?? 0,
2404
- human: entry.human ?? 0,
2405
- lastAt: entry.lastAt ?? null,
2406
- last:
2407
- last === null
2408
- ? null
2409
- : {
2410
- by: last.by,
2411
- at: last.at ?? null,
2412
- plan: last.plan ?? null,
2413
- step: last.step ?? null,
2414
- ordinal: last.ordinal ?? null,
2415
- // 指针落在的这一步现在的结果(现算):advanced / open / void
2416
- outcome: step === null ? null : step.status,
2417
- evidence: step === null ? null : (step.evidence ?? null),
2418
- stepDo: step === null ? null : step.do,
2419
- },
2420
- }
2421
- })
2422
- .sort((left, right) => String(left.name).localeCompare(String(right.name)))
1843
+ /** 一条判断升格成的事实(按 id 关联;旧账本按文本找回)。 */
1844
+ function factOf(state, hypothesis) {
1845
+ return (state.facts ?? []).find((item) => item.hypothesis === hypothesis.id) ?? (state.facts ?? []).find((item) => item.hypothesis === undefined && String(item.text ?? '') === String(hypothesis.claim ?? '')) ?? null
1846
+ }
1847
+
1848
+ function factRow(state, hypothesis) {
1849
+ const fact = factOf(state, hypothesis)
1850
+ return fact === null ? null : { id: fact.id, text: fact.text ?? null, scope: fact.scope ?? null, level: fact.level ?? null, at: fact.at ?? null, retracted: fact.review?.decision === 'retracted' }
1851
+ }
1852
+
1853
+ /** 面板读的历程:步骤换成「第几步」,末尾接上升格与撤回(它们不是证据,但同样改了可信度)。 */
1854
+ function historyRows(state, hypothesis) {
1855
+ const ordinalOf = (planId, stepId) => {
1856
+ const plan = state.plans.find((item) => item.id === planId)
1857
+ return plan?.steps?.find((step) => step.id === stepId)?.ordinal ?? null
1858
+ }
1859
+ const rows = (hypothesis.history ?? []).map((row) => (row.kind === 'evidence' ? { ...row, ordinal: ordinalOf(row.plan, row.step) } : { ...row }))
1860
+ const fact = factOf(state, hypothesis)
1861
+ const last = () => rows[rows.length - 1]?.to ?? 'testing'
1862
+ if (fact !== null) rows.push({ kind: 'promoted', at: fact.at ?? null, level: fact.level ?? null, from: last(), to: 'credible' })
1863
+ if (fact?.review?.decision === 'retracted') rows.push({ kind: 'retracted', at: fact.review.at ?? null, reason: fact.review.reason ?? null, from: last(), to: 'replaced' })
1864
+ return rows
2423
1865
  }
2424
1866
 
2425
1867
  /** 面板契约。浏览器读的就是这个,一字不改。 */
2426
1868
  export function view(state, sessionId) {
2427
1869
  const derived = derive(state)
1870
+ const promotedIds = new Set((state.facts ?? []).map((fact) => fact?.hypothesis).filter((id) => typeof id === 'string'))
2428
1871
  const plan = derived.activePlan ?? derived.closedPlans[derived.closedPlans.length - 1] ?? null
2429
1872
  const sid = sessionId === undefined || sessionId === null ? (state.sessionId ?? null) : String(sessionId)
2430
1873
  return {
2431
1874
  ok: true,
2432
1875
  mounted: true,
2433
1876
  sessionId: sid,
2434
- /** 收件箱(面板「需要你 N」的数据面):只有分诊信息,正文在各自的视图里。 */
2435
- inbox: derived.inbox,
2436
- hasOpenGate: derived.hasOpenGate,
2437
- /**
2438
- * 运行档:`value` = 当档(内核下发的那份)、`source` 说明它从哪来(部署预设 / 旧日志里的人门记录)。
2439
- * 面板与卡片都读这里 —— 投影是唯一真相。
2440
- * 注意**它不是可切换的开关**:`set_autonomy` 已摘除,现在只有部署预设会下发新值;
2441
- * `source === 'session'` 只可能来自旧日志。
2442
- */
2443
- /** 项目章程的读数(面板「技能 · 记忆」页签顶部那一行;点开走原生预览)。 */
2444
- constitution: state.constitution ?? null,
1877
+ /** 待处理:只陈述的几行(计划停下、结论矛盾);面板顶上与输入框旁读同一份。 */
1878
+ needYou: derived.needYou,
2445
1879
  /**
2446
1880
  * 被闸门裁断过几次(`block/counted`):世界树的**分段通道**画的就是它——
2447
1881
  * 这一步磨了几轮、其中几次被驳回。事实在投影里,面板只负责画。
2448
1882
  */
2449
1883
  blocks: state.blocks ?? {},
2450
1884
  /**
2451
- * **人放行**(`human/released`)的痕迹:每一条都带它绑在哪条轴上
2452
- * (`step` / `branch`)以及凭据(`via:'approval'`,原生审批栈的权威记录)。
1885
+ * **人放行**(`human/released`)的痕迹:每一条都带它绑在哪一步上
1886
+ * 以及凭据(`via:'ask'`:内核那次交付当场问出来的;旧账里是 `approval`)。
2453
1887
  * 面板与测试都读这里 —— 推断出来的放行不该有痕迹,所以这份读数本身就是判据。
2454
1888
  */
2455
- 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 })),
2456
- autonomy: {
2457
- value: state.autonomy?.effective?.value ?? null,
2458
- preset: state.autonomy?.effective?.preset ?? null,
2459
- source: state.autonomy?.effective?.source ?? null,
2460
- override: state.autonomy?.override ?? null,
2461
- },
2462
- brain: state.brain ?? null,
1889
+ releases: (state.releases ?? []).map((item) => ({ step: item.step ?? null, plan: item.plan ?? null, via: item.via ?? null, call: item.call ?? null, at: item.at ?? null })),
2463
1890
  /** 本体形状(面板页眉据此生成,不手抄)。 */
2464
1891
  ontology: state.ontology ?? null,
2465
1892
  /**
@@ -2474,6 +1901,9 @@ export function view(state, sessionId) {
2474
1901
  conflicts: derived.conflicts,
2475
1902
  health: derived.lexiconIssues,
2476
1903
  graph: graphProjection(state),
1904
+ /** 本体文件里的实体(带所在目录的容器)与跨文件问题:本体页的问题列表读它。 */
1905
+ entities: Array.isArray(state.entities) ? state.entities : [],
1906
+ problems: Array.isArray(state.ontologyProblems) ? state.ontologyProblems : [],
2477
1907
  },
2478
1908
  /**
2479
1909
  * **知识模式(分诊)**:面板据此把「这一格是知识主场还是普通进展」说清楚,
@@ -2496,23 +1926,6 @@ export function view(state, sessionId) {
2496
1926
  * 面板可以画它,内核的 pre-step 把它送进卡——判据与缺口读数一样,只有这一处。
2497
1927
  */
2498
1928
  preflight: knowledgePreflight(state, derived),
2499
- /**
2500
- * 续跑窗口的账:面板读它,于是「续跑停着——等裁决」这种**平台说不出来的话**
2501
- * 有地方说。轮数与相位仍然只在原生 dock 上出现(一个事实只在**一块**面上说),
2502
- * 这里交出去的只有:状态、它服务的事实对象、以及停下来的真实理由。
2503
- */
2504
- continuation:
2505
- state.continuation === null || state.continuation === undefined
2506
- ? null
2507
- : {
2508
- state: state.continuation.state,
2509
- goal: state.continuation.goal ?? null,
2510
- target: state.continuation.target ?? null,
2511
- why: state.continuation.why ?? null,
2512
- label: state.continuation.label ?? null,
2513
- },
2514
- /** 技能面:合并目录 + 本会话用量(「技能 · 记忆」页签的数据面)。 */
2515
- skills: { catalog: state.skillCatalog ?? null, usage: skillUsageView(state) },
2516
1929
  goal:
2517
1930
  state.goal === null
2518
1931
  ? null
@@ -2530,15 +1943,20 @@ export function view(state, sessionId) {
2530
1943
  closeVerdict: state.goal.closeVerdict ?? null,
2531
1944
  hypotheses: derived.hypotheses.map((hypothesis) => ({
2532
1945
  id: hypothesis.id,
1946
+ name: handleOf(hypothesis),
1947
+ /** 可信度分组(已验证 / 待核验 / 验证中 / 不确定 / 已推翻 / 已替换),面板与卡同一份。 */
1948
+ trust: trustOf(hypothesis, promotedIds.has(hypothesis.id)),
2533
1949
  claim: hypothesis.claim,
2534
1950
  refuteWhen: hypothesis.refute_when,
2535
1951
  status: hypothesis.status,
2536
1952
  supportedLevel: hypothesis.supportedLevel,
2537
1953
  refutations: hypothesis.refutations,
2538
1954
  inconclusive: hypothesis.inconclusive,
2539
- /** 从没被走过的等级(派生):面板据此说清「这一级是跳上来的」。 */
2540
- untouchedLevels: hypothesis.untouchedLevels,
2541
1955
  version: hypothesis.version,
1956
+ /** 「可信度怎么变的」:逐笔的之前 → 之后,末尾接上写进长期知识 / 被撤回。 */
1957
+ history: historyRows(state, hypothesis),
1958
+ /** 写进长期知识的那条(没升格就是 null):补充段的「范围」读它。 */
1959
+ fact: factRow(state, hypothesis),
2542
1960
  /** 断言随假设走(未升格):面板同画芯片(chip 同样在投影侧算好),标注「未升格」。 */
2543
1961
  assertions: Array.isArray(hypothesis.assertions)
2544
1962
  ? hypothesis.assertions.map((assertion) => ({ ...assertion, chip: formatAssertion(state.lexicon, assertion) }))
@@ -2564,7 +1982,9 @@ export function view(state, sessionId) {
2564
1982
  doneCriteria: step.done_criteria,
2565
1983
  tests: step.tests,
2566
1984
  status: step.status,
2567
- evidenceId: step.evidence,
1985
+ evidenceIds: step.evidence ?? [],
1986
+ /** 交付本身(谁判的、凭什么、出处);旧账为 null。 */
1987
+ delivery: step.delivery ?? null,
2568
1988
  advancedAt: step.advancedAt,
2569
1989
  voidReason: step.voidReason,
2570
1990
  })),
@@ -2578,10 +1998,6 @@ export function view(state, sessionId) {
2578
1998
  /** 计划自己的一句话(模型建计划时写的)。**给人看的名字**用它,内部 id 退回 tooltip。 */
2579
1999
  brief: plan.brief ?? '',
2580
2000
  blocked: plan.blocked ?? null,
2581
- // 授权记号(ClearAI `plan.confirmed_at` / `confirmed_by`):面板据此显示计划门。
2582
- confirmedAt: plan.confirmed_at ?? null,
2583
- confirmedBy: plan.confirmed_by ?? null,
2584
- confirmationPending: derived.planConfirmationPending && plan === derived.activePlan,
2585
2001
  steps: plan.steps.map((step) => ({
2586
2002
  id: step.id,
2587
2003
  ordinal: step.ordinal,
@@ -2595,7 +2011,9 @@ export function view(state, sessionId) {
2595
2011
  doneCriteria: step.done_criteria,
2596
2012
  tests: step.tests,
2597
2013
  status: step.status,
2598
- evidenceId: step.evidence,
2014
+ evidenceIds: step.evidence ?? [],
2015
+ /** 交付本身(谁判的、凭什么、出处);旧账为 null。 */
2016
+ delivery: step.delivery ?? null,
2599
2017
  advancedAt: step.advancedAt,
2600
2018
  voidReason: step.voidReason,
2601
2019
  })),
@@ -2623,12 +2041,11 @@ export function view(state, sessionId) {
2623
2041
  stepId: item.step,
2624
2042
  planId: item.plan,
2625
2043
  /**
2626
- * `branch` 与 `anchor` **必须交出去**:面板要给每条证据指一个**出处**——
2627
- * 独立证据指评估卡(文件)、自判证据指它锚定的产物。少了这两个字段,
2628
- * 「评估卡」那一项永远出不来(成因:世界线证据的评估卡按 `分支` 归属,
2629
- * 与证据的 `stepId` 不是同一个 id,只看 stepId 会对不上)。
2044
+ * `anchor` **必须交出去**:面板要给每条证据指一个**出处**——
2045
+ * 独立证据指评估卡(文件)、自判证据指它锚定的产物。
2630
2046
  */
2631
- branch: item.branch ?? null,
2047
+ /** 针对哪条判断(旧账为 null:由步骤检验的判断找回来)。 */
2048
+ hypothesis: item.hypothesis ?? null,
2632
2049
  anchor: item.anchor ?? 'artifact',
2633
2050
  /** 记账时定下的出处。旧日志没有这个字段 ⇒ 客户端走只读回退。 */
2634
2051
  origins: item.origins ?? [],
@@ -2666,6 +2083,8 @@ export function view(state, sessionId) {
2666
2083
  facts: derived.factRows.map((item) => ({
2667
2084
  id: item.id,
2668
2085
  text: item.text,
2086
+ /** 产出它的那条判断(旧账本没有,面板把这种事实单独列成一行)。 */
2087
+ hypothesis: item.hypothesis ?? null,
2669
2088
  /** 边界(推翻条件)与支持等级:面板与货架都要显示它——「已知」必须带边界。 */
2670
2089
  scope: item.scope ?? null,
2671
2090
  level: item.level ?? null,
@@ -2681,91 +2100,6 @@ export function view(state, sessionId) {
2681
2100
  refuted: item.refuted === true,
2682
2101
  review: item.review ?? null,
2683
2102
  })),
2684
- /**
2685
- * 侦察记录:**结局要能看出来**。
2686
- *
2687
- * 失效模式:中断一次跑动之后「状态上全部显示执行完成」,而宿主其实报了失败。
2688
- * 两处都修了:内核改成按 `stopReason` 落账(aborted/error 不算完成),这里把 `note` 交出去
2689
- * 并派生成三态——跑着 / 正常回灌 / 没正常结束。面板与资料面据此分诊,而不是一律当"完成"。
2690
- */
2691
- scouts: (state.scouts ?? []).map((item) => ({
2692
- id: item.id,
2693
- stepId: item.step,
2694
- trigger: item.trigger,
2695
- child: item.child,
2696
- capability: item.capability,
2697
- digest: item.digest ?? null,
2698
- conclusion: item.conclusion,
2699
- at: item.at,
2700
- note: item.note ?? null,
2701
- path: item.path ?? null,
2702
- status: item.conclusion === null || item.conclusion === undefined ? 'running' : item.note === null || item.note === undefined ? 'settled' : 'failed',
2703
- })),
2704
- forks: derived.forks.map((fork) => ({
2705
- tier: fork.tier ?? null,
2706
- degradedReason: fork.degradedReason ?? null,
2707
- merge: fork.merge ?? null,
2708
- mergeConflict: fork.mergeConflict ?? null,
2709
- /** 临时采纳:分差不足以称结论(或由判断而来)→ 面板要给「待复核」标记。 */
2710
- provisional: fork.merge?.provisional === true,
2711
- decisionNote: fork.merge?.decisionNote ?? null,
2712
- /** 人在面板上的裁决(结构化记录,by:'user'):内核的工具据此落实它。 */
2713
- humanDecision: fork.humanDecision ?? null,
2714
- id: fork.id,
2715
- stepId: fork.step,
2716
- question: fork.question,
2717
- phase: fork.phase,
2718
- decided: fork.settled,
2719
- abandonReason: fork.abandonReason,
2720
- /** 承载它的步骤现在是什么状态(void = 承诺已撤回)——面板据此说「随步骤作废而终止」。 */
2721
- ownerStatus: fork.ownerStatus ?? null,
2722
- ownerVoidReason: fork.ownerVoidReason ?? null,
2723
- /** 派生:步骤作废 + 还没收口。不是新状态,是两个事实的推论(见 derive 里的说明)。 */
2724
- orphaned: fork.orphaned === true,
2725
- decideBy: { metric: fork.decide_by?.metric ?? null, direction: fork.decide_by?.direction ?? null },
2726
- verdict: fork.verdict,
2727
- undecidable: fork.undecidable,
2728
- /**
2729
- * 算术推荐(`fork/recommended`):人门卡与面板的「推荐★」读的就是它。
2730
- * 它只在分叉**还没收口**时才有意义——收口之后 `verdict.winner` 才是结论。
2731
- */
2732
- recommended: fork.recommended ?? null,
2733
- recommendMargin: fork.recommendMargin ?? null,
2734
- recommendProvisional: fork.recommendProvisional === true,
2735
- recommendBy: fork.recommendBy ?? null,
2736
- /** 采纳时没能合并(以及为什么):决定登记了,产物靠一次普通交付落位。 */
2737
- mergeSkipped: fork.mergeSkipped ?? null,
2738
- arbitration: fork.arbitration ?? null,
2739
- arbitrationSession: fork.arbitrationSession ?? null,
2740
- branches: fork.branches.map((branch) => ({
2741
- id: branch.id,
2742
- label: branch.label,
2743
- approach: branch.approach,
2744
- doneCriteria: branch.done_criteria,
2745
- workspace: branch.workspace,
2746
- level: branch.level,
2747
- status: branch.status,
2748
- reading: branch.reading,
2749
- validity: branch.validity,
2750
- /** 派生:执行没跑成(`execution.ok === false`)。**它不是落选**——面板要分开说。 */
2751
- failed: branch.failed === true,
2752
- orphaned: branch.orphaned === true,
2753
- /** 派生:分叉已收口而执行者没报过 ⇒ 那句「结论会自动回灌」作废(见 derive 里的说明)。 */
2754
- unreturned: branch.unreturned === true,
2755
- execution: branch.execution ?? null,
2756
- verdict: branch.verdict,
2757
- basis: branch.basis,
2758
- evaluator: branch.evaluator,
2759
- execution: branch.execution ?? null,
2760
- cardPath: branch.card_path ?? null,
2761
- evaluatorSession: branch.evaluator_session ?? null,
2762
- gitBranch: branch.git_branch ?? null,
2763
- worktreePath: branch.worktree_path ?? null,
2764
- worktreeRemoved: branch.worktree_removed === true,
2765
- keptRef: branch.kept_ref !== false,
2766
- at: branch.at,
2767
- })),
2768
- })),
2769
2103
  }
2770
2104
  }
2771
2105