clearai-dsh 0.1.7 → 0.2.1

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/host.js CHANGED
@@ -18,7 +18,8 @@
18
18
  import { readdirSync, statSync } from 'node:fs'
19
19
  import { join, relative, resolve, sep } from 'node:path'
20
20
  import { z } from 'zod'
21
- import { HUMAN_GATE_ACTIONS, HUMAN_GATE_MARK, MUTATION_KIND, STATE_VERSION, applyEvent, applyMutations, derive, emptyState, renderCard, view } from './fold.js'
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, validateAssertions, validatePredicate, validateTerm } from './domain-language.js'
22
23
  import { install as installInvariants } from './invariant.js'
23
24
 
24
25
  export const name = 'clearai-host'
@@ -394,6 +395,46 @@ export function apply(ctx) {
394
395
  value: typeof request.value === 'string' ? request.value : null,
395
396
  note: typeof request.note === 'string' ? request.note.slice(0, 200) : null,
396
397
  }
398
+ /**
399
+ * **本体四动词(人的通道)**:词条字段在 RPC 边界上只收表内的那几个、带长度上限——
400
+ * 表外的字段一律剥掉(不是拒:人门消息进日志,日志里不该出现没约定的形状)。
401
+ * 判据与模型工具**同一份**:校验用 `domain-language` 的纯函数,对当前词汇判,
402
+ * 不过就 400 并把问题清单带回界面——「点了报成功、账上一字未改」不许再出现。
403
+ */
404
+ const ONTOLOGY_GATE_ACTIONS = ['register_term', 'register_predicate', 'revise_term', 'deprecate_entry']
405
+ if (ONTOLOGY_GATE_ACTIONS.includes(action)) {
406
+ const raw = request.entry ?? {}
407
+ const str = (key, cap = 300) => (typeof raw[key] === 'string' ? raw[key].slice(0, cap) : undefined)
408
+ detail.entry = {
409
+ id: str('id', 40),
410
+ label: str('label', 60),
411
+ gloss: str('gloss'),
412
+ basis: str('basis'),
413
+ parent: str('parent', 40),
414
+ domain: str('domain', 40),
415
+ reason: str('reason', 200),
416
+ unit: str('unit', 24),
417
+ aliases: Array.isArray(raw.aliases) ? raw.aliases.filter((item) => typeof item === 'string').slice(0, 8).map((item) => item.slice(0, 60)) : undefined,
418
+ functional: raw.functional === true ? true : undefined,
419
+ range:
420
+ raw.range !== null && typeof raw.range === 'object' && ['statement', 'quantity', 'formula', 'code', 'reference'].includes(String(raw.range.form))
421
+ ? { 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 }
422
+ : undefined,
423
+ }
424
+ const lexicon = stateOf(sessionId).lexicon
425
+ let problems = []
426
+ if (action === 'register_term') problems = validateTerm(lexicon, detail.entry)
427
+ if (action === 'register_predicate') problems = validatePredicate(lexicon, detail.entry)
428
+ if (action === 'revise_term' || action === 'deprecate_entry') {
429
+ const id = detail.entry.id ?? ''
430
+ const known = [...(lexicon.terms ?? []), ...(lexicon.predicates ?? [])].find((item) => item.id === id)
431
+ if (known === undefined) problems = [`unknown_entry:词汇里没有这个条目:${id}`]
432
+ else if (action === 'deprecate_entry' && known.status === 'deprecated') problems = [`already_deprecated:${id} 已经是废止状态`]
433
+ else if ((detail.entry.reason ?? '') === '' || detail.entry.reason === undefined) problems = ['reason_required:这一步要写一句缘由']
434
+ else if (action === 'revise_term' && detail.entry.label === undefined && detail.entry.gloss === undefined && detail.entry.aliases === undefined) problems = ['nothing_to_revise:label / gloss / aliases 至少给一个']
435
+ }
436
+ if (problems.length > 0) return reply(400, { ok: false, error: 'entry_rejected', problems })
437
+ }
397
438
  /**
398
439
  * 技能名要先过**取值校验**,再进日志。
399
440
  *
@@ -468,9 +509,13 @@ export function apply(ctx) {
468
509
  ? `人审查了被推翻的那条事实(${detail.value ?? '?'})后决定**撤回**它${detail.note === null ? '' : `,缘由:${detail.note}`}。`
469
510
  : action === 'keep_fact'
470
511
  ? `人审查了被推翻的那条事实(${detail.value ?? '?'})后判定**证据不可靠,维持原事实**${detail.note === null ? '' : `,缘由:${detail.note}`}。`
512
+ : ONTOLOGY_GATE_ACTIONS.includes(action)
513
+ ? `人在本体格里${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}` : ''}。`
471
514
  : '人在面板上做了一个动作。'
472
515
  const followUp =
473
- action === 'confirm_provisional'
516
+ ONTOLOGY_GATE_ACTIONS.includes(action)
517
+ ? '这条词汇变更已落账(`by:user`),与模型工具落的是同一本账、同一套判据;词汇货架会在下一拍同步'
518
+ : action === 'confirm_provisional'
474
519
  ? '这条确认已经落账(`by:user`);那道门随之消失,续跑可以继续'
475
520
  : action === 'retract_fact' || action === 'keep_fact'
476
521
  ? '这个决定已经落账,并会写进 `clear/knowledge/facts/` 那一份(下一轮引用它之前先看那条记录)'
@@ -610,6 +655,36 @@ export function apply(ctx) {
610
655
  return reply(200, { ok: true, complete: true, entries })
611
656
  })
612
657
 
658
+ /**
659
+ * `GET /api/clearai/inspector?sessionId=…&kind=…&id=…`
660
+ *
661
+ * **知识 Inspector**:图上点了一个节点或边,把它的定义 / 关系 / 断言 / 证据链 / 历史取回来。
662
+ *
663
+ * 为什么走路由而不是塞进 `view()`:选择是**动态**的——把每个节点每条边的完整链都预先
664
+ * 推进投影,等于对一张 61 节点 / 147 边的图各算一遍,而人一次只看一个。
665
+ * 组装仍然只有一处实现(`inspectGraphSelection`,纯函数在宿主半),所以
666
+ * 「客户端自己拼证据链」这条口子没有开。
667
+ *
668
+ * 只读:**不产生任何变更**,也拿不到写入口。
669
+ */
670
+ route('/api/clearai/inspector', ['GET'], async (httpRequest) => {
671
+ const url = new URL(httpRequest.url)
672
+ const sessionId = url.searchParams.get('sessionId') ?? ''
673
+ const session = ctx.sessions.get(sessionId)
674
+ if (session === undefined) return reply(404, { ok: false, error: 'no_live_session' })
675
+ /**
676
+ * 直接用本模块的折法读状态,不走 `ctx.get('clearai')`:那条门面是同一条 fiber 上
677
+ * 提供给**预设侧**用的,而这条路由只是把同一个纯函数接到 HTTP 上——
678
+ * 中间多一跳服务解析,只会多一种「服务没接上」的失败模式。
679
+ */
680
+ const state = ctx.sessionProjections.stateOf(session, 'clearai')
681
+ if (state === null || state === undefined) return reply(200, { ok: true, found: false })
682
+ const found = inspectGraphSelection(state, { kind: url.searchParams.get('kind') ?? '', id: url.searchParams.get('id') ?? '' }, derive(state))
683
+ /** 找不到不是错误:那个对象可能刚被废止或本来就不在(如实说 `found: false`,不编一份空的)。 */
684
+ if (found === null) return reply(200, { ok: true, found: false })
685
+ return reply(200, { ok: true, found: true, inspector: found })
686
+ })
687
+
613
688
  })
