clearai-dsh 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/fold.js CHANGED
@@ -25,12 +25,12 @@ export const MUTATION_KIND = 'clearai'
25
25
  * 状态版本:形状一变就 +1。
26
26
  * v2 → v3:多了「合并技能目录」与「本会话用了几次技能」两块(技能面)。
27
27
  * v3 → v4:多了「运行档」(人在场 / 无人值守)这块。
28
- * (§34 更正:它**不是**"可被人切换的事实"而是部署预设的初值——面板那个开关已删。)
29
- * v4 → v5:多了「项目章程」的读数(占位还有几条 / §1 当前阶段填没填 / 最后改动)。
28
+ * (更正:它**不是**"可被人切换的事实"而是部署预设的初值——面板那个开关已删。)
29
+ * v4 → v5:多了「项目章程」的读数(占位还有几条 / 章程的当前阶段填没填 / 最后改动)。
30
30
  * v5 → v6:多了「续跑窗口」的账(`continuation`):我们**对宿主说过的话**——布防、暂停、收兵、
31
31
  * 以及「它不在了,而那不是我们干的」。它以前记在内核的进程内存里,重启或分叉就说不清了。
32
32
  * v6 → v7:那条账多一个 `label`:我们最后一次在平台对象上写下的那句身份文本。
33
- * 它的用途只有一个:**分清「身份变了」与「人改写过了」**(§17.6)——前者我们换窗口,
33
+ * 它的用途只有一个:**分清「身份变了」与「人改写过了」**——前者我们换窗口,
34
34
  * 后者以人为准、一个字都不动。没有它,这两种情形在外部长得一模一样。
35
35
  * 投影缓存按版本判定,所以旧缓存会被丢弃、从日志重折一遍——用量是**从日志折出来的**,重折才完整。
36
36
  */
