clearai-dsh 0.3.1 → 0.5.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 (150) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/README.md +30 -30
  3. package/README.zh-CN.md +30 -30
  4. package/bin/clearai.mjs +29 -231
  5. package/cordis.patch.yml +3 -3
  6. package/lib/client.js +862 -2555
  7. package/lib/domain-language.js +614 -48
  8. package/lib/fold.js +449 -1020
  9. package/lib/host.js +47 -557
  10. package/lib/invariant.js +9 -12
  11. package/lib/knowledge-view.js +421 -225
  12. package/lib/lang.js +81 -0
  13. package/package.json +2 -2
  14. package/presets/clearai/agent.cordis.yml +57 -82
  15. package/presets/clearai/clearai.patch.yml +57 -82
  16. package/presets/clearai/plugins/clearai-kernel.js +2103 -5457
  17. package/presets/clearai/plugins/ontology.js +76 -20
  18. package/presets/clearai/plugins/prompts.js +75 -363
  19. package/presets/clearai/skills/clearai-loop/SKILL.md +81 -59
  20. package/presets/clearai/plugins/brain.js +0 -547
  21. package/presets/clearai/plugins/commands.js +0 -199
  22. package/presets/clearai/template/knowledge/README.md +0 -25
  23. package/presets/clearai/template/memory/README.md +0 -34
  24. package/presets/clearai/template/project.md +0 -49
  25. package/presets/clearai/template/skills/README.md +0 -37
  26. package/presets/clearai/template/skills/chart-diagram-qa/SKILL.md +0 -43
  27. package/presets/clearai/template/skills/citation-management/SKILL.md +0 -73
  28. package/presets/clearai/template/skills/citation-management/references/bibtex_formatting.md +0 -908
  29. package/presets/clearai/template/skills/citation-management/references/citation_validation.md +0 -794
  30. package/presets/clearai/template/skills/citation-management/references/google_scholar_search.md +0 -725
  31. package/presets/clearai/template/skills/citation-management/references/metadata_extraction.md +0 -870
  32. package/presets/clearai/template/skills/citation-management/references/pubmed_search.md +0 -839
  33. package/presets/clearai/template/skills/citation-management/scripts/doi_to_bibtex.py +0 -204
  34. package/presets/clearai/template/skills/citation-management/scripts/extract_metadata.py +0 -569
  35. package/presets/clearai/template/skills/citation-management/scripts/format_bibtex.py +0 -349
  36. package/presets/clearai/template/skills/citation-management/scripts/generate_schematic.py +0 -139
  37. package/presets/clearai/template/skills/citation-management/scripts/generate_schematic_ai.py +0 -817
  38. package/presets/clearai/template/skills/citation-management/scripts/search_google_scholar.py +0 -282
  39. package/presets/clearai/template/skills/citation-management/scripts/search_pubmed.py +0 -398
  40. package/presets/clearai/template/skills/citation-management/scripts/validate_citations.py +0 -497
  41. package/presets/clearai/template/skills/data-analysis/SKILL.md +0 -92
  42. package/presets/clearai/template/skills/data-analysis/checklists/readiness_check.md +0 -23
  43. package/presets/clearai/template/skills/data-analysis/templates/analysis_report.md.tpl +0 -63
  44. package/presets/clearai/template/skills/data-analysis/templates/cleaning_rules_draft.yaml.tpl +0 -32
  45. package/presets/clearai/template/skills/data-analysis/templates/data_dictionary.md.tpl +0 -12
  46. package/presets/clearai/template/skills/data-analysis/templates/domain_knowledge_template.md.tpl +0 -316
  47. package/presets/clearai/template/skills/data-analysis/templates/feature_candidates.json.tpl +0 -20
  48. package/presets/clearai/template/skills/data-analysis/templates/quality_scorecard.md.tpl +0 -30
  49. package/presets/clearai/template/skills/data-analysis/workflows/01-data-profiling.md +0 -42
  50. package/presets/clearai/template/skills/data-analysis/workflows/02-quality-audit.md +0 -36
  51. package/presets/clearai/template/skills/data-analysis/workflows/03-physical-correlation.md +0 -25
  52. package/presets/clearai/template/skills/data-analysis/workflows/04-unstructured-mining.md +0 -26
  53. package/presets/clearai/template/skills/data-qa-analysis/SKILL.md +0 -102
  54. package/presets/clearai/template/skills/data-qa-analysis/checklists/readiness_check.md +0 -62
  55. package/presets/clearai/template/skills/data-qa-analysis/templates/best_in_class_report.md.tpl +0 -56
  56. package/presets/clearai/template/skills/data-qa-analysis/templates/cleaning_rules_draft.yaml.tpl +0 -56
  57. package/presets/clearai/template/skills/data-qa-analysis/templates/data_dictionary.md.tpl +0 -13
  58. package/presets/clearai/template/skills/data-qa-analysis/templates/data_source_inventory_and_lineage.md.tpl +0 -146
  59. package/presets/clearai/template/skills/data-qa-analysis/templates/data_status_report.md.tpl +0 -60
  60. package/presets/clearai/template/skills/data-qa-analysis/templates/steady_state_rules.yaml.tpl +0 -41
  61. package/presets/clearai/template/skills/data-qa-analysis/templates/subsystem_registry.md.tpl +0 -101
  62. package/presets/clearai/template/skills/data-qa-analysis/templates/unified_execution_plan.md.tpl +0 -100
  63. package/presets/clearai/template/skills/data-qa-analysis/workflows/01-data-source-inventory-and-lineage.md +0 -194
  64. package/presets/clearai/template/skills/data-qa-analysis/workflows/02-data-alignment-and-tag-semantics.md +0 -122
  65. package/presets/clearai/template/skills/data-qa-analysis/workflows/03-steady-state-identification.md +0 -126
  66. package/presets/clearai/template/skills/data-qa-analysis/workflows/04-consumption-analysis.md +0 -152
  67. package/presets/clearai/template/skills/data-qa-analysis/workflows/05-best-in-class-and-optimization-space.md +0 -78
  68. package/presets/clearai/template/skills/domain-presearch/SKILL.md +0 -131
  69. package/presets/clearai/template/skills/domain-presearch/checklists/domain_checklist.md +0 -24
  70. package/presets/clearai/template/skills/domain-presearch/references/figure_code.md +0 -78
  71. package/presets/clearai/template/skills/domain-presearch/references/strategic_frameworks.md +0 -38
  72. package/presets/clearai/template/skills/exploration-loop/SKILL.md +0 -81
  73. package/presets/clearai/template/skills/exploratory-data-analysis/SKILL.md +0 -77
  74. package/presets/clearai/template/skills/exploratory-data-analysis/references/bioinformatics_genomics_formats.md +0 -664
  75. package/presets/clearai/template/skills/exploratory-data-analysis/references/chemistry_molecular_formats.md +0 -664
  76. package/presets/clearai/template/skills/exploratory-data-analysis/references/general_scientific_formats.md +0 -518
  77. package/presets/clearai/template/skills/exploratory-data-analysis/references/microscopy_imaging_formats.md +0 -620
  78. package/presets/clearai/template/skills/exploratory-data-analysis/references/proteomics_metabolomics_formats.md +0 -517
  79. package/presets/clearai/template/skills/exploratory-data-analysis/references/spectroscopy_analytical_formats.md +0 -633
  80. package/presets/clearai/template/skills/exploratory-data-analysis/scripts/eda_analyzer.py +0 -547
  81. package/presets/clearai/template/skills/hypothesis-generation/SKILL.md +0 -73
  82. package/presets/clearai/template/skills/hypothesis-generation/references/experimental_design_patterns.md +0 -329
  83. package/presets/clearai/template/skills/hypothesis-generation/references/hypothesis_quality_criteria.md +0 -198
  84. package/presets/clearai/template/skills/hypothesis-generation/references/literature_search_strategies.md +0 -622
  85. package/presets/clearai/template/skills/hypothesis-generation/scripts/generate_schematic.py +0 -139
  86. package/presets/clearai/template/skills/hypothesis-generation/scripts/generate_schematic_ai.py +0 -817
  87. package/presets/clearai/template/skills/literature-review/SKILL.md +0 -72
  88. package/presets/clearai/template/skills/literature-review/references/citation_styles.md +0 -166
  89. package/presets/clearai/template/skills/literature-review/references/database_strategies.md +0 -455
  90. package/presets/clearai/template/skills/literature-review/scripts/generate_pdf.py +0 -176
  91. package/presets/clearai/template/skills/literature-review/scripts/generate_schematic.py +0 -139
  92. package/presets/clearai/template/skills/literature-review/scripts/generate_schematic_ai.py +0 -817
  93. package/presets/clearai/template/skills/literature-review/scripts/search_databases.py +0 -303
  94. package/presets/clearai/template/skills/literature-review/scripts/verify_citations.py +0 -221
  95. package/presets/clearai/template/skills/paper-lookup/SKILL.md +0 -59
  96. package/presets/clearai/template/skills/paper-lookup/references/arxiv.md +0 -161
  97. package/presets/clearai/template/skills/paper-lookup/references/biorxiv.md +0 -118
  98. package/presets/clearai/template/skills/paper-lookup/references/core.md +0 -150
  99. package/presets/clearai/template/skills/paper-lookup/references/crossref.md +0 -181
  100. package/presets/clearai/template/skills/paper-lookup/references/medrxiv.md +0 -104
  101. package/presets/clearai/template/skills/paper-lookup/references/openalex.md +0 -174
  102. package/presets/clearai/template/skills/paper-lookup/references/pmc.md +0 -152
  103. package/presets/clearai/template/skills/paper-lookup/references/pubmed.md +0 -124
  104. package/presets/clearai/template/skills/paper-lookup/references/semantic-scholar.md +0 -203
  105. package/presets/clearai/template/skills/paper-lookup/references/unpaywall.md +0 -127
  106. package/presets/clearai/template/skills/process-presearch/SKILL.md +0 -196
  107. package/presets/clearai/template/skills/process-presearch/checklists/process_checklist.md +0 -18
  108. package/presets/clearai/template/skills/process-presearch/references/figure_code.md +0 -107
  109. package/presets/clearai/template/skills/process-presearch/references/source_attribution_example.md +0 -22
  110. package/presets/clearai/template/skills/process-understanding-extraction/SKILL.md +0 -69
  111. package/presets/clearai/template/skills/process-understanding-extraction/checklists/readiness_check.md +0 -34
  112. package/presets/clearai/template/skills/process-understanding-extraction/templates/docx_raw_dump_extractor.py.tpl +0 -132
  113. package/presets/clearai/template/skills/process-understanding-extraction/templates/entity_map_unit_topology.json.tpl +0 -86
  114. package/presets/clearai/template/skills/process-understanding-extraction/templates/process_brief.md.tpl +0 -89
  115. package/presets/clearai/template/skills/process-understanding-extraction/templates/process_brief_builder_from_raw_dump.py.tpl +0 -203
  116. package/presets/clearai/template/skills/process-understanding-extraction/templates/process_flow_mermaid.md.tpl +0 -41
  117. package/presets/clearai/template/skills/process-understanding-extraction/templates/unified_execution_plan.md.tpl +0 -53
  118. package/presets/clearai/template/skills/process-understanding-extraction/workflows/01-process-doc-discovery.md +0 -173
  119. package/presets/clearai/template/skills/process-understanding-extraction/workflows/02-process-understanding-and-diagramming.md +0 -106
  120. package/presets/clearai/template/skills/scientific-brainstorming/SKILL.md +0 -64
  121. package/presets/clearai/template/skills/scientific-brainstorming/references/brainstorming_methods.md +0 -326
  122. package/presets/clearai/template/skills/scientific-critical-thinking/SKILL.md +0 -72
  123. package/presets/clearai/template/skills/scientific-critical-thinking/references/common_biases.md +0 -364
  124. package/presets/clearai/template/skills/scientific-critical-thinking/references/evidence_hierarchy.md +0 -485
  125. package/presets/clearai/template/skills/scientific-critical-thinking/references/experimental_design.md +0 -496
  126. package/presets/clearai/template/skills/scientific-critical-thinking/references/logical_fallacies.md +0 -478
  127. package/presets/clearai/template/skills/scientific-critical-thinking/references/scientific_method.md +0 -169
  128. package/presets/clearai/template/skills/scientific-critical-thinking/references/statistical_pitfalls.md +0 -506
  129. package/presets/clearai/template/skills/skill-creator/SKILL.md +0 -109
  130. package/presets/clearai/template/skills/skill-creator/references/authoring-guide.md +0 -89
  131. package/presets/clearai/template/skills/statistical-analysis/SKILL.md +0 -79
  132. package/presets/clearai/template/skills/statistical-analysis/references/assumptions_and_diagnostics.md +0 -369
  133. package/presets/clearai/template/skills/statistical-analysis/references/bayesian_statistics.md +0 -653
  134. package/presets/clearai/template/skills/statistical-analysis/references/effect_sizes_and_power.md +0 -578
  135. package/presets/clearai/template/skills/statistical-analysis/references/reporting_standards.md +0 -469
  136. package/presets/clearai/template/skills/statistical-analysis/references/test_selection_guide.md +0 -129
  137. package/presets/clearai/template/skills/statistical-analysis/scripts/assumption_checks.py +0 -538
  138. package/presets/clearai/template/skills/web-artifact/SKILL.md +0 -165
  139. package/presets/clearai/template/skills/web-artifact/assets/renderer/renderer.css +0 -229
  140. package/presets/clearai/template/skills/web-artifact/assets/renderer/renderer.js +0 -373
  141. package/presets/clearai/template/skills/web-artifact/assets/vendor/elkjs/LICENSE +0 -263
  142. package/presets/clearai/template/skills/web-artifact/assets/vendor/elkjs/UPSTREAM.md +0 -26
  143. package/presets/clearai/template/skills/web-artifact/assets/vendor/elkjs/elk.bundled.js +0 -6605
  144. package/presets/clearai/template/skills/web-artifact/references/when-drawing-a-topology.md +0 -150
  145. package/presets/clearai/template/skills/web-artifact/references/when-the-page-must-work-offline.md +0 -62
  146. package/presets/clearai/template/skills/web-artifact/scripts/check_artifact.py +0 -167
  147. package/presets/clearai/template/skills/web-artifact/scripts/render_topology.js +0 -272
  148. package/presets/clearai/template/skills/what-if-oracle/LICENSE.txt +0 -5
  149. package/presets/clearai/template/skills/what-if-oracle/SKILL.md +0 -72
  150. 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, lessonFromFile, 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,35 @@ 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