614
689
 
615
690
  ctx.effect(
@@ -650,6 +725,41 @@ export function apply(ctx) {
650
725
  const next = applyMutations(stateOf(sessionId), mutations)
651
726
  return { state: next, card: renderCard(next), view: view(next) }
652
727
  },
728
+ /**
729
+ * **领域语言层的判据**(值形状、引用存在、值域、同一事实自洽)与货架正文。
730
+ *
731
+ * 为什么由宿主半提供,而不是预设侧自己写一份:判据**只能有一份**。
732
+ * 预设侧的工具与这条路由要判的是同一件事,而两份实现必然漂成
733
+ * 「登记时放行、升格时拒绝」——那种不一致在界面上与「这条还没验」长得一模一样。
734
+ * 所以判据住在纯函数模块里,预设侧经这道门调用它。
735
+ */
736
+ domain: {
737
+ validateTerm: (sessionId, draft) => validateTerm(stateOf(sessionId).lexicon, draft),
738
+ validatePredicate: (sessionId, draft) => validatePredicate(stateOf(sessionId).lexicon, draft),
739
+ validateAssertions: (sessionId, assertions) => validateAssertions(stateOf(sessionId).lexicon, assertions),
740
+ /**
741
+ * 货架正文。带 `mutations` 时按**这一步之后**的样子渲染——
742
+ * 工具在返回前就把货架写好,读的人不必等下一回合。
743
+ */
744
+ renderShelf: (sessionId, mutations = []) => {
745
+ const state = applyMutations(stateOf(sessionId), Array.isArray(mutations) ? mutations : [])
746
+ const next = derive(state)
747
+ // 在途命题也传进去:词汇刚立起来时「引用 0」会让人以为没人用,而断言已经在假设上了。
748
+ return describeDomainShelf(state.lexicon, next.factRows, next.hypotheses)
749
+ },
750
+ /** 一条断言的一行人话(货架 / 卡片 / 查询共用同一句话,免得三处各写一套)。 */
751
+ format: (sessionId, assertion) => formatAssertion(stateOf(sessionId).lexicon, assertion),
752
+ },
753
+ /**
754
+ * **知识 Inspector**:一个选择 → 它的定义 / 关系 / 断言 / 证据链 / 历史。
755
+ *
756
+ * 组装住在纯函数里(`inspectGraphSelection`),这里只把当前状态喂给它——
757
+ * 客户端因此永远拿不到「自己拼链」的机会,凡是读到链的地方都同源。
758
+ */
759
+ inspector: (sessionId, selection) => {
760
+ const state = stateOf(sessionId)
761
+ return inspectGraphSelection(state, selection, derive(state))
762
+ },
653
763
  }),