@@ -63,6 +63,13 @@ export function emptyState() {
63
63
  forks: [],
64
64
  /** 系统按触发派出去的侦察(子角色不是模型自由委派):结论进资料面,这里留一条记录 */
65
65
  scouts: [],
66
+ /**
67
+ * **运行时结算通知**(原生):子会话落定时,运行时把它的收尾消息作为一条 user message
68
+ * 投给父会话(`source.kind === 'subagent-settled'`)。它同时是两件事的证据:
69
+ * 模型**读到了**结论(它就是模型上下文里的文本),以及「这条子 run 结束了」这个事实。
70
+ * 折进来之后,账本侧的结算不必再去读子会话日志。
71
+ */
72
+ notices: [],
66
73
  /** 工作区里等人采纳的候选技能(内核扫描后落的事实)与已采纳的记录 */
67
74
  brainCandidates: [],
68
75
  skillPromotions: [],
@@ -84,27 +91,29 @@ export function emptyState() {
84
91
  blocks: {},
85
92
  releases: [],
86
93
  /**
87
- * 运行档(人在场 / 无人值守)。**它是人的事实,不是部署的常量**:
88
- * · `effective` = 内核随投影下发的那一份(只有内核知道组合配置),面板据此画开关;
89
- * · `override` = 人在面板上切过的那一份(人门 `set_autonomy`,署名是人)。
90
- * 两者分开摆,才说得清「这是部署给我的默认」还是「这是我此刻要的」。
94
+ * 运行档(人在场 / 无人值守)。**它是部署预设的初值,不是可切换的开关**:
95
+ * · 人门动词 `set_autonomy` 已摘除,面板上那个开关已经不存在;
96
+ * · `effective` = 内核随投影下发的那一份(只有内核知道组合配置),**只用于展示**;
97
+ * · `override` = 历史日志里可能留下的人门记录。读取侧保留它只是为了旧会话仍然读得通,
98
+ * 当前**没有任何写入者**——不要据此认为现在还能切换档位。
99
+ * · 它也不影响续跑:要不要继续由门状态算出来(见内核 turnDemand)。
91
100
  */
92
101
  autonomy: { override: null, effective: null },
93
102
  /**
94
103
  * 项目章程(`PROJECT.md`)的读数(内核扫描后随投影下发)。
95
104
  * 为什么要有它:章程的**读**侧早就接在原生指令文件上了,但**写**侧只有一句提示词——
96
- * 结果是跑完几轮、交付了产物,章程还是铺工作区那天的模板,谁也没发现(2026-09-11 实测)。
105
+ * 结果是跑完几轮、交付了产物,章程还是铺工作区那天的模板,谁也没发现——不提醒它就会一直漂着。
97
106
  * 事实摆在这里,人与模型都看得见「它还是不是空壳」。
98
107
  */
99
108
  constitution: null,
100
109
  /**
101
- * **本体形状**(§23):内核随投影下发的那份声明(对象/状态/边/等级)。
110
+ * **本体形状**:内核随投影下发的那份声明(对象/状态/边/等级)。
102
111
  * 为什么进投影而不是在界面里手抄:面板那一格的页眉要从**声明**生成——
103
112
  * 抄一份就会漂移,而漂移的界面比没有界面更坏。它与目录同一条纪律:变了才发。
104
113
  */
105
114
  ontology: null,
106
115
  /**
107
- * **续跑窗口的账**(§17):我们向平台说过的那句话——「这个窗口归我布防 / 我按了暂停 /
116
+ * **续跑窗口的账**:我们向平台说过的那句话——「这个窗口归我布防 / 我按了暂停 /
108
117
  * 我收兵了 / 它不在了而那不是我们干的」。形状见 `applyMutation` 的 `continuation/set`。
109
118
  *
110
119
  * 为什么它必须在投影里而不是内核内存里:宿主的 `paused` 相位分不清「人按的」与
@@ -258,12 +267,14 @@ export function applyMutation(state, mutation) {
258
267
  next.goal.status = mutation.status
259
268
  next.goal.closedAt = at
260
269
  next.goal.closeVerdict = mutation.verdict ?? null
270
+ // 结案时如实记下「没被任何证据触及的假设」:未判不是「没问题」,是「没看过」。
271
+ next.goal.unjudged = Array.isArray(mutation.unjudged) ? mutation.unjudged.slice() : []
261
272
  }
262
273
  break
263
274
  }
264
275
  case 'hypothesis/superseded': {
265
276
  /**
266
- * **终态黏住**(§22,与 ClearAI 本体 P3 同一个病、同一个修法):
277
+ * **终态黏住**(与本体里「推翻不可撤销」同一条原则):
267
278
  * 旧写法把任何还没被替代的行都改成 `superseded` —— 于是一条**已被推翻**的假设,
268
279
  * 会因为下一版假设清单里没再列它而变成「已被替代」。而「被推翻」与「被替代」是两件事:
269
280
  * 前者说这条猜想错了,后者说这条猜想不再被提。后者不该改写前者。
@@ -293,9 +304,11 @@ export function applyMutation(state, mutation) {
293
304
  blocked: undefined,
294
305
  /**
295
306
  * 授权记号(ClearAI `plan.confirmed_at` / `confirmed_by`)。
296
- * 两种取得方式,都可考:**显式动作**(人在面板上按确认 → `by:'user'`)与
297
- * **行为**(交付过一步 → `by:'progress'`);无人值守那一档在立约时由档自动确认
298
- * (`by:'autonomy'`——ClearAI 的原话是「选择 Goal 模式就是用户的无人值守授权」)。
307
+ * 两种取得方式,都可考:**显式动作**(人在原生审阅卡上批准 → `by:'user'`)与
308
+ * **行为**(交付过一步 → `by:'progress'`,见内核 AdvancePlan)。
309
+ * 第三种来源 `by:'autonomy'`(无人值守档在立约时自动确认)**已删除**:
310
+ * 那会让「计划经人确认」这条证据变成系统自己签的。读取侧不需要兼容它,
311
+ * 因为删除发生在写入侧,历史日志里最多出现 `user` 与 `progress`。
299
312
  * 注意这是一个**记号**,不是闸门:没确认的计划照样能被交付推起来,那一刻记号按事实补写。
300
313
  */
301
314
  confirmed_at: mutation.confirmed_at ?? null,
@@ -365,6 +378,7 @@ export function applyMutation(state, mutation) {
365
378
  digest: mutation.digest ?? null,
366
379
  bytes: mutation.bytes ?? null,
367
380
  note: mutation.note ?? null,
381
+ path: mutation.path ?? null,
368
382
  step: mutation.step ?? null,
369
383
  branch: mutation.branch ?? null,
370
384
  at,
@@ -410,10 +424,10 @@ export function applyMutation(state, mutation) {
410
424
  basis: mutation.basis ?? null,
411
425
  refs: mutation.refs ?? [],
412
426
  /**
413
- * **出处**(§27b):记账那一刻解析好的四类入口
427
+ * **出处**:记账那一刻解析好的四类入口
414
428
  * (`artifact` / `audit-card` / `evaluator-session` / `approval-record`)。
415
429
  * 界面只渲染它,不再自己猜「这一串是路径还是材料 id」——
416
- * 正是那种猜法让整排出处点不开(真数据实测)。
430
+ * 正是那种猜法让整排出处点不开(真数据踩过这个坑)。
417
431
  */
418
432
  origins: Array.isArray(mutation.origins) ? mutation.origins : [],
419
433
  anchor: mutation.anchor ?? 'artifact',
@@ -433,7 +447,7 @@ export function applyMutation(state, mutation) {
433
447
  }
434
448
  case 'human/released': {
435
449
  /**
436
- * 放行绑在**哪条轴**上要说清(2026-09-11,AUDIT §14-A):主线绑步骤、世界线绑分支。
450
+ * 放行绑在**哪条轴**上要说清:主线绑步骤、世界线绑分支。
437
451
  * `via` 记它凭什么算数——现在只有 `approval`(原生审批栈的权威记录),不推断。
438
452
  */
439
453
  next.releases.push({
@@ -449,7 +463,7 @@ export function applyMutation(state, mutation) {
449
463
  }
450
464
  case 'fact/promoted': {
451
465
  /**
452
- * 事实的字段(§23):`scope` 是它的**边界**(推翻条件)——没有边界的事实下一轮没人敢用;
466
+ * 事实的字段:`scope` 是它的**边界**(推翻条件)——没有边界的事实下一轮没人敢用;
453
467
  * `level` 是它被支持到哪一级(判者可查)。两者都来自升格那一刻的假设,不事后补。
454
468
  */
455
469
  next.facts.push({
@@ -543,6 +557,7 @@ export function applyMutation(state, mutation) {
543
557
  digest: mutation.digest ?? null,
544
558
  conclusion: null,
545
559
  note: null,
560
+ path: null,
546
561
  at,
547
562
  })
548
563
  break
@@ -552,6 +567,7 @@ export function applyMutation(state, mutation) {
552
567
  if (scout !== undefined) {
553
568
  scout.conclusion = mutation.conclusion ?? null
554
569
  scout.note = mutation.note ?? null
570
+ scout.path = mutation.path ?? null
555
571
  }
556
572
  break
557
573
  }
@@ -670,11 +686,11 @@ export function applyMutation(state, mutation) {
670
686
  }
671
687
  case 'continuation/set': {
672
688
  /**
673
- * 续跑窗口的账(§17)。四种状态就是「我们说过的话」的全部:
689
+ * 续跑窗口的账。四种状态就是「我们说过的话」的全部:
674
690
  * · `armed` —— 我们布了防(或按策略恢复了它):此刻它在为我们跑。
675
691
  * · `paused` —— **我们**按下的暂停,`why` 是真实理由(等裁决 / 等人门 / 阶段边界)。
676
692
  * · `stopped` —— 我们收的兵(目标达成 / 如实放弃 / 计划触礁)。
677
- * · `withdrawn` —— 它不在了,而**不是我们清的**(我们的清除永远与建同拍,见内核 §17.2):
693
+ * · `withdrawn` —— 它不在了,而**不是我们清的**(我们的清除永远与建同拍):
678
694
  * 这是平台上唯一说得出口的解释,所以它也只说这些,不说「谁」干的。
679
695
  * 一条记录:**窗口在会话里是单数**,所以后一条覆盖前一条,不留历史(历史在会话日志里)。
680
696
  */
@@ -684,7 +700,7 @@ export function applyMutation(state, mutation) {
684
700
  target: mutation.target ?? null,
685
701
  why: mutation.why ?? null,
686
702
  /**
687
- * 平台对象上那句身份文本的**最后一次观测值**(§17.6):我们写下它时记我们的,
703
+ * 平台对象上那句身份文本的**最后一次观测值**:我们写下它时记我们的,
688
704
  * 别人改写之后重记他的。判据是「平台上的文本 === 这条记录」⇒ 还是我们的。
689
705
  */
690
706
  label: mutation.label ?? null,
@@ -721,12 +737,26 @@ export function applyEvent(state, event) {
721
737
  // 内核观察到的事实(候选技能、采纳记录、合并目录)走**插件消息的结构化 section**——
722
738
  // 记在会话日志里,所以状态仍然可以从日志重放出来,而不是靠一句散文。
723
739
  const source = event.data?.source
740
+ /**
741
+ * 原生结算通知:先折它。结论 = 通知里「Its closing message:」之后的那些文本块
742
+ * (前面是运行时写的一行摘要,不是子会话说的话)。
743
+ */
744
+ if (source !== null && typeof source === 'object' && source.kind === 'subagent-settled') {
745
+ const blocks = (event.data?.content ?? []).filter((block) => block?.type === 'text').map((block) => String(block.text ?? ''))
746
+ const label = blocks.findIndex((text) => /closing message/i.test(text))
747
+ const conclusion = (label === -1 ? blocks.slice(1) : blocks.slice(label + 1)).join('\n').trim()
748
+ const child = source.senderSessionId === null || source.senderSessionId === undefined ? null : String(source.senderSessionId)
749
+ // `next` 必须在这一支里自己 clone:这道门里它还没被声明(旁边那段注释警告过同一个坑)。
750
+ const next = clone(state)
751
+ next.notices = [...(next.notices ?? []), { child, summary: String(source.summary ?? blocks[0] ?? ''), conclusion, at: typeof event.time === 'number' ? event.time : Date.now() }]
752
+ return next
753
+ }
724
754
  if (source !== null && typeof source === 'object' && source.kind === 'plugin' && Array.isArray(source.sections)) {
725
755
  /**
726
- * 一条插件消息里可能**同时**带好几件事实(内核一次 pre-step 把目录、当档、候选一起发)。
727
- * 所以这里是「逐件折」而不是「找到一件就 return」——2026-09-11 实测踩过:
728
- * 原来找到目录就 return,于是同一条消息里的**当档被吃掉**,
729
- * 表现是面板上的档位开关永远停在「还没定档」,而机制那边早就按新档跑了。
756
+ * 一条插件消息里可能**同时**带好几件事实(内核一次 pre-step 把目录、运行档、候选一起发)。
757
+ * 所以这里是「逐件折」而不是「找到一件就 return」——早退会漏掉后面的事件:
758
+ * 原来找到目录就 return,于是同一条消息里的**运行档被吃掉**,
759
+ * 表现是投影里的 effective 档永远停在「还没定档」,而机制那边早就按新档跑了。
730
760
  */
731
761
  const brain = source.sections.find((section) => section?.name === 'clearai/brain')
732
762
  const tier = source.sections.find((section) => section?.name === 'clearai/autonomy')
@@ -738,7 +768,7 @@ export function applyEvent(state, event) {
738
768
  * 注意它必须**在同一道门里**折:一条插件消息可能同时带目录、当档与事实变更,
739
769
  * 而 `next` 是这道门里才 clone 出来的。第一版把这段写在门**外**并引用了 `next`,
740
770
  * 抛出的 ReferenceError 被自己的 try/catch 吞掉——于是它一直是空操作
741
- * (2026-09-11 长测:世界线结论回灌「落账了」但投影里看不见,查了一轮才找到)。
771
+ * (失效模式:世界线结论回灌落账了,投影里却看不见)。
742
772
  */
743
773
  const ontologySection = source.sections.find((section) => section?.name === 'clearai/ontology')
744
774
  if (ontologySection !== undefined && typeof ontologySection.text === 'string') {
@@ -812,7 +842,9 @@ export function applyEvent(state, event) {
812
842
  const next = clone(state)
813
843
  for (const name of quoted) recordSkillUse(next, name, 'human', at, stepPointer(state))
814
844
  if (gate === null) return next
815
- // 人切运行档:记成一条**人的事实**,当值立刻生效(机制在内核那一侧,见 effectiveAutonomy)。
845
+ // **旧日志容忍分支**(写入者已摘除,只有旧日志可能带着):人切运行档曾经是一条人门动作,旧会话里可能留着。
846
+ // 读到它就照旧记成一条**人的事实**,让历史会话仍然读得通;当前面板上已经没有这个开关,
847
+ // 新的会话不会再产生这一条。不要据此认为"现在还能切档"——要删这个分支得先确认没有旧日志。
816
848
  if (gate.action === 'set_autonomy') {
817
849
  const value = String(gate.value ?? '')
818
850
  if (!AUTONOMY_VALUES.includes(value)) return next
@@ -898,7 +930,7 @@ export function derive(state) {
898
930
  .reduce((best, item) => Math.max(best, levelIndex(item.level)), -1)
899
931
  let status = hypothesis.status
900
932
  if (status === 'proposed' && rows.length > 0) status = 'alive'
901
- // 被推翻是黏性终态(§22):「已被替代」不该改写「已被推翻」——两件事。
933
+ // 被推翻是黏性终态:「已被替代」不该改写「已被推翻」——两件事。
902
934
  if (status !== 'superseded' && status !== 'refuted' && refutations > 0) status = 'refuted'
903
935
  return { ...hypothesis, status, supportedLevel: supportedLevel < 0 ? null : `L${supportedLevel}`, refutations, inconclusive }
904
936
  })
@@ -925,7 +957,7 @@ export function derive(state) {
925
957
  /**
926
958
  * 完成度(派生,不存)。
927
959
  *
928
- * 2026-09-11 长测抓到的自相矛盾:目标已经 achieved、两条事实都升格了,卡片却写「完成度 0%」。
960
+ * 一处自相矛盾的由来:目标已经 achieved、两条事实都升格了,卡片却写「完成度 0%」。
929
961
  * 两个原因:① 计划一关,活跃计划为 null,口径回落到假设;② 而**升格**(`fact/promoted`)
930
962
  * 是一条独立路径,它不改假设状态,于是分子是 0。
931
963
  *
@@ -949,7 +981,7 @@ export function derive(state) {
949
981
  }
950
982
 
951
983
  /**
952
- * 世界线的**派生状态**(2026-09-11 定的语义,见 recon 的「四条结束方式」):
984
+ * 世界线的**派生状态**(语义:见 recon 的「四条结束方式」):
953
985
  *
954
986
  * · `failed` —— 执行者没跑成(`worldline/executed{ok:false}`)。**它不是落选**:
955
987
  * 落选意味着它跑完了、被尺子排到了后面;失败是「世界没给它机会」。原来的实现在这两件事上
@@ -981,7 +1013,7 @@ export function derive(state) {
981
1013
  failed: branch.status === 'exploring' && branch.execution !== null && branch.execution !== undefined && branch.execution.ok === false,
982
1014
  orphaned: ownerVoided && !fork.settled && !fork.abandoned && branch.status === 'exploring',
983
1015
  /**
984
- * **执行者未归**(2026-09-11 长测抓到的第二句假话,与「失败」「孤儿」同一族):
1016
+ * **执行者未归**(与「失败」「孤儿」同一族的假话——不说破就会被当成还在跑):
985
1017
  *
986
1018
  * 执行者是异步派的,所以「它在跑」是一条会过期的状态。分叉一旦收口(收敛或放弃),
987
1019
  * 那条世界线就**没有归宿**了——回灌也进不了任何决定。可 `execution.ok` 仍是 `null`,
@@ -1000,7 +1032,7 @@ export function derive(state) {
1000
1032
  * 不可能残留成僵尸。
1001
1033
  * 条目只带**分诊信息**(标题/摘要/指向),不带全部正文:收件箱是分诊,不是问诊。
1002
1034
  *
1003
- * 2026-09-11 起这里**只放真门**:不拍板就真的推不动的那种。计划确认不再是条目(它是记号,
1035
+ * 这里**只放真门**:不拍板就真的推不动的那种。计划确认不再是条目(它是记号,
1004
1036
  * 不是闸门;见 HUMAN_GATE_ACTIONS 的收敛记录)——收件箱里的每一条都经得起「等人是必须的吗」。
1005
1037
  */
1006
1038
  const inbox = []
@@ -1030,7 +1062,7 @@ export function derive(state) {
1030
1062
  // 复核不是一次点击:要么认可(什么都不用做),要么让人/模型改判据重做一条。
1031
1063
  human_action: null,
1032
1064
  /**
1033
- * §37:它是**要一句话**的门 ✗ —— 原先"认可就什么都不用做"的写法有个洞:
1065
+ * 它是**要一句话**的门 ✗ —— "认可就什么都不用做"的写法有个洞:
1034
1066
  * 门开着 ⇒ 续跑停着(`hasOpenGate`)⇒ 什么都不做的话,系统就一直等 ✗。
1035
1067
  * 所以人得**说一声**;界面上必须把这件事说出来(以前既没按钮也没提示 ✗)。
1036
1068
  */
@@ -1095,23 +1127,27 @@ export function derive(state) {
1095
1127
  * · **agent 不可达**:这些动词**没有工具 schema**——模型能调的工具面里不存在它们,
1096
1128
  * 它只能看见「人做了什么」这条事实。
1097
1129
  *
1098
- * 2026-09-11 收敛(奥卡姆,用户拍板):动词表从 5 个缩到 4 个,砍的两个都是**重复**:
1130
+ * 动词表只有 4 个(奥卡姆:砍掉的两个都是**重复**):
1099
1131
  * · `confirm_plan` —— 原生 `dsh-plan-mode` 就是「用户复核的出口」;而我们自己的
1100
1132
  * `planIsAuthorized` 本来就承认「交付第一步即授权」。计划确认从来不是闸门(它是记号),
1101
1133
  * 少一个假装成闸门的按钮,界面就不再暗示一条不存在的约束。需要人拍板时用原生
1102
1134
  * `ask_user_question`(它同样把人的答复留在日志里,署名一样是人)。
1103
1135
  * · `invoke_skill` —— 原生 `/` 技能触发器(`dsh-client-ui-skill` 注册 trigger `/`,
1104
1136
  * `dsh-client-ui-input-trigger` 出候选菜单)做的正是同一件事,而且带候选菜单。
1105
- * 留下的四个各有原生没有的职责:
1137
+ * 留下的三个各有原生没有的职责:
1106
1138
  * · `adopt_branch`/`abandon_fork` —— `ConvergeFork` 只认「算术」或「人门」两条路,
1107
1139
  * 算不出来时这是唯一的结构化人裁决通道(不做 NLU 是纪律,不是懒);
1108
- * · `promote_skill` —— 候选技能扶正是**只有人能触发**的写动作(模型不能自举);
1109
- * · `set_autonomy` —— 「我现在在不在场」只有人知道,档是人写的事实,不是系统猜的。
1140
+ * · `promote_skill` —— 候选技能扶正是**只有人能触发**的写动作(模型不能自举)。
1141
+ *
1142
+ * 已摘除:`set_autonomy`。「在场与否」是**运行时状态**(有没有门开着、有没有裁决在飞),
1143
+ * 不是人在面板上按的一个开关;那个档位还顺手把「计划经人确认」变成系统自己签的。
1144
+ * 折法里仍留一条**只读**容忍分支(见 `applyEvent` 的注释):旧会话日志里可能有一条这样的记录,
1145
+ * 而历史必须继续读得通——但**当前没有任何写入者**,面板上也没有这个开关。
1110
1146
  */
1111
1147
  export const HUMAN_GATE_MARK = '[clearai·人门]'
1112
1148
  /** 面板上允许出现的动词。表外的动词一律拒(与贡献表同一套「表外的名字不许出现」)。 */
1113
1149
  export const HUMAN_GATE_ACTIONS = ['adopt_branch', 'abandon_fork', 'promote_skill']
1114
- /** 运行档的两个取值(`set_autonomy` 的合法参数)。 */
1150
+ /** 运行档的两个取值。**只用于读取旧日志**里的 `set_autonomy` 记录;当前没有写入口。 */
1115
1151
  export const AUTONOMY_VALUES = ['attended', 'unattended']
1116
1152
 
1117
1153
  /** 从一条用户消息里认出人门标记;不是标记就返回 null。 */
@@ -1174,7 +1210,7 @@ function skillUsageView(state) {
1174
1210
  .sort((left, right) => String(left.name).localeCompare(String(right.name)))
1175
1211
  }
1176
1212
 
1177
- /** 面板契约(DESIGN.md §3.2)。浏览器读的就是这个,一字不改。 */
1213
+ /** 面板契约。浏览器读的就是这个,一字不改。 */
1178
1214
  export function view(state, sessionId) {
1179
1215
  const derived = derive(state)
1180
1216
  const plan = derived.activePlan ?? derived.closedPlans[derived.closedPlans.length - 1] ?? null
@@ -1187,8 +1223,10 @@ export function view(state, sessionId) {
1187
1223
  inbox: derived.inbox,
1188
1224
  hasOpenGate: derived.hasOpenGate,
1189
1225
  /**
1190
- * 运行档:`value` = 当档(内核下发的那份)、`source` 说明它从哪来(部署预设 / 人在面板上切的)。
1191
- * 面板画开关与卡片写事实都读这里 —— 投影是唯一真相。
1226
+ * 运行档:`value` = 当档(内核下发的那份)、`source` 说明它从哪来(部署预设 / 旧日志里的人门记录)。
1227
+ * 面板与卡片都读这里 —— 投影是唯一真相。
1228
+ * 注意**它不是可切换的开关**:`set_autonomy` 已摘除,现在只有部署预设会下发新值;
1229
+ * `source === 'session'` 只可能来自旧日志。
1192
1230
  */
1193
1231
  /** 项目章程的读数(面板「技能 · 记忆」页签顶部那一行;点开走原生预览)。 */
1194
1232
  constitution: state.constitution ?? null,
@@ -1213,7 +1251,7 @@ export function view(state, sessionId) {
1213
1251
  /** 本体形状(面板页眉据此生成,不手抄)。 */
1214
1252
  ontology: state.ontology ?? null,
1215
1253
  /**
1216
- * 续跑窗口的账(§17):面板读它,于是「续跑停着——等裁决」这种**平台说不出来的话**
1254
+ * 续跑窗口的账:面板读它,于是「续跑停着——等裁决」这种**平台说不出来的话**
1217
1255
  * 有地方说。轮数与相位仍然只在原生 dock 上出现(一个事实只在**一块**面上说),
1218
1256
  * 这里交出去的只有:状态、它服务的事实对象、以及停下来的真实理由。
1219
1257
  */
@@ -1243,6 +1281,9 @@ export function view(state, sessionId) {
1243
1281
  phase: derived.phase,
1244
1282
  progress: derived.progress,
1245
1283
  revision: state.goal.revision,
1284
+ // 结案留痕:没被任何证据触及的假设。未判不是「没问题」,是「没看过」。
1285
+ unjudged: Array.isArray(state.goal.unjudged) ? state.goal.unjudged.slice() : [],
1286
+ closeVerdict: state.goal.closeVerdict ?? null,
1246
1287
  hypotheses: derived.hypotheses.map((hypothesis) => ({
1247
1288
  id: hypothesis.id,
1248
1289
  claim: hypothesis.claim,
@@ -1255,7 +1296,7 @@ export function view(state, sessionId) {
1255
1296
  })),
1256
1297
  },
1257
1298
  /**
1258
- * **全部计划**(§32):一个对话会有多个世界树。活动的那份照旧在 `plan` 里,
1299
+ * **全部计划**:一个对话会有多个世界树。活动的那份照旧在 `plan` 里,
1259
1300
  * 已收尾的留在这里 —— 它们没有被删(文档也归档在 `clear/goals/plans/<id>.md`),
1260
1301
  * 只是原先**没有入口**切回去看 ✗。
1261
1302
  */
@@ -1284,7 +1325,7 @@ export function view(state, sessionId) {
1284
1325
  : {
1285
1326
  id: plan.id,
1286
1327
  status: plan.status,
1287
- /** 计划自己的一句话(模型建计划时写的)。**给人看的名字**用它,内部 id 退回 tooltip(§18)。 */
1328
+ /** 计划自己的一句话(模型建计划时写的)。**给人看的名字**用它,内部 id 退回 tooltip。 */
1288
1329
  brief: plan.brief ?? '',
1289
1330
  blocked: plan.blocked ?? null,
1290
1331
  // 授权记号(ClearAI `plan.confirmed_at` / `confirmed_by`):面板据此显示计划门。
@@ -1312,10 +1353,10 @@ export function view(state, sessionId) {
1312
1353
  totalCount: plan.steps.filter((step) => step.status !== 'void').length,
1313
1354
  },
1314
1355
  /**
1315
- * **跨计划的步骤索引**(§25):步骤 id → 它的判据/归属。
1356
+ * **跨计划的步骤索引**:步骤 id → 它的判据/归属。
1316
1357
  *
1317
1358
  * 为什么必须有它(真数据踩出来的):计划会改版、收尾、重开——**命题的验证步常常留在旧计划里**
1318
- * (实测:一场里两条计划,命题的 `tests` 全在已收尾的那条上,而 `view.plan` 只交当前活动的那条)
1359
+ * (成因:一场里多条计划,命题的 `tests` 可能在已收尾的那条上,而 `view.plan` 只交当前活动的那条)
1319
1360
  * ⇒ 只查活动计划时,证据→步骤→命题这条链**全断**,面板上五个命题的证据与判者全显示「—」。
1320
1361
  * 索引是纯派生,不新增存储。
1321
1362
  */
@@ -1332,14 +1373,14 @@ export function view(state, sessionId) {
1332
1373
  stepId: item.step,
1333
1374
  planId: item.plan,
1334
1375
  /**
1335
- * `branch` 与 `anchor` **必须交出去**(§24 修):面板要给每条证据指一个**出处**——
1376
+ * `branch` 与 `anchor` **必须交出去**:面板要给每条证据指一个**出处**——
1336
1377
  * 独立证据指评估卡(文件)、自判证据指它锚定的产物。少了这两个字段,
1337
- * 「评估卡」那一项永远出不来(实测:世界线证据的评估卡按 `分支` 归属,
1378
+ * 「评估卡」那一项永远出不来(成因:世界线证据的评估卡按 `分支` 归属,
1338
1379
  * 与证据的 `stepId` 不是同一个 id,只看 stepId 会对不上)。
1339
1380
  */
1340
1381
  branch: item.branch ?? null,
1341
1382
  anchor: item.anchor ?? 'artifact',
1342
- /** 记账时定下的出处(§27b)。旧日志没有这个字段 ⇒ 客户端走只读回退。 */
1383
+ /** 记账时定下的出处。旧日志没有这个字段 ⇒ 客户端走只读回退。 */
1343
1384
  origins: item.origins ?? [],
1344
1385
  basisReviewable: item.basis_reviewable !== false,
1345
1386
  verdict: item.verdict,
@@ -1360,7 +1401,18 @@ export function view(state, sessionId) {
1360
1401
  capability: item.capability ?? null,
1361
1402
  at: item.at,
1362
1403
  })),
1363
- materials: state.materials.map((item) => ({ id: item.id, ref: item.ref, source: item.source, digest: item.digest, note: item.note ?? null, at: item.at })),
1404
+ materials: state.materials.map((item) => ({
1405
+ id: item.id,
1406
+ ref: item.ref,
1407
+ source: item.source,
1408
+ digest: item.digest,
1409
+ note: item.note ?? null,
1410
+ // 落盘路径:评估者与人靠它读全文(模型侧由卡片的资料面给出同一个指针)。
1411
+ path: item.path ?? null,
1412
+ bytes: item.bytes ?? null,
1413
+ step: item.step ?? null,
1414
+ at: item.at,
1415
+ })),
1364
1416
  facts: state.facts.map((item) => ({
1365
1417
  id: item.id,
1366
1418
  text: item.text,
@@ -1374,10 +1426,11 @@ export function view(state, sessionId) {
1374
1426
  /**
1375
1427
  * 侦察记录:**结局要能看出来**。
1376
1428
  *
1377
- * 2026-09-11 用户实测:中断一次跑动之后「状态上全部显示执行完成」,而 DSH 其实报了失败。
1429
+ * 失效模式:中断一次跑动之后「状态上全部显示执行完成」,而宿主其实报了失败。
1378
1430
  * 两处都修了:内核改成按 `stopReason` 落账(aborted/error 不算完成),这里把 `note` 交出去
1379
1431
  * 并派生成三态——跑着 / 正常回灌 / 没正常结束。面板与资料面据此分诊,而不是一律当"完成"。
1380
1432
  */
1433
+ notices: (state.notices ?? []).map((item) => ({ child: item.child, summary: item.summary, conclusion: item.conclusion, at: item.at })),
1381
1434
  scouts: (state.scouts ?? []).map((item) => ({
1382
1435
  id: item.id,
1383
1436
  stepId: item.step,
@@ -1388,6 +1441,7 @@ export function view(state, sessionId) {
1388
1441
  conclusion: item.conclusion,
1389
1442
  at: item.at,
1390
1443
  note: item.note ?? null,
1444
+ path: item.path ?? null,
1391
1445
  status: item.conclusion === null || item.conclusion === undefined ? 'running' : item.note === null || item.note === undefined ? 'settled' : 'failed',
1392
1446
  })),
1393
1447
  settlement: derived.settlement,
@@ -1468,7 +1522,7 @@ export function renderCard(state) {
1468
1522
  if (autonomy !== null) {
1469
1523
  lines.push(
1470
1524
  /**
1471
- * §34 之后档位**不再是面板上的开关**:它只是部署预设写的初值。
1525
+ * 档位**不再是面板上的开关**:它只是部署预设写的初值。
1472
1526
  * 所以卡片如实说"这件事是怎么配的",不再承诺"你可以在面板上切"(那是已删的能力 ✗)。
1473
1527
  */
1474
1528
  `- 运行档:${autonomy.value === 'unattended' ? '无人值守' : '人在场'}(部署预设写的;它不是"要不要人参与"的开关——要不要人由**门**决定:计划待确认/等裁决/有人在等)`,
@@ -1485,10 +1539,14 @@ export function renderCard(state) {
1485
1539
  if (derived.hypotheses.length > 0) {
1486
1540
  lines.push('- 假设状态(由证据算出):')
1487
1541
  for (const hypothesis of derived.hypotheses) {
1488
- lines.push(
1489
- ` · ${hypothesis.id} [${hypothesis.status}] ${hypothesis.claim} — 推翻条件:${hypothesis.refute_when}` +
1490
- `(支持到 ${hypothesis.supportedLevel ?? '—'} · 推翻 ${hypothesis.refutations} · 无法判定 ${hypothesis.inconclusive})`,
1491
- )
1542
+ /**
1543
+ * 三样全零 = **没人碰过它**,与「判过但无法判定」是两回事:
1544
+ * 前者要如实说「未触及」,后者本来就有「无法判定 n」这个读数。
1545
+ * 不逼 verdict,但也不许把「没看过」写成「没问题」。
1546
+ */
1547
+ const untouched = (hypothesis.supportedLevel === null || hypothesis.supportedLevel === undefined) && (hypothesis.refutations ?? 0) === 0 && (hypothesis.inconclusive ?? 0) === 0
1548
+ const readings = untouched ? '(未触及)' : `(支持到 ${hypothesis.supportedLevel ?? '—'} · 推翻 ${hypothesis.refutations} · 无法判定 ${hypothesis.inconclusive})`
1549
+ lines.push(` · ${hypothesis.id} [${hypothesis.status}] ${hypothesis.claim} — 推翻条件:${hypothesis.refute_when}${readings}`)
1492
1550
  }
1493
1551
  }
1494
1552
  if (plan === null) {
@@ -1496,21 +1554,21 @@ export function renderCard(state) {
1496
1554
  } else {
1497
1555
  if (derived.planConfirmationPending) {
1498
1556
  /**
1499
- * 2026-09-11 起这里只说**事实**,不再带「先引导确认」那句劝告。
1557
+ * 授权记号的语义:它是**归属**,不是闸门。
1500
1558
  *
1501
- * 为什么:确认计划那道门砍了(见 HUMAN_GATE_ACTIONS 的收敛记录)——它本来就不是闸门,
1502
- * 而那句劝告会让人(和模型)以为有一条必须走的路。现在这句只交代机理:
1503
- * 记号没落账,但**授权不会因此缺席**——交付第一步就按事实补写。
1559
+ * 记号没落账时,自动续跑会 hold(turnDemand),但显式推进不被阻止——
1560
+ * 第一次交付会在同一条变更里按事实补写 by='progress'(行为即授权)。
1561
+ * 卡片只交代机理,不劝人走哪条路:劝告会让人(和模型)以为有一道必须走的门。
1562
+ * 卡片不出现字段名(plan_confirmation_pending / confirmed_at 是账本词汇,
1563
+ * 不是给人与模型读的语言)——事实用一句人话说。
1504
1564
  */
1505
- lines.push('- plan_confirmation_pending: true')
1506
- lines.push('- confirmed_at:(null — 授权记号未落账;第一次交付会按事实补写(行为即授权)。不需要任何人先按什么)')
1565
+ lines.push('- 授权:记号未落账。未经人批准的计划不会自动续跑;显式推进时,第一次交付会按事实补写归属(行为即授权)')
1507
1566
  } else if (plan.confirmed_at === null) {
1508
1567
  // 记号没落账,但计划事实上已经推进过:授权已经发生。照旧写「等人确认」会与上一行
1509
1568
  // 当场矛盾,而同一段里两句打架的话比一句错话更糟(ClearAI 的原话)。
1510
- lines.push('- plan_confirmation_pending: false')
1511
- lines.push('- confirmed_at:(null,但本计划已推进过——授权已经发生,继续执行)')
1569
+ lines.push('- 授权:记号未落账,但本计划已推进过——授权已经发生(行为即授权),继续执行')
1512
1570
  } else {
1513
- lines.push(`- confirmed_at:${plan.confirmed_at}(${plan.confirmed_by === 'user' ? '人显式确认' : plan.confirmed_by === 'progress' ? '据推进事实补写' : String(plan.confirmed_by)})`)
1571
+ lines.push(`- 授权:已于 ${plan.confirmed_at} 落账(${plan.confirmed_by === 'user' ? '人显式批准' : plan.confirmed_by === 'progress' ? '据推进事实补写归属' : String(plan.confirmed_by)})`)
1514
1572
  }
1515
1573
  lines.push(`- 当前计划(${plan.id}${plan.goal === null ? '' : ` · 目标 ${plan.goal} 的一个阶段`})步骤:`)
1516
1574
  for (const step of plan.steps) {
@@ -1530,7 +1588,7 @@ export function renderCard(state) {
1530
1588
  for (const branch of fork.branches) {
1531
1589
  const reading = branch.reading === null || branch.reading === undefined ? '未报读数' : `读数 ${branch.reading}${branch.validity === 'usable' ? '' : '(不可用)'}`
1532
1590
  /**
1533
- * 执行者是**异步**派的(2026-09-11:ForkPlan 不再空等),所以「它跑完了没有」是一条
1591
+ * 执行者是**异步**派的(ForkPlan 不空等结论),所以「它跑完了没有」是一条
1534
1592
  * 独立的事实——不写出来,模型会以为分支在闲着,或者以为结论已经到手。
1535
1593
  */
1536
1594
  const executing = branch.execution ?? null
@@ -1571,6 +1629,33 @@ export function renderCard(state) {
1571
1629
  lines.push(`- 最近一条证据:${last.id} ${last.verdict}(${last.evaluator} · ${last.level})`)
1572
1630
  }
1573
1631
  if (state.facts.length > 0) lines.push(`- 已升格事实:${state.facts.length} 条`)
1632
+ // 结案时留的痕:未判的假设不是「没问题」,是「没看过」——结案之后也要看得见。
1633
+ if (Array.isArray(goal?.unjudged) && goal.unjudged.length > 0) lines.push(`- 结案留痕:有 ${goal.unjudged.length} 条假设没有被任何证据触及(${goal.unjudged.join(', ')})`)
1634
+ /**
1635
+ * **资料面**:外脑送回来的观测(不是你写的那些)**与还在跑的侦察**。
1636
+ *
1637
+ * 为什么必须有它:模型每一步唯一读到的窗口就是这张卡。从前结论只进 `view()`
1638
+ * (面板读得到、模型读不到),于是判据写成「与侦察结论一致」时,模型与独立评估者
1639
+ * 都无处可读,只能裁 inconclusive。这里给**指针 + 摘要**:全文落在
1640
+ * `clear/knowledge/materials/<id>.md`,要细节就 `read` 它——卡片不背长文。
1641
+ */
1642
+ {
1643
+ const foreign = state.materials.filter((material) => material.source !== 'self')
1644
+ const flying = state.scouts.filter((scout) => scout.conclusion === null || scout.conclusion === undefined)
1645
+ if (foreign.length > 0 || flying.length > 0) {
1646
+ lines.push('- 资料面(外脑送来的观测 + 还在跑的侦察;全文在工作区文件里,要细节就 read):')
1647
+ for (const material of foreign.slice(-3)) {
1648
+ const note = String(material.note ?? '')
1649
+ const excerpt = note.replace(/\s+/g, ' ').slice(0, 120)
1650
+ const where = material.path === null || material.path === undefined ? `账本 ${material.ref}` : material.path
1651
+ lines.push(` · [${material.source}] ${where}${material.bytes === null || material.bytes === undefined ? '' : `(${material.bytes} 字)`}:${excerpt}${note.length > 120 ? '…' : ''}`)
1652
+ }
1653
+ if (foreign.length > 3) lines.push(` · (还有 ${foreign.length - 3} 条更早的,全在 clear/knowledge/materials/ 下)`)
1654
+ for (const scout of flying) {
1655
+ lines.push(` · [在跑] 侦察 ${scout.id}${scout.trigger === null || scout.trigger === undefined ? '' : `(${scout.trigger})`}:结论回来时会作为观测送到你面前`)
1656
+ }
1657
+ }
1658
+ }
1574
1659
  lines.push('- 提醒:进度、阶段、假设状态都是系统算出来的;你不能声明它们,只能通过交付与裁决推进。')
1575
1660
  return lines.join('\n')
1576
1661
  }