+ * v16 → v17:**预期与未解释**:步骤带 `expect`(`step/expected` 也能补写);多了 `anomalies`(未解释项,
64
+ * `anomaly/opened` / `anomaly/resolved` 折进来,评估者报的随 `audit/settled` 一起来);目标带
65
+ * `irreversible`(不可逆动作的命令特征);放行带 `action`。对手判断(`rival` / `split_by`)删了,
66
+ * 旧账里的这两格不再读。
67
+ * v17 → v18:多了 `lessons`(经验:结案时经独立评估核过的「下次要留意什么」,`lesson/recorded` 折进来;
68
+ * 别的会话留下的从 `clear/knowledge/lessons/<id>.json` 经 `workspace/synced` 进来)。
69
+ * 投影缓存按版本判定,所以旧缓存会被丢弃、从日志重折一遍。
48
70
  */
49
- export const STATE_VERSION = 11
71
+ export const STATE_VERSION = 18
50
72
 
51
73
  /**
52
74
  * **只留台账、不折进视图**的变更类型(词汇表的另一半)。
@@ -56,11 +78,10 @@ export const STATE_VERSION = 11
56
78
  * · 发行自检按它比对内核实际落的每一类事实 —— 既没折、又不在清单里的,是**真的缺口**;
57
79
  * · 读代码的人不必逐条 grep 才知道哪些事实不上界面。
58
80
  *
59
- * 这四类都是**账本事实**:它们记「系统/账本做了什么」,不改变任何派生量
60
- * (准入检查、提交流水、快照、恢复),面板只展示当前可读取的证据与记录,
61
- * 而不是从投影里再长一份。
81
+ * 它们记「系统做了什么」(准入检查),不改变任何派生量。
82
+ * 旧日志里的 `git/*`(已删除的账本)不再列出:不认识的类型原样返回,本来就会被安静跳过。
62
83
  */
63
- export const LEDGER_ONLY_MUTATIONS = ['admission/checked', 'git/committed', 'git/restored', 'git/snapshot']
84
+ export const LEDGER_ONLY_MUTATIONS = ['admission/checked']
64
85
 
65
86
  /** 空状态。`init` 与「日志里还没有任何变更」都必须给出同一个形状。 */
66
87
  export function emptyState() {
@@ -72,45 +93,15 @@ export function emptyState() {
72
93
  audits: [],
73
94
  materials: [],
74
95
  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
96
  blocks: {},
97
97
  releases: [],
98
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
- * 事实摆在这里,人与模型都看得见「它还是不是空壳」。
99
+ * **未解释项**(认识论里的「反常」):和预期或本体对不上的观测。只有三个去处:
100
+ * 被解释(`explained`)、写明理由排除(`ruled_out`)、交给人(`escalated`);没去处的就是开着(`open`)。
112
101
  */
113
- constitution: null,
102
+ anomalies: [],
103
+ /** **经验**:本会话结案写下的(别的会话的在工作区文件里,派生时合进来)。 */
104
+ lessons: [],
114
105
  /**
115
106
  * **本体形状**:内核随投影下发的那份声明(对象/状态/边/等级)。
116
107
  * 为什么进投影而不是在界面里手抄:面板那一格的页眉要从**声明**生成——
@@ -149,87 +140,38 @@ export function emptyState() {
149
140
  */
150
141
  hostHealth: [],
151
142
  /**
152
- * **续跑窗口的账**:我们向平台说过的那句话——「这个窗口归我布防 / 我按了暂停 /
153
- * 我收兵了 / 它不在了而那不是我们干的」。形状见 `applyMutation` 的 `continuation/set`。
143
+ * **工作区文件快照**(`workspace/synced`):路径 → `{ digest, data }` 或 `{ digest, error }`。
154
144
  *
155
- * 为什么它必须在投影里而不是内核内存里:宿主的 `paused` 相位分不清「人按的」与
156
- * 「策略按的」,而这两者的处置正好相反(人按的绝不覆盖,自己按的要能恢复)。
157
- * 只记在内存里,一次重启就把我们自己的暂停误报成人的暂停——那是一句**不实的话**;
158
- * 而「我们说过的话要有账」本来就是这套设计的纪律(意图工具不能断言事实)。
145
+ * 攒下来的东西(事实、本体)住在项目文件里,跨会话活着;而投影只吃账本。内核每一拍把
146
+ * 这两处的**变化**折成一条变更,于是文件内容也是账本里一条可重放的事实——重放、分叉、
147
+ * 离线复判读到的都是那一刻的文件,而不是此刻盘上的。
148
+ */
149
+ workspace: { files: {} },
150
+ /**
151
+ * **本体文件的跨文件问题**(第二道校验,只提示):引用断了、类型对不上、单值关系两个取值……
152
+ * 有问题的节点或边不进图,问题列在这里,卡片与本体页都读它。
159
153
  */
160
- continuation: null,
154
+ ontologyProblems: [],
155
+ /**
156
+ * **人说话用的语言**('zh' / 'en';还没听到人说话时为 null)。从人的消息里折出来:
157
+ * 工具结果、运行态卡、面板读数都跟着它(见 `lang.js`)。
158
+ */
159
+ language: null,
161
160
  /** 正在飞的工具调用(来自 tool/call):让「评估者在裁决」成为一条可看见的事实 */
162
161
  inFlight: null,
163
162
  /** 本会话自己写过的路径(来自 tool/call 的 write/edit):L4 来源分离的判据 */
164
163
  written: [],
165
- /**
166
- * **会改工作区的工具调用**的一次计数(来自 tool/call 的 write/edit/bash/pwsh)。
167
- *
168
- * 为什么要有它:`written` 只认 write/edit,而探索最常走 bash(脚本自己产出文件),
169
- * 于是「这个会话到底动过工作区没有」用 `written` 答不全。内核在**回合边界**上要问的
170
- * 正是这一句(是否值得记一次工作区快照),所以这里记一个只增的计数——
171
- * 它是日志折出来的,可重放,不是第二本账。
172
- */
173
- writeCalls: 0,
174
164
  }
175
165
  }
176
166
 
177
167
  /**
178
- * 等裁决的三个工具:它们的调用在飞时,派生阶段是 `auditing`。
168
+ * 等裁决的工具:它们的调用在飞时,派生阶段是 `auditing`。`CloseGoal` 是 `Conclude` 的旧名,留着让旧日志照样折。
179
169
  * 其余工具不等外部裁决,不该让面板在每次调用时跳一下。
180
170
  */
181
- const VERDICT_WAITERS = new Set(['AdvancePlan', 'AdvanceWorldline', 'CloseGoal'])
171
+ const VERDICT_WAITERS = new Set(['AdvancePlan', 'Conclude', 'CloseGoal'])
182
172
 
183
173
  /** 记下来的自写路径有上限:它是判据,不是档案(档案在会话日志里)。 */
184
174
  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
175
 
234
176
  /** 深拷贝(状态是纯 JSON,这是唯一需要的工具)。 */
235
177
  function clone(value) {
@@ -257,6 +199,16 @@ function factFromHypothesis(facts, goalId, hypothesisId, claim) {
257
199
  })
258
200
  }
259
201
 
