clearai-dsh 0.2.8 → 0.3.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.
@@ -29,8 +29,14 @@ export const VALUE_FORMS = ['statement', 'quantity', 'formula', 'code', 'referen
29
29
  */
30
30
  export const OBJECT_KINDS = [...VALUE_FORMS, 'instance']
31
31
 
32
- /** 词条的两种角色。它们共用同一条生命周期(接纳 → 修订 → 黏性废止)。 */
33
- export const LEXICON_KINDS = ['term', 'predicate']
32
+ /**
33
+ * 词条的三种角色。概念与谓词共用同一条生命周期(接纳 → 修订 → 黏性废止)。
34
+ *
35
+ * **实例**在表里,但不在 `lexicon` 里:它的家在账本的 `state.entities`(每条带类型与出处),
36
+ * 因为「某个具体物在某出处下成立」是一条**主张**,而不是一次约定——它要能带依据被清点。
37
+ * 它**没有 parent**:`is_a` 只连概念,实例与概念的关系用断言说(见 `validateTerm`)。
38
+ */
39
+ export const LEXICON_KINDS = ['term', 'predicate', 'instance']
34
40
 
35
41
  /** id 的形状:小写字母开头的 slug。中英文之外的类型名一律不认,免得同一个概念有两种写法。 */
36
42
  const ID_PATTERN = /^[a-z][a-z0-9_]{1,39}$/
@@ -106,7 +112,8 @@ export function termChain(lexicon, id) {
106
112
  */
107
113
  export function validateTerm(lexicon, draft) {
108
114
  const problems = []
109
- const at = '概念'
115
+ const instance = isPlainObject(draft) && text(draft.kind) === 'instance'
116
+ const at = instance ? '实例' : '概念'
110
117
  if (!isPlainObject(draft)) return [problem('term_shape', `${at}必须是一个对象`)]
111
118
  const id = text(draft.id)
112
119
  if (!ID_PATTERN.test(id)) problems.push(problem('id_shape', `${at} id 要小写字母开头的 slug(字母/数字/下划线,≤40):收到「${id}」`))
@@ -128,6 +135,12 @@ export function validateTerm(lexicon, draft) {
128
135
  if (walk.cycle !== null || walk.missing !== null) problems.push(problem('parent_cycle', `父概念「${parent}」的父链已经成环或指空,不能再往上接`))
129
136
  }
130
137
  }
138
+ /**
139
+ * **实例不是概念**:它有类型与出处,没有父概念。`is_a` 是概念之间的事;
140
+ * 「这个样本属于哪一类」要用断言说——否则个体一多,本体层被具体物撑大,
141
+ * 而实体层仍然是空的(那正是这套词汇要给实例一个可写入口的原因)。
142
+ */
143
+ if (instance && parent !== '') problems.push(problem('instance_no_parent', `实例「${id}」不许有父概念:is_a 只连概念,它属于哪一类要用断言说`))
131
144
  return problems
132
145
  }
133
146
 
@@ -270,11 +283,33 @@ export function validateAssertion(lexicon, assertion) {
270
283
  /**
271
284
  * 一条事实的**整组**断言是否自洽。除了逐条校验,还查「同一主体同一谓词给了两个值」——
272
285
  * 单值谓词上这已经是自相矛盾,不该等到与别的事实比才发现。
286
+ *
287
+ * **断言主体必须可指认**(契约冻结第 4 条):`subject.id` 要么已经在实体图上
288
+ * (`state.entities` 里的 `${type}|${id}`),要么在**同一批**断言里以 `instance` 形态被引出过
289
+ * (宾语形态 = instance,值/类型对上)。两者都不是 ⇒ `assert_subject_unknown`:
290
+ * 断言只挂在命题上,图里没有那个对象,句子无法被机器比对。
291
+ *
292
+ * 两个入口都认,判据只有这一份:
293
+ * · 递**整份状态**(`{ lexicon, entities }`)—— 推荐,实体面在里面,这一条才真跑;
294
+ * · 递**词汇**(旧调用点)或 `options.legacy === true` —— 迁移期一次性放行(缺实体面时
295
+ * 无法判「已登记」,如实跳过而不是拿猜测拒人)。
273
296
  */
274
- export function validateAssertions(lexicon, assertions) {
297
+ export function validateAssertions(state, assertions, options = {}) {
275
298
  if (assertions === undefined || assertions === null) return []
276
299
  if (!Array.isArray(assertions)) return [problem('assertions_shape', 'assertions 只能是数组')]
277
300
  if (assertions.length === 0) return []
301
+ const stateForm = isPlainObject(state) && isPlainObject(state.lexicon)
302
+ const lexicon = stateForm ? state.lexicon : state
303
+ const entities = stateForm && Array.isArray(state.entities) ? state.entities : null
304
+ const legacy = options === true || (isPlainObject(options) && options.legacy === true)
305
+ const registered = new Set(entities === null ? [] : entities.map((entry) => `${text(entry?.type)}|${text(entry?.id)}`))
306
+ /** 这一批断言里被 `instance` 宾语**引出**的对象:值 + (给了类型就要求类型一致)。 */
307
+ const introduced = new Map()
308
+ for (const assertion of assertions) {
309
+ if (text(assertion?.object?.kind) !== 'instance') continue
310
+ const value = text(assertion.object.value)
311
+ if (value !== '') introduced.set(value, text(assertion.object.type))
312
+ }
278
313
  const problems = []
279
314
  const seen = new Map()
280
315
  for (const [index, assertion] of assertions.entries()) {
@@ -285,10 +320,33 @@ export function validateAssertions(lexicon, assertions) {
285
320
  problems.push(problem('assertion_self_conflict', `同一事实里「${text(assertion?.predicate)}」在主体「${text(assertion?.subject?.id)}」上给了两个值:${seen.get(key)} 与 ${value}`))
286
321
  }
287
322
  seen.set(key, value)
323
+ if (entities === null || legacy) continue
324
+ const subjectId = text(assertion?.subject?.id)
325
+ if (subjectId === '') continue
326
+ const subjectType = text(assertion?.subject?.type)
327
+ if (registered.has(`${subjectType}|${subjectId}`)) continue
328
+ const introducedType = introduced.get(subjectId)
329
+ if (introducedType !== undefined && (introducedType === '' || introducedType === subjectType)) continue
330
+ problems.push(subjectUnknown(`${subjectType}|${subjectId}`))
288
331
  }
289
332
  return problems
290
333
  }
291
334
 
335
+ /**
336
+ * `assert_subject_unknown` 的**报告形状**。
337
+ *
338
+ * 契约要求这一条带 `code` 与 `subject`,而消费它的内核把 problems 当文本拼
339
+ * (`` `- ${item}` ``)。所以这里给一个**两种读法都成立**的值:它是一段文本
340
+ * (`String(item)` / `item.includes(...)` 照常),同时挂着 `code` 与 `subject` 两个字段。
341
+ * 换成一个裸对象会让内核当场渲染出 `[object Object]`——那比不报更坏。
342
+ */
343
+ function subjectUnknown(subject) {
344
+ const message = new String(problem('assert_subject_unknown', `主体「${subject}」还不在实体图上:先登记这个实例(带类型与出处),或在这一批断言里让某个宾语以 instance 形态引出它`))
345
+ message.code = 'assert_subject_unknown'
346
+ message.subject = subject
347
+ return message
348
+ }
349
+
292
350
  /**
293
351
  * **冲突是派生读数,不是存储对象**:两条**未撤回**的已确认事实落在同一个单值谓词、
294
352
  * 同一主体、而客体不同,就是一对冲突。它只被**说出来**,不被裁决——哪条为真不是这里的事
@@ -392,6 +450,31 @@ export function lexiconHealth(lexicon, facts) {
392
450
  return issues.sort((a, b) => (a.kind === b.kind ? (a.id < b.id ? -1 : 1) : a.kind < b.kind ? -1 : 1))
393
451
  }
394
452
 
453
+ /**
454
+ * **每个概念被引用了多少次**:断言主体的类型、谓词声明的主词域 / 值域、以及实体断言的类型。
455
+ *
456
+ * 为什么要单独一份判据:「零引用」这件事有两处读者——货架上那一节,与 `deriveKnowledge`
457
+ * 的 `orphan_terms` 缺口。两处各算一遍,迟早会出现「货架说没人用、缺口说用了」这种
458
+ * 同一件事两种读数;所以引用面只在这里定义一次,两边都读它。
459
+ */
460
+ export function termUsage(lexicon, facts, entityAssertions = []) {
461
+ const normalized = normalizeLexicon(lexicon)
462
+ const usage = new Map()
463
+ const bump = (id) => {
464
+ const key = text(id)
465
+ if (key !== '') usage.set(key, (usage.get(key) ?? 0) + 1)
466
+ }
467
+ for (const predicate of normalized.predicates) {
468
+ bump(predicate.domain)
469
+ bump(isPlainObject(predicate.range) ? predicate.range.term : '')
470
+ }
471
+ for (const fact of Array.isArray(facts) ? facts : []) {
472
+ for (const assertion of Array.isArray(fact?.assertions) ? fact.assertions : []) bump(assertion?.subject?.type)
473
+ }
474
+ for (const item of Array.isArray(entityAssertions) ? entityAssertions : []) bump(item?.subject?.type)
475
+ return usage
476
+ }
477
+
395
478
  /**
396
479
  * **图是投影,不是存储**:同一份账本,永远算出同一组节点、同一组边、同一套默认坐标。
397
480
  *
@@ -405,6 +488,8 @@ export function lexiconHealth(lexicon, facts) {
405
488
  export function graphProjection(state) {
406
489
  const lexicon = normalizeLexicon(state?.lexicon)
407
490
  const facts = Array.isArray(state?.facts) ? state.facts : []
491
+ /** 实体断言:登记那一刻就落账的「某实例在某出处下成立某断言」,不等目标裁决。 */
492
+ const entityAssertions = Array.isArray(state?.entityAssertions) ? state.entityAssertions : []
408
493
  const nodes = []
409
494
  const edges = []
410
495
  const termById = new Map(lexicon.terms.map((item) => [item.id, item]))
@@ -418,6 +503,13 @@ export function graphProjection(state) {
418
503
  if (predicateId !== '') uses.set(predicateId, (uses.get(predicateId) ?? 0) + 1)
419
504
  }
420
505
  }
506
+ /** 实体断言的使用同样算「在用」:词被断言引用过,就不该报「没人用」。 */
507
+ for (const item of entityAssertions) {
508
+ const type = text(item?.subject?.type)
509
+ if (type !== '') uses.set(type, (uses.get(type) ?? 0) + 1)
510
+ const predicateId = text(item?.predicate)
511
+ if (predicateId !== '') uses.set(predicateId, (uses.get(predicateId) ?? 0) + 1)
512
+ }
421
513
  const depthOf = (term) => {
422
514
  let depth = 0
423
515
  let cursor = text(term.parent)
@@ -446,7 +538,48 @@ export function graphProjection(state) {
446
538
  const to = rangeTerm !== '' ? `term:${rangeTerm}` : form !== '' ? `form:${form}` : null
447
539
  if (to !== null) edges.push({ id: `predicate:${predicate.id}`, kind: 'predicate', layer: 'ontology', predicate: predicate.id, label: predicate.label ?? predicate.id, from: text(predicate.domain) === '' ? null : `term:${text(predicate.domain)}`, to, status: predicate.status ?? 'admitted', functional: predicate.functional === true })
448
540
  }
541
+ /**
542
+ * **实体层的三个来源合一**(契约冻结第 5 条)。
543
+ *
544
+ * 从前实体只有一个来源:已升格事实里的断言。于是「实体」是目标级裁决的副产品——
545
+ * 一条观察要在图上出现,得先过一遍与它无关的判据;而只给实体一个**登记节点的**
546
+ * 写入口更糟:图可以被「一堆孤立节点」满足,边照样不长(第 2 轮那个病的同一形状)。
547
+ * 所以实体有两个一等写入口,都在登记那一刻落账:
548
+ * · `entity/registered` → 节点(`source:'registered'`,带类型 / 依据 / 出处);
549
+ * · `entity/asserted` → **边**(`source:'asserted'`,有出处、未经独立裁决)。
550
+ *
551
+ * 合并规则:
552
+ * · 键 `${type}|${id}`(与从前的实例键同形),同键去重,节点优先级
553
+ * `registered > promoted > asserted`(先落的那种是这条节点的身份与展示来源);
554
+ * · 边有两个来源:事实断言(`promoted`,带等级 / 边界 / 可点复核)与实体断言
555
+ * (`asserted`,带出处);两者形状同形,靠 `source` 区分,UI 可分别画实线 / 虚线;
556
+ * · `source` 只在**真有登记或实体断言**的图上出现:两个来源都为空时,这一段的输出
557
+ * 与改造前逐字节一致(旧账本的图不许因为加字段而变)。
558
+ */
559
+ const merging = (Array.isArray(state?.entities) ? state.entities.length : 0) > 0 || entityAssertions.length > 0
449
560
  const instances = new Map()
561
+ if (merging) {
562
+ for (const entity of Array.isArray(state?.entities) ? state.entities : []) {
563
+ const type = text(entity?.type)
564
+ const entityId = text(entity?.id)
565
+ if (type === '' || entityId === '') continue
566
+ const key = `${type}|${entityId}`
567
+ if (instances.has(key)) continue
568
+ instances.set(key, {
569
+ id: key,
570
+ kind: 'instance',
571
+ layer: 'entity',
572
+ ref: entityId,
573
+ label: text(entity.label) || entityId,
574
+ type,
575
+ source: 'registered',
576
+ basis: entity.basis ?? null,
577
+ provenance: entity.provenance ?? null,
578
+ registeredAt: entity.registeredAt ?? null,
579
+ facts: [],
580
+ })
581
+ }
582
+ }
450
583
  for (const fact of facts) {
451
584
  const status = fact?.review?.decision === 'retracted' ? 'retracted' : fact?.refuted === true ? 'refuted' : 'live'
452
585
  for (const assertion of Array.isArray(fact?.assertions) ? fact.assertions : []) {
@@ -475,9 +608,62 @@ export function graphProjection(state) {
475
608
  instances.get(literalId).facts.push(fact.id ?? null)
476
609
  to = literalId
477
610
  }
478
- if (to !== null) edges.push({ id: `assertion:${fact.id ?? ''}:${predicateId}:${subjectKey(assertion)}`, kind: 'assertion', layer: 'entity', predicate: predicateId, label: text(lexicon.predicates.find((item) => item.id === predicateId)?.label) || predicateId, from: key, to, status, level: fact.level ?? null, fact: fact.id ?? null, scope: fact.scope ?? null, claim: text(fact.hypothesis) === '' ? null : text(fact.hypothesis) })
611
+ if (to !== null) {
612
+ const edge = { id: `assertion:${fact.id ?? ''}:${predicateId}:${subjectKey(assertion)}`, kind: 'assertion', layer: 'entity', predicate: predicateId, label: text(lexicon.predicates.find((item) => item.id === predicateId)?.label) || predicateId, from: key, to, status, level: fact.level ?? null, fact: fact.id ?? null, scope: fact.scope ?? null, claim: text(fact.hypothesis) === '' ? null : text(fact.hypothesis) }
613
+ /** 加在末尾:两个来源都空时这条边的键序与从前逐字相同。 */
614
+ if (merging) edge.source = 'promoted'
615
+ edges.push(edge)
616
+ }
479
617
  }
480
618
  }
619
+ for (const item of entityAssertions) {
620
+ const subject = isPlainObject(item?.subject) ? item.subject : {}
621
+ const subjectId = text(subject.id)
622
+ const predicateId = text(item?.predicate)
623
+ if (subjectId === '' || predicateId === '') continue
624
+ const subjectType = text(subject.type)
625
+ const key = `${subjectType}|${subjectId}`
626
+ if (!instances.has(key)) instances.set(key, { id: key, kind: 'instance', layer: 'entity', ref: subjectId, label: subjectId, type: subjectType === '' ? null : subjectType, source: 'asserted', facts: [] })
627
+ const object = isPlainObject(item?.object) ? item.object : {}
628
+ const objectKind = text(object.kind)
629
+ let to = null
630
+ if (objectKind === 'instance') {
631
+ const objectLabel = text(object.value)
632
+ if (objectLabel === '') continue
633
+ const objectType = text(object.type) === '' ? text(predicateById.get(predicateId)?.range?.term) : text(object.type)
634
+ const objectKeyId = `${objectType}|${objectLabel}`
635
+ if (!instances.has(objectKeyId)) instances.set(objectKeyId, { id: objectKeyId, kind: 'instance', layer: 'entity', ref: objectLabel, label: objectLabel, type: objectType === '' ? null : objectType, source: 'asserted', facts: [] })
636
+ to = objectKeyId
637
+ } else if (objectKind !== '') {
638
+ const literalId = `${predicateId}:${objectKey(item.object)}`
639
+ if (!instances.has(literalId)) instances.set(literalId, { id: literalId, kind: 'literal', layer: 'entity', ref: objectKey(item.object), label: formatObject(object), type: null, source: 'asserted', facts: [] })
640
+ to = literalId
641
+ }
642
+ if (to === null) continue
643
+ edges.push({
644
+ id: `assertion:${text(item.id)}:${predicateId}:${subjectKey(item)}`,
645
+ kind: 'assertion',
646
+ layer: 'entity',
647
+ source: 'asserted',
648
+ predicate: predicateId,
649
+ label: text(lexicon.predicates.find((entry) => entry.id === predicateId)?.label) || predicateId,
650
+ from: key,
651
+ to,
652
+ /**
653
+ * `status:'asserted'` 是**第三种边态**:有出处、登记那一刻就成立,但**没有**
654
+ * 过独立裁决,所以它既不是 `live`(已升格)也不是 `retracted`。画成虚线。
655
+ */
656
+ status: 'asserted',
657
+ level: null,
658
+ fact: null,
659
+ scope: null,
660
+ claim: null,
661
+ evidence: isPlainObject(item?.evidence) ? { kind: item.evidence.kind ?? null, ref: item.evidence.ref ?? null } : null,
662
+ assertion: text(item.id) === '' ? null : text(item.id),
663
+ })
664
+ }
665
+ /** 只从事实投影出来、又没被登记的节点:标成 `promoted`(只在合并的那张图上标,见上)。 */
666
+ if (merging) for (const instance of instances.values()) if (instance.source === undefined) instance.source = 'promoted'
481
667
  for (const instance of [...instances.values()].sort((a, b) => (a.id < b.id ? -1 : 1))) nodes.push(instance)
482
668
  /**
483
669
  * **确定性布局:按层分段,层内折行。**
@@ -637,25 +823,51 @@ export function applyLexiconMutation(lexicon, mutation, at) {
637
823
  * 为什么它必须是一份纯函数:同一份词汇与事实,谁渲染都得同一串字节——
638
824
  * 幂等写盘靠它(内容没变就不重写,文件时间戳是给人的读数),测试也靠它钉住。
639
825
  *
640
- * 三件事按顺序说:有什么词(概念 / 谓词)、它们长什么样(图)、用起来什么情况(引用与冲突)。
641
- * **不写"权威"二字就够了吗**:不够——所以抬头先写明这一份是读面,
642
- * 改它不会改词汇,词汇只认账本事件。
826
+ * **三节分开说**(契约冻结第 7 条):概念 / 个体(实例)/ 谓词。从前它们挤在一张「词条」表里,
827
+ * 于是「李赣」「狗熊哆嗦毛(样本)」这类**具体物**与「数值格式」这类**约定**长得一模一样——
828
+ * 读的人分不出哪些是可复用的语言、哪些是一次具体的记录。每节开头一句「这一节是什么」。
829
+ *
830
+ * **零引用的概念单独一节**:注册了却没有任何结论引用它,那它还是约定、不是已知。
831
+ * 这一节是这句话的可见面(与 `orphan_terms` 缺口读的是同一件事)。
832
+ *
833
+ * 第一个参数收两种形状:整份**状态**(推荐,个体那一节要有实体面)或旧的**词汇**;
834
+ * `options.view` 传 `knowledge-view.js` 的 `knowledgeView()` 输出时,「使用」一节读的就是
835
+ * 那份**单一叙述源**(缺口与进度不再由这里各写一套)。
643
836
  */
644
- export function describeDomainShelf(lexicon, facts, hypotheses = []) {
645
- const normalized = normalizeLexicon(lexicon)
837
+ export function describeDomainShelf(lexiconOrState, facts, hypotheses = [], options = {}) {
838
+ const stateForm = isPlainObject(lexiconOrState) && isPlainObject(lexiconOrState.lexicon)
839
+ const state = stateForm ? lexiconOrState : null
840
+ const normalized = normalizeLexicon(stateForm ? state.lexicon : lexiconOrState)
646
841
  const rows = Array.isArray(facts) ? facts : []
842
+ const view = isPlainObject(options) && isPlainObject(options.view) ? options.view : null
843
+ const entityAssertions = stateForm && Array.isArray(state.entityAssertions) ? state.entityAssertions : []
647
844
  const terms = [...normalized.terms].sort((a, b) => (a.id < b.id ? -1 : 1))
648
845
  const predicates = [...normalized.predicates].sort((a, b) => (a.id < b.id ? -1 : 1))
649
- const usage = new Map()
846
+ /**
847
+ * 实例那一节与图**同一份判据**:直接读投影,不在这里重数一遍。
848
+ * 没有状态(旧的词汇入口)时如实为空——不知道的事不编。
849
+ */
850
+ const entityNodes = state === null ? [] : graphProjection(state).nodes.filter((node) => node.layer === 'entity' && node.kind === 'instance')
851
+ /**
852
+ * **引用面读同一份判据**:概念那一列用 `termUsage`(断言主体 + 主词域 / 值域),
853
+ * 与 `orphan_terms` 缺口 / 零引用那一节完全同源——否则会出现「引用 0 却没进零引用一节」
854
+ * 这种同一张表里两种读数打架的形状。谓词那一列数的是断言条数。
855
+ */
856
+ const termRefs = termUsage(normalized, rows, entityAssertions)
857
+ const predicateRefs = new Map()
650
858
  for (const fact of rows) {
651
859
  for (const assertion of Array.isArray(fact?.assertions) ? fact.assertions : []) {
652
860
  const predicate = text(assertion?.predicate)
653
- if (predicate !== '') usage.set(predicate, (usage.get(predicate) ?? 0) + 1)
654
- const type = text(assertion?.subject?.type)
655
- if (type !== '') usage.set(type, (usage.get(type) ?? 0) + 1)
861
+ if (predicate !== '') predicateRefs.set(predicate, (predicateRefs.get(predicate) ?? 0) + 1)
656
862
  }
657
863
  }
864
+ for (const item of entityAssertions) {
865
+ const predicate = text(item?.predicate)
866
+ if (predicate !== '') predicateRefs.set(predicate, (predicateRefs.get(predicate) ?? 0) + 1)
867
+ }
658
868
  const conflicts = deriveConflicts(rows, normalized)
869
+ /** 零引用的概念:与 `deriveKnowledge` 的 `orphan_terms` 缺口读**同一份**引用面。 */
870
+ const orphans = terms.filter((term) => term.status !== 'deprecated' && (termRefs.get(term.id) ?? 0) === 0)
659
871
  const lines = [
660
872
  '# 领域本体(项目词汇)',
661
873
  '',
@@ -664,32 +876,50 @@ export function describeDomainShelf(lexicon, facts, hypotheses = []) {
664
876
  '> 语义变化(含义、主词域、值域、单值性)必须**换 id**:稳定 id 的含义不许在历史上悄悄改变。',
665
877
  '',
666
878
  ]
667
- if (terms.length === 0 && predicates.length === 0) {
879
+ if (terms.length === 0 && predicates.length === 0 && entityNodes.length === 0) {
668
880
  lines.push('(还没有词条。先注册概念与谓词,再让假设带上断言——引用不存在的词会在落账之前被拒。)', '')
669
881
  return `${lines.join('\n')}`
670
882
  }
671
- lines.push(`## 概念(${terms.length})`, '')
883
+ lines.push(`## 概念(${terms.length})`, '', '> 这一节是**语言**:可复用的类别与它们之间的 `is_a`。概念是约定,不带证据等级。', '')
672
884
  if (terms.length === 0) lines.push('(无)', '')
673
885
  else {
674
886
  lines.push('| id | 名称 | 释义 | 父概念 | 状态 | 引用 | 依据 |', '|---|---|---|---|---|---|---|')
675
887
  for (const term of terms) {
676
- lines.push(`| \`${term.id}\` | ${escapeCell(term.label ?? term.id)} | ${escapeCell(term.gloss ?? '')} | ${term.parent === null || term.parent === undefined ? '—' : `\`${term.parent}\``} | ${term.status === 'deprecated' ? '**已废止**' : '已接纳'} | ${usage.get(term.id) ?? 0} | ${escapeCell(term.basis ?? '—')} |`)
888
+ lines.push(`| \`${term.id}\` | ${escapeCell(term.label ?? term.id)} | ${escapeCell(term.gloss ?? '')} | ${term.parent === null || term.parent === undefined ? '—' : `\`${term.parent}\``} | ${term.status === 'deprecated' ? '**已废止**' : '已接纳'} | ${termRefs.get(term.id) ?? 0} | ${escapeCell(term.basis ?? '—')} |`)
677
889
  }
678
890
  lines.push('')
679
891
  }
680
- lines.push(`## 谓词(${predicates.length})`, '')
892
+ lines.push(`## 个体(实例)(${entityNodes.length})`, '', '> 这一节是**具体物**:某个实例在某出处下成立——它带类型与出处,不是约定,也不能再当概念用。', '')
893
+ if (entityNodes.length === 0) {
894
+ lines.push('(没有实例。`RegisterInstance` 登记一个,`Assert` 让它在图上长出边——只登记节点不产边,图仍然是空的。)', '')
895
+ } else {
896
+ lines.push('| id | 名称 | 类型 | 来源 | 依据 / 出处 |', '|---|---|---|---|---|')
897
+ for (const node of entityNodes) {
898
+ const registered = node.source === 'registered'
899
+ const evidence = registered ? [node.basis, isPlainObject(node.provenance) ? node.provenance.ref : null].filter((item) => typeof item === 'string' && item !== '').join(' · ') || '—' : node.source === 'asserted' ? '实体断言(有出处,未经独立裁决)' : '已升格事实的断言'
900
+ const sourceText = registered ? '已登记' : node.source === 'asserted' ? '实体断言' : '已升格'
901
+ lines.push(`| \`${node.id}\` | ${escapeCell(node.label ?? node.ref)} | ${text(node.type) === '' ? '—' : `\`${text(node.type)}\``} | ${sourceText} | ${escapeCell(evidence)} |`)
902
+ }
903
+ lines.push('')
904
+ }
905
+ lines.push(`## 谓词(${predicates.length})`, '', '> 这一节是**关系**:谓词说明两个概念/实例之间能说什么,以及它的主词域与值域。', '')
681
906
  if (predicates.length === 0) lines.push('(无)', '')
682
907
  else {
683
908
  lines.push('| id | 名称 | 主词域 | 值域 | 单值 | 状态 | 引用 | 依据 |', '|---|---|---|---|---|---|---|---|')
684
909
  for (const predicate of predicates) {
685
910
  const range = isPlainObject(predicate.range) ? predicate.range : {}
686
911
  const rangeText = text(range.term) !== '' ? `概念 \`${text(range.term)}\`` : `值形态 \`${text(range.form)}\`${text(range.unit) === '' ? '' : `(${text(range.unit)})`}`
687
- lines.push(`| \`${predicate.id}\` | ${escapeCell(predicate.label ?? predicate.id)} | ${text(predicate.domain) === '' ? '—' : `\`${text(predicate.domain)}\``} | ${rangeText} | ${predicate.functional === true ? '是' : '否'} | ${predicate.status === 'deprecated' ? '**已废止**' : '已接纳'} | ${usage.get(predicate.id) ?? 0} | ${escapeCell(predicate.basis ?? '—')} |`)
912
+ lines.push(`| \`${predicate.id}\` | ${escapeCell(predicate.label ?? predicate.id)} | ${text(predicate.domain) === '' ? '—' : `\`${text(predicate.domain)}\``} | ${rangeText} | ${predicate.functional === true ? '是' : '否'} | ${predicate.status === 'deprecated' ? '**已废止**' : '已接纳'} | ${predicateRefs.get(predicate.id) ?? 0} | ${escapeCell(predicate.basis ?? '—')} |`)
688
913
  }
689
914
  lines.push('')
690
915
  }
691
916
  const mermaid = describeDomainGraph(normalized)
692
917
  if (mermaid !== '') lines.push('## 图', '', mermaid, '')
918
+ if (orphans.length > 0) {
919
+ lines.push(`## 零引用的概念(${orphans.length})`, '', '> 这一节是**还没被用起来的约定**:没有任何结论引用它们,所以它们今天还不是「已知」。', '')
920
+ for (const term of orphans) lines.push(`- \`${term.id}\`(${escapeCell(term.label ?? term.id)}):要么在断言里用起来,要么在这里如实标出它未被引用`)
921
+ lines.push('')
922
+ }
693
923
  const deprecated = [...terms, ...predicates].filter((entry) => entry.status === 'deprecated')
694
924
  if (deprecated.length > 0) {
695
925
  lines.push('## 已废止(记录保留,新断言不许再引用)', '')
@@ -699,7 +929,19 @@ export function describeDomainShelf(lexicon, facts, hypotheses = []) {
699
929
  const typed = rows.filter((fact) => Array.isArray(fact?.assertions) && fact.assertions.length > 0).length
700
930
  const propositions = Array.isArray(hypotheses) ? hypotheses : []
701
931
  const typedPropositions = propositions.filter((item) => Array.isArray(item?.assertions) && item.assertions.length > 0).length
702
- lines.push('## 使用', '', `- 已升格事实里 ${typed}/${rows.length} 条带类型化断言;流转中的命题里 ${typedPropositions}/${propositions.length} 条带断言(未升格,不计入「引用」列)。`)
932
+ lines.push('## 使用', '')
933
+ if (view !== null) {
934
+ /** 单一叙述源:进度 / 缺口 / 下一步都读 `knowledgeView`,这里不再各写一套。 */
935
+ lines.push(`- ${view.headline.now} · ${view.headline.where}`)
936
+ if (Array.isArray(view.gaps) && view.gaps.length > 0) for (const gap of view.gaps) lines.push(`- **${gap.code}**(缺口 ${gap.count}):${gap.detail} → 下一步:${gap.nextAction}`)
937
+ else lines.push('- 结构完整:今天没有欠账(语言 / 实体图 / 断言 / 证据覆盖都不缺)。')
938
+ }
939
+ lines.push(`- 已升格事实里 ${typed}/${rows.length} 条带类型化断言;流转中的命题里 ${typedPropositions}/${propositions.length} 条带断言(未升格,不计入「引用」列)。`)
940
+ if (entityNodes.length > 0) {
941
+ const bySource = { registered: 0, promoted: 0, asserted: 0 }
942
+ for (const node of entityNodes) bySource[node.source] = (bySource[node.source] ?? 0) + 1
943
+ lines.push(`- 实体图:${entityNodes.length} 个实例节点(已登记 ${bySource.registered ?? 0} · 已升格 ${bySource.promoted ?? 0} · 实体断言 ${bySource.asserted ?? 0});${entityAssertions.length} 条实体断言(有出处,未经独立裁决)。`)
944
+ }
703
945
  if (conflicts.length > 0) {
704
946
  lines.push(`- **冲突 ${conflicts.length} 对**(只暴露,不裁决):`)
705
947
  for (const conflict of conflicts) {