clearai-dsh 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.md +6 -6
- package/README.zh-CN.md +6 -6
- package/bin/clearai.mjs +29 -231
- package/cordis.patch.yml +3 -3
- package/lib/client.js +4 -3
- package/lib/domain-language.js +71 -9
- package/lib/fold.js +109 -14
- package/lib/knowledge-view.js +85 -26
- package/package.json +2 -2
- package/presets/clearai/agent.cordis.yml +5 -2
- package/presets/clearai/clearai.patch.yml +5 -2
- package/presets/clearai/plugins/clearai-kernel.js +578 -60
- package/presets/clearai/plugins/ontology.js +21 -7
- package/presets/clearai/plugins/prompts.js +18 -16
- package/presets/clearai/skills/clearai-loop/SKILL.md +12 -4
package/lib/domain-language.js
CHANGED
|
@@ -1026,12 +1026,17 @@ export function describeDomainGraph(lexicon) {
|
|
|
1026
1026
|
//
|
|
1027
1027
|
// 会话账本只活在一次会话里,而研究要跨会话攒下来。所以「攒下来的东西」住在工作区的文件里:
|
|
1028
1028
|
// · `clear/knowledge/facts/<id>.json`:升格的事实,一条一个文件,只有系统写;
|
|
1029
|
+
// · `clear/knowledge/lessons/<id>.json`:结案时经独立评估核过的经验,一条一个文件,只有系统写;
|
|
1029
1030
|
// · `clear/ontology/{concepts,relations,entities}/**.json`:本体,模型用原生文件工具直接写。
|
|
1030
1031
|
// 内核每一拍把这两处的变化折成一条 `workspace/synced` 变更,投影从文件内容派生词汇、实体与
|
|
1031
1032
|
// 事实——折法仍然只吃账本,文件是账本之外唯一的输入,而且进账本时就是一条可重放的事实。
|
|
1032
1033
|
|
|
1033
1034
|
/** 事实文件所在的目录(相对工作区)。 */
|
|
1034
1035
|
export const FACTS_DIR = 'clear/knowledge/facts'
|
|
1036
|
+
/** 经验文件所在的目录(相对工作区)。 */
|
|
1037
|
+
export const LESSONS_DIR = 'clear/knowledge/lessons'
|
|
1038
|
+
/** 经验的四类:坑、要核的读数、会骗人的捷径、先验。 */
|
|
1039
|
+
export const LESSON_KINDS = ['trap', 'check', 'shortcut', 'prior']
|
|
1035
1040
|
/** 本体文件树的根(相对工作区)。 */
|
|
1036
1041
|
export const ONTOLOGY_DIR = 'clear/ontology'
|
|
1037
1042
|
/** 本体文件树的三支:目录名 → 文件种类。 */
|
|
@@ -1056,7 +1061,11 @@ function hashText(value) {
|
|
|
1056
1061
|
*/
|
|
1057
1062
|
export function definitionFingerprint(entry) {
|
|
1058
1063
|
if (!isPlainObject(entry)) return null
|
|
1059
|
-
|
|
1064
|
+
/** 单位与形状只在写了时进指纹:没写这两格的旧词条,指纹与从前逐字相同。 */
|
|
1065
|
+
const meaning =
|
|
1066
|
+
'range' in entry || 'domain' in entry
|
|
1067
|
+
? { domain: text(entry.domain), range: isPlainObject(entry.range) ? { term: text(entry.range.term), form: text(entry.range.form), unit: text(entry.range.unit) } : null, functional: entry.functional === true, ...(text(entry.shape) === '' ? {} : { shape: text(entry.shape) }) }
|
|
1068
|
+
: { gloss: text(entry.gloss), parent: text(entry.parent), ...(text(entry.unit) === '' ? {} : { unit: text(entry.unit) }) }
|
|
1060
1069
|
return `fnv:${hashText(JSON.stringify(meaning))}`
|
|
1061
1070
|
}
|
|
1062
1071
|
|
|
@@ -1114,6 +1123,8 @@ export function classifyWorkspacePath(path) {
|
|
|
1114
1123
|
const id = parts.at(-1).slice(0, -'.json'.length)
|
|
1115
1124
|
const facts = FACTS_DIR.split('/')
|
|
1116
1125
|
if (parts.length === facts.length + 1 && facts.every((part, index) => parts[index] === part)) return { kind: 'fact', id, dirs: [] }
|
|
1126
|
+
const lessons = LESSONS_DIR.split('/')
|
|
1127
|
+
if (parts.length === lessons.length + 1 && lessons.every((part, index) => parts[index] === part)) return { kind: 'lesson', id, dirs: [] }
|
|
1117
1128
|
const root = ONTOLOGY_DIR.split('/')
|
|
1118
1129
|
if (parts.length >= root.length + 2 && root.every((part, index) => parts[index] === part)) {
|
|
1119
1130
|
const kind = ONTOLOGY_BRANCHES[parts[root.length]]
|
|
@@ -1145,6 +1156,26 @@ export function factFromFile(data, path) {
|
|
|
1145
1156
|
}
|
|
1146
1157
|
}
|
|
1147
1158
|
|
|
1159
|
+
/**
|
|
1160
|
+
* 经验文件 → 经验行(与 `lesson/recorded` 折出来的同形)。`status: "retracted"` 的不再推送。
|
|
1161
|
+
* 经验改变的是注意力与做法(「这台装置的温度要拿参考读数核」),事实改变的是信念。
|
|
1162
|
+
*/
|
|
1163
|
+
export function lessonFromFile(data, path) {
|
|
1164
|
+
if (!isPlainObject(data) || text(data.id) === '' || text(data.text) === '') return null
|
|
1165
|
+
return {
|
|
1166
|
+
id: text(data.id),
|
|
1167
|
+
goal: data.goal ?? null,
|
|
1168
|
+
text: String(data.text),
|
|
1169
|
+
kind: LESSON_KINDS.includes(data.kind) ? data.kind : 'trap',
|
|
1170
|
+
about: Array.isArray(data.about) ? data.about.map(String).filter((item) => item.trim() !== '').slice(0, 6) : [],
|
|
1171
|
+
evidence: typeof data.evidence === 'string' ? data.evidence : null,
|
|
1172
|
+
boundary: typeof data.boundary === 'string' ? data.boundary : null,
|
|
1173
|
+
status: data.status === 'retracted' ? 'retracted' : 'active',
|
|
1174
|
+
path: path ?? null,
|
|
1175
|
+
at: typeof data.at === 'number' ? data.at : null,
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1148
1179
|
// ═══ 本体文件树:字段、单文件校验、从文件折出词汇与实体 ═════════════════════════
|
|
1149
1180
|
//
|
|
1150
1181
|
// 本体由模型用原生文件工具直接写,住在 `clear/ontology/{concepts,relations,entities}/` 下:
|
|
@@ -1164,6 +1195,14 @@ const ONTOLOGY_STATUS = ['active', 'deprecated']
|
|
|
1164
1195
|
* 三种本体文件的字段定义。内核把它铺成 `clear/ontology/SCHEMA.json`(只读),
|
|
1165
1196
|
* 写入时的校验与这份定义是同一份:字段在这里改,两边一起变。
|
|
1166
1197
|
*/
|
|
1198
|
+
/**
|
|
1199
|
+
* **本体只留能产出预期的东西**:概念分三类,度量必须写单位(释义就是口径);关系分四类,
|
|
1200
|
+
* 「影响」带大致形状——预期就从这里来(「温度升,杂质单调升」);「测量」写清怎么核读数。
|
|
1201
|
+
*/
|
|
1202
|
+
export const CONCEPT_KINDS = ['category', 'measure', 'phenomenon']
|
|
1203
|
+
export const RELATION_KINDS = ['affects', 'defines', 'measures', 'manifests_as']
|
|
1204
|
+
export const RELATION_SHAPES = ['increasing', 'decreasing', 'peak', 'threshold', 'coupled']
|
|
1205
|
+
|
|
1167
1206
|
export const ONTOLOGY_SCHEMA = bilingual({
|
|
1168
1207
|
about: [
|
|
1169
1208
|
'本体文件的字段定义(系统铺设,只读)。命名规则:X.json 描述节点 X,它的子节点放在同级的 X/ 目录里;id 必须等于文件名。概念目录嵌套 = is_a;实体目录嵌套 = 组成 / 属于;关系可以平铺,也可以分目录(只是分组)。引用一律只写 id,挪目录不断引用。写入时只查单个文件;引用断了、类型对不上等跨文件问题在卡片上提示,升格时才拦。',
|
|
@@ -1174,9 +1213,11 @@ export const ONTOLOGY_SCHEMA = bilingual({
|
|
|
1174
1213
|
required: {
|
|
1175
1214
|
id: ['slug:小写字母开头,字母/数字/下划线,≤40,等于文件名', 'slug: starts with a lowercase letter; letters, digits, underscores; ≤40; equals the file name'],
|
|
1176
1215
|
label: ['给人看的名字', 'the human-readable name'],
|
|
1177
|
-
gloss: ['
|
|
1216
|
+
gloss: ['一句话释义:它指什么;度量(良率、收率……)写明口径,也就是怎么算', 'one-sentence gloss: what it refers to; for a measure (yield, conversion …) state its definition, that is, how it is computed'],
|
|
1178
1217
|
},
|
|
1179
1218
|
optional: {
|
|
1219
|
+
kind: ['category = 类别(反应器、催化剂);measure = 度量(收率、杂质,要写 unit);phenomenon = 现象(漂移、失活)', 'category (reactor, catalyst); measure (yield, impurity; needs unit); phenomenon (drift, deactivation)'],
|
|
1220
|
+
unit: ['单位(度量必填,无量纲写 "1")', 'unit (required for a measure; "1" if dimensionless)'],
|
|
1180
1221
|
aliases: ['别名数组', 'array of aliases'],
|
|
1181
1222
|
basis: ['依据:哪份材料让这个词成立', 'basis: which material establishes this term'],
|
|
1182
1223
|
status: 'active | deprecated',
|
|
@@ -1203,6 +1244,9 @@ export const ONTOLOGY_SCHEMA = bilingual({
|
|
|
1203
1244
|
},
|
|
1204
1245
|
optional: {
|
|
1205
1246
|
gloss: ['一句话释义', 'one-sentence gloss'],
|
|
1247
|
+
kind: ['affects = 影响(一个量怎样随另一个量变);defines = 定义(一个度量怎么算);measures = 测量(哪个仪器测哪个量);manifests_as = 表现为(一个现象在读数上什么样)', 'affects (how one quantity moves with another); defines (how a measure is computed); measures (which instrument reads which quantity); manifests_as (what a phenomenon looks like in the readings)'],
|
|
1248
|
+
shape: [`影响的大致形状:${RELATION_SHAPES.join(' | ')};预期从这里来`, `rough shape of an effect: ${RELATION_SHAPES.join(' | ')}; expectations come from it`],
|
|
1249
|
+
check: ['测量关系:读数怎么核(参考探头、标样、重复点)', 'for a measurement: how to check the reading (reference probe, standard, repeat point)'],
|
|
1206
1250
|
domain: ['主语必须是哪个概念(下位概念也算)', 'which concept the subject must be (subconcepts count)'],
|
|
1207
1251
|
functional: ['true = 单值:同一主语只能有一个取值,两个不同取值会被报成冲突', 'true = single-valued: one value per subject; two different values are reported as a conflict'],
|
|
1208
1252
|
basis: ['依据', 'basis'],
|
|
@@ -1226,6 +1270,7 @@ export const ONTOLOGY_SCHEMA = bilingual({
|
|
|
1226
1270
|
},
|
|
1227
1271
|
optional: {
|
|
1228
1272
|
aliases: ['别名数组', 'array of aliases'],
|
|
1273
|
+
history: ['带日期的经历:[{"at": "YYYY-MM-DD", "what": "校准 / 更换 / 发现漂移", "evidence"?: {"kind", "ref"}}]', 'dated history: [{"at": "YYYY-MM-DD", "what": "calibrated / replaced / drift found", "evidence"?: {"kind", "ref"}}]'],
|
|
1229
1274
|
relations: [
|
|
1230
1275
|
'它对外的关系,每条一个出处:{"predicate": 关系 id, "object": 另一个实体的 id} 或 {"predicate": 关系 id, "value": 字面值, "unit"?: 单位};两种都要带 "evidence": {"kind", "ref"}',
|
|
1231
1276
|
'its outgoing relations, each with a source: {"predicate": relation id, "object": another entity id} or {"predicate": relation id, "value": literal, "unit"?: unit}; both need "evidence": {"kind", "ref"}',
|
|
@@ -1247,10 +1292,11 @@ export const ONTOLOGY_SCHEMA = bilingual({
|
|
|
1247
1292
|
})
|
|
1248
1293
|
|
|
1249
1294
|
const FIELDS = {
|
|
1250
|
-
concept: ['id', 'label', 'gloss', 'aliases', 'basis', 'status', 'replaced_by', 'note'],
|
|
1251
|
-
relation: ['id', 'label', 'gloss', 'domain', 'range', 'functional', 'basis', 'status', 'replaced_by', 'note'],
|
|
1252
|
-
entity: ['id', 'label', 'type', 'basis', 'provenance', 'aliases', 'relations', 'note'],
|
|
1295
|
+
concept: ['id', 'label', 'gloss', 'kind', 'unit', 'aliases', 'basis', 'status', 'replaced_by', 'note'],
|
|
1296
|
+
relation: ['id', 'label', 'gloss', 'kind', 'shape', 'check', 'domain', 'range', 'functional', 'basis', 'status', 'replaced_by', 'note'],
|
|
1297
|
+
entity: ['id', 'label', 'type', 'basis', 'provenance', 'aliases', 'relations', 'history', 'note'],
|
|
1253
1298
|
}
|
|
1299
|
+
|
|
1254
1300
|
const RELATION_FIELDS = ['predicate', 'object', 'value', 'unit', 'evidence', 'note']
|
|
1255
1301
|
|
|
1256
1302
|
/** 一个出处对象的形状问题(没有就返回 null)。 */
|
|
@@ -1269,7 +1315,7 @@ function evidenceShape(value, at) {
|
|
|
1269
1315
|
*/
|
|
1270
1316
|
export function checkOntologyFile(path, content) {
|
|
1271
1317
|
const place = classifyWorkspacePath(path)
|
|
1272
|
-
if (place === null || place.kind === 'fact') return [tr(`不是本体文件的位置:本体文件要放在 ${ONTOLOGY_DIR}/{concepts,relations,entities}/ 下,扩展名 .json`, `Not an ontology file location: ontology files go under ${ONTOLOGY_DIR}/{concepts,relations,entities}/ with a .json extension`)]
|
|
1318
|
+
if (place === null || place.kind === 'fact' || place.kind === 'lesson') return [tr(`不是本体文件的位置:本体文件要放在 ${ONTOLOGY_DIR}/{concepts,relations,entities}/ 下,扩展名 .json`, `Not an ontology file location: ontology files go under ${ONTOLOGY_DIR}/{concepts,relations,entities}/ with a .json extension`)]
|
|
1273
1319
|
let data = content
|
|
1274
1320
|
if (typeof content === 'string') {
|
|
1275
1321
|
try {
|
|
@@ -1298,9 +1344,18 @@ export function checkOntologyFile(path, content) {
|
|
|
1298
1344
|
for (const key of ['basis', 'gloss', 'note', 'replaced_by']) if (data[key] !== undefined && data[key] !== null && typeof data[key] !== 'string') problems.push(tr(`${key} 只能是字符串`, `${key} must be a string`))
|
|
1299
1345
|
if (data.status !== undefined && !ONTOLOGY_STATUS.includes(text(data.status))) problems.push(tr(`status 只能是 ${ONTOLOGY_STATUS.join(' / ')}`, `status must be ${ONTOLOGY_STATUS.join(' / ')}`))
|
|
1300
1346
|
if (kind === 'concept' && text(data.gloss) === '') problems.push(tr('gloss 必填:一句话说清它指什么,不然引用它的人各读各的', 'gloss is required: one sentence on what it refers to, or everyone who cites it reads it differently'))
|
|
1347
|
+
if (kind === 'concept') {
|
|
1348
|
+
if (data.kind !== undefined && !CONCEPT_KINDS.includes(text(data.kind))) problems.push(tr(`kind 只能是 ${CONCEPT_KINDS.join(' / ')}(类别 / 度量 / 现象)`, `kind must be ${CONCEPT_KINDS.join(' / ')}`))
|
|
1349
|
+
if (data.unit !== undefined && typeof data.unit !== 'string') problems.push(tr('unit 只能是字符串', 'unit must be a string'))
|
|
1350
|
+
if (text(data.kind) === 'measure' && text(data.unit) === '') problems.push(tr('度量要写 unit(无量纲写 "1"):口径和单位不写下来,「定义已变」就无从发现', 'a measure needs a unit (write "1" if dimensionless): without the definition and unit written down, a changed definition cannot be noticed'))
|
|
1351
|
+
}
|
|
1301
1352
|
if (kind === 'relation') {
|
|
1302
1353
|
if (data.domain !== undefined && data.domain !== null && !ID_PATTERN.test(text(data.domain))) problems.push(tr('domain 要是一个概念 id', 'domain must be a concept id'))
|
|
1303
1354
|
if (data.functional !== undefined && typeof data.functional !== 'boolean') problems.push(tr('functional 只能是 true / false', 'functional must be true / false'))
|
|
1355
|
+
if (data.kind !== undefined && !RELATION_KINDS.includes(text(data.kind))) problems.push(tr(`kind 只能是 ${RELATION_KINDS.join(' / ')}(影响 / 定义 / 测量 / 表现为)`, `kind must be ${RELATION_KINDS.join(' / ')}`))
|
|
1356
|
+
if (data.shape !== undefined && !RELATION_SHAPES.includes(text(data.shape))) problems.push(tr(`shape 只能是 ${RELATION_SHAPES.join(' / ')}(单调升 / 单调降 / 有峰 / 阈值 / 与别的量耦合)`, `shape must be ${RELATION_SHAPES.join(' / ')}`))
|
|
1357
|
+
if (data.shape !== undefined && data.kind !== undefined && text(data.kind) !== 'affects') problems.push(tr('shape 只给「影响」(kind=affects)的关系', 'shape only applies to an affects relation (kind=affects)'))
|
|
1358
|
+
if (data.check !== undefined && typeof data.check !== 'string') problems.push(tr('check 只能是字符串', 'check must be a string'))
|
|
1304
1359
|
const range = data.range
|
|
1305
1360
|
if (typeof range === 'string') {
|
|
1306
1361
|
if (!ID_PATTERN.test(text(range))) problems.push(tr('range 写成字符串时要是一个概念 id', 'range given as a string must be a concept id'))
|
|
@@ -1319,6 +1374,13 @@ export function checkOntologyFile(path, content) {
|
|
|
1319
1374
|
if (text(data.basis) === '') problems.push(tr('basis 必填:实体是观测,要说清哪份材料让它可以被指认', 'basis is required: an entity is an observation, so say which material identifies it'))
|
|
1320
1375
|
const shape = evidenceShape(data.provenance, 'provenance')
|
|
1321
1376
|
if (shape !== null) problems.push(tr(`${shape}(实体没有出处就进不了图)`, `${shape} (an entity without a source cannot enter the graph)`))
|
|
1377
|
+
if (data.history !== undefined) {
|
|
1378
|
+
if (!Array.isArray(data.history)) problems.push(tr('history 只能是数组', 'history must be an array'))
|
|
1379
|
+
else
|
|
1380
|
+
data.history.forEach((item, index) => {
|
|
1381
|
+
if (!isPlainObject(item) || !/^\d{4}-\d{2}-\d{2}/.test(text(item.at)) || text(item.what) === '') problems.push(tr(`history[${index}] 要是 {"at": "YYYY-MM-DD", "what": "发生了什么"}`, `history[${index}] must be {"at": "YYYY-MM-DD", "what": "what happened"}`))
|
|
1382
|
+
})
|
|
1383
|
+
}
|
|
1322
1384
|
if (data.relations !== undefined) {
|
|
1323
1385
|
if (!Array.isArray(data.relations)) problems.push(tr('relations 只能是数组', 'relations must be an array'))
|
|
1324
1386
|
else
|
|
@@ -1361,7 +1423,7 @@ export function materializeOntology(files) {
|
|
|
1361
1423
|
const flag = (path, id, code, detail, severity = 'warning') => problems.push({ path, id, code, detail, severity })
|
|
1362
1424
|
const entries = Object.entries(isPlainObject(files) ? files : {})
|
|
1363
1425
|
.map(([path, file]) => ({ path, file, place: classifyWorkspacePath(path) }))
|
|
1364
|
-
.filter((item) => item.place !== null && item.place.kind !== 'fact')
|
|
1426
|
+
.filter((item) => item.place !== null && item.place.kind !== 'fact' && item.place.kind !== 'lesson')
|
|
1365
1427
|
.sort((a, b) => (a.path < b.path ? -1 : 1))
|
|
1366
1428
|
const terms = []
|
|
1367
1429
|
const predicates = []
|
|
@@ -1389,9 +1451,9 @@ export function materializeOntology(files) {
|
|
|
1389
1451
|
const status = text(data.status) === 'deprecated' ? 'deprecated' : 'admitted'
|
|
1390
1452
|
const parent = place.dirs.length === 0 ? null : place.dirs.at(-1)
|
|
1391
1453
|
if (place.kind === 'concept') {
|
|
1392
|
-
terms.push({ id: place.id, label: text(data.label), gloss: text(data.gloss), aliases: Array.isArray(data.aliases) ? data.aliases.map(String) : [], parent, status, basis: text(data.basis) || null, replacedBy: text(data.replaced_by) || null, path })
|
|
1454
|
+
terms.push({ id: place.id, label: text(data.label), gloss: text(data.gloss), ...(CONCEPT_KINDS.includes(text(data.kind)) ? { kind: text(data.kind) } : {}), ...(text(data.unit) === '' ? {} : { unit: text(data.unit) }), aliases: Array.isArray(data.aliases) ? data.aliases.map(String) : [], parent, status, basis: text(data.basis) || null, replacedBy: text(data.replaced_by) || null, path })
|
|
1393
1455
|
} else if (place.kind === 'relation') {
|
|
1394
|
-
predicates.push({ id: place.id, label: text(data.label), gloss: text(data.gloss), domain: text(data.domain) || null, range: rangeOf(data.range), functional: data.functional === true, status, basis: text(data.basis) || null, replacedBy: text(data.replaced_by) || null, path })
|
|
1456
|
+
predicates.push({ id: place.id, label: text(data.label), gloss: text(data.gloss), ...(RELATION_KINDS.includes(text(data.kind)) ? { kind: text(data.kind) } : {}), ...(RELATION_SHAPES.includes(text(data.shape)) ? { shape: text(data.shape) } : {}), ...(text(data.check) === '' ? {} : { check: text(data.check) }), domain: text(data.domain) || null, range: rangeOf(data.range), functional: data.functional === true, status, basis: text(data.basis) || null, replacedBy: text(data.replaced_by) || null, path })
|
|
1395
1457
|
} else {
|
|
1396
1458
|
entityRows.push({ place, path, data, parent })
|
|
1397
1459
|
}
|
package/lib/fold.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* 与预设内核的契约:`meta = { kind: 'clearai', v: 1, mutation: { t, ... } }`。
|
|
17
17
|
* 词汇表由本文件的 `applyMutation` 定义;预设侧只负责产出,不负责解释。
|
|
18
18
|
*/
|
|
19
|
-
import { applyLexiconMutation, changedDefinitions, materializeOntology, classifyWorkspacePath, deriveConflicts, emptyLexicon, factFromFile, formatAssertion, graphProjection, lexiconHealth, normalizeLexicon, objectKey, termUsage, VALUE_FORMS } from './domain-language.js'
|
|
19
|
+
import { applyLexiconMutation, changedDefinitions, materializeOntology, classifyWorkspacePath, deriveConflicts, emptyLexicon, factFromFile, formatAssertion, lessonFromFile, graphProjection, lexiconHealth, normalizeLexicon, objectKey, termUsage, VALUE_FORMS } from './domain-language.js'
|
|
20
20
|
import { handleOf, knowledgeView, trustOf } from './knowledge-view.js'
|
|
21
21
|
import { bilingual, detectLanguage, messageText, tr, withLanguage } from './lang.js'
|
|
22
22
|
|
|
@@ -60,9 +60,15 @@ export const MUTATION_KIND = 'clearai'
|
|
|
60
60
|
* `definitionsChanged`。本体(词汇、实体、实体关系)改为从本体文件折出来,跨文件问题记在
|
|
61
61
|
* `ontologyProblems`。旧日志里没有这些,折出来就是空的。
|
|
62
62
|
* v15 → v16:多了 `language`(人说话用的语言,从人的消息折出来):系统写给人的话、面板里的读数都跟着它。
|
|
63
|
+
* v16 → v17:**预期与未解释**:步骤带 `expect`(`step/expected` 也能补写);多了 `anomalies`(未解释项,
|
|
64
|
+
* `anomaly/opened` / `anomaly/resolved` 折进来,评估者报的随 `audit/settled` 一起来);目标带
|
|
65
|
+
* `irreversible`(不可逆动作的命令特征);放行带 `action`。对手判断(`rival` / `split_by`)删了,
|
|
66
|
+
* 旧账里的这两格不再读。
|
|
67
|
+
* v17 → v18:多了 `lessons`(经验:结案时经独立评估核过的「下次要留意什么」,`lesson/recorded` 折进来;
|
|
68
|
+
* 别的会话留下的从 `clear/knowledge/lessons/<id>.json` 经 `workspace/synced` 进来)。
|
|
63
69
|
* 投影缓存按版本判定,所以旧缓存会被丢弃、从日志重折一遍。
|
|
64
70
|
*/
|
|
65
|
-
export const STATE_VERSION =
|
|
71
|
+
export const STATE_VERSION = 18
|
|
66
72
|
|
|
67
73
|
/**
|
|
68
74
|
* **只留台账、不折进视图**的变更类型(词汇表的另一半)。
|
|
@@ -89,6 +95,13 @@ export function emptyState() {
|
|
|
89
95
|
facts: [],
|
|
90
96
|
blocks: {},
|
|
91
97
|
releases: [],
|
|
98
|
+
/**
|
|
99
|
+
* **未解释项**(认识论里的「反常」):和预期或本体对不上的观测。只有三个去处:
|
|
100
|
+
* 被解释(`explained`)、写明理由排除(`ruled_out`)、交给人(`escalated`);没去处的就是开着(`open`)。
|
|
101
|
+
*/
|
|
102
|
+
anomalies: [],
|
|
103
|
+
/** **经验**:本会话结案写下的(别的会话的在工作区文件里,派生时合进来)。 */
|
|
104
|
+
lessons: [],
|
|
92
105
|
/**
|
|
93
106
|
* **本体形状**:内核随投影下发的那份声明(对象/状态/边/等级)。
|
|
94
107
|
* 为什么进投影而不是在界面里手抄:面板那一格的页眉要从**声明**生成——
|
|
@@ -205,6 +218,8 @@ function appendSteps(plan, rawSteps) {
|
|
|
205
218
|
artifacts: raw.artifacts ?? [],
|
|
206
219
|
done_criteria: raw.done_criteria,
|
|
207
220
|
tests: normalizeTests(raw.tests),
|
|
221
|
+
/** 动手前写下的预期(可选);落空的地方记成未解释项。 */
|
|
222
|
+
expect: typeof raw.expect === 'string' && raw.expect !== '' ? raw.expect : null,
|
|
208
223
|
status: 'open',
|
|
209
224
|
evidence: null,
|
|
210
225
|
advancedAt: null,
|
|
@@ -226,6 +241,7 @@ export function applyMutation(state, mutation) {
|
|
|
226
241
|
if (mutation === null || typeof mutation !== 'object' || typeof mutation.t !== 'string') return state
|
|
227
242
|
const at = typeof mutation.at === 'number' ? mutation.at : 0
|
|
228
243
|
const next = clone(state)
|
|
244
|
+
if (!Array.isArray(next.anomalies)) next.anomalies = []
|
|
229
245
|
const RANK = { open: 0, blocked: 0, advanced: 1, void: 1 }
|
|
230
246
|
const settle = (step, status) => {
|
|
231
247
|
if (step === undefined || step === null) return
|
|
@@ -243,6 +259,7 @@ export function applyMutation(state, mutation) {
|
|
|
243
259
|
previous.claim = mutation.claim
|
|
244
260
|
previous.done_criteria = mutation.done_criteria
|
|
245
261
|
previous.promote_at_level = mutation.promote_at_level
|
|
262
|
+
if (Array.isArray(mutation.irreversible)) previous.irreversible = clone(mutation.irreversible)
|
|
246
263
|
/** 修订可以带上新的速览与判据清单(没带就保持旧值——「不提供」不等于「清空」)。 */
|
|
247
264
|
if (typeof mutation.headline === 'string' && mutation.headline !== '') previous.headline = mutation.headline
|
|
248
265
|
if (Array.isArray(mutation.criteria)) previous.criteria = clone(mutation.criteria)
|
|
@@ -255,6 +272,8 @@ export function applyMutation(state, mutation) {
|
|
|
255
272
|
claim: mutation.claim,
|
|
256
273
|
done_criteria: mutation.done_criteria,
|
|
257
274
|
promote_at_level: mutation.promote_at_level,
|
|
275
|
+
/** 不可逆动作:`{action, command}`,命令里出现 `command` 那段原文就要人放行。 */
|
|
276
|
+
irreversible: Array.isArray(mutation.irreversible) ? clone(mutation.irreversible) : [],
|
|
258
277
|
status: 'open',
|
|
259
278
|
revision: mutation.revision ?? 1,
|
|
260
279
|
reasons: [mutation.reason ?? null],
|
|
@@ -297,6 +316,7 @@ export function applyMutation(state, mutation) {
|
|
|
297
316
|
known.refute_when = hypothesis.refute_when
|
|
298
317
|
known.version = hypothesis.version ?? known.version
|
|
299
318
|
if (typeof hypothesis.name === 'string' && hypothesis.name !== '') known.name = hypothesis.name
|
|
319
|
+
if (typeof hypothesis.retests === 'string' && hypothesis.retests !== '') known.retests = hypothesis.retests
|
|
300
320
|
if (Array.isArray(hypothesis.assertions)) known.assertions = clone(hypothesis.assertions)
|
|
301
321
|
continue
|
|
302
322
|
}
|
|
@@ -307,6 +327,8 @@ export function applyMutation(state, mutation) {
|
|
|
307
327
|
...(typeof hypothesis.name === 'string' && hypothesis.name !== '' ? { name: hypothesis.name } : {}),
|
|
308
328
|
claim,
|
|
309
329
|
refute_when: hypothesis.refute_when,
|
|
330
|
+
/** 复检的是哪条已有事实(事实 id;可以是别的会话留下的)。 */
|
|
331
|
+
...(typeof hypothesis.retests === 'string' && hypothesis.retests !== '' ? { retests: hypothesis.retests } : {}),
|
|
310
332
|
status: 'proposed',
|
|
311
333
|
version: hypothesis.version ?? 1,
|
|
312
334
|
/**
|
|
@@ -378,6 +400,44 @@ export function applyMutation(state, mutation) {
|
|
|
378
400
|
step.criteria_versions.push(mutation.new_criteria) // 旧版本留着,什么都不删
|
|
379
401
|
break
|
|
380
402
|
}
|
|
403
|
+
case 'step/expected': {
|
|
404
|
+
const step = stepOf(mutation.plan, mutation.step)
|
|
405
|
+
if (step === undefined || step.status !== 'open') break
|
|
406
|
+
step.expect = String(mutation.expect ?? '') || null
|
|
407
|
+
break
|
|
408
|
+
}
|
|
409
|
+
case 'anomaly/opened': {
|
|
410
|
+
if (next.anomalies.some((item) => item.id === mutation.id)) break
|
|
411
|
+
next.anomalies.push({ id: mutation.id, what: String(mutation.what ?? ''), anchor: mutation.anchor ?? null, step: mutation.step ?? null, by: mutation.by ?? 'model', status: 'open', reason: null, explainedBy: null, at, resolvedAt: null })
|
|
412
|
+
break
|
|
413
|
+
}
|
|
414
|
+
case 'lesson/recorded': {
|
|
415
|
+
if (!Array.isArray(next.lessons)) next.lessons = []
|
|
416
|
+
if (next.lessons.some((item) => item.id === mutation.id)) break
|
|
417
|
+
next.lessons.push({
|
|
418
|
+
id: mutation.id,
|
|
419
|
+
goal: mutation.goal ?? null,
|
|
420
|
+
text: String(mutation.text ?? ''),
|
|
421
|
+
kind: mutation.kind ?? 'trap',
|
|
422
|
+
about: Array.isArray(mutation.about) ? mutation.about.map(String) : [],
|
|
423
|
+
evidence: mutation.evidence ?? null,
|
|
424
|
+
boundary: mutation.boundary ?? null,
|
|
425
|
+
status: 'active',
|
|
426
|
+
path: mutation.path ?? null,
|
|
427
|
+
at,
|
|
428
|
+
})
|
|
429
|
+
break
|
|
430
|
+
}
|
|
431
|
+
case 'anomaly/resolved': {
|
|
432
|
+
const item = next.anomalies.find((entry) => entry.id === mutation.id)
|
|
433
|
+
if (item === undefined || item.status !== 'open') break
|
|
434
|
+
if (!['explained', 'ruled_out', 'escalated'].includes(mutation.outcome)) break
|
|
435
|
+
item.status = mutation.outcome
|
|
436
|
+
item.reason = mutation.reason ?? null
|
|
437
|
+
item.explainedBy = mutation.by ?? null
|
|
438
|
+
item.resolvedAt = at
|
|
439
|
+
break
|
|
440
|
+
}
|
|
381
441
|
case 'plan/voided': {
|
|
382
442
|
const step = stepOf(mutation.plan, mutation.step)
|
|
383
443
|
if (step === undefined) break
|
|
@@ -484,6 +544,13 @@ export function applyMutation(state, mutation) {
|
|
|
484
544
|
audit.card_path = mutation.card_path ?? null
|
|
485
545
|
if (mutation.digest !== undefined) audit.digest = mutation.digest ?? null
|
|
486
546
|
}
|
|
547
|
+
/** 评估者报的、做的人没登记的异常:记成未解释项(id 由裁决 id 派生,重放幂等)。 */
|
|
548
|
+
const found = Array.isArray(mutation.anomalies) ? mutation.anomalies : []
|
|
549
|
+
for (const [index, item] of found.entries()) {
|
|
550
|
+
const id = `${mutation.id}#u${index + 1}`
|
|
551
|
+
if (next.anomalies.some((entry) => entry.id === id)) continue
|
|
552
|
+
next.anomalies.push({ id, what: String(item?.what ?? ''), anchor: null, step: mutation.step ?? null, by: 'evaluator', matters: item?.matters ?? null, status: 'open', reason: null, explainedBy: null, at, resolvedAt: null })
|
|
553
|
+
}
|
|
487
554
|
break
|
|
488
555
|
}
|
|
489
556
|
case 'evidence/recorded': {
|
|
@@ -534,6 +601,8 @@ export function applyMutation(state, mutation) {
|
|
|
534
601
|
via: mutation.via ?? null,
|
|
535
602
|
at,
|
|
536
603
|
call: mutation.call ?? null,
|
|
604
|
+
/** 放行的是哪件不可逆动作(拦在命令上的那道门);步级放行没有这一格。 */
|
|
605
|
+
...(typeof mutation.action === 'string' && mutation.action !== '' ? { action: mutation.action } : {}),
|
|
537
606
|
})
|
|
538
607
|
break
|
|
539
608
|
}
|
|
@@ -584,9 +653,9 @@ export function applyMutation(state, mutation) {
|
|
|
584
653
|
*/
|
|
585
654
|
const touchedOntology = (mutation.changes ?? []).some((change) => {
|
|
586
655
|
const kind = classifyWorkspacePath(change?.path)?.kind
|
|
587
|
-
return kind !== undefined && kind !== 'fact'
|
|
656
|
+
return kind !== undefined && kind !== 'fact' && kind !== 'lesson'
|
|
588
657
|
})
|
|
589
|
-
if (touchedOntology || Object.keys(files).some((path) => classifyWorkspacePath(path)?.kind
|
|
658
|
+
if (touchedOntology || Object.keys(files).some((path) => !['fact', 'lesson'].includes(classifyWorkspacePath(path)?.kind))) {
|
|
590
659
|
const ontology = materializeOntology(files)
|
|
591
660
|
next.lexicon = ontology.lexicon
|
|
592
661
|
next.entities = ontology.entities
|
|
@@ -1077,7 +1146,9 @@ export function knowledgePreflight(state, derived) {
|
|
|
1077
1146
|
const factRows = Array.isArray(derived?.factRows) ? derived.factRows : []
|
|
1078
1147
|
const conflicts = Array.isArray(derived?.conflicts) ? derived.conflicts : []
|
|
1079
1148
|
/** 命题的文本面:目标主张 + 每条命题的主张(去空白后做包含判断)。 */
|
|
1080
|
-
|
|
1149
|
+
/** 加上下一步要做什么与它的预期:预期引用的关系也要递到眼前。 */
|
|
1150
|
+
const nextStep = (derived?.activePlan?.steps ?? []).find((step) => step?.status === 'open') ?? null
|
|
1151
|
+
const claimText = [state?.goal?.claim, ...hypotheses.map((item) => item.claim), nextStep?.do, nextStep?.expect].filter((text) => typeof text === 'string' && text !== '').map((text) => String(text).replace(/\s+/g, ''))
|
|
1081
1152
|
/** 命题已经引用的谓词(断言在假设上时就该算「在用」)。 */
|
|
1082
1153
|
const usedPredicates = new Set(hypotheses.flatMap((item) => (Array.isArray(item.assertions) ? item.assertions : [])).map((assertion) => String(assertion?.predicate ?? '')))
|
|
1083
1154
|
const hits = (entry) => {
|
|
@@ -1089,19 +1160,27 @@ export function knowledgePreflight(state, derived) {
|
|
|
1089
1160
|
const matchedPredicates = predicates.filter((predicate) => usedPredicates.has(predicate.id) || hits(predicate))
|
|
1090
1161
|
const matchedTermIds = new Set(matchedTerms.map((term) => term.id))
|
|
1091
1162
|
const matchedPredicateIds = new Set(matchedPredicates.map((predicate) => predicate.id))
|
|
1092
|
-
/**
|
|
1093
|
-
|
|
1094
|
-
|
|
1163
|
+
/**
|
|
1164
|
+
* 事实:断言引用了命中的谓词或概念;**或**升格时按词面挂上的定义里有命中的词(真跑里事实几乎都没有断言,
|
|
1165
|
+
* 只认断言等于从不命中);**或**有判断用 `retests` 指着它。撤回的不给。
|
|
1166
|
+
*/
|
|
1167
|
+
const retested = new Set(hypotheses.map((item) => item.retests).filter((id) => typeof id === 'string'))
|
|
1168
|
+
const matchedFacts = factRows.filter(
|
|
1169
|
+
(fact) =>
|
|
1170
|
+
fact.review?.decision !== 'retracted' &&
|
|
1171
|
+
(retested.has(fact.id) ||
|
|
1172
|
+
Object.keys(fact.definitions ?? {}).some((id) => matchedTermIds.has(id) || matchedPredicateIds.has(id)) ||
|
|
1173
|
+
(Array.isArray(fact.assertions) ? fact.assertions : []).some((assertion) => matchedPredicateIds.has(String(assertion?.predicate ?? '')) || matchedTermIds.has(String(assertion?.subject?.type ?? '')))),
|
|
1095
1174
|
)
|
|
1096
1175
|
return {
|
|
1097
1176
|
mode: 'knowledge',
|
|
1098
1177
|
/** 词面命中的词汇:模型接下来要写的结论大概率会用到它们。 */
|
|
1099
|
-
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 })),
|
|
1178
|
+
terms: matchedTerms.slice(0, LIMIT).map((term) => ({ id: term.id, label: term.label, gloss: term.gloss, kind: term.kind ?? null, unit: term.unit ?? null, parent: term.parent ?? null, status: term.status, uses: term.uses ?? 0, basis: term.basis ?? null })),
|
|
1100
1179
|
termsTruncated: Math.max(0, matchedTerms.length - LIMIT),
|
|
1101
|
-
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 })),
|
|
1180
|
+
predicates: matchedPredicates.slice(0, LIMIT).map((predicate) => ({ id: predicate.id, label: predicate.label, gloss: predicate.gloss ?? '', kind: predicate.kind ?? null, shape: predicate.shape ?? null, check: predicate.check ?? null, domain: predicate.domain, range: predicate.range, functional: predicate.functional === true, status: predicate.status, uses: predicate.uses ?? 0, basis: predicate.basis ?? null })),
|
|
1102
1181
|
predicatesTruncated: Math.max(0, matchedPredicates.length - LIMIT),
|
|
1103
1182
|
/** 命中的既有事实(可复用的「已知」)。 */
|
|
1104
|
-
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 })),
|
|
1183
|
+
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, foreign: fact.foreign === true })),
|
|
1105
1184
|
factsTruncated: Math.max(0, matchedFacts.length - LIMIT),
|
|
1106
1185
|
/** 冲突:有就带上(它们约束「哪些结论还不能随便写)。 */
|
|
1107
1186
|
conflicts: conflicts.map((conflict) => ({ predicate: conflict.predicate, subject: conflict.subject })),
|
|
@@ -1685,15 +1764,18 @@ export function derive(state) {
|
|
|
1685
1764
|
* · `review` ——人已经审查过(撤回 / 维持),决定连缘由一起留着。
|
|
1686
1765
|
*/
|
|
1687
1766
|
const lexicon = normalizeLexicon(state.lexicon)
|
|
1767
|
+
/** 声明了 `retests` 的判断被推翻 ⇒ 它复检的那条事实也算被推翻(跨会话只认事实 id)。 */
|
|
1768
|
+
const retestRefuted = new Set(hypotheses.filter((item) => typeof item.retests === 'string' && (item.refutations ?? 0) > 0).map((item) => item.retests))
|
|
1688
1769
|
const ownFacts = (state.facts ?? []).map((fact) => {
|
|
1689
1770
|
const linked = typeof fact.hypothesis === 'string' && fact.hypothesis !== ''
|
|
1690
1771
|
const owner = hypotheses.find((item) => (linked ? item.id === fact.hypothesis : String(item.claim ?? '') === String(fact.text ?? '')))
|
|
1691
|
-
return { ...fact, refuted: (owner?.refutations ?? 0) > 0, review: fact.review ?? null, foreign: false }
|
|
1772
|
+
return { ...fact, refuted: (owner?.refutations ?? 0) > 0 || retestRefuted.has(fact.id), review: fact.review ?? null, foreign: false }
|
|
1692
1773
|
})
|
|
1693
1774
|
/**
|
|
1694
1775
|
* **别的会话留下的事实**(住在 `clear/knowledge/facts/<id>.json`,经 `workspace/synced` 进账)。
|
|
1695
1776
|
* 同一个 id 本会话账上也有就以账上那条为准(它带着推翻读数);文件上的复核结论照样认。
|
|
1696
|
-
*
|
|
1777
|
+
* 它们的假设不在本会话,所以推翻要在这里重新检验一次才算:本会话的判断用 `retests` 指向它,
|
|
1778
|
+
* 那条判断被推翻,这条事实就标「被推翻」。
|
|
1697
1779
|
*/
|
|
1698
1780
|
const ownIds = new Set(ownFacts.map((fact) => fact.id))
|
|
1699
1781
|
const foreignFacts = []
|
|
@@ -1706,7 +1788,7 @@ export function derive(state) {
|
|
|
1706
1788
|
if (own.review === null && row.review !== null) own.review = row.review
|
|
1707
1789
|
continue
|
|
1708
1790
|
}
|
|
1709
|
-
foreignFacts.push({ ...row, refuted:
|
|
1791
|
+
foreignFacts.push({ ...row, refuted: retestRefuted.has(row.id), foreign: true })
|
|
1710
1792
|
}
|
|
1711
1793
|
foreignFacts.sort((a, b) => (a.at ?? 0) - (b.at ?? 0))
|
|
1712
1794
|
/**
|
|
@@ -1724,6 +1806,18 @@ export function derive(state) {
|
|
|
1724
1806
|
* 两者都吃 `factRows` 而不是 `state.facts`:事实的复核态与推翻标记是派生的,
|
|
1725
1807
|
* 在这里重算一遍就等于第二份判据。
|
|
1726
1808
|
*/
|
|
1809
|
+
/**
|
|
1810
|
+
* **经验行**:本会话写下的,加上别的会话留在 `clear/knowledge/lessons/` 的。同 id 以文件为准
|
|
1811
|
+
* (人可以在文件上标 `status: "retracted"` 撤回);撤回的不再推送。新的在前。
|
|
1812
|
+
*/
|
|
1813
|
+
const lessonById = new Map()
|
|
1814
|
+
for (const item of state.lessons ?? []) lessonById.set(item.id, { ...item })
|
|
1815
|
+
for (const [path, file] of Object.entries(state.workspace?.files ?? {})) {
|
|
1816
|
+
if (classifyWorkspacePath(path)?.kind !== 'lesson' || file?.data === undefined) continue
|
|
1817
|
+
const row = lessonFromFile(file.data, path)
|
|
1818
|
+
if (row !== null) lessonById.set(row.id, { ...lessonById.get(row.id), ...row, at: row.at ?? lessonById.get(row.id)?.at ?? null })
|
|
1819
|
+
}
|
|
1820
|
+
const lessonRows = [...lessonById.values()].filter((item) => item.status !== 'retracted').sort((a, b) => (b.at ?? 0) - (a.at ?? 0))
|
|
1727
1821
|
const conflicts = deriveConflicts(factRows, lexicon)
|
|
1728
1822
|
const lexiconIssues = lexiconHealth(lexicon, factRows)
|
|
1729
1823
|
/**
|
|
@@ -1771,6 +1865,7 @@ export function derive(state) {
|
|
|
1771
1865
|
closedPlans,
|
|
1772
1866
|
pendingAudit,
|
|
1773
1867
|
factRows,
|
|
1868
|
+
lessonRows,
|
|
1774
1869
|
settlement,
|
|
1775
1870
|
stepOf,
|
|
1776
1871
|
needYou,
|