clearai-dsh 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/README.md +28 -28
  3. package/README.zh-CN.md +28 -28
  4. package/lib/client.js +861 -2555
  5. package/lib/domain-language.js +552 -48
  6. package/lib/fold.js +351 -1015
  7. package/lib/host.js +47 -557
  8. package/lib/invariant.js +9 -12
  9. package/lib/knowledge-view.js +363 -226
  10. package/lib/lang.js +81 -0
  11. package/package.json +1 -1
  12. package/presets/clearai/agent.cordis.yml +54 -82
  13. package/presets/clearai/clearai.patch.yml +54 -82
  14. package/presets/clearai/plugins/clearai-kernel.js +1590 -5437
  15. package/presets/clearai/plugins/ontology.js +56 -14
  16. package/presets/clearai/plugins/prompts.js +73 -363
  17. package/presets/clearai/skills/clearai-loop/SKILL.md +73 -59
  18. package/presets/clearai/plugins/brain.js +0 -547
  19. package/presets/clearai/plugins/commands.js +0 -199
  20. package/presets/clearai/template/knowledge/README.md +0 -25
  21. package/presets/clearai/template/memory/README.md +0 -34
  22. package/presets/clearai/template/project.md +0 -49
  23. package/presets/clearai/template/skills/README.md +0 -37
  24. package/presets/clearai/template/skills/chart-diagram-qa/SKILL.md +0 -43
  25. package/presets/clearai/template/skills/citation-management/SKILL.md +0 -73
  26. package/presets/clearai/template/skills/citation-management/references/bibtex_formatting.md +0 -908
  27. package/presets/clearai/template/skills/citation-management/references/citation_validation.md +0 -794
  28. package/presets/clearai/template/skills/citation-management/references/google_scholar_search.md +0 -725
  29. package/presets/clearai/template/skills/citation-management/references/metadata_extraction.md +0 -870
  30. package/presets/clearai/template/skills/citation-management/references/pubmed_search.md +0 -839
  31. package/presets/clearai/template/skills/citation-management/scripts/doi_to_bibtex.py +0 -204
  32. package/presets/clearai/template/skills/citation-management/scripts/extract_metadata.py +0 -569
  33. package/presets/clearai/template/skills/citation-management/scripts/format_bibtex.py +0 -349
  34. package/presets/clearai/template/skills/citation-management/scripts/generate_schematic.py +0 -139
  35. package/presets/clearai/template/skills/citation-management/scripts/generate_schematic_ai.py +0 -817
  36. package/presets/clearai/template/skills/citation-management/scripts/search_google_scholar.py +0 -282
  37. package/presets/clearai/template/skills/citation-management/scripts/search_pubmed.py +0 -398
  38. package/presets/clearai/template/skills/citation-management/scripts/validate_citations.py +0 -497
  39. package/presets/clearai/template/skills/data-analysis/SKILL.md +0 -92
  40. package/presets/clearai/template/skills/data-analysis/checklists/readiness_check.md +0 -23
  41. package/presets/clearai/template/skills/data-analysis/templates/analysis_report.md.tpl +0 -63
  42. package/presets/clearai/template/skills/data-analysis/templates/cleaning_rules_draft.yaml.tpl +0 -32
  43. package/presets/clearai/template/skills/data-analysis/templates/data_dictionary.md.tpl +0 -12
  44. package/presets/clearai/template/skills/data-analysis/templates/domain_knowledge_template.md.tpl +0 -316
  45. package/presets/clearai/template/skills/data-analysis/templates/feature_candidates.json.tpl +0 -20
  46. package/presets/clearai/template/skills/data-analysis/templates/quality_scorecard.md.tpl +0 -30
  47. package/presets/clearai/template/skills/data-analysis/workflows/01-data-profiling.md +0 -42
  48. package/presets/clearai/template/skills/data-analysis/workflows/02-quality-audit.md +0 -36
  49. package/presets/clearai/template/skills/data-analysis/workflows/03-physical-correlation.md +0 -25
  50. package/presets/clearai/template/skills/data-analysis/workflows/04-unstructured-mining.md +0 -26
  51. package/presets/clearai/template/skills/data-qa-analysis/SKILL.md +0 -102
  52. package/presets/clearai/template/skills/data-qa-analysis/checklists/readiness_check.md +0 -62
  53. package/presets/clearai/template/skills/data-qa-analysis/templates/best_in_class_report.md.tpl +0 -56
  54. package/presets/clearai/template/skills/data-qa-analysis/templates/cleaning_rules_draft.yaml.tpl +0 -56
  55. package/presets/clearai/template/skills/data-qa-analysis/templates/data_dictionary.md.tpl +0 -13
  56. package/presets/clearai/template/skills/data-qa-analysis/templates/data_source_inventory_and_lineage.md.tpl +0 -146
  57. package/presets/clearai/template/skills/data-qa-analysis/templates/data_status_report.md.tpl +0 -60
  58. package/presets/clearai/template/skills/data-qa-analysis/templates/steady_state_rules.yaml.tpl +0 -41
  59. package/presets/clearai/template/skills/data-qa-analysis/templates/subsystem_registry.md.tpl +0 -101
  60. package/presets/clearai/template/skills/data-qa-analysis/templates/unified_execution_plan.md.tpl +0 -100
  61. package/presets/clearai/template/skills/data-qa-analysis/workflows/01-data-source-inventory-and-lineage.md +0 -194
  62. package/presets/clearai/template/skills/data-qa-analysis/workflows/02-data-alignment-and-tag-semantics.md +0 -122
  63. package/presets/clearai/template/skills/data-qa-analysis/workflows/03-steady-state-identification.md +0 -126
  64. package/presets/clearai/template/skills/data-qa-analysis/workflows/04-consumption-analysis.md +0 -152
  65. package/presets/clearai/template/skills/data-qa-analysis/workflows/05-best-in-class-and-optimization-space.md +0 -78
  66. package/presets/clearai/template/skills/domain-presearch/SKILL.md +0 -131
  67. package/presets/clearai/template/skills/domain-presearch/checklists/domain_checklist.md +0 -24
  68. package/presets/clearai/template/skills/domain-presearch/references/figure_code.md +0 -78
  69. package/presets/clearai/template/skills/domain-presearch/references/strategic_frameworks.md +0 -38
  70. package/presets/clearai/template/skills/exploration-loop/SKILL.md +0 -81
  71. package/presets/clearai/template/skills/exploratory-data-analysis/SKILL.md +0 -77
  72. package/presets/clearai/template/skills/exploratory-data-analysis/references/bioinformatics_genomics_formats.md +0 -664
  73. package/presets/clearai/template/skills/exploratory-data-analysis/references/chemistry_molecular_formats.md +0 -664
  74. package/presets/clearai/template/skills/exploratory-data-analysis/references/general_scientific_formats.md +0 -518
  75. package/presets/clearai/template/skills/exploratory-data-analysis/references/microscopy_imaging_formats.md +0 -620
  76. package/presets/clearai/template/skills/exploratory-data-analysis/references/proteomics_metabolomics_formats.md +0 -517
  77. package/presets/clearai/template/skills/exploratory-data-analysis/references/spectroscopy_analytical_formats.md +0 -633
  78. package/presets/clearai/template/skills/exploratory-data-analysis/scripts/eda_analyzer.py +0 -547
  79. package/presets/clearai/template/skills/hypothesis-generation/SKILL.md +0 -73
  80. package/presets/clearai/template/skills/hypothesis-generation/references/experimental_design_patterns.md +0 -329
  81. package/presets/clearai/template/skills/hypothesis-generation/references/hypothesis_quality_criteria.md +0 -198
  82. package/presets/clearai/template/skills/hypothesis-generation/references/literature_search_strategies.md +0 -622
  83. package/presets/clearai/template/skills/hypothesis-generation/scripts/generate_schematic.py +0 -139
  84. package/presets/clearai/template/skills/hypothesis-generation/scripts/generate_schematic_ai.py +0 -817
  85. package/presets/clearai/template/skills/literature-review/SKILL.md +0 -72
  86. package/presets/clearai/template/skills/literature-review/references/citation_styles.md +0 -166
  87. package/presets/clearai/template/skills/literature-review/references/database_strategies.md +0 -455
  88. package/presets/clearai/template/skills/literature-review/scripts/generate_pdf.py +0 -176
  89. package/presets/clearai/template/skills/literature-review/scripts/generate_schematic.py +0 -139
  90. package/presets/clearai/template/skills/literature-review/scripts/generate_schematic_ai.py +0 -817
  91. package/presets/clearai/template/skills/literature-review/scripts/search_databases.py +0 -303
  92. package/presets/clearai/template/skills/literature-review/scripts/verify_citations.py +0 -221
  93. package/presets/clearai/template/skills/paper-lookup/SKILL.md +0 -59
  94. package/presets/clearai/template/skills/paper-lookup/references/arxiv.md +0 -161
  95. package/presets/clearai/template/skills/paper-lookup/references/biorxiv.md +0 -118
  96. package/presets/clearai/template/skills/paper-lookup/references/core.md +0 -150
  97. package/presets/clearai/template/skills/paper-lookup/references/crossref.md +0 -181
  98. package/presets/clearai/template/skills/paper-lookup/references/medrxiv.md +0 -104
  99. package/presets/clearai/template/skills/paper-lookup/references/openalex.md +0 -174
  100. package/presets/clearai/template/skills/paper-lookup/references/pmc.md +0 -152
  101. package/presets/clearai/template/skills/paper-lookup/references/pubmed.md +0 -124
  102. package/presets/clearai/template/skills/paper-lookup/references/semantic-scholar.md +0 -203
  103. package/presets/clearai/template/skills/paper-lookup/references/unpaywall.md +0 -127
  104. package/presets/clearai/template/skills/process-presearch/SKILL.md +0 -196
  105. package/presets/clearai/template/skills/process-presearch/checklists/process_checklist.md +0 -18
  106. package/presets/clearai/template/skills/process-presearch/references/figure_code.md +0 -107
  107. package/presets/clearai/template/skills/process-presearch/references/source_attribution_example.md +0 -22
  108. package/presets/clearai/template/skills/process-understanding-extraction/SKILL.md +0 -69
  109. package/presets/clearai/template/skills/process-understanding-extraction/checklists/readiness_check.md +0 -34
  110. package/presets/clearai/template/skills/process-understanding-extraction/templates/docx_raw_dump_extractor.py.tpl +0 -132
  111. package/presets/clearai/template/skills/process-understanding-extraction/templates/entity_map_unit_topology.json.tpl +0 -86
  112. package/presets/clearai/template/skills/process-understanding-extraction/templates/process_brief.md.tpl +0 -89
  113. package/presets/clearai/template/skills/process-understanding-extraction/templates/process_brief_builder_from_raw_dump.py.tpl +0 -203
  114. package/presets/clearai/template/skills/process-understanding-extraction/templates/process_flow_mermaid.md.tpl +0 -41
  115. package/presets/clearai/template/skills/process-understanding-extraction/templates/unified_execution_plan.md.tpl +0 -53
  116. package/presets/clearai/template/skills/process-understanding-extraction/workflows/01-process-doc-discovery.md +0 -173
  117. package/presets/clearai/template/skills/process-understanding-extraction/workflows/02-process-understanding-and-diagramming.md +0 -106
  118. package/presets/clearai/template/skills/scientific-brainstorming/SKILL.md +0 -64
  119. package/presets/clearai/template/skills/scientific-brainstorming/references/brainstorming_methods.md +0 -326
  120. package/presets/clearai/template/skills/scientific-critical-thinking/SKILL.md +0 -72
  121. package/presets/clearai/template/skills/scientific-critical-thinking/references/common_biases.md +0 -364
  122. package/presets/clearai/template/skills/scientific-critical-thinking/references/evidence_hierarchy.md +0 -485
  123. package/presets/clearai/template/skills/scientific-critical-thinking/references/experimental_design.md +0 -496
  124. package/presets/clearai/template/skills/scientific-critical-thinking/references/logical_fallacies.md +0 -478
  125. package/presets/clearai/template/skills/scientific-critical-thinking/references/scientific_method.md +0 -169
  126. package/presets/clearai/template/skills/scientific-critical-thinking/references/statistical_pitfalls.md +0 -506
  127. package/presets/clearai/template/skills/skill-creator/SKILL.md +0 -109
  128. package/presets/clearai/template/skills/skill-creator/references/authoring-guide.md +0 -89
  129. package/presets/clearai/template/skills/statistical-analysis/SKILL.md +0 -79
  130. package/presets/clearai/template/skills/statistical-analysis/references/assumptions_and_diagnostics.md +0 -369
  131. package/presets/clearai/template/skills/statistical-analysis/references/bayesian_statistics.md +0 -653
  132. package/presets/clearai/template/skills/statistical-analysis/references/effect_sizes_and_power.md +0 -578
  133. package/presets/clearai/template/skills/statistical-analysis/references/reporting_standards.md +0 -469
  134. package/presets/clearai/template/skills/statistical-analysis/references/test_selection_guide.md +0 -129
  135. package/presets/clearai/template/skills/statistical-analysis/scripts/assumption_checks.py +0 -538
  136. package/presets/clearai/template/skills/web-artifact/SKILL.md +0 -165
  137. package/presets/clearai/template/skills/web-artifact/assets/renderer/renderer.css +0 -229
  138. package/presets/clearai/template/skills/web-artifact/assets/renderer/renderer.js +0 -373
  139. package/presets/clearai/template/skills/web-artifact/assets/vendor/elkjs/LICENSE +0 -263
  140. package/presets/clearai/template/skills/web-artifact/assets/vendor/elkjs/UPSTREAM.md +0 -26
  141. package/presets/clearai/template/skills/web-artifact/assets/vendor/elkjs/elk.bundled.js +0 -6605
  142. package/presets/clearai/template/skills/web-artifact/references/when-drawing-a-topology.md +0 -150
  143. package/presets/clearai/template/skills/web-artifact/references/when-the-page-must-work-offline.md +0 -62
  144. package/presets/clearai/template/skills/web-artifact/scripts/check_artifact.py +0 -167
  145. package/presets/clearai/template/skills/web-artifact/scripts/render_topology.js +0 -272
  146. package/presets/clearai/template/skills/what-if-oracle/LICENSE.txt +0 -5
  147. package/presets/clearai/template/skills/what-if-oracle/SKILL.md +0 -72
  148. package/presets/clearai/template/skills/what-if-oracle/references/scenario-templates.md +0 -154
package/lib/client.js CHANGED
@@ -81,12 +81,12 @@ var Wt=Object.create;var Le=Object.defineProperty;var Pt=Object.getOwnPropertyDe
81
81
  /**
82
82
  * clearai-dsh —— 浏览器半(ClearAI 的面板)。
83
83
  *
84
- * 它注册四个座位,外加输入框那两个:
85
- * 中栏 `conversation.view` 产物(声明 vs 盘上实际)、事实(已确认事实 + 在流转的命题)
86
- * 右栏 `sidebarRightTabs` 世界树(步骤/分叉/车道的拓扑与闸门)、技能 · 记忆(合并目录 + 本会话用量)
87
- * 输入框 `conversation.input` 计划芯片(步数 + 「需要你 N」)、续跑状态行
88
- * 除此之外还有一个**只给人**的写入口:世界树详情里的人门动作(采纳 / 放弃),它走宿主半的
89
- * `POST /api/clearai/gate`,落成一条署名为人的消息——模型能调的工具面里没有这些动词。
84
+ * 它注册三个座位:
85
+ * 中栏 `conversation.view` 本体(页眉 + 本体图 / 实体图 + 按可信度分组的结论)
86
+ * 右栏 `sidebarRightTabs` 世界树(计划的步骤,一行一步)
87
+ * 输入框 `conversation.input` 计划芯片(步数 + 「待处理 N」)
88
+ * 交付与文件改动走 DSH 原生的卡片,技能走原生技能目录,这里都不另做一份。
89
+ * 面板**只读**:没有写入口。要人拍板的事由开门的那次工具调用当场问人,要改词汇就在对话里说。
90
90
  *
91
91
  * 面板自己不取数、不重算:宿主半注册的**会话投影单元** `clearai`(见 `lib/index.js`)把算好的
92
92
  * 视图推下来,`useProjection('clearai')` 读它。投影为空(`undefined`)时渲染平静的空态,
@@ -185,8 +185,8 @@ window.__ModuleLoader__.load({
185
185
 
186
186
  /** 语言命名空间。表按**源文**索引:键就是中文原文,所以漏翻译一条只会退回中文,不会把 key 显示给人。 */
187
187
  const LOCALE_NS = 'clearai'
188
- const LOCALE_ZH = {"个节点": "个节点", "条边": "条边", "适配": "适配", "打开图谱工作区": "打开图谱工作区", "关闭工作区": "关闭工作区", "图组件不可用": "图组件不可用", "点节点看知识详情": "点节点看知识详情", "按此筛选": "按此筛选", "关闭": "关闭", "正在取这条知识的读数…": "正在取这条知识的读数…", "取不到读数": "取不到读数", "宿主没有这个对象的读数(可能刚被废止,或它不在当前词汇里)。": "宿主没有这个对象的读数(可能刚被废止,或它不在当前词汇里)。", "父概念": "父概念", "形态": "形态", "取值": "取值", "引用": "引用", "子概念": "子概念", "相关谓词": "相关谓词", "实例": "实例", "关系边": "关系边", "主词": "主词", "相关事实": "相关事实", "(还有更多,未列出)": "(还有更多,未列出)", "历史": "历史", "这条事实没有命题关联(旧账本):不拿文本相等去猜身份。": "这条事实没有命题关联(旧账本):不拿文本相等去猜身份。", "推翻条件": "推翻条件", "支持到": "支持到", "断言": "断言", "没有证据引用(旧事实或自判)。": "没有证据引用(旧事实或自判)。", "出处": "出处", "产生步骤": "产生步骤", "复核": "复核", "这条事实参与了一对冲突读数(只暴露,不裁决)": "这条事实参与了一对冲突读数(只暴露,不裁决)", "滚轮缩放 · 拖动平移 · 拖节点挪位": "滚轮缩放 · 拖动平移 · 拖节点挪位", "类型": "类型", "还有 ": "还有 ", " 条未列出 · 用 / 触发菜单选": " 条未列出 · 用 / 触发菜单选", "被推翻 · 待裁决": "被推翻 · 待裁决", "等待输入": "等待输入", "等待人工处理": "等待人工处理", "等待中": "等待中", "等待人工": "等待人工", "异常终止": "异常终止", "发送失败:": "发送失败:", "已获首条证据": "已获首条证据", "按判据推荐": "按判据推荐", "系统内部消息(非运行状态)": "系统内部消息(非运行状态)", "填写缘由后提交": "填写缘由后提交", "请填写缘由": "请填写缘由", "点击在右栏预览": "点击在右栏预览", "点击查看详情": "点击查看详情", "查看步骤详情": "查看步骤详情", "查看词条": "查看词条", "放弃需填写缘由后提交": "放弃需填写缘由后提交", "撤回需填写缘由后提交": "撤回需填写缘由后提交", "暂无数据。发送第一条消息后,此处显示已确立条目与在验命题。": "暂无数据。发送第一条消息后,此处显示已确立条目与在验命题。", "暂无数据。发送第一条消息后,此处显示技能目录与记忆。": "暂无数据。发送第一条消息后,此处显示技能目录与记忆。", "暂无命题。每条需一句主张与一句推翻条件;通过验证后升格为事实。": "暂无命题。每条需一句主张与一句推翻条件;通过验证后升格为事实。", "暂无目标。目标携带验收判据;判据在结果出现之前登记。": "暂无目标。目标携带验收判据;判据在结果出现之前登记。", "暂无目标。验证达门槛且无推翻的命题在验收时升格为事实。": "暂无目标。验证达门槛且无推翻的命题在验收时升格为事实。", "暂无计划。建立计划后,已交付步骤按阶段列于此处。": "暂无计划。建立计划后,已交付步骤按阶段列于此处。", "暂无计划。建立后此处显示计划拓扑:脊柱步、车道与收敛点。": "暂无计划。建立后此处显示计划拓扑:脊柱步、车道与收敛点。", "暂无记忆。": "暂无记忆。", "暂无证据。先登记判据,后执行验证。": "暂无证据。先登记判据,后执行验证。", "此层暂无节点。": "此层暂无节点。", "暂无技能或记忆。使用 `SaveSkill` / `WriteMemory`,或将技能放入 `clear/skills/`。": "暂无技能或记忆。使用 `SaveSkill` / `WriteMemory`,或将技能放入 `clear/skills/`。", "达门槛且无推翻的命题在目标验收时升格为事实(写入 clear/knowledge/facts/)。": "达门槛且无推翻的命题在目标验收时升格为事实(写入 clear/knowledge/facts/)。", "修改判据需重新建立世界线": "修改判据需重新建立世界线", "计划的拓扑与闸门:脊柱步、车道、收敛点、人工决策点": "计划的拓扑与闸门:脊柱步、车道、收敛点、人工决策点", "由内核执行 ConvergeFork 完成合并": "由内核执行 ConvergeFork 完成合并", "通过提问卡做出裁决(含判据与各分支读数)": "通过提问卡做出裁决(含判据与各分支读数)", "通过提问卡决定": "通过提问卡决定", "在世界树中查看此步骤": "在世界树中查看此步骤", "右栏预览": "右栏预览", "查看此步骤的证据": "查看此步骤的证据", "查看评估者": "查看评估者", "工作区现状(首轮对话后进入投影)": "工作区现状(首轮对话后进入投影)", "本体声明尚未进入投影;当前显示规范闭环的镜像,下一拍自动对齐。": "本体声明尚未进入投影;当前显示规范闭环的镜像,下一拍自动对齐。", "目录尚未到达(内核下一次 pre-step 下发)": "目录尚未到达(内核下一次 pre-step 下发)", "这些等级尚无证据:跳级不违规,但需说明原因": "这些等级尚无证据:跳级不违规,但需说明原因", "此谓词已不在词汇中(可能已废止);存量断言仍可读。": "此谓词已不在词汇中(可能已废止);存量断言仍可读。", "策略暂停(原因未记录,已告警)": "策略暂停(原因未记录,已告警)", "未升格:断言仍在命题上": "未升格:断言仍在命题上", "放弃缘由(必填)": "放弃缘由(必填)", "撤回缘由(必填)": "撤回缘由(必填)", "登记概念": "登记概念", "登记谓词": "登记谓词", "名称": "名称", "单位": "单位", "提交": "提交", "取消": "取消", "废止": "废止", "为什么?": "为什么?", "提交中…": "提交中…", "点节点按概念过滤 · 滚轮缩放 · 拖拽平移": "点节点按概念过滤 · 滚轮缩放 · 拖拽平移", "复位": "复位", "全景": "全景", "还原": "还原", "展开图带": "展开图带", "图里只画了前": "图里只画了前", " 个节点;全景可看全部": " 个节点;全景可看全部", "一条边": "一条边", "词汇": "词汇", "概念": "概念", "谓词": "谓词", "已废止": "已废止", "健康": "健康", "单值": "单值", "打开词汇货架(原生预览)": "打开词汇货架(原生预览)", "冲突": "冲突", " 对": " 对", "只暴露,不裁决;撤回或维持由人决定": "只暴露,不裁决;撤回或维持由人决定", "按": "按", "过滤": "", "清除": "清除", "按此谓词过滤": "按此谓词过滤", "在图里看": "在图里看", " · 未升格": " · 未升格", "释义": "释义", "主词域": "主词域", "值域": "值域", "本体": "本体", "本体货架 · ": "本体货架 · ", "这里会长出你的本体:已确立的条目与在验的命题。": "这里会长出你的本体:已确立的条目与在验的命题。", "本体图": "本体图", "实体图": "实体图", " · 交付 ": " · 交付 ", " · 人 ": " · 人 ", " · 在工作区外,面板不读正文": " · 在工作区外,面板不读正文", " · 尺子 ": " · 尺子 ", " · 执行没跑成": " · 执行没跑成", " · 支持到 ": " · 支持到 ", " · 最近 ": " · 最近 ", " · 证据 ": " · 证据 ", " · 车道 ": " · 车道 ", " · 随步骤作废而终止": " · 随步骤作废而终止", " 份)。": " 份)。", " 份产物,但没有任何计划声明过它们。点一条可以直接看(它们不计入交付)。": " 份产物,但没有任何计划声明过它们。点一条可以直接看(它们不计入交付)。", " 份没列出来(这一栏只列最近改动的 ": " 份没列出来(这一栏只列最近改动的 ", " 发起": " 发起", " 处声明": " 处声明", " 字节": " 字节", " 字节 · ": " 字节 · ", " 字节 · 未被任何计划声明": " 字节 · 未被任何计划声明", " 字节 · 资源 ": " 字节 · 资源 ", " 推翻:": " 推翻:", " 旁观 ": " 旁观 ", " 条": " 条", " 条)": " 条)", " 次侦察": " 次侦察", " 次评估者": " 次评估者", " 步": " 步", " 步)": " 步)", " 等 ": " 等 ", " 评估卡": " 评估卡", " 轮": " 轮", " 里打开": " 里打开", "(假设达到这一级且无推翻才升格为事实)": "(假设达到这一级且无推翻才升格为事实)", "(每次修订留痕,旧值不删)": "(每次修订留痕,旧值不删)", "(点一下开右栏「世界树」)": "(点一下开右栏「世界树」)", "(点一下开右栏「世界树」看拓扑)": "(点一下开右栏「世界树」看拓扑)", "(留档不删)": "(留档不删)", "(缺)": "(缺)", "(要独立评估)才达门槛": "(要独立评估)才达门槛", "),所以这个分叉没有归宿了:**随步骤作废而终止**——它既不是被人裁掉(没人做过这个决定),也不是被算术排掉(没有尺子排过它)。要收掉工作副本(保留 ref),对它调 AbandonFork(会弹人工确认)。": "),所以这个分叉没有归宿了:**随步骤作废而终止**——它既不是被人裁掉(没人做过这个决定),也不是被算术排掉(没有尺子排过它)。要收掉工作副本(保留 ref),对它调 AbandonFork(会弹人工确认)。", "),等目标验收时升格为事实": "),等目标验收时升格为事实", ")——它不是落选:落选意味着它跑完了、被尺子排到了后面;这一条是没跑成,所以也没有读数可以参与算术。": ")——它不是落选:落选意味着它跑完了、被尺子排到了后面;这一条是没跑成,所以也没有读数可以参与算术。", "**执行没跑成**(": "**执行没跑成**(", "**执行者未归**:这条世界线的执行者派出去之后没有回灌,而分叉已经收口——它再回来也没有归宿了,所以这里既不判它跑成也没跑成,也不再等它。它的产物(如果写过)还在它自己的工作副本里;要判断那条线到底做出了什么,直接看工作副本比看这条状态可靠。": "**执行者未归**:这条世界线的执行者派出去之后没有回灌,而分叉已经收口——它再回来也没有归宿了,所以这里既不判它跑成也没跑成,也不再等它。它的产物(如果写过)还在它自己的工作副本里;要判断那条线到底做出了什么,直接看工作副本比看这条状态可靠。", ":还差 ": ":还差 ", ";放弃缘由:": ";放弃缘由:", "clearai 面板:右栏页签类型注册失败 ": "clearai 面板:右栏页签类型注册失败 ", "clearai-loop: 席位跟着会话预设进出": "clearai-loop: 席位跟着会话预设进出", "世界树": "世界树", "世界线:": "世界线:", "事实": "事实", "产物": "产物", "人审查后决定撤回": "人审查后决定撤回", "人已撤回(记录保留)": "人已撤回(记录保留)", "撤回事实": "撤回事实", "维持原事实": "维持原事实", "未走过 ": "未走过 ", "认可,继续": "认可,继续", "确认撤回": "确认撤回", "确认撤回(记录保留,不再作为「已知」引用)": "确认撤回(记录保留,不再作为「已知」引用)", "人的裁决:": "人的裁决:", "仅人可引用": "仅人可引用", "会话日志里的原生审批对(不可伪造)": "会话日志里的原生审批对(不可伪造)", "依据": "依据", "侦 ": "侦 ", "侦察 · ": "侦察 · ", "候选": "候选", "做什么": "做什么", "做法": "做法", "其他(": "其他(", "出现推翻证据": "出现推翻证据", "出现推翻证据(终态,记录保留)": "出现推翻证据(终态,记录保留)", "分支": "分支", "切回历史的世界树(计划都还在,文档也归档在 clear/goals/plans/)": "切回历史的世界树(计划都还在,文档也归档在 clear/goals/plans/)", "判据": "判据", "判据:": "判据:", "判据待写": "判据待写", "升格门槛": "升格门槛", "原生预览打不开这条路(它在工作区外?)": "原生预览打不开这条路(它在工作区外?)", "原生预览打开它": "原生预览打开它", "原生预览打开它(计划声明的产物)": "原生预览打开它(计划声明的产物)", "合并目录、本会话用法、可引用可采纳": "合并目录、本会话用法、可引用可采纳", "命题": "命题", "命题 · ": "命题 · ", "在": "在", "在 ": "在 ", "声明的是目录;准入要求具体文件——目录不算物证": "声明的是目录;准入要求具体文件——目录不算物证", "多问我": "多问我", "完成度 ": "完成度 ", "完成度 —": "完成度 —", "审批记录": "审批记录", "工作区模板 · clear/skills": "工作区模板 · clear/skills", "已交付": "已交付", "已作废": "已作废", "已回灌": "已回灌", "已推翻": "已推翻", "已提出": "已提出", "已撤回": "已撤回", "已收尾 · 存档可看": "已收尾 · 存档可看", "已改版": "已改版", "已放弃": "已放弃", "已放弃探索:": "已放弃探索:", "已放弃的分叉": "已放弃的分叉", "已替代": "已替代", "已确认": "已确认", "已确认事实 · ": "已确认事实 · ", "已被下一版命题替代(版本留着,不参与当前推理)": "已被下一版命题替代(版本留着,不参与当前推理)", "已裁决": "已裁决", "已评估": "已评估", "已达成": "已达成", "已达门槛(": "已达门槛(", "已达门槛,已升格为事实": "已达门槛,已升格为事实", "已采纳": "已采纳", "待开计划": "待开计划", "待推进": "待推进", "待裁决": "待裁决", "我的 · ~/.dsh": "我的 · ~/.dsh", "打开 ": "打开 ", "打开「事实」那一格并展开这条命题": "打开「事实」那一格并展开这条命题", "打开世界树并选中产出这条事实的验证步": "打开世界树并选中产出这条事实的验证步", "打开世界树并选中产生这条证据的验证步": "打开世界树并选中产生这条证据的验证步", "打开失败:HTTP ": "打开失败:HTTP ", "打开计划文档(原生预览)": "打开计划文档(原生预览)", "技能 · 记忆": "技能 · 记忆", "探索中": "探索中", "推翻": "推翻", "推进中": "推进中", "提交中…": "提交中…", "提交失败:": "提交失败:", "支持": "支持", "支持到 ": "支持到 ", "收敛": "收敛", "收束:": "收束:", "收起": "收起", "收起详情": "收起详情", "放弃了这次探索": "放弃了这次探索", "放弃探索": "放弃探索", "放弃缘由(必填)": "Reason for abandoning (required)", "放弃这条分叉": "放弃这条分叉", "放弃这次探索(留档不删,ref 保留)": "放弃这次探索(留档不删,ref 保留)", "放行": "放行", "旁观写这条裁决的评估者子会话(论证过程)": "旁观写这条裁决的评估者子会话(论证过程)", "旁观评估者": "旁观评估者", "旁观这条世界线的评估者会话(只读)": "旁观这条世界线的评估者会话(只读)", "无法判定": "无法判定", "是目录(不算物证)": "是目录(不算物证)", "最后改动 ": "最后改动 ", "未分类": "未分类", "未声明": "未声明", "未收口(随步骤作废而终止)": "未收口(随步骤作废而终止)", "未知": "未知", "未走:": "未走:", "未采纳": "未采纳", "本会话 模型 ": "本会话 模型 ", "本项目 · .dsh / .agents": "本项目 · .dsh / .agents", "本项目写的 · clear/skills": "本项目写的 · clear/skills", "核心产物(": "核心产物(", "步 ": "步 ", "派出 ": "派出 ", "派生 · ": "派生 · ", "版本": "版本", "状态": "状态", "独立评估者": "独立评估者", "用原生预览打开章程(它每回合整份注入模型上下文)": "用原生预览打开章程(它每回合整份注入模型上下文)", "盘上已有(": "盘上已有(", "盘上没有": "盘上没有", "盘上没有这个文件": "盘上没有这个文件", "目录": "目录", "目录里已经没有(": "目录里已经没有(", "目标": "目标", "目标挂起": "目标挂起", "确认放弃": "确认放弃", "确认放弃(留档不删,ref 保留)": "确认放弃(留档不删,ref 保留)", "等独立裁决": "等独立裁决", "策略暂停": "策略暂停", "续跑:多问我——每个阶段收尾就停下,等你给下一阶段(文档里叫「人在场」)。点一下切成「自己拿主意」。": "续跑:多问我——每个阶段收尾就停下,等你给下一阶段(文档里叫「人在场」)。点一下切成「自己拿主意」。", "续跑:自己拿主意——立约即授权,按轮数自己往下跑,只在不可约的判断上开门(文档里叫「无人值守」)。点一下切成「多问我」。": "续跑:自己拿主意——立约即授权,按轮数自己往下跑,只在不可约的判断上开门(文档里叫「无人值守」)。点一下切成「多问我」。", "续跑停着:": "续跑停着:", "续跑已撤回": "续跑已撤回", "缘由必填": "缘由必填", "缺": "缺", "自判": "自判", "自定义": "自定义", "自己拿主意": "自己拿主意", "被 ": "被 ", "被下一版命题改写": "被下一版命题改写", "裁决": "裁决", "裁决:采纳 ": "裁决:采纳 ", "要你": "要你", "观测": "观测", "计划": "计划", "计划 ": "计划 ", "计划受阻,等人处置": "计划受阻,等人处置", "计划在建,**等你确认**": "计划在建,**等你确认**", "计划已交付 ": "计划已交付 ", "计划已收尾(": "计划已收尾(", "计划待确认": "计划待确认", "计划文档": "计划文档", "记忆(": "记忆(", "记忆索引": "记忆索引", "证据": "证据", "证据 ": "证据 ", "评 ": "评 ", "评估": "评估", "评估卡": "评估卡", "评估者": "评估者", "评估者 ": "评估者 ", "评估者会话 ": "评估者会话 ", "评估者在裁决": "评估者在裁决", "读不到交付物:": "读不到交付物:", "读取中…": "读取中…", "读数": "读数", "读数(尺子 ": "读数(尺子 ", "起过 ": "起过 ", "跑着": "跑着", "车道 · ": "车道 · ", "边界:": "边界:", "运行时注册": "运行时注册", "还有 ": "还有 ", "还没交付": "还没交付", "这一步已作废(缘由:": "这一步已作废(缘由:", "这一步没有声明产物。": "这一步没有声明产物。", "这个会话还没开始:面板的数据来自会话日志,发第一句话之后,这里会显示已确认的事实与正在流转的命题。": "这个会话还没开始:面板的数据来自会话日志,发第一句话之后,这里会显示已确认的事实与正在流转的命题。", "这个会话还没有 ClearAI 的状态。": "这个会话还没有 ClearAI 的状态。", "这个工作区盘上已经有 ": "这个工作区盘上已经有 ", "进度": "进度", "采纳": "采纳", "采纳 ": "采纳 ", "采纳:改写 frontmatter,模型从此加载得到它": "采纳:改写 frontmatter,模型从此加载得到它", "采纳了某条世界线": "采纳了某条世界线", "采纳此世界线": "采纳此世界线", "问题:": "问题:", "阶段": "阶段", "阶段 ": "阶段 ", "阶段交界": "阶段交界", "需要你 ": "需要你 ", "面板动作失败:": "面板动作失败:", "项目章程": "项目章程", "预设自带": "预设自带", "验证": "验证", "验证中": "验证中", "这次采纳没有合并:": "这次采纳没有合并:", "(决定已登记,产物由一次普通交付落位)": "(决定已登记,产物由一次普通交付落位)", "分支或工作副本已不在": "分支或工作副本已不在", "怎么补这一级?": "怎么补这一级?", "收起这一级要交的东西": "收起这一级要交的东西", "读不到等级说明": "读不到等级说明", "这一级看什么:": "这一级看什么:", "这一级要交什么:": "这一级要交什么:", "这一级要检查的对象:": "这一级要检查的对象:", "这条命题还没写明断言主体:先用 RegisterInstance 把实例连出处登记下来。": "这条命题还没写明断言主体:先用 RegisterInstance 把实例连出处登记下来。", "reason 怎么写:": "reason 怎么写:", "reason 里必须点到上面这些对象名,不能写「时间不够」。": "reason 里必须点到上面这些对象名,不能写「时间不够」。", "跳级本身不违规:要交的是「这一级为什么不适用」的理由,不是这一级的读数。": "跳级本身不违规:要交的是「这一级为什么不适用」的理由,不是这一级的读数。", "缺口": "缺口", "语言还没立:概念与谓词都还是空的": "语言还没立:概念与谓词都还是空的", "命题只有散文主张:没有能被机器比对的断言": "命题只有散文主张:没有能被机器比对的断言", "升格的事实没带断言:它进不了实体图": "升格的事实没带断言:它进不了实体图", "命题没被任何证据碰过": "命题没被任何证据碰过", "断言主体还没落到实体图": "断言主体还没落到实体图", "跳级没写理由": "跳级没写理由", "概念没有任何结论引用": "概念没有任何结论引用", "未登记的缺口类型": "未登记的缺口类型", "下一步:": "下一步:", "看实体图": "看实体图", "切到实体图那一层,看有哪些实例节点": "切到实体图那一层,看有哪些实例节点", "这一层还没有实例节点。": "这一层还没有实例节点。", "实例靠 RegisterInstance 登记(带出处);断言挂在命题上不算「已知」。": "实例靠 RegisterInstance 登记(带出处);断言挂在命题上不算「已知」。", " 个断言主体还没落到这一层:": " 个断言主体还没落到这一层:", "先 RegisterInstance 把实例连出处登记下来;确实不值得留下形态就如实说清": "先 RegisterInstance 把实例连出处登记下来;确实不值得留下形态就如实说清", "用 ExplainLevelSkip 写明「为什么这一级在本项目里不适用」": "用 ExplainLevelSkip 写明「为什么这一级在本项目里不适用」", "要么在断言里用起来,要么在货架上如实标出「未被引用」": "要么在断言里用起来,要么在货架上如实标出「未被引用」", "交给模型的写法:": "交给模型的写法:", "推理自检:结论只是自己推了一遍,没有引入任何外部输入": "推理自检:结论只是自己推了一遍,没有引入任何外部输入", "已有知识:引用自己或别人手上已有的材料": "已有知识:引用自己或别人手上已有的材料", "可复算:照一份能重跑的步骤自己算一遍": "可复算:照一份能重跑的步骤自己算一遍", "独立裁决:由另一个评估者读产物后给结论": "独立裁决:由另一个评估者读产物后给结论", "人放行:交付前有人看过并批准": "人放行:交付前有人看过并批准", "还没建计划:先想清楚要怎么回答": "还没建计划:先想清楚要怎么回答", "执行中:有活动计划且还有未落定的步": "执行中:有活动计划且还有未落定的步", "阶段边界:当前计划的步都落定了,该结案或起新计划": "阶段边界:当前计划的步都落定了,该结案或起新计划", "在等裁决:有交付/结案在飞,或上一次裁决还没回来": "在等裁决:有交付/结案在飞,或上一次裁决还没回来", "卡住了:连续几次没通过观测准入,停下等人": "卡住了:连续几次没通过观测准入,停下等人", "目标已达成(终局)": "目标已达成(终局)", "目标已如实放弃(终局)": "目标已如实放弃(终局)", "已登记:某实例在某出处下被登记下来(一等写入口)": "已登记:某实例在某出处下被登记下来(一等写入口)", "已升格:来自过了独立裁决的事实断言": "已升格:来自过了独立裁决的事实断言", "实体断言:登记那一刻就成立的边,有出处但未经独立裁决": "实体断言:登记那一刻就成立的边,有出处但未经独立裁决", "支持到哪一级:所有支持证据里最高的那一级": "支持到哪一级:所有支持证据里最高的那一级", "从没走过的等级:已用到最高级之下、一条证据都没有的级": "从没走过的等级:已用到最高级之下、一条证据都没有的级", "被推翻次数:收到过几条推翻证据": "被推翻次数:收到过几条推翻证据", "无法判定次数:判过但判不出来": "无法判定次数:判过但判不出来", "还没有概念与谓词:换一轮只能靠重读散文取用结论": "还没有概念与谓词:换一轮只能靠重读散文取用结论", "命题只有散文主张:两条结论是不是在说同一件事只能靠重读判断": "命题只有散文主张:两条结论是不是在说同一件事只能靠重读判断", "已升格事实没带断言:进不了实体图,也不能按概念取用": "已升格事实没带断言:进不了实体图,也不能按概念取用", "有命题一条证据都没碰过:没看过不等于没问题": "有命题一条证据都没碰过:没看过不等于没问题", "断言的主体还没有落到实体图上:句子只挂在命题上,不构成「已知」": "断言的主体还没有落到实体图上:句子只挂在命题上,不构成「已知」", "有等级被跳过而没写理由:跳级不违规,但要说清为什么不适用": "有等级被跳过而没写理由:跳级不违规,但要说清为什么不适用", "有概念没有任何结论引用:它们还只是约定,不是已知": "有概念没有任何结论引用:它们还只是约定,不是已知", "判据没改过:它还是立约时那一份(要原文读账本里的 done_criteria)": "判据没改过:它还是立约时那一份(要原文读账本里的 done_criteria)", "判据改动要有一份独立裁决:改「怎样算完成」不能被顺手做掉": "判据改动要有一份独立裁决:改「怎样算完成」不能被顺手做掉", "宿主会话服务读不到:这一刻拿不到会话,写盘可能写到错地方": "宿主会话服务读不到:这一刻拿不到会话,写盘可能写到错地方", "投影服务读不到:这一刻的读数是空的,不是「没有」": "投影服务读不到:这一刻的读数是空的,不是「没有」", " · 改过 ": " · 改过 ", " 次": " 次", " · 最近一次修订的独立裁决:": " · 最近一次修订的独立裁决:", "全文在 ": "全文在 ", "打开目标文档(原生预览)": "打开目标文档(原生预览)", "背景(不参与判定):": "背景(不参与判定):"}
189
- const LOCALE_EN = {"个节点": " nodes", "条边": " edges", "适配": "Fit", "打开图谱工作区": "Open the graph workspace", "关闭工作区": "Close the workspace", "图组件不可用": "The graph component is unavailable", "点节点看知识详情": "Click a node to see what it means", "按此筛选": "Filter by this", "关闭": "Close", "正在取这条知识的读数…": "Fetching this entry’s readings…", "取不到读数": "Could not fetch the readings", "宿主没有这个对象的读数(可能刚被废止,或它不在当前词汇里)。": "The host has no readings for this object (it may have just been deprecated, or it is not in the current vocabulary).", "父概念": "Parent concept", "形态": "Value form", "取值": "Value", "引用": "Used by", "子概念": "Child concepts", "相关谓词": "Related predicates", "实例": "Instances", "关系边": "Relation edges", "主词": "Subjects", "相关事实": "Related facts", "(还有更多,未列出)": " (more not listed)", "历史": "History", "这条事实没有命题关联(旧账本):不拿文本相等去猜身份。": "This fact has no proposition link (old ledger); text equality is not used to guess identity.", "推翻条件": "Refutation condition", "支持到": "Supported to", "断言": "Assertion", "没有证据引用(旧事实或自判)。": "No evidence cites it (an old fact, or self-judged).", "出处": "Provenance", "产生步骤": "Producing step", "复核": "Review", "这条事实参与了一对冲突读数(只暴露,不裁决)": "This fact takes part in a conflict reading (surfaced, never adjudicated)", "滚轮缩放 · 拖动平移 · 拖节点挪位": "wheel to zoom · drag to pan · drag a node to move it", "类型": "Type", "还有 ": "and ", " 条未列出 · 用 / 触发菜单选": " more not listed · pick them from the / menu", "被推翻 · 待裁决": "refuted · awaiting decision", "等待输入": "awaiting input", "等待人工处理": "awaiting human action", "等待中": "waiting", "等待人工": "awaiting human", "异常终止": "terminated abnormally", "发送失败:": "send failed: ", "已获首条证据": "first evidence received", "按判据推荐": "recommended by criterion", "系统内部消息(非运行状态)": "system-internal message (not a run state)", "填写缘由后提交": "fill in the reason, then submit", "请填写缘由": "reason required", "点击在右栏预览": "click to preview in the right sidebar", "点击查看详情": "click for details", "查看步骤详情": "view step details", "查看词条": "view term", "放弃需填写缘由后提交": "fill in the reason before abandoning", "撤回需填写缘由后提交": "fill in the reason before retracting", "暂无数据。发送第一条消息后,此处显示已确立条目与在验命题。": "No data yet. After your first message this shows established entries and propositions in verification.", "暂无数据。发送第一条消息后,此处显示技能目录与记忆。": "No data yet. After your first message this shows the skill catalogue and memory.", "暂无命题。每条需一句主张与一句推翻条件;通过验证后升格为事实。": "No propositions yet. Each needs a one-line claim and a refutation condition; verified ones are promoted to facts.", "暂无目标。目标携带验收判据;判据在结果出现之前登记。": "No goal yet. A goal carries its acceptance criterion; the criterion is registered before any result.", "暂无目标。验证达门槛且无推翻的命题在验收时升格为事实。": "No goal yet. Verified propositions reaching the threshold without refutation are promoted at acceptance.", "暂无计划。建立计划后,已交付步骤按阶段列于此处。": "No plan yet. Delivered steps are listed here by stage once a plan exists.", "暂无计划。建立后此处显示计划拓扑:脊柱步、车道与收敛点。": "No plan yet. Once committed, this shows the plan topology: spine steps, lanes, and convergence.", "暂无记忆。": "No memory yet.", "暂无证据。先登记判据,后执行验证。": "No evidence yet. Register the criterion first, then verify.", "此层暂无节点。": "No nodes on this layer yet.", "暂无技能或记忆。使用 `SaveSkill` / `WriteMemory`,或将技能放入 `clear/skills/`。": "No skills or memory yet. Use `SaveSkill` / `WriteMemory`, or put a skill under `clear/skills/`.", "达门槛且无推翻的命题在目标验收时升格为事实(写入 clear/knowledge/facts/)。": "Propositions reaching the threshold with no refutation are promoted to fact at goal acceptance (written under clear/knowledge/facts/).", "修改判据需重新建立世界线": "changing the criterion requires a new worldline", "计划的拓扑与闸门:脊柱步、车道、收敛点、人工决策点": "Plan topology and gates: spine steps, lanes, convergence, human decision points", "由内核执行 ConvergeFork 完成合并": "the kernel runs ConvergeFork to complete the merge", "通过提问卡做出裁决(含判据与各分支读数)": "decide via the question card (with the criterion and each branch's reading)", "通过提问卡决定": "decide via question card", "在世界树中查看此步骤": "see this step in Worldlines", "右栏预览": "preview in right sidebar", "查看此步骤的证据": "see the evidence for this step", "查看评估者": "view evaluator", "工作区现状(首轮对话后进入投影)": "workspace state (enters the projection after the first turn)", "本体声明尚未进入投影;当前显示规范闭环的镜像,下一拍自动对齐。": "The ontology declaration has not reached the projection; a canonical-loop mirror is shown and will align at the next step.", "目录尚未到达(内核下一次 pre-step 下发)": "the catalogue has not arrived yet (the kernel sends it on the next pre-step)", "这些等级尚无证据:跳级不违规,但需说明原因": "no evidence at these levels: skipping is not a violation, but state why", "此谓词已不在词汇中(可能已废止);存量断言仍可读。": "this predicate is no longer in the vocabulary (possibly deprecated); existing assertions remain readable.", "策略暂停(原因未记录,已告警)": "paused by policy (reason not recorded; warned)", "未升格:断言仍在命题上": "not yet promoted: the assertion is still on a proposition", "放弃缘由(必填)": "Reason for abandoning (required)", "撤回缘由(必填)": "Reason for retracting (required)", "登记概念": "Register a concept", "登记谓词": "Register a predicate", "名称": "Label", "单位": "Unit", "提交": "Submit", "取消": "Cancel", "废止": "Deprecate", "为什么?": "Why?", "提交中…": "Submitting…", "点节点按概念过滤 · 滚轮缩放 · 拖拽平移": "Click a node to filter by concept · wheel to zoom · drag to pan", "复位": "Reset view", "全景": "Panorama", "还原": "Restore", "展开图带": "Show the graph band", "图里只画了前": "Showing the first", " 个节点;全景可看全部": " nodes; the panorama shows all", "一条边": "An edge", "词汇": "Vocabulary", "概念": "concepts", "谓词": "predicates", "已废止": "deprecated", "健康": "Health", "单值": "single-valued", "打开词汇货架(原生预览)": "Open the vocabulary shelf (native preview)", "冲突": "Conflict", " 对": " pair(s)", "只暴露,不裁决;撤回或维持由人决定": "surfaced, never adjudicated; retracting or keeping is a human decision", "按": "Filtered by", "过滤": "", "清除": "Clear", "按此谓词过滤": "Filter by this predicate", "在图里看": "See it in the graph", " · 未升格": " · not yet promoted", "释义": "Gloss", "主词域": "Subject domain", "值域": "Range", "本体": "Ontology", "本体货架 · ": "Ontology shelf · ", "这里会长出你的本体:已确立的条目与在验的命题。": "This is where your ontology grows: established entries and propositions still in verification.", "本体图": "Ontology graph", "实体图": "Entity graph", " · 交付 ": " · delivered ", " · 人 ": " · human ", " · 在工作区外,面板不读正文": " · outside the workspace; the panel will not read it", " · 尺子 ": " · metric ", " · 执行没跑成": " · execution did not complete", " · 支持到 ": " · supported to ", " · 最近 ": " · latest ", " · 证据 ": " · evidence ", " · 车道 ": " · lanes ", " · 随步骤作废而终止": " · ended when its step was voided", " 份)。": " files).", " 份产物,但没有任何计划声明过它们。点一条可以直接看(它们不计入交付)。": " artifacts on disk, but no plan declared them. Click one to open it (they do not count toward delivery).", " 份没列出来(这一栏只列最近改动的 ": " more not listed (this column lists only the ", " 发起": " started", " 处声明": " declared", " 字节": " bytes", " 字节 · ": " bytes · ", " 字节 · 未被任何计划声明": " bytes · declared by no plan", " 字节 · 资源 ": " bytes · resources ", " 推翻:": "refuted by:", " 旁观 ": " observe ", " 条": " entries", " 条)": " entries)", " 次侦察": " scouts", " 次评估者": " evaluators", " 步": " steps", " 步)": " steps)", " 等 ": " and ", " 评估卡": " evaluation card", " 轮": " rounds", " 里打开": "", "(假设达到这一级且无推翻才升格为事实)": " (a hypothesis is promoted to fact only at this level and with no refutation)", "(每次修订留痕,旧值不删)": " (every revision is kept; old values are not deleted)", "(点一下开右栏「世界树」)": " (click to open Worldlines)", "(点一下开右栏「世界树」看拓扑)": " (click to see the topology in Worldlines)", "(留档不删)": " (kept on record, not deleted)", "(缺)": " (missing)", "(要独立评估)才达门槛": " (independent evaluation required) to reach the threshold", "),所以这个分叉没有归宿了:**随步骤作废而终止**——它既不是被人裁掉(没人做过这个决定),也不是被算术排掉(没有尺子排过它)。要收掉工作副本(保留 ref),对它调 AbandonFork(会弹人工确认)。": "), so this fork has no destination left: **it ended when its step was voided** — it was neither decided by a person (nobody made that call) nor ranked out by arithmetic (no metric ever ranked it). To drop the working copy (keeping the ref), call AbandonFork on it (it asks for human confirmation).", "),等目标验收时升格为事实": "), and is promoted to fact when the goal is accepted", ")——它不是落选:落选意味着它跑完了、被尺子排到了后面;这一条是没跑成,所以也没有读数可以参与算术。": ") — this is not a loss: losing means it ran and was ranked behind; this one did not run, so it has no reading to enter into the arithmetic.", "**执行没跑成**(": "**Execution did not complete** (", "**执行者未归**:这条世界线的执行者派出去之后没有回灌,而分叉已经收口——它再回来也没有归宿了,所以这里既不判它跑成也没跑成,也不再等它。它的产物(如果写过)还在它自己的工作副本里;要判断那条线到底做出了什么,直接看工作副本比看这条状态可靠。": "**Executor never returned**: this worldline's executor was dispatched but never reported back, and the fork has already converged — if it returned now there would be nowhere to land, so it is neither judged done nor judged failed, and it is no longer awaited. Whatever it wrote still sits in its own working copy; to judge what that line actually produced, read the working copy rather than this status.", ":还差 ": ": short by ", ";放弃缘由:": "; reason for abandoning: ", "clearai 面板:右栏页签类型注册失败 ": "clearai panel: failed to register the right-sidebar tab type ", "clearai-loop: 席位跟着会话预设进出": "clearai-loop: seats come and go with the session's preset", "世界树": "Worldlines", "世界线:": "Worldline: ", "事实": "Facts", "产物": "Deliverables", "人审查后决定撤回": "withdrawn after human review", "人已撤回(记录保留)": "withdrawn by a person (record kept)", "撤回事实": "Retract the fact", "维持原事实": "Keep the fact", "未走过 ": "untouched: ", "认可,继续": "Approve, continue", "确认撤回": "Confirm retraction", "确认撤回(记录保留,不再作为「已知」引用)": "Confirm retraction (the record is kept; it can no longer be cited as known)", "人的裁决:": "Human decision: ", "仅人可引用": "user-invocable only", "会话日志里的原生审批对(不可伪造)": "the native approval pair in the session log (cannot be forged)", "依据": "Basis", "侦 ": "S", "侦察 · ": "Scout · ", "候选": "candidate", "做什么": "What it does", "做法": "Approach", "其他(": "Other (", "出现推翻证据": "refuting evidence appeared", "出现推翻证据(终态,记录保留)": "refuting evidence appeared (terminal; record kept)", "分支": "Branch", "切回历史的世界树(计划都还在,文档也归档在 clear/goals/plans/)": "Switch back to an earlier worldline set (the plans are all still here, and their documents are archived under clear/goals/plans/)", "判据": "Criterion", "判据:": "Criterion: ", "判据待写": "criterion not written", "升格门槛": "Promotion threshold", "原生预览打不开这条路(它在工作区外?)": "The native preview cannot open this path (is it outside the workspace?)", "原生预览打开它": "Open it in the native preview", "原生预览打开它(计划声明的产物)": "Open it in the native preview (an artifact declared by the plan)", "合并目录、本会话用法、可引用可采纳": "Merged catalogue, usage in this session, invocable and adoptable", "命题": "Propositions", "命题 · ": "Propositions · ", "在": "in", "在 ": "in ", "声明的是目录;准入要求具体文件——目录不算物证": "This declares a directory; admission requires a concrete file — a directory is not physical evidence", "多问我": "Ask me more", "完成度 ": "Progress ", "完成度 —": "Progress —", "审批记录": "approval record", "工作区模板 · clear/skills": "Workspace template · clear/skills", "已交付": "delivered", "已作废": "voided", "已回灌": "reported back", "已推翻": "refuted", "已提出": "proposed", "已撤回": "withdrawn", "已收尾 · 存档可看": "closed · archived and readable", "已改版": "superseded", "已放弃": "abandoned", "已放弃探索:": "Exploration abandoned: ", "已放弃的分叉": "abandoned fork", "已替代": "superseded", "已确认": "confirmed", "已确认事实 · ": "Confirmed facts · ", "已被下一版命题替代(版本留着,不参与当前推理)": "superseded by a later version of the proposition (the version is kept but takes no part in current reasoning)", "已裁决": "decided", "已评估": "evaluated", "已达成": "achieved", "已达门槛(": "threshold reached (", "已达门槛,已升格为事实": "threshold reached; promoted to fact", "已采纳": "adopted", "待开计划": "no plan yet", "待推进": "to advance", "待裁决": "awaiting decision", "我的 · ~/.dsh": "Mine · ~/.dsh", "打开 ": "Open ", "打开「事实」那一格并展开这条命题": "Open the Facts pane and expand this proposition", "打开世界树并选中产出这条事实的验证步": "Open Worldlines and select the verification step that produced this fact", "打开世界树并选中产生这条证据的验证步": "Open Worldlines and select the verification step that produced this evidence", "打开失败:HTTP ": "Open failed: HTTP ", "打开计划文档(原生预览)": "Open the plan document (native preview)", "技能 · 记忆": "Skills · Memory", "探索中": "exploring", "推翻": "refute", "推进中": "in progress", "提交中…": "Submitting…", "提交失败:": "Submit failed: ", "支持": "support", "支持到 ": "supported to ", "收敛": "converge", "收束:": "Closed with: ", "收起": "Collapse", "收起详情": "Collapse details", "放弃了这次探索": "abandoned this exploration", "放弃探索": "Abandon exploration", "放弃缘由(必填)": "Reason for abandoning (required)", "放弃这条分叉": "Abandon this fork", "放弃这次探索(留档不删,ref 保留)": "Abandon this exploration (kept on record, ref preserved)", "放行": "release", "旁观写这条裁决的评估者子会话(论证过程)": "Observe the evaluator sub-session that wrote this verdict (the reasoning process)", "旁观评估者": "Observe evaluator", "旁观这条世界线的评估者会话(只读)": "Observe this worldline's evaluator session (read-only)", "无法判定": "inconclusive", "是目录(不算物证)": "is a directory (not physical evidence)", "最后改动 ": "Last changed ", "未分类": "Uncategorised", "未声明": "not declared", "未收口(随步骤作废而终止)": "not closed (ended when its step was voided)", "未知": "unknown", "未走:": "Not taken: ", "未采纳": "not adopted", "本会话 模型 ": "This session — model ", "本项目 · .dsh / .agents": "This project · .dsh / .agents", "本项目写的 · clear/skills": "Written by this project · clear/skills", "核心产物(": "Core deliverables (", "步 ": "step ", "派出 ": "dispatched ", "派生 · ": "derived · ", "版本": "Version", "状态": "Status", "独立评估者": "independent evaluator", "用原生预览打开章程(它每回合整份注入模型上下文)": "Open the charter in the native preview (it is injected whole into the model's context every turn)", "盘上已有(": "Already on disk (", "盘上没有": "not on disk", "盘上没有这个文件": "this file is not on disk", "目录": "directory", "目录里已经没有(": "no longer in the catalogue (", "目标": "Goal", "目标挂起": "goal suspended", "确认放弃": "Confirm abandon", "确认放弃(留档不删,ref 保留)": "Confirm abandon (kept on record, ref preserved)", "等独立裁决": "waiting for an independent verdict", "策略暂停": "paused by policy", "续跑:多问我——每个阶段收尾就停下,等你给下一阶段(文档里叫「人在场」)。点一下切成「自己拿主意」。": "Continuation: ask me more — it stops at the end of each stage and waits for you to give the next one (called \"attended\" in the docs). Click to switch to \"decide for yourself\".", "续跑:自己拿主意——立约即授权,按轮数自己往下跑,只在不可约的判断上开门(文档里叫「无人值守」)。点一下切成「多问我」。": "Continuation: decide for yourself — committing a plan is the authorisation, and it keeps going for a set number of rounds, opening a gate only for decisions it cannot reduce (called \"unattended\" in the docs). Click to switch to \"ask me more\".", "续跑停着:": "Continuation is stopped: ", "续跑已撤回": "Continuation was withdrawn", "缘由必填": "A reason is required", "缺": "missing", "自判": "self-judged", "自定义": "custom", "自己拿主意": "Decide for yourself", "被 ": "by ", "被下一版命题改写": "rewritten by a later version of the proposition", "裁决": "Verdict", "裁决:采纳 ": "Verdict: adopt ", "要你": "needs you", "观测": "Observation", "计划": "Plan", "计划 ": "Plan ", "计划受阻,等人处置": "plan blocked, waiting for a person", "计划在建,**等你确认**": "plan is being built, **waiting for your confirmation**", "计划已交付 ": "Plan delivered ", "计划已收尾(": "Plan closed (", "计划待确认": "plan awaiting confirmation", "计划文档": "Plan document", "记忆(": "Memory (", "记忆索引": "Memory index", "证据": "Evidence", "证据 ": "Evidence ", "评 ": "E", "评估": "Evaluation", "评估卡": "evaluation card", "评估者": "Evaluator", "评估者 ": "Evaluator ", "评估者会话 ": "Evaluator session ", "评估者在裁决": "an evaluator is deciding", "读不到交付物:": "Cannot read deliverables: ", "读取中…": "Reading…", "读数": "Reading", "读数(尺子 ": "Reading (metric ", "起过 ": "ran ", "跑着": "running", "车道 · ": "Lane · ", "边界:": "Scope: ", "运行时注册": "registered at runtime", "还有 ": "and ", "还没交付": "not delivered yet", "这一步已作废(缘由:": "This step was voided (reason: ", "这一步没有声明产物。": "This step declares no artifacts.", "这个会话还没开始:面板的数据来自会话日志,发第一句话之后,这里会显示已确认的事实与正在流转的命题。": "This session has not started: the panels read the session log, so after your first message this will show confirmed facts and the propositions in flight.", "这个会话还没有 ClearAI 的状态。": "This session has no ClearAI state yet.", "这个工作区盘上已经有 ": "This workspace already has ", "进度": "Progress", "采纳": "Adopt", "采纳 ": "Adopt ", "采纳:改写 frontmatter,模型从此加载得到它": "Adopt: rewrites the frontmatter so the model can load it from then on", "采纳了某条世界线": "adopted one worldline", "采纳此世界线": "Adopt this worldline", "问题:": "Question: ", "阶段": "Stage", "阶段 ": "Stage ", "阶段交界": "at a stage boundary", "需要你 ": "needs you ", "面板动作失败:": "Panel action failed: ", "项目章程": "Project charter", "预设自带": "shipped with the preset", "验证": "Verification", "验证中": "verifying", "这次采纳没有合并:": "This adoption was not merged: ", "(决定已登记,产物由一次普通交付落位)": " (the decision is on record; a normal delivery places the artifacts)", "分支或工作副本已不在": "its branch or working copy is gone", "怎么补这一级?": "How do I fill in this level?", "收起这一级要交的东西": "Hide what this level requires", "读不到等级说明": "Level descriptions are unavailable", "这一级看什么:": "What this level looks at: ", "这一级要交什么:": "What this level must hand in: ", "这一级要检查的对象:": "Objects this level must check: ", "这条命题还没写明断言主体:先用 RegisterInstance 把实例连出处登记下来。": "This proposition names no assertion subject yet: use RegisterInstance to register the instance together with its provenance.", "reason 怎么写:": "How to write the reason: ", "reason 里必须点到上面这些对象名,不能写「时间不够」。": "The reason must name those objects above; \"not enough time\" does not count.", "跳级本身不违规:要交的是「这一级为什么不适用」的理由,不是这一级的读数。": "Skipping a level is not itself a violation: what is owed is why this level does not apply here, not a reading for it.", "缺口": "Gaps", "语言还没立:概念与谓词都还是空的": "No language yet: both concepts and predicates are empty", "命题只有散文主张:没有能被机器比对的断言": "Prose-only proposition: it has no assertion a machine can compare", "升格的事实没带断言:它进不了实体图": "A promoted fact carries no assertion, so it cannot enter the entity graph", "命题没被任何证据碰过": "No evidence has touched this proposition", "断言主体还没落到实体图": "Assertion subjects have not landed on the entity graph", "跳级没写理由": "A level was skipped without a reason", "概念没有任何结论引用": "No conclusion cites this concept", "未登记的缺口类型": "Unregistered gap type", "下一步:": "Next: ", "看实体图": "Show the entity graph", "切到实体图那一层,看有哪些实例节点": "Switch to the entity layer to see which instance nodes exist", "这一层还没有实例节点。": "No instance nodes have landed on this layer yet.", "实例靠 RegisterInstance 登记(带出处);断言挂在命题上不算「已知」。": "Instances arrive through RegisterInstance, with provenance; an assertion left on a proposition does not count as known.", " 个断言主体还没落到这一层:": " assertion subjects have not landed on this layer: ", "先 RegisterInstance 把实例连出处登记下来;确实不值得留下形态就如实说清": "First register the instance with RegisterInstance, provenance included; if it truly does not deserve a lasting shape, say so plainly", "用 ExplainLevelSkip 写明「为什么这一级在本项目里不适用」": "Use ExplainLevelSkip to state why this level does not apply in this project", "要么在断言里用起来,要么在货架上如实标出「未被引用」": "Either put it to use in an assertion, or mark it plainly on the shelf as uncited", "交给模型的写法:": "How to hand it to the model: ", "推理自检:结论只是自己推了一遍,没有引入任何外部输入": "Reasoning self-check: the conclusion is only your own derivation, with no external input brought in", "已有知识:引用自己或别人手上已有的材料": "Existing knowledge: citing material you or someone else already has at hand", "可复算:照一份能重跑的步骤自己算一遍": "Reproducible: work it through yourself from a set of steps that can be re-run", "独立裁决:由另一个评估者读产物后给结论": "Independent verdict: another evaluator reads the artifact and gives the conclusion", "人放行:交付前有人看过并批准": "Human release: a person has looked at it and approved before delivery", "还没建计划:先想清楚要怎么回答": "No plan yet: work out how the question will be answered first", "执行中:有活动计划且还有未落定的步": "Executing: there is an active plan with steps still open", "阶段边界:当前计划的步都落定了,该结案或起新计划": "Stage boundary: every step of the current plan has landed; close the goal or plan the next stage", "在等裁决:有交付/结案在飞,或上一次裁决还没回来": "Awaiting verdict: a delivery or closure is in flight, or the last verdict has not returned", "卡住了:连续几次没通过观测准入,停下等人": "Stalled: admission failed several times in a row; stopped and waiting for a person", "目标已达成(终局)": "Goal achieved (terminal)", "目标已如实放弃(终局)": "Goal abandoned honestly (terminal)", "已登记:某实例在某出处下被登记下来(一等写入口)": "Registered: an instance was recorded against a provenance (a first-class write path)", "已升格:来自过了独立裁决的事实断言": "Promoted: an assertion from a fact that passed an independent verdict", "实体断言:登记那一刻就成立的边,有出处但未经独立裁决": "Asserted edge: holds from the moment it was recorded, with provenance but no independent verdict", "支持到哪一级:所有支持证据里最高的那一级": "Supported to: the highest level among supporting evidence", "从没走过的等级:已用到最高级之下、一条证据都没有的级": "Untouched levels: levels below the highest one used that have no evidence at all", "被推翻次数:收到过几条推翻证据": "Refutations: how many refuting pieces of evidence arrived", "无法判定次数:判过但判不出来": "Inconclusive: judged, but the material would not settle it", "还没有概念与谓词:换一轮只能靠重读散文取用结论": "No concepts or predicates yet: reusing conclusions means re-reading prose", "命题只有散文主张:两条结论是不是在说同一件事只能靠重读判断": "Prose-only claim: whether two conclusions say the same thing can only be decided by re-reading", "已升格事实没带断言:进不了实体图,也不能按概念取用": "A promoted fact carries no assertion: it cannot enter the entity graph or be fetched by concept", "有命题一条证据都没碰过:没看过不等于没问题": "Some claim has never been touched by evidence: unlooked is not the same as fine", "断言的主体还没有落到实体图上:句子只挂在命题上,不构成「已知」": "Assertion subjects have not landed on the entity graph: the sentence hangs on a proposition and is not yet known", "有等级被跳过而没写理由:跳级不违规,但要说清为什么不适用": "A level was skipped with no reason recorded: skipping is allowed, but say why it does not apply", "有概念没有任何结论引用:它们还只是约定,不是已知": "A concept is cited by no conclusion: it is still only a convention, not knowledge", "判据没改过:它还是立约时那一份(要原文读账本里的 done_criteria)": "The criterion has not changed: it is still the one registered at commit time (read done_criteria from the ledger)", "判据改动要有一份独立裁决:改「怎样算完成」不能被顺手做掉": "Changing the criterion needs an independent verdict: redefining done must not happen as a side effect", "宿主会话服务读不到:这一刻拿不到会话,写盘可能写到错地方": "The host session service is unreadable: no session right now, so writing could land in the wrong place", "投影服务读不到:这一刻的读数是空的,不是「没有」": "The projection service is unreadable: readings are empty right now, which is not the same as nothing", " · 改过 ": " · changed ", " 次": " times", " · 最近一次修订的独立裁决:": " · latest revision decided by an independent verdict: ", "全文在 ": "Full text at ", "打开目标文档(原生预览)": "Open the goal document (native preview)", "背景(不参与判定):": "Background (not part of the verdict): "}
188
+ const LOCALE_ZH = {":":":"," · 改过 ":" · 改过 "," · 最近一次修订的独立裁决:":" · 最近一次修订的独立裁决:"," 次":" 次"," 次评估者":" 次评估者"," 步":" 步"," 步)":" 步)"," 看核验记录":" 看核验记录"," 看核验过程":" 看核验过程"," 轮":" 轮","(点一下开右栏「世界树」)":"(点一下开右栏「世界树」)","(点一下开右栏「世界树」看拓扑)":"(点一下开右栏「世界树」看拓扑)","(缺)":"(缺)","clearai 面板:右栏页签类型注册失败 ":"clearai 面板:右栏页签类型注册失败 ","clearai-loop: 席位跟着会话预设进出":"clearai-loop: 席位跟着会话预设进出","」相关的":"」相关的","一句话":"一句话","一步检验":"一步检验","上位":"上位","下一步:找一步去检验它":"下一步:找一步去检验它","下一步:换个办法再检验":"下一步:换个办法再检验","下一步:独立核验通过 → 已验证":"下一步:独立核验通过 → 已验证","下一步:结案时写进长期知识":"下一步:结案时写进长期知识","下位":"下位","不确定":"不确定","世界树":"世界树","主语":"主语","事实":"事实","产物":"产物","人撤回":"人撤回","人放行":"人放行","代码":"代码","依据":"依据","值的形态":"值的形态","做什么":"做什么","入本体":"入本体","全屏":"全屏","全文在 ":"全文在 ","公式":"公式","关系":"关系","关闭":"关闭","写进长期知识":"写进长期知识","出自":"出自","切回历史的世界树(计划都还在,文档也归档在 clear/goals/plans/)":"切回历史的世界树(计划都还在,文档也归档在 clear/goals/plans/)","判据":"判据","判据:":"判据:","判断":"判断","原生预览打开它":"原生预览打开它","原生预览打开它(计划声明的产物)":"原生预览打开它(计划声明的产物)","取不到":"取不到","取值":"取值","只看与「":"只看与「","只看相关":"只看相关","可信度怎么变的":"可信度怎么变的","可复算":"可复算","命题":"命题","图组件不可用":"图组件不可用","在图上找它":"在图上找它","在检验这一站被推翻,不再往下走。":"在检验这一站被推翻,不再往下走。","实体图":"实体图","实体图还空着。":"实体图还空着。","实例":"实例","审批记录":"审批记录","已交付":"已交付","已作废":"已作废","已推翻":"已推翻","已提出":"已提出","已收尾 · 存档可看":"已收尾 · 存档可看","已替换":"已替换","已被替换,不再往下走。":"已被替换,不再往下走。","已验证":"已验证","引用":"引用","引用已有材料":"引用已有材料","待推进":"待推进","待核验":"待核验","打开「本体」那一格并展开这条结论":"打开「本体」那一格并展开这条结论","打开目标文档(原生预览)":"打开目标文档(原生预览)","打开计划文档(原生预览)":"打开计划文档(原生预览)","推翻":"推翻","提出":"提出","支持":"支持","收起":"收起","收起详情":"收起详情","放行":"放行","数值":"数值","暂无计划。建立后此处显示计划的步骤与闸门。":"暂无计划。建立后此处显示计划的步骤与闸门。","有 ":"有 ","有矛盾":"有矛盾","未声明":"未声明","本体":"本体","本体图":"本体图","查看此步骤的证据":"查看此步骤的证据","查看步骤详情":"查看步骤详情","查看评估者":"查看评估者","检验":"检验","检验结果:":"检验结果:","概念":"概念","正在取…":"正在取…","正在裁决":"正在裁决","步 ":"步 ","清除":"清除","点击查看详情":"点击查看详情","点开看进度和来历":"点开看进度和来历","状态":"状态","独立核验":"独立核验","独立核验:":"独立核验:","用到它的结论":"用到它的结论","盘上没有这个文件":"盘上没有这个文件","目标":"目标","相关":"相关","看核验":"看核验","看记录":"看记录","矛盾":"矛盾","第 n 步":"第 n 步","算错":"算错","类型":"类型","结论":"结论","结论照常可读。":"结论照常可读。","背景(不参与判定):":"背景(不参与判定):","自己推了一遍":"自己推了一遍","自己检验":"自己检验","范围":"范围","范围:":"范围:","补充":"补充","观测":"观测","计划":"计划","计划 ":"计划 ","计划受阻,等人处置":"计划受阻,等人处置","计划已交付 ":"计划已交付 ","计划已收尾(":"计划已收尾(","计划文档":"计划文档","计划的步骤与闸门":"计划的步骤与闸门","证据":"证据","评 ":"评 ","评估":"评估","评估卡":"评估卡","起过 ":"起过 ","还没有内容。发第一条消息后,这里显示本体图和结论。":"还没有内容。发第一条消息后,这里显示本体图和结论。","还没有立目标":"还没有立目标","还没有结论。模型立下判断之后,这里一行一条。":"还没有结论。模型立下判断之后,这里一行一条。","这一项已经不在当前的词里了(可能刚被废止)。":"这一项已经不在当前的词里了(可能刚被废止)。","这个词已废止,旧结论里的用法照样可读。":"这个词已废止,旧结论里的用法照样可读。","进度":"进度","退出全屏":"退出全屏","适配":"适配","释义":"释义","待处理":"待处理","待处理 ":"待处理 ","验证":"验证","验证中":"验证中","展开下一层":"展开下一层","收起下一层":"收起下一层","本体文件有 ":"本体文件有 "," 处问题(这些不进图)":" 处问题(这些不进图)"," 个断言的主语还没写成实体文件。":" 个断言的主语还没写成实体文件。","clear/ontology/entities/ 下的实体文件、写进长期知识的断言会出现在这里。":"clear/ontology/entities/ 下的实体文件、写进长期知识的断言会出现在这里。","本体图还空着。模型在 clear/ontology/concepts/ 和 relations/ 下写概念与关系文件之后,它们会长在这里。":"本体图还空着。模型在 clear/ontology/concepts/ 和 relations/ 下写概念与关系文件之后,它们会长在这里。"}
189
+ const LOCALE_EN = {":":": "," · 改过 ":" · changed "," · 最近一次修订的独立裁决:":" · latest revision decided by an independent verdict: "," 次":" times"," 次评估者":" evaluators"," 步":" steps"," 步)":" steps)"," 看核验记录":" Review record"," 看核验过程":" Watch the review"," 轮":" rounds","(点一下开右栏「世界树」)":" (click to open the World Tree)","(点一下开右栏「世界树」看拓扑)":" (click to see the steps in the World Tree)","(缺)":" (missing)","clearai 面板:右栏页签类型注册失败 ":"clearai panel: failed to register the right-sidebar tab type ","clearai-loop: 席位跟着会话预设进出":"clearai-loop: seats come and go with the session's preset","」相关的":"\"","一句话":"Statement","一步检验":"A test","上位":"Broader","下一步:找一步去检验它":"Next: test it in a step","下一步:换个办法再检验":"Next: test it another way","下一步:独立核验通过 → 已验证":"Next: independent check passes → Verified","下一步:结案时写进长期知识":"Next: written to long-term knowledge at close","下位":"Narrower","不确定":"Uncertain","世界树":"World Tree","主语":"Subject","事实":"Facts","产物":"Deliverables","人撤回":"Withdrawn by a person","人放行":"Released by a person","代码":"Code","依据":"Basis","值的形态":"Value form","做什么":"What it does","入本体":"In ontology","全屏":"Full screen","全文在 ":"Full text at ","公式":"Formula","关系":"Relation","关闭":"Close","写进长期知识":"Written to long-term knowledge","出自":"From","切回历史的世界树(计划都还在,文档也归档在 clear/goals/plans/)":"Switch back to an earlier World Tree (the plans are all still here, and their documents are archived under clear/goals/plans/)","判据":"Criterion","判据:":"Criterion: ","判断":"Judgment","原生预览打开它":"Open it in the native preview","原生预览打开它(计划声明的产物)":"Open it in the native preview (an artifact declared by the plan)","取不到":"Unavailable","取值":"Value","只看与「":"Only related to \"","只看相关":"Only related","可信度怎么变的":"How trust changed","可复算":"Reproducible","命题":"Propositions","图组件不可用":"The graph component is unavailable","在图上找它":"Find it on the graph","在检验这一站被推翻,不再往下走。":"Refuted at the test stage; it goes no further.","实体图":"Entity graph","实体图还空着。":"The entity graph is empty. ","实例":"Instances","审批记录":"approval record","已交付":"delivered","已作废":"voided","已推翻":"Refuted","已提出":"proposed","已收尾 · 存档可看":"closed · archived and readable","已替换":"Replaced","已被替换,不再往下走。":"Replaced; it goes no further.","已验证":"Verified","引用":"Reference","引用已有材料":"Cites existing material","待推进":"to advance","待核验":"Awaiting check","打开「本体」那一格并展开这条结论":"Open the Ontology pane and expand this conclusion","打开目标文档(原生预览)":"Open the goal document (native preview)","打开计划文档(原生预览)":"Open the plan document (native preview)","推翻":"Refute","提出":"Proposed","支持":"Support","收起":"Collapse","收起详情":"Collapse details","放行":"release","数值":"Quantity","暂无计划。建立后此处显示计划的步骤与闸门。":"No plan yet. Once created, its steps show here.","有 ":"","有矛盾":"Conflicting","未声明":"not declared","本体":"Ontology","本体图":"Ontology graph","查看此步骤的证据":"see the evidence for this step","查看步骤详情":"view step details","查看评估者":"view evaluator","检验":"Test","检验结果:":"Result: ","概念":"concepts","正在取…":"Loading…","正在裁决":"Deciding","步 ":"step ","清除":"Clear","点击查看详情":"click for details","点开看进度和来历":"Open to see progress and history","状态":"Status","独立核验":"Independent check","独立核验:":"Independent check: ","用到它的结论":"Conclusions using it","盘上没有这个文件":"this file is not on disk","目标":"Goal","相关":"Related","看核验":"See check","看记录":"See record","矛盾":"Conflicts","第 n 步":"Step n","算错":"Wrong if","类型":"Type","结论":"CONCLUSIONS","结论照常可读。":"conclusions are still readable.","背景(不参与判定):":"Background (not part of the verdict): ","自己推了一遍":"Reasoned through alone","自己检验":"Self-tested","范围":"Scope","范围:":"Scope: ","补充":"More","观测":"Observation","计划":"Plan","计划 ":"Plan ","计划受阻,等人处置":"plan blocked, waiting for a person","计划已交付 ":"Plan delivered ","计划已收尾(":"Plan closed (","计划文档":"Plan document","计划的步骤与闸门":"Plan steps","证据":"Evidence","评 ":"E","评估":"Evaluation","评估卡":"evaluation card","起过 ":"ran ","还没有内容。发第一条消息后,这里显示本体图和结论。":"Nothing yet. After the first message, the graph and conclusions show here.","还没有立目标":"No goal yet","还没有结论。模型立下判断之后,这里一行一条。":"No conclusions yet. Each judgment gets one line here.","这一项已经不在当前的词里了(可能刚被废止)。":"This item is no longer in the vocabulary (it may have just been deprecated).","这个词已废止,旧结论里的用法照样可读。":"This term is deprecated; older conclusions still read fine.","进度":"Progress","退出全屏":"Exit full screen","适配":"Fit","释义":"Gloss","待处理":"To handle","待处理 ":"to handle ","验证":"Verification","验证中":"Testing","展开下一层":"Expand the next level","收起下一层":"Collapse the next level","本体文件有 ":"Ontology files have "," 处问题(这些不进图)":" problems (these stay off the graph)"," 个断言的主语还没写成实体文件。":" assertion subjects have no entity file yet. ","clear/ontology/entities/ 下的实体文件、写进长期知识的断言会出现在这里。":"Entity files under clear/ontology/entities/ and assertions in long-term knowledge appear here.","本体图还空着。模型在 clear/ontology/concepts/ 和 relations/ 下写概念与关系文件之后,它们会长在这里。":"The ontology graph is empty. Concepts and relations appear once the model writes files under clear/ontology/concepts/ and relations/."}
190
190
 
191
191
  /**
192
192
  * 翻译函数:**由原生 locale 座位绑定**(`ctx.locale.bind`),不是我们自建的一套 i18n。
@@ -231,56 +231,11 @@ window.__ModuleLoader__.load({
231
231
  */
232
232
  const inject = ['slots', 'sessions', 'sidebarRightTabs', 'sidebarRight']
233
233
 
234
- const PHASE = lazyTable(() => ({
235
- drafting: t('判据待写'),
236
- planning: t('待开计划'),
237
- executing: t('推进中'),
238
- waiting: t('等待中'),
239
- stage_boundary: t('阶段交界'),
240
- auditing: t('评估者在裁决'),
241
- stalled: t('等待人工'),
242
- suspended: t('目标挂起'),
243
- achieved: t('已达成'),
244
- abandoned: t('已放弃'),
245
- superseded: t('已改版'),
246
- }))
247
- const HYPOTHESIS = lazyTable(() => ({
248
- proposed: t('已提出'),
249
- alive: t('验证中'),
250
- confirmed: t('已确认'),
251
- /** 规范词是「已替代」(与验证本体同词);「已改版」是旧写法。 */
252
- refuted: t('已推翻'),
253
- superseded: t('已替代'),
254
- retracted: t('已撤回'),
255
- }))
256
234
  const STEP = lazyTable(() => ({ open: t('待推进'), advanced: t('已交付'), void: t('已作废') }))
257
- const VERDICT = lazyTable(() => ({ support: t('支持'), refute: t('推翻'), inconclusive: t('无法判定') }))
258
- const EVALUATOR = lazyTable(() => ({ self: t('自判'), independent: t('独立评估者') }))
259
- const BRANCH_STATUS = lazyTable(() => ({ exploring: t('探索中'), evaluated: t('已评估'), adopted: t('已采纳'), pruned: t('未采纳') }))
260
- /** 人在面板上做过的裁决(树详情里要如实回放:谁、以什么理由)。 */
261
- const FORK_ACTION = lazyTable(() => ({ adopt_branch: t('采纳了某条世界线'), abandon_fork: t('放弃了这次探索') }))
262
- /** 侦察的三种结局(与 fold 的派生同源):跑着 / 正常回灌 / 没正常结束。 */
263
- const SCOUT = lazyTable(() => ({ running: t('跑着'), settled: t('已回灌'), failed: t('异常终止') }))
264
-
265
- /**
266
- * **缺口 code → 人话标签**。code 是机器词(`no_language` 这类),**不上屏**——
267
- * 人看到的是这一句。允许的 code 在这里是**显式清单**:表本身是惰性代理,
268
- * 用 `in` 判会把 `toString` 这类原型上的键也认成缺口类型。
269
- * 表里的每一句都在 LOCALE_ZH / LOCALE_EN 里成对登记(与其余面向人的串同一规矩)。
270
- */
271
- const GAP_CODES = ['no_language', 'prose_only_claims', 'unstructured_facts', 'untouched_claims', 'entities_unlanded', 'levels_skipped', 'orphan_terms']
272
- const GAP_LABEL = lazyTable(() => ({
273
- no_language: t('语言还没立:概念与谓词都还是空的'),
274
- prose_only_claims: t('命题只有散文主张:没有能被机器比对的断言'),
275
- unstructured_facts: t('升格的事实没带断言:它进不了实体图'),
276
- untouched_claims: t('命题没被任何证据碰过'),
277
- entities_unlanded: t('断言主体还没落到实体图'),
278
- levels_skipped: t('跳级没写理由'),
279
- orphan_terms: t('概念没有任何结论引用'),
280
- }))
281
-
282
- const STRONG = { refuted: true, stalled: true, refute: true }
283
-
235
+ const VERDICT = lazyTable(() => ({ support: t('支持'), refute: t('推翻'), inconclusive: t('不确定') }))
236
+
237
+ /** 「第 n 步」:整句做一个词条,英文才排得成 Step n。 */
238
+ const nth = (n) => t('第 n 步').replace('n', String(n))
284
239
  const dash = (value) => (value === null || value === undefined || value === '' ? '—' : String(value))
285
240
  const gloss = (table, value) => (value === null || value === undefined ? '—' : `${table[value] ?? value}`)
286
241
  const when = (at) => {
@@ -329,14 +284,124 @@ window.__ModuleLoader__.load({
329
284
  .clearai-treerow{display:flex;align-items:center;gap:6px;padding:0 6px;cursor:pointer;border-radius:4px;transition:background .12s}
330
285
  .clearai-treerow:hover{background:var(--dsw-alias-interactive-bg-hover)}
331
286
  .clearai-treerow[data-sel="1"]{background:var(--dsw-alias-interactive-bg-active, var(--dsw-alias-interactive-bg-hover))}
287
+ /* ── 本体格(第六阶段):期刊式排版 + 进度轨。颜色只用主题令牌,深浅色跟着宿主走。 ── */
288
+ .clearai-atlas{height:100%;overflow-y:auto;box-sizing:border-box;padding:22px 26px 180px;font-size:13px;line-height:1.6;color:var(--dsw-alias-label-primary)}
289
+ .clearai-serif,.clearai-question,.clearai-row-title,.clearai-card-title,.clearai-star>span{font-family:ui-serif,"Songti SC","Noto Serif SC","Source Han Serif SC",Georgia,serif}
290
+ .clearai-head{display:flex;flex-direction:column;gap:6px;padding-bottom:16px;margin-bottom:16px;border-bottom:.5px solid var(--dsw-alias-border-l2)}
291
+ .clearai-question{font-size:19px;font-weight:600;line-height:1.45;letter-spacing:.01em}
292
+ .clearai-counts{font-size:12px;color:var(--dsw-alias-label-secondary)}
293
+ .clearai-need{display:flex;gap:8px;align-items:baseline;font-size:12.5px;padding:4px 10px;border-left:2px solid var(--dsw-alias-state-warn-primary);background:var(--dsw-alias-bg-layer-1);border-radius:0 6px 6px 0}
294
+ .clearai-need b{font-weight:600;color:var(--dsw-alias-state-warn-primary);white-space:nowrap}
295
+ .clearai-track{display:flex;align-items:center;gap:8px;margin-top:6px;font-size:11.5px;color:var(--dsw-alias-label-tertiary)}
296
+ .clearai-track-stop{display:inline-flex;align-items:center;gap:5px;white-space:nowrap}
297
+ .clearai-track-stop i{width:7px;height:7px;border-radius:50%;border:1px solid var(--dsw-alias-border-l3);box-sizing:border-box}
298
+ .clearai-track-stop[data-on="1"]{color:var(--dsw-alias-label-primary)}
299
+ .clearai-track-stop[data-on="1"] i{background:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary)}
300
+ .clearai-track-line{flex:1 1 18px;max-width:56px;height:1px;background:var(--dsw-alias-border-l2)}
301
+ .clearai-track-line[data-on="1"]{background:var(--dsw-alias-label-secondary)}
302
+ /* 图 */
303
+ .clearai-graph{margin-bottom:18px}
304
+ .clearai-graph[data-full="1"]{position:fixed;inset:0;z-index:40;background:var(--dsw-alias-bg-base);padding:16px 20px;display:flex;flex-direction:column;margin:0}
305
+ .clearai-graph-bar{display:flex;align-items:center;gap:14px;margin-bottom:8px;font-size:12px}
306
+ .clearai-seg{display:inline-flex;border:.5px solid var(--dsw-alias-border-l3);border-radius:999px;padding:2px}
307
+ .clearai-seg>span{padding:2px 12px;border-radius:999px;cursor:pointer;color:var(--dsw-alias-label-secondary);white-space:nowrap}
308
+ .clearai-seg>span[data-on="1"]{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-label-primary-inverted)}
309
+ .clearai-legend{display:inline-flex;gap:12px;color:var(--dsw-alias-label-tertiary);font-size:11.5px}
310
+ .clearai-legend>span{display:inline-flex;align-items:center;gap:5px}
311
+ .clearai-legend>span::before{content:"";width:16px;border-top:1.3px solid var(--dsw-alias-label-secondary)}
312
+ .clearai-legend>span[data-l="dash"]::before{border-top-style:dashed;border-top-color:var(--dsw-alias-label-tertiary)}
313
+ .clearai-legend>span[data-l="warn"]::before{border-top-color:var(--dsw-alias-state-warn-primary)}
314
+ .clearai-graph-body{display:flex;flex-direction:column;gap:10px;min-height:0}
315
+ .clearai-graph[data-full="1"] .clearai-graph-body{flex:1 1 auto}
316
+ .clearai-graph-body[data-side="1"]{flex-direction:row}
317
+ .clearai-graph-canvas{height:340px;border:.5px solid var(--dsw-alias-border-l2);border-radius:10px;overflow:hidden;background:var(--dsw-alias-bg-base)}
318
+ .clearai-graph[data-full="1"] .clearai-graph-canvas{height:auto;flex:1 1 auto;min-width:0}
319
+ .clearai-graph-side{flex:0 0 auto}
320
+ .clearai-graph-body[data-side="1"] .clearai-graph-side{width:380px;overflow-y:auto}
321
+ .clearai-graph-problems{margin-top:6px;font-size:11.5px;color:var(--dsw-alias-label-secondary)}.clearai-graph-problems summary{cursor:pointer;color:var(--dsw-alias-state-warn-primary)}.clearai-graph-problems>div{padding:2px 0 2px 12px;line-height:1.5}.clearai-graph-problems>div[data-sev=info]{opacity:.7}.clearai-graph-problems code{font-size:11px}
322
+ .clearai-fold{margin-left:2px;padding:0 4px;border-radius:6px;font-weight:500;font-size:10.5px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-l2,rgba(127,127,127,.12));cursor:pointer}
323
+ .clearai-graph-empty{height:150px;display:flex;align-items:center;justify-content:center;text-align:center;padding:0 24px;border:.5px dashed var(--dsw-alias-border-l3);border-radius:10px;color:var(--dsw-alias-label-secondary);font-size:12.5px}
324
+ .react-flow__node.clearai-star-node .react-flow__handle{opacity:0;pointer-events:none;left:4.5px;top:50%;transform:translate(-50%,-50%);min-width:0;min-height:0;width:1px;height:1px;border:0}
325
+ .react-flow__node.clearai-star-node.selected,.react-flow__node.clearai-star-node:focus{box-shadow:none;outline:none}
326
+ .clearai-star{display:inline-flex;align-items:center;gap:6px;white-space:nowrap;font-size:12.5px;color:var(--dsw-alias-label-primary);cursor:pointer}
327
+ .clearai-star>i{flex:0 0 auto;width:9px;height:9px;border-radius:50%;box-sizing:border-box;border:1.4px solid var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base)}
328
+ .clearai-star[data-kind="instance"]>i{background:var(--dsw-alias-label-primary)}
329
+ .clearai-star[data-kind="literal"]>i{width:6px;height:6px;border:0;background:var(--dsw-alias-label-secondary)}
330
+ .clearai-star[data-kind="literal"]{color:var(--dsw-alias-label-secondary);font-size:11.5px}
331
+ .clearai-star[data-kind="value_type"]>i{border-radius:2px;width:8px;height:8px;border-color:var(--dsw-alias-label-tertiary)}
332
+ .clearai-star[data-kind="value_type"]{color:var(--dsw-alias-label-secondary);font-size:11.5px}
333
+ .clearai-star[data-state="pending"]>i{border-style:dashed;background:var(--dsw-alias-bg-base);border-color:var(--dsw-alias-label-secondary)}
334
+ .clearai-star[data-state="off"]{opacity:.45}
335
+ .clearai-star[data-state="off"]>i{border-style:dashed}
336
+ .clearai-star[data-state="conflict"]>i{border-color:var(--dsw-alias-state-warn-primary);background:var(--dsw-alias-state-warn-primary)}
337
+ .clearai-star[data-sel="1"]>i{box-shadow:0 0 0 4px var(--dsw-alias-interactive-bg-hover)}
338
+ .clearai-star[data-sel="1"]>span{font-weight:600}
339
+ /* 小卡 */
340
+ .clearai-card{display:flex;flex-direction:column;gap:8px;padding:12px 14px;border:.5px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1)}
341
+ .clearai-card-head{display:flex;align-items:baseline;gap:8px;font-size:12px}
342
+ .clearai-card-kind{font-size:11px;color:var(--dsw-alias-label-tertiary);letter-spacing:.06em}
343
+ .clearai-card-title{font-size:15px;font-weight:600}
344
+ .clearai-card-facts{display:flex;flex-direction:column;gap:4px}
345
+ .clearai-card-fact{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;font-size:12.5px}
346
+ /* 结论 */
347
+ .clearai-section-head{font-size:11px;letter-spacing:.14em;color:var(--dsw-alias-label-tertiary);margin:4px 0 4px}
348
+ .clearai-filter{display:flex;gap:10px;align-items:baseline;font-size:12px;color:var(--dsw-alias-label-secondary);margin:-8px 0 12px}
349
+ .clearai-list{display:flex;flex-direction:column;gap:10px}
350
+ .clearai-group-head{display:flex;gap:6px;align-items:baseline;font-size:11.5px;color:var(--dsw-alias-label-secondary);margin:6px 0 2px}
351
+ .clearai-group-head>span{color:var(--dsw-alias-label-tertiary)}
352
+ .clearai-row{border-top:.5px solid var(--dsw-alias-border-l1)}
353
+ .clearai-row[data-open="1"]{background:var(--dsw-alias-bg-layer-1);border-radius:8px;border-top-color:transparent}
354
+ .clearai-row-head{display:flex;align-items:baseline;gap:10px;padding:7px 8px;cursor:pointer;border-radius:8px}
355
+ .clearai-row-head:hover{background:var(--dsw-alias-interactive-bg-hover)}
356
+ .clearai-row-title{font-size:14px;font-weight:500;white-space:nowrap}
357
+ .clearai-row-sub{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;color:var(--dsw-alias-label-secondary)}
358
+ .clearai-row-step{margin-left:auto;font-size:11.5px;color:var(--dsw-alias-label-tertiary);white-space:nowrap}
359
+ .clearai-row[data-trust="replaced"] .clearai-row-title{text-decoration:line-through;color:var(--dsw-alias-label-tertiary)}
360
+ .clearai-trust{flex:0 0 auto;display:inline-block;font-size:11px;line-height:16px;padding:0 7px;border-radius:999px;border:.5px solid var(--dsw-alias-label-primary);color:var(--dsw-alias-label-primary);white-space:nowrap}
361
+ .clearai-trust[data-trust="credible"]{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-label-primary-inverted)}
362
+ .clearai-trust[data-trust="testing"]{border-color:var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary)}
363
+ .clearai-trust[data-trust="unclear"]{border-style:dashed;border-color:var(--dsw-alias-label-tertiary);color:var(--dsw-alias-label-secondary)}
364
+ .clearai-trust[data-trust="refuted"]{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
365
+ .clearai-trust[data-trust="replaced"]{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary)}
366
+ .clearai-trust[data-trust="conflict"]{border-color:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-primary)}
367
+ /* 点开一条:三段 */
368
+ .clearai-detail{display:flex;flex-direction:column;gap:16px;padding:6px 12px 14px 12px}
369
+ .clearai-part-head{display:flex;align-items:center;gap:8px;font-size:11.5px;color:var(--dsw-alias-label-tertiary);margin-bottom:8px}
370
+ .clearai-part-head b{width:16px;height:16px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary)}
371
+ .clearai-stations-bar{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:4px}
372
+ .clearai-stations-bar span{height:5px;border-radius:3px;background:var(--dsw-alias-border-l1)}
373
+ .clearai-stations-bar span[data-state="done"]{background:var(--dsw-alias-label-primary)}
374
+ .clearai-stations-bar span[data-state="next"]{background:repeating-linear-gradient(90deg,var(--dsw-alias-label-secondary) 0 4px,var(--dsw-alias-border-l1) 4px 8px)}
375
+ .clearai-stations-bar span[data-state="broken"]{background:var(--dsw-alias-state-error-primary)}
376
+ .clearai-stations-names{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:4px;margin-top:6px;font-size:11.5px;color:var(--dsw-alias-label-tertiary)}
377
+ .clearai-stations-names span[data-now="1"]{color:var(--dsw-alias-label-primary);font-weight:600}
378
+ .clearai-stations-note{margin-top:6px;font-size:12px;color:var(--dsw-alias-state-error-primary)}
379
+ .clearai-tl{position:relative;display:flex;flex-direction:column}
380
+ .clearai-tl::before{content:"";position:absolute;left:4px;top:9px;bottom:9px;width:1px;background:var(--dsw-alias-border-l2)}
381
+ .clearai-tl-row{position:relative;display:grid;grid-template-columns:9px 76px 1fr;gap:10px;align-items:baseline;padding:2px 0;font-size:12.5px}
382
+ .clearai-tl-dot{width:9px;height:9px;border-radius:50%;box-sizing:border-box;align-self:center;background:var(--dsw-alias-label-primary)}
383
+ .clearai-tl-row[data-kind="proposed"] .clearai-tl-dot{background:var(--dsw-alias-bg-base);border:1.4px solid var(--dsw-alias-label-tertiary)}
384
+ .clearai-tl-row[data-verdict="refute"] .clearai-tl-dot{background:var(--dsw-alias-state-error-primary)}
385
+ .clearai-tl-row[data-verdict="inconclusive"] .clearai-tl-dot{background:var(--dsw-alias-bg-base);border:1.4px dashed var(--dsw-alias-label-secondary)}
386
+ .clearai-tl-row[data-kind="promoted"] .clearai-tl-dot{background:var(--dsw-alias-brand-primary)}
387
+ .clearai-tl-row[data-kind="next"]{color:var(--dsw-alias-label-tertiary)}
388
+ .clearai-tl-row[data-kind="next"] .clearai-tl-dot{background:var(--dsw-alias-bg-base);border:1px dashed var(--dsw-alias-label-tertiary)}
389
+ .clearai-tl-time{font-size:11.5px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}
390
+ .clearai-tl-move{font-weight:600}
391
+ .clearai-kv{display:grid;grid-template-columns:44px 1fr;gap:5px 14px;font-size:12.5px;line-height:1.6}
392
+ .clearai-kv>span:nth-child(odd){color:var(--dsw-alias-label-tertiary)}
393
+ .clearai-chips{display:inline-flex;gap:6px;flex-wrap:wrap}
394
+ .clearai-quiet{color:var(--dsw-alias-label-secondary);font-size:12.5px;padding:6px 0}
332
395
  `
333
396
  /** 把上面那段 CSS 挂进页面(带 data-plugin 标记,宿主按包名记账,卸载时收掉)。 */
334
397
  function installStyles() {
335
398
  if (typeof document === 'undefined') return () => {}
336
- const existing = document.querySelector('style[data-plugin="clearai-dsh"]')
399
+ /** 只认自己那一张:图组件的样式表也挂着 data-plugin="clearai-dsh"(data-clearai="xyflow"),按包名查会误以为已经挂过。 */
400
+ const existing = document.querySelector('style[data-plugin="clearai-dsh"][data-clearai="panel"]')
337
401
  if (existing !== null) return () => {}
338
402
  const tag = document.createElement('style')
339
403
  tag.dataset.plugin = 'clearai-dsh'
404
+ tag.dataset.clearai = 'panel'
340
405
  tag.textContent = CSS
341
406
  document.head.append(tag)
342
407
  return () => {
@@ -426,47 +491,16 @@ window.__ModuleLoader__.load({
426
491
  evBasis: { color: 'var(--dsw-alias-label-secondary)', opacity: 0.85, flex: '1 1 auto', minWidth: 0 },
427
492
  fileLink: { color: 'var(--dsw-alias-brand-primary)', cursor: 'pointer', whiteSpace: 'nowrap' },
428
493
  pathFoot: { fontSize: 11, color: 'var(--dsw-alias-label-secondary)', opacity: 0.7, marginTop: 2 },
429
- /** 未走过等级那一行:安静,但**可点**——所以给下划线,颜色用链接令牌(它是一条通道)。 */
430
- levelChannel: { fontSize: 11.5, flex: '0 0 auto', cursor: 'pointer', textDecoration: 'underline', textUnderlineOffset: 2, color: 'var(--dsw-alias-brand-primary)' },
431
- /** 等级说明展开区:与 propBody 同一族的左侧竖线,但不占证明那一栏的位。 */
432
- levelGuide: { display: 'flex', flexDirection: 'column', gap: 6, marginTop: 6, paddingLeft: 10, borderLeft: '2px solid var(--dsw-alias-border-l2)' },
433
- levelRow: { display: 'flex', flexDirection: 'column', gap: 1, fontSize: 11.5 },
434
- levelHead: { fontWeight: 600, color: 'var(--dsw-alias-label-primary)' },
435
494
  /** 世界树页眉的判据小节:逐条带序号;左侧竖线说明它是「目标的一部分」而不是新面板。 */
436
495
  criteria: { display: 'flex', flexDirection: 'column', gap: 1, paddingLeft: 8, marginTop: 2, borderLeft: '2px solid var(--dsw-alias-border-l2)' },
437
496
  criteriaRow: { display: 'flex', gap: 6, alignItems: 'baseline', fontSize: 11.5 },
438
497
  }
439
-
440
- /** 密集列表里用的那一档按钮(11px 圆角)。 */
441
- function Chip(props) {
442
- return h('button', { type: 'button', className: 'clearai-chip', disabled: props.disabled === true, title: props.title, onClick: props.onClick }, props.children)
443
- }
444
498
  /** 可点的文本(替代裸下划线 span)。 */
445
499
  function Link(props) {
446
500
  return h('span', { className: 'clearai-link', title: props.title, onClick: props.onClick }, props.children)
447
501
  }
448
502
 
449
- /**
450
- * 读一个面板自己的 HTTP 响应。
451
- *
452
- * 为什么不能直接 `response.json()`:那条路上失败时返回的是**纯文本**
453
- * (`connection` 层查不到 exact fetch route 就回 `404 "not found"`),
454
- * 直接 parse 会抛出 `Unexpected token 'o', "not found" is not valid JSON`
455
- * ——人看到的是解析器的抱怨,不是发生了什么。
456
- * 这里统一成 `{ ok, status, payload, error }`,`error` 里带上状态码与原文。
457
- */
458
- const readResponse = async (response) => {
459
- const text = await response.text()
460
- let payload = null
461
- try {
462
- payload = text === '' ? null : JSON.parse(text)
463
- } catch {
464
- payload = null
465
- }
466
- if (payload !== null && payload.ok === true) return { ok: true, status: response.status, payload, error: null }
467
- const reason = payload !== null && typeof payload.error === 'string' ? payload.error : `${response.status} ${text.slice(0, 80)}`.trim()
468
- return { ok: false, status: response.status, payload, error: reason }
469
- }
503
+
470
504
 
471
505
  function Tag(props) {
472
506
  return h('span', { style: props.strong === true ? S.tagStrong : S.tag }, props.children)
@@ -514,7 +548,7 @@ window.__ModuleLoader__.load({
514
548
  /**
515
549
  * **目标那一块**(世界树页眉):主张一行 + **判据小节**(逐条带序号)。
516
550
  *
517
- * 判据是**多条**的清点清单(`SetGoal` 收 `criteria: string[]`)⇒ 挤成一句「判据:均值差…」
551
+ * 判据是**多条**的清点清单(`Frame` 收 `criteria: string[]`)⇒ 挤成一句「判据:均值差…」
518
552
  * 会把「第 3 条没做到」抹平。所以逐条列出来,修订史与全文指针各占一行。
519
553
  * 读的是 `knowledgeView` 那一份(与运行态卡**同一份**);拿不到(旧宿主 / 测试桩)时
520
554
  * 退回原来那句摘要——面板不许因为缺一份投影而空白,也不许自己再拼一套判据。
@@ -575,21 +609,6 @@ window.__ModuleLoader__.load({
575
609
  release: t('放行'),
576
610
  }))
577
611
 
578
- /**
579
- * 命题的分组 = **本体声明的五个状态**,顺序即认识论的顺序。
580
- *
581
- * 用词有权威出处:[`docs/verification-loop.md`](../docs/verification-loop.md) 的状态表
582
- * (那份文档明说「其他文档、提示词与代码注释提到这些概念时,以这里的叫法为准」)。
583
- * 界面因此不另造一套产品词——「已确认」的命题不在这一列:它已经**升格为事实**,在事实货架上。
584
- */
585
- const PROPOSITION_GROUPS = lazyTable(() => ([
586
- { status: 'proposed', label: t('已提出') },
587
- { status: 'alive', label: t('验证中') },
588
- { status: 'refuted', label: t('已推翻') },
589
- { status: 'superseded', label: t('已替代') },
590
- { status: 'retracted', label: t('已撤回') },
591
- ]))
592
-
593
612
  /**
594
613
  * 一行够用的摘要。**真数据逼出来的**:评估者的依据常常是几百字的一整段
595
614
  * (逐条对照判据 + 隔离标注),原样内联会把那一行撑到没法看。
@@ -605,9 +624,16 @@ window.__ModuleLoader__.load({
605
624
  const levelRank = (value) => LEVEL_RANK[value] ?? -1
606
625
 
607
626
  /**
608
- * 一条命题的证据。链是**证据 → 步骤 → 命题**(`evidence.stepId` → `step.tests.hypothesis`),
609
- * 与原始设计 `evidenceForHypothesis` 同一条链:证据挂在步骤上,不直接挂在命题上。
627
+ * 一条命题的证据。新账上每条证据写明它针对哪条命题(`evidence.hypothesis`);
628
+ * 旧账没有这一格,走**证据 → 步骤 → 命题**(`evidence.stepId` → `step.tests.hypotheses`)那条链。
610
629
  */
630
+ /** 一个步骤检验的命题 id(新形状 `tests.hypotheses`;旧形状单条 `tests.hypothesis` 照旧认)。 */
631
+ function testedOf(meta) {
632
+ const tests = meta?.tests
633
+ if (Array.isArray(tests?.hypotheses)) return tests.hypotheses
634
+ return typeof tests?.hypothesis === 'string' && tests.hypothesis !== '' ? [tests.hypothesis] : []
635
+ }
636
+
611
637
  function evidenceOf(data, hypothesisId) {
612
638
  /**
613
639
  * 走**跨计划**的步骤索引:命题的验证步常常留在**已收尾的旧计划**里
@@ -617,10 +643,13 @@ window.__ModuleLoader__.load({
617
643
  const index = data?.stepIndex ?? {}
618
644
  const steps = new Set(
619
645
  Object.entries(index)
620
- .filter(([, meta]) => meta?.tests?.hypothesis === hypothesisId)
646
+ .filter(([, meta]) => testedOf(meta).includes(hypothesisId))
621
647
  .map(([stepId]) => stepId),
622
648
  )
623
- return (data?.evidence ?? []).filter((item) => steps.has(item.stepId)).slice().sort((a, b) => (a.at ?? 0) - (b.at ?? 0))
649
+ return (data?.evidence ?? [])
650
+ .filter((item) => (item.hypothesis === null || item.hypothesis === undefined ? steps.has(item.stepId) : item.hypothesis === hypothesisId))
651
+ .slice()
652
+ .sort((a, b) => (a.at ?? 0) - (b.at ?? 0))
624
653
  }
625
654
 
626
655
  /**
@@ -689,68 +718,6 @@ window.__ModuleLoader__.load({
689
718
  return out
690
719
  }
691
720
 
692
- /** 一条证据一句话:`e-1 · L2 · 支持 · 自判 · 依据… · 出处`。 */
693
- function evidenceLine(data, item) {
694
- return h(
695
- 'div',
696
- { key: item.id, style: S.evRow },
697
- h('span', { style: S.evHead }, `${item.id} · ${dash(item.level)} · ${gloss(VERDICT, item.verdict)} · ${gloss(EVALUATOR, item.evaluator)}`),
698
- h('span', { style: S.evBasis, title: String(item.basis ?? '') }, brief(item.basis, 160)),
699
- ...originsOf(data, item).map((origin, index) => {
700
- /**
701
- * 四类入口各自的去处:
702
- * 产物 / 评估卡 → 右侧**原生预览**打开那个文件;
703
- * 看评估者 → 旁观**评估者子会话**(论证过程在里面,harness 原生能力);
704
- * 审批记录 → 没有文件(原生审批对在会话日志里),如实说明它凭什么不可伪造。
705
- */
706
- const open =
707
- origin.kind === 'evaluator-session'
708
- ? origin.session === null || data.openSpectator === undefined
709
- ? undefined
710
- : () => data.openSpectator(origin.session)
711
- : origin.path === null || data.openPreview === undefined
712
- ? undefined
713
- : () => data.openPreview(origin.path)
714
- const title =
715
- origin.kind === 'evaluator-session'
716
- ? t('旁观写这条裁决的评估者子会话(论证过程)')
717
- : origin.kind === 'approval-record'
718
- ? `${t('会话日志里的原生审批对(不可伪造)')}${origin.call === null ? '' : ` · ${origin.call}`}`
719
- : (origin.path ?? '')
720
- return h(
721
- 'span',
722
- {
723
- key: `${item.id}-${origin.label}-${index}`,
724
- style: open === undefined ? S.faint : S.fileLink,
725
- onClick:
726
- open === undefined
727
- ? undefined
728
- : (event) => {
729
- event?.stopPropagation?.()
730
- open()
731
- },
732
- title,
733
- },
734
- origin.label,
735
- )
736
- }),
737
- )
738
- }
739
-
740
- /** 当前处境一句话:这是这一格存在的理由——用户要知道「现在能不能当真」。 */
741
- function whereOf(data, row) {
742
- const evidence = evidenceOf(data, row.id)
743
- const threshold = data?.goal?.promoteAtLevel ?? 'L3'
744
- const refute = evidence.find((item) => item.verdict === 'refute')
745
- if (row.status === 'refuted') return refute === undefined ? t('出现推翻证据(终态,记录保留)') : `${t('被 ')}${refute.id}${t(' 推翻:')}${brief(refute.basis, 56)}`
746
- if (row.status === 'superseded') return t('已被下一版命题替代(版本留着,不参与当前推理)')
747
- if (row.status === 'confirmed') return t('已达门槛,已升格为事实')
748
- if (row.status === 'retracted') return t('人已撤回(记录保留)')
749
- if (row.supportedLevel === null || row.supportedLevel === undefined) return t('暂无证据。先登记判据,后执行验证。')
750
- if (levelRank(row.supportedLevel) >= levelRank(threshold)) return `${t('已达门槛(')}${row.supportedLevel}${t('),等目标验收时升格为事实')}`
751
- return `${t('支持到 ')}${row.supportedLevel}${t(':还差 ')}${threshold}${t('(要独立评估)才达门槛')}`
752
- }
753
-
754
721
  /**
755
722
  * 一条**事实**的证据落在哪一步(纯函数:可单测)。
756
723
  *
@@ -766,717 +733,358 @@ window.__ModuleLoader__.load({
766
733
  return null
767
734
  }
768
735
 
769
- /** 一条事实的证据落在哪份计划里:多份计划各有自己的世界树,跳过去时要切到那一份。 */
770
- function rowPlanOf(data, fact) {
771
- for (const id of fact?.evidenceIds ?? []) {
772
- const item = (data?.evidence ?? []).find((entry) => entry.id === id)
773
- if (typeof item?.planId === 'string' && item.planId !== '') return item.planId
774
- }
775
- return null
776
- }
736
+ /**
737
+ * ═══ 本体格(第六阶段:图为主) ═══
738
+ *
739
+ * 一屏三层,自上而下越来越细:
740
+ * ① 页眉:在回答什么 · 一行计数 · 待处理(只陈述)· 迷你进度轨(判断 → 检验 → 已验证 → 入本体);
741
+ * ② 图:本体图 / 实体图二选一,星图式(点 + 衬线标签;实线已验证、虚线待核验);
742
+ * ③ 结论:一行一条,按可信度分组;点开依次是 进度 → 可信度怎么变的 → 补充。
743
+ *
744
+ * 用词与运行态卡**同一套**(`knowledge-view.js` 的 TRUST / VERDICT_WORD / LEVEL_WORD):
745
+ * 单次检验的结果是 支持 / 推翻 / 不确定;一条结论的状态是 已验证 / 待核验 / 验证中 / 不确定 / 已推翻 / 已替换。
746
+ * 内部编号(h-… / e-… / s-…)不上屏:人认的是短名和「第几步」。
747
+ */
748
+ const TRUST_ORDER = ['credible', 'pending', 'testing', 'unclear', 'refuted', 'replaced']
749
+ const TRUST_WORD = lazyTable(() => ({ credible: t('已验证'), pending: t('待核验'), testing: t('验证中'), unclear: t('不确定'), refuted: t('已推翻'), replaced: t('已替换') }))
750
+ const LEVEL_WORD = lazyTable(() => ({ L0: t('自己推了一遍'), L1: t('引用已有材料'), L2: t('可复算'), L3: t('独立核验'), L4: t('人放行') }))
751
+ /** 进度轨的四站。顺序即一条结论要走的路。 */
752
+ const STATIONS = () => [t('判断'), t('检验'), t('已验证'), t('入本体')]
753
+ /** 还没走完时,下一步是什么(时间线末尾那条虚点)。 */
754
+ const NEXT_STEP = lazyTable(() => ({
755
+ testing: t('下一步:找一步去检验它'),
756
+ pending: t('下一步:独立核验通过 → 已验证'),
757
+ unclear: t('下一步:换个办法再检验'),
758
+ credible: t('下一步:结案时写进长期知识'),
759
+ }))
760
+ /** 值的形态(本体图上的小方块):内部词换成人话。 */
761
+ const FORM_WORD = lazyTable(() => ({ statement: t('一句话'), quantity: t('数值'), formula: t('公式'), code: t('代码'), reference: t('引用') }))
762
+ const KIND_WORD = lazyTable(() => ({ concept: t('概念'), value_type: t('值的形态'), instance: t('实例'), literal: t('取值'), edge: t('关系') }))
777
763
 
778
- /** 右侧那一列:最有力的一条证据给出的**等级 · 判者**——这就是「多可信」。 */
779
- function judgeOf(data, row) {
780
- const evidence = evidenceOf(data, row.id)
781
- const best = evidence.slice().sort((a, b) => levelRank(b.level) - levelRank(a.level))[0]
782
- return best === undefined ? '—' : `${dash(best.level)} · ${gloss(EVALUATOR, best.evaluator)}`
783
- }
764
+ /** 一步检验是怎么做的:独立核验直接说;自己做的,说到了哪一级(可复算 / 引用材料 / 推了一遍)。 */
765
+ const howOf = (row) => (row.evaluator === 'independent' ? t('独立核验') : (LEVEL_WORD[row.level] ?? t('自己检验')))
784
766
 
785
- /** 实际**走过**的转移(不画未走的岔路:那是过程知识,不是「我现在能信什么」)。 */
786
- function transitionsOf(data, row) {
787
- const evidence = evidenceOf(data, row.id)
788
- const out = []
789
- /**
790
- * `to` 一律用**声明里的原始状态键**(`alive` / `refuted` …),不做翻译——
791
- * 翻译只在渲染时做。混用两套键是踩过的坑:流转图按原始键查「这一跳走没走过」,
792
- * 而这里返回的是中文字面 ⇒ 命不中 ⇒ 走过的边全被画成"没走"。
793
- */
794
- if (evidence.length > 0) {
795
- const first = evidence[0]
796
- out.push({ to: 'alive', on: t('已获首条证据'), by: `${first.id} · ${dash(first.level)} ${gloss(VERDICT, first.verdict)}`, independent: first.evaluator === 'independent' })
767
+ /**
768
+ * **结论行的模型**(纯函数,导出给测试)。
769
+ *
770
+ * 一条判断一行;旧账本里没有判断关联的事实也各占一行(它们已经写进长期知识,只是不知道出自哪条判断)。
771
+ * 站位:写进长期知识 = 4,已验证 = 3,检验过 = 2,只是提出 = 1;被推翻的停在「检验」那一站。
772
+ */
773
+ function conclusionsOf(data) {
774
+ const facts = Array.isArray(data?.facts) ? data.facts : []
775
+ const ordinalOf = new Map()
776
+ for (const plan of [...(Array.isArray(data?.plans) ? data.plans : []), ...(data?.plan ? [data.plan] : [])]) {
777
+ for (const step of plan?.steps ?? []) if (typeof step?.id === 'string') ordinalOf.set(step.id, step.ordinal ?? null)
778
+ }
779
+ const testingStep = (id) => {
780
+ for (const [stepId, meta] of Object.entries(data?.stepIndex ?? {})) if (testedOf(meta).includes(id)) return ordinalOf.get(stepId) ?? null
781
+ return null
797
782
  }
798
- if (row.status === 'refuted') {
799
- const refute = evidence.find((item) => item.verdict === 'refute')
800
- out.push({
801
- to: 'refuted',
802
- on: t('出现推翻证据'),
803
- by: refute === undefined ? '—' : `${refute.id} · ${gloss(EVALUATOR, refute.evaluator)}`,
804
- independent: refute !== undefined && refute.evaluator === 'independent',
783
+ const linked = new Set()
784
+ const rows = (Array.isArray(data?.goal?.hypotheses) ? data.goal.hypotheses : []).map((hypothesis) => {
785
+ const history = Array.isArray(hypothesis.history) ? hypothesis.history : []
786
+ const fact = facts.find((item) => item.hypothesis === hypothesis.id) ?? null
787
+ if (fact !== null) linked.add(fact.id)
788
+ const lastEvidence = [...history].reverse().find((row) => row.kind === 'evidence') ?? null
789
+ const promoted = fact !== null && fact.review?.decision !== 'retracted'
790
+ const trust = TRUST_ORDER.includes(hypothesis.trust) ? hypothesis.trust : 'testing'
791
+ const station = trust === 'refuted' ? 2 : promoted ? 4 : trust === 'credible' ? 3 : lastEvidence !== null ? 2 : 1
792
+ return {
793
+ key: hypothesis.id,
794
+ hypothesis: hypothesis.id,
795
+ name: String(hypothesis.name ?? ''),
796
+ claim: String(hypothesis.claim ?? ''),
797
+ trust,
798
+ station,
799
+ history,
800
+ refuteWhen: hypothesis.refuteWhen ?? null,
801
+ scope: fact?.scope ?? hypothesis.fact?.scope ?? null,
802
+ basis: lastEvidence?.basis ?? null,
803
+ evidenceId: lastEvidence?.id ?? null,
804
+ step: lastEvidence?.ordinal ?? testingStep(hypothesis.id),
805
+ assertions: fact?.assertions ?? hypothesis.assertions ?? null,
806
+ fact: fact?.id ?? null,
807
+ }
808
+ })
809
+ for (const fact of facts) {
810
+ if (linked.has(fact.id)) continue
811
+ const trust = fact.review?.decision === 'retracted' ? 'replaced' : fact.refuted === true ? 'refuted' : 'credible'
812
+ rows.push({
813
+ key: fact.id,
814
+ hypothesis: null,
815
+ name: '',
816
+ claim: String(fact.text ?? ''),
817
+ trust,
818
+ station: trust === 'credible' ? 4 : 2,
819
+ history: [{ kind: 'promoted', at: fact.at ?? null, level: fact.level ?? null, to: 'credible' }],
820
+ refuteWhen: null,
821
+ scope: fact.scope ?? null,
822
+ basis: null,
823
+ evidenceId: (fact.evidenceIds ?? [])[0] ?? null,
824
+ step: null,
825
+ assertions: fact.assertions ?? null,
826
+ fact: fact.id,
805
827
  })
806
828
  }
807
- if (row.status === 'superseded') out.push({ to: 'superseded', on: t('被下一版命题改写'), by: row.version > 1 ? `v${row.version}` : '—', independent: false })
808
- if (row.status === 'retracted') out.push({ to: 'retracted', on: t('人审查后决定撤回'), by: '—', independent: false })
809
- return out
829
+ return rows
810
830
  }
811
831
 
812
- /**
813
- * 流转图的**固定布局**(产品面,不是通用图布局器)。
814
- *
815
- * 主干一行(左→右):已提出 → 验证中 → 已确认;终态分支挂在下面一行。
816
- * 这样无论声明有没有下发、有几条边,**横向主干都在** —— 不会退化成竖排。
817
- * 声明下发后,边的 `on` / `actor` 从声明取(未走的边据此写清"这件事叫什么、谁发起");
818
- * 声明还没下来的旧会话,用规范闭环的安全镜像,并在标题里如实标注。
819
- */
820
- const FLOW_LAYOUT = {
821
- nodeW: 112,
822
- nodeH: 30,
823
- width: 600,
824
- height: 152,
825
- trunkY: 20,
826
- branchY: 100,
827
- place: {
828
- proposed: { x: 16, y: 20 },
829
- alive: { x: 166, y: 20 },
830
- confirmed: { x: 316, y: 20 },
831
- refuted: { x: 166, y: 100 },
832
- superseded: { x: 316, y: 100 },
833
- retracted: { x: 466, y: 100 },
834
- },
835
- /** 声明缺失时的规范镜像:只用来画图,标题会说明它不是实时声明。 */
836
- fallbackEdges: [
837
- ['proposed', 'alive', 'system', 'evidence_appended'],
838
- ['alive', 'confirmed', 'system', 'promotion_threshold'],
839
- ['alive', 'refuted', 'system', 'refuting_evidence'],
840
- ['proposed', 'superseded', 'model', 'hypothesis_revised'],
841
- ['confirmed', 'retracted', 'human', 'retraction_request'],
842
- ],
832
+ /** 短名与主张重复时只说一遍(没起短名的旧判断,短名就是主张的开头)。 */
833
+ const titleOf = (row) => {
834
+ const name = row.name.trim()
835
+ if (name === '' || name.endsWith('…') || row.claim.startsWith(name)) return { title: brief(row.claim, 40), sub: null }
836
+ return { title: name, sub: brief(row.claim, 60) }
843
837
  }
844
838
 
845
- /**
846
- * **流转图**:横向主干 + 分支的 SVG 状态机。
847
- *
848
- * 三条着色规矩(与世界树同一套视觉语言):
849
- * · **走过的边**:实线 + 颜色(独立裁决用品牌强调色、自判用前景色),旁边写清触发与凭据;
850
- * · **当前态**:品牌色描边 + 呼吸光环,并在节点上写「当前」;
851
- * · **没走的边**:虚线 + 低饱和,写明「这件事 · 谁发起」。
852
- */
853
- function StateFlow(props) {
854
- const { data, row } = props
855
- const spec = (data?.ontology?.objects ?? []).find((object) => object.name === 'hypothesis') ?? null
856
- const declared = Array.isArray(spec?.edges) && spec.edges.length > 0 ? spec.edges : null
857
- const edges = declared ?? FLOW_LAYOUT.fallbackEdges
858
- const path = transitionsOf(data, row)
859
- const taken = new Map(path.map((step) => [step.to, step]))
860
- const current = row.status
861
- const L = FLOW_LAYOUT
862
- const items = []
863
- for (const [from, to, actor, on] of edges) {
864
- const a = L.place[from]
865
- const b = L.place[to]
866
- if (a === undefined || b === undefined || from === to) continue
867
- const step = taken.get(to)
868
- const done = step !== undefined
869
- const color = done ? (step.independent === true ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-label-primary)') : 'var(--dsw-alias-border-l2)'
870
- const horizontal = a.y === b.y && b.x > a.x
871
- const x1 = horizontal ? a.x + L.nodeW : a.x + L.nodeW / 2
872
- const y1 = horizontal ? a.y + L.nodeH / 2 : a.y + L.nodeH
873
- const x2 = horizontal ? b.x : b.x + L.nodeW / 2
874
- const y2 = horizontal ? b.y + L.nodeH / 2 : b.y
875
- const midY = (y1 + y2) / 2
876
- items.push(
877
- h(
878
- 'path',
879
- {
880
- key: `edge-${from}-${to}`,
881
- d: horizontal ? `M ${x1} ${y1} H ${x2}` : `M ${x1} ${y1} V ${midY} H ${x2} V ${y2}`,
882
- fill: 'none',
883
- stroke: color,
884
- strokeWidth: done ? 2 : 1,
885
- strokeDasharray: done ? undefined : '4 3',
886
- },
887
- h(
888
- 'title',
889
- null,
890
- done ? `${step.on} · ${step.by}` : `${t('未走:')}${on ?? to} · ${actor ?? '?'}${t(' 发起')}`,
891
- ),
892
- ),
893
- )
894
- const label = done ? `${step.by.split(' · ')[0]} · ${step.by.split(' · ')[1] ?? ''}`.trim() : ''
895
- if (label !== '') {
896
- items.push(
897
- h(
898
- 'text',
899
- {
900
- key: `label-${from}-${to}`,
901
- x: horizontal ? (x1 + x2) / 2 : x2 + 5,
902
- y: horizontal ? y1 - 7 : midY - 4,
903
- textAnchor: horizontal ? 'middle' : 'start',
904
- fontSize: 9.5,
905
- fill: color,
906
- },
907
- label,
908
- ),
909
- )
910
- }
911
- }
912
- for (const [state, position] of Object.entries(L.place)) {
913
- const reached = state === 'proposed' || taken.has(state)
914
- const isNow = state === current
915
- items.push(
916
- h('rect', {
917
- key: `node-${state}`,
918
- x: position.x,
919
- y: position.y,
920
- width: L.nodeW,
921
- height: L.nodeH,
922
- rx: L.nodeH / 2,
923
- fill: isNow ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
924
- stroke: isNow ? 'var(--dsw-alias-brand-primary)' : reached ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-border-l2)',
925
- strokeWidth: isNow ? 2 : 1,
926
- strokeDasharray: reached || isNow ? undefined : '4 3',
927
- className: isNow ? 'clearai-breathe-stroke' : undefined,
928
- }),
929
- )
930
- items.push(
931
- h(
932
- 'text',
933
- {
934
- key: `text-${state}`,
935
- x: position.x + L.nodeW / 2,
936
- y: position.y + L.nodeH / 2 + 4,
937
- textAnchor: 'middle',
938
- fontSize: 11,
939
- fill: isNow ? 'var(--dsw-alias-brand-primary)' : reached ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-secondary)',
940
- },
941
- `${gloss(HYPOTHESIS, state)}${isNow ? ' · 当前' : ''}`,
942
- ),
943
- )
944
- }
945
- /**
946
- * 图下那几行**来路**:图上只标走过那一跳的证据 id(窄间距里读得清),
947
- * 完整的「这件事叫什么 + 谁做的」写在这里 —— 图管结构,文字管凭据,各占各的地方。
948
- */
949
- const walked = path.filter((step) => step.to !== 'retracted' || current === 'retracted')
839
+ /** 状态小签:实心 = 已验证,描边 = 待核验,淡描边 = 验证中,虚边 = 不确定,红 = 已推翻,删除线 = 已替换。 */
840
+ function TrustTag(props) {
841
+ return h('span', { className: 'clearai-trust', 'data-trust': props.trust }, TRUST_WORD[props.trust] ?? props.trust)
842
+ }
843
+
844
+ /** 四站进度条:走过的实心,在路上的那一段斜纹,当前站名加粗。 */
845
+ function StationBar(props) {
846
+ const reached = props.station
847
+ const stopped = props.trust === 'refuted' || props.trust === 'replaced'
848
+ const names = STATIONS()
950
849
  return h(
951
850
  'div',
952
- { style: { margin: '4px 0 8px', maxWidth: '100%' } },
851
+ { className: 'clearai-stations', 'data-stopped': stopped ? '1' : '0' },
953
852
  h(
954
853
  'div',
955
- { style: { overflowX: 'auto' } },
956
- h(
957
- 'svg',
958
- { width: L.width, height: L.height, viewBox: `0 0 ${L.width} ${L.height}`, style: { display: 'block', minWidth: L.width } },
959
- ...items,
960
- ),
854
+ { className: 'clearai-stations-bar' },
855
+ ...names.map((_, index) => h('span', { key: `seg-${index}`, 'data-state': index < reached ? (stopped && index === reached - 1 ? 'broken' : 'done') : index === reached && !stopped ? 'next' : 'todo' })),
961
856
  ),
962
- walked.length === 0
963
- ? null
964
- : h(
965
- 'div',
966
- { style: { display: 'flex', flexDirection: 'column', gap: 1, marginTop: 2 } },
967
- ...walked.map((step) =>
968
- h(
969
- 'div',
970
- { key: `walked-${step.to}`, style: { fontSize: 11, color: 'var(--dsw-alias-label-secondary)' } },
971
- `${step.on} → ${gloss(HYPOTHESIS, step.to)} · ${step.by}`,
972
- ),
973
- ),
974
- ),
975
- declared === null
976
- ? h('div', { style: { ...S.faint, fontSize: 11, marginTop: 2 } }, t('本体声明尚未进入投影;当前显示规范闭环的镜像,下一拍自动对齐。'))
977
- : null,
857
+ h('div', { className: 'clearai-stations-names' }, ...names.map((name, index) => h('span', { key: `name-${index}`, 'data-now': index === reached - 1 ? '1' : '0' }, name))),
858
+ stopped ? h('div', { className: 'clearai-stations-note' }, props.trust === 'refuted' ? t('在检验这一站被推翻,不再往下走。') : t('已被替换,不再往下走。')) : null,
978
859
  )
979
860
  }
980
861
 
981
- /**
982
- * 一条断言里的人与物:**等级说明要点名「这一级要检查的对象」**,名字就从这里来。
983
- * 只做展示用拼装,不重算账本(断言在账本里已经是定型的形状)。
984
- */
985
- const assertionSubjectName = (assertion) => {
986
- const subject = assertion?.subject ?? {}
987
- const id = String(subject.id ?? '')
988
- if (id === '') return ''
989
- const type = String(subject.type ?? '')
990
- return type === '' ? id : `${type}|${id}`
991
- }
992
- const assertionObjectName = (object) => {
993
- if (object === null || object === undefined) return ''
994
- const value = String(object.value ?? '')
995
- if (object.kind !== 'quantity') return value
996
- const unit = String(object.unit ?? '')
997
- return unit === '' ? value : `${value} ${unit}`
998
- }
999
- const assertionName = (assertion) => {
1000
- const chip = String(assertion?.chip ?? '')
1001
- if (chip !== '') return chip
1002
- const subject = assertionSubjectName(assertion)
1003
- const predicate = String(assertion?.predicate ?? '')
1004
- const object = assertionObjectName(assertion?.object)
1005
- return `${subject} · ${predicate} = ${object}`
1006
- }
1007
-
1008
- /**
1009
- * 等级说明**只有一个来源**:折法侧经宿主投影下发的 `knowledgeView`;
1010
- * 它的底稿是 `preset/plugins/prompts.js` 里那五级说明。
1011
- * 客户端**绝不手抄第二份**——两处各写一套,漂移是迟早的事。
1012
- * 取不到就返回 null,由调用点如实说「读不到等级说明」。
1013
- */
1014
- const levelTableOf = (data) => {
1015
- const view = data?.knowledgeView ?? null
1016
- const table = view?.levels ?? view?.glossary ?? null
1017
- return table !== null && typeof table === 'object' ? table : null
1018
- }
1019
-
1020
- /**
1021
- * **等级通道**:未走过的等级不再是一句陈述,而是一行**可点**的东西。
1022
- *
1023
- * 点开看到的是 `ExplainLevelSkip` 要交的三件:
1024
- * ① 这一级要检查的对象(这条命题自己的断言主体——reason 里必须点到它们);
1025
- * ② reason 该怎么写(以及模板);
1026
- * ③ 这一级在这一档里可能不适用的理由从哪来说(等级说明的 `plain` / `where`)。
1027
- *
1028
- * 「跳过」本身不违规,所以这里**不劝、不拦**:只把要交的东西摆出来。
1029
- */
1030
- const LevelGuide = ({ row, data }) => {
1031
- const untouched = Array.isArray(row?.untouchedLevels) ? row.untouchedLevels : []
1032
- const table = levelTableOf(data)
1033
- const objects = (Array.isArray(row?.assertions) ? row.assertions : []).map(assertionName).filter((text) => text !== '')
1034
- const hypothesis = String(row?.id ?? '')
862
+ /** 可信度怎么变的:一笔一行,变了的那一笔写出「之前 → 之后」。 */
863
+ function TrustTimeline(props) {
864
+ const rows = props.history
865
+ const lines = rows.map((row, index) => {
866
+ let text = ''
867
+ if (row.kind === 'proposed') text = t('提出')
868
+ else if (row.kind === 'evidence') text = `${row.ordinal === null || row.ordinal === undefined ? t('一步检验') : `${nth(row.ordinal)}`}${t(':')}${VERDICT[row.verdict] ?? row.verdict} · ${howOf(row)}`
869
+ else if (row.kind === 'promoted') text = t('写进长期知识')
870
+ else if (row.kind === 'retracted') text = `${t('人撤回')}${row.reason ? `:${brief(row.reason, 40)}` : ''}`
871
+ const moved = row.from !== undefined && row.from !== row.to
872
+ return h(
873
+ 'div',
874
+ { key: `tl-${index}`, className: 'clearai-tl-row', 'data-kind': row.kind, 'data-verdict': row.verdict ?? '' },
875
+ h('span', { className: 'clearai-tl-dot' }),
876
+ h('span', { className: 'clearai-tl-time' }, when(row.at)),
877
+ h('span', null, text, moved ? h('b', { className: 'clearai-tl-move' }, ` · ${TRUST_WORD[row.from] ?? row.from} → ${TRUST_WORD[row.to] ?? row.to}`) : null),
878
+ )
879
+ })
880
+ const next = props.station >= 4 || props.trust === 'refuted' || props.trust === 'replaced' ? null : (NEXT_STEP[props.trust] ?? null)
881
+ if (next !== null) lines.push(h('div', { key: 'tl-next', className: 'clearai-tl-row', 'data-kind': 'next' }, h('span', { className: 'clearai-tl-dot' }), h('span', { className: 'clearai-tl-time' }, ''), h('span', null, next)))
882
+ return h('div', { className: 'clearai-tl' }, ...lines)
883
+ }
884
+
885
+ /** 点开一条:进度 → 可信度怎么变的 → 补充,依次往下。 */
886
+ function ConclusionDetail(props) {
887
+ const { row, data } = props
888
+ const evidence = row.evidenceId === null ? null : ((data?.evidence ?? []).find((item) => item.id === row.evidenceId) ?? null)
889
+ const origins = evidence === null ? [] : originsOf(data, evidence)
890
+ const subjects = [...new Set((Array.isArray(row.assertions) ? row.assertions : []).map((assertion) => String(assertion?.subject?.id ?? '')).filter((id) => id !== ''))]
891
+ const part = (index, title, body) => h('div', { className: 'clearai-part' }, h('div', { className: 'clearai-part-head' }, h('b', null, String(index)), title), body)
892
+ const kv = (label, value) => (value === null || value === undefined || value === '' ? [] : [h('span', { key: `k-${label}` }, label), h('span', { key: `v-${label}` }, value)])
1035
893
  return h(
1036
894
  'div',
1037
- { style: S.levelGuide },
1038
- ...untouched.map((level) => {
1039
- const info = table === null ? null : table[level] ?? null
1040
- const plain = info === null ? null : String(info.plain ?? '')
1041
- const where = info === null ? null : String(info.where ?? '')
1042
- const nextAction = info === null ? null : String(info.nextAction ?? '')
1043
- return h(
895
+ { className: 'clearai-detail' },
896
+ part(1, t('进度'), h(StationBar, { station: row.station, trust: row.trust })),
897
+ part(2, t('可信度怎么变的'), h(TrustTimeline, { history: row.history, station: row.station, trust: row.trust })),
898
+ part(
899
+ 3,
900
+ t('补充'),
901
+ h(
1044
902
  'div',
1045
- { key: level, style: S.levelRow },
1046
- h('div', { style: S.levelHead }, `${level} · ${plain === null || plain === '' ? t('读不到等级说明') : t(plain)}`),
1047
- where === null || where === '' ? null : h('div', { style: S.faint }, `${t('这一级看什么:')}${t(where)}`),
1048
- nextAction === null || nextAction === '' ? null : h('div', { style: S.faint }, `${t('这一级要交什么:')}${t(nextAction)}`),
1049
- h(
1050
- 'div',
1051
- { style: S.faint },
1052
- `${t('这一级要检查的对象:')}${objects.length === 0 ? t('这条命题还没写明断言主体:先用 RegisterInstance 把实例连出处登记下来。') : objects.join(';')}`,
903
+ { className: 'clearai-kv' },
904
+ ...kv(
905
+ t('依据'),
906
+ row.basis === null
907
+ ? null
908
+ : h(
909
+ 'span',
910
+ null,
911
+ h('span', { title: String(row.basis) }, brief(row.basis, 120)),
912
+ ...origins
913
+ .filter((origin) => origin.path !== null || origin.session !== null)
914
+ .slice(0, 2)
915
+ .map((origin, index) =>
916
+ h(
917
+ 'span',
918
+ {
919
+ key: `o-${index}`,
920
+ className: 'clearai-link',
921
+ style: { marginLeft: 8 },
922
+ onClick: () => (origin.session !== null && origin.kind === 'evaluator-session' ? data.openSpectator?.(origin.session) : data.openPreview?.(origin.path)),
923
+ title: origin.path ?? '',
924
+ },
925
+ origin.kind === 'audit-card' || origin.kind === 'evaluator-session' ? t('看核验') : t('看记录'),
926
+ ),
927
+ ),
928
+ ),
1053
929
  ),
1054
- h('div', { style: S.faint }, `${t('reason 怎么写:')}${t('reason 里必须点到上面这些对象名,不能写「时间不够」。')}`),
1055
- h('div', { style: S.faint }, t('跳级本身不违规:要交的是「这一级为什么不适用」的理由,不是这一级的读数。')),
1056
- /**
1057
- * 模板本身是**代码**,不翻译;它前面那句话是人话,走 t()。
1058
- * (verb 名与字段名是模型的接口,换语言也不该换。)
1059
- */
1060
- h('div', { style: S.faint }, t('交给模型的写法:'), h('span', { style: S.mono }, ` ExplainLevelSkip { hypothesis: '${hypothesis}', levels: ['${level}'], reason: '…' }`)),
1061
- )
1062
- }),
930
+ ...kv(t('范围'), row.scope === row.refuteWhen ? null : row.scope),
931
+ ...kv(t('算错'), row.refuteWhen),
932
+ ...kv(
933
+ t('相关'),
934
+ subjects.length === 0
935
+ ? null
936
+ : h('span', { className: 'clearai-chips' }, ...subjects.map((id) => h('span', { key: id, className: 'clearai-chip', onClick: () => props.onFocus?.(id), title: t('在图上找它') }, labelOfEntity(data, id)))),
937
+ ),
938
+ ...kv(t('出自'), row.step === null ? null : `${nth(row.step)}`),
939
+ ),
940
+ ),
1063
941
  )
1064
942
  }
1065
943
 
1066
- /**
1067
- * **命题行**:一行一条命题,默认只有主张 + 当前处境 + 等级判者。
1068
- * 点开才出现「凭什么」——流转图 + 每条证据的出处。
1069
- */
1070
- function PropositionRow(props) {
1071
- const { data, row, open, onToggle } = props
1072
- const evidence = evidenceOf(data, row.id)
1073
- const path = transitionsOf(data, row)
1074
- const untouched = Array.isArray(row.untouchedLevels) ? row.untouchedLevels : []
1075
- /**
1076
- * 等级通道的展开是**界面状态**(点了哪一条,不进账本)。
1077
- * `levelsOpen` 是给测试缝的显式覆盖:真 React 里点不动的地方,测试要能直接渲染展开态。
1078
- */
1079
- const [levelsShown, setLevelsShown] = React.useState(false)
1080
- const levelsOpen = props.levelsOpen ?? levelsShown
944
+ /** 实例在图上的名字(登记时给的 label);图上没有它就退回 id。 */
945
+ const labelOfEntity = (data, id) => String((data?.lexicon?.graph?.nodes ?? []).find((node) => node.layer === 'entity' && String(node.ref ?? '') === id)?.label ?? id)
946
+
947
+ /** 一条结论一行:状态签 + 短名(+ 主张)+ 出自第几步。 */
948
+ function ConclusionRow(props) {
949
+ const { row, open } = props
950
+ const { title, sub } = titleOf(row)
1081
951
  return h(
1082
952
  'div',
1083
- { style: open === true ? S.propOpen : S.propRow },
953
+ { className: 'clearai-row', 'data-open': open ? '1' : '0', 'data-trust': row.trust },
1084
954
  h(
1085
955
  'div',
1086
- { style: S.propHead, onClick: onToggle },
1087
- h('span', { style: { ...S.propClaim, ...(row.status === 'refuted' || row.status === 'superseded' ? S.stale : {}) } }, dash(row.claim)),
1088
- h('span', { style: S.propWhere }, whereOf(data, row)),
1089
- h('span', { style: S.propJudge }, `${gloss(HYPOTHESIS, row.status)} · ${judgeOf(data, row)}`),
1090
- /**
1091
- * **从没被走过的等级**:等级越往上,系统补的独立性越多(独立裁决、人放行),
1092
- * 所以「直接跳到高等级」这件事本身不违规(首次测量没有廉价路),但必须看得见——
1093
- * 与「这条假设从没被证据碰过」标成「未触及」同一条规矩。
1094
- *
1095
- * 它同时是**通道**,不是陈述:点开就是 `ExplainLevelSkip` 要交的东西
1096
- * (要检查哪些对象、reason 怎么写、这一级为什么可能不适用)。
1097
- * 只说「尚无证据」而不给出口,读者只会知道欠账、不知道还法。
1098
- */
1099
- untouched.length === 0
1100
- ? null
1101
- : h(
1102
- 'span',
1103
- {
1104
- style: S.levelChannel,
1105
- title: t('这些等级尚无证据:跳级不违规,但需说明原因'),
1106
- onClick: (event) => {
1107
- event?.stopPropagation?.()
1108
- setLevelsShown(levelsOpen !== true)
1109
- },
1110
- },
1111
- `${t('未走过 ')}${untouched.join('/')} · ${levelsOpen === true ? t('收起这一级要交的东西') : t('怎么补这一级?')}`,
1112
- ),
956
+ { className: 'clearai-row-head', onClick: () => props.onToggle(row.key), title: open ? t('收起') : t('点开看进度和来历') },
957
+ h(TrustTag, { trust: row.trust }),
958
+ h('span', { className: 'clearai-row-title' }, title),
959
+ sub === null ? null : h('span', { className: 'clearai-row-sub' }, sub),
960
+ h('span', { className: 'clearai-row-step' }, row.step === null ? '' : `${nth(row.step)}`),
1113
961
  ),
1114
- h(AssertionChips, { assertions: row.assertions, ui: data.assertionUI, promoted: false }),
1115
- levelsOpen === true ? h(LevelGuide, { row, data }) : null,
1116
- open === true
1117
- ? h(
1118
- 'div',
1119
- { style: S.propBody },
1120
- h(StateFlow, { data, row }),
1121
- evidence.length === 0 ? null : h('div', { style: S.evList }, ...evidence.map((item) => evidenceLine(data, item))),
1122
- /**
1123
- * 图上那一跳的完整凭据(触发 + 判者 + 两个可点入口)。
1124
- * 这里**不再**摆本体文件路径:那是文档坐标,不是面板上的动作 ——
1125
- * 「这句话不用了」——人拍板删的。
1126
- */
1127
- evidence.length === 0
1128
- ? null
1129
- : h(
1130
- 'div',
1131
- { style: S.pathFoot },
1132
- h(
1133
- Link,
1134
- {
1135
- onClick: () => {
1136
- const last = evidence[evidence.length - 1]
1137
- treeFocus.set({ plan: last.planId ?? null, step: last.stepId, branch: last.branch ?? null })
1138
- if (data.openRail !== undefined) data.openRail('clearai-worldtree')
1139
- },
1140
- title: t('打开世界树并选中产生这条证据的验证步'),
1141
- },
1142
- t('在世界树中查看此步骤'),
1143
- ),
1144
- ),
1145
- )
1146
- : null,
1147
- )
1148
- }
1149
-
1150
- /** **命题货架**:按本体状态分组;空组不出现。已确认的不在这里(它已升格为事实)。 */
1151
- function PropositionShelf(props) {
1152
- /**
1153
- * 打开器**自己接**(不靠外层注入):谁渲染这个货架,点证据/点事实都能开原件。
1154
- * 踩过的坑:只有最外层 `Facts` 注入 `openPreview` 时,单独渲染货架(测试缝、
1155
- * 以后的复用)会让整排出处看着能点、点了没反应。
1156
- */
1157
- /**
1158
- * `props.X ?? 已有值`:外层 `Facts` 只传 `{data}`,而打开器在 `data` 里 ——
1159
- * 直接写 `openRail: props.openRail` 会用 `undefined` 把它盖掉 ⇒ 链接永不渲染 ✗(踩过的坑)。
1160
- */
1161
- const base = props.data ?? (typeof props.useProjection === 'function' ? props.useProjection('clearai') : undefined) ?? {}
1162
- const data = { ...base, openPreview: props.openPreview ?? base.openPreview, openRail: props.openRail ?? base.openRail, openSpectator: props.openSpectator ?? base.openSpectator }
1163
- const rows = (data.goal?.hypotheses ?? []).filter((row) => row.status !== 'confirmed')
1164
- const groups = PROPOSITION_GROUPS.map((group) => ({ ...group, rows: rows.filter((row) => row.status === group.status) })).filter((group) => group.rows.length > 0)
1165
- return h(
1166
- Section,
1167
- { title: `${t('命题 · ')}${rows.length}` },
1168
- rows.length === 0
1169
- ? h('div', { style: S.faint }, t('暂无命题。每条需一句主张与一句推翻条件;通过验证后升格为事实。'))
1170
- : h(
1171
- 'div',
1172
- null,
1173
- ...groups.map((group) =>
1174
- h(
1175
- 'div',
1176
- { key: group.status },
1177
- h('div', { style: S.group }, group.label, h('span', { style: S.groupN }, String(group.rows.length))),
1178
- ...group.rows.map((row) => h(PropositionRow, { key: row.id, data, row, open: props.open === row.id, onToggle: () => props.onToggle(row.id) })),
1179
- ),
1180
- ),
1181
- ),
962
+ open ? h(ConclusionDetail, { row, data: props.data, onFocus: props.onFocus }) : null,
1182
963
  )
1183
964
  }
1184
965
 
1185
- /**
1186
- * 一条缺口的**去处**(没有就返回 null,由行里如实只写「下一步」)。
1187
- *
1188
- * 「可点则跳」不是每条都能跳:缺口是**账本自己算出来的欠账**,有的欠在模型那一侧
1189
- * (要用哪个动词补),有的欠在本面板别处。能跳到具体去处的才挂链接;
1190
- * 跳不到的**不假装可点**——那会教人学会「点了没反应」。
1191
- */
1192
- const gapTarget = (code, data, onShowEntity) => {
1193
- if (code === 'entities_unlanded' && typeof onShowEntity === 'function') return { label: t('看实体图'), title: t('切到实体图那一层,看有哪些实例节点'), run: onShowEntity }
1194
- if ((code === 'no_language' || code === 'orphan_terms') && typeof data?.openPreview === 'function') return { label: t('打开词汇货架(原生预览)'), title: t('原生预览打开它'), run: () => data.openPreview('clear/ontology/domain.md') }
1195
- return null
1196
- }
1197
-
1198
- /**
1199
- * **缺口行**:「结构完整」这类结论之外,这一栏把**欠账**摆出来。
1200
- *
1201
- * 每条 = code 的人话 + 计数 + 现状(折法侧写好的 `detail`)+ 下一步(`nextAction`),
1202
- * 能跳的去处挂成链接。缺口的算法在折法侧(账本是唯一生产者),这里**不重算**——
1203
- * 客户端自己再算一套,两边迟早各说各话。
1204
- */
1205
- const GapShelf = ({ data, onShowEntity }) => {
1206
- const gaps = Array.isArray(data?.knowledge?.gaps) ? data.knowledge.gaps : []
1207
- if (gaps.length === 0) return null
966
+ /** 结论列表:按可信度分组,空组不出现。 */
967
+ function ConclusionList(props) {
968
+ const rows = props.rows
969
+ if (rows.length === 0) return h('div', { className: 'clearai-quiet' }, t('还没有结论。模型立下判断之后,这里一行一条。'))
1208
970
  return h(
1209
- Section,
1210
- { title: `${t('缺口')} ${gaps.length}` },
1211
- ...gaps.map((gap, index) => {
1212
- const code = String(gap?.code ?? '')
1213
- const label = GAP_CODES.includes(code) ? GAP_LABEL[code] : null
1214
- const count = typeof gap?.count === 'number' ? gap.count : null
1215
- const target = gapTarget(code, data, onShowEntity)
1216
- const detail = String(gap?.detail ?? '')
1217
- const nextAction = String(gap?.nextAction ?? '')
971
+ 'div',
972
+ { className: 'clearai-list' },
973
+ ...TRUST_ORDER.map((trust) => {
974
+ const group = rows.filter((row) => row.trust === trust)
975
+ if (group.length === 0) return null
1218
976
  return h(
1219
977
  'div',
1220
- { key: `${code}-${index}`, style: S.row },
1221
- h(
1222
- 'div',
1223
- { style: S.inline },
1224
- label === null
1225
- ? h('span', { style: S.tagStrong, title: code }, t('未登记的缺口类型'))
1226
- : h('span', { style: S.tagStrong }, label),
1227
- count === null ? null : h('span', { style: S.faint }, String(count)),
1228
- target === null ? null : h(Link, { title: target.title, onClick: target.run }, target.label),
1229
- ),
1230
- detail === '' ? null : h('div', { style: S.faint }, t(detail)),
1231
- nextAction === '' ? null : h('div', { style: S.dim }, `${t('下一步:')}${t(nextAction)}`),
978
+ { key: trust, className: 'clearai-group' },
979
+ h('div', { className: 'clearai-group-head' }, TRUST_WORD[trust], h('span', null, String(group.length))),
980
+ ...group.map((row) => h(ConclusionRow, { key: row.key, row, open: props.open === row.key, onToggle: props.onToggle, data: props.data, onFocus: props.onFocus })),
1232
981
  )
1233
982
  }),
1234
983
  )
1235
984
  }
1236
985
 
1237
- /** **事实货架**:已确认、可作已知的那一层。每条都带边界——没有边界的事实没人敢用。 */
1238
- function FactShelf(props) {
1239
- const base = props.data ?? (typeof props.useProjection === 'function' ? props.useProjection('clearai') : undefined) ?? {}
1240
- const data = { ...base, openPreview: props.openPreview ?? base.openPreview, openRail: props.openRail ?? base.openRail, openSpectator: props.openSpectator ?? base.openSpectator }
1241
- const facts = data.facts ?? []
986
+ /** 页眉:在回答什么 · 一行计数 · 待处理 · 迷你进度轨。 */
987
+ function AtlasHeader(props) {
988
+ const { data, rows } = props
989
+ const live = rows.filter((row) => row.trust !== 'replaced')
990
+ const counts = TRUST_ORDER.filter((trust) => trust !== 'replaced')
991
+ .map((trust) => [trust, rows.filter((row) => row.trust === trust).length])
992
+ .filter(([, count]) => count > 0)
993
+ const stations = STATIONS().map((name, index) => [name, live.filter((row) => row.station >= index + 1).length])
994
+ const needYou = Array.isArray(data?.needYou) ? data.needYou : []
1242
995
  return h(
1243
- Section,
1244
- { title: `${t('本体货架 · ')}${facts.length}`, mark: true },
1245
- facts.length === 0
1246
- ? h(
1247
- 'div',
1248
- { style: S.faint },
1249
- data.goal === null || data.goal === undefined
1250
- ? t('暂无目标。验证达门槛且无推翻的命题在验收时升格为事实。')
1251
- : t('达门槛且无推翻的命题在目标验收时升格为事实(写入 clear/knowledge/facts/)。'),
1252
- )
996
+ 'div',
997
+ { className: 'clearai-head' },
998
+ h('div', { className: 'clearai-question', title: String(data?.goal?.claim ?? '') }, data?.goal ? String(data.knowledgeView?.goal?.headline ?? data.goal.claim ?? '') : t('还没有立目标')),
999
+ counts.length === 0 ? null : h('div', { className: 'clearai-counts' }, counts.map(([trust, count]) => `${TRUST_WORD[trust]} ${count}`).join(' · ')),
1000
+ ...needYou.map((item, index) => h('div', { key: `need-${index}`, className: 'clearai-need' }, h('b', null, t('待处理')), String(item?.text ?? ''))),
1001
+ live.length === 0
1002
+ ? null
1253
1003
  : h(
1254
1004
  'div',
1255
- null,
1256
- ...facts.map((row) =>
1257
- h(
1258
- 'div',
1259
- {
1260
- key: row.id,
1261
- style: S.factRow,
1262
- /** 这一格**就是**事实库:点这条事实开它的原件(右侧原生预览),不再另挂一个「打开事实库」。 */
1263
- onClick: row.path === null || row.path === undefined || data.openPreview === undefined ? undefined : () => data.openPreview(row.path),
1264
- title: row.path === null || row.path === undefined ? '' : `${t('打开 ')}${row.path}`,
1265
- },
1266
- h(
1267
- 'div',
1268
- { style: S.factClaim },
1269
- /**
1270
- * 复核状态直接说在标题旁边:一条**已撤回**的事实仍留在这里(P5:记录不删),
1271
- * 但它已经不能当「已知」引用了——不写出来,读者会照旧引用它。
1272
- */
1273
- dash(row.text),
1274
- row.review?.decision === 'retracted'
1275
- ? h('span', { style: { ...S.tag, marginLeft: 6 } }, t('人已撤回(记录保留)'))
1276
- : row.refuted === true
1277
- ? h('span', { style: { ...S.tag, marginLeft: 6 } }, t('被推翻 · 待裁决'))
1278
- : null,
1279
- ),
1280
- h(
1281
- 'div',
1282
- { style: S.factMeta, title: row.scope === null || row.scope === undefined ? '' : String(row.scope) },
1283
- /**
1284
- * 边界和证据的「依据」是同一类字段 ⇒ **同一条规矩**:列表放摘要、全文进 tooltip。
1285
- * 真数据里边界常是 200~300 字,三条事实就把这一格撑到一千多字 ✗(货架只放摘要)。
1286
- */
1287
- `${t('边界:')}${row.scope === null || row.scope === undefined || String(row.scope).trim() === '' ? '(未写——引用前请谨慎)' : brief(row.scope, 80)}${t(' · 支持到 ')}${dash(row.level)}${t(' · 证据 ')}${(row.evidenceIds ?? []).length}${t(' 条')}`,
1288
- /**
1289
- * 原件的标签用**文件名**(「打开原件」四平八稳,但一排下来同样认不出是谁)。
1290
- */
1291
- /**
1292
- * 整行已经可点开原件 ⇒ 不再挂一个同样动作的「事实存档」✗(同一动作不开两条路)。
1293
- * 原件路径在整行的 tooltip 里(`打开 <path>`)。
1294
- */
1295
- /**
1296
- * 跳去它那一步。**不把 `openRail` 当成渲染前提** ——
1297
- * 把渲染前提挂在它上面,整条链接会一起消失。
1298
- * 拿不到 `openRail` 也照常渲染:点了至少把聚焦设上、并试一次原生切视图。
1299
- */
1300
- stepOfFact(data, row) === null
1301
- ? null
1302
- : h(
1303
- Link,
1304
- {
1305
- onClick: (event) => {
1306
- event?.stopPropagation?.()
1307
- treeFocus.set({ plan: rowPlanOf(data, row), step: stepOfFact(data, row), branch: null })
1308
- if (data.openRail !== undefined) data.openRail('clearai-worldtree')
1309
- },
1310
- title: t('打开世界树并选中产出这条事实的验证步'),
1311
- },
1312
- t('在世界树中查看此步骤'),
1313
- ),
1314
- /**
1315
- * **冲突内联标记**:冲突行只给指针,受害的条目自己也要亮出来——
1316
- * 读者扫到这一行时不必回头去对那一行指针。
1317
- */
1318
- data.conflictOf !== undefined && data.conflictOf.has(row.id)
1319
- ? h('div', { style: S.row }, h('span', { style: { ...S.tag, borderColor: 'rgba(220,38,38,0.7)', color: '#dc2626' } }, `${t('冲突')} · ${String(data.conflictOf.get(row.id).predicate)}`), h('span', { style: S.faint }, ` ${data.conflictOf.get(row.id).sides.map((side) => `${side.fact}(${side.value})`).join(' ')}`))
1320
- : null,
1321
- h(AssertionChips, { assertions: row.assertions, ui: data.assertionUI, promoted: true }),
1322
- ),
1323
- ),
1324
- ),
1005
+ { className: 'clearai-track' },
1006
+ ...stations.flatMap(([name, count], index) => [
1007
+ index === 0 ? null : h('span', { key: `line-${index}`, className: 'clearai-track-line', 'data-on': count > 0 ? '1' : '0' }),
1008
+ h('span', { key: `st-${index}`, className: 'clearai-track-stop', 'data-on': count > 0 ? '1' : '0' }, h('i', null), `${name} ${count}`),
1009
+ ]),
1325
1010
  ),
1326
1011
  )
1327
1012
  }
1328
1013
 
1329
1014
  /**
1330
- * **「事实」页签**:一个**知识货架**,不是机制说明页。
1015
+ * **本体格**(中栏视图)。
1331
1016
  *
1332
- * 上架 = 已确认事实(可用作下一轮已知,按支持等级分级、每条带边界);
1333
- * 下架 = 命题(人与模型提出的候选知识,按本体状态分组流转)。
1334
- * 默认只回答一个问题:「我现在能信什么」;点开一条命题才回答「凭什么」。
1017
+ * 取数与从前一样:宿主半的会话投影单元 `clearai` 由插座作为 `useProjection` 交下来。
1018
+ * 界面状态(图层、全屏、选中、展开哪条、只看哪个词)全是界面的事,一个字节都不进账本。
1335
1019
  */
1336
- function Facts(props) {
1337
- // 取数与 Panel/Deliverables 同一姿势:宿主半的会话投影单元 `clearai` 由插座作为标准 prop 交下来。
1020
+ function Atlas(props) {
1338
1021
  const projected = typeof props.useProjection === 'function' ? props.useProjection('clearai') : undefined
1339
1022
  const [manual, setManual] = React.useState(null)
1340
- /** 本体格的界面状态:图层次/全景/图带开关/过滤/芯片展开/词汇区开关。全是界面状态,不进账本。 */
1341
1023
  const [layer, setLayer] = React.useState('ontology')
1342
- /** 图谱工作区是否打开(真正的全屏 overlay,不是把图带拉高)。界面状态,不进账本。 */
1343
1024
  const [workspace, setWorkspace] = React.useState(false)
1344
- const [bandOpen, setBandOpen] = React.useState(true)
1345
1025
  const [filter, setFilter] = React.useState(null)
1346
- const [chipKey, setChipKey] = React.useState(null)
1347
- const [vocabOpen, setVocabOpen] = React.useState(null)
1026
+ const [focusNode, setFocusNode] = React.useState(null)
1348
1027
  const incoming = useFocus(factsFocus)
1028
+ /** 样式表由 `apply` 挂;面板被单独挂载(截图、预览)时也自己挂一次——同一个标记,不会挂两份。 */
1029
+ React.useEffect(() => {
1030
+ installStyles()
1031
+ }, [])
1349
1032
  if (projected === undefined || projected === null) {
1350
- return h('div', { style: S.wrap }, h('div', { style: S.bar }, h('span', { style: S.title }, t('本体'))), h(Empty, null, t('暂无数据。发送第一条消息后,此处显示已确立条目与在验命题。')))
1033
+ return h('div', { className: 'clearai-atlas' }, h(Empty, null, t('还没有内容。发第一条消息后,这里显示本体图和结论。')))
1351
1034
  }
1352
1035
  const data = { ...projected, openPreview: props.openPreview, openRail: props.openRail, openSpectator: props.openSpectator }
1353
- const facts = data.facts ?? []
1354
- const propositions = (data.goal?.hypotheses ?? []).filter((row) => row.status !== 'confirmed')
1355
- /** 外面点进来的聚焦优先;手动点行仍然有效(聚焦为 null 时用它)。 */
1036
+ const rows = conclusionsOf(data)
1037
+ const lexicon = data.lexicon ?? null
1038
+ /** 世界树那边点进来的聚焦优先;手动点行仍然有效。 */
1356
1039
  const focused = incoming === null || incoming === undefined ? null : propositionForStep(data, incoming.step)
1357
1040
  const open = focused ?? manual
1358
- /**
1359
- * **本体层**:没有词条时下面这一切都不存在(零成本契约)——
1360
- * 图带、冲突行、过滤、芯片、维护区,每一个都以「有东西可说」为前提。
1361
- */
1362
- const lexicon = data.lexicon ?? null
1363
- const hasVocabulary = lexicon !== null && ((lexicon.terms ?? []).length > 0 || (lexicon.predicates ?? []).length > 0)
1364
- const conflicts = hasVocabulary === true ? (lexicon.conflicts ?? []) : []
1365
- /** factId → 它卷入的那对冲突(内联标记用;同一事实卷多对时取第一对)。 */
1366
- const conflictOf = new Map()
1367
- for (const conflict of conflicts) for (const side of conflict.sides) if (typeof side.fact === 'string' && !conflictOf.has(side.fact)) conflictOf.set(side.fact, conflict)
1368
- /** 过滤词条:图带点概念节点或芯片动作都会给词条 id;文本兜底匹配。 */
1369
1041
  const filterTerm = filter === null ? null : ((lexicon?.terms ?? []).find((item) => item.id === filter) ?? (lexicon?.predicates ?? []).find((item) => item.id === filter) ?? { id: filter, label: filter, aliases: [] })
1370
- const matchedFacts = filterTerm === null ? facts : facts.filter((row) => termMatches(filterTerm, row))
1371
- const matchedPropositions = filterTerm === null ? propositions : propositions.filter((row) => termMatches(filterTerm, row))
1372
- const filteredData = filterTerm === null ? data : { ...data, facts: matchedFacts, goal: { ...data.goal, hypotheses: matchedPropositions } }
1373
- /** 断言芯片的公共道具:词条卡从词汇里查,动作回连过滤与图带。 */
1374
- const assertionUI = hasVocabulary === true ? { lexicon, openKey: chipKey, onToggle: (key) => setChipKey(chipKey === key ? null : key), onFilter: (id) => { setFilter(id); setChipKey(null) }, onGraph: () => { setBandOpen(true); setChipKey(null) } } : null
1375
- filteredData.assertionUI = assertionUI
1376
- filteredData.conflictOf = conflictOf
1377
- /** 语言先于句子:0 条目 0 命题而有词条时,维护区自动展开。 */
1378
- const vocabAutoOpen = vocabOpen === null ? (facts.length === 0 && propositions.length > 0 === false && hasVocabulary === true) : vocabOpen
1379
- const vocabActuallyOpen = vocabOpen === null ? (facts.length === 0 && propositions.length === 0 && hasVocabulary === true) : vocabOpen
1380
- /**
1381
- * **缺口**:折法侧算好的欠账(客户端的唯一来源是投影)。空数组 = 不欠,这一栏整块不出现。
1382
- * 实体图空态要用的那条读数也从这里取:`entities_unlanded` 的 count
1383
- * 就是「多少个断言主体还没有落到实体图」。
1384
- */
1042
+ const shown = filterTerm === null ? rows : rows.filter((row) => termMatches(filterTerm, row))
1385
1043
  const gaps = Array.isArray(data.knowledge?.gaps) ? data.knowledge.gaps : []
1386
1044
  const unlandedRow = gaps.find((gap) => gap?.code === 'entities_unlanded')
1387
- const unlanded = typeof unlandedRow?.count === 'number' ? unlandedRow.count : null
1388
- /** 缺口行的去处之一:把图带切到实体层(空态的出口就在这一层)。 */
1389
- const showEntity = () => {
1390
- setLayer('entity')
1391
- setBandOpen(true)
1392
- }
1393
- const bar = h(
1394
- 'div',
1395
- { style: S.bar },
1396
- h('span', { style: S.title }, t('本体')),
1397
- )
1398
- if (hasVocabulary === false) {
1399
- /** 零成本:没有词条时,这一格与从前逐像素相同(两个货架 + 页眉)——但缺口照报。 */
1400
- return h('div', { style: S.wrap }, bar, h(GapShelf, { data, onShowEntity: showEntity }), h(FactShelf, { data }), h(PropositionShelf, {
1401
- data,
1402
- open,
1403
- onToggle: (id) => {
1404
- factsFocus.set(null)
1405
- setManual(open === id ? null : id)
1406
- },
1407
- }))
1408
- }
1409
1045
  return h(
1410
1046
  'div',
1411
- { style: S.wrap },
1412
- bar,
1413
- /** 缺口行:欠账摆在最前面(它决定这个目标现在能不能收口),每条都带下一步。 */
1414
- h(GapShelf, { data, onShowEntity: showEntity }),
1415
- /** 冲突行:仅当有冲突。一行指针 + 内联标记,不做大区块(例外才打扰)。 */
1416
- conflicts.length > 0
1417
- ? h(
1418
- 'div',
1419
- { style: S.section },
1420
- h('div', { style: S.head }, `${t('冲突')} ${conflicts.length}${t(' 对')}`),
1421
- ...conflicts.map((conflict, index) =>
1422
- h('div', { key: `cf-${index}`, style: S.row }, `${String(conflict.predicate)} · ${String(conflict.subject)}:`, ...conflict.sides.map((side, sideIndex) => h('span', { key: `cf-${index}-${sideIndex}`, style: S.faint }, ` ${String(side.fact ?? '?')}(${String(side.value)})`)), ` —— ${t('只暴露,不裁决;撤回或维持由人决定')}`),
1423
- ),
1424
- )
1425
- : null,
1426
- bandOpen === false
1427
- ? h('div', { style: S.section }, h('span', { style: S.chipAction, onClick: () => setBandOpen(true) }, t('展开图带')))
1428
- : h(GraphBand, { lexicon, layer, unlanded, onLayer: setLayer, onFilter: (id) => setFilter(filter === id ? null : id), sessionId: data.sessionId, fullscreen: workspace, onToggleFullscreen: () => setWorkspace(workspace !== true) }),
1429
- /** 过滤状态行:仅当过滤激活;N/M 说真话,✕ 一键清除。 */
1047
+ { className: 'clearai-atlas' },
1048
+ h(AtlasHeader, { data, rows }),
1049
+ h(GraphBand, {
1050
+ lexicon,
1051
+ layer,
1052
+ unlanded: typeof unlandedRow?.count === 'number' ? unlandedRow.count : null,
1053
+ onLayer: setLayer,
1054
+ onFilter: (id) => setFilter(filter === id ? null : id),
1055
+ focus: focusNode,
1056
+ sessionId: data.sessionId,
1057
+ fullscreen: workspace,
1058
+ onToggleFullscreen: () => setWorkspace(workspace !== true),
1059
+ }),
1430
1060
  filterTerm === null
1431
1061
  ? null
1432
1062
  : h(
1433
1063
  'div',
1434
- { style: S.inline },
1435
- h('span', { style: S.tag }, `${t('按')}「${String(filterTerm.label ?? filterTerm.id)}」${t('过滤')}:${matchedFacts.length + matchedPropositions.length}/${facts.length + propositions.length}${t(' 条')}`),
1436
- h('span', { style: S.chipAction, onClick: () => setFilter(null) }, t('清除')),
1064
+ { className: 'clearai-filter' },
1065
+ `${t('只看与「')}${String(filterTerm.label ?? filterTerm.id)}${t('」相关的')}${t(':')}${shown.length}/${rows.length}`,
1066
+ h('span', { className: 'clearai-link', onClick: () => setFilter(null) }, t('清除')),
1437
1067
  ),
1438
- h(FactShelf, { data: filteredData }),
1439
- h(PropositionShelf, {
1440
- data: filteredData,
1068
+ h('div', { className: 'clearai-section-head' }, t('结论')),
1069
+ h(ConclusionList, {
1070
+ rows: shown,
1441
1071
  open,
1442
- onToggle: (id) => {
1443
- // 手动点行:先清掉外面的聚焦(否则它一直压着手动选择,点了没反应 ✗)
1072
+ data,
1073
+ onToggle: (key) => {
1444
1074
  factsFocus.set(null)
1445
- setManual(open === id ? null : id)
1075
+ setManual(open === key ? null : key)
1076
+ },
1077
+ onFocus: (id) => {
1078
+ const node = (lexicon?.graph?.nodes ?? []).find((item) => item.layer === 'entity' && String(item.ref ?? '') === id)
1079
+ if (node !== undefined) {
1080
+ setLayer('entity')
1081
+ setFocusNode(node.id)
1082
+ }
1446
1083
  },
1447
1084
  }),
1448
- h(VocabBlock, { lexicon, open: vocabActuallyOpen, onToggle: () => setVocabOpen(vocabActuallyOpen !== true), openPreview: props.openPreview, sessionId: data.sessionId }),
1449
1085
  )
1450
1086
  }
1451
1087
 
1452
- /**
1453
- * 「点开看」的目标:每个会话一份的小状态。
1454
- *
1455
- * 它只是**界面状态**(点了哪一条),不是事实——事实仍然只有投影与读面两个来源。
1456
- * 预览页签**第一次用到时才注册**:宁可少一张页签,也不放一张空的在那里。
1457
- */
1458
- /**
1459
- * 打开 DSH **原生**的文档预览。
1460
- *
1461
- * 第一性原理:预览是「把一条路径渲染成人能看的东西」——而宿主已经有这件东西了,而且比我们全:
1462
- * `dsh-client-ui-sidebar-documentpreview` 注册了 **6 个实现**(text / markdown / html / image /
1463
- * pdf / code),自己按地址认领标签、自己按后缀挑实现、自己分页读盘。我们原来那套
1464
- * `<pre>` + 自己写读面,等于**把同一件事做第二遍,而且只做出了其中一种**(把 md 显示成源码、
1465
- * 根本打不开图)。
1466
- *
1467
- * 奥卡姆:不新造渲染器,也不新造读面——只按原生约定的地址把文件**递给**它:
1468
- * `dsh-resource://file/session/<sessionId>/<逐段编码的路径>`
1469
- * (地址语法与编码规则照抄 `workspace-path/file-address`:`/#?` 之前、按 `/` 分段、逐段解码。)
1470
- * 路径是绝对路径时,分段里会自然出现一个空段,拼回来仍是绝对路径——原生就是这么分的。
1471
- *
1472
- * **第二参是会话 id,不是路径**(传错会直接读空):读面按它查工作区根
1473
- * (`workspaceFileScope`),拿错了就原样报
1474
- * `lookup provider "workspaceFileScope" did not resolve the requested identity`。
1475
- * 谁调用谁负责给对 —— 见 `apply` 里的 `openPreviewFor`,按**座位自己的** props 定型。
1476
- */
1477
- /** 时间戳 → 「YYYY-MM-DD HH:mm」。与宿主半卡片里的写法一致:同一份事实在两处长得不一样,人就会怀疑其中一处。 */
1478
- const stampOf = (value) => (typeof value === 'number' && Number.isFinite(value) ? new Date(value).toISOString().slice(0, 16).replace('T', ' ') : t('未知'))
1479
-
1480
1088
  /** 逐段编码:照抄原生的 `encodeSegment`——`:` 保持字面(Windows 盘符),其余交给 encodeURIComponent。 */
1481
1089
  const encodeAddressSegment = (segment) => encodeURIComponent(segment).replace(/%3A/gi, ':')
1482
1090
  const fileAddress = (sessionId, path) => {
@@ -1506,236 +1114,18 @@ window.__ModuleLoader__.load({
1506
1114
  }
1507
1115
 
1508
1116
  /**
1509
- * 产物视图(中栏第一眼该看「我拿到了什么」)。
1117
+ * 世界树(右栏):计划的步骤与闸门,一条脊柱从上往下。
1118
+ *
1119
+ * 几何照抄 ClearAI `PlanTree.tsx`(`ROW_H=30 / NODE_R=6`):行高固定 → 拓扑常驻不变。
1120
+ * 四条正交通道各答一个问题、可任意叠加:
1510
1121
  *
1511
- * 数据走宿主读面(`/api/clearai/deliverables`):交付物的**存在性是文件系统事实**,
1512
- * 浏览器读不了盘,所以由宿主在请求时即时 stat(顺带给出字节与修改时间)。
1513
- * 投影一变(推进了一步、多了一条证据)就重新取一次——面板不许比状态慢一拍。
1122
+ * ① **填充** 已落定终局了吗 实心 = 是(于是「实心到哪儿就是做到哪儿」,进度成了一眼可读的形)
1123
+ * ② **颜色** 什么结局 绿=过 灰=待办/作废 蓝=在跑 琥珀=等你
1124
+ * ③ **光环** 此刻在动吗 虚线环 = 独立评估者正在审这一步
1125
+ * ④ **分段** 被闸门裁断过几次 N>1 才分段,封顶 3 段,驳回的那几段画红
1514
1126
  *
1515
- * 这条读面回**两半**,这一栏也画两半:
1516
- * · `stages` = **声明**(计划里写了要交什么、谁验的、盘上在不在);
1517
- * · `outputs` = **实际**(products/ 底下真有、但没有任何计划声明过的)。
1518
- * 为什么两半都要:在一个已经有产物的老项目里开新会话,原来这一栏
1519
- * 只有「阶段 0 · 交付 0/0」—— 可人此刻问的是「我拿到了什么」,盘上那份 46 KB 的 HTML
1520
- * 明明就在。声明与实际是两件事,少画一半就等于对着半张表回答。
1521
- */
1522
- function Deliverables(props) {
1523
- const data = typeof props.useProjection === 'function' ? props.useProjection('clearai') : undefined
1524
- const sessionId = data === null || data === undefined ? null : data.sessionId
1525
- const [stages, setStages] = React.useState(null)
1526
- const [onDisk, setOnDisk] = React.useState([])
1527
- const [error, setError] = React.useState(null)
1528
- const stamp = data === null || data === undefined ? '' : `${data.plan?.id ?? ''}:${data.plan?.advancedCount ?? 0}:${data.plan?.totalCount ?? 0}:${(data.evidence || []).length}:${(data.facts || []).length}`
1529
- React.useEffect(() => {
1530
- if (typeof sessionId !== 'string' || sessionId === '') return
1531
- let alive = true
1532
- fetch(`/api/clearai/deliverables?sessionId=${encodeURIComponent(sessionId)}`)
1533
- .then(readResponse)
1534
- .then((result) => {
1535
- if (!alive) return
1536
- if (result.ok === true) {
1537
- setStages(result.payload.stages ?? [])
1538
- setOnDisk(result.payload.outputs ?? [])
1539
- } else setError(result.error)
1540
- })
1541
- .catch((thrown) => {
1542
- if (alive) setError(String(thrown?.message ?? thrown))
1543
- })
1544
- return () => {
1545
- alive = false
1546
- }
1547
- }, [sessionId, stamp])
1548
-
1549
- const openStage = data === null || data === undefined ? null : data.plan
1550
- // 点一条产物 → **原生预览**(md 渲染成 md、png 直接出图、html 直接看、pdf 用原生 pdf.js)
1551
- const open = (path) => props.openPreview?.(path)
1552
- if (sessionId === null) {
1553
- return h('div', { style: S.wrap }, h('div', { style: S.bar }, h('span', { style: S.title }, t('产物'))), h(Empty, null, t('这个会话还没有 ClearAI 的状态。')))
1554
- }
1555
- const advanced = (stages ?? []).flatMap((stage) => stage.steps.filter((step) => step.status === 'advanced' && step.artifacts.length > 0))
1556
- const delivered = advanced.reduce((sum, step) => sum + step.artifacts.filter((artifact) => artifact.exists).length, 0)
1557
- const declared = (stages ?? []).flatMap((stage) => stage.steps).reduce((sum, step) => sum + step.artifacts.length, 0)
1558
- /**
1559
- * 「核心产物」:**盘上真的在**的输出成果(products/),跨阶段汇总在最上面。
1560
- * 中间过程(lab/)不进这一栏——这正是 ClearAI 分类的用途:别让 100 个中间文件
1561
- * 淹没 3 份交付物。
1562
- */
1563
- /**
1564
- * 「核心产物」= **一份文件一行**。
1565
- *
1566
- * 原先摊平的是「步骤 × 声明产物」这对组合 ⇒ 一个文件被几个步骤声明就出现几次 ✗
1567
- * (真数据:`qinrun-dipforming-3d.html` 被 build/verify-3d/rebuild/integrate 四步都声明了,
1568
- * 于是同一份 1153646 字节的文件列了四行,计数也跟着膨胀 ✗)。
1569
- * 「后面几步也声明了它」是**有用的信息**(那几步在验它、整合它),所以去重时把它收进 `steps`,
1570
- * 而不是丢掉。
1571
- */
1572
- const byPath = new Map()
1573
- for (const stage of stages ?? []) {
1574
- for (const step of stage.steps) {
1575
- for (const artifact of step.artifacts) {
1576
- if (artifact.exists !== true) continue
1577
- if (artifact.area?.key !== 'output' && artifact.area?.key !== 'other') continue
1578
- const seen = byPath.get(artifact.path)
1579
- if (seen === undefined) byPath.set(artifact.path, { ...artifact, stage: stage.plan, steps: [step.id] })
1580
- else if (!seen.steps.includes(step.id)) seen.steps.push(step.id)
1581
- }
1582
- }
1583
- }
1584
- const coreOutputs = [...byPath.values()]
1585
- /** 盘上已有的:不铺满一屏(它只是「第一眼」),但如实说还有多少没列。 */
1586
- const ON_DISK_SHOWN = 25
1587
- const shownOnDisk = onDisk.slice(0, ON_DISK_SHOWN)
1588
- const hiddenOnDisk = onDisk.length - shownOnDisk.length
1589
- const clickable = coreOutputs.length + onDisk.length
1590
- return h(
1591
- 'div',
1592
- { style: S.wrap },
1593
- h(
1594
- 'div',
1595
- { style: S.bar },
1596
- h('span', { style: S.title }, t('产物')),
1597
- /**
1598
- * 两个计数的**单位不同**,所以分开说:「处」= 步骤声明(一份文件被四步声明算四处),
1599
- * 「份」= 去重后的文件。混着写会让人以为盘上有四份。
1600
- */
1601
- h('span', { style: S.faint }, `${t('阶段 ')}${(stages ?? []).length}${t(' · 交付 ')}${delivered}/${declared}${t(' 处声明')}${onDisk.length === 0 ? '' : ` · 盘上 ${onDisk.length} 份`}`),
1602
- clickable === 0 ? null : h('span', { style: { ...S.faint, marginLeft: 'auto' } }, t('点击在右栏预览')),
1603
- ),
1604
- error !== null ? h('div', { style: S.faint }, `${t('读不到交付物:')}${error}`) : null,
1605
- coreOutputs.length === 0
1606
- ? null
1607
- : h(
1608
- 'div',
1609
- { style: S.section },
1610
- h('div', { style: S.head }, `${t('核心产物(')}${coreOutputs.length})`),
1611
- coreOutputs.map((artifact) =>
1612
- h(
1613
- 'div',
1614
- { key: artifact.path, style: S.inline },
1615
- h('span', { style: S.tagStrong }, artifact.area?.label ?? t('未分类')),
1616
- h(Link, { onClick: () => open(artifact.path) }, h('span', { style: S.mono }, artifact.path)),
1617
- h(
1618
- 'span',
1619
- { style: S.faint, title: artifact.steps.join(' · ') },
1620
- `${artifact.bytes}${t(' 字节 · ')}${artifact.steps.length === 1 ? `步 ${artifact.steps[0]}` : `步 ${artifact.steps[0]} 等 ${artifact.steps.length} 步`}`,
1621
- ),
1622
- ),
1623
- ),
1624
- ),
1625
- /**
1626
- * 「盘上已有」= 实际那一半。它**不进**交付计数(计数只认计划声明),
1627
- * 所以每条都带上这句交代,免得人以为计划多交了几份。
1628
- */
1629
- onDisk.length === 0
1630
- ? null
1631
- : h(
1632
- 'div',
1633
- { style: S.section },
1634
- h('div', { style: S.head }, `${t('盘上已有(')}${onDisk.length})`),
1635
- ...shownOnDisk.map((item) =>
1636
- h(
1637
- 'div',
1638
- { key: item.path, style: S.inline },
1639
- h('span', { style: S.tag }, item.area?.label ?? t('未分类')),
1640
- h(Link, { onClick: () => open(item.path) }, h('span', { style: S.mono }, item.path)),
1641
- h('span', { style: S.faint }, `${item.bytes}${t(' 字节 · 未被任何计划声明')}`),
1642
- ),
1643
- ),
1644
- hiddenOnDisk <= 0 ? null : h('div', { style: S.faint }, `${t('还有 ')}${hiddenOnDisk}${t(' 份没列出来(这一栏只列最近改动的 ')}${ON_DISK_SHOWN}${t(' 份)。')}`),
1645
- ),
1646
- stages === null
1647
- ? h('div', { style: S.faint }, t('读取中…'))
1648
- : stages.length === 0
1649
- ? h(
1650
- Empty,
1651
- null,
1652
- onDisk.length === 0
1653
- ? t('暂无计划。建立计划后,已交付步骤按阶段列于此处。')
1654
- : `${t('这个工作区盘上已经有 ')}${onDisk.length}${t(' 份产物,但没有任何计划声明过它们。点一条可以直接看(它们不计入交付)。')}`,
1655
- )
1656
- : stages.map((stage) =>
1657
- h(
1658
- 'div',
1659
- { key: stage.plan, style: S.section },
1660
- h(
1661
- 'div',
1662
- { style: S.head },
1663
- // 「待确认」改成「授权记号未落账」:前者暗示有一道要人按的门,而门已经砍了——
1664
- // 记号会在第一次交付时按事实补写(行为即授权)。
1665
- `${openStage !== null && openStage.id === stage.plan ? '当前阶段 · ' : ''}${stage.plan} · ${dash(stage.status)}${stage.confirmedAt === null ? ' · 授权记号未落账(交付即落账)' : ''}`,
1666
- ),
1667
- stage.brief === '' ? null : h('div', { style: S.faint }, stage.brief),
1668
- stage.summary === null || stage.summary === undefined ? null : h('div', { style: S.faint }, `${t('收束:')}${stage.summary}`),
1669
- stage.steps.map((step) =>
1670
- h(
1671
- 'div',
1672
- { key: step.id, style: S.row },
1673
- h(
1674
- 'div',
1675
- { style: S.inline },
1676
- h('span', { style: step.status === 'advanced' ? S.tagStrong : S.tag }, gloss(STEP, step.status)),
1677
- h('span', { style: S.mono }, `${step.ordinal}. ${step.id}`),
1678
- step.level === null ? null : h('span', { style: S.faint }, step.level),
1679
- h('span', null, step.do),
1680
- ),
1681
- h('div', { style: S.faint }, `${t('判据:')}${step.doneCriteria}${step.voidReason === null || step.voidReason === undefined ? '' : `(作废:${step.voidReason})`}`),
1682
- step.artifacts.length === 0
1683
- ? h('div', { style: S.faint }, t('这一步没有声明产物。'))
1684
- : h(
1685
- 'div',
1686
- null,
1687
- step.artifacts.map((artifact) =>
1688
- h(
1689
- 'div',
1690
- { key: artifact.path, style: S.inline },
1691
- h('span', { style: artifact.exists ? S.tagStrong : S.tag }, artifact.exists ? t('在') : artifact.directory === true ? t('目录') : t('缺')),
1692
- h('span', { style: S.faint }, artifact.area?.label ?? t('未分类')),
1693
- /**
1694
- * 判据为「缺」的那条**不是链接**:点进去只会撞一个不存在的路径
1695
- * (原生预览会照实报错)。机制上不可点,胜过在文案里劝人别点。
1696
- * 它带来的信息仍然齐全:路径、分类、「盘上没有」。
1697
- */
1698
- artifact.exists
1699
- ? h('span', { className: 'clearai-link', onClick: () => open(artifact.path), title: t('右栏预览') }, artifact.path)
1700
- : h(
1701
- 'span',
1702
- { style: S.mono, title: artifact.directory === true ? t('声明的是目录;准入要求具体文件——目录不算物证') : t('盘上没有这个文件') },
1703
- artifact.path,
1704
- ),
1705
- artifact.exists
1706
- ? h('span', { style: S.faint }, `${artifact.bytes}${t(' 字节')}`)
1707
- : h('span', { style: S.faint }, artifact.directory === true ? t('是目录(不算物证)') : t('盘上没有')),
1708
- ),
1709
- ),
1710
- ),
1711
- ),
1712
- ),
1713
- ),
1714
- ),
1715
- )
1716
- }
1717
-
1718
- /**
1719
- * 世界树(右栏):计划即世界树——脊柱上一串步,到 fork 就地裂成并行车道,
1720
- * 收敛画成菱形,再回到同一条脊柱(git-graph 式)。
1721
- *
1722
- * 几何与**四条正交通道**照抄 ClearAI `PlanTree.tsx`(`ROW_H=30 / LANE_W=15 / NODE_R=6`):
1723
- * 行高固定 → 拓扑常驻不变;车道不移位 → 叉开与收敛看得清。四条通道各答一个问题、可任意叠加:
1724
- *
1725
- * ① **填充** 已落定终局了吗 实心 = 是(于是「实心到哪儿就是做到哪儿」,进度成了一眼可读的形)
1726
- * ② **颜色** 什么结局 绿=过/采纳 灰=待办/落选 蓝=在跑 琥珀=等你
1727
- * ③ **光环** 此刻在动吗 呼吸 = 是(步骤与世界线共用同一惯用法)
1728
- * ④ **分段** 被闸门裁断过几次 N>1 才分段,封顶 3 段,驳回的那几段画红
1729
- *
1730
- * 这一处曾丢过一批行为(分岔/推进/闪烁/选择/裁减/变灰)。必须都在的:
1731
- * 空心圈与实心核、呼吸光环与虚线审计环、轮次分段弧、落选/作废的**变灰划掉**、
1732
- * 虚线轨道(落选那条)、点行选中 → 树下详情(含给人的两个动作:采纳这条世界线 / 放弃探索)。
1733
- *
1734
- * 第一性原理:一棵树好不好读,取决于**每一条通道只答一个问题**。把「在动」「落定」
1735
- * 「结局」「几轮」挤进同一个实心点,读者就得靠记住上一次的颜色差——那不是可视化,是暗号。
1736
- *
1737
- * 奥卡姆:不画的东西也写清楚为什么——越位内环(我们的投影里没有「越位」这条事实)、
1738
- * run 级状态色(同上)、任务选择器与结算单(进展页签已有),都不引入。
1127
+ * 第一性原理:一棵树好不好读,取决于**每一条通道只答一个问题**。
1128
+ * 并行探索不在这里画车道:竞争路线是竞争的假设,各由一个步骤检验,所以它们就是脊柱上的几步。
1739
1129
  */
1740
1130
  const TREE = {
1741
1131
  rowHeight: 30,
@@ -1751,17 +1141,10 @@ window.__ModuleLoader__.load({
1751
1141
  done: 'rgb(var(--dsw-color-success, 16 185 129))',
1752
1142
  running: '#7dd3fc',
1753
1143
  pending: '#cbd5e1',
1754
- adopted: 'rgb(var(--dsw-color-success, 16 185 129))',
1755
- recommend: 'rgb(var(--dsw-color-warning, 245 158 11))',
1756
- evaluated: '#64748b',
1757
- exploring: '#94a3b8',
1758
1144
  pruned: '#cbd5e1',
1759
- /** 执行没跑成:低饱和红(照抄原设计的 branchFailed)。它不是落选,所以不能与灰混用。 */
1760
- failed: '#c98f8f',
1761
- /** 落选/作废那条轨道画虚线(3 3),与实线轨道分开。 */
1762
1145
  rail: 'hsla(220, 9%, 46%, 0.35)',
1763
1146
  }
1764
- /** 一步/一条车道**被闸门裁断过几次**(`block/counted` 折出来的驳回次数 + 通过的那一次)。 */
1147
+ /** 一步**被闸门裁断过几次**(`block/counted` 折出来的驳回次数 + 通过的那一次)。 */
1765
1148
  function treeLoops(blocks, planId, stepId, advanced) {
1766
1149
  const counted = Number(blocks?.[planId]?.[stepId] ?? 0)
1767
1150
  const fails = Number.isFinite(counted) && counted > 0 ? counted : 0
@@ -1769,33 +1152,15 @@ window.__ModuleLoader__.load({
1769
1152
  }
1770
1153
 
1771
1154
  /**
1772
- * 把「计划 + 世界线」摊成带车道的行:脊柱 + fork 车道 + 收敛。纯函数,便于断言拓扑。
1773
- *
1774
- * 放弃过的分叉**画一行**(不铺车道):它是留档的事实,不是一条还在走的路;
1775
- * 原设计里那棵树也把它留着——「什么都不删」在图上就是「还在,但灰了」。
1776
- */
1777
- /**
1778
- * **对外聚焦通道**:别的面点一个步骤/世界线,世界树要能**选中那一行**。
1779
- *
1780
- * 为什么需要它:树原先只有**内部**选中状态(点行看详情),外面够不着 ⇒
1781
- * 「在世界树里看这一步」只能把树打开,不定位 ✗ —— 这是"点击跳转闭环"里缺的那一环。
1155
+ * **对外聚焦通道**:别的面点一个步骤,世界树要能**选中那一行**。
1782
1156
  *
1783
- * 只做两件最小的事:
1784
- * · 一个**纯函数** `rowIndexOf(rows, target)`:行身份 = `step.id`(+ 有分支时 `branch.id`),
1785
- * 返回行下标或 null(可单测 ✓);
1157
+ * · 一个**纯函数** `rowIndexOf(rows, target)`:行身份 = `step.id`,返回行下标或 null(可单测 ✓);
1786
1158
  * · 一个**订阅式小 store**:聚焦是**界面状态**,不是事实 —— 不写任何变更、不进账本 ✓。
1787
1159
  */
1788
1160
  function rowIndexOf(rows, target) {
1789
1161
  if (target === null || target === undefined || typeof target.step !== 'string' || target.step === '') return null
1790
- const sameStep = rows.map((row, index) => ({ row, index })).filter((item) => item.row?.step?.id === target.step)
1791
- if (sameStep.length === 0) return null
1792
- if (typeof target.branch === 'string' && target.branch !== '') {
1793
- const branch = sameStep.find((item) => item.row?.branch?.id === target.branch)
1794
- return branch === undefined ? null : branch.index
1795
- }
1796
- // 没指定分支:优先"代表这一步"的那一行(step / fork / abandoned),否则退到第一行
1797
- const head = sameStep.find((item) => item.row?.kind === 'step' || item.row?.kind === 'fork' || item.row?.kind === 'abandoned')
1798
- return (head ?? sameStep[0]).index
1162
+ const index = rows.findIndex((row) => row?.step?.id === target.step)
1163
+ return index === -1 ? null : index
1799
1164
  }
1800
1165
 
1801
1166
  /**
@@ -1835,86 +1200,28 @@ window.__ModuleLoader__.load({
1835
1200
  /**
1836
1201
  * 哪条命题的证据来自这一步(纯函数:可单测)。
1837
1202
  *
1838
- * 唯一的关系就是**步骤上登记的 `tests.hypothesis`** —— 不猜:这一步没登记命题
1203
+ * 唯一的关系就是**步骤上登记的 `tests.hypotheses`**(检验几条就取第一条展开)—— 不猜:这一步没登记命题
1839
1204
  * (自判的 L0 步骤常常没有)就返回 `null`,界面因此**不乱展开**任何一条命题 ✓。
1840
1205
  * (第一版这里写过一个"退一步看证据挂谁"的回退,而它又去查同一个索引 ⇒ 循环 ✗,已删。)
1841
1206
  */
1842
1207
  function propositionForStep(data, stepId) {
1843
1208
  if (typeof stepId !== 'string' || stepId === '') return null
1844
- const hypothesis = (data?.stepIndex ?? {})[stepId]?.tests?.hypothesis
1209
+ const hypothesis = testedOf((data?.stepIndex ?? {})[stepId])[0]
1845
1210
  return typeof hypothesis === 'string' && hypothesis !== '' ? hypothesis : null
1846
1211
  }
1847
1212
 
1848
- function treeRows(plan, forks) {
1849
- const rows = []
1850
- for (const step of plan?.steps ?? []) {
1851
- const fork = (forks ?? []).find((item) => item.stepId === step.id)
1852
- if (fork === undefined) {
1853
- rows.push({ kind: 'step', lane: 0, step })
1854
- continue
1855
- }
1856
- // 放弃过的分叉画成**一行灰掉的留档**(不是消失:什么都不删在图上就是「还在,但灰了」)。
1857
- // 判据是 fold 派生的 `phase === 'abandoned'`(它同时带 abandonReason)。
1858
- if (fork.phase === 'abandoned') {
1859
- rows.push({ kind: 'abandoned', lane: 0, step, fork })
1860
- continue
1861
- }
1862
- rows.push({ kind: 'fork', lane: 0, step, fork })
1863
- // 车道从 **1** 起(照抄原型):第 0 道是脊柱——第一条车道要是压在脊柱上,
1864
- // 扇出去的四条线会缩成一条往下掉的蛇。
1865
- fork.branches.forEach((branch, index) => {
1866
- rows.push({ kind: 'branch', lane: 1 + index, step, fork, branch })
1867
- })
1868
- rows.push({ kind: 'converge', lane: 0, step, fork })
1869
- }
1870
- return rows
1213
+ /** 计划摊成行:一步一行,全在脊柱上。纯函数,便于断言拓扑。 */
1214
+ function treeRows(plan) {
1215
+ return (plan?.steps ?? []).map((step) => ({ kind: 'step', lane: 0, step }))
1871
1216
  }
1872
1217
 
1873
- /**
1874
- * 行与行之间的**连接**(轨道):不是「相邻两行连一条」,而是显式的扇出/扇入。
1875
- *
1876
- * 按相邻行连线会把「脊柱裂成四条并行车道」——
1877
- * 被画成了一条往右下掉的**蛇**——分叉点只连到第一条车道,第二条连第三条……
1878
- * 那不是分叉,是排队。原设计用的是显式连接表(fork → 每条车道、每条车道 → 收敛菱形),
1879
- * 所以四条车道是从同一个点扇出去的。这里照它重写。
1880
- */
1218
+ /** 行与行之间的连接:相邻两步连一段脊柱。 */
1881
1219
  function treeConns(rows) {
1882
- const conns = []
1883
- let prevMain = null
1884
- let forkRow = null
1885
- let branchRows = []
1886
- rows.forEach((row, index) => {
1887
- if (row.kind === 'step' || row.kind === 'abandoned' || row.kind === 'fork') {
1888
- // 脊柱(含放弃的留档行)接上一段主线
1889
- if (prevMain !== null) conns.push({ from: prevMain, fromLane: 0, to: index, toLane: 0 })
1890
- prevMain = index
1891
- if (row.kind === 'fork') {
1892
- forkRow = index
1893
- branchRows = []
1894
- }
1895
- return
1896
- }
1897
- if (row.kind === 'branch') {
1898
- // 扇出:从**分叉点**连到这一条车道(不是从上一条车道连过来)
1899
- if (forkRow !== null) conns.push({ from: forkRow, fromLane: 0, to: index, toLane: row.lane })
1900
- branchRows.push({ idx: index, lane: row.lane })
1901
- return
1902
- }
1903
- // 收敛:每条车道各收一条线进来,然后主线继续
1904
- for (const branch of branchRows) conns.push({ from: branch.idx, fromLane: branch.lane, to: index, toLane: 0 })
1905
- branchRows = []
1906
- forkRow = null
1907
- prevMain = index
1908
- })
1909
- return conns
1220
+ return rows.slice(1).map((_, index) => ({ from: index, fromLane: 0, to: index + 1, toLane: 0 }))
1910
1221
  }
1911
1222
 
1912
- /**
1913
- * 轨道该在节点**边缘**收住,不进节点内部(照抄原型):圆形让 NODE_R + 描边,
1914
- * 收敛菱形按对角让 NODE_R·√2(它是旋转 45° 的方,竖直方向的顶点比半径远)。
1915
- * 两端切线都是竖直的,所以直接在 y 上让开即可,不必求切向。
1916
- */
1917
- const railClear = (row) => (row?.kind === 'converge' ? TREE.radius * Math.SQRT2 + 1 : TREE.radius + 1.5)
1223
+ /** 轨道在节点**边缘**收住,不进节点内部(照抄原型)。 */
1224
+ const railClear = () => TREE.radius + 1.5
1918
1225
 
1919
1226
  /**
1920
1227
  * 一条连接画成什么路径:同车道 = 直线;跨车道 = 三次贝塞尔(控制点与端点同 x,
@@ -1943,70 +1250,18 @@ window.__ModuleLoader__.load({
1943
1250
  const audits = context?.audits ?? []
1944
1251
  const planId = context?.planId ?? null
1945
1252
  const stepId = row.step?.id ?? null
1946
- const advanced = row.kind === 'branch' ? row.branch.status === 'adopted' : row.step?.status === 'advanced'
1253
+ const advanced = row.step?.status === 'advanced'
1947
1254
  const loops = treeLoops(blocks, planId, stepId, advanced)
1948
- // ① 填充:落定终局了吗(与原设计同一条既有谓词:adopted|pruned 已是终局,advanced|void 同理)
1949
- const settled =
1950
- row.kind === 'branch'
1951
- ? row.branch.status === 'adopted' || row.branch.status === 'pruned'
1952
- : row.kind === 'converge'
1953
- ? row.fork.phase === 'settled'
1954
- : row.step.status === 'advanced' || row.step.status === 'void'
1955
- // ③ 光环:此刻在动吗——两种事实,各自一条规则,都不猜
1956
- // · 虚线环 = 有 Evaluator 正在审这一步(audits 里 verdict 还没回来)
1957
- // · 实心呼吸 = 这条世界线还在探索(原设计的 run 级状态我们这侧没有,所以只画我们真有的)
1958
- const auditing = row.kind !== 'branch' && audits.some((audit) => audit.stepId === stepId && (audit.verdict === null || audit.verdict === undefined))
1959
- /**
1960
- * 光环 = 「此刻在动吗」。两个**必须**排除的假话:
1961
- * · 步骤已经作废/交付了,它上面那条车道不可能还在跑(分叉没跟着收口时会留下这种孤儿);
1962
- * · 分叉自己已经放弃了(它下面没有「在动」的东西)。
1963
- * 状态机那一侧该不该让分叉跟着步骤一起收口,是另一件事(记在 recon 里待定);
1964
- * 但**界面不许说假话**这一条不依赖那个决定。
1965
- */
1966
- const ownerAlive = row.step?.status !== 'void' && row.step?.status !== 'advanced'
1967
- // 失败的世界线不再「在动」:它死了,不是还在跑。
1968
- const dead = row.kind === 'branch' && row.branch.failed === true
1969
- /**
1970
- * 「执行者未归」也不在动:分叉已经收口(收敛或放弃),派出去的执行者**再回来也没有归宿**。
1971
- * 它既不是失败(没人知道它跑成没跑成),也不是在跑(「结论会自动回灌」那句承诺实现不了)——
1972
- * 所以它是第三个静止态:不判成败、不呼吸(不说破就会被读成还在跑)。
1973
- */
1974
- const unreturned = row.kind === 'branch' && row.branch.unreturned === true
1975
- const live = row.kind === 'branch' && row.branch.status === 'exploring' && ownerAlive && !dead && !unreturned
1255
+ // ① 填充:落定终局了吗(advanced | void)
1256
+ const settled = row.step.status === 'advanced' || row.step.status === 'void'
1257
+ // ③ 光环:有 Evaluator 正在审这一步(audits 里 verdict 还没回来)
1258
+ const auditing = audits.some((audit) => audit.stepId === stepId && (audit.verdict === null || audit.verdict === undefined))
1976
1259
  // ② 颜色:什么结局
1977
- const color =
1978
- row.kind === 'branch'
1979
- ? row.branch.failed === true
1980
- ? TREE_COLOR.failed
1981
- : row.branch.status === 'adopted'
1982
- ? TREE_COLOR.adopted
1983
- : row.branch.status === 'pruned'
1984
- ? TREE_COLOR.pruned
1985
- : row.branch.status === 'evaluated'
1986
- ? TREE_COLOR.evaluated
1987
- : row.fork.recommended === row.branch.id && row.fork.phase !== 'settled'
1988
- ? TREE_COLOR.recommend
1989
- : TREE_COLOR.exploring
1990
- : row.kind === 'converge'
1991
- ? row.fork.phase === 'settled'
1992
- ? TREE_COLOR.adopted
1993
- : row.fork.phase === 'deciding'
1994
- ? TREE_COLOR.recommend
1995
- : TREE_COLOR.exploring
1996
- : row.kind === 'abandoned'
1997
- ? TREE_COLOR.pruned
1998
- : row.step.status === 'advanced'
1999
- ? TREE_COLOR.done
2000
- : row.step.status === 'void'
2001
- ? TREE_COLOR.pruned
2002
- : TREE_COLOR.pending
2003
- // 变灰划掉:作废的步、落选的车道、放弃的分叉(单增:留档不删)
2004
- const greyed =
2005
- (row.kind === 'step' && row.step.status === 'void') ||
2006
- row.kind === 'abandoned' ||
2007
- (row.kind === 'branch' && (row.branch.status === 'pruned' || row.branch.failed === true || row.branch.orphaned === true))
2008
- const done = row.kind === 'step' && row.step.status === 'advanced'
2009
- return { settled, color, live, auditing, greyed, done, loops, ...loops }
1260
+ const color = row.step.status === 'advanced' ? TREE_COLOR.done : row.step.status === 'void' ? TREE_COLOR.pruned : TREE_COLOR.pending
1261
+ // 变灰划掉:作废的步(单增:留档不删)
1262
+ const greyed = row.step.status === 'void'
1263
+ const done = row.step.status === 'advanced'
1264
+ return { settled, color, live: false, auditing, greyed, done, loops, ...loops }
2010
1265
  }
2011
1266
 
2012
1267
  /**
@@ -2020,7 +1275,7 @@ window.__ModuleLoader__.load({
2020
1275
  const gap = total <= 1 ? 0 : Math.min(TREE.arcGap, slot * 0.5)
2021
1276
  return { strokeDasharray: `${slot - gap} ${circumference - slot + gap}`, strokeDashoffset: -(index * slot) }
2022
1277
  }
2023
- /** 要画的弧:只取闸门轮次,封顶 3 段(超出的轮数由行右的 `∞N` 徽标承载)。 */
1278
+ /** 要画的弧:只取闸门轮次,封顶 3 段(超出的轮数由行右的 「N 轮」承载)。 */
2024
1279
  function treeArcs(marks) {
2025
1280
  // 无事发生(一次就过)= 不画弧:节点保持一个干净的圆点。
2026
1281
  if (marks.fails === 0 && marks.rounds <= 1) return []
@@ -2054,27 +1309,26 @@ window.__ModuleLoader__.load({
2054
1309
  const chosen = wanted === null ? null : plans.find((item) => item.id === wanted) ?? null
2055
1310
  const plan = chosen ?? (data === null || data === undefined ? null : data.plan)
2056
1311
  if (plan === null || plan === undefined) {
2057
- return h('div', { style: S.wrap }, h('div', { style: S.bar }, h('span', { style: S.title }, t('世界树'))), h(Empty, null, t('暂无计划。建立后此处显示计划拓扑:脊柱步、车道与收敛点。')))
1312
+ return h('div', { style: S.wrap }, h('div', { style: S.bar }, h('span', { style: S.title }, t('世界树'))), h(Empty, null, t('暂无计划。建立后此处显示计划的步骤与闸门。')))
2058
1313
  }
2059
- const rows = treeRows(plan, data.forks)
1314
+ const rows = treeRows(plan)
2060
1315
  /** 页眉那一句:计划的首个非空行,过长再截(整篇在 tooltip 与「计划文档」里)。 */
2061
1316
  const briefLine = String(plan.brief ?? '').split('\n').map((line) => line.trim()).find((line) => line !== '' && !line.startsWith('#')) ?? ''
2062
1317
  const briefText = String(plan.brief ?? '').trim()
2063
- const titleText = briefLine === '' ? plan.id : `${brief(briefLine, 44)} · ${plan.id}`
1318
+ /** 计划编号(p-…)不上屏:人认的是那一句思路。 */
1319
+ const titleText = briefLine === '' ? '' : brief(briefLine, 44)
2064
1320
  /** 外面点进来的聚焦优先;手动点行仍然有效(聚焦为 null 时用它)。 */
2065
1321
  const focused = rowIndexOf(rows, focus)
2066
1322
  const selected = focused === null ? manual : focused
2067
1323
  const context = { blocks: data.blocks ?? null, audits: data.audits ?? [], planId: plan.id }
2068
- const laneCount = rows.reduce((max, row) => Math.max(max, row.lane), 0) + 1
2069
- const gutter = TREE.padX * 2 + laneCount * TREE.laneWidth
1324
+ const gutter = TREE.padX * 2 + TREE.laneWidth
2070
1325
  const x = (lane) => TREE.padX + lane * TREE.laneWidth
2071
1326
  const y = (index) => index * TREE.rowHeight + TREE.rowHeight / 2 + 4
2072
1327
  const marksOf = rows.map((row) => treeMarks(row, context))
2073
- // 轨道:显式连接表(扇出/扇入)。落选那条画虚线(3 3)用灰——「这条路走过了,但没走通」。
1328
+ // 轨道:相邻两步连一段脊柱;通向作废步的那段画灰。
2074
1329
  const svg = treeConns(rows).map((conn, index) => {
2075
- const to = rows[conn.to]
2076
1330
  const dashed = marksOf[conn.to].greyed
2077
- return h('path', { key: `rail-${index}`, d: railPath(conn, rows), fill: 'none', stroke: dashed ? TREE_COLOR.pruned : TREE_COLOR.rail, strokeWidth: 1, strokeDasharray: dashed && to.kind === 'branch' ? '3 3' : undefined })
1331
+ return h('path', { key: `rail-${index}`, d: railPath(conn, rows), fill: 'none', stroke: dashed ? TREE_COLOR.pruned : TREE_COLOR.rail, strokeWidth: 1 })
2078
1332
  })
2079
1333
  rows.forEach((row, index) => {
2080
1334
  const marks = marksOf[index]
@@ -2085,25 +1339,8 @@ window.__ModuleLoader__.load({
2085
1339
  // ③ 光环(先画,在节点下面)
2086
1340
  if (marks.auditing) {
2087
1341
  nodes.push(h('circle', { key: 'halo', cx, cy, r: TREE.halo, fill: 'none', stroke: TREE_COLOR.running, strokeWidth: 1.4, strokeDasharray: '2 2.5', className: 'clearai-breathe-stroke' }))
2088
- } else if (marks.live) {
2089
- nodes.push(h('circle', { key: 'halo', cx, cy, r: TREE.halo, fill: marks.color, className: 'clearai-breathe' }))
2090
1342
  }
2091
- if (row.kind === 'converge') {
2092
- nodes.push(
2093
- h('rect', {
2094
- key: 'node',
2095
- x: cx - TREE.radius,
2096
- y: cy - TREE.radius,
2097
- width: TREE.radius * 2,
2098
- height: TREE.radius * 2,
2099
- rx: 1,
2100
- transform: `rotate(45 ${cx} ${cy})`,
2101
- fill: marks.settled ? marks.color : 'transparent',
2102
- stroke: marks.color,
2103
- strokeWidth: 1.4,
2104
- }),
2105
- )
2106
- } else {
1343
+ {
2107
1344
  // ① 填充:落定的画实心核(有分段时核再小一圈,不与弧粘连)
2108
1345
  if (marks.settled) {
2109
1346
  nodes.push(h('circle', { key: 'core', cx, cy, r: arcs.length > 0 ? TREE.radius - TREE.arcWidth - 1 : TREE.radius, fill: marks.color }))
@@ -2133,42 +1370,12 @@ window.__ModuleLoader__.load({
2133
1370
  svg.push(h('g', { key: `node-${index}` }, nodes))
2134
1371
  })
2135
1372
  const counts = (row) => {
2136
- if (row.kind === 'branch' || row.kind === 'converge') return null
2137
1373
  const marks = treeMarks(row, context)
2138
- const stepId = row.step.id
2139
- const scouts = (data.scouts ?? []).filter((item) => item.stepId === stepId).length
2140
- const judges = (data.audits ?? []).filter((item) => item.stepId === stepId).length
2141
- return { marks, scouts, judges }
1374
+ const judges = (data.audits ?? []).filter((item) => item.stepId === row.step.id).length
1375
+ return { marks, judges }
2142
1376
  }
2143
- /**
2144
- * 行文只放**名字**,状态交给颜色/形状/标签说。但「失败」与「随步骤作废而终止」是两种
2145
- * 容易被误读成「还在跑」的状态,所以它们各带一个后缀(语义:
2146
- * 失败不是落选,孤儿不是被裁)。
2147
- */
2148
- const rowSuffix = (row) =>
2149
- row.kind === 'branch' && row.branch.failed === true
2150
- ? t(' · 执行没跑成')
2151
- : row.kind === 'branch' && row.branch.orphaned === true
2152
- ? t(' · 随步骤作废而终止')
2153
- : ''
2154
- const rowLabel = (row) =>
2155
- row.kind === 'branch'
2156
- ? `${row.branch.label || row.branch.id}${rowSuffix(row)}`
2157
- : row.kind === 'converge'
2158
- ? row.fork.phase === 'settled'
2159
- ? t('已采纳')
2160
- : row.fork.phase === 'orphaned'
2161
- ? t('未收口(随步骤作废而终止)')
2162
- : row.fork.phase === 'deciding'
2163
- ? t('待裁决')
2164
- : t('收敛')
2165
- : row.kind === 'abandoned'
2166
- ? `${t('已放弃探索:')}${brief(row.fork.question, 24)}`
2167
- : /**
2168
- * 行里只放**认得出这一步的那几个字**:`do` 常常是整句话(真数据里 ~100 字 ✗),
2169
- * 全文在点开的详情里。
2170
- */
2171
- `${row.step.ordinal}. ${brief(row.step.do, 26)}`
1377
+ /** 行里只放**认得出这一步的那几个字**:`do` 常常是整句话,全文在点开的详情里。 */
1378
+ const rowLabel = (row) => `${row.step.ordinal}. ${brief(row.step.do, 26)}`
2172
1379
  return h(
2173
1380
  'div',
2174
1381
  { style: S.wrap },
@@ -2210,7 +1417,7 @@ window.__ModuleLoader__.load({
2210
1417
  h(
2211
1418
  'span',
2212
1419
  { style: S.faint },
2213
- `${t('步 ')}${rows.filter((row) => row.kind === 'step' || row.kind === 'fork').length}${t(' · 车道 ')}${laneCount}`,
1420
+ `${t('步 ')}${rows.length}`,
2214
1421
  ),
2215
1422
  props.openPreview === undefined
2216
1423
  ? null
@@ -2218,12 +1425,12 @@ window.__ModuleLoader__.load({
2218
1425
  h('span', { style: { ...S.faint, marginLeft: 'auto' } }, t('点击查看详情')),
2219
1426
  ),
2220
1427
  /**
2221
- * 这一格是**计划的一切**:目标是脊柱的起点、拓扑是它的形状、
2222
- * 要你拍板的那一下(人门)就发生在某条车道上——所以三样都在这张图上,
1428
+ * 这一格是**计划的一切**:目标是脊柱的起点、步骤是它的形状、
1429
+ * 要你拍板的那一下(人门)就发生在某一步上——所以三样都在这张图上,
2223
1430
  * 而不是散在三个页签里(「进展」那一格因此撤了)。
2224
1431
  */
2225
1432
  h(GoalLine, { goal: data.goal, view: data.knowledgeView, openPreview: props.openPreview }),
2226
- h(Inbox, { data }),
1433
+ h(NeedYou, { data }),
2227
1434
  h(
2228
1435
  'div',
2229
1436
  { style: { display: 'flex', gap: 8, alignItems: 'flex-start' } },
@@ -2258,28 +1465,24 @@ window.__ModuleLoader__.load({
2258
1465
  overflow: 'hidden',
2259
1466
  textOverflow: 'ellipsis',
2260
1467
  whiteSpace: 'nowrap',
2261
- fontWeight: row.kind === 'converge' || row.kind === 'fork' ? 600 : 400,
1468
+ fontWeight: 400,
2262
1469
  ...(marks.greyed
2263
1470
  ? { color: 'var(--dsw-alias-label-secondary)', opacity: 0.55, textDecoration: 'line-through' }
2264
1471
  : marks.done
2265
- ? { opacity: 0.6, textDecoration: 'line-through' }
1472
+ ? { color: 'var(--dsw-alias-label-secondary)' }
2266
1473
  : {}),
2267
1474
  },
2268
1475
  },
2269
1476
  rowLabel(row),
2270
1477
  ),
2271
- row.kind === 'branch' && row.fork.recommended === row.branch.id && row.fork.phase !== 'settled' ? h('span', { style: { color: TREE_COLOR.recommend, flex: '0 0 auto' }, title: t('按判据推荐') }, '★') : null,
2272
- row.kind === 'branch' && row.branch.reading !== null && row.branch.reading !== undefined
2273
- ? h('span', { style: { ...S.mono, color: marks.color, flex: '0 0 auto' }, title: `${t('读数(尺子 ')}${row.fork.decideBy?.metric ?? '—'})` }, String(row.branch.reading))
2274
- : null,
2275
1478
  stat === null ? null : h(
2276
1479
  'span',
2277
1480
  { style: { flex: '0 0 auto', display: 'flex', gap: 6, ...S.faint, marginLeft: 'auto' } },
2278
- stat.marks.loops.rounds > 0
2279
- ? h('span', { title: `${stat.marks.loops.rounds}${t(' 轮')}${stat.marks.loops.fails > 0 ? ` · 其中 ${stat.marks.loops.fails} 次被驳回` : ''}` }, `∞${stat.marks.loops.rounds}`)
1481
+ /** 说人话:只有返工过才说「做了 N 轮」;起过独立核验就写「独立核验」。 */
1482
+ stat.marks.loops.rounds > 1
1483
+ ? h('span', { title: `${stat.marks.loops.rounds}${t(' 轮')}${stat.marks.loops.fails > 0 ? ` · 其中 ${stat.marks.loops.fails} 次被驳回` : ''}` }, `${stat.marks.loops.rounds}${t(' 轮')}`)
2280
1484
  : null,
2281
- stat.scouts > 0 ? h('span', { title: `${t('派出 ')}${stat.scouts}${t(' 次侦察')}` }, `${t('侦 ')}${stat.scouts}`) : null,
2282
- stat.judges > 0 ? h('span', { title: `${t('起过 ')}${stat.judges}${t(' 次评估者')}` }, `${t('评 ')}${stat.judges}`) : null,
1485
+ stat.judges > 0 ? h('span', { title: `${t('起过 ')}${stat.judges}${t(' 次评估者')}` }, t('独立核验')) : null,
2283
1486
  ),
2284
1487
  )
2285
1488
  }),
@@ -2289,92 +1492,22 @@ window.__ModuleLoader__.load({
2289
1492
  )
2290
1493
  }
2291
1494
 
2292
- /**
2293
- * 树下的详情:选中行的**事实**都在这里(树上只有形状,文字细节在这里)。
2294
- *
2295
- * 分叉待裁决时,这里也是**给人的那两个动作**的落点(与原设计一致:
2296
- * 「采纳、改判、放弃探索的动作都在 PlanTree 里」)。放弃必须带缘由——
2297
- * 留痕可考,不是摆设(原设计用对话框强制必填,这里用同一个纪律:没缘由按钮不生效)。
2298
- */
1495
+ /** 树下的详情:选中那一步的**事实**都在这里(树上只有形状,文字细节在这里)。 */
2299
1496
  function TreeDetail(props) {
2300
1497
  const row = props.row
2301
1498
  const data = props.data ?? {}
2302
- const [note, setNote] = React.useState('')
2303
- const [busy, setBusy] = React.useState(null)
2304
- const [error, setError] = React.useState(null)
2305
- const send = (action, extra) => {
2306
- setBusy(action)
2307
- setError(null)
2308
- fetch('/api/clearai/gate', {
2309
- method: 'POST',
2310
- headers: { 'content-type': 'application/json' },
2311
- body: JSON.stringify({ sessionId: data.sessionId, action, plan: data.plan?.id ?? null, step: row.step?.id ?? null, note: note.trim() === '' ? null : note.trim(), ...extra }),
2312
- })
2313
- .then(readResponse)
2314
- .then((result) => {
2315
- if (result.ok !== true) setError(result.error)
2316
- })
2317
- .catch((thrown) => setError(String(thrown?.message ?? thrown)))
2318
- .finally(() => setBusy(null))
2319
- }
2320
1499
  const step = row.step
2321
- const fork = row.fork ?? null
2322
1500
  const evidence = (data.evidence ?? []).filter((item) => item.stepId === step?.id)
2323
1501
  const audits = (data.audits ?? []).filter((item) => item.stepId === step?.id)
2324
- const scouts = (data.scouts ?? []).filter((item) => item.stepId === step?.id)
2325
- const branch = row.kind === 'branch' ? row.branch : null
2326
- const deciding = fork !== null && fork.phase === 'deciding' && fork.abandonReason === null
2327
1502
  return h(
2328
1503
  'div',
2329
1504
  { style: { marginTop: 10, borderTop: '.5px solid var(--dsw-alias-border-l1)', paddingTop: 8 } },
2330
1505
  h(
2331
1506
  'div',
2332
1507
  { style: S.inline },
2333
- h('span', { style: S.head }, row.kind === 'branch' ? `${t('车道 · ')}${branch.label || branch.id}` : row.kind === 'converge' ? t('收敛') : row.kind === 'abandoned' ? t('已放弃的分叉') : `${t('步 ')}${step.ordinal} · ${step.id}`),
1508
+ h('span', { style: S.head, title: step.id }, `${nth(step.ordinal)}`),
2334
1509
  h(Link, { onClick: () => props.onClose?.(), title: t('收起详情') }, h('span', { style: S.faint }, t('收起'))),
2335
1510
  ),
2336
- row.kind === 'abandoned'
2337
- ? h('div', { style: S.faint }, `${t('问题:')}${fork.question}${t(';放弃缘由:')}${fork.abandonReason ?? '未记'}${t('(留档不删)')}`)
2338
- : null,
2339
- fork?.orphaned === true
2340
- ? h(
2341
- 'div',
2342
- { style: S.faint },
2343
- `${t('这一步已作废(缘由:')}${fork.ownerVoidReason ?? '未记'}${t('),所以这个分叉没有归宿了:**随步骤作废而终止**——它既不是被人裁掉(没人做过这个决定),也不是被算术排掉(没有尺子排过它)。要收掉工作副本(保留 ref),对它调 AbandonFork(会弹人工确认)。')}`,
2344
- )
2345
- : null,
2346
- branch?.failed === true
2347
- ? h(
2348
- 'div',
2349
- { style: S.faint },
2350
- `${t('**执行没跑成**(')}${branch.execution?.note ?? 'failed'}${t(')——它不是落选:落选意味着它跑完了、被尺子排到了后面;这一条是没跑成,所以也没有读数可以参与算术。')}${branch.execution?.conclusion === null || branch.execution?.conclusion === undefined || branch.execution.conclusion === '' ? '' : `执行者最后说的话:${String(branch.execution.conclusion).slice(0, 160)}`}`,
2351
- )
2352
- : null,
2353
- branch?.unreturned === true
2354
- ? h(
2355
- 'div',
2356
- { style: S.faint },
2357
- t('**执行者未归**:这条世界线的执行者派出去之后没有回灌,而分叉已经收口——它再回来也没有归宿了,所以这里既不判它跑成也没跑成,也不再等它。它的产物(如果写过)还在它自己的工作副本里;要判断那条线到底做出了什么,直接看工作副本比看这条状态可靠。'),
2358
- )
2359
- : null,
2360
- branch !== null
2361
- ? h(
2362
- 'div',
2363
- { style: S.kv },
2364
- h('span', { style: S.faint }, t('做法')), h('span', null, branch.approach ?? '—'),
2365
- h('span', { style: S.faint }, t('判据')), h('span', null, branch.doneCriteria ?? '—'),
2366
- h('span', { style: S.faint }, t('读数')), h('span', { style: S.mono }, branch.reading === null || branch.reading === undefined ? '—' : `${branch.reading}${branch.validity === 'usable' ? '' : '(不可用)'}`),
2367
- h('span', { style: S.faint }, t('状态')), h('span', null, `${gloss(BRANCH_STATUS, branch.status)}${branch.verdict === null || branch.verdict === undefined ? '' : ` · 结论 ${branch.verdict}`}`),
2368
- branch.basis === null || branch.basis === undefined ? null : h('span', { style: S.faint }, t('依据')),
2369
- branch.basis === null || branch.basis === undefined ? null : h('span', null, branch.basis),
2370
- branch.gitBranch === null || branch.gitBranch === undefined ? null : h('span', { style: S.faint }, t('分支')),
2371
- branch.gitBranch === null || branch.gitBranch === undefined ? null : h('span', { style: S.mono }, `${branch.gitBranch}${branch.worktreeRemoved === true ? '(工作副本已清理,ref 保留)' : ''}`),
2372
- branch.evaluatorSession === null || branch.evaluatorSession === undefined ? null : h('span', { style: S.faint }, t('评估者')),
2373
- branch.evaluatorSession === null || branch.evaluatorSession === undefined
2374
- ? null
2375
- : h(Link, { onClick: () => props.openSpectator?.(branch.evaluatorSession), title: t('旁观这条世界线的评估者会话(只读)') }, h('span', { style: S.mono }, branch.evaluatorSession)),
2376
- )
2377
- : null,
2378
1511
  step === null || step === undefined
2379
1512
  ? null
2380
1513
  : h(
@@ -2400,690 +1533,72 @@ window.__ModuleLoader__.load({
2400
1533
  * 「没查过」不许说成「缺」(真数据里树详情因此把每个产物都写成 `undefined(缺)` ✗)。
2401
1534
  */
2402
1535
  const path = typeof artifact === 'string' ? artifact : artifact?.path
2403
- const exists = typeof artifact === 'string' ? null : (artifact?.exists ?? null)
2404
- if (typeof path !== 'string' || path === '') return null
2405
- return h(
2406
- 'span',
2407
- { key: `${path}-${index}`, style: { marginRight: 8 } },
2408
- exists === false
2409
- ? h('span', { style: { ...S.mono, opacity: 0.6 }, title: t('盘上没有这个文件') }, `${path}${t('(缺)')}`)
2410
- : h(Link, { onClick: () => props.openPreview?.(path), title: exists === true ? t('原生预览打开它') : t('原生预览打开它(计划声明的产物)') }, h('span', { style: S.mono }, path)),
2411
- )
2412
- }),
2413
- ),
2414
- /**
2415
- * **反向跳**:从树里的这一步跳到「事实」那一格,并展开对应的命题。
2416
- * 切中栏视图走**原生正门** selectPanel(经可选读法拿到的 layout 服务;守卫:
2417
- * 没注册的 key 它会抛,而抛了不该把整棵树带下水)。
2418
- */
2419
- props.openFacts === undefined
2420
- ? null
2421
- : h('span', { style: S.faint }, t('证据')),
2422
- props.openFacts === undefined
2423
- ? null
2424
- : h(
2425
- 'span',
2426
- null,
2427
- h(
2428
- Link,
2429
- {
2430
- onClick: () => props.openFacts(step.id),
2431
- title: t('打开「事实」那一格并展开这条命题'),
2432
- },
2433
- t('查看此步骤的证据'),
2434
- ),
2435
- ),
2436
- ),
2437
- evidence.length === 0 && audits.length === 0 && scouts.length === 0
2438
- ? null
2439
- : h(
2440
- 'div',
2441
- { style: { marginTop: 6 } },
2442
- evidence.map((item) => h('div', { key: item.id, style: S.faint }, `${t('证据 ')}${item.id} · ${item.verdict ?? '—'}${item.level === null || item.level === undefined ? '' : ` · ${item.level}`} · ${item.evaluator ?? '—'}`)),
2443
- audits.map((item) =>
2444
- h(
2445
- 'div',
2446
- { key: item.id, style: S.faint },
2447
- `${t('评估者 ')}${item.verdict === null || item.verdict === undefined ? '正在裁决' : item.verdict} · ${item.evaluator ?? '—'}`,
2448
- item.evaluatorSession === null || item.evaluatorSession === undefined ? null : h(Link, { onClick: () => props.openSpectator?.(item.evaluatorSession) }, h('span', { style: S.mono }, `${t(' 旁观 ')}${item.evaluatorSession}`)),
2449
- item.cardPath === null || item.cardPath === undefined ? null : h(Link, { onClick: () => props.openPreview?.(item.cardPath) }, h('span', { style: S.mono }, t(' 评估卡'))),
2450
- ),
2451
- ),
2452
- scouts.map((item) => h('div', { key: item.id, style: S.faint }, `${t('侦察 · ')}${gloss(SCOUT, item.status)}${item.conclusion === null || item.conclusion === undefined ? '' : `:${String(item.conclusion).slice(0, 80)}`}`)),
2453
- ),
2454
- fork === null
2455
- ? null
2456
- : h(
2457
- 'div',
2458
- { style: { marginTop: 6 } },
2459
- h('div', { style: S.faint }, `${t('世界线:')}${fork.question}${t(' · 尺子 ')}${fork.decideBy?.metric ?? '—'}(${fork.decideBy?.direction === 'min' ? '越小越好' : '越大越好'}) · ${fork.phase === 'settled' ? '已收敛' : fork.phase === 'deciding' ? '待裁决' : '探索中'}`),
2460
- fork.verdict === null || fork.verdict === undefined
2461
- ? null
2462
- : h('div', { style: S.faint }, `${t('裁决:采纳 ')}${fork.verdict.winner}${fork.verdict.tie === true ? '(并列)' : ''}${fork.verdict.margin === null || fork.verdict.margin === undefined ? '' : ` · 余量 ${fork.verdict.margin}`}${fork.provisional === true ? ' · 临时采纳,待复核' : ''}`),
2463
- // 采纳了但没合并:如实说(账上写过分支、后来对象没了,合并就做不到)。
2464
- fork.mergeSkipped === null || fork.mergeSkipped === undefined
2465
- ? null
2466
- : h('div', { style: S.faint }, `${t('这次采纳没有合并:')}${fork.mergeSkipped.reason ?? t('分支或工作副本已不在')}${t('(决定已登记,产物由一次普通交付落位)')}`),
2467
- fork.humanDecision === null || fork.humanDecision === undefined ? null : h('div', { style: S.faint }, `${t('人的裁决:')}${gloss(FORK_ACTION, fork.humanDecision.action)}${fork.humanDecision.note === null || fork.humanDecision.note === undefined ? '' : ` · ${fork.humanDecision.note}`}`),
2468
- ),
2469
- deciding
2470
- ? h(
2471
- 'div',
2472
- { style: { marginTop: 6, display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' } },
2473
- h(Chip, { disabled: busy !== null, onClick: () => send('adopt_branch', { fork: fork.id, branch: branch === null ? fork.recommended ?? null : branch.id }), title: t('由内核执行 ConvergeFork 完成合并') }, busy === 'adopt_branch' ? t('提交中…') : t('采纳此世界线')),
2474
- h('input', {
2475
- value: note,
2476
- placeholder: t('放弃缘由(必填)'),
2477
- onChange: (event) => setNote(event.target.value),
2478
- style: { fontSize: 11.5, padding: '2px 6px', borderRadius: 6, border: '.5px solid var(--dsw-alias-border-l3)', background: 'transparent', color: 'inherit', minWidth: 120 },
2479
- }),
2480
- h(
2481
- Chip,
2482
- {
2483
- disabled: busy !== null || note.trim() === '',
2484
- onClick: () => send('abandon_fork', { fork: fork.id }),
2485
- title: note.trim() === '' ? t('放弃要留痕:先写缘由') : t('放弃这次探索(留档不删,ref 保留)'),
2486
- },
2487
- busy === 'abandon_fork' ? t('提交中…') : t('放弃探索'),
2488
- ),
2489
- error === null ? null : h('span', { style: S.faint }, `${t('提交失败:')}${error}`),
2490
- )
2491
- : null,
2492
- )
2493
- }
2494
- /**
2495
- * 技能目录的**来源分组**(人此刻在问「我现在能用哪些技能」)。
2496
- *
2497
- * 分组表是**照抄宿主的来源名**,不是我们自己编的层次;表外的来源如实显示成
2498
- * `其他(<来源>)`——宁可见到一个陌生的名字,也不把一条技能藏起来。
2499
- */
2500
- const SOURCE_GROUPS = lazyTable(() => ([
2501
- { test: (source) => source === 'bundled', label: t('预设自带'), order: 10 },
2502
- { test: (source) => source === 'clearai-template', label: t('工作区模板 · clear/skills'), order: 20 },
2503
- { test: (source) => source === 'clearai-workspace', label: t('本项目写的 · clear/skills'), order: 30 },
2504
- { test: (source) => source === 'project-dsh' || source === 'project-agents', label: t('本项目 · .dsh / .agents'), order: 40 },
2505
- { test: (source) => source === 'user-dsh' || source === 'user-agents', label: t('我的 · ~/.dsh'), order: 50 },
2506
- { test: (source) => source === 'clearai-memory', label: t('记忆索引'), order: 60 },
2507
- { test: (source) => source === 'runtime', label: t('运行时注册'), order: 70 },
2508
- { test: (source) => source === 'custom', label: t('自定义'), order: 80 },
2509
- ]))
2510
- const groupOf = (source) => SOURCE_GROUPS.find((group) => group.test(source)) ?? { label: `${t('其他(')}${source})`, order: 90 }
2511
- /** 一步的当前结果(指针的落点):用量行要说清「那一步现在怎么样了」。 */
2512
- const STEP_OUTCOME = lazyTable(() => ({ advanced: t('已交付'), open: t('还没交付'), void: t('已作废') }))
2513
-
2514
- /**
2515
- * 技能 · 记忆页签(右栏)。
2516
- *
2517
- * 技能清单 = **宿主原生的合并目录**(`projectionValues.clearai.skills.catalog`)——
2518
- * 内核每个 pre-step 调一次 `ctx.skills.snapshot`,于是面板与模型看的是**同一张表**
2519
- * (预设自带 / 我的 / 本项目的 / clear/skills 里的候选,合并后按 rank 取赢家)。
2520
- *
2521
- * 用法 = **本会话的加载记录**(`skills.usage`),从日志折出来的:模型加载是
2522
- * `tool/call name='skill'`,人引用是一条含 `/名字` 的用户消息。给的是**指针**
2523
- * (谁在第几步加载的、那一步现在什么结果),不是跨会话的比率——判据混杂的比率会冒充事实
2524
- * 归并而不是各写一份。
2525
- *
2526
- * 写动作只留**一个**,而且它走既有的原生机制:
2527
- * · 「采纳」→ 人门动词 `promote_skill` → 内核改写 frontmatter(`status: active`)。
2528
- * 引用走**原生**的 `/` 技能触发器(输入框里打 `/`,出候选菜单,选一条即注入正文)——
2529
- * 我们原来在旁边又放了一个「引用」按钮,做的是同一件事,已砍掉(奥卡姆:重复不是能力)。
2530
- */
2531
- function BrainTab(props) {
2532
- const sessionId = props.sessionId
2533
- const sessions = props.useSessions
2534
- const data =
2535
- typeof sessions === 'function' && typeof sessionId === 'string'
2536
- ? sessions((snapshot) => snapshot?.byId?.[sessionId]?.projectionValues?.clearai)
2537
- : undefined
2538
- const brain = data === null || data === undefined ? null : data.brain
2539
- const skills = data === null || data === undefined || data.skills === null || data.skills === undefined ? null : data.skills
2540
- const catalog = skills === null ? null : skills.catalog
2541
- const usage = new Map((skills !== null && Array.isArray(skills.usage) ? skills.usage : []).map((item) => [item.name, item]))
2542
- const [error, setError] = React.useState(null)
2543
- const [busy, setBusy] = React.useState(false)
2544
- /**
2545
- * 投影里还没有目录时,去问**工作区现状**(宿主读面 `GET /api/clearai/brain`)。
2546
- *
2547
- * 为什么要有这一条:面板的数据来自会话日志,而内核只在 pre-step 里落事实——
2548
- * 新建的会话在第一轮对话之前**没有任何日志**,于是页签是空的,可工作区里其实
2549
- * 已经躺着模板技能。这条读面直接问宿主的 skills 服务,
2550
- * 拿到的就是模型看到的那张合并目录;渲染仍走同一套分组,并如实标注「还没进投影」。
2551
- */
2552
- const [live, setLive] = React.useState(null)
2553
- const needsLive = catalog === null
2554
- React.useEffect(() => {
2555
- if (!needsLive || typeof sessionId !== 'string') return undefined
2556
- let alive = true
2557
- fetch(`/api/clearai/brain?sessionId=${encodeURIComponent(sessionId)}`)
2558
- .then(readResponse)
2559
- .then((result) => {
2560
- if (alive && result.ok === true && Array.isArray(result.payload?.entries)) setLive({ entries: result.payload.entries })
2561
- })
2562
- .catch(() => {})
2563
- return () => {
2564
- alive = false
2565
- }
2566
- }, [sessionId, needsLive])
2567
- const fromLive = catalog === null && live !== null
2568
- const entries = catalog !== null && Array.isArray(catalog.entries) ? catalog.entries : fromLive ? live.entries : null
2569
- /** 宿主装没装「用编辑器打开」那条原生路由:装了就多给一个入口,没装就不显示。 */
2570
- const [apps, setApps] = React.useState(null)
2571
-
2572
- React.useEffect(() => {
2573
- let alive = true
2574
- fetch('/open-in-app/apps')
2575
- .then((response) => (response.ok ? response.json() : null))
2576
- .then((payload) => {
2577
- if (alive) setApps(Array.isArray(payload?.apps) ? payload.apps.filter((id) => typeof id === 'string') : [])
2578
- })
2579
- .catch(() => {
2580
- if (alive) setApps([])
2581
- })
2582
- return () => {
2583
- alive = false
2584
- }
2585
- }, [])
2586
-
2587
- const promote = (name, viaAsk = false) => {
2588
- setBusy(true)
2589
- fetch('/api/clearai/gate', {
2590
- method: 'POST',
2591
- headers: { 'content-type': 'application/json' },
2592
- body: JSON.stringify(viaAsk ? { sessionId, action: 'ask', gate: 'promote_skill', skill: name } : { sessionId, action: 'promote_skill', skill: name }),
2593
- })
2594
- .then(readResponse)
2595
- .then((result) => {
2596
- if (result.ok !== true) setError(result.error)
2597
- })
2598
- .catch((thrown) => setError(String(thrown?.message ?? thrown)))
2599
- .finally(() => setBusy(false))
2600
- }
2601
-
2602
- /**
2603
- * 「引用」按钮已删掉(人门动词 `invoke_skill` 一并摘除)。
2604
- *
2605
- * 原生的 `/` 技能触发器做的正是同一件事,而且更好:`dsh-client-ui-skill` 注册
2606
- * `trigger: '/'`,`dsh-client-ui-input-trigger` 出候选菜单,选一条就把技能正文
2607
- * 作为指令注入这一回合——我们的按钮不过是在旁边又写了一遍同一个手势。
2608
- * 少一个入口 = 少一处会与原生菜单走偏的语义(面板仍保留「采纳」:那是模型做不到的写动作)。
2609
- */
2610
- /** 工作区外的技能(比如 `~/.dsh/skills`):面板读不了,交给宿主的「用编辑器打开」原生路由。 */
2611
- const openInApp = (dir) => {
2612
- const app = Array.isArray(apps) ? apps[0] : undefined
2613
- if (typeof app !== 'string') return
2614
- fetch('/open-in-app/open', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ app, path: dir }) })
2615
- .then((response) => {
2616
- if (!response.ok) setError(`${t('打开失败:HTTP ')}${response.status}`)
2617
- })
2618
- .catch((thrown) => setError(String(thrown?.message ?? thrown)))
2619
- }
2620
- /**
2621
- * 点技能名 → **原生**文档预览(markdown 会渲染成 markdown,不是源码)。
2622
- *
2623
- * 用内核随投影发下来的 `file`(技能目录下的 SKILL.md):虚拟条目(记忆索引)没有正文文件,
2624
- * `file` 是 null,面板就不画链接(点进去撞 404 是踩过的坑)。
2625
- */
2626
- const pointAt = (skill) => {
2627
- if (typeof skill.file !== 'string') return
2628
- if (props.openPreview?.(skill.file) !== true) setError(t('原生预览打不开这条路(它在工作区外?)'))
2629
- }
2630
-
2631
- if (brain === null || brain === undefined) {
2632
- // 两种情况要分开说:会话还没开始(没有第一轮对话 = 还没有任何事实)vs 真的没有技能或记忆。
2633
- const note =
2634
- data === undefined
2635
- ? t('暂无数据。发送第一条消息后,此处显示技能目录与记忆。')
2636
- : t('暂无技能或记忆。使用 `SaveSkill` / `WriteMemory`,或将技能放入 `clear/skills/`。')
2637
- return h('div', { style: S.wrap }, h('div', { style: S.bar }, h('span', { style: S.title }, t('技能 · 记忆'))), h(Empty, null, note))
2638
- }
2639
- const overview = Array.isArray(brain.skills) ? brain.skills : []
2640
- const memory = brain.memory ?? { count: 0, files: [] }
2641
- /** 章程读数(内核扫的)+ 打开它的那条原生预览通道。 */
2642
- const constitution = data === null || data === undefined ? null : (data.constitution ?? null)
2643
- const openAt = (path) => props.openPreview?.(path)
2644
- const overviewOf = new Map(overview.map((skill) => [skill.name, skill]))
2645
- // 目录还没到(内核下一次 pre-step 才发):如实说,不拿 brain.skills 冒充合并目录。
2646
- const listed = entries === null ? overview.map((skill) => ({ name: skill.name, description: skill.description, source: skill.status === 'candidate' ? 'clearai-workspace' : 'clearai-template', model: skill.status !== 'candidate', user: true, dir: null, inside: null, stale: true })) : entries
2647
- const groups = []
2648
- for (const skill of listed) {
2649
- const group = groupOf(String(skill.source ?? 'unknown'))
2650
- const bucket = groups.find((item) => item.label === group.label) ?? { label: group.label, order: group.order, items: [] }
2651
- if (!groups.includes(bucket)) groups.push(bucket)
2652
- bucket.items.push(skill)
2653
- }
2654
- groups.sort((left, right) => left.order - right.order || left.label.localeCompare(right.label))
2655
- const quotedNames = new Set(listed.map((skill) => skill.name))
2656
- const orphanUsage = (skills !== null && Array.isArray(skills.usage) ? skills.usage : []).filter((item) => !quotedNames.has(item.name))
2657
- const usageLine = (item) => {
2658
- if (item === undefined) return null
2659
- const last = item.last
2660
- const pointer = last === null ? '—' : `${last.by === 'model' ? '模型' : '人'}${last.ordinal === null ? '' : ` · 第 ${last.ordinal} 步`}${last.outcome === null || last.outcome === undefined ? '' : ` · ${STEP_OUTCOME[last.outcome] ?? last.outcome}`}`
2661
- return `${t('本会话 模型 ')}${item.model}${t(' · 人 ')}${item.human}${t(' · 最近 ')}${pointer}`
2662
- }
2663
- const row = (skill) => {
2664
- const own = overviewOf.get(skill.name)
2665
- const candidate = own?.status === 'candidate'
2666
- const item = usage.get(skill.name)
2667
- const canOpen = typeof skill.file === 'string'
2668
- return h(
2669
- 'div',
2670
- { key: skill.name, style: S.row },
2671
- h(
2672
- 'div',
2673
- { style: S.inline },
2674
- candidate ? h('span', { style: S.tagStrong }, t('候选')) : null,
2675
- skill.model === false && !candidate ? h('span', { style: S.tag }, t('仅人可引用')) : null,
2676
- h(
2677
- 'span',
2678
- {
2679
- style: S.mono,
2680
- className: canOpen ? 'clearai-link' : undefined,
2681
- onClick: canOpen ? () => pointAt(skill) : undefined,
2682
- title: [
2683
- String(skill.description ?? '').replace(/\s+/g, ' ').trim(),
2684
- own === undefined ? '' : `${own.bytes}${t(' 字节 · 资源 ')}${own.resources} · ${dash(own.tier ?? own.version)}`,
2685
- ].filter((line) => line !== '').join('\n'),
2686
- },
2687
- skill.name,
2688
- ),
2689
- /**
2690
- * 机器读数(字节 / 资源数 / 层级)**不渲染**:它们不帮人认出一条技能,
2691
- * 20 条排下来就是噪声(真数据这一格 4741 字里很大一份是它们 ✗),
2692
- * 而且藏成 `display:none` 也还是 DOM 里的噪声 ⇒ 干脆不画,挂到技能名的 tooltip。
2693
- */
2694
- candidate ? h(Chip, { disabled: busy, onClick: () => promote(skill.name), title: t('采纳:改写 frontmatter,模型从此加载得到它') }, t('采纳')) : null,
2695
- // 混合路径:同一件事也可以摆到原生提问卡上答(卡里带它的描述与后果)。
2696
- candidate ? h('span', { className: busy ? undefined : 'clearai-link', style: S.faint, onClick: busy ? undefined : () => promote(skill.name, true), title: t('通过提问卡决定') }, t('通过提问卡决定')) : null,
2697
- ),
2698
- /**
2699
- * 描述只留**认得出这条技能的那一句**(真数据里每条是一整段「适用/不适用」✗,
2700
- * 18 条一起看就是信息爆炸)。全文在技能名的 tooltip 里,要选技能用原生的 `/` 触发菜单。
2701
- */
2702
- skill.description === null || skill.description === undefined || skill.description === ''
2703
- ? null
2704
- : h('div', { style: S.faint }, brief(skill.description, 60)),
2705
- usageLine(item) === null ? null : h('div', { style: S.dim }, usageLine(item)),
2706
- // 工作区外的技能:面板读不到正文(路径守卫),所以如实给绝对路径 + 宿主的编辑器入口。
2707
- skill.inside === false && typeof skill.dir === 'string' ? h('div', { style: S.faint }, `${skill.dir}${t(' · 在工作区外,面板不读正文')}`) : null,
2708
- skill.inside === false && typeof skill.dir === 'string' && Array.isArray(apps) && typeof apps[0] === 'string'
2709
- ? h(Chip, { onClick: () => openInApp(skill.dir) }, `${t('在 ')}${apps[0]}${t(' 里打开')}`)
2710
- : null,
2711
- )
2712
- }
2713
- return h(
2714
- 'div',
2715
- { style: S.wrap },
2716
- h(
2717
- 'div',
2718
- { style: S.bar },
2719
- h('span', { style: S.title }, t('技能 · 记忆')),
2720
- /**
2721
- * 计数在下面各段标题里(同屏重复计数 = 噪声);
2722
- * 「与模型看到的是同一张表」是实现保证、不是用户信息,与事实那一格同一条规矩 → 去掉。
2723
- * 只留**状态**:目录到没到、这份是不是"本会话还没有第一轮对话"的工作区现状。
2724
- */
2725
- entries === null || fromLive ? h('span', { style: S.faint }, entries === null ? t('目录尚未到达(内核下一次 pre-step 下发)') : t('工作区现状(首轮对话后进入投影)')) : null,
2726
- ),
2727
- error !== null ? h('div', { style: S.faint }, `${t('面板动作失败:')}${error}`) : null,
2728
- /**
2729
- * 章程那一行(这个页签的第一层):**只有文件系统事实** + 一个点开就走的原生预览。
2730
- *
2731
- * 为什么不再有「占位 X/Y 条」:那是对文本做格式解析
2732
- * ——模型把章程第 5 节写成 `- **变更记录**(一行即…)`,条目数就会数错,
2733
- * **改一个标点「事实」就变**;而且它奖励的是「把数字清掉」而不是「把章程写实」。
2734
- * 章程每回合本来就被原生指令文件整份注入模型上下文,面板这边只需要说清
2735
- * 「它在哪、什么时候动过」——`mtime` 不依赖任何格式约定。
2736
- */
2737
- constitution === null || constitution === undefined
2738
- ? null
2739
- : h(
2740
- 'div',
2741
- { style: S.section },
2742
- h('div', { style: S.head }, t('项目章程')),
2743
- h(
2744
- 'div',
2745
- { style: S.inline },
2746
- /**
2747
- * 文件名只在**链接**上说一次(原来段标题里还写一遍 ⇒ 同一行同一路径出现两遍 ✗)。
2748
- * 点它是**动作**(原生预览),字节数与改动时间是文件系统事实 —— 这些留着有意义。
2749
- */
2750
- h(
2751
- Link,
2752
- { onClick: () => openAt(constitution.legacy === true ? 'clear/project.md' : 'PROJECT.md'), title: t('用原生预览打开章程(它每回合整份注入模型上下文)') },
2753
- h('span', { style: S.mono }, constitution.legacy === true ? 'clear/project.md' : 'PROJECT.md'),
2754
- ),
2755
- h('span', { style: S.faint }, `${t('最后改动 ')}${stampOf(constitution.modifiedAt)} · ${constitution.bytes}${t(' 字节')}`),
2756
- ),
2757
- ),
2758
- ...groups.map((group) => {
2759
- /**
2760
- * **每段封顶**:合并目录在真机器上可以有一百多条(全局技能 + 模板 + 记忆),
2761
- * 全列出来就是信息爆炸——这一格的用途是"认出这条技能",不是"读完整个目录"。
2762
- *
2763
- * 保留顺序**按用途排**,不是按字母:
2764
- * ① 候选(等人拍板,不列出来那件事就没有出口);
2765
- * ② 本会话真的用过(正在被引用的那些);
2766
- * ③ 其余按原序补到上限。
2767
- * 被截掉的那部分不消失:`/` 原生触发菜单仍然是完整目录,这里只留一句话说清还有多少。
2768
- */
2769
- const shownItems = (() => {
2770
- const cap = 8
2771
- if (group.items.length <= cap) return group.items
2772
- const candidates = group.items.filter((skill) => overviewOf.get(skill.name)?.status === 'candidate')
2773
- const used = group.items.filter((skill) => skill.status === 'candidate' ? false : usage.has(skill.name))
2774
- const picked = []
2775
- for (const skill of [...candidates, ...used, ...group.items]) {
2776
- if (picked.length >= cap) break
2777
- if (!picked.includes(skill)) picked.push(skill)
2778
- }
2779
- return picked
2780
- })()
2781
- const hidden = group.items.length - shownItems.length
2782
- return h(
2783
- 'div',
2784
- { key: group.label, style: S.section },
2785
- h('div', { style: S.head }, `${group.label}(${group.items.length})`),
2786
- shownItems.map((skill) => row(skill)),
2787
- hidden <= 0 ? null : h('div', { style: S.faint }, `${t('还有 ')}${hidden}${t(' 条未列出 · 用 / 触发菜单选')}`),
2788
- )
2789
- }),
2790
- orphanUsage.length === 0
2791
- ? null
2792
- : h(
2793
- 'div',
2794
- { style: S.section },
2795
- h('div', { style: S.head }, `${t('目录里已经没有(')}${orphanUsage.length})`),
2796
- orphanUsage.map((item) => h('div', { key: item.name, style: S.row }, h('span', { style: S.mono }, item.name), h('span', { style: S.faint }, usageLine(item) ?? ''))),
2797
- ),
2798
- h(
2799
- 'div',
2800
- { style: S.section },
2801
- h('div', { style: S.head }, `${t('记忆(')}${memory.count}${t(' 条)')}`),
2802
- (Array.isArray(memory.files) ? memory.files : []).map((file) =>
2803
- h(
2804
- 'div',
2805
- { key: file.file, style: S.row },
2806
- h(Link, { onClick: () => props.openPreview?.(file.path) }, h('span', { style: S.mono }, file.file)),
2807
- /**
2808
- * 一个记忆文件里的条目可能十几条 ⇒ 行里只给**前两条 + 共几条**,
2809
- * 全文进 tooltip(默认少而准,细节靠悬停/点开)。
2810
- */
2811
- h(
2812
- 'span',
2813
- { style: S.faint, title: (file.entries ?? []).map((entry) => `${entry.kind === 'lesson' ? '经验' : '事实'}:${entry.title}`).join('\n') },
2814
- (() => {
2815
- const all = (file.entries ?? []).map((entry) => `${entry.kind === 'lesson' ? '经验' : '事实'}:${entry.title}`)
2816
- if (all.length === 0) return ''
2817
- return all.length <= 2 ? all.join(' / ') : `${all.slice(0, 2).join(' / ')}${t(' 等 ')}${all.length}${t(' 条')}`
2818
- })(),
2819
- ),
2820
- ),
2821
- ),
2822
- memory.count === 0 ? h('div', { style: S.faint }, t('暂无记忆。')) : null,
2823
- ),
2824
- )
2825
- }
2826
-
2827
- /**
2828
- * 人门区。**人门优先于运行态**——等人的事永远最先说。
2829
- * 写通道只给人:模型能调的工具面里没有这些动词。按下去之后由宿主平面把它变成一条
2830
- * 带结构化标记的用户消息,折进投影(`by:'user'`),面板随投影自己更新。
2831
- */
2832
- function Inbox(props) {
2833
- const data = props.data
2834
- const sessionId = data === null || data === undefined ? null : data.sessionId
2835
- const items = data === null || data === undefined || !Array.isArray(data.inbox) ? [] : data.inbox
2836
- const [busy, setBusy] = React.useState(null)
2837
- const [error, setError] = React.useState(null)
2838
- // 放弃的分叉:要写缘由才提交(两步),缘由按分叉 id 存着——
2839
- // 同一个屏上可能有两条分叉,不能共用一个输入框。
2840
- const [abandonFork, setAbandon] = React.useState(null)
2841
- const [abandonNote, setAbandonNote] = React.useState('')
2842
- /**
2843
- * 撤回一条事实也要写缘由,按事实 id 分开存(同一屏上可能有多条被推翻的事实)。
2844
- * 分工与「放弃这条分叉」一样:**机制只记录**,「值得索要理由」这条规矩在界面上——
2845
- * 撤回改变的是下一轮模型会引用什么,而「维持原事实」不改任何面,所以它是一键。
2846
- */
2847
- const [retractFact, setRetract] = React.useState(null)
2848
- const [retractNote, setRetractNote] = React.useState('')
2849
- if (items.length === 0) return null
2850
- const send = (item, extra) => {
2851
- setBusy(`${item.kind}:${item.plan || ''}:${item.step || ''}`)
2852
- setError(null)
2853
- // `gate` 只有「用提问卡决定」那一步用得上:它是**界面手势**,不是事实动词。
2854
- /**
2855
- * `value` 是**标的**(比如被推翻的那条事实的 id):宿主据此先核它还在不在,
2856
- * 核不到就回 409,而不是收下一条什么都不改的动作。
2857
- */
2858
- const body = { sessionId, action: item.human_action, plan: item.plan ?? null, fork: extra?.fork ?? item.fork ?? null, branch: extra?.branch ?? null, gate: extra?.gate ?? null, skill: extra?.skill ?? null, value: item.value ?? null, note: extra?.note ?? null }
2859
- fetch('/api/clearai/gate', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
2860
- .then(readResponse)
2861
- .then((result) => {
2862
- if (result.ok !== true) setError(result.error)
2863
- })
2864
- .catch((thrown) => setError(String(thrown?.message ?? thrown)))
2865
- .finally(() => setBusy(null))
2866
- }
2867
- /**
2868
- * 收件箱里现在只有**真门**(计划确认已砍:它是记号不是闸门,见 fold 的 HUMAN_GATE_ACTIONS)。
2869
- *
2870
- * 门要什么由**数据**说(`item.needs`),不在这里按 kind 猜 ✗:
2871
- * · `click` ⇒ 给按钮;
2872
- * · `word` ⇒ 给**一句提示**(「说一句话就行 —— <要说什么>」),
2873
- * 因为那种门本来就不是点击能表达的(复核 / 解除阻塞),而它照样会按住续跑。
2874
- *
2875
- * 但**世界线裁决那一种不走这里**:它要点出每条分支的读数,只有下面那一块拿得到
2876
- * 分支数据。两条路都渲染同一条条目,就等于同一道门上摆两个按钮——其中一个还没有
2877
- * 分支可裁,点下去只会落一条**什么都不改**的人门记录,而界面还会回一句成功。
2878
- */
2879
- const FORK_RENDERED = 'fork_adopt'
2880
- /** 世界线那一块真正会渲染的分叉(判据与条目本身一字不差:同一个分叉、同一步)。 */
2881
- const forkRows = (data.forks || []).filter((fork) => fork.phase === 'deciding' && (fork.humanDecision ?? null) === null)
2882
- const covered = new Set(forkRows.map((fork) => fork.stepId))
2883
- const restItems = items.filter((item) => !(item.kind === FORK_RENDERED && covered.has(item.step)))
2884
- /**
2885
- * 这一层给什么,由**它真的能落实什么**决定:
2886
- * · 候选技能 → 「采纳」(白名单动词,一步到位);
2887
- * · 落不到世界线块的裁决条目 → 「用提问卡决定」——它不是点击能裁的,
2888
- * 而提问卡那条路会把判据与各分支读数铺进对话框,答案照样落账;
2889
- * · 要一句话的门 → 不摆按钮,给一句话提示。
2890
- */
2891
- /** 门要什么由**数据**说(`needs`);每个 kind 给什么按钮也在这一处说完,别处不再猜。 */
2892
- const label = (item) =>
2893
- item.needs !== 'click'
2894
- ? null
2895
- : item.kind === FORK_RENDERED
2896
- ? t('通过提问卡决定')
2897
- : item.kind === 'fact_refutation'
2898
- ? t('撤回事实')
2899
- : item.kind === 'provisional_review'
2900
- ? t('认可,继续')
2901
- : item.kind === 'skill_candidate'
2902
- ? t('采纳')
2903
- : null
2904
- return h(
2905
- 'div',
2906
- { style: S.gate },
2907
- h('div', { style: S.head }, `${t('需要你 ')}${items.length}`),
2908
- ...forkRows.length > 0
2909
- ? forkRows
2910
- .map((fork) =>
2911
- h(
2912
- 'div',
2913
- { key: `adopt-${fork.id}`, style: S.gateRow },
2914
- h('span', { style: S.mono }, `${fork.stepId} · ${fork.question}`),
2915
- /**
2916
- * 混合路径:同一道门,也可以摆到**原生提问卡**上去答。
2917
- * 为什么两条路都留:面板这条是熟手的快路(一步到位);提问卡那条把
2918
- * 判据、各分支的读数与推荐**铺在对话框里**,而且它会挡住输入框——
2919
- * 人正在看对话时,那是最不会被忽略的位置。两条路的落账完全相同。
2920
- */
2921
- h(
2922
- 'button',
2923
- {
2924
- type: 'button',
2925
- className: 'clearai-btn',
2926
- disabled: busy !== null,
2927
- title: t('通过提问卡做出裁决(含判据与各分支读数)'),
2928
- onClick: () => send({ kind: 'fork_adopt', human_action: 'ask', plan: fork.plan ?? null, step: fork.stepId }, { gate: 'fork_adopt', fork: fork.id }),
2929
- },
2930
- t('通过提问卡决定'),
2931
- ),
2932
- ...fork.branches.map((branch) =>
1536
+ const exists = typeof artifact === 'string' ? null : (artifact?.exists ?? null)
1537
+ if (typeof path !== 'string' || path === '') return null
1538
+ return h(
1539
+ 'span',
1540
+ { key: `${path}-${index}`, style: { marginRight: 8 } },
1541
+ exists === false
1542
+ ? h('span', { style: { ...S.mono, opacity: 0.6 }, title: t('盘上没有这个文件') }, `${path}${t('(缺)')}`)
1543
+ : h(Link, { onClick: () => props.openPreview?.(path), title: exists === true ? t('原生预览打开它') : t('原生预览打开它(计划声明的产物)') }, h('span', { style: S.mono }, path)),
1544
+ )
1545
+ }),
1546
+ ),
1547
+ /**
1548
+ * **反向跳**:从树里的这一步跳到「事实」那一格,并展开对应的命题。
1549
+ * 切中栏视图走**原生正门** selectPanel(经可选读法拿到的 layout 服务;守卫:
1550
+ * 没注册的 key 它会抛,而抛了不该把整棵树带下水)。
1551
+ */
1552
+ props.openFacts === undefined
1553
+ ? null
1554
+ : h('span', { style: S.faint }, t('证据')),
1555
+ props.openFacts === undefined
1556
+ ? null
1557
+ : h(
1558
+ 'span',
1559
+ null,
2933
1560
  h(
2934
- 'button',
1561
+ Link,
2935
1562
  {
2936
- key: branch.id,
2937
- type: 'button',
2938
- tone: 'warn',
2939
- disabled: busy !== null,
2940
- onClick: () => send({ kind: 'fork_adopt', human_action: 'adopt_branch', plan: fork.plan ?? null, step: fork.stepId }, { fork: fork.id, branch: branch.id }),
1563
+ onClick: () => props.openFacts(step.id),
1564
+ title: t('打开「本体」那一格并展开这条结论'),
2941
1565
  },
2942
- `${t('采纳 ')}${branch.label}${branch.reading === null || branch.reading === undefined ? '' : `(${branch.reading})`}`,
1566
+ t('查看此步骤的证据'),
2943
1567
  ),
2944
1568
  ),
2945
- /**
2946
- * 放弃要留痕:两下才生效(先点开缘由框,写了才能提交)。
2947
- * 单次点击就放弃会把「为什么」永远丢掉——而放弃正是最需要缘由的那一类决定。
2948
- * 与树下详情同一条纪律(那边是按钮 disabled,这里是两步)。
2949
- */
2950
- h(
2951
- 'button',
2952
- {
2953
- type: 'button',
2954
- className: 'clearai-btn',
2955
- disabled: busy !== null,
2956
- onClick: () => setAbandon(fork.id),
2957
- title: t('放弃需填写缘由后提交'),
2958
- },
2959
- t('放弃这条分叉'),
2960
- ),
2961
- abandonFork === fork.id
2962
- ? h(
2963
- 'span',
2964
- { style: { display: 'inline-flex', gap: 6, alignItems: 'center' } },
2965
- h('input', {
2966
- value: abandonNote,
2967
- placeholder: t('放弃缘由(必填)'),
2968
- onChange: (event) => setAbandonNote(event.target.value),
2969
- style: { fontSize: 11.5, padding: '2px 6px', borderRadius: 6, border: '.5px solid var(--dsw-alias-border-l3)', background: 'transparent', color: 'inherit', minWidth: 140 },
2970
- }),
2971
- h(
2972
- 'button',
2973
- {
2974
- type: 'button',
2975
- className: 'clearai-btn',
2976
- disabled: busy !== null || abandonNote.trim() === '',
2977
- onClick: () => send({ kind: 'fork_adopt', human_action: 'abandon_fork', plan: fork.plan ?? null, step: fork.stepId }, { fork: fork.id, note: abandonNote.trim() }),
2978
- title: abandonNote.trim() === '' ? t('缘由必填') : t('确认放弃(留档不删,ref 保留)'),
2979
- },
2980
- t('确认放弃'),
2981
- ),
2982
- )
2983
- : null,
1569
+ ),
1570
+ evidence.length === 0 && audits.length === 0
1571
+ ? null
1572
+ : h(
1573
+ 'div',
1574
+ { style: { marginTop: 6 } },
1575
+ evidence.map((item) => h('div', { key: item.id, style: S.faint }, `${t('检验结果:')}${VERDICT[item.verdict] ?? '—'} · ${howOf(item)}${item.basis ? ` · ${brief(item.basis, 60)}` : ''}`)),
1576
+ audits.map((item) =>
1577
+ h(
1578
+ 'div',
1579
+ { key: item.id, style: S.faint },
1580
+ `${t('独立核验:')}${item.verdict === null || item.verdict === undefined ? t('正在裁决') : (VERDICT[item.verdict] ?? item.verdict)}`,
1581
+ item.evaluatorSession === null || item.evaluatorSession === undefined ? null : h(Link, { onClick: () => props.openSpectator?.(item.evaluatorSession) }, h('span', null, t(' 看核验过程'))),
1582
+ item.cardPath === null || item.cardPath === undefined ? null : h(Link, { onClick: () => props.openPreview?.(item.cardPath) }, h('span', null, t(' 看核验记录'))),
2984
1583
  ),
2985
- )
2986
- : [],
2987
- ...restItems.map((item, index) => {
2988
- /** 裁决条目走**界面手势**:`ask` 只是把卡摆上去,答案回到宿主那条老路落账。 */
2989
- const asCard = item.kind === FORK_RENDERED
2990
- const act = asCard ? { ...item, human_action: 'ask' } : item
2991
- const extra = asCard ? { gate: FORK_RENDERED, fork: item.fork ?? null } : null
2992
- return h(
2993
- 'div',
2994
- { key: `${item.kind}-${index}`, style: S.gateRow, title: item.kind },
2995
- /** 机器词(`provisional_review` 这类)**不上屏** ✗ —— 进 tooltip,给人看的是标题那句话。 */
2996
- label(item) === null ? null : h('span', { style: S.tagStrong }, `${t('要你')}${label(item)}`),
2997
- h('span', null, item.title),
2998
- h('span', { style: S.faint }, item.summary),
2999
- label(item) !== null
3000
- ? h(
3001
- 'button',
3002
- {
3003
- type: 'button',
3004
- className: 'clearai-btn',
3005
- disabled: busy !== null,
3006
- /** 撤回先开缘由框(两步):「为什么撤回」是最值得留下的那句话。 */
3007
- onClick: () => (item.kind === 'fact_refutation' ? setRetract(item.value) : send(act, extra)),
3008
- title: item.kind === 'fact_refutation' ? t('撤回需填写缘由后提交') : undefined,
3009
- },
3010
- label(item),
3011
- )
3012
- : item.needs === 'word'
3013
- ? h('span', { style: { ...S.faint, opacity: 0.9 } }, `${t('')}${item.ask ?? '说一句你的决定'}`)
3014
- : null,
3015
- /**
3016
- * 事实复核那道门有**两个**结局,而且两个都必须能一键落地:
3017
- * 只给「撤回」的话,「判定证据不可靠、维持原事实」就只能靠不说话——
3018
- * 而门开着会按住续跑,于是系统一直等一个永远不会来的动作。
3019
- */
3020
- item.kind === 'fact_refutation'
3021
- ? h(
3022
- 'button',
3023
- { type: 'button', className: 'clearai-btn', disabled: busy !== null, onClick: () => send({ ...item, human_action: 'keep_fact' }, null) },
3024
- t('维持原事实'),
3025
- )
3026
- : null,
3027
- item.kind === 'fact_refutation' && retractFact === item.value
3028
- ? h(
3029
- 'span',
3030
- { style: { display: 'inline-flex', gap: 6, alignItems: 'center' } },
3031
- h('input', {
3032
- value: retractNote,
3033
- placeholder: t('撤回缘由(必填)'),
3034
- onChange: (event) => setRetractNote(event.target.value),
3035
- style: { fontSize: 11.5, padding: '2px 6px', borderRadius: 6, border: '.5px solid var(--dsw-alias-border-l3)', background: 'transparent', color: 'inherit', minWidth: 140 },
3036
- }),
3037
- h(
3038
- 'button',
3039
- {
3040
- type: 'button',
3041
- className: 'clearai-btn',
3042
- disabled: busy !== null || retractNote.trim() === '',
3043
- onClick: () => send(act, { note: retractNote.trim() }),
3044
- title: retractNote.trim() === '' ? t('缘由必填') : t('确认撤回(记录保留,不再作为「已知」引用)'),
3045
- },
3046
- t('确认撤回'),
3047
- ),
3048
- )
3049
- : null,
3050
- )
3051
- }),
3052
- error === null ? null : h('div', { style: S.faint }, `${t('发送失败:')}${error}`),
1584
+ ),
1585
+ ),
3053
1586
  )
3054
1587
  }
3055
1588
 
1589
+ /** 待处理:只陈述的几行(计划停下、结论矛盾),没有按钮——要怎么办,在对话里说。 */
1590
+ function NeedYou(props) {
1591
+ const items = Array.isArray(props.data?.needYou) ? props.data.needYou : []
1592
+ if (items.length === 0) return null
1593
+ return h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, margin: '0 0 8px' } }, ...items.map((item, index) => h('div', { key: `need-${index}`, className: 'clearai-need' }, h('b', null, t('待处理')), String(item?.text ?? ''))))
1594
+ }
1595
+
3056
1596
  /**
3057
1597
  * 旁观入口(阶段 4 的 4.5):点一下跳到那个子会话,**只看不动手**。
3058
1598
  * 判断不许匿名——评估者/仲裁是谁、在哪,面板上给得出入口。
3059
1599
  * 拿不到会话服务就退化成一行文本 id:宁可少一个按钮,也不假装能跳。
3060
1600
  */
3061
1601
 
3062
- /**
3063
- * 续跑档的两个小工具(纯函数,便于直接断言文案)。
3064
- *
3065
- * **说行为,不说机制**:人要回答的是「我走开的时候它还接着干吗」,不是「这是哪个档」。
3066
- * 哲学词(人在场 / 无人值守)留在文档、运行态卡与提示词里——那些读者是模型和写文档的人;
3067
- * 给人点的那一格说人话,并在 tooltip 里注明两个词是一回事。
3068
- *
3069
- * 为什么是「多问我 / 自己拿主意」而不是「先问我 / 别问我」:两档都**会**为不可约的判断开门
3070
- * (湿实验、护栏级授权、L4 放行),所以只能写程度,不能写绝对——绝对的说法是假承诺。
3071
- */
3072
- const TIER_TEXT = lazyTable(() => ({ attended: t('多问我'), unattended: t('自己拿主意') }))
3073
- const TIER_HINT = lazyTable(() => ({
3074
- attended: t('续跑:多问我——每个阶段收尾就停下,等你给下一阶段(文档里叫「人在场」)。点一下切成「自己拿主意」。'),
3075
- unattended: t('续跑:自己拿主意——立约即授权,按轮数自己往下跑,只在不可约的判断上开门(文档里叫「无人值守」)。点一下切成「多问我」。'),
3076
- }))
3077
- const tierLabel = (value, pending) => `${TIER_TEXT[value] ?? value}${pending ? '(已切,下一步生效)' : ''}`
3078
-
3079
- /**
3080
- * 「多问我 / 自己跑」那个开关**删掉了**。
3081
- *
3082
- * 第一性原理 + 奥坎姆:「我要不要在场」是**运行时状态**——有没有门开着、有没有裁决在飞、
3083
- * 有没有开着的步;这些系统自己知道 ✗,不该要用户预先声明。
3084
- * 而且那一档还顺手把「计划经人确认」变成系统自己签的 ✗(与 L4「人放行」同一类病)。
3085
- * 续跑本身走**原生 goal**(宿主的回合驱动);我们只决定"什么时候该布防、给多大保险丝"。
3086
- */
3087
1602
  /**
3088
1603
  * 计划的小图形:一棵**迷你世界树**——一根脊柱、一条岔、两个节点。
3089
1604
  * 用 `currentColor`:颜色留给状态(等人确认 = 琥珀,受阻 = 琥珀,收尾 = 灰),形状留给语义。
@@ -3125,29 +1640,26 @@ window.__ModuleLoader__.load({
3125
1640
  * 一格只放**一个符号**:进度。要人注意的事不换符号,只换颜色。
3126
1641
  *
3127
1642
  * 这条工具行上的格子只承担**可点、且只有我们知道**的两句:
3128
- * 需要你 N(人门计数,点了开世界树)· 续跑停着(为什么停,人是可以处置的)。
1643
+ * 待处理 N(人门计数,点了开世界树)· 续跑停着(为什么停,人是可以处置的)。
3129
1644
  * 「已达成 · 100%」那一类与原生目标提示说的是同一件事,不在这里重复。
3130
1645
  */
3131
- const inboxCount = Array.isArray(data?.inbox) ? data.inbox.length : 0
3132
- const cont = data?.continuation ?? null
3133
- const holdNote = cont === null || cont === undefined || cont.state !== 'paused' ? null : `${t('续跑停着:')}${HOLD_WHY[cont.why] ?? cont.why ?? '策略暂停'}`
3134
- const pending = plan.confirmationPending === true
1646
+ const inboxCount = Array.isArray(data?.needYou) ? data.needYou.length : 0
3135
1647
  const blocked = plan.blocked !== null && plan.blocked !== undefined
3136
- const attention = pending || blocked
1648
+ const attention = blocked
3137
1649
  /**
3138
1650
  * 符号**恒定是进度**(形状稳定才学得会):要人注意不在符号上换字,
3139
1651
  * 而是换颜色(与世界树同一条规矩:形状说状态,别让人去猜一个 '?')。
3140
- * 「需要你 N」就在这一格(`waiting`):门开着是**事实面**上最要紧的一句,
1652
+ * 「待处理 N」就在这一格(`waiting`):门开着是**事实面**上最要紧的一句,
3141
1653
  * 而它可点——点一下开世界树,那里才看得到要裁什么。
3142
1654
  */
3143
1655
  const symbol = `${done}/${total}`
3144
1656
  const brief = typeof plan.brief === 'string' && plan.brief.trim() !== '' ? plan.brief.trim() : null
3145
- const meaning = pending ? t('计划在建,**等你确认**') : blocked ? t('计划受阻,等人处置') : plan.status === 'closed' ? `${t('计划已收尾(')}${done}/${total}${t(' 步)')}` : `${t('计划已交付 ')}${done}/${total}${t(' 步')}`
1657
+ const meaning = blocked ? t('计划受阻,等人处置') : plan.status === 'closed' ? `${t('计划已收尾(')}${done}/${total}${t(' 步)')}` : `${t('计划已交付 ')}${done}/${total}${t(' 步')}`
3146
1658
  /**
3147
- * 等人时**只说一句**:先「需要你 N」(人门计数),没有门才说续跑停着的原因。
1659
+ * 等人时**只说一句**:先「待处理 N」(人门计数),没有门才说续跑停着的原因。
3148
1660
  * 两者是同一根轴(为什么在等人)⇒ 一格只放一个,不并列。
3149
1661
  */
3150
- const waiting = inboxCount > 0 ? `${t('需要你 ')}${inboxCount}` : null
1662
+ const waiting = inboxCount > 0 ? `${t('待处理 ')}${inboxCount}` : null
3151
1663
  const attentionNow = attention || waiting !== null
3152
1664
  return h(
3153
1665
  'button',
@@ -3167,19 +1679,6 @@ window.__ModuleLoader__.load({
3167
1679
  )
3168
1680
  }
3169
1681
 
3170
- /**
3171
- * 策略暂停的**理由**对照表(值来自内核 `holdReason`,不是这里自创的):
3172
- * 面板要说的正是「平台说不出的那一句」——为什么停。新增理由时这张表跟着补,
3173
- * 补漏了也不骗人:认不出来的值原样显示,而不是显示一个体面的空话。
3174
- */
3175
- /** 窗口停下的理由 = **真的有人的事**(「一阶段收尾」那条已删:它是档位的表达,不是人的事)。 */
3176
- const HOLD_WHY = lazyTable(() => ({
3177
- audit: t('等独立裁决'),
3178
- plan_confirm: t('计划待确认'),
3179
- gate: t('等待人工处理'),
3180
- unknown: t('策略暂停(原因未记录,已告警)'),
3181
- }))
3182
-
3183
1682
  /**
3184
1683
  * 一步的**人话**名字:`do` 是模型写的那句「做什么」,截短了放进一行。
3185
1684
  * 内部步 id(`s-3`)只进 tooltip 与世界树——它在日志里是账,在界面上不是给人看的。
@@ -3189,47 +1688,12 @@ window.__ModuleLoader__.load({
3189
1688
  return value.length <= 12 ? value : `${value.slice(0, 12)}…`
3190
1689
  }
3191
1690
 
3192
- /**
3193
- * **续跑注**:只在**有话要说**时出现的一格。
3194
- *
3195
- * 输入框下那条整行删掉之后,它说的三句话里有两句**别处无处可说** ✗:
3196
- * · 「续跑已撤回」——人按下清除的那一瞬,dock 已经消失,而内核还没到下一拍;
3197
- * · 「续跑停着:<理由>」——平台只会说"目标已暂停",说不出**为什么**;
3198
- * 所以把它们移到工具行(与计划 chip 同一行,**不占新行**):正常跑着时一个字都不说 ✓。
3199
- */
3200
- function ContinuationNote(props) {
3201
- const data = typeof props.useProjection === 'function' ? props.useProjection('clearai') : undefined
3202
- const hostGoal = typeof props.useProjection === 'function' ? props.useProjection('goal') : undefined
3203
- const cont = data === null || data === undefined ? null : (data.continuation ?? null)
3204
- if (cont === null) return null
3205
- const vanished = hostGoal === null || hostGoal === undefined
3206
- /** 门要**一句话**时(dock 只会说"目标已暂停"),这句话说得更准:等你说一声 ✓ */
3207
- const wordGate = Array.isArray(data?.inbox) && data.inbox.some((item) => item.needs === 'word')
3208
- const holdText = wordGate ? t('等待输入') : (HOLD_WHY[cont.why] ?? cont.why ?? t('策略暂停'))
3209
- const text =
3210
- cont.state === 'withdrawn' || ((cont.state === 'armed' || cont.state === 'paused') && vanished)
3211
- ? t('续跑已撤回')
3212
- : cont.state === 'paused'
3213
- ? `${t('续跑停着:')}${holdText}`
3214
- : null
3215
- if (text === null) return null
3216
- return h(
3217
- 'span',
3218
- {
3219
- className: 'clearai-toolctl',
3220
- style: { opacity: 0.8, fontSize: 11.5, cursor: 'default' },
3221
- title: t('系统内部消息(非运行状态)'),
3222
- },
3223
- text,
3224
- )
3225
- }
3226
-
3227
1691
  /**
3228
1692
  * 输入框下那条派生状态条**删掉了**。
3229
1693
  *
3230
1694
  * 它说的事(阶段 / 完成度 / 当前步)与**原生目标提示**、与工具行那颗计划 chip 的 `N/M`
3231
1695
  * 说的是同一件 ✗ —— 而它**独占一行,把输入框整个顶上去** ✗。
3232
- * 唯一可点、且只有我们知道的那一件(「需要你 N」)已经并进计划 chip:
1696
+ * 唯一可点、且只有我们知道的那一件(「待处理 N」)已经并进计划 chip:
3233
1697
  * 同一行、一点直达世界树。剩下的「为什么停」也在那颗 chip 上只说一句。
3234
1698
  */
3235
1699
  /**
@@ -3265,65 +1729,6 @@ window.__ModuleLoader__.load({
3265
1729
  return hitsAssertion || hitsText
3266
1730
  }
3267
1731
 
3268
- /**
3269
- * **断言芯片行**:一条已确立条目(或命题)的断言,点开就地展开词条卡。
3270
- *
3271
- * 为什么是「就地展开」而不是跳走:芯片的用途就是**读这条结论时**顺手看清
3272
- * 「这个谓词什么意思、是不是单值、依据是什么」——跳一步,读者的上下文就断了。
3273
- * 词条卡的两个人物(筛选此概念 / 在图里看)由外层经 `ui` 递进来。
3274
- */
3275
- const AssertionChips = ({ assertions, ui, promoted }) => {
3276
- const rows = Array.isArray(assertions) ? assertions : []
3277
- if (ui === null || ui === undefined || rows.length === 0) return null
3278
- return h(
3279
- 'div',
3280
- { style: S.chipRow },
3281
- ...rows.map((assertion, index) => {
3282
- const key = `${promoted === false ? 'p' : 'f'}-${index}`
3283
- const open = ui.openKey === key
3284
- const predicate = (ui.lexicon?.predicates ?? []).find((item) => item.id === assertion?.predicate) ?? null
3285
- const label = String(assertion?.chip ?? `${String(assertion?.subject?.id ?? '')} · ${String(assertion?.predicate ?? '')}`)
3286
- const card =
3287
- open === false
3288
- ? null
3289
- : h(
3290
- 'div',
3291
- { style: S.chipCard },
3292
- predicate === null
3293
- ? h('div', { style: S.faint }, t('此谓词已不在词汇中(可能已废止);存量断言仍可读。'))
3294
- : h('div', null,
3295
- h('div', { style: S.chipCardTitle }, `${predicate.label ?? predicate.id} · ${predicate.id}`),
3296
- h('div', { style: S.kv }, h('span', { style: S.faint }, `${t('释义')}:`), ` ${String(predicate.gloss ?? '—')}`),
3297
- h('div', { style: S.kv }, h('span', { style: S.faint }, `${t('主词域')}:`), ` ${String(predicate.domain ?? '—')}`),
3298
- h('div', { style: S.kv }, h('span', { style: S.faint }, `${t('值域')}:`), ` ${predicate.range?.term ?? (predicate.range?.form ?? '—') + (predicate.range?.unit ?? '')}`),
3299
- h('div', { style: S.kv }, h('span', { style: S.faint }, `${t('单值')}:`), ` ${predicate.functional === true ? '✓' : '—'}`),
3300
- h('div', { style: S.kv }, h('span', { style: S.faint }, `${t('依据')}:`), ` ${String(predicate.basis ?? '—')}`),
3301
- ),
3302
- h(
3303
- 'div',
3304
- { style: S.chipActions },
3305
- h('span', { style: S.chipAction, onClick: () => ui.onFilter(assertion?.predicate ?? null) }, t('按此谓词过滤')),
3306
- h('span', { style: S.chipAction, onClick: () => ui.onGraph() }, t('在图里看')),
3307
- ),
3308
- )
3309
- return h(
3310
- 'div',
3311
- { key, style: S.chipWrap },
3312
- h(
3313
- 'span',
3314
- {
3315
- style: open === true ? { ...S.chip, borderColor: 'rgba(74,163,255,0.9)' } : S.chip,
3316
- onClick: () => ui.onToggle(key),
3317
- title: promoted === false ? t('未升格:断言仍在命题上') : t('查看词条'),
3318
- },
3319
- `${label}${promoted === false ? t(' · 未升格') : ''}`,
3320
- ),
3321
- card,
3322
- )
3323
- }),
3324
- )
3325
- }
3326
-
3327
1732
  /**
3328
1733
  * **力导向布局**(graphology + ForceAtlas2),与参考实现 `refs/semantica/explorer` 同一套。
3329
1734
  *
@@ -3400,28 +1805,18 @@ window.__ModuleLoader__.load({
3400
1805
  }
3401
1806
 
3402
1807
  /**
3403
- * **知识 Inspector**:点击图上的节点或边之后,回答「这是什么、凭什么信、谁改过它」。
1808
+ * **节点小卡**:点图上的一个点或一条边,回答「这是什么、哪些结论用到它」。
3404
1809
  *
3405
- * 为什么要有它:图能画出来不等于图是知识入口。从前点一个节点只得到「按此过滤」——
3406
- * 读的人仍然不知道这个词是什么意思。现在那条链补齐:
3407
- *
3408
- * ```text
3409
- * 概念 → 定义 / 父概念 / 子概念 / 用它的谓词 / 实例 / 相关事实 / 登记修订史
3410
- * 断言边 → 事实 → 命题 → 证据 → 出处 → 产生步骤 → 复核态
3411
- * ```
3412
- *
3413
- * 数据**不在这里拼**:组装住在宿主半的 `inspectGraphSelection`(判据只有一处),
3414
- * 这一层只请求与渲染。请求失败不清空旧详情——把上一次的结果换成一片空白,
3415
- * 读的人会以为「这条知识没了」。
1810
+ * 数据仍然不在这里拼:组装住在宿主半的 `inspectGraphSelection`(经 `/api/clearai/inspector`),
1811
+ * 这一层只请求与渲染。卡上只说人话:种类、释义、上下位、关系、用到它的结论;
1812
+ * 编号、等级代码、证据 id 都不上屏(结论的来历在下面的结论列表里点开看)。
1813
+ * 请求失败不清空旧内容——把上一次的结果换成一片空白,读的人会以为「这条知识没了」。
3416
1814
  */
3417
- const GraphInspector = ({ selection, sessionId, onFilter, onClose, inspector: injected }) => {
1815
+ const GraphInspector = ({ selection, sessionId, onFilter, onClose, inspector: injected, names }) => {
3418
1816
  const [state, setState] = React.useState({ loading: false, inspector: null, error: null })
3419
1817
  const key = selection === null ? '' : `${selection.kind}:${selection.id}`
3420
1818
  React.useEffect(() => {
3421
- /**
3422
- * 宿主已经把这份读数喂进来时不再问一次。这条缝同时服务两件事:
3423
- * 全屏工作区(一次取好、两处渲染)与测试(同步渲染桩里等不到异步结果)。
3424
- */
1819
+ /** 宿主已经把读数喂进来时不再问一次(全屏工作区与测试都走这条缝)。 */
3425
1820
  if (injected !== undefined) return undefined
3426
1821
  if (key === '' || typeof sessionId !== 'string' || sessionId === '') return undefined
3427
1822
  let live = true
@@ -3445,472 +1840,399 @@ window.__ModuleLoader__.load({
3445
1840
  const data = injected === undefined ? state.inspector : injected
3446
1841
  const busy = injected === undefined && state.loading === true
3447
1842
  const failure = injected === undefined ? state.error : null
3448
- const line = (label, value) => (value === null || value === undefined || value === '' ? null : h('div', { style: S.kv }, h('span', { style: S.faint }, `${t(label)}:`), ` ${String(value)}`))
3449
- /** 事实的一张卡:链的每一段都在这里,读的人不必再去别处找。 */
3450
- const factCard = (fact, index) =>
3451
- h(
3452
- 'div',
3453
- { key: `f-${fact.id ?? index}`, style: { ...S.chipCard, marginTop: 6 } },
3454
- h('div', { style: S.chipCardTitle }, `${String(fact.id ?? '')} · ${String(fact.text ?? '')}`),
3455
- h('div', { style: S.faint }, `${String(fact.level ?? '—')} · ${String(fact.status ?? '')}${fact.at === null || fact.at === undefined ? '' : ` · ${new Date(fact.at).toISOString().slice(0, 19)}`}`),
3456
- line('边界', fact.scope),
3457
- fact.hypothesis === null
3458
- ? h('div', { style: S.faint }, t('这条事实没有命题关联(旧账本):不拿文本相等去猜身份。'))
3459
- : h(
3460
- 'div',
3461
- null,
3462
- line('命题', `${fact.hypothesis.id} — ${String(fact.hypothesis.claim ?? '')}`),
3463
- line('推翻条件', fact.hypothesis.refuteWhen),
3464
- line('支持到', `${String(fact.hypothesis.supportedLevel ?? '—')}${fact.hypothesis.refutations > 0 ? ` · 推翻 ${fact.hypothesis.refutations}` : ''}`),
3465
- ),
3466
- ...fact.assertions.map((assertion, position) => h('div', { key: `a-${position}`, style: S.kv }, h('span', { style: S.faint }, `${t('断言')}:`), ` ${String(assertion.chip ?? '')}`)),
3467
- fact.evidence.length === 0
3468
- ? h('div', { style: S.faint }, t('没有证据引用(旧事实或自判)。'))
3469
- : h(
3470
- 'div',
3471
- null,
3472
- ...fact.evidence.map((item) =>
3473
- h(
3474
- 'div',
3475
- { key: `e-${item.id}` },
3476
- line('证据', `${item.id} · ${item.verdict} · ${item.level} · ${item.evaluator}`),
3477
- item.basis === null ? null : line('依据', item.basis),
3478
- item.origins.length === 0 ? null : line('出处', item.origins.map((origin) => `${origin.kind}${origin.path === undefined ? '' : `:${origin.path}`}${origin.session === undefined ? '' : `:${origin.session}`}`).join('、')),
3479
- item.step === null ? null : line('产生步骤', `${item.step.plan}/${item.step.id} · ${String(item.step.doneCriteria ?? '')}`),
3480
- ),
3481
- ),
3482
- ),
3483
- fact.review === null ? null : line('复核', `${fact.review.decision}${fact.review.reason === null ? '' : ` — ${fact.review.reason}`}`),
3484
- fact.conflicts.length === 0 ? null : line('冲突', t('这条事实参与了一对冲突读数(只暴露,不裁决)')),
3485
- )
1843
+ const definition = data?.definition ?? {}
1844
+ const relations = data?.relations ?? {}
1845
+ const kv = (label, value) => (value === null || value === undefined || value === '' ? [] : [h('span', { key: `k-${label}` }, label), h('span', { key: `v-${label}` }, String(value))])
1846
+ const value = definition.value === undefined || definition.value === null ? null : `${String(definition.value)}${definition.unit ? ` ${String(definition.unit)}` : ''}`
1847
+ /** 用到它的结论:写进长期知识的那几条;被人撤回的标「已替换」,卷进矛盾的标「有矛盾」。 */
1848
+ const factTrust = (fact) => (fact.review?.decision === 'retracted' ? 'replaced' : (fact.conflicts ?? []).length > 0 ? 'conflict' : 'credible')
1849
+ const facts = Array.isArray(data?.facts) ? data.facts : []
1850
+ const title = String(selection.label ?? data?.selection?.label ?? selection.id)
1851
+ /** 关系串里的主语 / 宾语是实例 id(`i_jepa`):换成图上的名字(`I-JEPA`)再上屏。 */
1852
+ const named = (text) => [...(names ?? new Map()).entries()].sort((left, right) => right[0].length - left[0].length).reduce((out, [ref, label]) => out.split(ref).join(label), String(text ?? ''))
3486
1853
  return h(
3487
1854
  'div',
3488
- { style: { ...S.chipCard, marginTop: 6, maxHeight: 360, overflow: 'auto' } },
1855
+ { className: 'clearai-card' },
3489
1856
  h(
3490
1857
  'div',
3491
- { style: S.inline },
3492
- h('span', { style: S.chipCardTitle }, data === null ? `${String(selection.label ?? selection.id)}` : `${String(data.selection.label ?? data.selection.id)}`),
3493
- h('span', { style: S.faint }, `${String(selection.kind)} · ${String(selection.id)}`),
1858
+ { className: 'clearai-card-head' },
1859
+ h('span', { className: 'clearai-card-kind' }, KIND_WORD[selection.kind] ?? t('关系')),
1860
+ h('span', { className: 'clearai-card-title' }, title),
3494
1861
  h('span', { style: { flex: '1 1 auto' } }),
3495
- h('span', { style: S.chipAction, onClick: onFilter }, t('按此筛选')),
3496
- h('span', { style: S.chipAction, onClick: onClose }, t('关闭')),
1862
+ h('span', { className: 'clearai-link', onClick: onFilter }, t('只看相关')),
1863
+ h('span', { className: 'clearai-link', onClick: onClose }, t('关闭')),
3497
1864
  ),
3498
- busy === true ? h('div', { style: S.faint }, t('正在取这条知识的读数…')) : null,
3499
- failure !== null ? h('div', { style: S.faint }, `${t('取不到读数')}:${failure}`) : null,
3500
- busy === false && failure === null && data === null ? h('div', { style: S.faint }, t('宿主没有这个对象的读数(可能刚被废止,或它不在当前词汇里)。')) : null,
1865
+ busy === true ? h('div', { className: 'clearai-quiet' }, t('正在取…')) : null,
1866
+ failure !== null ? h('div', { className: 'clearai-quiet' }, `${t('取不到')}${t(':')}${failure}`) : null,
1867
+ busy === false && failure === null && data === null ? h('div', { className: 'clearai-quiet' }, t('这一项已经不在当前的词里了(可能刚被废止)。')) : null,
3501
1868
  data === null
3502
1869
  ? null
3503
1870
  : h(
3504
1871
  'div',
3505
- null,
3506
- data.definition?.gloss === undefined || data.definition.gloss === null ? null : h('div', { style: S.kv }, h('span', { style: S.faint }, `${t('释义')}:`), ` ${String(data.definition.gloss)}`),
3507
- line('依据', data.definition?.basis),
3508
- line('状态', data.definition?.status),
3509
- line('父概念', data.definition?.parentLabel ?? data.definition?.parent),
3510
- line('主词域', data.definition?.domainLabel ?? data.definition?.domain),
3511
- line('值域', data.definition?.range === null || data.definition?.range === undefined ? null : JSON.stringify(data.definition.range)),
3512
- line('形态', data.definition?.form),
3513
- line('取值', data.definition?.value === undefined || data.definition?.value === null ? null : `${String(data.definition.value)}${data.definition.unit === null || data.definition.unit === undefined ? '' : ` ${String(data.definition.unit)}`}`),
3514
- line('类型', data.definition?.typeLabel ?? data.definition?.type),
3515
- line('引用', data.definition?.uses),
3516
- data.note === null || data.note === undefined ? null : h('div', { style: S.faint }, String(data.note)),
3517
- data.relations === undefined || data.relations === null
3518
- ? null
3519
- : h(
3520
- 'div',
3521
- null,
3522
- (data.relations.children ?? []).length === 0 ? null : line('子概念', data.relations.children.map((item) => item.label).join('、')),
3523
- (data.relations.predicates ?? []).length === 0 ? null : line('相关谓词', data.relations.predicates.map((item) => `${item.label}(${item.id})`).join('、')),
3524
- (data.relations.instances ?? []).length === 0 ? null : line('实例', data.relations.instances.map((item) => `${item.label}×${item.factCount}`).join('、')),
3525
- (data.relations.edges ?? []).length === 0 ? null : line('关系边', data.relations.edges.map((edge) => `${edge.direction === 'out' ? '→' : '←'}${edge.chip}`).join('、')),
3526
- (data.relations.subjects ?? []).length === 0 ? null : line('主词', data.relations.subjects.map((item) => item.ref).join('、')),
3527
- ),
3528
- (data.facts ?? []).length === 0 ? null : h('div', { style: { ...S.faint, marginTop: 6 } }, `${t('相关事实')} ${data.facts.length}${data.factsTruncated > 0 ? t('(还有更多,未列出)') : ''}:`),
3529
- ...(data.facts ?? []).map(factCard),
3530
- (data.conflicts ?? []).length === 0 ? null : h('div', { style: { ...S.faint, marginTop: 6 } }, `${t('冲突')} ${data.conflicts.length}:${data.conflicts.map((conflict) => `${conflict.predicate} · ${conflict.subject}`).join('、')}`),
3531
- (data.history ?? []).length === 0 ? null : h('div', { style: { ...S.faint, marginTop: 6 } }, `${t('历史')}:`),
3532
- ...(data.history ?? []).map((event, index) =>
3533
- h('div', { key: `h-${index}`, style: S.kv }, h('span', { style: S.faint }, event.at === null || event.at === undefined ? '—' : new Date(event.at).toISOString().slice(0, 19)), ` ${String(event.summary ?? event.kind)}${event.reason === null || event.reason === undefined ? '' : ` — ${String(event.reason)}`}`),
1872
+ { className: 'clearai-kv' },
1873
+ ...kv(t('释义'), definition.gloss),
1874
+ ...kv(t('上位'), definition.parentLabel ?? definition.parent),
1875
+ ...kv(t('下位'), (relations.children ?? []).map((item) => item.label).join('、')),
1876
+ ...kv(t('主语'), definition.domainLabel ?? definition.domain),
1877
+ ...kv(t('类型'), definition.typeLabel ?? definition.type),
1878
+ ...kv(t('取值'), value),
1879
+ ...kv(t('依据'), definition.basis),
1880
+ ...kv(t('关系'), (relations.edges ?? []).slice(0, 6).map((edge) => named(edge.chip)).join('、')),
1881
+ ...kv(t('实例'), (relations.instances ?? []).slice(0, 6).map((item) => item.label).join('、')),
1882
+ definition.status === 'deprecated' ? h('span', { key: 'dep', style: { gridColumn: '1 / -1' }, className: 'clearai-quiet' }, t('这个词已废止,旧结论里的用法照样可读。')) : null,
1883
+ ),
1884
+ facts.length === 0
1885
+ ? null
1886
+ : h(
1887
+ 'div',
1888
+ { className: 'clearai-card-facts' },
1889
+ h('div', { className: 'clearai-group-head' }, t('用到它的结论'), h('span', null, `${facts.length}${data.factsTruncated > 0 ? '+' : ''}`)),
1890
+ ...facts.slice(0, 4).map((fact, index) =>
1891
+ h(
1892
+ 'div',
1893
+ { key: `f-${index}`, className: 'clearai-card-fact' },
1894
+ factTrust(fact) === 'conflict' ? h('span', { className: 'clearai-trust', 'data-trust': 'conflict' }, t('有矛盾')) : h(TrustTag, { trust: factTrust(fact) }),
1895
+ h('span', null, brief(fact.text, 60)),
1896
+ fact.scope && fact.scope !== fact.hypothesis?.refuteWhen ? h('span', { className: 'clearai-row-sub' }, `${t('范围:')}${brief(fact.scope, 40)}`) : null,
1897
+ ),
3534
1898
  ),
3535
1899
  ),
3536
1900
  )
3537
1901
  }
3538
1902
 
3539
1903
  /**
3540
- * **图带 / 图谱工作区(React Flow)**。
3541
- *
3542
- * 渲染交给 @xyflow/react——拖节点、拖画布、滚轮缩放、框选、MiniMap 都是库的事,
3543
- * 我们不再手写 SVG 交互。这一层只做三件 ClearAI 自己的事:
3544
- * · **适配投影**:graphProjection() 的节点 / 边 → React Flow 的 nodes / edges,
3545
- * 类型 / 冲突 / 状态的视觉编码在这里(不在投影里,投影是语义不是样式);
3546
- * · **点击语义**:点节点 / 边 = 打开 Inspector;过滤是 Inspector 里的显式动作;
3547
- * · **全屏工作区**:position: fixed 的真 overlay,打开时自动 fit。
1904
+ * **星图(React Flow)**:本体图 / 实体图二选一。
3548
1905
  *
3549
- * 数据不在这里拼:Inspector 的证据链住在宿主半(inspectGraphSelection),
3550
- * 这一层只拿投影与路由的结果。所有交互状态(viewport / 选中 / 拖动)是界面状态,
3551
- * 一个字节都不进账本。
3552
- */
3553
- const GraphBand = ({ lexicon, layer, onLayer, fullscreen, onToggleFullscreen, onFilter, sessionId, unlanded }) => {
1906
+ * 渲染交给 @xyflow/react——拖节点、拖画布、滚轮缩放都是库的事。这一层只做三件 ClearAI 自己的事:
1907
+ * · **适配投影**:graphProjection() 的节点 / 边 → React Flow 的 nodes / edges;
1908
+ * 视觉编码在这里(投影是语义不是样式):点 + 衬线标签;概念是空心圈、实例是实心点、
1909
+ * 取值是小点;边实线 = 已验证(写进长期知识的断言),虚线 = 待核验(登记了、没过独立裁决),
1910
+ * 淡线 = 上下位;卷进矛盾的点和边用告警色。
1911
+ * · **点击语义**:点节点 / 边 = 打开小卡;「只看相关」是卡上的显式动作。
1912
+ * · **全屏**:position: fixed 的真 overlay,打开时自动适配。
1913
+ */
1914
+ const GraphBand = ({ lexicon, layer, onLayer, fullscreen, onToggleFullscreen, onFilter, sessionId, unlanded, focus }) => {
3554
1915
  const [picked, setPicked] = React.useState(null)
3555
1916
  const graph = lexicon?.graph ?? { nodes: [], edges: [], bounds: { width: 0, height: 0 } }
3556
1917
  const conflicts = Array.isArray(lexicon?.conflicts) ? lexicon.conflicts : []
3557
1918
  const conflicted = new Set(conflicts.flatMap((item) => item.sides.map((side) => side.fact)).filter((id) => typeof id === 'string'))
3558
- const allNodes = graph.nodes.filter((node) => node.layer === layer)
3559
- const allEdges = graph.edges.filter((edge) => edge.layer === layer && typeof edge.from === 'string' && typeof edge.to === 'string')
3560
- const nodeById = new Map(allNodes.map((node) => [node.id, node]))
1919
+ const layerNodes = graph.nodes.filter((node) => node.layer === layer)
1920
+ const layerEdges = graph.edges.filter((edge) => edge.layer === layer && typeof edge.from === 'string' && typeof edge.to === 'string')
1921
+ const nodeById = new Map(layerNodes.map((node) => [node.id, node]))
1922
+ const problems = Array.isArray(lexicon?.problems) ? lexicon.problems : []
3561
1923
 
3562
- /** 图上的标签只放得下一小段:截断加省略号,全文在 Inspector 里(点开就有)。 */
3563
- const trimLabel = (text, limit) => {
3564
- const value = String(text ?? '')
3565
- return value.length <= limit ? value : `${value.slice(0, limit - 1)}…`
3566
- }
1924
+ /**
1925
+ * **目录嵌套就是子图**:概念的父是 `node.parent`(is_a),实体的父是 `node.container`(组成)。
1926
+ * 一个父可以收起,收起后它的整棵子树藏起来,子树上的边改连到这个父身上(去掉自环、去重)。
1927
+ * 点多(> 40)时默认收到第一层,点开 +N 再往下看;点少时全展开。
1928
+ */
1929
+ const parentOf = (node) => {
1930
+ const id = node.kind === 'concept' ? (typeof node.parent === 'string' && node.parent !== '' ? `term:${node.parent}` : null) : (node.container ?? null)
1931
+ return id !== null && id !== node.id && nodeById.has(id) ? id : null
1932
+ }
1933
+ const parentById = new Map(layerNodes.map((node) => [node.id, parentOf(node)]))
1934
+ const childCount = new Map()
1935
+ for (const [, parent] of parentById) if (parent !== null) childCount.set(parent, (childCount.get(parent) ?? 0) + 1)
1936
+ const depthOfNode = (id) => {
1937
+ let depth = 0
1938
+ const seen = new Set([id])
1939
+ for (let at = parentById.get(id); at !== null && at !== undefined && !seen.has(at); at = parentById.get(at)) {
1940
+ seen.add(at)
1941
+ depth += 1
1942
+ }
1943
+ return depth
1944
+ }
1945
+ const [toggled, setToggled] = React.useState({})
1946
+ React.useEffect(() => {
1947
+ setToggled({})
1948
+ }, [layer])
1949
+ const crowded = layerNodes.length > 40
1950
+ const isCollapsed = (id) => (childCount.get(id) ?? 0) > 0 && (toggled[id] ?? (crowded && depthOfNode(id) >= 1))
1951
+ /** 往上找第一个没被收起的祖先挡住的点:自己可见就是自己。 */
1952
+ const shownAs = (id) => {
1953
+ let shown = id
1954
+ const seen = new Set([id])
1955
+ for (let at = parentById.get(id); at !== null && at !== undefined && !seen.has(at); at = parentById.get(at)) {
1956
+ seen.add(at)
1957
+ if (isCollapsed(at)) shown = at
1958
+ }
1959
+ return shown
1960
+ }
1961
+ const hiddenUnder = new Map()
1962
+ for (const node of layerNodes) {
1963
+ const shown = shownAs(node.id)
1964
+ if (shown !== node.id) hiddenUnder.set(shown, (hiddenUnder.get(shown) ?? 0) + 1)
1965
+ }
1966
+ const allNodes = layerNodes.filter((node) => shownAs(node.id) === node.id)
1967
+ const allEdges = (() => {
1968
+ const out = []
1969
+ const seen = new Set()
1970
+ for (const edge of layerEdges) {
1971
+ const from = nodeById.has(edge.from) ? shownAs(edge.from) : edge.from
1972
+ const to = nodeById.has(edge.to) ? shownAs(edge.to) : edge.to
1973
+ if (from === to) continue
1974
+ if (from === edge.from && to === edge.to) {
1975
+ out.push(edge)
1976
+ continue
1977
+ }
1978
+ const key = `${edge.kind}|${edge.label ?? ''}|${from}|${to}`
1979
+ if (seen.has(key)) continue
1980
+ seen.add(key)
1981
+ out.push({ ...edge, id: `${edge.id}@${from}>${to}`, from, to })
1982
+ }
1983
+ return out
1984
+ })()
1985
+ const toggle = (id) => setToggled((current) => ({ ...current, [id]: !isCollapsed(id) }))
1986
+ const countOf = (name) => graph.nodes.filter((node) => node.layer === name).length
1987
+ const entityNames = new Map(graph.nodes.filter((node) => node.kind === 'instance' && typeof node.ref === 'string' && node.ref !== '' && node.label !== node.ref).map((node) => [String(node.ref), String(node.label)]))
1988
+
1989
+ /** 图上的标签只放得下一小段:截断加省略号,全文在小卡里。 */
1990
+ const trimLabel = (text, limit) => {
1991
+ const value = String(text ?? '')
1992
+ return value.length <= limit ? value : `${value.slice(0, limit - 1)}…`
1993
+ }
3567
1994
 
3568
- /** 视觉编码:类型 → 颜色(概念蓝 / 值形态橙 / 实例绿 / 字面值紫)。 */
3569
- const nodeStyle = (node) => ({
3570
- background: node.kind === 'concept' ? 'rgba(74,163,255,0.16)' : node.kind === 'value_type' ? 'rgba(217,119,6,0.16)' : node.kind === 'instance' ? 'rgba(22,163,74,0.16)' : 'rgba(147,51,234,0.16)',
3571
- border: node.status === 'deprecated' ? '1px dashed rgba(127,127,127,0.4)' : `1px solid ${node.kind === 'concept' ? 'rgba(74,163,255,0.4)' : node.kind === 'value_type' ? 'rgba(217,119,6,0.4)' : node.kind === 'instance' ? 'rgba(22,163,74,0.4)' : 'rgba(147,51,234,0.4)'}`,
3572
- borderRadius: 6,
3573
- padding: '6px 12px',
3574
- fontSize: 12,
3575
- width: 168,
3576
- textAlign: 'center',
3577
- })
1995
+ /**
1996
+ * **节点位置是受控的,所以必须自己接住拖动**:不给 `onNodesChange`,
1997
+ * React Flow 内部的拖动没有地方落地,节点根本拖不动。位置是界面状态,换层或图变了就清掉。
1998
+ */
1999
+ const [dragged, setDragged] = React.useState({})
2000
+ const graphKey = `${layer}|${layerNodes.length}|${layerEdges.length}`
2001
+ React.useEffect(() => {
2002
+ setDragged({})
2003
+ }, [graphKey])
2004
+ const onNodesChange = React.useCallback((changes) => {
2005
+ setDragged((current) => {
2006
+ let next = current
2007
+ for (const change of changes) {
2008
+ if (change?.type !== 'position' || change.position === undefined) continue
2009
+ if (next === current) next = { ...current }
2010
+ next[change.id] = change.position
2011
+ }
2012
+ return next
2013
+ })
2014
+ }, [])
3578
2015
 
3579
- /**
3580
- * **节点位置是受控的,所以必须自己接住拖动**。
3581
- *
3582
- * React Flow 的 `nodes` 是受控 prop:不给 `onNodesChange`,它内部的拖动**没有地方落地**——
3583
- * 表现就是**节点根本拖不动**(拖前拖后 transform 一模一样)。
3584
- * 位置是**界面状态**(布局不是知识,不进账本),所以它住在这里;
3585
- * 换层或图变了就清掉,免得上一张图的拖动痕迹贴到新图上。
3586
- */
3587
- const [dragged, setDragged] = React.useState({})
3588
- const graphKey = `${layer}|${allNodes.length}|${allEdges.length}`
3589
- React.useEffect(() => {
3590
- setDragged({})
3591
- }, [graphKey])
3592
- const onNodesChange = React.useCallback((changes) => {
3593
- setDragged((current) => {
3594
- let next = current
3595
- for (const change of changes) {
3596
- if (change?.type !== 'position' || change.position === undefined) continue
3597
- if (next === current) next = { ...current }
3598
- next[change.id] = change.position
3599
- }
3600
- return next
3601
- })
3602
- }, [])
2016
+ /** 一个点的状态:矛盾 > 废止 > 待核验(只登记、没有写进长期知识的事实)> 平常。 */
2017
+ const nodeState = (node) => {
2018
+ if (Array.isArray(node.facts) && node.facts.some((id) => conflicted.has(id))) return 'conflict'
2019
+ if (node.status === 'deprecated') return 'off'
2020
+ if (node.kind !== 'concept' && node.kind !== 'value_type' && node.source === 'asserted' && (node.facts ?? []).length === 0) return 'pending'
2021
+ return 'on'
2022
+ }
2023
+ const selectedId = picked?.kind === 'node' ? picked.node.id : (focus ?? null)
3603
2024
 
3604
2025
  /**
3605
- * 投影节点 → React Flow 节点。
3606
- *
3607
- * 坐标 = **力导向松弛过的投影坐标**:投影给的按层折行排布作起点(确定性、读得懂),
3608
- * FA2 再把它松弛成这个图**自己的形状**(枢纽、簇)。拿不到 FA2 时就是起点本身。
2026
+ * 投影节点 → React Flow 节点。坐标 = 力导向松弛过的投影坐标(起点确定 ⇒ 同一份账本同一张图)。
2027
+ * 尺寸显式给:只写在 style 里的话,React Flow 要等测量完成才知道它多大。
3609
2028
  */
3610
- const rfNodes = forceLayout(allNodes.map((node) => ({
3611
- id: node.id,
3612
- type: 'default',
3613
- /** 拖过的以拖动为准(界面状态);没拖过的用布局算出来的。 */
3614
- position: dragged[node.id] ?? { x: node.x ?? 0, y: node.y ?? 0 },
3615
- /**
3616
- * **尺寸要显式给**。只写在 `style` 里的话,React Flow 要等测量完成才知道它多大,
3617
- * 而 **MiniMap 在测量完成之前拿不到宽高就直接跳过这些节点**——缩略图因此是一块空白
3618
- * (真机截图里就是那样)。`initialWidth/initialHeight` 是官方的「测量前尺寸」。
3619
- */
3620
- initialWidth: 168,
3621
- initialHeight: 34,
3622
- /** 图上只放得下一小段;全文在 Inspector 里,点开就有。 */
3623
- data: { label: trimLabel(`${node.label ?? node.ref ?? node.id}${node.uses > 0 ? ` (${node.uses})` : ''}`, 22) },
3624
- style: nodeStyle(node),
3625
- /** 冲突节点标红。 */
3626
- ...(Array.isArray(node.facts) && node.facts.some((id) => conflicted.has(id)) ? { className: 'clearai-node-conflict' } : {}),
3627
- })), allEdges.map((edge) => ({ source: edge.from, target: edge.to })))
2029
+ /**
2030
+ * 小图(十来个点)不跑力导向:几个点、几条边时它常常把点排成一条线。
2031
+ * 改成一圈:连得最多的那个点居中(它是这张图的主语),其余按投影顺序绕一圈——确定、好读。
2032
+ * 点少(六个以内)时不放中心:边都是圈上的弦,不会从某个点身上穿过去、压住它的标签。
2033
+ */
2034
+ const small = allNodes.length <= 14
2035
+ const ring = (() => {
2036
+ if (!small) return null
2037
+ const degree = new Map(allNodes.map((node) => [node.id, 0]))
2038
+ for (const edge of allEdges) {
2039
+ if (degree.has(edge.from)) degree.set(edge.from, degree.get(edge.from) + 1)
2040
+ if (degree.has(edge.to)) degree.set(edge.to, degree.get(edge.to) + 1)
2041
+ }
2042
+ const hub = allNodes.length >= 7 ? [...allNodes].sort((left, right) => degree.get(right.id) - degree.get(left.id))[0] : null
2043
+ const around = allNodes.filter((node) => node !== hub)
2044
+ const radius = 120 + around.length * 14
2045
+ const out = new Map()
2046
+ if (hub !== null) out.set(hub.id, { x: 0, y: 0 })
2047
+ around.forEach((node, index) => {
2048
+ const angle = -Math.PI / 2 + (index / around.length) * Math.PI * 2
2049
+ out.set(node.id, { x: Math.cos(angle) * radius * 1.6, y: Math.sin(angle) * radius })
2050
+ })
2051
+ return out
2052
+ })()
2053
+ const layout = small ? (nodes) => nodes : forceLayout
2054
+ const rfNodes = layout(
2055
+ allNodes.map((node) => ({
2056
+ id: node.id,
2057
+ type: 'default',
2058
+ position: dragged[node.id] ?? ring?.get(node.id) ?? { x: node.x ?? 0, y: node.y ?? 0 },
2059
+ initialWidth: 120,
2060
+ initialHeight: 26,
2061
+ selected: node.id === selectedId,
2062
+ data: {
2063
+ label: h(
2064
+ 'span',
2065
+ { className: 'clearai-star', 'data-kind': node.kind, 'data-state': nodeState(node), 'data-sel': node.id === selectedId ? '1' : '0' },
2066
+ h('i', null),
2067
+ h('span', null, trimLabel(node.kind === 'value_type' ? (FORM_WORD[node.ref] ?? node.label ?? node.ref) : (node.label ?? node.ref ?? node.id), 16)),
2068
+ (childCount.get(node.id) ?? 0) > 0
2069
+ ? h(
2070
+ 'b',
2071
+ {
2072
+ className: 'clearai-fold',
2073
+ title: isCollapsed(node.id) ? t('展开下一层') : t('收起下一层'),
2074
+ onClick: (event) => {
2075
+ event.stopPropagation()
2076
+ toggle(node.id)
2077
+ },
2078
+ },
2079
+ isCollapsed(node.id) ? `+${hiddenUnder.get(node.id) ?? 0}` : '−',
2080
+ )
2081
+ : null,
2082
+ ),
2083
+ },
2084
+ className: 'clearai-star-node',
2085
+ style: { background: 'transparent', border: 0, padding: 0, width: 'auto', boxShadow: 'none' },
2086
+ })),
2087
+ allEdges.map((edge) => ({ source: edge.from, target: edge.to })),
2088
+ )
3628
2089
 
3629
- /** 投影边 → React Flow 边(类型 / 冲突的视觉编码)。 */
2090
+ /** 投影边 → React Flow 边:实线已验证、虚线待核验、淡线上下位、告警色是矛盾。 */
2091
+ const edgeTone = (edge) => {
2092
+ if (edge.kind === 'assertion' && conflicted.has(edge.fact)) return { stroke: 'var(--dsw-alias-state-warn-primary)', strokeWidth: 1.4 }
2093
+ if (edge.kind === 'is_a') return { stroke: 'var(--dsw-alias-border-l3)', strokeWidth: 1, strokeDasharray: '1 3' }
2094
+ if (edge.status === 'retracted') return { stroke: 'var(--dsw-alias-border-l2)', strokeWidth: 1, strokeDasharray: '2 4' }
2095
+ if (edge.kind === 'assertion' && edge.status !== 'live') return { stroke: 'var(--dsw-alias-label-tertiary)', strokeWidth: 1.1, strokeDasharray: '5 4' }
2096
+ return { stroke: edge.kind === 'assertion' ? 'var(--dsw-alias-label-secondary)' : 'var(--dsw-alias-label-tertiary)', strokeWidth: edge.kind === 'assertion' ? 1.3 : 1 }
2097
+ }
3630
2098
  const rfEdges = allEdges.map((edge) => ({
3631
2099
  id: edge.id,
3632
2100
  source: edge.from,
3633
2101
  target: edge.to,
3634
- label: edge.label ?? '',
3635
- labelStyle: { fontSize: 9 },
3636
- labelBgStyle: { fill: 'var(--dsw-alias-bg-layer-1)', fillOpacity: 0.85 },
3637
- animated: edge.kind === 'assertion' && edge.status === 'live',
3638
- style: {
3639
- stroke: edge.kind === 'assertion' && conflicted.has(edge.fact) ? '#dc2626' : edge.kind === 'is_a' ? 'rgba(127,127,127,0.4)' : 'rgba(74,163,255,0.5)',
3640
- strokeWidth: edge.kind === 'assertion' ? 1.5 : 1,
3641
- ...(edge.kind === 'is_a' ? { strokeDasharray: '4 3' } : {}),
3642
- },
2102
+ type: 'straight',
2103
+ label: edge.kind === 'is_a' ? '' : (edge.label ?? ''),
2104
+ labelStyle: { fontSize: 10, fill: 'var(--dsw-alias-label-secondary)' },
2105
+ labelBgStyle: { fill: 'var(--dsw-alias-bg-base)', fillOpacity: 0.9 },
2106
+ labelBgPadding: [3, 1],
2107
+ style: edgeTone(edge),
3643
2108
  }))
3644
2109
 
3645
- /** 点击语义:点节点 / 边 = 打开 Inspector。 */
3646
2110
  const onNodeClick = (_event, node) => {
3647
2111
  const original = nodeById.get(node.id)
3648
- if (original === undefined) return
3649
- setPicked({ kind: 'node', node: original })
2112
+ if (original !== undefined) setPicked({ kind: 'node', node: original })
3650
2113
  }
3651
2114
  const onEdgeClick = (_event, edge) => {
3652
2115
  const original = allEdges.find((item) => item.id === edge.id)
3653
- if (original === undefined) return
3654
- setPicked({ kind: 'edge', edge: original })
2116
+ if (original !== undefined) setPicked({ kind: 'edge', edge: original })
3655
2117
  }
2118
+ /** 结论那边点了「相关」里的一个主语:当成在图上点了它。 */
2119
+ React.useEffect(() => {
2120
+ if (focus === null || focus === undefined) return
2121
+ const original = nodeById.get(focus)
2122
+ if (original !== undefined) setPicked({ kind: 'node', node: original })
2123
+ }, [focus])
3656
2124
 
3657
- /** Inspector 要看的对象。 */
3658
2125
  const inspection =
3659
2126
  picked === null
3660
2127
  ? null
3661
2128
  : picked.kind === 'edge'
3662
2129
  ? { kind: 'edge', id: picked.edge.id, label: picked.edge.label }
3663
2130
  : { kind: picked.node.kind, id: picked.node.kind === 'concept' ? String(picked.node.ref ?? '') : picked.node.id, label: picked.node.label ?? picked.node.ref ?? picked.node.id }
3664
- /** 「按此筛选」:概念筛概念、实例筛实体、断言边筛谓词。 */
3665
- const filterTarget = picked === null ? null : picked.kind === 'edge' ? picked.edge.predicate ?? null : String(picked.node.ref ?? '')
2131
+ /** 「只看相关」:概念筛概念、实例筛实体、断言边筛谓词。 */
2132
+ const filterTarget = picked === null ? null : picked.kind === 'edge' ? (picked.edge.predicate ?? null) : String(picked.node.ref ?? '')
3666
2133
 
3667
2134
  /**
3668
- * React Flow 的**实例**(用来 fit / 重新适配)。
3669
- *
3670
- * 注意:v12 里给 `<ReactFlow ref={…}>` 传 ref 拿到的是 DOM 节点,不是带 `fitView` 的
3671
- * 实例——这样写调用会**静默变成空操作**。实例要从 `onInit` 拿。
3672
- * 真机上踩到过:内联图带先按小画布 fit 了一次,切到全屏后画布大了十倍却没重新适配,
3673
- * 于是图缩在左上角一小块(截图里一眼可见)。
2135
+ * React Flow 的**实例**(用来适配)要从 `onInit` 拿:v12 里 ref 拿到的是 DOM 节点,
2136
+ * 在它上面调 `fitView` 会静默变成空操作。画布的几何或内容变了就重新适配一次。
3674
2137
  */
3675
2138
  const rfRef = React.useRef(null)
3676
- /**
3677
- * **什么时候重新适配**:画布的**几何**或**内容**变了就得重来一次。
3678
- *
3679
- * 三件都会让刚才那张图不在眼前:打开/关闭工作区(画布尺寸变了)、
3680
- * 切层(整套节点换了)、开/关详情抽屉(画布变窄)。
3681
- * React Flow 的视口变换**不会自己跟**这些变化——不重算就得到「详情读得到、图却空了」
3682
- * (真机截图里就是这个样子)。这里只在**这几件事发生**时重算,人自己拖过/缩过的视角
3683
- * 不会因为数据刷新被夺走。
3684
- */
3685
2139
  React.useEffect(() => {
3686
- const timer = setTimeout(() => rfRef.current?.fitView?.({ padding: 0.15, duration: 200 }), 60)
2140
+ const timer = setTimeout(() => rfRef.current?.fitView?.({ padding: 0.18, duration: 200 }), 60)
3687
2141
  return () => clearTimeout(timer)
3688
2142
  }, [fullscreen, layer, picked !== null, allNodes.length])
3689
2143
 
3690
- const shell = fullscreen === true ? { position: 'fixed', inset: 0, zIndex: 40, background: 'var(--dsw-alias-bg-layer-1)', padding: 12, display: 'flex', flexDirection: 'column', overflow: 'hidden' } : { ...S.section, padding: 6 }
3691
- const canvasBox = fullscreen === true ? { flex: '1 1 auto', minHeight: 240, minWidth: 0, overflow: 'hidden', border: '1px solid rgba(127,127,127,0.18)', borderRadius: 6 } : { height: 208, overflow: 'hidden', border: '1px solid rgba(127,127,127,0.18)', borderRadius: 6 }
3692
- /**
3693
- * **工作区里详情走右侧抽屉,不占画布的高度。**
3694
- *
3695
- * 这条不是审美:详情摆在画布下方时,它一出现画布就变矮,而 React Flow 的视口变换
3696
- * 不会跟着变——于是**刚点开的那个节点直接掉出可视区**(真机上就是这样:详情读得到,
3697
- * 图却空了)。侧栏只压缩宽度,而宽度方向的适配本来就有 `fitView` 管。
3698
- * 内联图带不适用:那里只有两百来像素高,横向没地方放抽屉。
3699
- */
2144
+ /** 全屏时小卡走右侧抽屉:摆在画布下方会把画布压矮,刚点开的那个点直接掉出可视区。 */
3700
2145
  const sideBySide = fullscreen === true
3701
-
2146
+ const empty =
2147
+ layer === 'entity'
2148
+ ? `${typeof unlanded === 'number' && unlanded > 0 ? `${t('有 ')}${unlanded}${t(' 个断言的主语还没写成实体文件。')}` : t('实体图还空着。')}${t('clear/ontology/entities/ 下的实体文件、写进长期知识的断言会出现在这里。')}`
2149
+ : t('本体图还空着。模型在 clear/ontology/concepts/ 和 relations/ 下写概念与关系文件之后,它们会长在这里。')
3702
2150
  return h(
3703
2151
  'div',
3704
- { style: shell },
3705
- h(
3706
- 'div',
3707
- { style: S.inline },
3708
- h('span', { style: { ...S.tag, cursor: 'pointer', opacity: layer === 'ontology' ? 1 : 0.5 }, onClick: () => { setPicked(null); onLayer('ontology') } }, t('本体图')),
3709
- h('span', { style: { ...S.tag, cursor: 'pointer', opacity: layer === 'entity' ? 1 : 0.5 }, onClick: () => { setPicked(null); onLayer('entity') } }, t('实体图')),
3710
- h('span', { style: { ...S.faint, flex: '1 1 auto' } }, t('点节点看知识详情')),
3711
- h('span', { style: S.chipAction, onClick: () => rfRef.current?.fitView({ padding: 0.15, duration: 200 }) }, t('适配')),
3712
- h('span', { style: S.chipAction, onClick: onToggleFullscreen }, fullscreen === true ? t('关闭工作区') : t('打开图谱工作区')),
3713
- ),
2152
+ { className: 'clearai-graph', 'data-full': fullscreen === true ? '1' : '0' },
3714
2153
  h(
3715
2154
  'div',
3716
- { style: { display: 'flex', flexDirection: sideBySide ? 'row' : 'column', gap: 8, flex: '1 1 auto', minHeight: 0, alignItems: 'stretch' } },
2155
+ { className: 'clearai-graph-bar' },
3717
2156
  h(
3718
- 'div',
3719
- { style: { display: 'flex', flexDirection: 'column', gap: 6, flex: '1 1 auto', minWidth: 0, minHeight: 0 } },
3720
- XYFlow === null || XYFlow.ReactFlow === undefined || XYFlow.ReactFlow === null
3721
- ? h('div', { style: S.faint }, `${t('图组件不可用')}(${String(XYFLOW_LOAD.reason ?? '')}):${t('投影还在,事实与命题照常可读。')}`)
3722
- : allNodes.length === 0
3723
- ? h(
3724
- 'div',
3725
- { style: S.faint },
3726
- /**
3727
- * **空态要说清"这一层的东西从哪来"**,不能只说「暂无」。
3728
- * 实体层尤其:它是本项目里唯一需要**独立写入口**的一层(RegisterInstance),
3729
- * 而图上看不见它时,人最容易得出的错结论是「断言都写了,图迟早会自己长出来」。
3730
- * 所以两句:为什么是空的 + 什么不算数。
3731
- * 别的层仍是一句平静的「暂无」:那里确实只是还没登记。
3732
- */
3733
- layer === 'entity'
3734
- ? `${typeof unlanded === 'number' && unlanded > 0 ? `${unlanded}${t(' 个断言主体还没落到这一层:')}` : t('这一层还没有实例节点。')}${t('实例靠 RegisterInstance 登记(带出处);断言挂在命题上不算「已知」。')}`
3735
- : t('此层暂无节点。'),
3736
- )
3737
- : h(
3738
- 'div',
3739
- { style: canvasBox },
3740
- h(XYFlow.ReactFlow, {
3741
- onInit: (instance) => {
3742
- rfRef.current = instance
3743
- },
3744
- onNodesChange,
3745
- nodes: rfNodes,
3746
- edges: rfEdges,
3747
- onNodeClick,
3748
- onEdgeClick,
3749
- onPaneClick: () => setPicked(null),
3750
- fitView: true,
3751
- minZoom: 0.2,
3752
- maxZoom: 3,
3753
- proOptions: { hideAttribution: true },
3754
- style: { background: 'transparent' },
3755
- nodesDraggable: true,
3756
- nodesConnectable: false,
3757
- elementsSelectable: true,
3758
- panOnDrag: true,
3759
- zoomOnScroll: true,
3760
- zoomOnDoubleClick: false,
3761
- },
3762
- h(XYFlow.Controls, { showInteractive: false }),
3763
- h(XYFlow.MiniMap, {
3764
- pannable: true,
3765
- zoomable: true,
3766
- /**
3767
- * **缩略图要看得懂**:默认节点是灰白的,在大图上等于一块空白方块——
3768
- * 这里按类型给实色(与画布上的淡色底不同:缩略图里小块面积小,淡色看不见)。
3769
- * 遮罩(视口指示)也调淡一点,别把整张缩略图盖成一个白框。
3770
- */
3771
- nodeColor: (node) => {
3772
- const layerKind = nodeById.get(node.id)?.kind ?? ''
3773
- if (layerKind === 'concept') return '#6ea8fe'
3774
- if (layerKind === 'value_type') return '#e0a458'
3775
- if (layerKind === 'instance') return '#4caf7d'
3776
- if (layerKind === 'literal') return '#a97fe0'
3777
- return '#9aa4b2'
3778
- },
3779
- maskColor: 'rgba(127,127,127,0.18)',
3780
- maskStrokeColor: 'rgba(127,127,127,0.45)',
3781
- style: { background: 'var(--dsw-alias-bg-layer-2)', border: '1px solid rgba(127,127,127,0.25)' },
3782
- }),
3783
- h(XYFlow.Background, { variant: 'dots', gap: 20, size: 1, color: 'rgba(127,127,127,0.15)' }),
3784
- ),
2157
+ 'span',
2158
+ { className: 'clearai-seg' },
2159
+ h('span', { 'data-on': layer === 'ontology' ? '1' : '0', onClick: () => { setPicked(null); onLayer('ontology') } }, `${t('本体图')} ${countOf('ontology')}`),
2160
+ h('span', { 'data-on': layer === 'entity' ? '1' : '0', onClick: () => { setPicked(null); onLayer('entity') } }, `${t('实体图')} ${countOf('entity')}`),
3785
2161
  ),
3786
- allNodes.length > 0
3787
- ? h('div', { style: S.faint }, `${allNodes.length} ${t('个节点')} · ${allEdges.length} ${t('条边')}${conflicts.length > 0 ? ` · ${t('冲突')} ${conflicts.length}` : ''}`)
3788
- : null,
2162
+ h(
2163
+ 'span',
2164
+ { className: 'clearai-legend' },
2165
+ h('span', { 'data-l': 'solid' }, t('已验证')),
2166
+ h('span', { 'data-l': 'dash' }, t('待核验')),
2167
+ conflicts.length > 0 ? h('span', { 'data-l': 'warn' }, `${t('矛盾')} ${conflicts.length}`) : null,
3789
2168
  ),
2169
+ h('span', { style: { flex: '1 1 auto' } }),
2170
+ allNodes.length === 0 ? null : h('span', { className: 'clearai-link', onClick: () => rfRef.current?.fitView?.({ padding: 0.18, duration: 200 }) }, t('适配')),
2171
+ h('span', { className: 'clearai-link', onClick: onToggleFullscreen }, fullscreen === true ? t('退出全屏') : t('全屏')),
2172
+ ),
2173
+ h(
2174
+ 'div',
2175
+ { className: 'clearai-graph-body', 'data-side': sideBySide ? '1' : '0' },
2176
+ XYFlow === null || XYFlow.ReactFlow === undefined || XYFlow.ReactFlow === null
2177
+ ? h('div', { className: 'clearai-graph-empty' }, `${t('图组件不可用')}(${String(XYFLOW_LOAD.reason ?? '')}):${t('结论照常可读。')}`)
2178
+ : allNodes.length === 0
2179
+ ? h('div', { className: 'clearai-graph-empty' }, empty)
2180
+ : h(
2181
+ 'div',
2182
+ { className: 'clearai-graph-canvas' },
2183
+ h(
2184
+ XYFlow.ReactFlow,
2185
+ {
2186
+ onInit: (instance) => {
2187
+ rfRef.current = instance
2188
+ },
2189
+ onNodesChange,
2190
+ nodes: rfNodes,
2191
+ edges: rfEdges,
2192
+ onNodeClick,
2193
+ onEdgeClick,
2194
+ onPaneClick: () => setPicked(null),
2195
+ fitView: true,
2196
+ minZoom: 0.2,
2197
+ maxZoom: 3,
2198
+ proOptions: { hideAttribution: true },
2199
+ style: { background: 'transparent' },
2200
+ nodesDraggable: true,
2201
+ nodesConnectable: false,
2202
+ elementsSelectable: true,
2203
+ panOnDrag: true,
2204
+ zoomOnScroll: true,
2205
+ zoomOnDoubleClick: false,
2206
+ },
2207
+ h(XYFlow.Controls, { showInteractive: false }),
2208
+ h(XYFlow.Background, { variant: 'dots', gap: 22, size: 1, color: 'var(--dsw-alias-border-l2)' }),
2209
+ ),
2210
+ ),
3790
2211
  picked === null
3791
2212
  ? null
3792
2213
  : h(
3793
2214
  'div',
3794
- { style: sideBySide ? { width: 380, flex: '0 0 auto', overflowY: 'auto', minHeight: 0 } : { flex: '0 0 auto' } },
3795
- h(GraphInspector, {
3796
- selection: inspection,
3797
- sessionId,
3798
- onFilter: () => onFilter(filterTarget),
3799
- onClose: () => setPicked(null),
3800
- }),
2215
+ { className: 'clearai-graph-side' },
2216
+ h(GraphInspector, { selection: inspection, sessionId, names: entityNames, onFilter: () => onFilter(filterTarget), onClose: () => setPicked(null) }),
3801
2217
  ),
3802
2218
  ),
3803
- )
3804
- }
3805
-
3806
- /**
3807
- * **词汇维护区**(折叠):词条表、健康度、废止列表、货架入口。
3808
- * 默认收起——它是参考材料,不挡结论;**0 条目 0 命题而有词条时自动展开**:
3809
- * 语言先于句子时,立好的词得有地方站。
3810
- */
3811
- const VocabBlock = ({ lexicon, open, onToggle, openPreview, sessionId }) => {
3812
- const terms = Array.isArray(lexicon?.terms) ? lexicon.terms : []
3813
- const predicates = Array.isArray(lexicon?.predicates) ? lexicon.predicates : []
3814
- const health = Array.isArray(lexicon?.health) ? lexicon.health : []
3815
- const warnings = health.filter((issue) => issue.severity === 'warning')
3816
- const deprecated = [...terms, ...predicates].filter((entry) => entry.status === 'deprecated')
3817
- /** 编辑是**具名动词的图形前端**:抽屉里填字段,提交走人门通道,面板从不写文件。 */
3818
- const [drawer, setDrawer] = React.useState(null)
3819
- const [busy, setBusy] = React.useState(false)
3820
- const [error, setError] = React.useState(null)
3821
- const submitOnto = (action, entry) => {
3822
- setBusy(true)
3823
- setError(null)
3824
- fetch('/api/clearai/gate', {
3825
- method: 'POST',
3826
- headers: { 'content-type': 'application/json' },
3827
- body: JSON.stringify({ sessionId, action, entry }),
3828
- })
3829
- .then((response) => response.json())
3830
- .then((result) => {
3831
- if (result.ok !== true) setError((result.problems ?? [result.error ?? 'failed']).join(';').slice(0, 200))
3832
- else setDrawer(null)
3833
- })
3834
- .catch((thrown) => setError(String(thrown?.message ?? thrown)))
3835
- .finally(() => setBusy(false))
3836
- }
3837
- if (terms.length === 0 && predicates.length === 0) return null
3838
- return h(
3839
- 'div',
3840
- { style: S.section },
3841
- h(
3842
- 'div',
3843
- { style: { ...S.head, cursor: 'pointer' }, onClick: onToggle },
3844
- `${t('词汇')}(${terms.length} ${t('概念')} · ${predicates.length} ${t('谓词')}${deprecated.length > 0 ? ` · ${deprecated.length} ${t('已废止')}` : ''}) ${open === true ? '▾' : '▸'}`,
3845
- ),
3846
- open === false
2219
+ /** 读时的跨文件检查只标不拦:有问题的那一份不进图,这里说清是哪个文件、哪里不对。 */
2220
+ problems.length === 0
3847
2221
  ? null
3848
2222
  : h(
3849
- 'div',
3850
- null,
3851
- ...terms.map((term) => h('div', { key: `vt-${term.id}`, style: S.row }, h('span', { style: S.mono }, term.id), h('span', { style: S.faint }, ` ${String(term.label ?? '')}${term.status === 'deprecated' ? ` · ${t('已废止')}` : ''}${term.parent ? ` ↖${term.parent}` : ''}`), term.status === 'deprecated' ? null : h('span', { style: S.chipAction, onClick: () => { const reason = window?.prompt?.(`${t('废止')} ${term.id}:${t('为什么?')}`) ; if (typeof reason === 'string' && reason.trim() !== '') submitOnto('deprecate_entry', { id: term.id, reason: reason.trim() }) } }, t('废止')))),
3852
- ...predicates.map((predicate) => h('div', { key: `vp-${predicate.id}`, style: S.row }, h('span', { style: S.mono }, predicate.id), h('span', { style: S.faint }, ` ${String(predicate.label ?? '')}${predicate.functional === true ? ` · ${t('单值')}` : ''}${predicate.status === 'deprecated' ? ` · ${t('已废止')}` : ''}`), predicate.status === 'deprecated' ? null : h('span', { style: S.chipAction, onClick: () => { const reason = window?.prompt?.(`${t('废止')} ${predicate.id}:${t('为什么?')}`) ; if (typeof reason === 'string' && reason.trim() !== '') submitOnto('deprecate_entry', { id: predicate.id, reason: reason.trim() }) } }, t('废止')))),
3853
- warnings.length > 0 ? h('div', { style: S.row }, `${t('健康')}:`, ...warnings.map((issue, index) => h('div', { key: `vh-${index}`, style: S.faint }, ` ${issue.id}:${issue.detail}`))) : null,
3854
- deprecated.length > 0
3855
- ? h('div', { style: S.row }, ...deprecated.map((entry) => h('div', { key: `vd-${entry.id}`, style: S.faint }, ` ${entry.id} — ${String(entry.deprecated?.reason ?? '')}`)))
3856
- : null,
3857
- openPreview === undefined ? null : h('span', { style: S.chipAction, onClick: () => openPreview('clear/ontology/domain.md') }, t('打开词汇货架(原生预览)')),
3858
- /**
3859
- * **编辑入口(阶段 E)**:登记概念 / 登记谓词两个表单抽屉;条目行的
3860
- * 修订与废止在各自那行(见下)。提交 = 一次人门动词;判据在宿主半,
3861
- * 界面只把表单递过去、把问题清单带回来。
3862
- */
3863
- h('div', { style: S.inline },
3864
- h('span', { style: S.chipAction, onClick: () => setDrawer({ kind: 'term' }) }, `+ ${t('概念')}`),
3865
- h('span', { style: S.chipAction, onClick: () => setDrawer({ kind: 'predicate' }) }, `+ ${t('谓词')}`),
2223
+ 'details',
2224
+ { className: 'clearai-graph-problems' },
2225
+ h('summary', null, `${t('本体文件有 ')}${problems.length}${t(' 处问题(这些不进图)')}`),
2226
+ ...problems.slice(0, 12).map((item, index) =>
2227
+ h('div', { key: `${item.path ?? ''}#${index}`, 'data-sev': item.severity === 'error' ? 'error' : item.severity === 'info' ? 'info' : 'warning' }, h('code', null, String(item.path ?? '')), ` ${String(item.detail ?? item.code ?? '')}`),
3866
2228
  ),
3867
- drawer === null
3868
- ? null
3869
- : h(OntoDrawer, { drawer, setDrawer, busy, error, onSubmit: submitOnto }),
2229
+ problems.length > 12 ? h('div', null, `… ${problems.length - 12}`) : null,
3870
2230
  ),
3871
2231
  )
3872
2232
  }
3873
2233
 
3874
- /**
3875
- * **词汇抽屉**:登记概念/谓词的最小表单。字段与动词参数一一对应;
3876
- * 判据不在这里重复——服务端拒了就把问题清单原样亮出来。
3877
- */
3878
- const OntoDrawer = ({ drawer, setDrawer, busy, error, onSubmit }) => {
3879
- const [form, setForm] = React.useState({ id: '', label: '', gloss: '', basis: '', parent: '', domain: '', unit: '', form: 'quantity', functional: false })
3880
- const field = (key, placeholder) =>
3881
- h('input', {
3882
- value: form[key] ?? '',
3883
- placeholder,
3884
- style: { ...S.chip, cursor: 'text', width: '100%' },
3885
- onInput: (event) => setForm({ ...form, [key]: event?.target?.value ?? '' }),
3886
- })
3887
- const isTerm = drawer.kind === 'term'
3888
- const entry = isTerm
3889
- ? { id: form.id, label: form.label, gloss: form.gloss, basis: form.basis, parent: form.parent === '' ? undefined : form.parent }
3890
- : { id: form.id, label: form.label, gloss: form.gloss, basis: form.basis, domain: form.domain === '' ? undefined : form.domain, range: { form: form.form, unit: form.unit === '' ? undefined : form.unit }, functional: form.functional === true ? true : undefined }
3891
- return h(
3892
- 'div',
3893
- { style: S.chipCard },
3894
- h('div', { style: S.chipCardTitle }, isTerm ? t('登记概念') : t('登记谓词')),
3895
- field('id', 'id(snake_case)'),
3896
- field('label', isTerm ? t('名称') : t('名称')),
3897
- field('gloss', t('释义(一句话)')),
3898
- isTerm ? field('parent', `${t('父概念')} id(可选)`) : field('domain', `${t('主词域')} id(可选)`),
3899
- isTerm === true ? null : field('unit', `${t('单位')}(可选)`),
3900
- isTerm === true ? null : h('label', { style: S.faint }, h('input', { type: 'checkbox', checked: form.functional === true, onChange: (event) => setForm({ ...form, functional: event?.target?.checked === true }) }), ` ${t('单值')}`),
3901
- field('basis', `${t('依据')}(必填)`),
3902
- error === null ? null : h('div', { style: { color: '#dc2626', fontSize: 11 } }, String(error)),
3903
- h('div', { style: S.chipActions },
3904
- h('span', { style: S.chipAction, onClick: busy === true ? undefined : () => onSubmit(isTerm === true ? 'register_term' : 'register_predicate', entry) }, busy === true ? t('提交中…') : t('提交')),
3905
- h('span', { style: S.chipAction, onClick: () => setDrawer(null) }, t('取消')),
3906
- ),
3907
- )
3908
- }
3909
-
3910
- const LocalizedDeliverables = withLocale(Deliverables)
3911
- const LocalizedFacts = withLocale(Facts)
2234
+ const LocalizedFacts = withLocale(Atlas)
3912
2235
  const LocalizedWorldTree = withLocale(WorldTree)
3913
- const LocalizedBrainTab = withLocale(BrainTab)
3914
2236
 
3915
2237
  /** 会话预设的 id:面板只在这个模式的会话里出现。 */
3916
2238
  const PRESET_ID = 'clearai'
@@ -4052,8 +2374,7 @@ window.__ModuleLoader__.load({
4052
2374
  * 右栏页签却是 Worldlines)。惰性取值让原生每次渲染现问一次,切换语言立刻跟上。
4053
2375
  */
4054
2376
  const rightRail = [
4055
- { id: 'clearai-worldtree', kind: 'clearai-worldtree', label: () => t('世界树'), order: 15, description: () => t('计划的拓扑与闸门:脊柱步、车道、收敛点、人工决策点'), icon: NATIVE_ICONS.IconBranchOutline16 },
4056
- { id: 'clearai-brain', kind: 'clearai-brain', label: () => t('技能 · 记忆'), order: 20, description: () => t('合并目录、本会话用法、可引用可采纳'), icon: NATIVE_ICONS.IconSkillOutline16 },
2377
+ { id: 'clearai-worldtree', kind: 'clearai-worldtree', label: () => t('世界树'), order: 15, description: () => t('计划的步骤与闸门'), icon: NATIVE_ICONS.IconBranchOutline16 },
4057
2378
  ]
4058
2379
  const shown = new Set()
4059
2380
  const syncRail = () => {
@@ -4084,8 +2405,7 @@ window.__ModuleLoader__.load({
4084
2405
  }
4085
2406
  const body =
4086
2407
  tab.body ??
4087
- (tab.id === 'clearai-worldtree'
4088
- ? (props) =>
2408
+ ((props) =>
4089
2409
  h(LocalizedWorldTree, {
4090
2410
  ...props,
4091
2411
  openPreview: openPreviewFor(props),
@@ -4106,8 +2426,7 @@ window.__ModuleLoader__.load({
4106
2426
  return false
4107
2427
  }
4108
2428
  },
4109
- })
4110
- : (props) => h(LocalizedBrainTab, { ...props, isCurrentPreset, openRail, openPreview: openPreviewFor(props) }))
2429
+ }))
4111
2430
  own.push(ctx.effect(() => slots.inject('sidebar.right.pane.tab', () => slots.register({ name: 'sidebar.right.pane.tab', key: tab.id }, body)), `clearai: rail body ${tab.id}`))
4112
2431
  own.push(ctx.effect(() => slots.inject('sidebar.right.pane.tab.title', () => slots.register({ name: 'sidebar.right.pane.tab.title', key: tab.id }, () => tab.label)), `clearai: rail title ${tab.id}`))
4113
2432
  railDisposers.set(tab.id, () => {
@@ -4124,7 +2443,7 @@ window.__ModuleLoader__.load({
4124
2443
  const railDisposers = new Map()
4125
2444
 
4126
2445
  /**
4127
- * 打开右栏的某张页签(跨面跳转:点「需要你 N」→ 进展)。
2446
+ * 打开右栏的某张页签(跨面跳转:点「待处理 N」→ 进展)。
4128
2447
  * 不再有我们自己的「预览」页签了:预览走**原生**的文档预览(`openNativePreview`)——
4129
2448
  * 那一条既有 markdown/图片/pdf/html 的渲染,也有它自己的分页读盘。
4130
2449
  */
@@ -4162,15 +2481,8 @@ window.__ModuleLoader__.load({
4162
2481
  /** 每个座位按自己的 props 造一个打开器:共用一个「反正差不多」的闭包就是上面那个 bug。 */
4163
2482
  const openPreviewFor = (props) => (path) => openNativePreview(sidebarRight, sessionIdFor(props), path)
4164
2483
 
4165
- /**
4166
- * 中栏视图:**产物**(中栏第一眼该看「我拿到了什么」)。
4167
- * 原来占着中栏的「循环」面板搬进右栏「进展」——那种全景适合与对话并排,
4168
- * 而中栏这个位子(native 已有 chat / trajectory)该留给**结果**。
4169
- */
4170
- occupy('conversation.view', () => ({ id: 'clearai-deliverables', order: 15, label: t('产物') }), (props) => h(LocalizedDeliverables, { ...props, openRail, openPreview: openPreviewFor(props) }))
4171
2484
  /**
4172
2485
  * 中栏视图:**事实**——整条闭环一屏看完(假设是起点,事实是沉淀)。
4173
- * 放在「产物」之后:先看拿到了什么,再看**凭什么**(以及已经确认了什么)。
4174
2486
  * 这一格与模型读的 `clear/knowledge/facts/INDEX.md` 是**同一张表**。
4175
2487
  */
4176
2488
  /**
@@ -4200,17 +2512,11 @@ window.__ModuleLoader__.load({
4200
2512
  * 而我们只在**当前会话是本预设**时占座(`occupy`),会话切走就还给它。
4201
2513
  */
4202
2514
  occupy('conversation.input.plan', { priority: -1 }, (props) => h(PlanChip, { ...props, openRail }))
4203
- occupy('conversation.input.left', { id: 'clearai-continuation', order: 110 }, (props) => h(ContinuationNote, props))
4204
- /**
4205
- * **续跑档控制搬进工具行**(控制归工具行,事实归输入框下方)。
4206
- * `conversation.input.left` 是**加法**座位(list),与原生「完全权限」并排——
4207
- * 同一类东西(我能改的)放同一行,这是原生的排版,不是我们发明的。
4208
- */
4209
2515
  // 输入框下方的常驻派生条(运行态卡在人这一侧的对应物)
4210
2516
  /**
4211
2517
  * **输入框下那一条不再注册**:它说的话(阶段 / 完成度 / 当前步)与原生目标提示、
4212
2518
  * 与工具行那颗计划 chip 重复 ✗,却**独占一行把输入框顶上去** ✗。
4213
- * 唯一可点、且只有我们知道的那件事(「需要你 N」)已经并进计划 chip(同一行,一点直达世界树)。
2519
+ * 唯一可点、且只有我们知道的那件事(「待处理 N」)已经并进计划 chip(同一行,一点直达世界树)。
4214
2520
  */
4215
2521
  ctx.effect(() => sessions.list.subscribe(() => {
4216
2522
  for (const sync of seats) sync()
@@ -4232,12 +2538,12 @@ window.__ModuleLoader__.load({
4232
2538
  * 真的跑一遍渲染路径(捕 undefined 字段访问这类只有渲染时才炸的错)。
4233
2539
  * 仍然不是给别的包用的接口。
4234
2540
  */
4235
- exports.__components = { PlanChip, ContinuationNote, Deliverables, WorldTree, BrainTab, GraphBand, GraphInspector, VocabBlock, AssertionChips, Inbox, TreeDetail, Facts, FactShelf, PropositionShelf, PropositionRow, GapShelf, LevelGuide, ClearAIMark, LOOP_LABEL, PROPOSITION_GROUPS }
2541
+ exports.__components = { PlanChip, WorldTree, GraphBand, GraphInspector, NeedYou, TreeDetail, Atlas, AtlasHeader, ConclusionList, ConclusionRow, ConclusionDetail, StationBar, TrustTimeline, ClearAIMark, LOOP_LABEL }
4236
2542
  /**
4237
2543
  * 测试缝之三:命题那一列的**派生**是纯函数(分组、处境、来路、证据链),
4238
2544
  * 渲染本身没法在没浏览器的地方细究——把它导出去,让测试直接断言派生结果。
4239
2545
  */
4240
- exports.__propositions = { evidenceOf, transitionsOf, whereOf, judgeOf, originsOf, stepOfFact }
2546
+ exports.__propositions = { evidenceOf, originsOf, stepOfFact, conclusionsOf }
4241
2547
  /** 测试缝之四:本体格的过滤判据(纯函数)。 */
4242
2548
  exports.__ontology = { termMatches }
4243
2549
  return module.exports