654
764
  'clearai: read facade',
655
765
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clearai-dsh",
3
- "version": "0.1.7",
3
+ "version": "0.2.1",
4
4
  "description": "ClearAI: The Epistemic Loop, native to DSH.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -35,6 +35,10 @@
35
35
  "node": ">=22"
36
36
  },
37
37
  "dependencies": {
38
+ "@xyflow/react": "^12.11.6",
39
+ "docx": "^9.7.1",
40
+ "graphology": "^0.26.0",
41
+ "graphology-layout-forceatlas2": "^0.10.1",
38
42
  "zod": "^4.6.1"
39
43
  },
40
44
  "dsh": {
@@ -59,7 +63,7 @@
59
63
  "worldline"
60
64
  ],
61
65
  "scripts": {
62
- "test": "bash test/run.sh",
66
+ "test": "node tools/build-vendor.mjs && bash test/run.sh",
63
67
  "build": "node tools/build-package.mjs",
64
68
  "verify": "node tools/verify-package.mjs",
65
69
  "verify:deploy": "node tools/verify-deploy.mjs",
@@ -67,6 +71,13 @@
67
71
  "pack": "node tools/build-package.mjs && npm pack ./dist/clearai-dsh",
68
72
  "release": "node tools/build-package.mjs && node tools/verify-package.mjs && node tools/verify-clean-install.mjs && npm pack ./dist/clearai-dsh --pack-destination dist",
69
73
  "e2e": "node tools/e2e-run.mjs",
70
- "e2e:long": "node tools/e2e-parallel.mjs"
74
+ "e2e:long": "node tools/e2e-parallel.mjs",
75
+ "vendor": "node tools/build-vendor.mjs",
76
+ "check:browser": "node tools/browser-graph-check.mjs",
77
+ "shots": "node tools/graph-shots.mjs"
78
+ },
79
+ "devDependencies": {
80
+ "esbuild": "^0.28.2",
81
+ "playwright-core": "^1.63.0"
71
82
  }
