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/CHANGELOG.md +59 -0
- package/README.md +89 -74
- package/README.zh-CN.md +86 -71
- package/lib/client.js +944 -69
- package/lib/domain-language.js +758 -0
- package/lib/fold.js +894 -12
- package/lib/host.js +112 -2
- package/package.json +14 -3
- package/presets/clearai/agent.cordis.yml +10 -3
- package/presets/clearai/plugins/clearai-kernel.js +550 -12
- package/presets/clearai/plugins/prompts.js +47 -0
- package/presets/clearai/preset.yml +3 -3
- package/presets/clearai/skills/clearai-loop/SKILL.md +16 -13
package/lib/fold.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* 与预设内核的契约:`meta = { kind: 'clearai', v: 1, mutation: { t, ... } }`。
|
|
17
17
|
* 词汇表由本文件的 `applyMutation` 定义;预设侧只负责产出,不负责解释。
|
|
18
18
|
*/
|
|
19
|
+
import { applyLexiconMutation, deriveConflicts, emptyLexicon, formatAssertion, graphProjection, lexiconHealth, normalizeLexicon, objectKey, VALUE_FORMS } from './domain-language.js'
|
|
19
20
|
|
|
20
21
|
/** 世界线分支状态的秩。秩只增;一旦出现 adopted,整个分叉的分支状态冻结。 */
|
|
21
22
|
const BRANCH_RANK = { exploring: 0, evaluated: 1, adopted: 2, pruned: 2 }
|
|
@@ -35,9 +36,12 @@ export const MUTATION_KIND = 'clearai'
|
|
|
35
36
|
* v6 → v7:那条账多一个 `label`:我们最后一次在平台对象上写下的那句身份文本。
|
|
36
37
|
* 它的用途只有一个:**分清「身份变了」与「人改写过了」**——前者我们换窗口,
|
|
37
38
|
* 后者以人为准、一个字都不动。没有它,这两种情形在外部长得一模一样。
|
|
39
|
+
* v9 → v10:多了「领域词汇」(`lexicon`)与类型化事实:断言进 `fact.promoted`,
|
|
40
|
+
* 事实与假设按 id 关联(旧账本仍按主张文本)。旧日志折出来的 `lexicon` 是空表,
|
|
41
|
+
* 没有断言的事实照旧可读——形状变了才 +1,不是语义变了才 +1。
|
|
38
42
|
* 投影缓存按版本判定,所以旧缓存会被丢弃、从日志重折一遍——用量是**从日志折出来的**,重折才完整。
|
|
39
43
|
*/
|
|
40
|
-
export const STATE_VERSION =
|
|
44
|
+
export const STATE_VERSION = 10
|
|
41
45
|
|
|
42
46
|
/**
|
|
43
47
|
* **只留台账、不折进视图**的变更类型(词汇表的另一半)。
|
|
@@ -108,6 +112,15 @@ export function emptyState() {
|
|
|
108
112
|
* 抄一份就会漂移,而漂移的界面比没有界面更坏。它与目录同一条纪律:变了才发。
|
|
109
113
|
*/
|
|
110
114
|
ontology: null,
|
|
115
|
+
/**
|
|
116
|
+
* **领域词汇**(概念与谓词)与它的读面。
|
|
117
|
+
*
|
|
118
|
+
* 为什么它与上面的 `ontology` 是两个字段:`ontology` 是**过程本体**的形状
|
|
119
|
+
* (插件自己的后台流转结构,随版本走、不可编辑);`lexicon` 是**领域本体**的内容
|
|
120
|
+
* (项目自己的语言:概念、谓词、值形态),由账本里的本体事件折出来、可增删改。
|
|
121
|
+
* 名字分开,是因为权威不同——一个随发布变,一个随项目长。
|
|
122
|
+
*/
|
|
123
|
+
lexicon: emptyLexicon(),
|
|
111
124
|
/**
|
|
112
125
|
* **续跑窗口的账**:我们向平台说过的那句话——「这个窗口归我布防 / 我按了暂停 /
|
|
113
126
|
* 我收兵了 / 它不在了而那不是我们干的」。形状见 `applyMutation` 的 `continuation/set`。
|
|
@@ -196,6 +209,18 @@ function clone(value) {
|
|
|
196
209
|
return value === undefined ? undefined : JSON.parse(JSON.stringify(value))
|
|
197
210
|
}
|
|
198
211
|
|
|
212
|
+
/**
|
|
213
|
+
* 假设 ↔ 事实的关联:优先按 `hypothesis` id(新账本),旧账本按主张文本——
|
|
214
|
+
* 那时的事实还没有那个字段。顺序不能反:id 是身份,文本只是当时的措辞,
|
|
215
|
+
* 改一次措辞就断链的关联迟早会把「这条假设已被升格」读成「还没升格」。
|
|
216
|
+
*/
|
|
217
|
+
function factFromHypothesis(facts, goalId, hypothesisId, claim) {
|
|
218
|
+
return (Array.isArray(facts) ? facts : []).some((fact) => {
|
|
219
|
+
if (typeof fact?.hypothesis === 'string' && fact.hypothesis !== '') return fact.hypothesis === hypothesisId
|
|
220
|
+
return fact?.goal === goalId && String(fact.text ?? '') === String(claim ?? '')
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
199
224
|
function appendSteps(plan, rawSteps) {
|
|
200
225
|
for (const raw of rawSteps) {
|
|
201
226
|
plan.steps.push({
|
|
@@ -260,13 +285,38 @@ export function applyMutation(state, mutation) {
|
|
|
260
285
|
}
|
|
261
286
|
}
|
|
262
287
|
for (const hypothesis of mutation.hypotheses ?? []) {
|
|
288
|
+
/**
|
|
289
|
+
* **一个 id 只对应一条主张,永远**(与领域词汇那条「语义变化必须换 id」同一条纪律)。
|
|
290
|
+
*
|
|
291
|
+
* 真跑里这条缺失的代价很大:修订目标时内核给**同一句话**发了新 id,而这里照单全收,
|
|
292
|
+
* 于是一张运行态卡上出现 4 条主张的 6~8 行读数——同一句话挂着两个 id、各报一个状态
|
|
293
|
+
* (一个「已支持」、另一个「未触及」),模型得自己去调和两份打架的读数。
|
|
294
|
+
*
|
|
295
|
+
* 所以这里按 id **upsert**,但**只更新不改变身份的那些字段**(断言、推翻条件、版本):
|
|
296
|
+
* 修订时给一条老命题补上断言是正当的更新;而换掉它的 `claim` 就是拿旧 id 说新话——
|
|
297
|
+
* 那一律拒(内核已在源头保证换主张必换 id),终态(`superseded` / `refuted`)同样黏住。
|
|
298
|
+
*/
|
|
299
|
+
const claim = String(hypothesis.claim ?? '')
|
|
300
|
+
const known = next.hypotheses.find((item) => item.id === hypothesis.id)
|
|
301
|
+
if (known !== undefined) {
|
|
302
|
+
if (String(known.claim ?? '') !== claim) continue
|
|
303
|
+
known.refute_when = hypothesis.refute_when
|
|
304
|
+
known.version = hypothesis.version ?? known.version
|
|
305
|
+
if (Array.isArray(hypothesis.assertions)) known.assertions = clone(hypothesis.assertions)
|
|
306
|
+
continue
|
|
307
|
+
}
|
|
263
308
|
next.hypotheses.push({
|
|
264
309
|
id: hypothesis.id,
|
|
265
310
|
goal: mutation.id,
|
|
266
|
-
claim
|
|
311
|
+
claim,
|
|
267
312
|
refute_when: hypothesis.refute_when,
|
|
268
313
|
status: 'proposed',
|
|
269
314
|
version: hypothesis.version ?? 1,
|
|
315
|
+
/**
|
|
316
|
+
* **类型化断言随假设走**:登记时校验、升格时定型。
|
|
317
|
+
* 不写断言照旧成立(宽松+校验);写了就是「这条主张用这门语言怎么说」。
|
|
318
|
+
*/
|
|
319
|
+
assertions: Array.isArray(hypothesis.assertions) ? clone(hypothesis.assertions) : null,
|
|
270
320
|
at,
|
|
271
321
|
})
|
|
272
322
|
}
|
|
@@ -290,7 +340,7 @@ export function applyMutation(state, mutation) {
|
|
|
290
340
|
* 前者说这条猜想错了,后者说这条猜想不再被提。后者不该改写前者。
|
|
291
341
|
* 已经升格成事实的(`claim` 出现在 facts 里)同理:事实在,假设就不能被悄悄换掉。
|
|
292
342
|
*/
|
|
293
|
-
const promoted = next.facts
|
|
343
|
+
const promoted = factFromHypothesis(next.facts, mutation.goal, mutation.id, mutation.claim)
|
|
294
344
|
const row = next.hypotheses.find((item) => item.id === mutation.id && item.status !== 'superseded' && item.status !== 'refuted' && !promoted)
|
|
295
345
|
if (row !== undefined) {
|
|
296
346
|
row.status = 'superseded'
|
|
@@ -475,15 +525,19 @@ export function applyMutation(state, mutation) {
|
|
|
475
525
|
/**
|
|
476
526
|
* 事实的字段:`scope` 是它的**边界**(推翻条件)——没有边界的事实下一轮没人敢用;
|
|
477
527
|
* `level` 是它被支持到哪一级(判者可查)。两者都来自升格那一刻的假设,不事后补。
|
|
528
|
+
* `hypothesis` 是产出它的那条假设(新账本才有),`assertions` 是类型化断言——
|
|
529
|
+
* 没提供就是 null:**不提供放行,提供即严校**(校验发生在落账之前)。
|
|
478
530
|
*/
|
|
479
531
|
next.facts.push({
|
|
480
532
|
id: mutation.id,
|
|
481
533
|
goal: mutation.goal,
|
|
534
|
+
hypothesis: mutation.hypothesis ?? null,
|
|
482
535
|
text: mutation.text,
|
|
483
536
|
scope: mutation.scope ?? null,
|
|
484
537
|
level: mutation.level ?? null,
|
|
485
538
|
evidence: mutation.evidence ?? [],
|
|
486
539
|
path: mutation.path ?? null,
|
|
540
|
+
assertions: Array.isArray(mutation.assertions) ? clone(mutation.assertions) : null,
|
|
487
541
|
at,
|
|
488
542
|
})
|
|
489
543
|
break
|
|
@@ -748,6 +802,22 @@ export function applyMutation(state, mutation) {
|
|
|
748
802
|
}
|
|
749
803
|
break
|
|
750
804
|
}
|
|
805
|
+
/**
|
|
806
|
+
* **领域词汇的六个事件**:接纳 / 版本化修订 / 黏性废止(概念与谓词各三条)。
|
|
807
|
+
*
|
|
808
|
+
* 折法在这里只做解释:把事件折成 `lexicon`。校验(引用是否存在、形状对不对、
|
|
809
|
+
* 语义变化有没有偷偷走修订)全部发生在**落账之前**——预设侧的工具与宿主路由
|
|
810
|
+
* 用的是 `domain-language.js` 的同一份判据;折法不重复判一遍,否则两份判据必然漂。
|
|
811
|
+
*/
|
|
812
|
+
case 'ontology/term_added':
|
|
813
|
+
case 'ontology/predicate_added':
|
|
814
|
+
case 'ontology/term_revised':
|
|
815
|
+
case 'ontology/predicate_revised':
|
|
816
|
+
case 'ontology/term_deprecated':
|
|
817
|
+
case 'ontology/predicate_deprecated': {
|
|
818
|
+
next.lexicon = applyLexiconMutation(next.lexicon ?? emptyLexicon(), mutation, at)
|
|
819
|
+
break
|
|
820
|
+
}
|
|
751
821
|
default: {
|
|
752
822
|
return state // 不认识:不感兴趣,原样返回
|
|
753
823
|
}
|
|
@@ -763,6 +833,22 @@ export function applyMutations(state, mutations) {
|
|
|
763
833
|
return next
|
|
764
834
|
}
|
|
765
835
|
|
|
836
|
+
/**
|
|
837
|
+
* **给一批变更盖上事件时间**。
|
|
838
|
+
*
|
|
839
|
+
* 产出方(内核)只写「发生了什么」,不读时钟;时间属于**日志里的那一刻**(`event.time`)。
|
|
840
|
+
* 变更自己带了 `at` 就以它为准(有些路径确实知道更准的时刻),否则用事件时间。
|
|
841
|
+
* 事件也没时间(旧日志/合成事件)时**不动它**——那里本来就是 0,不假装知道。
|
|
842
|
+
*/
|
|
843
|
+
function stampAt(mutations, time) {
|
|
844
|
+
if (time === null || !Array.isArray(mutations) || mutations.length === 0) return mutations
|
|
845
|
+
return mutations.map((mutation) => {
|
|
846
|
+
if (mutation === null || typeof mutation !== 'object') return mutation
|
|
847
|
+
if (typeof mutation.at === 'number') return mutation
|
|
848
|
+
return { ...mutation, at: time }
|
|
849
|
+
})
|
|
850
|
+
}
|
|
851
|
+
|
|
766
852
|
/**
|
|
767
853
|
* 会话日志事件 → 状态。这是投影的入口:除了工具结果里的变更记录,
|
|
768
854
|
* 还吃三条**关于过程本身的事实**——
|
|
@@ -843,7 +929,7 @@ export function applyEvent(state, event) {
|
|
|
843
929
|
try {
|
|
844
930
|
const payload = JSON.parse(facts.text)
|
|
845
931
|
if (Array.isArray(payload?.mutations) && payload.mutations.length > 0) {
|
|
846
|
-
next = applyMutations(next, payload.mutations)
|
|
932
|
+
next = applyMutations(next, stampAt(payload.mutations, typeof event.time === 'number' ? event.time : null))
|
|
847
933
|
touched = true
|
|
848
934
|
}
|
|
849
935
|
} catch {
|
|
@@ -911,6 +997,29 @@ export function applyEvent(state, event) {
|
|
|
911
997
|
fact.review = { decision: gate.action === 'retract_fact' ? 'retracted' : 'kept', reason: gate.note ?? null, at, by: 'user' }
|
|
912
998
|
return next
|
|
913
999
|
}
|
|
1000
|
+
/**
|
|
1001
|
+
* **本体四动词(人的通道)**:路由侧已按同一份判据校验过(表外的值进不了日志),
|
|
1002
|
+
* 折法只做两件事——落成带 `by:'user'` 的词汇事件,以及**幂等**(人可能点两下、
|
|
1003
|
+
* 消息也可能重放:同 id 再登记不重复落,修订/废止的幂等由 applyLexiconMutation 自己保证)。
|
|
1004
|
+
*/
|
|
1005
|
+
if (gate.action === 'register_term' || gate.action === 'register_predicate') {
|
|
1006
|
+
const entry = gate.entry ?? {}
|
|
1007
|
+
const taken = (next.lexicon.terms ?? []).some((item) => item.id === entry.id) || (next.lexicon.predicates ?? []).some((item) => item.id === entry.id)
|
|
1008
|
+
if (taken === true) return next
|
|
1009
|
+
next.lexicon = applyLexiconMutation(next.lexicon, { t: gate.action === 'register_term' ? 'ontology/term_added' : 'ontology/predicate_added', ...entry, by: 'user' }, at)
|
|
1010
|
+
return next
|
|
1011
|
+
}
|
|
1012
|
+
if (gate.action === 'revise_term') {
|
|
1013
|
+
const entry = gate.entry ?? {}
|
|
1014
|
+
next.lexicon = applyLexiconMutation(next.lexicon, { t: 'ontology/term_revised', ...entry, by: 'user' }, at)
|
|
1015
|
+
return next
|
|
1016
|
+
}
|
|
1017
|
+
if (gate.action === 'deprecate_entry') {
|
|
1018
|
+
const entry = gate.entry ?? {}
|
|
1019
|
+
const kind = (next.lexicon.terms ?? []).some((item) => item.id === entry.id) ? 'ontology/term_deprecated' : 'ontology/predicate_deprecated'
|
|
1020
|
+
next.lexicon = applyLexiconMutation(next.lexicon, { t: kind, ...entry, by: 'user' }, at)
|
|
1021
|
+
return next
|
|
1022
|
+
}
|
|
914
1023
|
const fork = next.forks.find((item) => item.id === (gate.fork ?? null))
|
|
915
1024
|
if (fork === undefined) return next
|
|
916
1025
|
fork.humanDecision = {
|
|
@@ -961,7 +1070,17 @@ export function applyEvent(state, event) {
|
|
|
961
1070
|
const meta = event.data === undefined || event.data === null ? undefined : event.data.meta
|
|
962
1071
|
let next = state
|
|
963
1072
|
if (meta !== null && meta !== undefined && typeof meta === 'object' && meta.kind === MUTATION_KIND) {
|
|
964
|
-
|
|
1073
|
+
/**
|
|
1074
|
+
* **时间由折法盖上去,不由产出方写**:内核只写「发生了什么」,不知道也不该关心
|
|
1075
|
+
* 这条记录落在日志的哪一刻;而事件本身带着 `time`,那才是权威。
|
|
1076
|
+
*
|
|
1077
|
+
* 不盖的后果是全局性的:每条变更的 `at` 都取 `mutation.at ?? 0`,于是凡是显示时间的
|
|
1078
|
+
* 地方——Inspector 的历史、计划开合、证据时刻——一律是 **1970-01-01**。
|
|
1079
|
+
* 盖在这里(而不是让内核每条都写一遍时间)同时满足两件事:内核不必读时钟,
|
|
1080
|
+
* 折法仍然只有一个入口。
|
|
1081
|
+
*/
|
|
1082
|
+
const stamped = stampAt(Array.isArray(meta.mutations) ? meta.mutations : meta.mutation === undefined ? [] : [meta.mutation], typeof event.time === 'number' ? event.time : null)
|
|
1083
|
+
next = applyMutations(next, stamped)
|
|
965
1084
|
}
|
|
966
1085
|
if (next.inFlight !== null) {
|
|
967
1086
|
const cleared = next === state ? clone(state) : next
|
|
@@ -975,6 +1094,614 @@ export function applyEvent(state, event) {
|
|
|
975
1094
|
|
|
976
1095
|
// ── 派生:阶段 / 完成度 / 假设状态 / 世界线阶段,全部现算,状态里不存 ──────────
|
|
977
1096
|
|
|
1097
|
+
/** 假设的终态:到了这里就不再是「还要继续做的活」。 */
|
|
1098
|
+
const TERMINAL_HYPOTHESIS = new Set(['refuted', 'superseded', 'retracted'])
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* **知识模式(分诊)与缺口读数。**
|
|
1102
|
+
*
|
|
1103
|
+
* 判据是**结构的,不是词法的**:这里不猜「这句话像不像研究任务」。立约(`SetGoal`)并用
|
|
1104
|
+
* 相互竞争的假设登记它,是模型自己已经做出的那次承诺——它意味着这件事要跨多轮、要有依据、
|
|
1105
|
+
* 要有可复核的结论。日常问答从不立约,于是从不进这一档。用词面启发式去猜任务类型,
|
|
1106
|
+
* 正是真跑里评估者抓到的那类「硬编码比例」的老路:猜错了没人能复核,而结构判据可以。
|
|
1107
|
+
*
|
|
1108
|
+
* 缺口(`gaps`)是**读数,不是拦截**,也不新增账本:每一条都从已有事实算出来,并且指得出
|
|
1109
|
+
* 一个今天就能补的动作。它们要进运行态卡——模型每一步唯一读到的那个窗口。让缺口每回合可见,
|
|
1110
|
+
* 比让提示词多叮嘱一句可靠:提示词会被读成建议,而卡里的读数不会。
|
|
1111
|
+
*/
|
|
1112
|
+
export function deriveKnowledge(state, hypotheses, factRows, lexicon) {
|
|
1113
|
+
const goal = state?.goal ?? null
|
|
1114
|
+
const open = goal !== null && goal.status !== 'achieved' && goal.status !== 'abandoned'
|
|
1115
|
+
const registered = hypotheses.filter((item) => !TERMINAL_HYPOTHESIS.has(item.status))
|
|
1116
|
+
/**
|
|
1117
|
+
* 模式的**唯一**判据:目标还开着,而且它带着登记过的假设。
|
|
1118
|
+
*
|
|
1119
|
+
* 为什么不是「目标存在」就够:`minHypotheses` 是部署自己的产品立场(内核缺省是 0 = 机制中立)。
|
|
1120
|
+
* 一个把下限设成 0 的部署明确说了「我不要这条纪律」,那时它也不该收到知识模式的读数。
|
|
1121
|
+
*/
|
|
1122
|
+
const mode = open && registered.length > 0 ? 'knowledge' : 'ordinary'
|
|
1123
|
+
const terms = Array.isArray(lexicon?.terms) ? lexicon.terms : []
|
|
1124
|
+
const predicates = Array.isArray(lexicon?.predicates) ? lexicon.predicates : []
|
|
1125
|
+
const gaps = []
|
|
1126
|
+
if (mode === 'knowledge') {
|
|
1127
|
+
/**
|
|
1128
|
+
* ① 语言还没立起来:跨轮复用与「按概念取用已知」都要求先有词汇。
|
|
1129
|
+
* 判据是**两个都空**——只有概念没有谓词时,关系还说不出来,但语言已经开张了,
|
|
1130
|
+
* 那是进展不是缺口(否则每注册一个概念就多一条永远擦不掉的抱怨)。
|
|
1131
|
+
*/
|
|
1132
|
+
if (terms.length === 0 && predicates.length === 0) {
|
|
1133
|
+
gaps.push({
|
|
1134
|
+
code: 'no_language',
|
|
1135
|
+
count: registered.length,
|
|
1136
|
+
detail: `${registered.length} 条在验命题,但还没有任何概念与谓词:换一轮只能靠重读散文取用它们`,
|
|
1137
|
+
})
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* ② 只有散文主张的命题:断言是可选的(「加法,不是门槛」),所以这里**不是违规**,
|
|
1141
|
+
* 而是「这条主张还不能被机器比对」。只算非终态的:被推翻/被替代的不再欠这一笔。
|
|
1142
|
+
*/
|
|
1143
|
+
const proseOnly = registered.filter((item) => !Array.isArray(item.assertions) || item.assertions.length === 0)
|
|
1144
|
+
if (proseOnly.length > 0) {
|
|
1145
|
+
gaps.push({
|
|
1146
|
+
code: 'prose_only_claims',
|
|
1147
|
+
count: proseOnly.length,
|
|
1148
|
+
detail: `${proseOnly.length} 条在验命题只有散文主张:两条结论是不是在说同一件事,只能靠重读判断`,
|
|
1149
|
+
})
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* ③ **升格时没带断言**。只算 0.2.0 那条路走出来的事实(`hypothesis` 已关联)——
|
|
1153
|
+
* 更早的事实补不上断言(见已知缺口),把它们算成欠账就是一条永远还不掉的抱怨。
|
|
1154
|
+
*/
|
|
1155
|
+
const unstructured = factRows.filter((fact) => typeof fact.hypothesis === 'string' && fact.hypothesis !== '' && (!Array.isArray(fact.assertions) || fact.assertions.length === 0))
|
|
1156
|
+
if (unstructured.length > 0) {
|
|
1157
|
+
gaps.push({
|
|
1158
|
+
code: 'unstructured_facts',
|
|
1159
|
+
count: unstructured.length,
|
|
1160
|
+
detail: `${unstructured.length} 条已升格事实没有断言:它们进不了实体图,也不能按概念取用`,
|
|
1161
|
+
})
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* ④ **从没被证据碰过的命题**。只在这次会话已经真的跑出过证据之后才报——计划刚立、
|
|
1165
|
+
* 一步都还没走时,所有假设都是「没碰过」,那是正常的起点而不是缺口。
|
|
1166
|
+
* 它和结案时那条 `unjudged` 是同一条先例:**不逼裁决,但不许把「没看过」写成「没问题」**。
|
|
1167
|
+
*/
|
|
1168
|
+
if (Array.isArray(state?.evidence) && state.evidence.length > 0) {
|
|
1169
|
+
const untouched = registered.filter((item) => item.refutations + item.inconclusive === 0 && item.supportedLevel === null)
|
|
1170
|
+
if (untouched.length > 0) {
|
|
1171
|
+
gaps.push({
|
|
1172
|
+
code: 'untouched_claims',
|
|
1173
|
+
count: untouched.length,
|
|
1174
|
+
detail: `${untouched.length} 条在验命题还没有任何证据碰过(支持 / 推翻 / 无法判定都算碰过)`,
|
|
1175
|
+
})
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return {
|
|
1180
|
+
mode,
|
|
1181
|
+
/** 一句人话:为什么进了这一档(或为什么没进)。它进卡,所以不许写成术语。 */
|
|
1182
|
+
why:
|
|
1183
|
+
mode === 'knowledge'
|
|
1184
|
+
? `目标还开着,带着 ${registered.length} 条登记过的命题——这是跨轮、要依据、要可复核结论的活`
|
|
1185
|
+
: open
|
|
1186
|
+
? '目标还开着,但没有登记命题:按普通任务推进'
|
|
1187
|
+
: '没有开着的目标:按普通任务推进',
|
|
1188
|
+
gaps,
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* **知识预检(preflight)**:进入知识模式那一刻,把「已知」主动送到模型面前。
|
|
1194
|
+
*
|
|
1195
|
+
* 解决的问题:真跑里模型不查就开工——不是因为不知道有 \`QueryKnowledge\`,
|
|
1196
|
+
* 而是因为**没人提醒它此刻该查**。提示词会被读成建议;卡里的读数不会。
|
|
1197
|
+
* 于是这里把相关性判断做成投影:从当前目标与命题的文本出发,圈出**有界**的一组
|
|
1198
|
+
* 已有词汇、事实与冲突,随运行态卡注入。模型只在这份摘要不够用时才需要精确查询。
|
|
1199
|
+
*
|
|
1200
|
+
* 三条纪律(与缺口读数同一套):
|
|
1201
|
+
* · **只读**:不产生变更,不新增状态——它就是 \`derive\` 的另一个读面;
|
|
1202
|
+
* · **有界**:terms / predicates / facts 各有上限,超出如实说「还有 N 个未列出」;
|
|
1203
|
+
* · **不猜语义**:相关性是**词面命中**(label / id / alias 出现在主张文本里),
|
|
1204
|
+
* 不是「系统认为相关」——命中的依据逐条可复核。
|
|
1205
|
+
*
|
|
1206
|
+
* 判据的形状:主张文本(去空白)包含词条的 label / id / alias(去空白),
|
|
1207
|
+
* 或者命题已有的断言引用了该谓词。前者是「这个词已经在对话里出现了」,后者是「已经在用了」。
|
|
1208
|
+
*/
|
|
1209
|
+
export function knowledgePreflight(state, derived) {
|
|
1210
|
+
const knowledge = derived?.knowledge ?? deriveKnowledge(state, derived?.hypotheses ?? [], derived?.factRows ?? [], derived?.lexicon)
|
|
1211
|
+
if (knowledge.mode !== 'knowledge') return null
|
|
1212
|
+
const terms = Array.isArray(derived?.lexicon?.terms) ? derived.lexicon.terms : []
|
|
1213
|
+
const predicates = Array.isArray(derived?.lexicon?.predicates) ? derived.lexicon.predicates : []
|
|
1214
|
+
const hypotheses = Array.isArray(derived?.hypotheses) ? derived.hypotheses : []
|
|
1215
|
+
const factRows = Array.isArray(derived?.factRows) ? derived.factRows : []
|
|
1216
|
+
const conflicts = Array.isArray(derived?.conflicts) ? derived.conflicts : []
|
|
1217
|
+
/** 命题的文本面:目标主张 + 每条命题的主张(去空白后做包含判断)。 */
|
|
1218
|
+
const claimText = [state?.goal?.claim, ...hypotheses.map((item) => item.claim)].filter((text) => typeof text === 'string' && text !== '').map((text) => String(text).replace(/\s+/g, ''))
|
|
1219
|
+
/** 命题已经引用的谓词(断言在假设上时就该算「在用」)。 */
|
|
1220
|
+
const usedPredicates = new Set(hypotheses.flatMap((item) => (Array.isArray(item.assertions) ? item.assertions : [])).map((assertion) => String(assertion?.predicate ?? '')))
|
|
1221
|
+
const hits = (entry) => {
|
|
1222
|
+
const candidates = [entry.id, entry.label, ...(Array.isArray(entry.aliases) ? entry.aliases : [])].filter((text) => typeof text === 'string' && text !== '').map((text) => String(text).replace(/\s+/g, ''))
|
|
1223
|
+
return candidates.some((candidate) => claimText.some((text) => text.includes(candidate)))
|
|
1224
|
+
}
|
|
1225
|
+
const LIMIT = 20
|
|
1226
|
+
const matchedTerms = terms.filter((term) => hits(term) && term.status !== 'deprecated')
|
|
1227
|
+
const matchedPredicates = predicates.filter((predicate) => usedPredicates.has(predicate.id) || hits(predicate))
|
|
1228
|
+
const matchedTermIds = new Set(matchedTerms.map((term) => term.id))
|
|
1229
|
+
const matchedPredicateIds = new Set(matchedPredicates.map((predicate) => predicate.id))
|
|
1230
|
+
/** 事实:断言引用了命中的谓词,或主词类型是命中的概念。 */
|
|
1231
|
+
const matchedFacts = factRows.filter((fact) =>
|
|
1232
|
+
(Array.isArray(fact.assertions) ? fact.assertions : []).some((assertion) => matchedPredicateIds.has(String(assertion?.predicate ?? '')) || matchedTermIds.has(String(assertion?.subject?.type ?? ''))),
|
|
1233
|
+
)
|
|
1234
|
+
return {
|
|
1235
|
+
mode: 'knowledge',
|
|
1236
|
+
/** 词面命中的词汇:模型接下来要写的结论大概率会用到它们。 */
|
|
1237
|
+
terms: matchedTerms.slice(0, LIMIT).map((term) => ({ id: term.id, label: term.label, gloss: term.gloss, parent: term.parent ?? null, status: term.status, uses: term.uses ?? 0, basis: term.basis ?? null })),
|
|
1238
|
+
termsTruncated: Math.max(0, matchedTerms.length - LIMIT),
|
|
1239
|
+
predicates: matchedPredicates.slice(0, LIMIT).map((predicate) => ({ id: predicate.id, label: predicate.label, domain: predicate.domain, range: predicate.range, functional: predicate.functional === true, status: predicate.status, uses: predicate.uses ?? 0, basis: predicate.basis ?? null })),
|
|
1240
|
+
predicatesTruncated: Math.max(0, matchedPredicates.length - LIMIT),
|
|
1241
|
+
/** 命中的既有事实(可复用的「已知」)。 */
|
|
1242
|
+
facts: matchedFacts.slice(0, LIMIT).map((fact) => ({ id: fact.id, text: fact.text, level: fact.level ?? null, scope: fact.scope ?? null, hypothesis: fact.hypothesis ?? null, review: fact.review?.decision ?? null })),
|
|
1243
|
+
factsTruncated: Math.max(0, matchedFacts.length - LIMIT),
|
|
1244
|
+
/** 冲突:有就带上(它们约束「哪些结论还不能随便写)。 */
|
|
1245
|
+
conflicts: conflicts.map((conflict) => ({ predicate: conflict.predicate, subject: conflict.subject })),
|
|
1246
|
+
/** 缺口读数(与卡里同一份:`deriveKnowledge` 的 gaps)。 */
|
|
1247
|
+
gaps: knowledge.gaps,
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// ── 知识 Inspector:一个选择 → 它的定义 / 关系 / 断言 / 证据链 / 历史 ──────────
|
|
1252
|
+
|
|
1253
|
+
/**
|
|
1254
|
+
* **图上的一个选择(节点或边),它的全部知识读数。**
|
|
1255
|
+
*
|
|
1256
|
+
* 为什么要有它:图能画出来不等于图是知识入口。点击一个节点却只得到「按此过滤」,
|
|
1257
|
+
* 读的人仍然不知道这个词是什么意思、凭什么信、谁改过它。这一份投影把那条链补齐:
|
|
1258
|
+
*
|
|
1259
|
+
* ```text
|
|
1260
|
+
* 概念 → 定义 / 父概念 / 子概念 / 用它的谓词 / 相关实例 / 相关事实 / 历史
|
|
1261
|
+
* 谓词 → 主词域 / 值域 / 单值性 / 用它的事实 / 由它产生的冲突 / 历史
|
|
1262
|
+
* 实例 → 类型 / 入边 / 出边 / 每条断言的完整链 / 冲突
|
|
1263
|
+
* 字面值 → 值形态 / 取值 / 产生它的事实 / 出处
|
|
1264
|
+
* 断言边 → 事实 → 命题 → 证据 → 出处 → 产生步骤 → 复核态
|
|
1265
|
+
* ```
|
|
1266
|
+
*
|
|
1267
|
+
* 三条纪律与其余读面同一套:
|
|
1268
|
+
* · **只读**:纯函数,不产生变更、不新增状态;判据只有这一处(客户端不许自己拼链);
|
|
1269
|
+
* · **有界**:每一类列表都有上限,超出如实报 truncated;
|
|
1270
|
+
* · **不编**:关联不到就如实给空,不猜「大概相关」——旧账本没有 `hypothesis` 关联时
|
|
1271
|
+
* 给 `null`,不拿文本相等去冒充身份(那条退路只属于折法,不属于读面)。
|
|
1272
|
+
*
|
|
1273
|
+
* 选择可以是 `{ kind, id }`,也可以直接给图上的 id 串(前缀即类型,见 `normalizeSelection`)。
|
|
1274
|
+
*/
|
|
1275
|
+
export function inspectGraphSelection(state, selection, derived) {
|
|
1276
|
+
const picked = normalizeSelection(selection)
|
|
1277
|
+
if (picked === null) return null
|
|
1278
|
+
const rows = derived ?? derive(state)
|
|
1279
|
+
const context = inspectorContext(state, rows)
|
|
1280
|
+
switch (picked.kind) {
|
|
1281
|
+
case 'concept':
|
|
1282
|
+
return inspectConcept(context, picked.id)
|
|
1283
|
+
case 'value_type':
|
|
1284
|
+
return inspectValueType(context, picked.id)
|
|
1285
|
+
case 'predicate':
|
|
1286
|
+
return inspectPredicate(context, picked.id)
|
|
1287
|
+
case 'instance':
|
|
1288
|
+
return inspectInstance(context, picked.id)
|
|
1289
|
+
case 'literal':
|
|
1290
|
+
return inspectLiteral(context, picked.id)
|
|
1291
|
+
case 'edge':
|
|
1292
|
+
return inspectEdge(context, picked.id)
|
|
1293
|
+
default:
|
|
1294
|
+
return null
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/** 列表上限:读面有界,超出如实报 truncated(与知识预检同一套纪律)。 */
|
|
1299
|
+
const INSPECT_LIMIT = 30
|
|
1300
|
+
const cut = (list, limit = INSPECT_LIMIT) => ({ rows: list.slice(0, limit), truncated: Math.max(0, list.length - limit) })
|
|
1301
|
+
|
|
1302
|
+
/**
|
|
1303
|
+
* 把选择归一到 `{ kind, id }`。
|
|
1304
|
+
*
|
|
1305
|
+
* 图上的 id 串自带类型前缀(`graphProjection` 定的形状),所以两种调用方式都认:
|
|
1306
|
+
* 客户端手上有节点对象时给 `{ kind, id }` 最稳;只拿到 id 串时按前缀解析。
|
|
1307
|
+
* 解析不出来**返回 null**,不猜一个类型——猜错会让 Inspector 端出一份别的东西的定义。
|
|
1308
|
+
*/
|
|
1309
|
+
function normalizeSelection(selection) {
|
|
1310
|
+
const raw = typeof selection === 'string' ? { id: selection } : selection
|
|
1311
|
+
if (raw === null || typeof raw !== 'object') return null
|
|
1312
|
+
const id = String(raw.id ?? '')
|
|
1313
|
+
if (id === '') return null
|
|
1314
|
+
/** 调用方声明的类型优先认;声明了就必须对得上,不许「说 A 给 B」。 */
|
|
1315
|
+
const declared = typeof raw.kind === 'string' ? raw.kind : ''
|
|
1316
|
+
const ok = (kind) => declared === '' || declared === kind
|
|
1317
|
+
if (id.startsWith('term:')) return ok('concept') ? { kind: 'concept', id: id.slice('term:'.length) } : null
|
|
1318
|
+
if (id.startsWith('is_a:')) return ok('concept') ? { kind: 'concept', id: id.slice('is_a:'.length) } : null
|
|
1319
|
+
if (id.startsWith('form:')) return ok('value_type') ? { kind: 'value_type', id: id.slice('form:'.length) } : null
|
|
1320
|
+
if (id.startsWith('predicate:')) return ok('predicate') ? { kind: 'predicate', id: id.slice('predicate:'.length) } : null
|
|
1321
|
+
if (id.startsWith('assertion:')) return ok('edge') ? { kind: 'edge', id } : null
|
|
1322
|
+
if (declared === 'concept' || declared === 'predicate' || declared === 'value_type' || declared === 'edge') return { kind: declared, id }
|
|
1323
|
+
/** 剩下的两种由图上的 id 形状定:实例是 `类型|名称`,字面值是 `谓词:值形态:取值`。 */
|
|
1324
|
+
if (id.includes('|')) return ok('instance') ? { kind: 'instance', id } : null
|
|
1325
|
+
if (id.includes(':')) return ok('literal') ? { kind: 'literal', id } : null
|
|
1326
|
+
return null
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Inspector 的**一次性的索引**:把状态里的几组东西按 id 归好,省得每个分支各建一遍。
|
|
1331
|
+
* 它不缓存、不外传——每次询问建一次,免得读面带上一份会过期的索引。
|
|
1332
|
+
*/
|
|
1333
|
+
function inspectorContext(state, derived) {
|
|
1334
|
+
const facts = Array.isArray(derived?.factRows) ? derived.factRows : Array.isArray(state?.facts) ? state.facts : []
|
|
1335
|
+
const lexicon = normalizeLexicon(state?.lexicon)
|
|
1336
|
+
const hypothesisById = new Map((state?.hypotheses ?? []).map((item) => [String(item.id ?? ''), item]))
|
|
1337
|
+
/**
|
|
1338
|
+
* 命题**优先读派生的那一份**:`supportedLevel` / `refutations` / `inconclusive` 都是现算的,
|
|
1339
|
+
* 只读原始状态的话,Inspector 会写「支持到 —」而卡片上明明有等级——同一件事两处说法不同。
|
|
1340
|
+
*/
|
|
1341
|
+
const derivedHypothesisById = new Map((Array.isArray(derived?.hypotheses) ? derived.hypotheses : []).map((item) => [String(item.id ?? ''), item]))
|
|
1342
|
+
const evidenceById = new Map((state?.evidence ?? []).map((item) => [String(item.id ?? ''), item]))
|
|
1343
|
+
const materialById = new Map((state?.materials ?? []).map((item) => [String(item.id ?? ''), item]))
|
|
1344
|
+
const stepOf = (planId, stepId) =>
|
|
1345
|
+
(state?.plans ?? []).find((plan) => plan.id === planId)?.steps.find((step) => step.id === stepId) ?? null
|
|
1346
|
+
return { state, derived, facts, lexicon, hypothesisById, derivedHypothesisById, evidenceById, materialById, stepOf, conflicts: Array.isArray(derived?.conflicts) ? derived.conflicts : [] }
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/** 一条断言的一行人话(与货架 / 卡片同源:`domain-language` 的 `formatAssertion`)。 */
|
|
1350
|
+
const say = (lexicon, assertion) => formatAssertion(lexicon, assertion)
|
|
1351
|
+
|
|
1352
|
+
/**
|
|
1353
|
+
* **一条事实的完整链**:事实 → 命题 → 证据 → 出处 → 产生步骤 → 复核态。
|
|
1354
|
+
*
|
|
1355
|
+
* 这是阶段 5 的核心:图上一条边背后到底站着什么。每一段都来自账本已有的东西,
|
|
1356
|
+
* 没有一段是这里推断出来的。
|
|
1357
|
+
*/
|
|
1358
|
+
function factChain(context, fact) {
|
|
1359
|
+
const { lexicon, hypothesisById, derivedHypothesisById, evidenceById, stepOf, conflicts } = context
|
|
1360
|
+
const evidenceIds = Array.isArray(fact?.evidence) ? fact.evidence : []
|
|
1361
|
+
const evidence = evidenceIds
|
|
1362
|
+
.map((id) => evidenceById.get(String(id)))
|
|
1363
|
+
.filter((item) => item !== undefined)
|
|
1364
|
+
.map((item) => {
|
|
1365
|
+
const step = stepOf(item.plan, item.step) ?? null
|
|
1366
|
+
return {
|
|
1367
|
+
id: item.id,
|
|
1368
|
+
verdict: item.verdict,
|
|
1369
|
+
level: item.level,
|
|
1370
|
+
evaluator: item.evaluator,
|
|
1371
|
+
basis: item.basis ?? null,
|
|
1372
|
+
anchor: item.anchor ?? null,
|
|
1373
|
+
at: item.at ?? null,
|
|
1374
|
+
/** 四类出处由账本记账时解析好,这里只搬运。 */
|
|
1375
|
+
origins: Array.isArray(item.origins) ? item.origins : [],
|
|
1376
|
+
refs: Array.isArray(item.refs) ? item.refs : [],
|
|
1377
|
+
/** 材料(id 或路径)也带上摘要,读的人不必再开一次文件。 */
|
|
1378
|
+
materials: (Array.isArray(item.refs) ? item.refs : [])
|
|
1379
|
+
.map((ref) => context.materialById.get(String(ref)))
|
|
1380
|
+
.filter((item2) => item2 !== undefined)
|
|
1381
|
+
.map((item2) => ({ id: item2.id, source: item2.source ?? null, path: item2.path ?? null, note: (item2.note ?? '').slice(0, 200) })),
|
|
1382
|
+
step: step === null ? null : { plan: item.plan, id: item.step, do: step.do ?? null, doneCriteria: step.done_criteria ?? null, status: step.status ?? null },
|
|
1383
|
+
}
|
|
1384
|
+
})
|
|
1385
|
+
const hypothesisId = typeof fact?.hypothesis === 'string' && fact.hypothesis !== '' ? fact.hypothesis : null
|
|
1386
|
+
const hypothesis = hypothesisId === null ? null : derivedHypothesisById.get(hypothesisId) ?? hypothesisById.get(hypothesisId) ?? null
|
|
1387
|
+
const assertions = Array.isArray(fact?.assertions) ? fact.assertions : []
|
|
1388
|
+
return {
|
|
1389
|
+
id: fact?.id ?? null,
|
|
1390
|
+
text: fact?.text ?? null,
|
|
1391
|
+
level: fact?.level ?? null,
|
|
1392
|
+
scope: fact?.scope ?? null,
|
|
1393
|
+
path: fact?.path ?? null,
|
|
1394
|
+
at: fact?.at ?? null,
|
|
1395
|
+
/** `live` / `refuted`(有推翻证据待人裁决)/ `retracted`(人已撤回)。 */
|
|
1396
|
+
status: fact?.review?.decision === 'retracted' ? 'retracted' : fact?.refuted === true ? 'refuted' : 'live',
|
|
1397
|
+
review: fact?.review === undefined || fact?.review === null ? null : { decision: fact.review.decision ?? null, reason: fact.review.reason ?? null, at: fact.review.at ?? null },
|
|
1398
|
+
assertions: assertions.map((assertion) => ({ ...assertion, chip: say(lexicon, assertion) })),
|
|
1399
|
+
hypothesis:
|
|
1400
|
+
hypothesis === null
|
|
1401
|
+
? null
|
|
1402
|
+
: { id: hypothesis.id, claim: hypothesis.claim ?? null, refuteWhen: hypothesis.refute_when ?? null, status: hypothesis.status ?? null, supportedLevel: hypothesis.supportedLevel ?? null, refutations: hypothesis.refutations ?? 0, inconclusive: hypothesis.inconclusive ?? 0 },
|
|
1403
|
+
evidence,
|
|
1404
|
+
/** 由这条事实参与构成的冲突(只暴露,不裁决——与投影同一句话)。 */
|
|
1405
|
+
conflicts: conflicts
|
|
1406
|
+
.filter((conflict) => (Array.isArray(conflict.sides) ? conflict.sides : []).some((side) => side.fact === fact?.id))
|
|
1407
|
+
.map((conflict) => ({ predicate: conflict.predicate, subject: conflict.subject, sides: conflict.sides.map((side) => ({ fact: side.fact ?? null, value: side.value ?? null })) })),
|
|
1408
|
+
history: factHistory(fact, evidence),
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/**
|
|
1413
|
+
* 事实的**留痕**:升格 / 评审决定 / 每条证据什么时候到的。
|
|
1414
|
+
* 它是「状态里真的留着的那几条记录」,不是完整事件流——完整事件流在会话日志里,
|
|
1415
|
+
* 读面不假装自己有它(见 known-gaps)。
|
|
1416
|
+
*/
|
|
1417
|
+
function factHistory(fact, evidence) {
|
|
1418
|
+
const events = []
|
|
1419
|
+
if (fact?.at !== undefined && fact?.at !== null) events.push({ kind: 'fact/promoted', at: fact.at, summary: `升格为事实(支持到 ${fact.level ?? '—'})` })
|
|
1420
|
+
if (fact?.review !== undefined && fact?.review !== null) {
|
|
1421
|
+
events.push({
|
|
1422
|
+
kind: 'fact/reviewed',
|
|
1423
|
+
at: fact.review.at ?? null,
|
|
1424
|
+
summary: fact.review.decision === 'retracted' ? '人审查后**撤回**(记录保留)' : '人审查后**维持**(判证据不可靠)',
|
|
1425
|
+
reason: fact.review.reason ?? null,
|
|
1426
|
+
})
|
|
1427
|
+
}
|
|
1428
|
+
for (const item of evidence) events.push({ kind: 'evidence/recorded', at: item.at ?? null, summary: `${item.verdict}(${item.evaluator} · ${item.level})`, reason: item.basis ?? null })
|
|
1429
|
+
return events.filter((event) => event.at !== null).sort((left, right) => left.at - right.at)
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
/** 词汇条目的留痕:登记 / 历次修订 / 废止。登记与修订本来就存在词条里(折法保留的)。 */
|
|
1433
|
+
function entryHistory(entry, label) {
|
|
1434
|
+
const events = []
|
|
1435
|
+
if (entry?.at !== undefined && entry?.at !== null) events.push({ kind: `${label}_added`, at: entry.at, summary: `登记(依据:${entry.basis ?? '—'})`, by: entry.by ?? null })
|
|
1436
|
+
for (const revision of Array.isArray(entry?.revisions) ? entry.revisions : []) {
|
|
1437
|
+
events.push({ kind: `${label}_revised`, at: revision.at ?? null, summary: `修订到 v${revision.version ?? '?'}`, reason: revision.reason ?? null, by: revision.by ?? null })
|
|
1438
|
+
}
|
|
1439
|
+
if (entry?.deprecated !== undefined && entry?.deprecated !== null) {
|
|
1440
|
+
events.push({ kind: `${label}_deprecated`, at: entry.deprecated.at ?? null, summary: '废止(黏性终态,没有复活)', reason: entry.deprecated.reason ?? null, by: entry.deprecated.by ?? null })
|
|
1441
|
+
}
|
|
1442
|
+
return events.filter((event) => event.at !== null || event.kind.endsWith('_deprecated')).sort((left, right) => (left.at ?? 0) - (right.at ?? 0))
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
/** 事实的断言是否碰到这个概念(主词类型 = 概念 id)。 */
|
|
1446
|
+
const assertsOn = (fact, termId) => (Array.isArray(fact?.assertions) ? fact.assertions : []).some((assertion) => String(assertion?.subject?.type ?? '') === termId)
|
|
1447
|
+
const usesPredicate = (fact, predicateId) => (Array.isArray(fact?.assertions) ? fact.assertions : []).some((assertion) => String(assertion?.predicate ?? '') === predicateId)
|
|
1448
|
+
|
|
1449
|
+
/** 概念节点:语言里这个词是什么、它连着谁、哪些事实在用它。 */
|
|
1450
|
+
function inspectConcept(context, termId) {
|
|
1451
|
+
const { lexicon, facts, conflicts, state } = context
|
|
1452
|
+
const term = lexicon.terms.find((item) => item.id === termId)
|
|
1453
|
+
if (term === undefined) return null
|
|
1454
|
+
const children = lexicon.terms.filter((item) => String(item.parent ?? '') === termId)
|
|
1455
|
+
const predicates = lexicon.predicates.filter((item) => String(item.domain ?? '') === termId || String(item.range?.term ?? '') === termId)
|
|
1456
|
+
const relatedFacts = facts.filter((fact) => assertsOn(fact, termId))
|
|
1457
|
+
/** 实例:这一概念下主词出现过的具体对象(从断言投影,与实体图同一份判据)。 */
|
|
1458
|
+
const instances = new Map()
|
|
1459
|
+
for (const fact of relatedFacts) {
|
|
1460
|
+
for (const assertion of Array.isArray(fact.assertions) ? fact.assertions : []) {
|
|
1461
|
+
if (String(assertion?.subject?.type ?? '') !== termId) continue
|
|
1462
|
+
const key = String(assertion.subject.id ?? '')
|
|
1463
|
+
if (key === '') continue
|
|
1464
|
+
if (!instances.has(key)) instances.set(key, { id: `${termId}|${key}`, ref: key, label: key, facts: [] })
|
|
1465
|
+
instances.get(key).facts.push(fact.id ?? null)
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
const chain = cut(relatedFacts)
|
|
1469
|
+
return {
|
|
1470
|
+
selection: { kind: 'concept', id: term.id, label: term.label ?? term.id },
|
|
1471
|
+
definition: {
|
|
1472
|
+
id: term.id,
|
|
1473
|
+
label: term.label ?? term.id,
|
|
1474
|
+
gloss: term.gloss ?? null,
|
|
1475
|
+
aliases: Array.isArray(term.aliases) ? term.aliases : [],
|
|
1476
|
+
parent: term.parent === null || term.parent === undefined ? null : String(term.parent),
|
|
1477
|
+
status: term.status ?? 'admitted',
|
|
1478
|
+
version: term.version ?? 1,
|
|
1479
|
+
basis: term.basis ?? null,
|
|
1480
|
+
by: term.by ?? null,
|
|
1481
|
+
at: term.at ?? null,
|
|
1482
|
+
uses: term.uses ?? 0,
|
|
1483
|
+
},
|
|
1484
|
+
relations: {
|
|
1485
|
+
parent: term.parent === null || term.parent === undefined ? null : { id: String(term.parent), label: lexicon.terms.find((item) => item.id === String(term.parent))?.label ?? String(term.parent) },
|
|
1486
|
+
children: children.map((item) => ({ id: item.id, label: item.label ?? item.id, status: item.status ?? 'admitted' })),
|
|
1487
|
+
predicates: predicates.map((item) => ({ id: item.id, label: item.label ?? item.id, domain: item.domain ?? null, range: item.range ?? null, functional: item.functional === true, status: item.status ?? 'admitted' })),
|
|
1488
|
+
instances: [...instances.values()].sort((left, right) => (left.id < right.id ? -1 : 1)).map((item) => ({ id: item.id, ref: item.ref, label: item.label, factCount: item.facts.length })),
|
|
1489
|
+
},
|
|
1490
|
+
facts: chain.rows.map((fact) => factChain(context, fact)),
|
|
1491
|
+
factsTruncated: chain.truncated,
|
|
1492
|
+
conflicts: conflicts
|
|
1493
|
+
.filter((conflict) => String(conflict.subject ?? '').startsWith(`${termId}|`) || predicates.some((predicate) => predicate.id === conflict.predicate))
|
|
1494
|
+
.map((conflict) => ({ predicate: conflict.predicate, subject: conflict.subject, sides: conflict.sides.map((side) => ({ fact: side.fact ?? null, value: side.value ?? null })) })),
|
|
1495
|
+
history: entryHistory(term, 'term'),
|
|
1496
|
+
actions: { canFilter: true, canExpand: true, canEdit: true },
|
|
1497
|
+
/** 词汇是**约定**,不需要证据等级——把它说清楚,免得读的人以为它「没验」。 */
|
|
1498
|
+
note: '概念是约定,不是主张:它不带证据等级。用这个词写下的句子才需要。',
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
/** 值形态节点:系统固定的五种之一(不独立治理,所以它没有历史)。 */
|
|
1503
|
+
function inspectValueType(context, form) {
|
|
1504
|
+
const { lexicon, facts } = context
|
|
1505
|
+
const known = lexicon.predicates.filter((item) => String(item.range?.form ?? '') === form)
|
|
1506
|
+
if (known.length === 0 && !VALUE_FORMS.includes(form)) return null
|
|
1507
|
+
const used = facts.filter((fact) => (Array.isArray(fact.assertions) ? fact.assertions : []).some((assertion) => String(assertion?.object?.kind ?? '') === form))
|
|
1508
|
+
const chain = cut(used)
|
|
1509
|
+
return {
|
|
1510
|
+
selection: { kind: 'value_type', id: form, label: form },
|
|
1511
|
+
definition: { id: form, label: form, kind: 'value_type', status: 'builtin', gloss: VALUE_FORM_GLOSS[form] ?? null },
|
|
1512
|
+
relations: { predicates: known.map((item) => ({ id: item.id, label: item.label ?? item.id, domain: item.domain ?? null, range: item.range ?? null })) },
|
|
1513
|
+
facts: chain.rows.map((fact) => factChain(context, fact)),
|
|
1514
|
+
factsTruncated: chain.truncated,
|
|
1515
|
+
conflicts: [],
|
|
1516
|
+
/** 值形态由系统固定,不进领域本体、也没有版本史——如实说空,不编一段。 */
|
|
1517
|
+
history: [],
|
|
1518
|
+
actions: { canFilter: true, canExpand: false, canEdit: false },
|
|
1519
|
+
note: '值形态由系统固定(statement / quantity / formula / code / reference),不独立治理,所以没有版本史。',
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/** 谓词节点:它允许什么关系、谁在用、用出了哪些冲突。 */
|
|
1524
|
+
function inspectPredicate(context, predicateId) {
|
|
1525
|
+
const { lexicon, facts, conflicts } = context
|
|
1526
|
+
const predicate = lexicon.predicates.find((item) => item.id === predicateId)
|
|
1527
|
+
if (predicate === undefined) return null
|
|
1528
|
+
const used = facts.filter((fact) => usesPredicate(fact, predicateId))
|
|
1529
|
+
const chain = cut(used)
|
|
1530
|
+
const range = predicate.range ?? null
|
|
1531
|
+
return {
|
|
1532
|
+
selection: { kind: 'predicate', id: predicate.id, label: predicate.label ?? predicate.id },
|
|
1533
|
+
definition: {
|
|
1534
|
+
id: predicate.id,
|
|
1535
|
+
label: predicate.label ?? predicate.id,
|
|
1536
|
+
gloss: predicate.gloss ?? null,
|
|
1537
|
+
domain: predicate.domain ?? null,
|
|
1538
|
+
/** 主词域的**名字**也带上:只给 id,读的人还要自己回词汇表里找。 */
|
|
1539
|
+
domainLabel: predicate.domain === null || predicate.domain === undefined ? null : lexicon.terms.find((item) => item.id === String(predicate.domain))?.label ?? String(predicate.domain),
|
|
1540
|
+
range,
|
|
1541
|
+
functional: predicate.functional === true,
|
|
1542
|
+
status: predicate.status ?? 'admitted',
|
|
1543
|
+
version: predicate.version ?? 1,
|
|
1544
|
+
basis: predicate.basis ?? null,
|
|
1545
|
+
by: predicate.by ?? null,
|
|
1546
|
+
at: predicate.at ?? null,
|
|
1547
|
+
uses: predicate.uses ?? 0,
|
|
1548
|
+
},
|
|
1549
|
+
relations: {
|
|
1550
|
+
valueForm: range?.form ?? null,
|
|
1551
|
+
rangeTerm: range?.term ?? null,
|
|
1552
|
+
/** 用这个谓词写下的**断言**(不是事实:一条事实可以带多条断言)。 */
|
|
1553
|
+
assertions: used.length,
|
|
1554
|
+
subjects: [...new Set(used.flatMap((fact) => (Array.isArray(fact.assertions) ? fact.assertions : []).filter((assertion) => String(assertion?.predicate ?? '') === predicateId).map((assertion) => `${String(assertion?.subject?.type ?? '')}|${String(assertion?.subject?.id ?? '')}`)))].map((key) => ({ key, ref: key.split('|')[1] ?? key, type: key.split('|')[0] ?? '' })),
|
|
1555
|
+
},
|
|
1556
|
+
facts: chain.rows.map((fact) => factChain(context, fact)),
|
|
1557
|
+
factsTruncated: chain.truncated,
|
|
1558
|
+
conflicts: conflicts
|
|
1559
|
+
.filter((conflict) => conflict.predicate === predicateId)
|
|
1560
|
+
.map((conflict) => ({ predicate: conflict.predicate, subject: conflict.subject, sides: conflict.sides.map((side) => ({ fact: side.fact ?? null, value: side.value ?? null })) })),
|
|
1561
|
+
history: entryHistory(predicate, 'predicate'),
|
|
1562
|
+
actions: { canFilter: true, canExpand: true, canEdit: true },
|
|
1563
|
+
note: predicate.functional === true ? '单值谓词:同一主词上两条未撤回的确认事实取值不同时,系统给出一对冲突读数(只暴露,不裁决)。' : null,
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
/** 实例节点:某个具体对象身上挂着的全部断言与它们的链。 */
|
|
1568
|
+
function inspectInstance(context, key) {
|
|
1569
|
+
const { lexicon, facts, conflicts } = context
|
|
1570
|
+
const separator = key.indexOf('|')
|
|
1571
|
+
if (separator < 0) return null
|
|
1572
|
+
const type = key.slice(0, separator)
|
|
1573
|
+
const ref = key.slice(separator + 1)
|
|
1574
|
+
if (ref === '') return null
|
|
1575
|
+
const outgoing = []
|
|
1576
|
+
const incoming = []
|
|
1577
|
+
for (const fact of facts) {
|
|
1578
|
+
for (const assertion of Array.isArray(fact.assertions) ? fact.assertions : []) {
|
|
1579
|
+
const subjectMatches = String(assertion?.subject?.type ?? '') === type && String(assertion?.subject?.id ?? '') === ref
|
|
1580
|
+
const objectMatches = String(assertion?.object?.kind ?? '') === 'instance' && String(assertion?.object?.value ?? '') === ref && (String(assertion?.object?.type ?? '') === type || String(assertion?.object?.type ?? '') === '')
|
|
1581
|
+
if (subjectMatches) outgoing.push({ fact, assertion, direction: 'out' })
|
|
1582
|
+
if (objectMatches) incoming.push({ fact, assertion, direction: 'in' })
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
if (outgoing.length === 0 && incoming.length === 0 && !lexicon.terms.some((item) => item.id === type)) return null
|
|
1586
|
+
const touchedFacts = [...new Set([...outgoing, ...incoming].map((item) => item.fact))]
|
|
1587
|
+
const chain = cut(touchedFacts)
|
|
1588
|
+
const edges = [...outgoing, ...incoming].map((item) => ({
|
|
1589
|
+
direction: item.direction,
|
|
1590
|
+
predicate: String(item.assertion?.predicate ?? ''),
|
|
1591
|
+
predicateLabel: lexicon.predicates.find((entry) => entry.id === String(item.assertion?.predicate ?? ''))?.label ?? null,
|
|
1592
|
+
chip: say(lexicon, item.assertion),
|
|
1593
|
+
fact: item.fact.id ?? null,
|
|
1594
|
+
level: item.fact.level ?? null,
|
|
1595
|
+
status: item.fact.review?.decision === 'retracted' ? 'retracted' : item.fact.refuted === true ? 'refuted' : 'live',
|
|
1596
|
+
}))
|
|
1597
|
+
return {
|
|
1598
|
+
selection: { kind: 'instance', id: key, label: ref },
|
|
1599
|
+
definition: {
|
|
1600
|
+
id: key,
|
|
1601
|
+
ref,
|
|
1602
|
+
type: type === '' ? null : type,
|
|
1603
|
+
typeLabel: type === '' ? null : lexicon.terms.find((item) => item.id === type)?.label ?? type,
|
|
1604
|
+
/** 实例**不注册**(从断言投影出来),所以它没有独立生命周期与版本史。 */
|
|
1605
|
+
status: 'projected',
|
|
1606
|
+
},
|
|
1607
|
+
relations: { edges, out: outgoing.length, in: incoming.length },
|
|
1608
|
+
facts: chain.rows.map((fact) => factChain(context, fact)),
|
|
1609
|
+
factsTruncated: chain.truncated,
|
|
1610
|
+
conflicts: conflicts
|
|
1611
|
+
.filter((conflict) => String(conflict.subject ?? '') === key)
|
|
1612
|
+
.map((conflict) => ({ predicate: conflict.predicate, subject: conflict.subject, sides: conflict.sides.map((side) => ({ fact: side.fact ?? null, value: side.value ?? null })) })),
|
|
1613
|
+
history: [],
|
|
1614
|
+
actions: { canFilter: true, canExpand: true, canEdit: false },
|
|
1615
|
+
note: '实例由断言投影出来,不单独注册、也不做实体消解(同名即同节点);它没有自己的版本史。',
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
/** 字面值节点:一个取值,以及是谁写下它的。 */
|
|
1620
|
+
function inspectLiteral(context, key) {
|
|
1621
|
+
const { lexicon, facts } = context
|
|
1622
|
+
const separator = key.indexOf(':')
|
|
1623
|
+
if (separator < 0) return null
|
|
1624
|
+
const predicateId = key.slice(0, separator)
|
|
1625
|
+
const objectPart = key.slice(separator + 1)
|
|
1626
|
+
const holders = []
|
|
1627
|
+
for (const fact of facts) {
|
|
1628
|
+
for (const assertion of Array.isArray(fact.assertions) ? fact.assertions : []) {
|
|
1629
|
+
if (String(assertion?.predicate ?? '') !== predicateId) continue
|
|
1630
|
+
if (objectKey(assertion.object) !== objectPart) continue
|
|
1631
|
+
holders.push({ fact, assertion })
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
if (holders.length === 0) return null
|
|
1635
|
+
const predicate = lexicon.predicates.find((item) => item.id === predicateId) ?? null
|
|
1636
|
+
const touchedFacts = [...new Set(holders.map((item) => item.fact))]
|
|
1637
|
+
const chain = cut(touchedFacts)
|
|
1638
|
+
return {
|
|
1639
|
+
selection: { kind: 'literal', id: key, label: say(lexicon, holders[0].assertion) },
|
|
1640
|
+
definition: {
|
|
1641
|
+
id: key,
|
|
1642
|
+
predicate: predicateId,
|
|
1643
|
+
predicateLabel: predicate?.label ?? predicateId,
|
|
1644
|
+
form: holders[0].assertion?.object?.kind ?? null,
|
|
1645
|
+
value: holders[0].assertion?.object?.value ?? null,
|
|
1646
|
+
unit: holders[0].assertion?.object?.unit ?? null,
|
|
1647
|
+
/** 值形态决定「这个取值是什么东西」:数值 / 公式 / 代码路径 / 引用。 */
|
|
1648
|
+
status: 'projected',
|
|
1649
|
+
},
|
|
1650
|
+
relations: { holders: holders.length, predicateRange: predicate?.range ?? null },
|
|
1651
|
+
facts: chain.rows.map((fact) => factChain(context, fact)),
|
|
1652
|
+
factsTruncated: chain.truncated,
|
|
1653
|
+
conflicts: [],
|
|
1654
|
+
history: [],
|
|
1655
|
+
actions: { canFilter: true, canExpand: true, canEdit: false },
|
|
1656
|
+
note: '字面值是断言里的客体,由事实投影出来;它没有独立生命周期。',
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
/** 断言边:一条事实边的完整链(图上点击一条边时看的就是它)。 */
|
|
1661
|
+
function inspectEdge(context, edgeId) {
|
|
1662
|
+
const { lexicon, facts, conflicts } = context
|
|
1663
|
+
const parts = edgeId.split(':')
|
|
1664
|
+
if (parts[0] !== 'assertion' || parts.length < 4) return null
|
|
1665
|
+
const factId = parts[1]
|
|
1666
|
+
const predicateId = parts[2]
|
|
1667
|
+
/** 主语键里有 `|`,而它自己可能带 `:`——所以从第 4 段起重新拼回去(不按段数硬切)。 */
|
|
1668
|
+
const subjectKey = parts.slice(3).join(':')
|
|
1669
|
+
const fact = facts.find((item) => String(item.id ?? '') === factId)
|
|
1670
|
+
if (fact === undefined) return null
|
|
1671
|
+
const predicate = lexicon.predicates.find((item) => item.id === predicateId) ?? null
|
|
1672
|
+
const chain = factChain(context, fact)
|
|
1673
|
+
const separator = subjectKey.indexOf('|')
|
|
1674
|
+
return {
|
|
1675
|
+
selection: { kind: 'edge', id: edgeId, label: `${predicate?.label ?? predicateId} · ${fact.text ?? ''}`.slice(0, 120) },
|
|
1676
|
+
definition: {
|
|
1677
|
+
kind: 'assertion',
|
|
1678
|
+
predicate: predicateId,
|
|
1679
|
+
predicateLabel: predicate?.label ?? predicateId,
|
|
1680
|
+
predicateGloss: predicate?.gloss ?? null,
|
|
1681
|
+
domain: predicate?.domain ?? null,
|
|
1682
|
+
range: predicate?.range ?? null,
|
|
1683
|
+
functional: predicate?.functional === true,
|
|
1684
|
+
subject: separator < 0 ? { type: null, id: subjectKey } : { type: subjectKey.slice(0, separator), id: subjectKey.slice(separator + 1) },
|
|
1685
|
+
},
|
|
1686
|
+
/** 一条边就是一条事实:**链在 `facts[0]` 里**,读面不把它拆成两处说。 */
|
|
1687
|
+
facts: [chain],
|
|
1688
|
+
factsTruncated: 0,
|
|
1689
|
+
conflicts: chain.conflicts,
|
|
1690
|
+
history: chain.history,
|
|
1691
|
+
actions: { canFilter: true, canExpand: true, canEdit: false },
|
|
1692
|
+
note: predicate?.functional === true ? '这条边落在单值谓词上:同一主词出现第二个不同取值时会产生冲突(只暴露,不裁决)。' : null,
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
/** 五种值形态的名字与一句话解释(名字取自 `domain-language` 的枚举,这里只加给人读的说明)。 */
|
|
1697
|
+
const VALUE_FORM_GLOSS = {
|
|
1698
|
+
statement: '短陈述字符串',
|
|
1699
|
+
quantity: '数值 + 单位',
|
|
1700
|
+
formula: '公式源码(LaTeX;本版不做语义解析)',
|
|
1701
|
+
code: '指向工作区里真有的文件路径',
|
|
1702
|
+
reference: '外部引用(文献 / URL / 编号)',
|
|
1703
|
+
}
|
|
1704
|
+
|
|
978
1705
|
export function derive(state) {
|
|
979
1706
|
const activePlan = state.plans.find((plan) => plan.status === 'active') ?? null
|
|
980
1707
|
const closedPlans = state.plans.filter((plan) => plan.status === 'closed')
|
|
@@ -987,6 +1714,13 @@ export function derive(state) {
|
|
|
987
1714
|
* 读的是事实上的 `review`——人的动作落在事实那一侧,facts 是唯一事实源。
|
|
988
1715
|
*/
|
|
989
1716
|
const retractedClaims = new Set((state.facts ?? []).filter((item) => item.review?.decision === 'retracted').map((item) => String(item.text ?? '')))
|
|
1717
|
+
/** 同一件事,新的关联方式:事实带 `hypothesis` 时按 id 撤(文本只是旧账本的退路)。 */
|
|
1718
|
+
const retractedHypotheses = new Set(
|
|
1719
|
+
(state.facts ?? [])
|
|
1720
|
+
.filter((item) => item.review?.decision === 'retracted')
|
|
1721
|
+
.map((item) => item.hypothesis)
|
|
1722
|
+
.filter((id) => typeof id === 'string' && id !== ''),
|
|
1723
|
+
)
|
|
990
1724
|
const hypotheses = state.hypotheses.map((hypothesis) => {
|
|
991
1725
|
const rows = []
|
|
992
1726
|
for (const item of state.evidence) {
|
|
@@ -1000,7 +1734,7 @@ export function derive(state) {
|
|
|
1000
1734
|
.reduce((best, item) => Math.max(best, levelIndex(item.level)), -1)
|
|
1001
1735
|
let status = hypothesis.status
|
|
1002
1736
|
// 撤回优先于一切:那是人对已升格事实的裁决,后来的支持证据不复活它。
|
|
1003
|
-
if (retractedClaims.has(String(hypothesis.claim ?? ''))) status = 'retracted'
|
|
1737
|
+
if (retractedHypotheses.has(hypothesis.id) || retractedClaims.has(String(hypothesis.claim ?? ''))) status = 'retracted'
|
|
1004
1738
|
else if (status === 'proposed' && rows.length > 0) status = 'alive'
|
|
1005
1739
|
// 被推翻是黏性终态:「已被替代」不该改写「已被推翻」——两件事。
|
|
1006
1740
|
if (status !== 'superseded' && status !== 'refuted' && status !== 'retracted' && refutations > 0) status = 'refuted'
|
|
@@ -1053,6 +1787,11 @@ export function derive(state) {
|
|
|
1053
1787
|
* · **升格算数**:回落口径里,被升格成事实的假设也算「已确认」——升格本来就是它最硬的确认。
|
|
1054
1788
|
*/
|
|
1055
1789
|
const promotedClaims = new Set((state.facts ?? []).map((item) => String(item.text ?? '')))
|
|
1790
|
+
const promotedHypotheses = new Set(
|
|
1791
|
+
(state.facts ?? [])
|
|
1792
|
+
.map((item) => item.hypothesis)
|
|
1793
|
+
.filter((id) => typeof id === 'string' && id !== ''),
|
|
1794
|
+
)
|
|
1056
1795
|
const live = activePlan?.steps.filter((step) => step.status !== 'void') ?? []
|
|
1057
1796
|
if (state.goal?.status === 'achieved') progress = 1
|
|
1058
1797
|
else if (state.goal?.status === 'abandoned') progress = 0
|
|
@@ -1060,7 +1799,7 @@ export function derive(state) {
|
|
|
1060
1799
|
else {
|
|
1061
1800
|
const effective = hypotheses.filter((item) => item.status !== 'superseded' && item.status !== 'retracted')
|
|
1062
1801
|
if (effective.length > 0) {
|
|
1063
|
-
const done = effective.filter((item) => item.status === 'confirmed' || promotedClaims.has(String(item.claim ?? ''))).length
|
|
1802
|
+
const done = effective.filter((item) => item.status === 'confirmed' || promotedHypotheses.has(item.id) || promotedClaims.has(String(item.claim ?? ''))).length
|
|
1064
1803
|
progress = done / effective.length
|
|
1065
1804
|
}
|
|
1066
1805
|
}
|
|
@@ -1196,9 +1935,23 @@ export function derive(state) {
|
|
|
1196
1935
|
* · `review` ——人已经审查过(撤回 / 维持),决定连缘由一起留着。
|
|
1197
1936
|
*/
|
|
1198
1937
|
const factRows = (state.facts ?? []).map((fact) => {
|
|
1199
|
-
const
|
|
1938
|
+
const linked = typeof fact.hypothesis === 'string' && fact.hypothesis !== ''
|
|
1939
|
+
const owner = hypotheses.find((item) => (linked ? item.id === fact.hypothesis : String(item.claim ?? '') === String(fact.text ?? '')))
|
|
1200
1940
|
return { ...fact, refuted: (owner?.refutations ?? 0) > 0, review: fact.review ?? null }
|
|
1201
1941
|
})
|
|
1942
|
+
|
|
1943
|
+
/**
|
|
1944
|
+
* **领域词汇的派生读数**(两条,都不新存东西):
|
|
1945
|
+
* · `conflicts`——同一个单值谓词、同一主体、两个不同客体的**成对**事实。它是读数,
|
|
1946
|
+
* 不是裁决:这里不撤回任何一侧,也不判断哪条为真(那是证据与人的事)。
|
|
1947
|
+
* · `lexiconHealth`——悬空引用、父链成环、没人用的条目、被废止条目仍在使用。
|
|
1948
|
+
* 全是提示,不拦任何操作。
|
|
1949
|
+
* 两者都吃 `factRows` 而不是 `state.facts`:事实的复核态与推翻标记是派生的,
|
|
1950
|
+
* 在这里重算一遍就等于第二份判据。
|
|
1951
|
+
*/
|
|
1952
|
+
const lexicon = normalizeLexicon(state.lexicon)
|
|
1953
|
+
const conflicts = deriveConflicts(factRows, lexicon)
|
|
1954
|
+
const lexiconIssues = lexiconHealth(lexicon, factRows)
|
|
1202
1955
|
for (const fact of factRows) {
|
|
1203
1956
|
/**
|
|
1204
1957
|
* **推翻证据只标记事实,撤不撤由人定**(数据本身也可能是错的)。
|
|
@@ -1232,7 +1985,28 @@ export function derive(state) {
|
|
|
1232
1985
|
}
|
|
1233
1986
|
})
|
|
1234
1987
|
|
|
1235
|
-
return {
|
|
1988
|
+
return {
|
|
1989
|
+
phase,
|
|
1990
|
+
progress,
|
|
1991
|
+
hypotheses,
|
|
1992
|
+
activePlan,
|
|
1993
|
+
closedPlans,
|
|
1994
|
+
pendingAudit,
|
|
1995
|
+
forks,
|
|
1996
|
+
factRows,
|
|
1997
|
+
settlement,
|
|
1998
|
+
stepOf,
|
|
1999
|
+
inbox,
|
|
2000
|
+
hasOpenGate,
|
|
2001
|
+
planConfirmationPending,
|
|
2002
|
+
planIsAuthorized,
|
|
2003
|
+
/** 领域词汇与它的两条派生读数(见上面那段:冲突与健康度都只是读数)。 */
|
|
2004
|
+
lexicon,
|
|
2005
|
+
conflicts,
|
|
2006
|
+
lexiconIssues,
|
|
2007
|
+
/** 知识模式(分诊)与缺口读数:结构判据,不猜词面(见 `deriveKnowledge`)。 */
|
|
2008
|
+
knowledge: deriveKnowledge(state, hypotheses, factRows, lexicon),
|
|
2009
|
+
}
|
|
1236
2010
|
}
|
|
1237
2011
|
|
|
1238
2012
|
/**
|
|
@@ -1266,7 +2040,24 @@ export function derive(state) {
|
|
|
1266
2040
|
*/
|
|
1267
2041
|
export const HUMAN_GATE_MARK = '[clearai·人门]'
|
|
1268
2042
|
/** 面板上允许出现的动词。表外的动词一律拒(与贡献表同一套「表外的名字不许出现」)。 */
|
|
1269
|
-
export const HUMAN_GATE_ACTIONS = [
|
|
2043
|
+
export const HUMAN_GATE_ACTIONS = [
|
|
2044
|
+
'adopt_branch',
|
|
2045
|
+
'abandon_fork',
|
|
2046
|
+
'promote_skill',
|
|
2047
|
+
'retract_fact',
|
|
2048
|
+
'keep_fact',
|
|
2049
|
+
'confirm_provisional',
|
|
2050
|
+
/**
|
|
2051
|
+
* **本体四动词(人的通道)**:面板抽屉发的就是它们;模型有同名语义的工具
|
|
2052
|
+
* (`RegisterTerm` 等),但这两个面落的是**同一套判据与同一本账**——判据在宿主半的
|
|
2053
|
+
* `domain` 门里,词汇事件只有一种折法。这个数组在内核那一侧有一份**逐字镜像**
|
|
2054
|
+
* (预设面不能 import 这一层),两边相等由 authority-boundary 套件钉死。
|
|
2055
|
+
*/
|
|
2056
|
+
'register_term',
|
|
2057
|
+
'register_predicate',
|
|
2058
|
+
'revise_term',
|
|
2059
|
+
'deprecate_entry',
|
|
2060
|
+
]
|
|
1270
2061
|
/** 运行档的两个取值。**只用于读取旧日志**里的 `set_autonomy` 记录;当前没有写入口。 */
|
|
1271
2062
|
export const AUTONOMY_VALUES = ['attended', 'unattended']
|
|
1272
2063
|
|
|
@@ -1370,6 +2161,29 @@ export function view(state, sessionId) {
|
|
|
1370
2161
|
brain: state.brain ?? null,
|
|
1371
2162
|
/** 本体形状(面板页眉据此生成,不手抄)。 */
|
|
1372
2163
|
ontology: state.ontology ?? null,
|
|
2164
|
+
/**
|
|
2165
|
+
* **领域词汇的读面**:概念、谓词、冲突、健康读数与图投影。
|
|
2166
|
+
*
|
|
2167
|
+
* 客户端不重算——它连折法都读不到(浏览器那半是独立模块),所以图在这里算好推下去。
|
|
2168
|
+
* 于是「同一份账本 ⇒ 同一张图」是投影的性质,不是两处渲染约定出来的巧合。
|
|
2169
|
+
*/
|
|
2170
|
+
lexicon: {
|
|
2171
|
+
terms: derived.lexicon.terms,
|
|
2172
|
+
predicates: derived.lexicon.predicates,
|
|
2173
|
+
conflicts: derived.conflicts,
|
|
2174
|
+
health: derived.lexiconIssues,
|
|
2175
|
+
graph: graphProjection(state),
|
|
2176
|
+
},
|
|
2177
|
+
/**
|
|
2178
|
+
* **知识模式(分诊)**:面板据此把「这一格是知识主场还是普通进展」说清楚,
|
|
2179
|
+
* 与卡片读的是同一份派生(判据只有一处:`deriveKnowledge`)。
|
|
2180
|
+
*/
|
|
2181
|
+
knowledge: derived.knowledge,
|
|
2182
|
+
/**
|
|
2183
|
+
* **知识预检**:进入知识模式那一刻的相关已知(词汇 / 事实 / 冲突,有界)。
|
|
2184
|
+
* 面板可以画它,内核的 pre-step 把它送进卡——判据与缺口读数一样,只有这一处。
|
|
2185
|
+
*/
|
|
2186
|
+
preflight: knowledgePreflight(state, derived),
|
|
1373
2187
|
/**
|
|
1374
2188
|
* 续跑窗口的账:面板读它,于是「续跑停着——等裁决」这种**平台说不出来的话**
|
|
1375
2189
|
* 有地方说。轮数与相位仍然只在原生 dock 上出现(一个事实只在**一块**面上说),
|
|
@@ -1413,6 +2227,10 @@ export function view(state, sessionId) {
|
|
|
1413
2227
|
/** 从没被走过的等级(派生):面板据此说清「这一级是跳上来的」。 */
|
|
1414
2228
|
untouchedLevels: hypothesis.untouchedLevels,
|
|
1415
2229
|
version: hypothesis.version,
|
|
2230
|
+
/** 断言随假设走(未升格):面板同画芯片(chip 同样在投影侧算好),标注「未升格」。 */
|
|
2231
|
+
assertions: Array.isArray(hypothesis.assertions)
|
|
2232
|
+
? hypothesis.assertions.map((assertion) => ({ ...assertion, chip: formatAssertion(state.lexicon, assertion) }))
|
|
2233
|
+
: null,
|
|
1416
2234
|
})),
|
|
1417
2235
|
},
|
|
1418
2236
|
/**
|
|
@@ -1542,6 +2360,11 @@ export function view(state, sessionId) {
|
|
|
1542
2360
|
evidenceIds: item.evidence,
|
|
1543
2361
|
path: item.path,
|
|
1544
2362
|
at: item.at,
|
|
2363
|
+
/**
|
|
2364
|
+
* **类型化断言**(可 null):事实的内容形态。面板用它画断言芯片——
|
|
2365
|
+
* 点开就地展开词条卡,这是「事实 ↔ 本体」那座桥的界面侧。
|
|
2366
|
+
*/
|
|
2367
|
+
assertions: Array.isArray(item.assertions) ? item.assertions : null,
|
|
1545
2368
|
/** 派生:收到过推翻证据(要复核)与人的审查决定(撤回 / 维持)。 */
|
|
1546
2369
|
refuted: item.refuted === true,
|
|
1547
2370
|
review: item.review ?? null,
|
|
@@ -1681,10 +2504,48 @@ export function renderCard(state) {
|
|
|
1681
2504
|
lines.push(` · ${hypothesis.id} [${hypothesis.status}] ${hypothesis.claim} — 推翻条件:${hypothesis.refute_when}${readings}`)
|
|
1682
2505
|
}
|
|
1683
2506
|
}
|
|
2507
|
+
/**
|
|
2508
|
+
* **知识模式与缺口**:这一行是「分诊」在模型侧的全部可见面。
|
|
2509
|
+
*
|
|
2510
|
+
* 普通任务**一个字都不加**(提示词与卡片同一条零成本纪律:不立约的会话不长出新字节)。
|
|
2511
|
+
* 进了这一档才报,而且报的是**缺口 + 指得出动作的读数**,不是纪律条文——
|
|
2512
|
+
* 真跑里模型跳过本体步骤不是因为不知道有这件工具,而是因为完成函数里没有它;
|
|
2513
|
+
* 缺口逐回合摆在卡上,那件事才进得了它的视野。
|
|
2514
|
+
*/
|
|
2515
|
+
if (derived.knowledge.mode === 'knowledge') {
|
|
2516
|
+
lines.push(`- 知识模式:${derived.knowledge.why}`)
|
|
2517
|
+
/**
|
|
2518
|
+
* **知识预检**:与缺口同一拍出现的相关已知。
|
|
2519
|
+
*
|
|
2520
|
+
* 它解决的是「模型不查就开工」——相关性判断在这里做完(词面命中,逐条可复核),
|
|
2521
|
+
* 模型拿到的是**可以直接引用的 id 清单**,而不是一句「去查 QueryKnowledge」。
|
|
2522
|
+
* 没有命中时如实说「没找到相关的」,不把空读数写成「世上没有」。
|
|
2523
|
+
*/
|
|
2524
|
+
const preflight = knowledgePreflight(state, derived)
|
|
2525
|
+
if (preflight !== null) {
|
|
2526
|
+
if (preflight.terms.length > 0 || preflight.predicates.length > 0) {
|
|
2527
|
+
const more = (count) => (count > 0 ? `(还有 ${count} 个未列出)` : '')
|
|
2528
|
+
lines.push(
|
|
2529
|
+
` · 相关已知(词面命中,可直接引用):概念 ${preflight.terms.map((term) => `${term.id}${term.label === term.id ? '' : `(${term.label})`}`).join('、') || '无'}${more(preflight.termsTruncated)};谓词 ${preflight.predicates.map((predicate) => `${predicate.id}${predicate.label === predicate.id ? '' : `(${predicate.label})`}`).join('、') || '无'}${more(preflight.predicatesTruncated)}`,
|
|
2530
|
+
)
|
|
2531
|
+
if (preflight.facts.length > 0) {
|
|
2532
|
+
lines.push(` · 可复用事实 ${preflight.facts.length} 条:${preflight.facts.map((fact) => `${fact.id}(${fact.level ?? '?'})`).join('、')}${preflight.factsTruncated > 0 ? `(还有 ${preflight.factsTruncated} 条未列出)` : ''}`)
|
|
2533
|
+
}
|
|
2534
|
+
} else {
|
|
2535
|
+
lines.push(' · 相关已知:当前主张文本没有命中任何已有概念 / 谓词——要写断言就先立词(带依据),别把查不到当成不存在')
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
if (derived.knowledge.gaps.length === 0) {
|
|
2539
|
+
lines.push(' · 结构完整:语言、断言的命题、带断言的已升格事实、证据覆盖这四项今天都不欠')
|
|
2540
|
+
} else {
|
|
2541
|
+
const total = derived.knowledge.gaps.reduce((sum, gap) => sum + gap.count, 0)
|
|
2542
|
+
lines.push(` · 缺口 ${total} 条(${derived.knowledge.gaps.map((gap) => `${gap.code} ${gap.count}`).join(' · ')}):`)
|
|
2543
|
+
for (const gap of derived.knowledge.gaps) lines.push(` - ${gap.detail}`)
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
1684
2546
|
if (plan === null) {
|
|
1685
2547
|
lines.push('- 当前计划:无活动计划')
|
|
1686
|
-
} else {
|
|
1687
|
-
if (derived.planConfirmationPending) {
|
|
2548
|
+
} else { if (derived.planConfirmationPending) {
|
|
1688
2549
|
/**
|
|
1689
2550
|
* 授权记号的语义:它是**归属**,不是闸门。
|
|
1690
2551
|
*
|
|
@@ -1761,6 +2622,27 @@ export function renderCard(state) {
|
|
|
1761
2622
|
lines.push(`- 最近一条证据:${last.id} ${last.verdict}(${last.evaluator} · ${last.level})`)
|
|
1762
2623
|
}
|
|
1763
2624
|
if (state.facts.length > 0) lines.push(`- 已升格事实:${state.facts.length} 条`)
|
|
2625
|
+
/**
|
|
2626
|
+
* **领域词汇与类型化事实**的一句话读数。
|
|
2627
|
+
*
|
|
2628
|
+
* 为什么要进卡片:模型要靠它决定「这条结论能不能写成断言、要不要先登记词」。而**冲突**
|
|
2629
|
+
* 必须说出来、但不许自动处置——卡片只报数并指向那两条事实,撤不撤由人(或由证据)定。
|
|
2630
|
+
* 事实没有断言时也报一句:那是「这门语言还没用起来」的读数,不是错误。
|
|
2631
|
+
*/
|
|
2632
|
+
{
|
|
2633
|
+
const lexicon = derived.lexicon
|
|
2634
|
+
const typed = state.facts.filter((fact) => Array.isArray(fact.assertions) && fact.assertions.length > 0).length
|
|
2635
|
+
if (lexicon.terms.length > 0 || lexicon.predicates.length > 0) {
|
|
2636
|
+
lines.push(`- 领域词汇:${lexicon.terms.length} 个概念 · ${lexicon.predicates.length} 个谓词;已升格事实里 ${typed}/${state.facts.length} 条带类型化断言`)
|
|
2637
|
+
if (derived.lexiconIssues.some((issue) => issue.severity === 'warning')) lines.push(` · 词汇健康度有 ${derived.lexiconIssues.filter((issue) => issue.severity === 'warning').length} 条待看(悬空引用 / 成环;在面板「本体」里)`)
|
|
2638
|
+
}
|
|
2639
|
+
if (derived.conflicts.length > 0) {
|
|
2640
|
+
for (const conflict of derived.conflicts) {
|
|
2641
|
+
const sides = conflict.sides.map((side) => `${side.fact ?? '?'}(${side.value})`).join(' 对 ')
|
|
2642
|
+
lines.push(`- **冲突(${conflict.predicate} · ${conflict.subject})**:${sides}——两条都还没被撤回。它是读数不是裁决:要么用证据推翻一侧,要么由人撤回一侧;系统不替你选。`)
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
1764
2646
|
// 结案时留的痕:未判的假设不是「没问题」,是「没看过」——结案之后也要看得见。
|
|
1765
2647
|
if (Array.isArray(goal?.unjudged) && goal.unjudged.length > 0) lines.push(`- 结案留痕:有 ${goal.unjudged.length} 条假设没有被任何证据触及(${goal.unjudged.join(', ')})`)
|
|
1766
2648
|
/**
|