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/host.js CHANGED
@@ -15,13 +15,12 @@
15
15
  * `useProjection('clearai')` 读——免轮询、自带变更通知。
16
16
  */
17
17
 
18
- import { readdirSync, statSync } from 'node:fs'
19
- import { join, relative, resolve, sep } from 'node:path'
20
18
  import { z } from 'zod'
21
- import { HUMAN_GATE_ACTIONS, HUMAN_GATE_MARK, MUTATION_KIND, STATE_VERSION, applyEvent, applyMutations, derive, emptyState, inspectGraphSelection, renderCard, view } from './fold.js'
22
- import { describeDomainShelf, formatAssertion, graphProjection, validateAssertions, validatePredicate, validateTerm } from './domain-language.js'
19
+ import { MUTATION_KIND, STATE_VERSION, applyEvent, applyMutations, derive, emptyState, inspectGraphSelection, renderCard, view } from './fold.js'
20
+ import { ONTOLOGY_SCHEMA, checkOntologyFile, describeDomainShelf, fingerprintDefinitions, formatAssertion, graphProjection, validateAssertions, validatePredicate, validateTerm } from './domain-language.js'
23
21
  import { knowledgeView as knowledgeViewOf } from './knowledge-view.js'
24
22
  import { install as installInvariants } from './invariant.js'
23
+ import { detectLanguage, tr, withLanguage } from './lang.js'
25
24
 
26
25
  export const name = 'clearai-host'
27
26
  /** 投影注册表与会话存储:两个都是宿主服务,这里只消费。 */
@@ -37,14 +36,8 @@ const stateSchema = z.looseObject({
37
36
  audits: z.array(z.unknown()),
38
37
  materials: z.array(z.unknown()),
39
38
  facts: z.array(z.unknown()),
40
- forks: z.array(z.unknown()),
41
- scouts: z.array(z.unknown()),
42
39
  blocks: z.record(z.string(), z.unknown()),
43
40
  releases: z.array(z.unknown()),
44
- /** 宿主原生的合并技能目录(内核取一次快照,变了才发)。 */
45
- skillCatalog: z.unknown().nullable(),
46
- /** 本会话的技能加载记录(从 tool/call 与 `/名字` 用户消息折出来,零新账)。 */
47
- skillUsage: z.record(z.string(), z.unknown()),
48
41
  })
49
42
 
50
43
  /** 浏览器读到的那个视图(面板契约——客户端与宿主半各自消费它的同一批字段)。 */
@@ -54,19 +47,12 @@ const viewSchema = z.looseObject({
54
47
  sessionId: z.string().nullable(),
55
48
  goal: z.unknown().nullable(),
56
49
  plan: z.unknown().nullable(),
57
- /** 收件箱:面板「需要你 N」的数据面(条目只带分诊信息,见 fold.js 的 derive)。 */
58
- inbox: z.array(z.unknown()),
59
- hasOpenGate: z.boolean(),
60
- /** 外脑全景:技能清单与记忆索引(内核扫描后随投影下发,面板据此画「技能 · 记忆」页签)。 */
61
- brain: z.unknown().nullable(),
62
- /** 技能面:**合并目录**(与模型同一张表)+ 本会话用量(面板「技能 · 记忆」页签)。 */
63
- skills: z.unknown(),
50
+ /** 待处理:只陈述的几行(计划停下、结论矛盾),见 fold.js 的 derive。 */
51
+ needYou: z.array(z.unknown()),
64
52
  evidence: z.array(z.unknown()),
65
53
  audits: z.array(z.unknown()),
66
54
  materials: z.array(z.unknown()),
67
55
  facts: z.array(z.unknown()),
68
- forks: z.array(z.unknown()),
69
- scouts: z.array(z.unknown()),
70
56
  })
71
57
 