202
+ /**
203
+ * 步骤检验的判断一律折成 `{hypotheses: id[], level}`:新账写 `hypotheses`(可多条),
204
+ * 旧账只有单条 `hypothesis`。读面只认这一种形状。
205
+ */
206
+ export function normalizeTests(tests) {
207
+ if (tests === undefined || tests === null || typeof tests !== 'object') return null
208
+ const hypotheses = Array.isArray(tests.hypotheses) ? tests.hypotheses.map(String) : typeof tests.hypothesis === 'string' && tests.hypothesis !== '' ? [tests.hypothesis] : []
209
+ return { hypotheses, level: tests.level ?? null }
210
+ }
211
+
260
212
  function appendSteps(plan, rawSteps) {
261
213
  for (const raw of rawSteps) {
262
214
  plan.steps.push({
@@ -265,7 +217,9 @@ function appendSteps(plan, rawSteps) {
265
217
  do: raw.do,
266
218
  artifacts: raw.artifacts ?? [],
267
219
  done_criteria: raw.done_criteria,
268
- tests: raw.tests ?? null,
220
+ tests: normalizeTests(raw.tests),
221
+ /** 动手前写下的预期(可选);落空的地方记成未解释项。 */
222
+ expect: typeof raw.expect === 'string' && raw.expect !== '' ? raw.expect : null,
269
223
  status: 'open',
270
224
  evidence: null,
271
225
  advancedAt: null,
@@ -287,6 +241,7 @@ export function applyMutation(state, mutation) {
287
241
  if (mutation === null || typeof mutation !== 'object' || typeof mutation.t !== 'string') return state
288
242
  const at = typeof mutation.at === 'number' ? mutation.at : 0
289
243
  const next = clone(state)
244
+ if (!Array.isArray(next.anomalies)) next.anomalies = []
290
245
  const RANK = { open: 0, blocked: 0, advanced: 1, void: 1 }
291
246
  const settle = (step, status) => {
292
247
  if (step === undefined || step === null) return
@@ -304,6 +259,7 @@ export function applyMutation(state, mutation) {
304
259
  previous.claim = mutation.claim
305
260
  previous.done_criteria = mutation.done_criteria
306
261
  previous.promote_at_level = mutation.promote_at_level
262
+ if (Array.isArray(mutation.irreversible)) previous.irreversible = clone(mutation.irreversible)
307
263
  /** 修订可以带上新的速览与判据清单(没带就保持旧值——「不提供」不等于「清空」)。 */
308
264
  if (typeof mutation.headline === 'string' && mutation.headline !== '') previous.headline = mutation.headline
309
265
  if (Array.isArray(mutation.criteria)) previous.criteria = clone(mutation.criteria)
@@ -316,6 +272,8 @@ export function applyMutation(state, mutation) {
316
272
  claim: mutation.claim,
317
273
  done_criteria: mutation.done_criteria,
318
274
  promote_at_level: mutation.promote_at_level,
275
+ /** 不可逆动作:`{action, command}`,命令里出现 `command` 那段原文就要人放行。 */
276
+ irreversible: Array.isArray(mutation.irreversible) ? clone(mutation.irreversible) : [],
319
277
  status: 'open',
320
278
  revision: mutation.revision ?? 1,
321
279
  reasons: [mutation.reason ?? null],
@@ -357,14 +315,20 @@ export function applyMutation(state, mutation) {
357
315
  if (String(known.claim ?? '') !== claim) continue
358
316
  known.refute_when = hypothesis.refute_when
359
317
  known.version = hypothesis.version ?? known.version
318
+ if (typeof hypothesis.name === 'string' && hypothesis.name !== '') known.name = hypothesis.name
319
+ if (typeof hypothesis.retests === 'string' && hypothesis.retests !== '') known.retests = hypothesis.retests
360
320
  if (Array.isArray(hypothesis.assertions)) known.assertions = clone(hypothesis.assertions)
361
321
  continue
362
322
  }
363
323
  next.hypotheses.push({
364
324
  id: hypothesis.id,
365
325
  goal: mutation.id,
326
+ /** 短名(第六阶段起,模型自己起);旧日志没有它,读的一侧取主张开头。 */
327
+ ...(typeof hypothesis.name === 'string' && hypothesis.name !== '' ? { name: hypothesis.name } : {}),
366
328
  claim,
367
329
  refute_when: hypothesis.refute_when,
330
+ /** 复检的是哪条已有事实(事实 id;可以是别的会话留下的)。 */
331
+ ...(typeof hypothesis.retests === 'string' && hypothesis.retests !== '' ? { retests: hypothesis.retests } : {}),
368
332
  status: 'proposed',
369
333
  version: hypothesis.version ?? 1,
370
334
  /**
@@ -372,12 +336,6 @@ export function applyMutation(state, mutation) {
372
336
  * 不写断言照旧成立(宽松+校验);写了就是「这条主张用这门语言怎么说」。
373
337
  */
374
338
  assertions: Array.isArray(hypothesis.assertions) ? clone(hypothesis.assertions) : null,
375
- /**
376
- * **跳级理由**(`level/skipped` 折进来)。跳级本身不违规(首次测量没有廉价路
377
- * 可走),但「我直接上了 L3、下面几级从没走过」必须能说清为什么不适用——
378
- * 理由落在这里,`derive()` 会把被理由覆盖的层从 `untouchedLevels` 里减掉。
379
- */
380
- skips: [],
381
339
  at,
382
340
  })
383
341
  }
@@ -423,31 +381,12 @@ export function applyMutation(state, mutation) {
423
381
  closedAt: null,
424
382
  summary: null,
425
383
  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
384
  steps: [],
438
385
  }
439
386
  next.plans.push(plan)
440
387
  appendSteps(plan, mutation.steps ?? [])
441
388
  break
442
389
  }
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
390
  case 'plan/amended': {
452
391
  const plan = planOf(mutation.plan)
453
392
  if (plan === undefined || plan.status !== 'active') break
@@ -461,6 +400,44 @@ export function applyMutation(state, mutation) {
461
400
  step.criteria_versions.push(mutation.new_criteria) // 旧版本留着,什么都不删
462
401
  break
463
402
  }
403
+ case 'step/expected': {
404
+ const step = stepOf(mutation.plan, mutation.step)
405
+ if (step === undefined || step.status !== 'open') break
406
+ step.expect = String(mutation.expect ?? '') || null
407
+ break
408
+ }
409
+ case 'anomaly/opened': {
410
+ if (next.anomalies.some((item) => item.id === mutation.id)) break
411
+ next.anomalies.push({ id: mutation.id, what: String(mutation.what ?? ''), anchor: mutation.anchor ?? null, step: mutation.step ?? null, by: mutation.by ?? 'model', status: 'open', reason: null, explainedBy: null, at, resolvedAt: null })
412
+ break
413
+ }
414
+ case 'lesson/recorded': {
415
+ if (!Array.isArray(next.lessons)) next.lessons = []
416
+ if (next.lessons.some((item) => item.id === mutation.id)) break
417
+ next.lessons.push({
418
+ id: mutation.id,
419
+ goal: mutation.goal ?? null,
420
+ text: String(mutation.text ?? ''),
421
+ kind: mutation.kind ?? 'trap',
422
+ about: Array.isArray(mutation.about) ? mutation.about.map(String) : [],
423
+ evidence: mutation.evidence ?? null,
424
+ boundary: mutation.boundary ?? null,
425
+ status: 'active',
426
+ path: mutation.path ?? null,
427
+ at,
428
+ })
429
+ break
430
+ }
431
+ case 'anomaly/resolved': {
432
+ const item = next.anomalies.find((entry) => entry.id === mutation.id)
433
+ if (item === undefined || item.status !== 'open') break
434
+ if (!['explained', 'ruled_out', 'escalated'].includes(mutation.outcome)) break
435
+ item.status = mutation.outcome
436
+ item.reason = mutation.reason ?? null
437
+ item.explainedBy = mutation.by ?? null
438
+ item.resolvedAt = at
439
+ break
440
+ }
464
441
  case 'plan/voided': {
465
442
  const step = stepOf(mutation.plan, mutation.step)
466
443
  if (step === undefined) break
@@ -503,7 +480,6 @@ export function applyMutation(state, mutation) {
503
480
  note: mutation.note ?? null,
504
481
  path: mutation.path ?? null,
505
482
  step: mutation.step ?? null,
506
- branch: mutation.branch ?? null,
507
483
  at,
508
484
  })
509
485
  break
@@ -560,11 +536,21 @@ export function applyMutation(state, mutation) {
560
536
  const audit = next.audits.find((item) => item.id === mutation.id)
561
537
  if (audit !== undefined) {
562
538
  audit.verdict = mutation.verdict
539
+ /** 两项裁决:交付成立吗 + 每条判断的结果。旧账没有这两格,`verdict` 那时说的就是交付成立吗。 */
540
+ audit.holds = mutation.holds ?? null
541
+ audit.results = Array.isArray(mutation.results) ? mutation.results : []
563
542
  audit.shortfalls = mutation.shortfalls ?? []
564
543
  audit.basis = mutation.basis ?? null
565
544
  audit.card_path = mutation.card_path ?? null
566
545
  if (mutation.digest !== undefined) audit.digest = mutation.digest ?? null
567
546
  }
547
+ /** 评估者报的、做的人没登记的异常:记成未解释项(id 由裁决 id 派生,重放幂等)。 */
548
+ const found = Array.isArray(mutation.anomalies) ? mutation.anomalies : []
549
+ for (const [index, item] of found.entries()) {
550
+ const id = `${mutation.id}#u${index + 1}`
551
+ if (next.anomalies.some((entry) => entry.id === id)) continue
552
+ next.anomalies.push({ id, what: String(item?.what ?? ''), anchor: null, step: mutation.step ?? null, by: 'evaluator', matters: item?.matters ?? null, status: 'open', reason: null, explainedBy: null, at, resolvedAt: null })
553
+ }
568
554
  break
569
555
  }
570
556
  case 'evidence/recorded': {
@@ -572,7 +558,8 @@ export function applyMutation(state, mutation) {
572
558
  id: mutation.id,
573
559
  step: mutation.step,
574
560
  plan: mutation.plan,
575
- branch: mutation.branch ?? null,
561
+ /** 针对哪条判断;旧账没有这一格(`undefined`),按步骤检验的判断找回来。 */
562
+ hypothesis: mutation.hypothesis,
576
563
  verdict: mutation.verdict,
577
564
  level: mutation.level,
578
565
  evaluator: mutation.evaluator,
@@ -595,24 +582,27 @@ export function applyMutation(state, mutation) {
595
582
  const step = stepOf(mutation.plan, mutation.step)
596
583
  if (step !== undefined) {
597
584
  settle(step, 'advanced')
598
- step.evidence = mutation.evidence
585
+ /** 新账是证据 id 的数组(每条判断一份,不检验判断就是空数组);旧账是单个 id。 */
586
+ step.evidence = Array.isArray(mutation.evidence) ? mutation.evidence : mutation.evidence === undefined || mutation.evidence === null ? [] : [mutation.evidence]
587
+ /** 交付本身:谁判的、凭什么、出处。旧账没有,读面退回到证据。 */
588
+ step.delivery =
589
+ mutation.evaluator === undefined
590
+ ? null
591
+ : { evaluator: mutation.evaluator, basis: mutation.basis ?? null, refs: Array.isArray(mutation.refs) ? mutation.refs : [], origins: Array.isArray(mutation.origins) ? mutation.origins : [] }
599
592
  step.advancedAt = at
600
593
  }
601
594
  break
602
595
  }
603
596
  case 'human/released': {
604
- /**
605
- * 放行绑在**哪条轴**上要说清:主线绑步骤、世界线绑分支。
606
- * `via` 记它凭什么算数——现在只有 `approval`(原生审批栈的权威记录),不推断。
607
- */
597
+ /** `via` 记它凭什么算数——现在只有 `approval`(原生审批栈的权威记录),不推断。 */
608
598
  next.releases.push({
609
599
  step: mutation.step ?? null,
610
600
  plan: mutation.plan ?? null,
611
- fork: mutation.fork ?? null,
612
- branch: mutation.branch ?? null,
613
601
  via: mutation.via ?? null,
614
602
  at,
615
603
  call: mutation.call ?? null,
604
+ /** 放行的是哪件不可逆动作(拦在命令上的那道门);步级放行没有这一格。 */
605
+ ...(typeof mutation.action === 'string' && mutation.action !== '' ? { action: mutation.action } : {}),
616
606
  })
617
607
  break
618
608
  }
@@ -633,10 +623,47 @@ export function applyMutation(state, mutation) {
633
623
  evidence: mutation.evidence ?? [],
634
624
  path: mutation.path ?? null,
635
625
  assertions: Array.isArray(mutation.assertions) ? clone(mutation.assertions) : null,
626
+ /** 升格那一刻,断言用到的词条各自的含义指纹:之后定义改了,这条事实要复核。 */
627
+ definitions: mutation.definitions !== null && typeof mutation.definitions === 'object' && !Array.isArray(mutation.definitions) ? clone(mutation.definitions) : null,
636
628
  at,
637
629
  })
638
630
  break
639
631
  }
632
+ /**
633
+ * **工作区同步**(`workspace/synced`,内核发):事实文件与本体文件这一拍的变化。
634
+ * 每条 change 是一个文件的新样子(`data` 或读不成时的 `error`),或 `removed`。
635
+ * 按路径覆盖,所以同一批变化折两遍结果一样。
636
+ */
637
+ case 'workspace/synced': {
638
+ const files = next.workspace !== null && typeof next.workspace === 'object' && next.workspace.files !== null && typeof next.workspace.files === 'object' ? next.workspace.files : {}
639
+ next.workspace = { ...(next.workspace ?? {}), files }
640
+ for (const change of Array.isArray(mutation.changes) ? mutation.changes : []) {
641
+ const path = typeof change?.path === 'string' ? change.path : ''
642
+ if (path === '') continue
643
+ if (change.removed === true) {
644
+ delete files[path]
645
+ continue
646
+ }
647
+ files[path] = change.error !== undefined && change.error !== null ? { digest: change.digest ?? null, error: String(change.error) } : { digest: change.digest ?? null, data: change.data ?? null }
648
+ }
649
+ /**
650
+ * **本体从文件折出来**:词汇、实体、实体断言整份按文件重算——文件就是权威,
651
+ * 账本里只记「那一刻文件是什么样」。旧会话里工具写下的本体事件仍折得出来,
652
+ * 但一旦这个会话同步过本体文件,就以文件为准(旧会话不迁移)。
653
+ */
654
+ const touchedOntology = (mutation.changes ?? []).some((change) => {
655
+ const kind = classifyWorkspacePath(change?.path)?.kind
656
+ return kind !== undefined && kind !== 'fact' && kind !== 'lesson'
657
+ })
658
+ if (touchedOntology || Object.keys(files).some((path) => !['fact', 'lesson'].includes(classifyWorkspacePath(path)?.kind))) {
659
+ const ontology = materializeOntology(files)
660
+ next.lexicon = ontology.lexicon
661
+ next.entities = ontology.entities
662
+ next.entityAssertions = ontology.entityAssertions
663
+ next.ontologyProblems = ontology.problems
664
+ }
665
+ break
666
+ }
640
667
  case 'fact/reviewed': {
641
668
  /**
642
669
  * **人审查过一条被推翻标记的事实**:撤回它,或判证据不可靠、维持原事实。
@@ -650,260 +677,6 @@ export function applyMutation(state, mutation) {
650
677
  fact.review = { decision: mutation.decision === 'retracted' ? 'retracted' : 'kept', reason: mutation.reason ?? null, at, by: mutation.by ?? 'user' }
651
678
  break
652
679
  }
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
680
  case 'ontology/term_added':
908
681
  case 'ontology/predicate_added':
909
682
  case 'ontology/term_revised':
@@ -972,19 +745,6 @@ export function applyMutation(state, mutation) {
972
745
  })
973
746
  break
974
747
  }
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
748
  /**
989
749
  * **判据修订**(`criteria/revised`):改「怎样算完成」这件事本身。
990
750
  *
@@ -1063,6 +823,16 @@ function stampAt(mutations, time) {
1063
823
  * 宿主读已发布 V3 日志时把旧形状抬升成 `plugin:clearai`;但事件被**直接**喂进这个纯函数时
1064
824
  * (测试、旧导出、重放工具)仍带着旧形状,所以两侧都认。
1065
825
  */
826
+ /** 人写的一条消息是哪种语言(插件消息、续跑提示不算)。 */
827
+ function humanLanguage(message) {
828
+ if (message === null || typeof message !== 'object' || message.role !== 'user') return null
829
+ const kind = message.source?.kind
830
+ if (kind !== undefined && kind !== 'user') return null
831
+ const text = messageText(message)
832
+ if (text.startsWith(HUMAN_GATE_MARK)) return null
833
+ return detectLanguage(text)
834
+ }
835
+
1066
836
  function isClearaiSource(source) {
1067
837
  if (source === null || typeof source !== 'object' || !Array.isArray(source.sections)) return false
1068
838
  return source.kind === 'plugin:clearai' || (source.kind === 'plugin' && source.plugin === 'clearai')
@@ -1078,6 +848,13 @@ function isClearaiSource(source) {
1078
848
  */
1079
849
  export function applyEvent(state, event) {
1080
850
  if (event === null || typeof event !== 'object') return state
851
+ // 人说话用的语言:每一条人写的消息都可能改判(没有信号就不改)。这一拍折出来的文字(本体问题等)也用它。
852
+ const spoken = event.type === 'user/message' ? humanLanguage(event.data) : null
853
+ const base = spoken !== null && spoken !== state?.language ? { ...state, language: spoken } : state
854
+ return withLanguage(base?.language, () => foldEvent(base, event))
855
+ }
856
+
857
+ function foldEvent(state, event) {
1081
858
  if (event.type === 'user/message') {
1082
859
  // 内核观察到的事实(候选技能、采纳记录、合并目录)走**插件消息的结构化 section**——
1083
860
  // 记在会话日志里,所以状态仍然可以从日志重放出来,而不是靠一句散文。
@@ -1100,8 +877,6 @@ export function applyEvent(state, event) {
1100
877
  * 原来找到目录就 return,于是同一条消息里的**运行档被吃掉**,
1101
878
  * 表现是投影里的 effective 档永远停在「还没定档」,而机制那边早就按新档跑了。
1102
879
  */
1103
- const brain = source.sections.find((section) => section?.name === 'clearai/brain')
1104
- const tier = source.sections.find((section) => section?.name === 'clearai/autonomy')
1105
880
  /**
1106
881
  * 内核在**回合之间**观察到的事实(目前:世界线执行者跑完)。
1107
882
  * 与工具结果里的 `meta.mutations` **同形**,所以直接走同一个 `applyMutations`——
@@ -1122,28 +897,9 @@ export function applyEvent(state, event) {
1122
897
  }
1123
898
  }
1124
899
  const facts = source.sections.find((section) => section?.name === 'clearai/mutations')
1125
- if (brain !== undefined || tier !== undefined || facts !== undefined) {
900
+ if (facts !== undefined) {
1126
901
  let next = clone(state)
1127
902
  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
903
  if (facts !== undefined && typeof facts.text === 'string') {
1148
904
  try {
1149
905
  const payload = JSON.parse(facts.text)
@@ -1155,61 +911,18 @@ export function applyEvent(state, event) {
1155
911
  // 同上:坏 payload 不当事实,也不影响别的。
1156
912
  }
1157
913
  }
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
914
  if (touched) return next
1174
915
  }
1175
916
  }
1176
- // 人在面板上按下的人门动作 → 落成**事实**(不是请求):确认计划就地生效;
1177
- // 世界线的裁决记在分叉上,由内核的 ConvergeFork/AbandonFork 落实(合并与清理是内核的活)。
917
+ // 旧日志里人在面板上按下的人门动作 → 照旧落成**事实**(面板已不再发)。
1178
918
  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
919
+ if (gate === null) return state
1183
920
  const at = typeof event.time === 'number' ? event.time : Date.now()
1184
921
  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
922
  /**
1197
923
  * 人审查一条事实:标的在 `value`(面板送出的是事实 id),缘由在 `note`。
1198
924
  * 已经审过的不再改(第一次决定为准,与计划授权那条同一条纪律)。
1199
925
  */
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
926
  if (gate.action === 'retract_fact' || gate.action === 'keep_fact') {
1214
927
  const fact = next.facts.find((item) => item.id === (gate.value ?? null))
1215
928
  if (fact === undefined || fact.review !== undefined) return next
@@ -1239,31 +952,12 @@ export function applyEvent(state, event) {
1239
952
  next.lexicon = applyLexiconMutation(next.lexicon, { t: kind, ...entry, by: 'user' }, at)
1240
953
  return next
1241
954
  }
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
955
  return next
1251
956
  }
1252
957
  if (event.type === 'tool/call') {
1253
958
  const data = event.data ?? {}
1254
959
  if (typeof data.name !== 'string') return state
1255
960
  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
961
  if (data.name === 'write' || data.name === 'edit') {
1268
962
  let args = null
1269
963
  try {
@@ -1277,11 +971,6 @@ export function applyEvent(state, event) {
1277
971
  if (next.written.length > MAX_WRITTEN) next.written.splice(0, next.written.length - MAX_WRITTEN)
1278
972
  }
1279
973
  }
1280
- /**
1281
- * 会改工作区的那几件工具都记一笔:`write`/`edit` 是直接的,`bash`/`pwsh` 是间接的
1282
- * (脚本自己产出文件,内核看不见)。计数只增,判据留给用它的地方。
1283
- */
1284
- if (WRITE_CAPABLE_TOOLS.has(data.name)) next.writeCalls = (next.writeCalls ?? 0) + 1
1285
974
  next.inFlight = VERDICT_WAITERS.has(data.name) ? { name: data.name, callId: data.callId ?? null, at: typeof event.time === 'number' ? event.time : 0 } : null
1286
975
  return next
1287
976
  }
@@ -1316,10 +1005,30 @@ export function applyEvent(state, event) {
1316
1005
  /** 假设的终态:到了这里就不再是「还要继续做的活」。 */
1317
1006
  const TERMINAL_HYPOTHESIS = new Set(['refuted', 'superseded', 'retracted'])
1318
1007
 
1008
+ /**
1009
+ * **断言主体落图了吗**:一条判断的断言里,主体还不是实体图节点的那些(去重)。
1010
+ *
1011
+ * 节点有几种来路,都带出处或独立裁决:实体文件(`clear/ontology/entities/`)里的实体与它们的关系、升格事实里的主体,
1012
+ * 以及它们以 instance 形态引出的宾语。`derive()` 把读数挂在每条判断上(`unlanded`),
1013
+ * 缺口 ③ 与 `Conclude` 的实体门读的都是它——「卡上说落了」与「门说没落」不会出现两种读数。
1014
+ */
1015
+ function subjectsOffGraph(hypothesis, nodes) {
1016
+ const seen = new Map()
1017
+ for (const assertion of Array.isArray(hypothesis?.assertions) ? hypothesis.assertions : []) {
1018
+ const id = String(assertion?.subject?.id ?? '').trim()
1019
+ const type = String(assertion?.subject?.type ?? '').trim()
1020
+ if (id === '' || type === '') continue
1021
+ const key = `${type}|${id}`
1022
+ if (seen.has(key) || nodes.has(key)) continue
1023
+ seen.set(key, { id, type })
1024
+ }
1025
+ return [...seen.values()]
1026
+ }
1027
+
1319
1028
  /**
1320
1029
  * **知识模式(分诊)与缺口读数。**
1321
1030
  *
1322
- * 判据是**结构的,不是词法的**:这里不猜「这句话像不像研究任务」。立约(`SetGoal`)并用
1031
+ * 判据是**结构的,不是词法的**:这里不猜「这句话像不像研究任务」。立约(`Frame`)并用
1323
1032
  * 相互竞争的假设登记它,是模型自己已经做出的那次承诺——它意味着这件事要跨多轮、要有依据、
1324
1033
  * 要有可复核的结论。日常问答从不立约,于是从不进这一档。用词面启发式去猜任务类型,
1325
1034
  * 正是真跑里评估者抓到的那类「硬编码比例」的老路:猜错了没人能复核,而结构判据可以。
@@ -1344,48 +1053,11 @@ export function deriveKnowledge(state, hypotheses, factRows, lexicon) {
1344
1053
  const gaps = []
1345
1054
  if (mode === 'knowledge') {
1346
1055
  /**
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` 是同一条先例:**不逼裁决,但不许把「没看过」写成「没问题」**。
1056
+ * 缺口只留三种,都是「这一轮的结论还不能被复用」的直接读数,每种都指得到今天就能补的动作。
1057
+ *
1058
+ * ① **从没被证据碰过的判断**。只在这次会话已经真的跑出过证据之后才报——计划刚立、
1059
+ * 一步都还没走时,所有判断都是「没碰过」,那是正常的起点而不是缺口。
1060
+ * 不逼裁决,但不许把「没看过」写成「没问题」。
1389
1061
  */
1390
1062
  if (Array.isArray(state?.evidence) && state.evidence.length > 0) {
1391
1063
  const untouched = registered.filter((item) => item.refutations + item.inconclusive === 0 && item.supportedLevel === null)
@@ -1393,89 +1065,57 @@ export function deriveKnowledge(state, hypotheses, factRows, lexicon) {
1393
1065
  gaps.push({
1394
1066
  code: 'untouched_claims',
1395
1067
  count: untouched.length,
1396
- detail: `${untouched.length} 条在验命题还没有任何证据碰过(支持 / 推翻 / 无法判定都算碰过)`,
1397
- nextAction: '给它派一个带 tests 的步骤并交付:支持 / 推翻 / 无法判定都算碰过',
1068
+ detail: tr(`${untouched.length} 条验证中的判断还没有任何证据碰过(支持 / 推翻 / 不确定都算碰过)`, `${untouched.length} judgments under test have not been touched by any evidence (support, refute and inconclusive all count)`),
1069
+ nextAction: tr('给它派一个带 tests 的步骤并交付:支持 / 推翻 / 无法判定都算碰过', 'Give it a step with tests and deliver it: support, refute and inconclusive all count'),
1398
1070
  })
1399
1071
  }
1400
1072
  }
1401
1073
  /**
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` 就是**还没落地的个数**。
1074
+ * ② 只有散文主张的判断:断言是可选的,所以这里**不是违规**,也不拦结案,
1075
+ * 而是「这条主张还不能被机器比对、下一轮也按概念取不到」。只算非终态的。
1417
1076
  */
1077
+ const proseOnly = registered.filter((item) => !Array.isArray(item.assertions) || item.assertions.length === 0)
1078
+ if (proseOnly.length > 0) {
1079
+ gaps.push({
1080
+ code: 'prose_only_claims',
1081
+ count: proseOnly.length,
1082
+ 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`),
1083
+ 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)'),
1084
+ })
1085
+ }
1418
1086
  /**
1419
- * **"落地"的判据是"这句断言在图上有边",不是"图上有这个节点"。**
1087
+ * ③ **断言主体没落图**。判据与 `Conclude` 的实体门是同一条(每条判断上的 `unlanded`):
1088
+ * 断言的主体是不是实体图上带出处的节点。逐个主体判,`count` 就是还没落地的个数——
1089
+ * 随便登记一个无关节点不会让它消失。
1420
1090
  *
1421
- * 只数节点会漏掉最要命的那种形态:实例登记了(图上有节点),而关于它的那句话还挂在命题上
1422
- * ——图有节点、没有边,"实体图长出来了"仍然是一句空话。所以这里取**断言边的起点集合**:
1423
- * 一条边要么来自带出处的实体断言(`Assert`),要么来自过了独立裁决的升格事实。
1091
+ * 为什么看节点、不看边:边由升格本身落下(事实带着它的断言进图),
1092
+ * 要求在升格之前另在实体文件里把同一句话再写一遍,只是让模型重复劳动。
1424
1093
  */
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))
1094
+ const unlanded = [...new Map(registered.flatMap((item) => item.unlanded ?? []).map((item) => [`${item.type}|${item.id}`, item])).values()]
1428
1095
  if (unlanded.length > 0) {
1429
1096
  gaps.push({
1430
1097
  code: 'entities_unlanded',
1431
1098
  count: unlanded.length,
1432
- detail: `${unlanded.length} 个断言主体还没有落到实体图(共 ${wantedKeys.size} 个):断言只挂在命题上,不构成「已知」`,
1433
- nextAction: '先 RegisterInstance 把实例连出处登记下来;确实不值得留下形态就如实说清',
1434
- })
1435
- }
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 写明「为什么这一级在本项目里不适用」',
1099
+ detail: tr(
1100
+ `${unlanded.length} 个断言主体还没有落到实体图(${unlanded.map((item) => `${item.type}|${item.id}`).join('、')}):断言只挂在命题上,不构成「已知」`,
1101
+ `${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"`,
1102
+ ),
1103
+ nextAction: tr(
1104
+ '给这些主体各写一个实体文件(clear/ontology/entities/<id>.json,带类型与出处);确实不值得留下形态就把断言从判断上拿掉(Frame 修订)',
1105
+ '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)',
1106
+ ),
1451
1107
  })
1452
1108
  }
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
1109
  }
1470
1110
  return {
1471
1111
  mode,
1472
1112
  /** 一句人话:为什么进了这一档(或为什么没进)。它进卡,所以不许写成术语。 */
1473
1113
  why:
1474
1114
  mode === 'knowledge'
1475
- ? `目标还开着,带着 ${registered.length} 条登记过的命题——这是跨轮、要依据、要可复核结论的活`
1115
+ ? tr(`目标还开着,带着 ${registered.length} 条登记过的命题——这是跨轮、要依据、要可复核结论的活`, `The goal is open with ${registered.length} registered propositions: work across turns that needs evidence and checkable conclusions`)
1476
1116
  : open
1477
- ? '目标还开着,但没有登记命题:按普通任务推进'
1478
- : '没有开着的目标:按普通任务推进',
1117
+ ? tr('目标还开着,但没有登记命题:按普通任务推进', 'The goal is open but has no registered propositions: proceed as an ordinary task')
1118
+ : tr('没有开着的目标:按普通任务推进', 'No open goal: proceed as an ordinary task'),
1479
1119
  gaps,
1480
1120
  }
1481
1121
  }
@@ -1483,10 +1123,10 @@ export function deriveKnowledge(state, hypotheses, factRows, lexicon) {
1483
1123
  /**
1484
1124
  * **知识预检(preflight)**:进入知识模式那一刻,把「已知」主动送到模型面前。
1485
1125
  *
1486
- * 解决的问题:真跑里模型不查就开工——不是因为不知道有 \`QueryKnowledge\`,
1126
+ * 解决的问题:真跑里模型不查就开工——不是因为没有查询的入口,
1487
1127
  * 而是因为**没人提醒它此刻该查**。提示词会被读成建议;卡里的读数不会。
1488
1128
  * 于是这里把相关性判断做成投影:从当前目标与命题的文本出发,圈出**有界**的一组
1489
- * 已有词汇、事实与冲突,随运行态卡注入。模型只在这份摘要不够用时才需要精确查询。
1129
+ * 已有词汇、事实与冲突,随运行态卡注入。模型只在这份摘要不够用时才需要去读全文。
1490
1130
  *
1491
1131
  * 三条纪律(与缺口读数同一套):
1492
1132
  * · **只读**:不产生变更,不新增状态——它就是 \`derive\` 的另一个读面;
@@ -1506,7 +1146,9 @@ export function knowledgePreflight(state, derived) {
1506
1146
  const factRows = Array.isArray(derived?.factRows) ? derived.factRows : []
1507
1147
  const conflicts = Array.isArray(derived?.conflicts) ? derived.conflicts : []
1508
1148
  /** 命题的文本面:目标主张 + 每条命题的主张(去空白后做包含判断)。 */
1509
- const claimText = [state?.goal?.claim, ...hypotheses.map((item) => item.claim)].filter((text) => typeof text === 'string' && text !== '').map((text) => String(text).replace(/\s+/g, ''))
1149
+ /** 加上下一步要做什么与它的预期:预期引用的关系也要递到眼前。 */
1150
+ const nextStep = (derived?.activePlan?.steps ?? []).find((step) => step?.status === 'open') ?? null
1151
+ const claimText = [state?.goal?.claim, ...hypotheses.map((item) => item.claim), nextStep?.do, nextStep?.expect].filter((text) => typeof text === 'string' && text !== '').map((text) => String(text).replace(/\s+/g, ''))
1510
1152
  /** 命题已经引用的谓词(断言在假设上时就该算「在用」)。 */
1511
1153
  const usedPredicates = new Set(hypotheses.flatMap((item) => (Array.isArray(item.assertions) ? item.assertions : [])).map((assertion) => String(assertion?.predicate ?? '')))
1512
1154
  const hits = (entry) => {
@@ -1518,19 +1160,27 @@ export function knowledgePreflight(state, derived) {
1518
1160
  const matchedPredicates = predicates.filter((predicate) => usedPredicates.has(predicate.id) || hits(predicate))
1519
1161
  const matchedTermIds = new Set(matchedTerms.map((term) => term.id))
1520
1162
  const matchedPredicateIds = new Set(matchedPredicates.map((predicate) => predicate.id))
1521
- /** 事实:断言引用了命中的谓词,或主词类型是命中的概念。 */
1522
- const matchedFacts = factRows.filter((fact) =>
1523
- (Array.isArray(fact.assertions) ? fact.assertions : []).some((assertion) => matchedPredicateIds.has(String(assertion?.predicate ?? '')) || matchedTermIds.has(String(assertion?.subject?.type ?? ''))),
1163
+ /**
1164
+ * 事实:断言引用了命中的谓词或概念;**或**升格时按词面挂上的定义里有命中的词(真跑里事实几乎都没有断言,
1165
+ * 只认断言等于从不命中);**或**有判断用 `retests` 指着它。撤回的不给。
1166
+ */
1167
+ const retested = new Set(hypotheses.map((item) => item.retests).filter((id) => typeof id === 'string'))
1168
+ const matchedFacts = factRows.filter(
1169
+ (fact) =>
1170
+ fact.review?.decision !== 'retracted' &&
1171
+ (retested.has(fact.id) ||
1172
+ Object.keys(fact.definitions ?? {}).some((id) => matchedTermIds.has(id) || matchedPredicateIds.has(id)) ||
1173
+ (Array.isArray(fact.assertions) ? fact.assertions : []).some((assertion) => matchedPredicateIds.has(String(assertion?.predicate ?? '')) || matchedTermIds.has(String(assertion?.subject?.type ?? '')))),
1524
1174
  )
1525
1175
  return {
1526
1176
  mode: 'knowledge',
1527
1177
  /** 词面命中的词汇:模型接下来要写的结论大概率会用到它们。 */
1528
- terms: matchedTerms.slice(0, LIMIT).map((term) => ({ id: term.id, label: term.label, gloss: term.gloss, parent: term.parent ?? null, status: term.status, uses: term.uses ?? 0, basis: term.basis ?? null })),
1178
+ terms: matchedTerms.slice(0, LIMIT).map((term) => ({ id: term.id, label: term.label, gloss: term.gloss, kind: term.kind ?? null, unit: term.unit ?? null, parent: term.parent ?? null, status: term.status, uses: term.uses ?? 0, basis: term.basis ?? null })),
1529
1179
  termsTruncated: Math.max(0, matchedTerms.length - LIMIT),
1530
- predicates: matchedPredicates.slice(0, LIMIT).map((predicate) => ({ id: predicate.id, label: predicate.label, domain: predicate.domain, range: predicate.range, functional: predicate.functional === true, status: predicate.status, uses: predicate.uses ?? 0, basis: predicate.basis ?? null })),
1180
+ predicates: matchedPredicates.slice(0, LIMIT).map((predicate) => ({ id: predicate.id, label: predicate.label, gloss: predicate.gloss ?? '', kind: predicate.kind ?? null, shape: predicate.shape ?? null, check: predicate.check ?? null, domain: predicate.domain, range: predicate.range, functional: predicate.functional === true, status: predicate.status, uses: predicate.uses ?? 0, basis: predicate.basis ?? null })),
1531
1181
  predicatesTruncated: Math.max(0, matchedPredicates.length - LIMIT),
1532
1182
  /** 命中的既有事实(可复用的「已知」)。 */
1533
- facts: matchedFacts.slice(0, LIMIT).map((fact) => ({ id: fact.id, text: fact.text, level: fact.level ?? null, scope: fact.scope ?? null, hypothesis: fact.hypothesis ?? null, review: fact.review?.decision ?? null })),
1183
+ facts: matchedFacts.slice(0, LIMIT).map((fact) => ({ id: fact.id, text: fact.text, level: fact.level ?? null, scope: fact.scope ?? null, hypothesis: fact.hypothesis ?? null, review: fact.review?.decision ?? null, foreign: fact.foreign === true })),
1534
1184
  factsTruncated: Math.max(0, matchedFacts.length - LIMIT),
1535
1185
  /** 冲突:有就带上(它们约束「哪些结论还不能随便写)。 */
1536
1186
  conflicts: conflicts.map((conflict) => ({ predicate: conflict.predicate, subject: conflict.subject })),
@@ -1707,12 +1357,12 @@ function factChain(context, fact) {
1707
1357
  */
1708
1358
  function factHistory(fact, evidence) {
1709
1359
  const events = []
1710
- if (fact?.at !== undefined && fact?.at !== null) events.push({ kind: 'fact/promoted', at: fact.at, summary: `升格为事实(支持到 ${fact.level ?? '—'})` })
1360
+ 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
1361
  if (fact?.review !== undefined && fact?.review !== null) {
1712
1362
  events.push({
1713
1363
  kind: 'fact/reviewed',
1714
1364
  at: fact.review.at ?? null,
1715
- summary: fact.review.decision === 'retracted' ? '人审查后**撤回**(记录保留)' : '人审查后**维持**(判证据不可靠)',
1365
+ 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
1366
  reason: fact.review.reason ?? null,
1717
1367
  })
1718
1368
  }
@@ -1723,12 +1373,12 @@ function factHistory(fact, evidence) {
1723
1373
  /** 词汇条目的留痕:登记 / 历次修订 / 废止。登记与修订本来就存在词条里(折法保留的)。 */
1724
1374
  function entryHistory(entry, label) {
1725
1375
  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 })
1376
+ 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
1377
  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 })
1378
+ 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
1379
  }
1730
1380
  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 })
1381
+ 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
1382
  }
1733
1383
  return events.filter((event) => event.at !== null || event.kind.endsWith('_deprecated')).sort((left, right) => (left.at ?? 0) - (right.at ?? 0))
1734
1384
  }
@@ -1786,7 +1436,7 @@ function inspectConcept(context, termId) {
1786
1436
  history: entryHistory(term, 'term'),
1787
1437
  actions: { canFilter: true, canExpand: true, canEdit: true },
1788
1438
  /** 词汇是**约定**,不需要证据等级——把它说清楚,免得读的人以为它「没验」。 */
1789
- note: '概念是约定,不是主张:它不带证据等级。用这个词写下的句子才需要。',
1439
+ note: tr('概念是约定,不是主张:它不带证据等级。用这个词写下的句子才需要。', 'A concept is a convention, not a claim: it carries no evidence level. Only sentences written with it do.'),
1790
1440
  }
1791
1441
  }
1792
1442
 
@@ -1807,7 +1457,7 @@ function inspectValueType(context, form) {
1807
1457
  /** 值形态由系统固定,不进领域本体、也没有版本史——如实说空,不编一段。 */
1808
1458
  history: [],
1809
1459
  actions: { canFilter: true, canExpand: false, canEdit: false },
1810
- note: '值形态由系统固定(statement / quantity / formula / code / reference),不独立治理,所以没有版本史。',
1460
+ 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
1461
  }
1812
1462
  }
1813
1463
 
@@ -1851,7 +1501,7 @@ function inspectPredicate(context, predicateId) {
1851
1501
  .map((conflict) => ({ predicate: conflict.predicate, subject: conflict.subject, sides: conflict.sides.map((side) => ({ fact: side.fact ?? null, value: side.value ?? null })) })),
1852
1502
  history: entryHistory(predicate, 'predicate'),
1853
1503
  actions: { canFilter: true, canExpand: true, canEdit: true },
1854
- note: predicate.functional === true ? '单值谓词:同一主词上两条未撤回的确认事实取值不同时,系统给出一对冲突读数(只暴露,不裁决)。' : null,
1504
+ 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
1505
  }
1856
1506
  }
1857
1507
 
@@ -1903,7 +1553,7 @@ function inspectInstance(context, key) {
1903
1553
  .map((conflict) => ({ predicate: conflict.predicate, subject: conflict.subject, sides: conflict.sides.map((side) => ({ fact: side.fact ?? null, value: side.value ?? null })) })),
1904
1554
  history: [],
1905
1555
  actions: { canFilter: true, canExpand: true, canEdit: false },
1906
- note: '实例由断言投影出来,不单独注册、也不做实体消解(同名即同节点);它没有自己的版本史。',
1556
+ 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
1557
  }
1908
1558
  }
1909
1559
 
@@ -1944,7 +1594,7 @@ function inspectLiteral(context, key) {
1944
1594
  conflicts: [],
1945
1595
  history: [],
1946
1596
  actions: { canFilter: true, canExpand: true, canEdit: false },
1947
- note: '字面值是断言里的客体,由事实投影出来;它没有独立生命周期。',
1597
+ note: tr('字面值是断言里的客体,由事实投影出来;它没有独立生命周期。', 'A literal value is the object of an assertion, projected from facts; it has no lifecycle of its own.'),
1948
1598
  }
1949
1599
  }
1950
1600
 
@@ -1980,18 +1630,42 @@ function inspectEdge(context, edgeId) {
1980
1630
  conflicts: chain.conflicts,
1981
1631
  history: chain.history,
1982
1632
  actions: { canFilter: true, canExpand: true, canEdit: false },
1983
- note: predicate?.functional === true ? '这条边落在单值谓词上:同一主词出现第二个不同取值时会产生冲突(只暴露,不裁决)。' : null,
1633
+ 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,
1634
+ }
1635
+ }
1636
+
1637
+
1638
+ /**
1639
+ * **可信度怎么变的**:按证据落账的先后重放一遍,每一笔记下「之前 → 之后」。
1640
+ * 面板「点开一条」的第二段读的就是它;分组判据仍只有 `trustOf` 一处,这里只是逐笔调用。
1641
+ * 升格(写进长期知识)和撤回不在这里——它们由读的一侧接在末尾(见 `view`)。
1642
+ */
1643
+ function trustHistory(hypothesis, rows) {
1644
+ const history = [{ kind: 'proposed', at: hypothesis.at ?? null, to: 'testing' }]
1645
+ let replay = { status: 'proposed', supportedLevel: null, refutations: 0, inconclusive: 0 }
1646
+ const ordered = rows
1647
+ .map((item, index) => ({ item, index }))
1648
+ .sort((left, right) => (left.item.at ?? 0) - (right.item.at ?? 0) || left.index - right.index)
1649
+ .map(({ item }) => item)
1650
+ for (const item of ordered) {
1651
+ const from = trustOf(replay)
1652
+ if (item.verdict === 'refute') replay = { ...replay, refutations: replay.refutations + 1 }
1653
+ else if (item.verdict === 'inconclusive') replay = { ...replay, inconclusive: replay.inconclusive + 1 }
1654
+ else if (item.verdict === 'support' && LEVELS.indexOf(String(item.level ?? '').toUpperCase()) > LEVELS.indexOf(String(replay.supportedLevel ?? '').toUpperCase())) replay = { ...replay, supportedLevel: item.level }
1655
+ 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
1656
  }
1657
+ return history
1985
1658
  }
1986
1659
 
1987
1660
  /** 五种值形态的名字与一句话解释(名字取自 `domain-language` 的枚举,这里只加给人读的说明)。 */
1988
- const VALUE_FORM_GLOSS = {
1989
- statement: '短陈述字符串',
1990
- quantity: '数值 + 单位',
1991
- formula: '公式源码(LaTeX;本版不做语义解析)',
1992
- code: '指向工作区里真有的文件路径',
1993
- reference: '外部引用(文献 / URL / 编号)',
1661
+ const VALUE_FORM_GLOSS_TEXT = {
1662
+ statement: ['短陈述字符串', 'A short statement string'],
1663
+ quantity: ['数值 + 单位', 'A number plus a unit'],
1664
+ formula: ['公式源码(LaTeX;本版不做语义解析)', 'Formula source (LaTeX; its meaning is not parsed)'],
1665
+ code: ['指向工作区里真有的文件路径', 'A path to a file that exists in the workspace'],
1666
+ reference: ['外部引用(文献 / URL / 编号)', 'An external reference (paper / URL / identifier)'],
1994
1667
  }
1668
+ const VALUE_FORM_GLOSS = bilingual(VALUE_FORM_GLOSS_TEXT)
1995
1669
 
1996
1670
  export function derive(state) {
1997
1671
  const activePlan = state.plans.find((plan) => plan.status === 'active') ?? null
@@ -2012,11 +1686,16 @@ export function derive(state) {
2012
1686
  .map((item) => item.hypothesis)
2013
1687
  .filter((id) => typeof id === 'string' && id !== ''),
2014
1688
  )
1689
+ const instanceNodes = new Set(graphProjection(state).nodes.filter((node) => node.kind === 'instance').map((node) => String(node.id)))
2015
1690
  const hypotheses = state.hypotheses.map((hypothesis) => {
2016
1691
  const rows = []
2017
1692
  for (const item of state.evidence) {
1693
+ if (item.hypothesis !== undefined) {
1694
+ if (item.hypothesis === hypothesis.id) rows.push(item)
1695
+ continue
1696
+ }
2018
1697
  const step = stepOf(item.plan, item.step)
2019
- if (step?.tests?.hypothesis === hypothesis.id) rows.push(item)
1698
+ if ((normalizeTests(step?.tests)?.hypotheses ?? []).includes(hypothesis.id)) rows.push(item)
2020
1699
  }
2021
1700
  const refutations = rows.filter((item) => item.verdict === 'refute').length
2022
1701
  const inconclusive = rows.filter((item) => item.verdict === 'inconclusive').length
@@ -2029,40 +1708,13 @@ export function derive(state) {
2029
1708
  else if (status === 'proposed' && rows.length > 0) status = 'alive'
2030
1709
  // 被推翻是黏性终态:「已被替代」不该改写「已被推翻」——两件事。
2031
1710
  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 }
1711
+ return { ...hypothesis, status, supportedLevel: supportedLevel < 0 ? null : `L${supportedLevel}`, refutations, inconclusive, unlanded: subjectsOffGraph(hypothesis, instanceNodes), history: trustHistory(hypothesis, rows) }
2057
1712
  })
2058
1713
 
2059
1714
  /**
2060
1715
  * `plan_is_authorized`:有记号 **或** 已经真的推进过(行为即授权)。
2061
1716
  * 第二个分支在 ClearAI 那边是给存量文档用的;这里同样留着——事实与意图冲突时以事实为准。
2062
1717
  */
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
1718
  let phase = null
2067
1719
  let progress = null
2068
1720
  if (state.goal !== null) {
@@ -2106,140 +1758,44 @@ export function derive(state) {
2106
1758
  }
2107
1759
  }
2108
1760
 
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
1761
  /**
2234
1762
  * 事实那一行的**两个读数**(都派生,不另存):
2235
1763
  * · `refuted`——它的假设收到过推翻证据 ⇒ 这条事实要复核;
2236
1764
  * · `review` ——人已经审查过(撤回 / 维持),决定连缘由一起留着。
2237
1765
  */
2238
- const factRows = (state.facts ?? []).map((fact) => {
1766
+ const lexicon = normalizeLexicon(state.lexicon)
1767
+ /** 声明了 `retests` 的判断被推翻 ⇒ 它复检的那条事实也算被推翻(跨会话只认事实 id)。 */
1768
+ const retestRefuted = new Set(hypotheses.filter((item) => typeof item.retests === 'string' && (item.refutations ?? 0) > 0).map((item) => item.retests))
1769
+ const ownFacts = (state.facts ?? []).map((fact) => {
2239
1770
  const linked = typeof fact.hypothesis === 'string' && fact.hypothesis !== ''
2240
1771
  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 }
1772
+ return { ...fact, refuted: (owner?.refutations ?? 0) > 0 || retestRefuted.has(fact.id), review: fact.review ?? null, foreign: false }
2242
1773
  })
1774
+ /**
1775
+ * **别的会话留下的事实**(住在 `clear/knowledge/facts/<id>.json`,经 `workspace/synced` 进账)。
1776
+ * 同一个 id 本会话账上也有就以账上那条为准(它带着推翻读数);文件上的复核结论照样认。
1777
+ * 它们的假设不在本会话,所以推翻要在这里重新检验一次才算:本会话的判断用 `retests` 指向它,
1778
+ * 那条判断被推翻,这条事实就标「被推翻」。
1779
+ */
1780
+ const ownIds = new Set(ownFacts.map((fact) => fact.id))
1781
+ const foreignFacts = []
1782
+ for (const [path, file] of Object.entries(state.workspace?.files ?? {})) {
1783
+ if (classifyWorkspacePath(path)?.kind !== 'fact' || file?.data === undefined) continue
1784
+ const row = factFromFile(file.data, path)
1785
+ if (row === null) continue
1786
+ if (ownIds.has(row.id)) {
1787
+ const own = ownFacts.find((fact) => fact.id === row.id)
1788
+ if (own.review === null && row.review !== null) own.review = row.review
1789
+ continue
1790
+ }
1791
+ foreignFacts.push({ ...row, refuted: retestRefuted.has(row.id), foreign: true })
1792
+ }
1793
+ foreignFacts.sort((a, b) => (a.at ?? 0) - (b.at ?? 0))
1794
+ /**
1795
+ * `definitionsChanged`:升格那一刻用到的词条,**含义**此后改过或已不在的那些 id。
1796
+ * 只是读数,不撤回事实:改了定义的人知道为什么改,复核交给人与证据。
1797
+ */
1798
+ const factRows = [...foreignFacts, ...ownFacts].map((fact) => ({ ...fact, definitionsChanged: changedDefinitions(lexicon, fact.definitions) }))
2243
1799
 
2244
1800
  /**
2245
1801
  * **领域词汇的派生读数**(两条,都不新存东西):
@@ -2250,28 +1806,43 @@ export function derive(state) {
2250
1806
  * 两者都吃 `factRows` 而不是 `state.facts`:事实的复核态与推翻标记是派生的,
2251
1807
  * 在这里重算一遍就等于第二份判据。
2252
1808
  */
2253
- const lexicon = normalizeLexicon(state.lexicon)
1809
+ /**
1810
+ * **经验行**:本会话写下的,加上别的会话留在 `clear/knowledge/lessons/` 的。同 id 以文件为准
1811
+ * (人可以在文件上标 `status: "retracted"` 撤回);撤回的不再推送。新的在前。
1812
+ */
1813
+ const lessonById = new Map()
1814
+ for (const item of state.lessons ?? []) lessonById.set(item.id, { ...item })
1815
+ for (const [path, file] of Object.entries(state.workspace?.files ?? {})) {
1816
+ if (classifyWorkspacePath(path)?.kind !== 'lesson' || file?.data === undefined) continue
1817
+ const row = lessonFromFile(file.data, path)
1818
+ if (row !== null) lessonById.set(row.id, { ...lessonById.get(row.id), ...row, at: row.at ?? lessonById.get(row.id)?.at ?? null })
1819
+ }
1820
+ const lessonRows = [...lessonById.values()].filter((item) => item.status !== 'retracted').sort((a, b) => (b.at ?? 0) - (a.at ?? 0))
2254
1821
  const conflicts = deriveConflicts(factRows, lexicon)
2255
1822
  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',
1823
+ /**
1824
+ * **待处理**(第六阶段,取代空了的收件箱):只陈述、不放按钮——决定在对话里说,
1825
+ * 或由开门的那次调用用原生提问卡问。两类:计划连拦停下了;已写进长期知识的结论互相矛盾。
1826
+ * 都是派生读数:计划一解封、矛盾一方被推翻,条目自然消失。
1827
+ */
1828
+ const needYou = []
1829
+ 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?`) })
1830
+ for (const conflict of conflicts) {
1831
+ const values = (conflict.sides ?? []).map((side) => String(side.value ?? '?'))
1832
+ needYou.push({
1833
+ kind: 'conflict',
1834
+ text: tr(
1835
+ `${String(conflict.subject ?? '')}的${String(conflict.predicate ?? '')}:${values.map((value) => `「${value}」`).join('和')}矛盾,以哪个为准?`,
1836
+ `${String(conflict.predicate ?? '')} of ${String(conflict.subject ?? '')}: ${values.map((value) => `"${value}"`).join(' and ')} conflict. Which one holds?`,
1837
+ ),
2273
1838
  })
2274
1839
  }
1840
+ for (const fact of factRows) {
1841
+ if (fact.definitionsChanged.length === 0 || fact.review?.decision === 'retracted') continue
1842
+ const head = String(fact.text ?? '').replace(/\s+/g, ' ').trim()
1843
+ 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?`) })
1844
+ }
1845
+
2275
1846
 
2276
1847
  const settlement = state.evidence.map((item) => {
2277
1848
  const step = stepOf(item.plan, item.step)
@@ -2293,14 +1864,11 @@ export function derive(state) {
2293
1864
  activePlan,
2294
1865
  closedPlans,
2295
1866
  pendingAudit,
2296
- forks,
2297
1867
  factRows,
1868
+ lessonRows,
2298
1869
  settlement,
2299
1870
  stepOf,
2300
- inbox,
2301
- hasOpenGate,
2302
- planConfirmationPending,
2303
- planIsAuthorized,
1871
+ needYou,
2304
1872
  /** 领域词汇与它的两条派生读数(见上面那段:冲突与健康度都只是读数)。 */
2305
1873
  lexicon,
2306
1874
  conflicts,
@@ -2315,52 +1883,36 @@ export function derive(state) {
2315
1883
  *
2316
1884
  * 人在面板上按的每一个动作,都经宿主平面的路由变成**一条用户消息**进会话日志——
2317
1885
  * 内容是一行结构化标记,不是自然语言。为什么要这样:
2318
- * · **可审计**:谁在什么时候做了什么(采纳哪条世界线、扶正哪个技能、把档切成什么),
1886
+ * · **可审计**:谁在什么时候做了什么(撤回或维持哪条事实、改了哪个词条),
2319
1887
  * 就在日志里(`source.kind==='user'`);
2320
1888
  * · **不做 NLU**:ClearAI 把确认短语白名单整体删掉了,理由是「语义判断只归模型,
2321
1889
  * harness 只做顺序可判定的题」。标记进得来、自然语言进不来,是同一条纪律;
2322
1890
  * · **agent 不可达**:这些动词**没有工具 schema**——模型能调的工具面里不存在它们,
2323
1891
  * 它只能看见「人做了什么」这条事实。
2324
1892
  *
2325
- * 动词表只有 3 个(奥卡姆:另外砍掉的两个都是**重复**):
1893
+ * 砍掉的动词(奥卡姆:都是**重复**,或者它服务的机制已经删除):
2326
1894
  * · `confirm_plan` —— 原生 `dsh-plan-mode` 就是「用户复核的出口」;而我们自己的
2327
1895
  * `planIsAuthorized` 本来就承认「交付第一步即授权」。计划确认从来不是闸门(它是记号),
2328
1896
  * 少一个假装成闸门的按钮,界面就不再暗示一条不存在的约束。需要人拍板时用原生
2329
1897
  * `ask_user_question`(它同样把人的答复留在日志里,署名一样是人)。
2330
1898
  * · `invoke_skill` —— 原生 `/` 技能触发器(`dsh-client-ui-skill` 注册 trigger `/`,
2331
1899
  * `dsh-client-ui-input-trigger` 出候选菜单)做的正是同一件事,而且带候选菜单。
2332
- * 留下的三个各有原生没有的职责:
2333
- * · `adopt_branch`/`abandon_fork` —— `ConvergeFork` 只认「算术」或「人门」两条路,
2334
- * 算不出来时这是唯一的结构化人裁决通道(不做 NLU 是纪律,不是懒);
2335
- * · `promote_skill` —— 候选技能扶正是**只有人能触发**的写动作(模型不能自举)。
1900
+ * · `adopt_branch` / `abandon_fork` / `confirm_provisional` —— 世界线已删除,并行探索交给原生子任务;
1901
+ * · `promote_skill` —— 外脑已删除,技能走原生技能目录。
1902
+ * 旧日志里的这些动作不再折:`parseHumanGate` 按动词表拒收,原样跳过。
2336
1903
  *
2337
- * 已摘除:`set_autonomy`。「在场与否」是**运行时状态**(有没有门开着、有没有裁决在飞),
2338
- * 不是人在面板上按的一个开关;那个档位还顺手把「计划经人确认」变成系统自己签的。
2339
- * 折法里仍留一条**只读**容忍分支(见 `applyEvent` 的注释):旧会话日志里可能有一条这样的记录,
2340
- * 而历史必须继续读得通——但**当前没有任何写入者**,面板上也没有这个开关。
1904
+ * 已摘除:`set_autonomy`(运行档整个删了,旧日志里的这条记录原样跳过);
1905
+ * `retract_fact` / `keep_fact`(这道门改由内核当场问人,见下面的 `LEGACY_GATE_ACTIONS`)。
2341
1906
  */
2342
1907
  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']
1908
+ /**
1909
+ * **只在读旧日志时认**的动词。面板早已没有写入口(第六阶段把 `/api/clearai/gate` 整条拿掉):
1910
+ * 撤回 / 维持事实从第三阶段起由内核在那次交付里当场问人;本体四动词随编辑抽屉一起删,
1911
+ * 要改词汇就在对话里说,模型去改 `clear/ontology/` 下的文件。
1912
+ * 但旧会话里人按过的决定必须照旧折出来,否则重放时一条审过的事实会变回「待复核」、
1913
+ * 人登记的词会凭空消失。
1914
+ */
1915
+ const LEGACY_GATE_ACTIONS = ['retract_fact', 'keep_fact', 'register_term', 'register_predicate', 'revise_term', 'deprecate_entry']
2364
1916
 
2365
1917
  /** 从一条用户消息里认出人门标记;不是标记就返回 null。 */
2366
1918
  export function parseHumanGate(message) {
@@ -2376,90 +1928,60 @@ export function parseHumanGate(message) {
2376
1928
  try {
2377
1929
  const parsed = JSON.parse(line)
2378
1930
  if (parsed === null || typeof parsed !== 'object') return null
2379
- if (!HUMAN_GATE_ACTIONS.includes(String(parsed.action))) return null
1931
+ if (!LEGACY_GATE_ACTIONS.includes(String(parsed.action))) return null
2380
1932
  return parsed
2381
1933
  } catch {
2382
1934
  return null
2383
1935
  }
2384
1936
  }
2385
1937
 
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)))
1938
+ /** 一条判断升格成的事实(按 id 关联;旧账本按文本找回)。 */
1939
+ function factOf(state, hypothesis) {
1940
+ return (state.facts ?? []).find((item) => item.hypothesis === hypothesis.id) ?? (state.facts ?? []).find((item) => item.hypothesis === undefined && String(item.text ?? '') === String(hypothesis.claim ?? '')) ?? null
1941
+ }
1942
+
1943
+ function factRow(state, hypothesis) {
1944
+ const fact = factOf(state, hypothesis)
1945
+ 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' }
1946
+ }
1947
+
1948
+ /** 面板读的历程:步骤换成「第几步」,末尾接上升格与撤回(它们不是证据,但同样改了可信度)。 */
1949
+ function historyRows(state, hypothesis) {
1950
+ const ordinalOf = (planId, stepId) => {
1951
+ const plan = state.plans.find((item) => item.id === planId)
1952
+ return plan?.steps?.find((step) => step.id === stepId)?.ordinal ?? null
1953
+ }
1954
+ const rows = (hypothesis.history ?? []).map((row) => (row.kind === 'evidence' ? { ...row, ordinal: ordinalOf(row.plan, row.step) } : { ...row }))
1955
+ const fact = factOf(state, hypothesis)
1956
+ const last = () => rows[rows.length - 1]?.to ?? 'testing'
1957
+ if (fact !== null) rows.push({ kind: 'promoted', at: fact.at ?? null, level: fact.level ?? null, from: last(), to: 'credible' })
1958
+ if (fact?.review?.decision === 'retracted') rows.push({ kind: 'retracted', at: fact.review.at ?? null, reason: fact.review.reason ?? null, from: last(), to: 'replaced' })
1959
+ return rows
2423
1960
  }
2424
1961
 
2425
1962
  /** 面板契约。浏览器读的就是这个,一字不改。 */
2426
1963
  export function view(state, sessionId) {
2427
1964
  const derived = derive(state)
1965
+ const promotedIds = new Set((state.facts ?? []).map((fact) => fact?.hypothesis).filter((id) => typeof id === 'string'))
2428
1966
  const plan = derived.activePlan ?? derived.closedPlans[derived.closedPlans.length - 1] ?? null
2429
1967
  const sid = sessionId === undefined || sessionId === null ? (state.sessionId ?? null) : String(sessionId)
2430
1968
  return {
2431
1969
  ok: true,
2432
1970
  mounted: true,
2433
1971
  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,
1972
+ /** 待处理:只陈述的几行(计划停下、结论矛盾);面板顶上与输入框旁读同一份。 */
1973
+ needYou: derived.needYou,
2445
1974
  /**
2446
1975
  * 被闸门裁断过几次(`block/counted`):世界树的**分段通道**画的就是它——
2447
1976
  * 这一步磨了几轮、其中几次被驳回。事实在投影里,面板只负责画。
2448
1977
  */
2449
1978
  blocks: state.blocks ?? {},
2450
1979
  /**
2451
- * **人放行**(`human/released`)的痕迹:每一条都带它绑在哪条轴上
2452
- * (`step` / `branch`)以及凭据(`via:'approval'`,原生审批栈的权威记录)。
1980
+ * **人放行**(`human/released`)的痕迹:每一条都带它绑在哪一步上
1981
+ * 以及凭据(`via:'ask'`:内核那次交付当场问出来的;旧账里是 `approval`)。
2453
1982
  * 面板与测试都读这里 —— 推断出来的放行不该有痕迹,所以这份读数本身就是判据。
2454
1983
  */
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,
1984
+ 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
1985
  /** 本体形状(面板页眉据此生成,不手抄)。 */
2464
1986
  ontology: state.ontology ?? null,
2465
1987
  /**
@@ -2474,6 +1996,9 @@ export function view(state, sessionId) {
2474
1996
  conflicts: derived.conflicts,
2475
1997
  health: derived.lexiconIssues,
2476
1998
  graph: graphProjection(state),
1999
+ /** 本体文件里的实体(带所在目录的容器)与跨文件问题:本体页的问题列表读它。 */
2000
+ entities: Array.isArray(state.entities) ? state.entities : [],
2001
+ problems: Array.isArray(state.ontologyProblems) ? state.ontologyProblems : [],
2477
2002
  },
2478
2003
  /**
2479
2004
  * **知识模式(分诊)**:面板据此把「这一格是知识主场还是普通进展」说清楚,
@@ -2496,23 +2021,6 @@ export function view(state, sessionId) {
2496
2021
  * 面板可以画它,内核的 pre-step 把它送进卡——判据与缺口读数一样,只有这一处。
2497
2022
  */
2498
2023
  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
2024
  goal:
2517
2025
  state.goal === null
2518
2026
  ? null
@@ -2530,15 +2038,20 @@ export function view(state, sessionId) {
2530
2038
  closeVerdict: state.goal.closeVerdict ?? null,
2531
2039
  hypotheses: derived.hypotheses.map((hypothesis) => ({
2532
2040
  id: hypothesis.id,
2041
+ name: handleOf(hypothesis),
2042
+ /** 可信度分组(已验证 / 待核验 / 验证中 / 不确定 / 已推翻 / 已替换),面板与卡同一份。 */
2043
+ trust: trustOf(hypothesis, promotedIds.has(hypothesis.id)),
2533
2044
  claim: hypothesis.claim,
2534
2045
  refuteWhen: hypothesis.refute_when,
2535
2046
  status: hypothesis.status,
2536
2047
  supportedLevel: hypothesis.supportedLevel,
2537
2048
  refutations: hypothesis.refutations,
2538
2049
  inconclusive: hypothesis.inconclusive,
2539
- /** 从没被走过的等级(派生):面板据此说清「这一级是跳上来的」。 */
2540
- untouchedLevels: hypothesis.untouchedLevels,
2541
2050
  version: hypothesis.version,
2051
+ /** 「可信度怎么变的」:逐笔的之前 → 之后,末尾接上写进长期知识 / 被撤回。 */
2052
+ history: historyRows(state, hypothesis),
2053
+ /** 写进长期知识的那条(没升格就是 null):补充段的「范围」读它。 */
2054
+ fact: factRow(state, hypothesis),
2542
2055
  /** 断言随假设走(未升格):面板同画芯片(chip 同样在投影侧算好),标注「未升格」。 */
2543
2056
  assertions: Array.isArray(hypothesis.assertions)
2544
2057
  ? hypothesis.assertions.map((assertion) => ({ ...assertion, chip: formatAssertion(state.lexicon, assertion) }))
@@ -2564,7 +2077,9 @@ export function view(state, sessionId) {
2564
2077
  doneCriteria: step.done_criteria,
2565
2078
  tests: step.tests,
2566
2079
  status: step.status,
2567
- evidenceId: step.evidence,
2080
+ evidenceIds: step.evidence ?? [],
2081
+ /** 交付本身(谁判的、凭什么、出处);旧账为 null。 */
2082
+ delivery: step.delivery ?? null,
2568
2083
  advancedAt: step.advancedAt,
2569
2084
  voidReason: step.voidReason,
2570
2085
  })),
@@ -2578,10 +2093,6 @@ export function view(state, sessionId) {
2578
2093
  /** 计划自己的一句话(模型建计划时写的)。**给人看的名字**用它,内部 id 退回 tooltip。 */
2579
2094
  brief: plan.brief ?? '',
2580
2095
  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
2096
  steps: plan.steps.map((step) => ({
2586
2097
  id: step.id,
2587
2098
  ordinal: step.ordinal,
@@ -2595,7 +2106,9 @@ export function view(state, sessionId) {
2595
2106
  doneCriteria: step.done_criteria,
2596
2107
  tests: step.tests,
2597
2108
  status: step.status,
2598
- evidenceId: step.evidence,
2109
+ evidenceIds: step.evidence ?? [],
2110
+ /** 交付本身(谁判的、凭什么、出处);旧账为 null。 */
2111
+ delivery: step.delivery ?? null,
2599
2112
  advancedAt: step.advancedAt,
2600
2113
  voidReason: step.voidReason,
2601
2114
  })),
@@ -2623,12 +2136,11 @@ export function view(state, sessionId) {
2623
2136
  stepId: item.step,
2624
2137
  planId: item.plan,
2625
2138
  /**
2626
- * `branch` 与 `anchor` **必须交出去**:面板要给每条证据指一个**出处**——
2627
- * 独立证据指评估卡(文件)、自判证据指它锚定的产物。少了这两个字段,
2628
- * 「评估卡」那一项永远出不来(成因:世界线证据的评估卡按 `分支` 归属,
2629
- * 与证据的 `stepId` 不是同一个 id,只看 stepId 会对不上)。
2139
+ * `anchor` **必须交出去**:面板要给每条证据指一个**出处**——
2140
+ * 独立证据指评估卡(文件)、自判证据指它锚定的产物。
2630
2141
  */
2631
- branch: item.branch ?? null,
2142
+ /** 针对哪条判断(旧账为 null:由步骤检验的判断找回来)。 */
2143
+ hypothesis: item.hypothesis ?? null,
2632
2144
  anchor: item.anchor ?? 'artifact',
2633
2145
  /** 记账时定下的出处。旧日志没有这个字段 ⇒ 客户端走只读回退。 */
2634
2146
  origins: item.origins ?? [],
@@ -2666,6 +2178,8 @@ export function view(state, sessionId) {
2666
2178
  facts: derived.factRows.map((item) => ({
2667
2179
  id: item.id,
2668
2180
  text: item.text,
2181
+ /** 产出它的那条判断(旧账本没有,面板把这种事实单独列成一行)。 */
2182
+ hypothesis: item.hypothesis ?? null,
2669
2183
  /** 边界(推翻条件)与支持等级:面板与货架都要显示它——「已知」必须带边界。 */
2670
2184
  scope: item.scope ?? null,
2671
2185
  level: item.level ?? null,
@@ -2681,91 +2195,6 @@ export function view(state, sessionId) {
2681
2195
  refuted: item.refuted === true,
2682
2196
  review: item.review ?? null,
2683
2197
  })),
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
2198
  }
2770
2199
  }
2771
2200