72
83
  }
@@ -111,7 +111,7 @@
111
111
 
112
112
  # ── 认识论内核(本预设的灵魂) ───────────────────────────────────────────────
113
113
 
114
- # 22 件意图工具(目标 2 + 计划 8 + 世界线 6 + 侦察 2 + 外脑 2 + 账本 2,见 `MECHANISM_TOOLS`)
114
+ # 29 件意图工具(目标 2 + 计划 8 + 世界线 6 + 侦察 2 + 外脑 2 + 账本 2 + 领域语言 7,见 `MECHANISM_TOOLS`)
115
115
  # + 一个 guard + 每回合派生的运行态卡 + 只增不删的台账 + 面板数据路由。
116
116
  # 逐条对照 docs/loop-philosophy.md 的五条哲学,见 ./plugins/clearai-kernel.js 的文件头。
117
117
  #
@@ -150,6 +150,13 @@
150
150
  # 只有一个猜想时,「验证」容易退化成找证据支持自己。修订目标不受此限。
151
151
  # 内核缺省是 0(=机制中立);**这里写 2 是产品立场**,所以它是硬门,不是文案。
152
152
  minHypotheses: 2
153
+ # 知识门:将要升格的命题必须已有断言的形态,否则结案被拒(在派评估者**之前**拦)。
154
+ # 为什么要有它:断言一直是「加法,不是门槛」,于是模型的最优策略就是
155
+ # 「检索 → 总结 → 写报告」——本体图、实体图、认识论三张图都长不出来,
156
+ # 因为**完成函数里没有它们**。让缺口进卡只解决「看得见」,这一道解决「绕不过」。
157
+ # 它与 minHypotheses 是两条不同的立场(开工要有候选对比 / 结论要有形态),所以是两个键。
158
+ # 内核缺省 false(= 断言始终是加法);这里写 true 是产品立场。
159
+ requireTypedPromotion: true
153
160
  # deny_rules 十条里可移植的九条(路径越狱那条不搬:宿主沙箱已经拥有它,ClearAI 自己也说「不重复」)
154
161
  bashDenyRules: true
155
162
  # 独立评估者:spawn = fresh context(fork 会继承历史,做的人与判的人就分不开了)
@@ -188,8 +195,8 @@
188
195
  # 当场抛错——未知机制 / 未知工具 / 未知段 / 已关机制却仍列着它的工具,四种错法都在装配期炸,
189
196
  # 而不是静默少装一件工具、等某一轮才发现。
190
197
  #
191
- # 这里只写**机制开关**;tools / sections 缺省 = 目录全量(22 件意图工具、23 段提示词定义、
192
- # 同一时刻 22 段在场——澄清协议那一段由 autonomy 在两套措辞里收敛)。
198
+ # 这里只写**机制开关**;tools / sections 缺省 = 目录全量(29 件意图工具、24 段提示词定义、
199
+ # 同一时刻 23 段在场——澄清协议那一段由 autonomy 在两套措辞里收敛)。
193
200
  # 这些数字不靠人眼维持:`node tools/verify-truth-table.mjs` 会拿代码算出来的数核对它们。
194
201
  # 要裁剪就把 tools 或 sections 显式写出来:
195
202
  # tools: [SetGoal, CreatePlan, AdvancePlan, ...] # 名字必须都在工具目录里