72
58
  export function apply(ctx) {
@@ -96,7 +82,8 @@ export function apply(ctx) {
96
82
  const memoView = (state) => {
97
83
  if (state === lastState && lastView !== null) return lastView
98
84
  lastState = state
99
- lastView = view(state)
85
+ // 面板读数跟着人说话的语言(`state.language`);还没听到人说话时沿用当前语言。
86
+ lastView = withLanguage(state?.language, () => view(state))
100
87
  return lastView
101
88
  }
102
89
 
@@ -165,7 +152,7 @@ export function apply(ctx) {
165
152
  sessions = undefined
166
153
  }
167
154
  if (sessions === undefined || sessions === null) {
168
- noteHostHealth('sessions', '宿主半此刻拿不到会话服务:读面退回空态(不是「这个会话不存在」)')
155
+ noteHostHealth('sessions', tr('宿主半此刻拿不到会话服务:读面退回空态(不是「这个会话不存在」)', 'The host half cannot reach the session service right now: readings fall back to empty (this does not mean the session is missing)'))
169
156
  return undefined
170
157
  }
171
158
  return sessions
@@ -180,7 +167,7 @@ export function apply(ctx) {
180
167
  projections = undefined
181
168
  }
182
169
  if (projections === undefined || projections === null) {
183
- noteHostHealth('sessionProjections', '宿主半此刻拿不到投影服务:读面退回空态(不是「这个会话没有状态」)')
170
+ noteHostHealth('sessionProjections', tr('宿主半此刻拿不到投影服务:读面退回空态(不是「这个会话没有状态」)', 'The host half cannot reach the projection service right now: readings fall back to empty (this does not mean the session has no state)'))
184
171
  return undefined
185
172
  }
186
173
  return projections
@@ -198,7 +185,7 @@ export function apply(ctx) {
198
185
  try {
199
186
  ctx.on('internal/status', (fiber, oldValue) => {
200
187
  if (fiber !== ctx.fiber || oldValue !== FIBER_ACTIVE) return
201
- const detail = `宿主半 fiber 掉出 ACTIVE(${oldValue} → ${fiber.state}):两个注入服务这一刻都读不到,读面退回空态`
188
+ const detail = tr(`宿主半 fiber 掉出 ACTIVE(${oldValue} → ${fiber.state}):两个注入服务这一刻都读不到,读面退回空态`, `The host half fiber left ACTIVE (${oldValue} → ${fiber.state}): neither injected service can be read right now, so readings fall back to empty`)
202
189
  noteHostHealth('sessions', detail)
203
190
  noteHostHealth('sessionProjections', detail)
204
191
  ctx.logger?.warn?.(`clearai: ${detail}`)
@@ -249,32 +236,20 @@ export function apply(ctx) {
249
236
  }
250
237
 
251
238
  /** 面板视图:与 `state()` 同一份降级读数(健康事实一起交出去)。 */
252
- const viewOf = (sessionId) => withHostHealth(view(stateOf(sessionId)))
239
+ const viewOf = (sessionId) => {
240
+ const state = stateOf(sessionId)
241
+ return withHostHealth(withLanguage(state?.language, () => view(state)))
242
+ }
253
243
 
254
244
  /**
255
- * ═══ 人门通道(D2=B:面板可写,但只写人门动作)═══
256
- *
257
- * 三条硬约束,每一条都有测试盯着:
258
- * ① **动词白名单**:adopt_branch / abandon_fork / promote_skill,
259
- * 表外一律拒(与贡献表同一套「表外的名字不许出现」)——**取值**也在这一层校验
260
- * (技能名合原生语法),宁可 400,也不静默半生效;
261
- * `set_autonomy` 已摘掉:「在场与否」是运行时状态,不该是面板上的一个开关 ✗;
262
- * ② **agent 不可达**:这些动词**没有工具 schema**——模型的工具面里不存在它们。
263
- * 模型能做的只是读到「人做了什么」这条事实(它进的是会话日志,不是内核状态);
264
- * ③ **留下署名**:动作变成一条 `source.kind === 'user'` 的消息,折进投影时写
265
- * `by:'user'`——「谁在什么时候裁了哪条世界线、扶正了哪个技能、把档切成了什么」都在日志里。
245
+ * ═══ 面板只读 ═══
266
246
  *
267
- * 两个动词已砍掉(奥卡姆:它们是重复,不是能力):
268
- * · `confirm_plan` —— 原生 `dsh-plan-mode` 就是「用户复核的出口」;而授权本来就「交付即落账」;
269
- * · `invoke_skill` —— 原生 `/` 技能触发器做同一件事(还带候选菜单),我们那条只是在旁边
270
- * 又写了一遍同一个手势。
271
- *
272
- * 为什么走 HTTP 路由而不是 `harness.handle`:那是 cordis **动态插件**的座位,静态客户端包
273
- * 没有它(`host.call` 只在动态运行时的包里存在)。宿主平面的路由是这条
274
- * 通道在静态包里的对应物,与 ClearAI 面板自己的写接口(FastAPI 路由)同一个层级。
247
+ * 面板没有写入口。原来的人门通道 `/api/clearai/gate` 在第六阶段整条拿掉:
248
+ * 撤回 / 维持事实第三阶段起由内核在那次交付里当场问人;本体四动词随编辑抽屉一起删,
249
+ * 要改词汇就在对话里说。旧日志里人按过的动作仍由 `fold.js` 的 `parseHumanGate` 照旧折出来。
275
250
  */
276
251
  /**
277
- * 面板用的三条 HTTP 面**必须挂 `connection` 的 exact fetch route 表**,不能只挂 `webServer`——
252
+ * 面板用的 HTTP 面**必须挂 `connection` 的 exact fetch route 表**,不能只挂 `webServer`——
278
253
  * 挂错层,浏览器里一按就是 404。
279
254
  *
280
255
  * 浏览器那侧的 `/api/*` 先落到 connection 的共享 channel:它先查 exact fetch route 表;
@@ -289,199 +264,6 @@ export function apply(ctx) {
289
264
  * `ctx.get('webServer')` 拿一次、拿不到就整段跳过」的写法,在服务晚到时是**静默不挂**。
290
265
  */
291
266
  const reply = (status, payload) => Response.json(payload, { status, headers: { 'cache-control': 'no-store' } })
292
- /**
293
- * 产物的**语义分类**:
294
- * 章程 `PROJECT.md` / 项目资料 `input/` / 分析过程 `lab/` / 输出成果 `products/` / 外脑 `clear/`。
295
- * 面板据此把「核心产物」排在前面,而不是把 100 个中间文件与 3 份交付物混在一起。
296
- */
297
- const pathAreaOf = (path) => {
298
- const clean = String(path ?? '').replace(/^\.\//, '')
299
- if (clean === 'PROJECT.md' || clean === 'project.md') return { key: 'constitution', label: '章程' }
300
- if (clean.startsWith('input/')) return { key: 'input', label: '项目资料' }
301
- if (clean.startsWith('lab/')) return { key: 'process', label: '分析过程' }
302
- if (clean.startsWith('products/')) return { key: 'output', label: '输出成果' }
303
- if (clean.startsWith('clear/')) return { key: 'brain', label: '外脑' }
304
- return { key: 'other', label: '未分类' }
305
- }
306
- const resolveInside = (cwd, candidate) => {
307
- if (typeof candidate !== 'string' || candidate === '') return null
308
- const base = resolve(cwd)
309
- const target = resolve(base, candidate)
310
- if (target !== base && !target.startsWith(`${base}${sep}`)) return null
311
- return target
312
- }
313
- /**
314
- * 盘上**实际**的输出成果:`products/` 底下真有、但**任何计划都还没声明**的那些文件。
315
- *
316
- * 为什么要它:在一个已经有产物的老项目里开新会话,「产物」栏只有
317
- * 「阶段 0 · 交付 0/0」—— 因为这一栏原来只认投影里的**声明**。可人此刻问的是「我拿到了什么」,
318
- * 盘上那份 46 KB 的 HTML 明明就在。声明与实际是两件事,这一栏本来就该两边都看得见。
319
- *
320
- * 奥卡姆:不新开读面、不建第二本账 —— 就是一次有界的目录遍历,字段与声明的产物**同一形状**,
321
- * 所以面板能复用同一行渲染。边界写死在这里(depth ≤ 5、扫 ≤2000 条、返回 ≤200 条、
322
- * 跳隐藏项与符号链接目录),因为这一栏是给人「第一眼」看的,把整个工作区倒进来就等于
323
- * 把它变回一个文件管理器。
324
- *
325
- * 两个上限分工不同:**扫描上限**防的是巨大的 products/(扫描要停得下来),
326
- * **返回上限**防的是把 2000 条全塞给面板。上限留在结果侧而不是扫描侧,返回的那 200 条
327
- * 才真是**最近改动的**那 200 条,而不是 readdir 碰巧先遇到的那批。
328
- */
329
- const OUTPUT_MAX_DEPTH = 5
330
- const OUTPUT_MAX_SCAN = 2000
331
- const OUTPUT_MAX_FILES = 200
332
- const listWorkspaceOutputs = (cwd) => {
333
- const root = resolveInside(cwd, 'products')
334
- if (root === null) return []
335
- const workspaceRoot = resolve(cwd)
336
- const found = []
337
- let scanned = 0
338
- const walk = (dir, depth) => {
339
- if (depth > OUTPUT_MAX_DEPTH || scanned >= OUTPUT_MAX_SCAN) return
340
- let entries = []
341
- try {
342
- entries = readdirSync(dir, { withFileTypes: true })
343
- } catch {
344
- // 目录读不了(权限/竞态):跳过这一支,不把整条读面拖成失败。
345
- return
346
- }
347
- for (const entry of entries) {
348
- if (scanned >= OUTPUT_MAX_SCAN) return
349
- if (entry.name.startsWith('.')) continue
350
- const absolute = join(dir, entry.name)
351
- // 符号链接不进(目录可能成环;文件也不是「盘上真的有一份」)。
352
- if (entry.isDirectory()) {
353
- walk(absolute, depth + 1)
354
- continue
355
- }
356
- if (!entry.isFile()) continue
357
- scanned += 1
358
- let info = null
359
- try {
360
- info = statSync(absolute)
361
- } catch {
362
- continue
363
- }
364
- // 路径一律**相对工作区**(与声明的产物同一形式):面板把它原样交给原生预览,
365
- // 原生按工作区根解析 —— 给成相对 products/ 的路径会指到别的文件上去。
366
- found.push({ relative: relative(workspaceRoot, absolute), bytes: info.size, modifiedAt: info.mtimeMs })
367
- }
368
- }
369
- walk(root, 1)
370
- // 最近动过的排前面:人问的是「我拿到了什么」,新东西更可能是答案。
371
- return found.sort((left, right) => right.modifiedAt - left.modifiedAt).slice(0, OUTPUT_MAX_FILES)
372
- }
373
- /**
374
- * 一条人门消息:**署名是人 + 结构化标记 + 一句人话**。
375
- * 两条路(面板直接点 / 原生提问卡答)共用它 ⇒ 无论从哪儿来,落进日志的是同一种事实。
376
- */
377
- function appendHumanGate(agent, { detail, human, followUp }) {
378
- const text = `${HUMAN_GATE_MARK} ${JSON.stringify(detail)}\n${human}这是**结构化的决定,不是商量**:${followUp}。\n`
379
- const message = {
380
- id: `clearai-gate-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
381
- role: 'user',
382
- content: [{ type: 'text', text }],
383
- source: { kind: 'user' },
384
- }
385
- // 跑着就插到最近的步边界(steer);闲着就叫醒它(followup)。
386
- if (agent.status === 'running') agent.steer(message)
387
- else agent.followup(message)
388
- }
389
-
390
- /**
391
- * 把一道开着的人门**建成一张原生提问卡**(混合路径:内容走原生,裁决回我们的账本)。
392
- *
393
- * 问题与选项都从**投影**现算(此刻真实的分叉与技能),不在前端拼——前端只发一个手势。
394
- * 认不出来(那道门已经不在了)就返回 `null`:如实拒掉,不摆一张过期的卡。
395
- */
396
- function buildGateQuestion(gate, { sessionId, fork, skill }) {
397
- const state = stateOf(sessionId)
398
- const projected = view(state)
399
- if (gate === 'fork_adopt') {
400
- const target = (projected.forks ?? []).find((item) => item.id === fork && item.phase === 'deciding')
401
- if (target === undefined) return null
402
- const branches = target.branches ?? []
403
- if (branches.length === 0) return null
404
- const recommended = branches.find((branch) => branch.id === target.recommended) ?? null
405
- const lines = [
406
- `**要裁决的**:${target.question}`,
407
- `**判据**:${target.decideBy?.metric ?? '—'}(取${target.decideBy?.direction === 'min' ? '最小' : '最大'})`,
408
- '',
409
- ...branches.map((branch) => `- **${branch.label}**(${branch.id}${branch.id === target.recommended ? ' · 推荐' : ''}):${branch.approach} · 读数 ${branch.reading ?? '—'} · 有效性 ${branch.validity ?? '—'}`),
410
- ]
411
- if (target.provisional === true) lines.push('', '⚠️ 这次裁决还是**暂定**的:证据不够硬。')
412
- return {
413
- kind: 'adopt_branch',
414
- questions: [
415
- {
416
- id: 'fork-adopt',
417
- header: '世界线裁决',
418
- question: `采纳哪条世界线?(推荐:${recommended?.label ?? '无'})`,
419
- detail: lines.join('\n'),
420
- options: branches.map((branch) => ({
421
- label: branch.label,
422
- description: `${branch.approach} · 读数 ${branch.reading ?? '—'}`,
423
- })),
424
- },
425
- ],
426
- byLabel: new Map(branches.map((branch) => [branch.label, branch.id])),
427
- fork: target.id,
428
- }
429
- }
430
- if (gate === 'promote_skill') {
431
- const candidate = (projected.brain?.skills ?? []).find((item) => item.name === skill && item.status === 'candidate')
432
- if (candidate === undefined) return null
433
- return {
434
- kind: 'promote_skill',
435
- questions: [
436
- {
437
- id: 'promote-skill',
438
- header: '候选技能',
439
- question: `采纳候选技能「${candidate.name}」?`,
440
- detail: [`**它是什么**:${candidate.description ?? '(没有描述)'}`, `**落在哪**:${candidate.path ?? '—'}`, '', '采纳之后它会进入你的技能目录(内核改写 frontmatter),模型从此加载得到它。'].join('\n'),
441
- options: [
442
- { label: '采纳', description: '写进技能目录,模型从此加载得到它。' },
443
- { label: '先不采纳', description: '候选留在工作区里,你以后还可以采纳。' },
444
- ],
445
- },
446
- ],
447
- byLabel: new Map([['采纳', 'yes'], ['先不采纳', 'no']]),
448
- skill: candidate.name,
449
- }
450
- }
451
- return null
452
- }
453
-
454
- /**
455
- * 把原生卡上的答案翻译成**人门动作**;人没作答(撤下卡/跳过)⇒ `null`,什么都不落。
456
- * 判据用的是我们自己的标签(与内核的 `intent.approve` 同一条纪律:按标签判,不按按钮文字)。
457
- */
458
- function answerOf(asked, answer) {
459
- const item = (Array.isArray(answer?.answers) ? answer.answers : []).find((entry) => entry?.id === asked.questions[0].id)
460
- const selected = Array.isArray(item?.selected) ? item.selected : []
461
- const custom = typeof item?.custom === 'string' ? item.custom.trim() : ''
462
- if (selected.length === 0 && custom === '') return null
463
- if (asked.kind === 'adopt_branch') {
464
- // 人可以直接选一条,也可以在自定义框里打一条世界线 id 或名字。
465
- const branchId = selected.length === 1 ? (asked.byLabel.get(selected[0]) ?? null) : (asked.byLabel.get(custom) ?? (/^b-[a-z0-9]+$/.test(custom) ? custom : null))
466
- if (branchId === null) return null
467
- return {
468
- detail: { action: 'adopt_branch', plan: null, fork: asked.fork, branch: branchId, skill: null, value: null, note: null },
469
- human: `人在原生提问卡上裁决:采纳这条世界线(${branchId})。`,
470
- followUp: '跑 ConvergeFork 落实它——合并与清理是内核的活',
471
- }
472
- }
473
- if (asked.kind === 'promote_skill') {
474
- const choice = selected.length === 1 ? (asked.byLabel.get(selected[0]) ?? null) : (asked.byLabel.get(custom) ?? null)
475
- if (choice !== 'yes') return null // 「先不采纳」= 人不采纳:什么都不落(与直接路径一致:不采纳不是一条事实)
476
- return {
477
- detail: { action: 'promote_skill', plan: null, fork: null, branch: null, skill: asked.skill, value: null, note: null },
478
- human: `人在原生提问卡上采纳了候选技能「${asked.skill}」——它从此进你的技能目录。`,
479
- followUp: '候选状态正由内核改写(`status: active`),下一个回合它就在你的技能目录里',
480
- }
481
- }
482
- return null
483
- }
484
-
485
267
  ctx.inject(['connection'], (connectionCtx) => {
486
268
  /** 挂一条 exact fetch route:归属当前 fiber,connection 消失时自动收掉。 */
487
269
  const route = (path, methods, fetch) =>
@@ -490,298 +272,6 @@ export function apply(ctx) {
490
272
  `clearai: ${path}`,
491
273
  )
492
274
 
493
- route('/api/clearai/gate', ['POST'], async (httpRequest) => {
494
- let request = null
495
- try {
496
- request = await httpRequest.json()
497
- } catch {
498
- // 连 JSON 都不是:如实说,而不是当成「未知动词」。
499
- return reply(400, { ok: false, error: 'bad_json' })
500
- }
501
- const action = request === null || typeof request !== 'object' ? '' : String(request.action ?? '')
502
- /**
503
- * `ask` 是**界面手势**,不是事实动词:它把某道等人的人门摆到**原生提问卡**上,
504
- * 再由人在卡里选——选出来的答案仍走下面那条老路(人门消息 → 内核落事实)。
505
- * 所以白名单分两层:能**写事实**的只有 `HUMAN_GATE_ACTIONS`;能**发起提问**的另有一个。
506
- */
507
- const ASKABLE_GATES = ['fork_adopt', 'promote_skill']
508
- if (action !== 'ask' && !HUMAN_GATE_ACTIONS.includes(action)) return reply(400, { ok: false, error: 'unknown_gate_action' })
509
- const sessionId = request === null || typeof request !== 'object' ? '' : String(request.sessionId ?? '')
510
- const agents = ctx.get('agents')
511
- const agent = sessionId === '' ? undefined : agents?.get?.(sessionId)
512
- if (agent === undefined) return reply(404, { ok: false, error: 'no_live_session' })
513
- // 只挑需要的最小标量,不在 RPC 边界上搬活的会话对象。
514
- const detail = {
515
- action,
516
- plan: typeof request.plan === 'string' ? request.plan : null,
517
- fork: typeof request.fork === 'string' ? request.fork : null,
518
- branch: typeof request.branch === 'string' ? request.branch : null,
519
- skill: typeof request.skill === 'string' ? request.skill : null,
520
- value: typeof request.value === 'string' ? request.value : null,
521
- note: typeof request.note === 'string' ? request.note.slice(0, 200) : null,
522
- }
523
- /**
524
- * **本体四动词(人的通道)**:词条字段在 RPC 边界上只收表内的那几个、带长度上限——
525
- * 表外的字段一律剥掉(不是拒:人门消息进日志,日志里不该出现没约定的形状)。
526
- * 判据与模型工具**同一份**:校验用 `domain-language` 的纯函数,对当前词汇判,
527
- * 不过就 400 并把问题清单带回界面——「点了报成功、账上一字未改」不许再出现。
528
- */
529
- const ONTOLOGY_GATE_ACTIONS = ['register_term', 'register_predicate', 'revise_term', 'deprecate_entry']
530
- if (ONTOLOGY_GATE_ACTIONS.includes(action)) {
531
- const raw = request.entry ?? {}
532
- const str = (key, cap = 300) => (typeof raw[key] === 'string' ? raw[key].slice(0, cap) : undefined)
533
- detail.entry = {
534
- id: str('id', 40),
535
- label: str('label', 60),
536
- gloss: str('gloss'),
537
- basis: str('basis'),
538
- parent: str('parent', 40),
539
- domain: str('domain', 40),
540
- reason: str('reason', 200),
541
- unit: str('unit', 24),
542
- aliases: Array.isArray(raw.aliases) ? raw.aliases.filter((item) => typeof item === 'string').slice(0, 8).map((item) => item.slice(0, 60)) : undefined,
543
- functional: raw.functional === true ? true : undefined,
544
- range:
545
- raw.range !== null && typeof raw.range === 'object' && ['statement', 'quantity', 'formula', 'code', 'reference'].includes(String(raw.range.form))
546
- ? { form: String(raw.range.form), unit: typeof raw.range.unit === 'string' ? raw.range.unit.slice(0, 24) : undefined, term: typeof raw.range.term === 'string' ? raw.range.term.slice(0, 40) : undefined }
547
- : undefined,
548
- }
549
- const lexicon = stateOf(sessionId).lexicon
550
- let problems = []
551
- if (action === 'register_term') problems = validateTerm(lexicon, detail.entry)
552
- if (action === 'register_predicate') problems = validatePredicate(lexicon, detail.entry)
553
- if (action === 'revise_term' || action === 'deprecate_entry') {
554
- const id = detail.entry.id ?? ''
555
- const known = [...(lexicon.terms ?? []), ...(lexicon.predicates ?? [])].find((item) => item.id === id)
556
- if (known === undefined) problems = [`unknown_entry:词汇里没有这个条目:${id}`]
557
- else if (action === 'deprecate_entry' && known.status === 'deprecated') problems = [`already_deprecated:${id} 已经是废止状态`]
558
- else if ((detail.entry.reason ?? '') === '' || detail.entry.reason === undefined) problems = ['reason_required:这一步要写一句缘由']
559
- else if (action === 'revise_term' && detail.entry.label === undefined && detail.entry.gloss === undefined && detail.entry.aliases === undefined) problems = ['nothing_to_revise:label / gloss / aliases 至少给一个']
560
- }
561
- if (problems.length > 0) return reply(400, { ok: false, error: 'entry_rejected', problems })
562
- }
563
- /**
564
- * 技能名要先过**取值校验**,再进日志。
565
- *
566
- * 它必须是原生那条语法(kebab-case):这个名字会变成 `/<名字>` 手势(原生 pre-step
567
- * 认的就是 `[a-z0-9]+(-[a-z0-9]+)*`),放别的形状进去只会静默不生效——比拒绝更坏。
568
- * 表外的值一律拒,与动词白名单同一套纪律。
569
- */
570
- if (action === 'promote_skill' && (detail.skill === null || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(detail.skill))) {
571
- return reply(400, { ok: false, error: 'bad_skill_name' })
572
- }
573
- /**
574
- * 撤回 / 维持一条事实:先**核标的还在不在**。
575
- *
576
- * 与提问卡那条同一个理由:`buildGateQuestion` 认不出那道门时回 409,而不是摆一张过期的卡。
577
- * 这里如果不核,面板会收到一句成功、而账本上一字未改——「点了报成功、什么都没做」
578
- * 正是这套界面最不能有的那类东西。
579
- */
580
- if (action === 'confirm_provisional') {
581
- const projected = view(stateOf(sessionId))
582
- const fork = (projected.forks ?? []).find((item) => item.id === detail.fork)
583
- if (fork === undefined) return reply(409, { ok: false, error: 'fork_not_found' })
584
- if (fork.merge?.provisional !== true) return reply(409, { ok: false, error: 'not_provisional' })
585
- if (fork.merge?.confirmed !== null && fork.merge?.confirmed !== undefined) return reply(409, { ok: false, error: 'already_confirmed' })
586
- }
587
- if (action === 'retract_fact' || action === 'keep_fact') {
588
- const projected = view(stateOf(sessionId))
589
- const fact = (projected.facts ?? []).find((item) => item.id === detail.value)
590
- if (fact === undefined) return reply(409, { ok: false, error: 'fact_not_found' })
591
- if (fact.review !== null && fact.review !== undefined) return reply(409, { ok: false, error: 'fact_already_reviewed' })
592
- }
593
- /**
594
- * ── 混合路径:点我们那条 → 用**原生提问卡**问 ──────────────────────
595
- *
596
- * 为什么值这一条:我们自己的人门行(fork 裁决 / 采纳技能)现在只能在右栏面板里点按钮,
597
- * 而「人正在看对话」的时候,卡就在他眼前、还把输入框挡住——那是原生最擅长的形态。
598
- * 借的只是**问答界面**:答案回来之后,落账的那条事实与直接点按钮**一模一样**
599
- * (同一个人门消息、同一套校验、同一本账)。借界面,不借账。
600
- */
601
- if (action === 'ask') {
602
- const gate = String(request.gate ?? '')
603
- if (!ASKABLE_GATES.includes(gate)) return reply(400, { ok: false, error: 'bad_gate' })
604
- const questions = ctx.get('userQuestions')
605
- if (questions === undefined || typeof questions.ask !== 'function') {
606
- return reply(409, { ok: false, error: 'no_question_channel' })
607
- }
608
- const asked = buildGateQuestion(gate, { sessionId, fork: typeof request.fork === 'string' ? request.fork : null, skill: typeof request.skill === 'string' ? request.skill : null })
609
- if (asked === null) return reply(409, { ok: false, error: 'gate_not_open' })
610
- /**
611
- * **不占着 HTTP 请求等人**:点击只是「把卡摆上去」,人什么时候答都行。
612
- * 答案到了再落账(与直接点按钮同一条路),失败也只记一行日志——界面手势不该 500。
613
- */
614
- void questions
615
- .ask({ questions: asked.questions, agent })
616
- .then((answer) => {
617
- const chosen = answerOf(asked, answer)
618
- if (chosen === null) return null // 人撤下了卡:什么都不落(他不算答过)
619
- return appendHumanGate(agent, chosen)
620
- })
621
- .catch((error) => {
622
- ctx.logger?.warn?.(`clearai gate: 原生提问失败 ${String(error?.message ?? error).slice(0, 160)}`)
623
- })
624
- return reply(200, { ok: true, asked: true, gate })
625
- }
626
- const human =
627
- action === 'adopt_branch'
628
- ? `人在面板上裁决:采纳这条世界线(${detail.branch ?? '?'})。`
629
- : action === 'promote_skill'
630
- ? `人在面板上采纳了候选技能「${detail.skill ?? '?'}」——它从此进你的技能目录。`
631
- : action === 'confirm_provisional'
632
- ? `人复核了那次**临时采纳**(${detail.fork ?? '?'})并认可它:这个结论不再是「待复核」。`
633
- : action === 'retract_fact'
634
- ? `人审查了被推翻的那条事实(${detail.value ?? '?'})后决定**撤回**它${detail.note === null ? '' : `,缘由:${detail.note}`}。`
635
- : action === 'keep_fact'
636
- ? `人审查了被推翻的那条事实(${detail.value ?? '?'})后判定**证据不可靠,维持原事实**${detail.note === null ? '' : `,缘由:${detail.note}`}。`
637
- : ONTOLOGY_GATE_ACTIONS.includes(action)
638
- ? `人在本体格里${action === 'register_term' ? `登记了概念「${detail.entry?.label ?? detail.entry?.id ?? '?'}」` : action === 'register_predicate' ? `登记了谓词「${detail.entry?.label ?? detail.entry?.id ?? '?'}」` : action === 'revise_term' ? `修订了「${detail.entry?.id ?? '?'}」的展示信息` : `废止了「${detail.entry?.id ?? '?'}」`}${detail.entry?.basis ? `,依据:${detail.entry.basis}` : ''}${detail.entry?.reason ? `,缘由:${detail.entry.reason}` : ''}。`
639
- : '人在面板上做了一个动作。'
640
- const followUp =
641
- ONTOLOGY_GATE_ACTIONS.includes(action)
642
- ? '这条词汇变更已落账(`by:user`),与模型工具落的是同一本账、同一套判据;词汇货架会在下一拍同步'
643
- : action === 'confirm_provisional'
644
- ? '这条确认已经落账(`by:user`);那道门随之消失,续跑可以继续'
645
- : action === 'retract_fact' || action === 'keep_fact'
646
- ? '这个决定已经落账,并会写进 `clear/knowledge/facts/` 那一份(下一轮引用它之前先看那条记录)'
647
- : action === 'promote_skill'
648
- ? '候选状态正由内核改写(`status: active`),下一个回合它就在你的技能目录里'
649
- /* 说准(rather than 说满):这句话写下的那一刻只到 inbox、还没进投影,
650
- 所以不能断言「已经在投影里生效」——失效模式是模型看到它与卡片矛盾,停下来问人。 */
651
- : '跑 ConvergeFork 落实它——合并与清理是内核的活'
652
- try {
653
- appendHumanGate(agent, { detail, human, followUp })
654
- } catch (error) {
655
- return reply(500, { ok: false, error: String(error?.message ?? error).slice(0, 200) })
656
- }
657
- return reply(200, { ok: true, action, sessionId })
658
- })
659
-
660
- /**
661
- * ═══ 两条只读读面(面板在浏览器里读不了盘)═══
662
- *
663
- * `GET /api/clearai/deliverables?sessionId=` 交付物:按阶段(Plan)分组,每条给出
664
- * 声明(步骤 artifacts)、盘上在不在、多大、谁验的。**声明 vs 实际**是这个产品的核心对照。
665
- * `GET /api/clearai/brain?sessionId=` 工作区现状:技能目录(内核还没落事实时面板的兜底)。
666
- *
667
- * **没有「读文件正文」那条了**:预览交给 DSH 原生的文档预览
668
- * (`dsh-client-ui-sidebar-documentpreview` 的 6 个实现 + 它自己的分页读盘),
669
- * 我们只按原生地址把文件递过去。少一条读面 = 少一处要维护的路径守卫,
670
- * 而且原生那边本来就有 markdown / 图片 / pdf / html 的渲染。
671
- */
672
- route('/api/clearai/deliverables', ['GET'], (httpRequest) => {
673
- const url = new URL(httpRequest.url)
674
- const session = sessionOf(url.searchParams.get('sessionId') ?? '')
675
- const cwd = session?.header?.cwd
676
- if (typeof cwd !== 'string' || cwd === '') return reply(404, { ok: false, error: 'no_live_session' })
677
- // 只挑叶子字段构造属于我们自己的 JSON:不把投影对象整体搬出去。
678
- // 投影这一刻读不到 ⇒ 「声明」那一半给空;盘上「实际」那一半是读盘,不吃投影,照给。
679
- const projections = projectionsOf()
680
- const state = projections === undefined || typeof projections.stateOf !== 'function' ? null : projections.stateOf(session, 'clearai')
681
- const stages = (state?.plans ?? []).map((plan) => ({
682
- plan: plan.id,
683
- status: plan.status,
684
- brief: plan.brief ?? '',
685
- confirmedAt: plan.confirmed_at ?? null,
686
- openedAt: plan.at ?? null,
687
- closedAt: plan.closedAt ?? null,
688
- summary: plan.summary ?? null,
689
- steps: (plan.steps ?? []).map((step) => ({
690
- id: step.id,
691
- ordinal: step.ordinal,
692
- do: step.do,
693
- status: step.status,
694
- doneCriteria: step.done_criteria,
695
- level: step.tests?.level ?? null,
696
- advancedAt: step.advancedAt ?? null,
697
- voidReason: step.voidReason ?? null,
698
- artifacts: (step.artifacts ?? []).map((artifact) => {
699
- const absolute = resolveInside(cwd, artifact)
700
- let info = null
701
- try {
702
- info = absolute === null ? null : statSync(absolute)
703
- } catch {
704
- info = null
705
- }
706
- const isFile = info !== null && info.isFile()
707
- /**
708
- * `exists: false` 有两种成因,**面板必须分开说**(混着写会误导下一步动作):
709
- * 模型把 `lab/`(一个目录)声明成物证时,准入按「空目录」拒了它;
710
- * 而面板原来一律写「缺 · 盘上没有这个文件」——它明明在,只是不是文件。
711
- * 事实有两种,界面就别用一句话盖住。
712
- */
713
- return {
714
- path: artifact,
715
- area: pathAreaOf(artifact),
716
- exists: isFile,
717
- directory: info !== null && info.isDirectory(),
718
- bytes: isFile ? info.size : null,
719
- modifiedAt: isFile ? info.mtimeMs : null,
720
- }
721
- }),
722
- })),
723
- }))
724
- /**
725
- * 「实际」那一半:盘上真有、但没有任何计划声明过的 products/ 文件。
726
- * 与声明过的按**路径**去重 —— 同一条不许在栏里出现两遍。
727
- */
728
- const declared = new Set(stages.flatMap((stage) => stage.steps.flatMap((step) => step.artifacts.map((artifact) => artifact.path))))
729
- const outputs = listWorkspaceOutputs(cwd)
730
- .filter((item) => !declared.has(item.relative))
731
- .map((item) => ({ path: item.relative, area: pathAreaOf(item.relative), exists: true, bytes: item.bytes, modifiedAt: item.modifiedAt, declared: false }))
732
- return reply(200, { ok: true, stages, outputs })
733
- })
734
-
735
- /**
736
- * `GET /api/clearai/brain?sessionId=` —— 工作区**现状**的实时技能目录。
737
- *
738
- * 为什么需要它:面板的数据来自会话日志的投影,而内核只在
739
- * `agent/pre-step` 里落事实;于是**新建的会话在第一轮对话之前**,「技能 · 记忆」页签是空的
740
- * (提示还让人「先在 clear/skills/ 里放一个技能」——可那里明明躺着 18 个模板技能)。
741
- *
742
- * 这条读面直接问宿主的 `skills` 服务(它就是模型看到的那张合并目录),返回字段与内核
743
- * 随投影下发的 `catalog.entries` **逐字段一致**,所以面板能用同一套渲染。
744
- * 它是**读面不是事实**:面板会如实标注「本会话还没有第一轮对话,这份还没进投影」。
745
- */
746
- route('/api/clearai/brain', ['GET'], async (httpRequest) => {
747
- const url = new URL(httpRequest.url)
748
- const session = sessionOf(url.searchParams.get('sessionId') ?? '')
749
- const cwd = session?.header?.cwd
750
- if (typeof cwd !== 'string' || cwd === '') return reply(404, { ok: false, error: 'no_live_session' })
751
- const skills = ctx.get('skills')
752
- if (skills === undefined || typeof skills.snapshot !== 'function') return reply(200, { ok: false, error: 'no_skill_registry' })
753
- const agent = ctx.get('agents')?.get?.(session.id)
754
- let snapshot = null
755
- try {
756
- snapshot = await skills.snapshot({ cwd, ...(agent === undefined ? {} : { scope: agent }) })
757
- } catch {
758
- return reply(200, { ok: false, error: 'snapshot_failed' })
759
- }
760
- // 观测不完整就不发:一张缺几条的表比空表更坏(它会看起来像「技能被删了」)。
761
- if (snapshot === null || snapshot.complete !== true) return reply(200, { ok: false, error: 'incomplete' })
762
- const prefix = cwd.endsWith(sep) ? cwd : `${cwd}${sep}`
763
- const entries = (Array.isArray(snapshot.skills) ? snapshot.skills : [])
764
- .map((skill) => {
765
- const base = skill?.resourceBase
766
- const dir = base !== null && typeof base === 'object' && base.kind === 'directory' && typeof base.path === 'string' ? base.path : null
767
- return {
768
- name: String(skill?.name ?? ''),
769
- description: String(skill?.description ?? '').slice(0, 300),
770
- when_to_use: typeof skill?.whenToUse === 'string' ? skill.whenToUse.slice(0, 300) : null,
771
- source: String(skill?.source ?? 'unknown'),
772
- provider: String(skill?.provider ?? ''),
773
- model: skill?.invocation?.modelInvocable === true,
774
- user: skill?.invocation?.userInvocable === true,
775
- dir,
776
- // 与内核同一套约定:虚拟条目(记忆索引)没有正文文件。
777
- file: dir === null || String(skill?.source ?? '') === 'clearai-memory' ? null : join(dir, 'SKILL.md'),
778
- inside: dir === null ? null : dir === cwd || dir.startsWith(prefix),
779
- }
780
- })
781
- .filter((entry) => entry.name !== '')
782
- return reply(200, { ok: true, complete: true, entries })
783
- })
784
-
785
275
  /**
786
276
  * `GET /api/clearai/inspector?sessionId=…&kind=…&id=…`
787
277
  *
@@ -807,7 +297,7 @@ export function apply(ctx) {
807
297
  const projections = projectionsOf()
808
298
  const state = projections === undefined || typeof projections.stateOf !== 'function' ? null : projections.stateOf(session, 'clearai')
809
299
  if (state === null || state === undefined) return reply(200, { ok: true, found: false })
810
- const found = inspectGraphSelection(state, { kind: url.searchParams.get('kind') ?? '', id: url.searchParams.get('id') ?? '' }, derive(state))
300
+ const found = withLanguage(state.language, () => inspectGraphSelection(state, { kind: url.searchParams.get('kind') ?? '', id: url.searchParams.get('id') ?? '' }, derive(state)))
811
301
  /** 找不到不是错误:那个对象可能刚被废止或本来就不在(如实说 `found: false`,不编一份空的)。 */
812
302
  if (found === null) return reply(200, { ok: true, found: false })
813
303
  return reply(200, { ok: true, found: true, inspector: found })
@@ -820,38 +310,26 @@ export function apply(ctx) {
820
310
  ctx.provide('clearai', {
821
311
  /** 原始状态(工具做不变量判断用)。调用方不得修改。 */
822
312
  state: stateOf,
823
- /** 派生:阶段/完成度/假设状态/世界线阶段,全部现算。 */
313
+ /** 派生:阶段/完成度/假设状态,全部现算。 */
824
314
  derive: (sessionId) => derive(stateOf(sessionId)),
825
315
  /** 面板视图(与 wire 同一份,降级时一并交出席位健康事实)。 */
826
316
  view: viewOf,
827
- /**
828
- * 运行态卡:注给模型的**事实**。
829
- *
830
- * `overrides.autonomy`:面板上曾可以切档,而那一拍投影里还是旧档
831
- * (消息先入 inbox、后落日志,而 pre-step 跑在它落账之前)。切档入口已经摘除,
832
- * 但**这条覆盖通道保留**:一是旧会话日志里仍有那种记录,二是它保证了"卡片说的就是
833
- * 这一回合真正要跑的机制"这条不变量——两句话互相矛盾比一句话过期更糟
834
- * (旧投影与新消息同框时,模型会被矛盾卡住——所以宁缺毋假)。
835
- */
836
- renderCard: (sessionId, overrides = {}) => {
837
- const state = stateOf(sessionId)
838
- if (overrides.autonomy !== 'attended' && overrides.autonomy !== 'unattended') return renderCard(state)
839
- return renderCard({
840
- ...state,
841
- autonomy: {
842
- ...(state.autonomy ?? {}),
843
- effective: { value: overrides.autonomy, source: 'session', preset: state.autonomy?.effective?.preset ?? null },
844
- },
845
- })
846
- },
317
+ /** 一段人写的话是哪种语言(内核在人发消息的那一刻用它,规则只有 `lang.js` 这一份)。 */
318
+ detectLanguage,
319
+ /** 运行态卡:注给模型的**事实**。 */
320
+ renderCard: (sessionId) => renderCard(stateOf(sessionId)),
847
321
  /**
848
322
  * 预演:把本次调用要落的变更先折一遍,好让工具返回的卡片是**这一步之后**的样子。
849
323
  * 投影要到结果落账才前进,而工具在返回时就需要说清新状态——
850
324
  * 与其在预设侧复制一份 fold,不如让 fold 的拥有者替它算。
851
325
  */
852
326
  preview: (sessionId, mutations) => {
853
- const next = applyMutations(stateOf(sessionId), mutations)
854
- return { state: next, card: renderCard(next), view: view(next) }
327
+ /**
328
+ * 预演的是**这次调用返回之后**的样子:那时这次调用已经不在飞了。
329
+ * 不清掉 `inFlight`,交付自己的结果上就会写着「在等裁决」(等的正是它自己)。
330
+ */
331
+ const next = applyMutations({ ...stateOf(sessionId), inFlight: null }, mutations)
332
+ return { state: next, derived: derive(next), card: renderCard(next), view: view(next) }
855
333
  },
856
334
  /**
857
335
  * **领域语言层的判据**(值形状、引用存在、值域、同一事实自洽)与货架正文。
@@ -869,7 +347,14 @@ export function apply(ctx) {
869
347
  * 而「登记过哪些实例」住在 `state.entities` 里。只递词汇的话,那条判据永远无从判断,
870
348
  * 只能迁移期一律放行——那就等于没有这条判据。
871
349
  */
872
- validateAssertions: (sessionId, assertions, options = {}) => validateAssertions(stateOf(sessionId), assertions, options),
350
+ validateAssertions: (sessionId, assertions, options = {}) => validateAssertions(applyMutations(stateOf(sessionId), Array.isArray(options?.mutations) ? options.mutations : []), assertions, options),
351
+ /**
352
+ * **本体文件的第一道校验**(单个文件:格式、字段、id 等于文件名)。内核在模型写
353
+ * `clear/ontology/` 之前调它;折法读文件时用的是同一个函数,所以「写得进」与「读得懂」是一回事。
354
+ */
355
+ checkFile: (path, content) => checkOntologyFile(path, content),
356
+ /** 本体文件的字段定义(内核铺成 `clear/ontology/SCHEMA.json`)。 */
357
+ schema: () => ONTOLOGY_SCHEMA,
873
358
  /**
874
359
  * 货架正文。带 `mutations` 时按**这一步之后**的样子渲染——
875
360
  * 工具在返回前就把货架写好,读的人不必等下一回合。
@@ -879,13 +364,18 @@ export function apply(ctx) {
879
364
  const next = derive(state)
880
365
  // 在途命题也传进去:词汇刚立起来时「引用 0」会让人以为没人用,而断言已经在假设上了。
881
366
  // `view`:货架的「使用」一节与运行态卡 / 右栏读**同一份**叙述(单一叙述源)。
882
- return describeDomainShelf(state.lexicon, next.factRows, next.hypotheses, { view: knowledgeViewOf(state) })
367
+ // 递**整份状态**:实例一节与断言引用读 `state.entities` / `state.entityAssertions`,只递词汇这两节永远是 0。
368
+ return describeDomainShelf(state, next.factRows, next.hypotheses, { view: knowledgeViewOf(state) })
883
369
  },
370
+ /**
371
+ * 一组断言用到的词条**此刻的含义指纹**(写进事实文件):之后定义改了,事实就知道要复核。
372
+ * 带 `mutations` 时按这一步之后的词汇算(工作区刚同步进来的定义也算数)。
373
+ */
374
+ definitions: (sessionId, assertions, mutations = [], said = '') => fingerprintDefinitions(applyMutations(stateOf(sessionId), Array.isArray(mutations) ? mutations : []).lexicon, assertions, { text: said }),
884
375
  /** 一条断言的一行人话(货架 / 卡片 / 查询共用同一句话,免得三处各写一套)。 */
885
376
  format: (sessionId, assertion) => formatAssertion(stateOf(sessionId).lexicon, assertion),
886
377
  /**
887
- * **当前的图投影**(节点 / 边)。给内核用:谓词登记与 `Assert` 都要判
888
- * 「这个类型是已登记的概念吗」——判据只有一份,就在这张投影里。只读,不落盘。
378
+ * **当前的图投影**(节点 / 边)。只读,不落盘。
889
379
  */
890
380
  graph: (sessionId) => graphProjection(stateOf(sessionId)),
891
381
  },