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
|
@@ -401,7 +401,22 @@ export const HUMAN_GATE_MARK = '[clearai·人门]'
|
|
|
401
401
|
* 两侧白名单一旦各自维护,摘动词时只改一侧 ⇒ 内核仍认、宿主不认 ⇒ **等价性用例立刻红** ✓。
|
|
402
402
|
* 这正是那条用例存在的意义:两份实现漂移不许静默。
|
|
403
403
|
*/
|
|
404
|
-
|
|
404
|
+
/**
|
|
405
|
+
* **逐字镜像 `ui/lib/fold.js` 的同名清单**(预设面不能 import 宿主半,于是只能两份;
|
|
406
|
+
* 两份相等由 `test/authority-boundary.test.mjs` 钉死——它红的时候,是清单漂了,不是测试坏了)。
|
|
407
|
+
*/
|
|
408
|
+
export const HUMAN_GATE_ACTIONS = [
|
|
409
|
+
'adopt_branch',
|
|
410
|
+
'abandon_fork',
|
|
411
|
+
'promote_skill',
|
|
412
|
+
'retract_fact',
|
|
413
|
+
'keep_fact',
|
|
414
|
+
'confirm_provisional',
|
|
415
|
+
'register_term',
|
|
416
|
+
'register_predicate',
|
|
417
|
+
'revise_term',
|
|
418
|
+
'deprecate_entry',
|
|
419
|
+
]
|
|
405
420
|
export function parseHumanGateMessage(message) {
|
|
406
421
|
if (message === null || typeof message !== 'object') return null
|
|
407
422
|
// 署名必须是人:插件与模型来源的同名标记不算人门动作(与宿主半同一条纪律)。
|
|
@@ -446,6 +461,7 @@ export const CONFIG_KEYS = [
|
|
|
446
461
|
'templateDir',
|
|
447
462
|
'l4RejectSelfWritten',
|
|
448
463
|
'minHypotheses',
|
|
464
|
+
'requireTypedPromotion',
|
|
449
465
|
'bashDenyRules',
|
|
450
466
|
'auditProvider',
|
|
451
467
|
'auditTimeoutMs',
|
|
@@ -478,6 +494,8 @@ export const MECHANISM_TOOLS = {
|
|
|
478
494
|
brain: ['SaveSkill', 'WriteMemory'],
|
|
479
495
|
/** 账本两件:它长在 git 上(A 层用工作区自己的仓库,B 层用数据区的旁路账本,与世界线共用一本)。 */
|
|
480
496
|
ledger: ['FileHistory', 'RestoreFile'],
|
|
497
|
+
/** 领域语言:七个动词 = 词汇的全部写入口(注册/修订/废止) + 一个读入口(按概念取已知)。 */
|
|
498
|
+
ontology: ['RegisterTerm', 'RegisterPredicate', 'ReviseTerm', 'RevisePredicate', 'DeprecateTerm', 'DeprecatePredicate', 'QueryKnowledge'],
|
|
481
499
|
}
|
|
482
500
|
const TOOL_CATALOG = new Set(Object.values(MECHANISM_TOOLS).flat())
|
|
483
501
|
|
|
@@ -625,6 +643,14 @@ export function apply(ctx, config = {}) {
|
|
|
625
643
|
l4RejectSelfWritten: config.l4RejectSelfWritten !== false,
|
|
626
644
|
/** ClearAI 代码里「≥2 条假设」只是文案;默认不强制。 */
|
|
627
645
|
minHypotheses: config.minHypotheses ?? 0,
|
|
646
|
+
/**
|
|
647
|
+
* **知识门**:将要升格的命题必须已有断言的形态,否则结案被拒。
|
|
648
|
+
*
|
|
649
|
+
* 与 `minHypotheses` 是**两条不同的立场**,所以是两个键,不是一个:
|
|
650
|
+
* 前者说「开工要有候选对比」,这条说「结论要有形态」。一个部署完全可以只要前者。
|
|
651
|
+
* 机制侧缺省关(= 断言始终是加法),preset 里写 true——与 `blockedThreshold` 同一个模式。
|
|
652
|
+
*/
|
|
653
|
+
requireTypedPromotion: config.requireTypedPromotion === true,
|
|
628
654
|
bashDenyRules: config.bashDenyRules !== false,
|
|
629
655
|
auditProvider: config.auditProvider ?? 'spawn',
|
|
630
656
|
auditTimeoutMs: config.auditTimeoutMs ?? 240000,
|
|
@@ -703,6 +729,24 @@ export function apply(ctx, config = {}) {
|
|
|
703
729
|
return process.cwd()
|
|
704
730
|
}
|
|
705
731
|
|
|
732
|
+
/**
|
|
733
|
+
* 这个会话是不是**派出去的子会话**(评估者 / 侦察 / 执行者 / 横评仲裁)。
|
|
734
|
+
*
|
|
735
|
+
* 判据读宿主的会话头:`dsh-subagent` 生成子会话时写死 `parentSession`——与它写死
|
|
736
|
+
* `cwd: parentHeader.cwd` 是同一处,所以「共享工作区」与「身份是子会话」总是一起出现。
|
|
737
|
+
* 会话服务问不出来时按「不是子会话」处理:与 `sessionCwd` 的退路同一个方向,
|
|
738
|
+
* 拿不到证据时维持既有行为,不新增一条静默分支。
|
|
739
|
+
*/
|
|
740
|
+
function isSpawnedChild(sessionId) {
|
|
741
|
+
try {
|
|
742
|
+
const header = ctx.get('sessions')?.get?.(sessionId)?.header
|
|
743
|
+
if (header === null || typeof header !== 'object') return false
|
|
744
|
+
return header.origin === 'subagent' || header.parentSession !== undefined
|
|
745
|
+
} catch {
|
|
746
|
+
return false
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
706
750
|
/**
|
|
707
751
|
* 认一条假设:`id` 最稳,**原文**与**唯一前缀**(≥8 字)也认。
|
|
708
752
|
*
|
|
@@ -2557,7 +2601,7 @@ export function apply(ctx, config = {}) {
|
|
|
2557
2601
|
const facts = Array.isArray(state?.facts) ? state.facts : []
|
|
2558
2602
|
if (facts.length === 0) return null
|
|
2559
2603
|
const lines = [
|
|
2560
|
-
'#
|
|
2604
|
+
'# 本体内容(已确立条目,可作为「已知」引用;未结构化的旧条目照旧在架)',
|
|
2561
2605
|
'',
|
|
2562
2606
|
'> 每条都带**边界**(推翻条件)与**支持等级**:引用它之前先看边界还在不在。',
|
|
2563
2607
|
'> 这一份由系统维护(做的人不能写 `clear/knowledge/facts`);新的在前。',
|
|
@@ -2587,6 +2631,11 @@ export function apply(ctx, config = {}) {
|
|
|
2587
2631
|
|
|
2588
2632
|
/** 写货架;内容没变就返回 null(调用方据此决定要不要在卡里提一句)。 */
|
|
2589
2633
|
function ensureFactsShelf(sessionId, state) {
|
|
2634
|
+
/**
|
|
2635
|
+
* 与词汇货架**同一条所有权规则,同一个位置**(写入口):子会话的投影里
|
|
2636
|
+
* 没有主线的事实,让它铺只会按它自己那份重写 `INDEX.md`。
|
|
2637
|
+
*/
|
|
2638
|
+
if (isSpawnedChild(sessionId)) return null
|
|
2590
2639
|
/**
|
|
2591
2640
|
* 货架要显示「被推翻」那个读数,而它是**派生的**(fold 的 derive),不在原始状态里。
|
|
2592
2641
|
* 所以这里问一次读面,而不是在货架里重算一遍(重算 = 第二份判据,必然漂)。
|
|
@@ -2605,6 +2654,74 @@ export function apply(ctx, config = {}) {
|
|
|
2605
2654
|
}
|
|
2606
2655
|
}
|
|
2607
2656
|
|
|
2657
|
+
/**
|
|
2658
|
+
* **领域词汇货架**:把折出来的词汇落成 `clear/ontology/domain.md`。
|
|
2659
|
+
*
|
|
2660
|
+
* 与过程本体货架同一条纪律(读面 + 幂等),但**触发时机不同**:过程本体随发布版本,
|
|
2661
|
+
* 一个会话铺一次就够;词汇每一步都可能变,所以每个本体动词落账之后都要重铺一遍——
|
|
2662
|
+
* 内容没变就不重写(文件时间戳是给人的读数,不是噪声)。
|
|
2663
|
+
*
|
|
2664
|
+
* 带 `mutations` 时按**这一步之后**的样子渲染:工具返回前货架就已更新,读的人不必等下一回合。
|
|
2665
|
+
*/
|
|
2666
|
+
/**
|
|
2667
|
+
* 铺领域词汇货架(幂等)。
|
|
2668
|
+
*
|
|
2669
|
+
* **写入口自带所有权判据:派出去的子会话结构上写不进这份文件。**
|
|
2670
|
+
*
|
|
2671
|
+
* 规则一句话:工作区级读面属于拥有账本的会话。子会话(评估者 / 侦察 / 执行者)与主线
|
|
2672
|
+
* 共享同一个工作区,却持有**另一份(空的)投影**——让它照自己的投影重铺,
|
|
2673
|
+
* `renderShelf(子会话)` 渲染出的就是「还没有词条」的占位版。真跑里评估者两次读到
|
|
2674
|
+
* 7 行占位版、主线连读三次都是 96 行 21 词条,两边各自稳定:文件在「谁最后铺了一拍」
|
|
2675
|
+
* 之间摆动,而两边谁都没说谎。子会话**读**这份货架(评估者核对判据正要读它),但不写。
|
|
2676
|
+
*
|
|
2677
|
+
* 判据放在**写函数里**而不是调用点,与 `tools/pre-execute` 拒模型写 `clear/` 是同一条
|
|
2678
|
+
* 纪律:边界住在咽喉点,新增多少调用点都绕不过(不可表达优于不可违反)。
|
|
2679
|
+
*/
|
|
2680
|
+
function ensureDomainShelf(hostService, sessionId, mutations = []) {
|
|
2681
|
+
if (hostService?.domain?.renderShelf === undefined) return ''
|
|
2682
|
+
if (isSpawnedChild(sessionId)) return ''
|
|
2683
|
+
try {
|
|
2684
|
+
const body = hostService.domain.renderShelf(sessionId, Array.isArray(mutations) ? mutations : [])
|
|
2685
|
+
const file = join(sessionCwd(sessionId), 'clear', 'ontology', 'domain.md')
|
|
2686
|
+
if (existsSync(file) && readFileSync(file, 'utf8') === body) return ''
|
|
2687
|
+
writeTextFile(file, body)
|
|
2688
|
+
return `\n词汇货架已更新:${join('clear', 'ontology', 'domain.md')}(概念 / 谓词 / 图 / 引用)。**它是读面,不是权威**——要改词汇就调注册 / 修订 / 废止动词。`
|
|
2689
|
+
} catch (error) {
|
|
2690
|
+
ctx.logger?.warn?.(`clearai domain shelf: 写入失败 ${String(error?.message ?? error).slice(0, 160)}`)
|
|
2691
|
+
return ''
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
/**
|
|
2696
|
+
* 领域判据的宿主入口。**拿不到就明确拒,不抛**:预设与宿主半同包同版本,
|
|
2697
|
+
* 但一个缺了这道门的宿主(旧包、裁剪过的部署、测试桩)不该让工具在 `undefined` 上崩——
|
|
2698
|
+
* 崩掉会让模型以为是自己的参数错了,而真实原因是这一层没接上。
|
|
2699
|
+
*/
|
|
2700
|
+
function domainJudge(hostService) {
|
|
2701
|
+
return hostService?.domain ?? null
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
/** 在折出来的词汇里找一个条目(概念或谓词)。判据与折法同源:读的就是 `state.lexicon`。 */
|
|
2705
|
+
function findLexiconEntry(state, id) {
|
|
2706
|
+
const lexicon = state?.lexicon ?? {}
|
|
2707
|
+
const wanted = String(id ?? '').trim()
|
|
2708
|
+
if (wanted === '') return null
|
|
2709
|
+
const term = (Array.isArray(lexicon.terms) ? lexicon.terms : []).find((item) => item.id === wanted)
|
|
2710
|
+
if (term !== undefined) return { kind: 'term', entry: term }
|
|
2711
|
+
const predicate = (Array.isArray(lexicon.predicates) ? lexicon.predicates : []).find((item) => item.id === wanted)
|
|
2712
|
+
if (predicate !== undefined) return { kind: 'predicate', entry: predicate }
|
|
2713
|
+
return null
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
/** 查询条件的一行人话:让「查不到」也说得清查的是什么。 */
|
|
2717
|
+
function describeFilter(filter) {
|
|
2718
|
+
const parts = []
|
|
2719
|
+
if (filter.term !== '') parts.push(`概念「${filter.term}」`)
|
|
2720
|
+
if (filter.predicate !== '') parts.push(`谓词「${filter.predicate}」`)
|
|
2721
|
+
if (filter.subject !== '') parts.push(`主体「${filter.subject}」`)
|
|
2722
|
+
return `查询:${parts.join(' · ')}`
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2608
2725
|
/**
|
|
2609
2726
|
* **本体货架**:把已装的那份本体落成 `clear/ontology/<id>.md`。
|
|
2610
2727
|
*
|
|
@@ -2685,10 +2802,34 @@ export function apply(ctx, config = {}) {
|
|
|
2685
2802
|
promote_at_level: { type: 'string', enum: LEVELS, description: '升格门槛(默认 L3)' },
|
|
2686
2803
|
hypotheses: {
|
|
2687
2804
|
type: 'array',
|
|
2688
|
-
description:
|
|
2805
|
+
description:
|
|
2806
|
+
'候选假设:每条一句话主张 + 一句推翻条件;可带**类型化断言**(可选,提供即严校:谓词与概念必须已登记、宾语形态要合值域、同一事实里不许自相矛盾)。不写断言照旧成立——断言是加法,不是门槛。',
|
|
2689
2807
|
items: {
|
|
2690
2808
|
type: 'object',
|
|
2691
|
-
properties: {
|
|
2809
|
+
properties: {
|
|
2810
|
+
claim: { type: 'string' },
|
|
2811
|
+
refute_when: { type: 'string' },
|
|
2812
|
+
assertions: {
|
|
2813
|
+
type: 'array',
|
|
2814
|
+
description: '断言:主词–谓词–宾语(引用领域词汇里的 id)',
|
|
2815
|
+
items: {
|
|
2816
|
+
type: 'object',
|
|
2817
|
+
properties: {
|
|
2818
|
+
predicate: { type: 'string' },
|
|
2819
|
+
subject: { type: 'object', properties: { id: { type: 'string' }, type: { type: 'string' } }, required: ['id'], additionalProperties: false },
|
|
2820
|
+
object: {
|
|
2821
|
+
type: 'object',
|
|
2822
|
+
properties: { kind: { type: 'string', enum: ['statement', 'quantity', 'formula', 'code', 'reference', 'instance'] }, value: {}, unit: { type: 'string' }, type: { type: 'string' } },
|
|
2823
|
+
required: ['kind'],
|
|
2824
|
+
additionalProperties: false,
|
|
2825
|
+
},
|
|
2826
|
+
qualifiers: { type: 'object' },
|
|
2827
|
+
},
|
|
2828
|
+
required: ['predicate', 'subject', 'object'],
|
|
2829
|
+
additionalProperties: false,
|
|
2830
|
+
},
|
|
2831
|
+
},
|
|
2832
|
+
},
|
|
2692
2833
|
required: ['claim', 'refute_when'],
|
|
2693
2834
|
additionalProperties: false,
|
|
2694
2835
|
},
|
|
@@ -2714,6 +2855,19 @@ export function apply(ctx, config = {}) {
|
|
|
2714
2855
|
for (const hypothesis of hypotheses) {
|
|
2715
2856
|
if (typeof hypothesis?.claim !== 'string' || hypothesis.claim.trim() === '') return fail('hypothesis_claim_required', '每条假设要有一句话主张。')
|
|
2716
2857
|
if (typeof hypothesis?.refute_when !== 'string' || hypothesis.refute_when.trim() === '') return fail('hypothesis_refute_required', '每条假设必须写清「什么结果会推翻它」——没有推翻条件的假设无法被检验。')
|
|
2858
|
+
/**
|
|
2859
|
+
* **宽松+校验**:不写断言放行(断言是加法),写了就在**落账之前**严校——
|
|
2860
|
+
* 引用不存在的谓词 / 概念、宾语形态不合值域、同一事实自相矛盾,一律当场拒。
|
|
2861
|
+
* 判据来自宿主半(`hostService.domain`),与折法和读面同源。
|
|
2862
|
+
*/
|
|
2863
|
+
if (hypothesis.assertions !== undefined && hypothesis.assertions !== null) {
|
|
2864
|
+
const judge = domainJudge(hostService)
|
|
2865
|
+
if (judge === null) return fail('domain_unavailable', '这一层的宿主没有提供领域判据(domain facade):无法校验断言。请检查宿主半与预设是否同版本。')
|
|
2866
|
+
const problems = judge.validateAssertions(sessionId, hypothesis.assertions)
|
|
2867
|
+
if (problems.length > 0) {
|
|
2868
|
+
return fail('assertions_rejected', `这条假设的断言不能成立(先注册词汇,或改断言):\n${problems.map((item) => `- ${item}`).join('\n')}`)
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2717
2871
|
}
|
|
2718
2872
|
// 假设数量下限:首次立目标就得带够候选——0 条一样拦(候选对比是检验的前提,
|
|
2719
2873
|
// 只有一个猜想时「验证」容易退化成找证据支持自己)。修订不受此限。
|
|
@@ -2727,6 +2881,35 @@ export function apply(ctx, config = {}) {
|
|
|
2727
2881
|
const goalId = isRevision ? state.goal.id : uniqueId('g')
|
|
2728
2882
|
const revision = isRevision ? state.goal.revision + 1 : 1
|
|
2729
2883
|
const promoteAtLevel = LEVELS.includes(args.promote_at_level) ? args.promote_at_level : 'L3'
|
|
2884
|
+
/**
|
|
2885
|
+
* **修订不许给同一句话发新身份。**
|
|
2886
|
+
*
|
|
2887
|
+
* 真跑踩出来的:一轮长跑里目标改过一次版,卡上就出现 4 条主张的 6~8 行读数——
|
|
2888
|
+
* 同一句话挂着两个 id、各报一个状态(一个「已支持」、另一个「未触及」),
|
|
2889
|
+
* 模型得自己去调和两份自相矛盾的读数。而 id 是身份:主张原文没变就该用回原来的 id,
|
|
2890
|
+
* 这样「这条猜想被验到哪一级」跨版本仍然接着算。
|
|
2891
|
+
*
|
|
2892
|
+
* 反过来,**这一版没再列出来的**要如实落成 `hypothesis/superseded`:
|
|
2893
|
+
* 折法早就认识这条变更,只是从来没有人发过它(与 `retracted` 当年那个「声明了没有生产者」
|
|
2894
|
+
* 是同一种病)。不发它,被放弃的猜想会永远挂在 `proposed` 上,结案时又变成一条假的「没看过」。
|
|
2895
|
+
*/
|
|
2896
|
+
const existing = state.hypotheses.filter((item) => item.goal === goalId)
|
|
2897
|
+
const claimKey = (text) => String(text ?? '').trim().replace(/\s+/g, ' ')
|
|
2898
|
+
const idByClaim = new Map(existing.map((item) => [claimKey(item.claim), item.id]))
|
|
2899
|
+
const reused = new Set()
|
|
2900
|
+
const nextHypotheses = hypotheses.map((hypothesis, index) => {
|
|
2901
|
+
const claim = hypothesis.claim.trim()
|
|
2902
|
+
const carried = idByClaim.get(claimKey(claim))
|
|
2903
|
+
if (carried !== undefined) reused.add(carried)
|
|
2904
|
+
return {
|
|
2905
|
+
id: carried ?? `h-${Math.random().toString(36).slice(2, 8)}`,
|
|
2906
|
+
claim,
|
|
2907
|
+
refute_when: hypothesis.refute_when.trim(),
|
|
2908
|
+
/** 断言随假设落账;没写就是 null(加法,不是门槛)。 */
|
|
2909
|
+
assertions: Array.isArray(hypothesis.assertions) ? hypothesis.assertions : null,
|
|
2910
|
+
version: index + 1,
|
|
2911
|
+
}
|
|
2912
|
+
})
|
|
2730
2913
|
mutations.push({
|
|
2731
2914
|
t: 'goal/set',
|
|
2732
2915
|
id: goalId,
|
|
@@ -2735,13 +2918,14 @@ export function apply(ctx, config = {}) {
|
|
|
2735
2918
|
promote_at_level: promoteAtLevel,
|
|
2736
2919
|
revision,
|
|
2737
2920
|
reason: isRevision ? String(args.reason).trim() : null,
|
|
2738
|
-
hypotheses:
|
|
2739
|
-
id: `h-${Math.random().toString(36).slice(2, 8)}`,
|
|
2740
|
-
claim: hypothesis.claim.trim(),
|
|
2741
|
-
refute_when: hypothesis.refute_when.trim(),
|
|
2742
|
-
version: index + 1,
|
|
2743
|
-
})),
|
|
2921
|
+
hypotheses: nextHypotheses,
|
|
2744
2922
|
})
|
|
2923
|
+
for (const dropped of existing) {
|
|
2924
|
+
if (reused.has(dropped.id)) continue
|
|
2925
|
+
const promoted = (state.facts ?? []).some((fact) => fact.hypothesis === dropped.id)
|
|
2926
|
+
if (promoted) continue
|
|
2927
|
+
mutations.push({ t: 'hypothesis/superseded', goal: goalId, id: dropped.id, claim: dropped.claim, by: `rev${revision}` })
|
|
2928
|
+
}
|
|
2745
2929
|
let scoutNote = ''
|
|
2746
2930
|
if (!isRevision && CFG.precommitRecon) {
|
|
2747
2931
|
// 立约前侦察:harness 发起(不是模型请求),一生一次,且只在真的有人给过材料时做
|
|
@@ -2836,6 +3020,43 @@ export function apply(ctx, config = {}) {
|
|
|
2836
3020
|
)
|
|
2837
3021
|
}
|
|
2838
3022
|
const derived = hostService.derive(sessionId)
|
|
3023
|
+
/**
|
|
3024
|
+
* **知识门:核心结论不许以纯散文升格。**
|
|
3025
|
+
*
|
|
3026
|
+
* 位置有讲究——它坐在「计划已收尾」之后、**派评估者之前**。判据与准入同一条顺序纪律:
|
|
3027
|
+
* 先把能做的前提查完,再花钱请人裁决;等评估卡回来才发现没形态,那一次子 run 就白花了。
|
|
3028
|
+
*
|
|
3029
|
+
* 为什么需要它:断言一直是「加法,不是门槛」,于是真跑里模型的最优策略就是
|
|
3030
|
+
* 「检索 → 总结 → 写报告」——本体、实体、认识论三张图都长不出来,因为完成函数里没有它们。
|
|
3031
|
+
* 让缺口进卡(见 `renderCard`)只解决「看得见」;这一道解决「绕不过」。
|
|
3032
|
+
*
|
|
3033
|
+
* **判据是结构谓词,不是词面**:将要升格的命题里,只要有一条没有断言就拦。
|
|
3034
|
+
* 出口有两条,都是诚实的:补上断言的形态再结,或者如实 `abandoned`。
|
|
3035
|
+
* 缺口不许被伪装成 support(那是「造证」,比不结案坏得多)。
|
|
3036
|
+
*
|
|
3037
|
+
* 开关是 `requireTypedPromotion`(机制缺省关,preset 里开):它与 `minHypotheses`
|
|
3038
|
+
* 是两条不同的立场,所以不共用一个键。另外它**只在知识模式下生效**——
|
|
3039
|
+
* 没有登记的命题就没有「形态」可谈,那时拦下来的只是一句空话。
|
|
3040
|
+
*/
|
|
3041
|
+
if (CFG.requireTypedPromotion && derived.knowledge.mode === 'knowledge') {
|
|
3042
|
+
const threshold = levelIndexOf(goal.promote_at_level)
|
|
3043
|
+
/** 与下面那段升格循环**逐字同一套谓词**:将要升格的就是这几条,一条不多一条不少。 */
|
|
3044
|
+
const promotable = derived.hypotheses.filter(
|
|
3045
|
+
(hypothesis) =>
|
|
3046
|
+
(hypothesis.status === 'alive' || hypothesis.status === 'proposed') &&
|
|
3047
|
+
(hypothesis.refutations ?? 0) === 0 &&
|
|
3048
|
+
levelIndexOf(hypothesis.supportedLevel) >= threshold,
|
|
3049
|
+
)
|
|
3050
|
+
const untyped = promotable.filter((hypothesis) => !Array.isArray(hypothesis.assertions) || hypothesis.assertions.length === 0)
|
|
3051
|
+
if (untyped.length > 0) {
|
|
3052
|
+
return fail(
|
|
3053
|
+
'claims_untyped',
|
|
3054
|
+
`有 ${untyped.length} 条命题已经验到门槛、却**没有断言的形态**,再往下就是散文升格:\n${untyped
|
|
3055
|
+
.map((hypothesis) => `- ${hypothesis.id}(${hypothesis.supportedLevel}):${hypothesis.claim}`)
|
|
3056
|
+
.join('\n')}\n把结论写成「主词 · 谓词 = 宾语」才进得了实体图,下一轮也才按概念取用得到。两条路:\n① **补形态再结**:词汇里没有对应的概念 / 谓词就先 \`RegisterTerm\` / \`RegisterPredicate\`,再用 \`SetGoal\` 修订目标、把这些命题连断言一起重列一遍(主张原文一字不动就会用回原 id,验到哪一级接着算),然后重新结案;\n② **如实放弃**:这些结论不值得留下形态,就用 \`CloseGoal(outcome="abandoned")\` 说清阻塞收兵。\n别为了让门放行而编一个词——词汇是约定,它将长期约束这个项目怎么写结论。`,
|
|
3057
|
+
)
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
2839
3060
|
const unfinished = plan === null ? [] : plan.steps.filter((step) => step.status === 'open')
|
|
2840
3061
|
const syntheticStep = { id: `goal:${goal.id}`, ordinal: 0, do: `核验目标 ${goal.id} 的判据与转写忠实度`, done_criteria: goal.done_criteria, artifacts: [], tests: null }
|
|
2841
3062
|
const gate = {
|
|
@@ -2909,10 +3130,17 @@ export function apply(ctx, config = {}) {
|
|
|
2909
3130
|
t: 'fact/promoted',
|
|
2910
3131
|
id: factId,
|
|
2911
3132
|
goal: goal.id,
|
|
3133
|
+
/**
|
|
3134
|
+
* **身份与内容一起定型**:`hypothesis` 是产出它的那条假设(按 id 关联,
|
|
3135
|
+
* 措辞改了也认得出),`assertions` 是这条事实的类型化内容(没写就是 null)。
|
|
3136
|
+
* 断言只在**升格这一刻**落地——旧事实不会被回溯改写。
|
|
3137
|
+
*/
|
|
3138
|
+
hypothesis: hypothesis.id,
|
|
2912
3139
|
text: hypothesis.claim,
|
|
2913
3140
|
scope: hypothesis.refute_when ?? null,
|
|
2914
3141
|
level: hypothesis.supportedLevel ?? null,
|
|
2915
3142
|
evidence: state.evidence.filter((item) => item.verdict === 'support').map((item) => item.id),
|
|
3143
|
+
assertions: Array.isArray(hypothesis.assertions) ? hypothesis.assertions : null,
|
|
2916
3144
|
path,
|
|
2917
3145
|
})
|
|
2918
3146
|
promoted.push(hypothesis.claim)
|
|
@@ -3007,6 +3235,301 @@ export function apply(ctx, config = {}) {
|
|
|
3007
3235
|
additionalProperties: false,
|
|
3008
3236
|
}
|
|
3009
3237
|
|
|
3238
|
+
|
|
3239
|
+
// ── 领域语言(本体动词)─────────────────────────────────────────────────
|
|
3240
|
+
/**
|
|
3241
|
+
* 七个动词 = 领域词汇的**全部写入口**(注册 / 修订 / 废止,概念与谓词各一套)+ 一个读入口。
|
|
3242
|
+
*
|
|
3243
|
+
* 它们只做一件事:把「这个领域有哪些概念、哪些谓词、关系取什么值形态」写进账本事件,
|
|
3244
|
+
* 由折法折成 `state.lexicon`;再由同一份折法长出本体图、冲突读数与货架。
|
|
3245
|
+
*
|
|
3246
|
+
* 四条贯穿所有动词的纪律:
|
|
3247
|
+
* · **判据只有一份**——校验经 `hostService.domain.*`(与折法、读面同源),预设侧不复制规则;
|
|
3248
|
+
* · **依据必填**——约定可以自愿,不能无来由;
|
|
3249
|
+
* · **没有删除**——修订留版本,废止留缘由且是黏性终态;
|
|
3250
|
+
* · **语义变化必须换 id**——改展示信息走修订,改含义/主词域/值域走「废止 + 新注册」。
|
|
3251
|
+
*/
|
|
3252
|
+
defineTool({
|
|
3253
|
+
name: 'RegisterTerm',
|
|
3254
|
+
description:
|
|
3255
|
+
'登记一个领域概念(本体图上的节点)。id 用小写 slug;`gloss` 一句话说清它指什么;`basis` 必填——哪份材料、哪条事实或人说的哪句话让这个词成立。`parent` 可选(is_a,只能连概念)。登记是**约定**不是主张:它不需要证据等级,但从此可以出现在断言里。要改展示信息用 ReviseTerm,要作废用 DeprecateTerm(记录不会删)。',
|
|
3256
|
+
parameters: {
|
|
3257
|
+
type: 'object',
|
|
3258
|
+
properties: {
|
|
3259
|
+
id: { type: 'string', description: '小写字母开头的 slug(字母/数字/下划线,≤40)' },
|
|
3260
|
+
label: { type: 'string', description: '给人看的名字' },
|
|
3261
|
+
gloss: { type: 'string', description: '一句话释义' },
|
|
3262
|
+
aliases: { type: 'array', items: { type: 'string' }, description: '别名(可选)' },
|
|
3263
|
+
parent: { type: 'string', description: '父概念 id(可选,is_a)' },
|
|
3264
|
+
basis: { type: 'string', description: '依据:哪份材料 / 哪条事实 / 谁说的' },
|
|
3265
|
+
},
|
|
3266
|
+
required: ['id', 'label', 'gloss', 'basis'],
|
|
3267
|
+
additionalProperties: false,
|
|
3268
|
+
},
|
|
3269
|
+
output: CARD_OUTPUT,
|
|
3270
|
+
async execute(args, exec) {
|
|
3271
|
+
const call = open(exec)
|
|
3272
|
+
if (call.ok !== true) return call.response
|
|
3273
|
+
const { hostService, sessionId, mutations } = call
|
|
3274
|
+
const done = finish(hostService, sessionId, mutations)
|
|
3275
|
+
const judge = domainJudge(hostService)
|
|
3276
|
+
if (judge === null) return fail('domain_unavailable', '这一层的宿主没有提供领域判据(domain facade):无法校验词汇。请检查宿主半与预设是否同版本。')
|
|
3277
|
+
const problems = judge.validateTerm(sessionId, args)
|
|
3278
|
+
if (problems.length > 0) return fail('term_rejected', `这个词不能登记:\n${problems.map((item) => `- ${item}`).join('\n')}`)
|
|
3279
|
+
mutations.push({
|
|
3280
|
+
t: 'ontology/term_added',
|
|
3281
|
+
id: String(args.id).trim(),
|
|
3282
|
+
label: String(args.label).trim(),
|
|
3283
|
+
gloss: String(args.gloss).trim(),
|
|
3284
|
+
aliases: Array.isArray(args.aliases) ? args.aliases.map((alias) => String(alias)) : [],
|
|
3285
|
+
parent: args.parent === undefined ? null : String(args.parent).trim(),
|
|
3286
|
+
basis: String(args.basis).trim(),
|
|
3287
|
+
})
|
|
3288
|
+
const note = ensureDomainShelf(hostService, sessionId, mutations)
|
|
3289
|
+
return done({
|
|
3290
|
+
ok: true,
|
|
3291
|
+
code: 'term_registered',
|
|
3292
|
+
message: `概念 ${String(args.id).trim()} 已登记。它现在是本体图上的一个节点:新断言可以引用它。${note}`,
|
|
3293
|
+
})
|
|
3294
|
+
},
|
|
3295
|
+
})
|
|
3296
|
+
|
|
3297
|
+
defineTool({
|
|
3298
|
+
name: 'RegisterPredicate',
|
|
3299
|
+
description:
|
|
3300
|
+
'登记一个领域谓词(本体图上的边)。值域二选一:`range={form:"quantity"|"statement"|"formula"|"code"|"reference",unit?}` 说宾语是一个**字面值**,或 `range={term:"<概念 id>"}` 说宾语是**另一个概念的实例**。`domain` 可选(主词域,声明了它,断言的主体就必须写明类型)。`functional=true` 表示单值:同一主体出现两个不同取值时,投影会给出一对**冲突**(只暴露,不裁决)。',
|
|
3301
|
+
parameters: {
|
|
3302
|
+
type: 'object',
|
|
3303
|
+
properties: {
|
|
3304
|
+
id: { type: 'string', description: '小写字母开头的 slug' },
|
|
3305
|
+
label: { type: 'string', description: '给人看的名字' },
|
|
3306
|
+
gloss: { type: 'string', description: '一句话释义(可选)' },
|
|
3307
|
+
domain: { type: 'string', description: '主词域:概念 id(可选)' },
|
|
3308
|
+
range: {
|
|
3309
|
+
type: 'object',
|
|
3310
|
+
description: '值域:{form,unit?} 或 {term}',
|
|
3311
|
+
properties: {
|
|
3312
|
+
form: { type: 'string', enum: ['statement', 'quantity', 'formula', 'code', 'reference'] },
|
|
3313
|
+
unit: { type: 'string' },
|
|
3314
|
+
term: { type: 'string' },
|
|
3315
|
+
},
|
|
3316
|
+
additionalProperties: false,
|
|
3317
|
+
},
|
|
3318
|
+
functional: { type: 'boolean', description: '是否单值(默认否)' },
|
|
3319
|
+
basis: { type: 'string', description: '依据(必填)' },
|
|
3320
|
+
},
|
|
3321
|
+
required: ['id', 'label', 'range', 'basis'],
|
|
3322
|
+
additionalProperties: false,
|
|
3323
|
+
},
|
|
3324
|
+
output: CARD_OUTPUT,
|
|
3325
|
+
async execute(args, exec) {
|
|
3326
|
+
const call = open(exec)
|
|
3327
|
+
if (call.ok !== true) return call.response
|
|
3328
|
+
const { hostService, sessionId, mutations } = call
|
|
3329
|
+
const done = finish(hostService, sessionId, mutations)
|
|
3330
|
+
const judge = domainJudge(hostService)
|
|
3331
|
+
if (judge === null) return fail('domain_unavailable', '这一层的宿主没有提供领域判据(domain facade):无法校验词汇。请检查宿主半与预设是否同版本。')
|
|
3332
|
+
const problems = judge.validatePredicate(sessionId, args)
|
|
3333
|
+
if (problems.length > 0) return fail('predicate_rejected', `这个谓词不能登记:\n${problems.map((item) => `- ${item}`).join('\n')}`)
|
|
3334
|
+
mutations.push({
|
|
3335
|
+
t: 'ontology/predicate_added',
|
|
3336
|
+
id: String(args.id).trim(),
|
|
3337
|
+
label: String(args.label).trim(),
|
|
3338
|
+
gloss: args.gloss === undefined ? '' : String(args.gloss).trim(),
|
|
3339
|
+
domain: args.domain === undefined ? null : String(args.domain).trim(),
|
|
3340
|
+
range: args.range,
|
|
3341
|
+
functional: args.functional === true,
|
|
3342
|
+
basis: String(args.basis).trim(),
|
|
3343
|
+
})
|
|
3344
|
+
const note = ensureDomainShelf(hostService, sessionId, mutations)
|
|
3345
|
+
return done({
|
|
3346
|
+
ok: true,
|
|
3347
|
+
code: 'predicate_registered',
|
|
3348
|
+
message: `谓词 ${String(args.id).trim()} 已登记(${args.range?.term ? `宾语是概念 ${args.range.term} 的实例` : `宾语取 ${args.range?.form} 形态`}${args.functional === true ? ' · 单值' : ''})。下一步:在 SetGoal 的假设里带上断言,引用它。${note}`,
|
|
3349
|
+
})
|
|
3350
|
+
},
|
|
3351
|
+
})
|
|
3352
|
+
|
|
3353
|
+
defineTool({
|
|
3354
|
+
name: 'ReviseTerm',
|
|
3355
|
+
description:
|
|
3356
|
+
'修订一个概念的**展示信息**(名字 / 释义 / 别名):版本 +1,旧值留在账上,id 不变。**语义变化不许走这条路**——含义、父概念变了就废止旧条目、注册新条目;稳定 id 的含义在历史上悄悄改变,等于拿今天的释义重写所有旧事实。',
|
|
3357
|
+
parameters: {
|
|
3358
|
+
type: 'object',
|
|
3359
|
+
properties: {
|
|
3360
|
+
id: { type: 'string' },
|
|
3361
|
+
label: { type: 'string', description: '新名字(可选)' },
|
|
3362
|
+
gloss: { type: 'string', description: '新释义(可选)' },
|
|
3363
|
+
aliases: { type: 'array', items: { type: 'string' } },
|
|
3364
|
+
reason: { type: 'string', description: '为什么改(必填)' },
|
|
3365
|
+
},
|
|
3366
|
+
required: ['id', 'reason'],
|
|
3367
|
+
additionalProperties: false,
|
|
3368
|
+
},
|
|
3369
|
+
output: CARD_OUTPUT,
|
|
3370
|
+
async execute(args, exec) {
|
|
3371
|
+
const call = open(exec)
|
|
3372
|
+
if (call.ok !== true) return call.response
|
|
3373
|
+
const { hostService, sessionId, state, mutations } = call
|
|
3374
|
+
const done = finish(hostService, sessionId, mutations)
|
|
3375
|
+
const entry = findLexiconEntry(state, args.id)
|
|
3376
|
+
if (entry === null || entry.kind !== 'term') return fail('unknown_term', `词汇里没有这个概念:${String(args.id)}`)
|
|
3377
|
+
if (entry.entry.status === 'deprecated') return fail('term_deprecated', `概念 ${String(args.id)} 已废止,不能修订(要恢复语义就注册新条目)。`)
|
|
3378
|
+
if (typeof args.reason !== 'string' || args.reason.trim() === '') return fail('reason_required', '修订要写一句原因:改了什么、为什么改。')
|
|
3379
|
+
if (args.label === undefined && args.gloss === undefined && args.aliases === undefined) return fail('nothing_to_revise', '没有要改的字段:label / gloss / aliases 至少给一个。')
|
|
3380
|
+
mutations.push({ t: 'ontology/term_revised', id: String(args.id), label: args.label === undefined ? undefined : String(args.label).trim(), gloss: args.gloss === undefined ? undefined : String(args.gloss).trim(), aliases: Array.isArray(args.aliases) ? args.aliases.map((alias) => String(alias)) : undefined, reason: String(args.reason).trim() })
|
|
3381
|
+
const note = ensureDomainShelf(hostService, sessionId, mutations)
|
|
3382
|
+
return done({ ok: true, code: 'term_revised', message: `概念 ${String(args.id)} 已修订(旧值留在账上)。${note}` })
|
|
3383
|
+
},
|
|
3384
|
+
})
|
|
3385
|
+
|
|
3386
|
+
defineTool({
|
|
3387
|
+
name: 'RevisePredicate',
|
|
3388
|
+
description: '修订一个谓词的**展示信息**(名字 / 释义)。值域、主词域与单值性是语义——那三样变了要废止旧谓词、注册新的。',
|
|
3389
|
+
parameters: {
|
|
3390
|
+
type: 'object',
|
|
3391
|
+
properties: { id: { type: 'string' }, label: { type: 'string' }, gloss: { type: 'string' }, reason: { type: 'string' } },
|
|
3392
|
+
required: ['id', 'reason'],
|
|
3393
|
+
additionalProperties: false,
|
|
3394
|
+
},
|
|
3395
|
+
output: CARD_OUTPUT,
|
|
3396
|
+
async execute(args, exec) {
|
|
3397
|
+
const call = open(exec)
|
|
3398
|
+
if (call.ok !== true) return call.response
|
|
3399
|
+
const { hostService, sessionId, state, mutations } = call
|
|
3400
|
+
const done = finish(hostService, sessionId, mutations)
|
|
3401
|
+
const entry = findLexiconEntry(state, args.id)
|
|
3402
|
+
if (entry === null || entry.kind !== 'predicate') return fail('unknown_predicate', `词汇里没有这个谓词:${String(args.id)}`)
|
|
3403
|
+
if (entry.entry.status === 'deprecated') return fail('predicate_deprecated', `谓词 ${String(args.id)} 已废止,不能修订。`)
|
|
3404
|
+
if (typeof args.reason !== 'string' || args.reason.trim() === '') return fail('reason_required', '修订要写一句原因。')
|
|
3405
|
+
mutations.push({ t: 'ontology/predicate_revised', id: String(args.id), label: args.label === undefined ? undefined : String(args.label).trim(), gloss: args.gloss === undefined ? undefined : String(args.gloss).trim(), reason: String(args.reason).trim() })
|
|
3406
|
+
const note = ensureDomainShelf(hostService, sessionId, mutations)
|
|
3407
|
+
return done({ ok: true, code: 'predicate_revised', message: `谓词 ${String(args.id)} 已修订(版本 +1,旧值留着)。${note}` })
|
|
3408
|
+
},
|
|
3409
|
+
})
|
|
3410
|
+
|
|
3411
|
+
/**
|
|
3412
|
+
* **废止是黏性终态**:没有复活这条路。存量事实照旧可读(历史留着),
|
|
3413
|
+
* 引用它的**新**断言会被拒,并在货架上标明「所用术语已废止」。
|
|
3414
|
+
*/
|
|
3415
|
+
defineTool({
|
|
3416
|
+
name: 'DeprecateTerm',
|
|
3417
|
+
description:
|
|
3418
|
+
'废止一个概念(或谓词):缘由必填。**没有删除**——条目、旧版本与引用过它的事实全部留着;新断言不许再引用它,存量事实在货架上标明「所用术语已废止」。要恢复语义就注册一个新条目(那是覆盖,不是复活)。',
|
|
3419
|
+
parameters: {
|
|
3420
|
+
type: 'object',
|
|
3421
|
+
properties: { id: { type: 'string', description: '概念或谓词的 id' }, reason: { type: 'string', description: '为什么废止(必填)' } },
|
|
3422
|
+
required: ['id', 'reason'],
|
|
3423
|
+
additionalProperties: false,
|
|
3424
|
+
},
|
|
3425
|
+
output: CARD_OUTPUT,
|
|
3426
|
+
async execute(args, exec) {
|
|
3427
|
+
const call = open(exec)
|
|
3428
|
+
if (call.ok !== true) return call.response
|
|
3429
|
+
const { hostService, sessionId, state, mutations } = call
|
|
3430
|
+
const done = finish(hostService, sessionId, mutations)
|
|
3431
|
+
const entry = findLexiconEntry(state, args.id)
|
|
3432
|
+
if (entry === null) return fail('unknown_entry', `词汇里没有这个条目:${String(args.id)}`)
|
|
3433
|
+
if (entry.entry.status === 'deprecated') return fail('already_deprecated', `${String(args.id)} 已经是废止状态(这一步不会重复记账)。`)
|
|
3434
|
+
if (typeof args.reason !== 'string' || args.reason.trim() === '') return fail('reason_required', '废止要写缘由:为什么这个词不再用。')
|
|
3435
|
+
mutations.push({ t: entry.kind === 'term' ? 'ontology/term_deprecated' : 'ontology/predicate_deprecated', id: String(args.id), reason: String(args.reason).trim() })
|
|
3436
|
+
const note = ensureDomainShelf(hostService, sessionId, mutations)
|
|
3437
|
+
return done({
|
|
3438
|
+
ok: true,
|
|
3439
|
+
code: 'entry_deprecated',
|
|
3440
|
+
message: `${entry.kind === 'term' ? '概念' : '谓词'} ${String(args.id)} 已废止(记录保留,新断言不许再引用;引用过它的事实照旧可读)。${note}`,
|
|
3441
|
+
})
|
|
3442
|
+
},
|
|
3443
|
+
})
|
|
3444
|
+
|
|
3445
|
+
defineTool({
|
|
3446
|
+
name: 'DeprecatePredicate',
|
|
3447
|
+
description: '废止一个谓词:缘由必填,记录保留,新断言不许再引用它(与 DeprecateTerm 同一条纪律,单列出来是为了让参数与语义各自说清)。',
|
|
3448
|
+
parameters: {
|
|
3449
|
+
type: 'object',
|
|
3450
|
+
properties: { id: { type: 'string' }, reason: { type: 'string' } },
|
|
3451
|
+
required: ['id', 'reason'],
|
|
3452
|
+
additionalProperties: false,
|
|
3453
|
+
},
|
|
3454
|
+
output: CARD_OUTPUT,
|
|
3455
|
+
async execute(args, exec) {
|
|
3456
|
+
const call = open(exec)
|
|
3457
|
+
if (call.ok !== true) return call.response
|
|
3458
|
+
const { hostService, sessionId, state, mutations } = call
|
|
3459
|
+
const done = finish(hostService, sessionId, mutations)
|
|
3460
|
+
const entry = findLexiconEntry(state, args.id)
|
|
3461
|
+
if (entry === null || entry.kind !== 'predicate') return fail('unknown_predicate', `词汇里没有这个谓词:${String(args.id)}`)
|
|
3462
|
+
if (entry.entry.status === 'deprecated') return fail('already_deprecated', `${String(args.id)} 已经是废止状态。`)
|
|
3463
|
+
if (typeof args.reason !== 'string' || args.reason.trim() === '') return fail('reason_required', '废止要写缘由。')
|
|
3464
|
+
mutations.push({ t: 'ontology/predicate_deprecated', id: String(args.id), reason: String(args.reason).trim() })
|
|
3465
|
+
const note = ensureDomainShelf(hostService, sessionId, mutations)
|
|
3466
|
+
return done({ ok: true, code: 'entry_deprecated', message: `谓词 ${String(args.id)} 已废止(记录保留,新断言不许再引用)。${note}` })
|
|
3467
|
+
},
|
|
3468
|
+
})
|
|
3469
|
+
|
|
3470
|
+
/**
|
|
3471
|
+
* **螺旋上半圈的入口**:按概念 / 谓词 / 主体取「已知」。
|
|
3472
|
+
* 读数全部来自投影(事实与流转中的假设),不新建任何存储;它也不改任何东西。
|
|
3473
|
+
*/
|
|
3474
|
+
defineTool({
|
|
3475
|
+
name: 'QueryKnowledge',
|
|
3476
|
+
description:
|
|
3477
|
+
'按概念 / 谓词 / 主体查「已知」:返回带类型化断言的**已升格事实**与**还在流转的命题**。要复用前面的结论就用它,而不是把事实库重读一遍。只读:它不改任何东西。',
|
|
3478
|
+
parameters: {
|
|
3479
|
+
type: 'object',
|
|
3480
|
+
properties: {
|
|
3481
|
+
term: { type: 'string', description: '概念 id:匹配断言的主体类型 / 宾语概念 / 词条本身' },
|
|
3482
|
+
predicate: { type: 'string', description: '谓词 id' },
|
|
3483
|
+
subject: { type: 'string', description: '主体名称(实例)' },
|
|
3484
|
+
},
|
|
3485
|
+
additionalProperties: false,
|
|
3486
|
+
},
|
|
3487
|
+
output: CARD_OUTPUT,
|
|
3488
|
+
async execute(args, exec) {
|
|
3489
|
+
const call = open(exec)
|
|
3490
|
+
if (call.ok !== true) return call.response
|
|
3491
|
+
const { hostService, sessionId, state, mutations } = call
|
|
3492
|
+
const done = finish(hostService, sessionId, mutations)
|
|
3493
|
+
const derived = hostService.derive(sessionId)
|
|
3494
|
+
const filter = { term: args.term === undefined ? '' : String(args.term).trim(), predicate: args.predicate === undefined ? '' : String(args.predicate).trim(), subject: args.subject === undefined ? '' : String(args.subject).trim() }
|
|
3495
|
+
if (filter.term === '' && filter.predicate === '' && filter.subject === '') return fail('query_empty', '至少给一个条件:term / predicate / subject。')
|
|
3496
|
+
const match = (assertion) => {
|
|
3497
|
+
const subject = assertion?.subject ?? {}
|
|
3498
|
+
const object = assertion?.object ?? {}
|
|
3499
|
+
if (filter.predicate !== '' && String(assertion?.predicate ?? '') !== filter.predicate) return false
|
|
3500
|
+
if (filter.subject !== '' && String(subject.id ?? '') !== filter.subject) return false
|
|
3501
|
+
if (filter.term !== '') {
|
|
3502
|
+
const hits = [String(subject.type ?? ''), String(subject.id ?? ''), String(object.kind) === 'instance' ? String(object.type ?? '') : '', String(object.kind) === 'instance' ? String(object.value ?? '') : '', String(assertion?.predicate ?? '')]
|
|
3503
|
+
if (!hits.includes(filter.term)) return false
|
|
3504
|
+
}
|
|
3505
|
+
return true
|
|
3506
|
+
}
|
|
3507
|
+
const facts = derived.factRows.filter((fact) => (Array.isArray(fact.assertions) ? fact.assertions : []).some(match))
|
|
3508
|
+
const inFlight = derived.hypotheses.filter((hypothesis) => (Array.isArray(hypothesis.assertions) ? hypothesis.assertions : []).some(match))
|
|
3509
|
+
if (facts.length === 0 && inFlight.length === 0) {
|
|
3510
|
+
return done({ ok: true, code: 'knowledge_empty', message: `这个词汇条件下还没有任何东西:${describeFilter(filter)}。要么先登记/验证,要么换个条件——**不要**把「查不到」写成「不存在」。` })
|
|
3511
|
+
}
|
|
3512
|
+
const lines = []
|
|
3513
|
+
if (facts.length > 0) {
|
|
3514
|
+
lines.push(`已知(已升格,可作为已知引用):${facts.length} 条`)
|
|
3515
|
+
for (const fact of facts.slice(0, 12)) {
|
|
3516
|
+
lines.push(`- ${fact.id} · ${clip(String(fact.text ?? ''), 120)}(支持到 ${fact.level ?? '—'}${fact.review?.decision === 'retracted' ? ' · **已撤回**' : fact.refuted === true ? ' · 有推翻证据等人决定' : ''})`)
|
|
3517
|
+
lines.push(` 边界:${clip(String(fact.scope ?? '(未写)'), 120)}`)
|
|
3518
|
+
for (const assertion of fact.assertions ?? []) if (match(assertion)) lines.push(` 断言:${hostService.domain.format(sessionId, assertion)}`)
|
|
3519
|
+
}
|
|
3520
|
+
if (facts.length > 12) lines.push(` (还有 ${facts.length - 12} 条,见 clear/knowledge/facts/INDEX.md)`)
|
|
3521
|
+
}
|
|
3522
|
+
if (inFlight.length > 0) {
|
|
3523
|
+
lines.push(`还在流转的命题(未升格):${inFlight.length} 条`)
|
|
3524
|
+
for (const hypothesis of inFlight.slice(0, 8)) {
|
|
3525
|
+
lines.push(`- ${hypothesis.id} [${hypothesis.status}] ${clip(String(hypothesis.claim ?? ''), 100)}${hypothesis.supportedLevel === null ? '' : `(支持到 ${hypothesis.supportedLevel})`}`)
|
|
3526
|
+
for (const assertion of hypothesis.assertions ?? []) if (match(assertion)) lines.push(` 断言:${hostService.domain.format(sessionId, assertion)}`)
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
return done({ ok: true, code: 'knowledge_found', message: `${describeFilter(filter)}\n${lines.join('\n')}` })
|
|
3530
|
+
},
|
|
3531
|
+
})
|
|
3532
|
+
|
|
3010
3533
|
defineTool({
|
|
3011
3534
|
name: 'CreatePlan',
|
|
3012
3535
|
description:
|
|
@@ -5707,7 +6230,12 @@ export function apply(ctx, config = {}) {
|
|
|
5707
6230
|
|
|
5708
6231
|
function protectedRoots(sessionId) {
|
|
5709
6232
|
const cwd = sessionCwd(sessionId)
|
|
5710
|
-
|
|
6233
|
+
/**
|
|
6234
|
+
* 系统所有的四格:`evidence` / `facts` / `goals`,加上 `ontology`(词汇货架)。
|
|
6235
|
+
* 为什么词汇也在此列:货架是**渲染**,词条的权威在账本事件里——
|
|
6236
|
+
* 谁直接改那份 markdown,谁就制造了一份谁也不认的词汇表(面板读的是折法)。
|
|
6237
|
+
*/
|
|
6238
|
+
return [join(cwd, 'clear', 'evidence'), join(cwd, 'clear', 'knowledge', 'facts'), join(cwd, 'clear', 'goals'), join(cwd, 'clear', 'ontology')]
|
|
5711
6239
|
}
|
|
5712
6240
|
|
|
5713
6241
|
function touchesProtected(sessionId, value) {
|
|
@@ -5844,7 +6372,7 @@ export function apply(ctx, config = {}) {
|
|
|
5844
6372
|
// 「评估卡与事实只能由系统写」:做的人写不进证据面(执行者同样适用)
|
|
5845
6373
|
const suspect = touchesProtected(sessionId, args.file_path) ?? touchesProtected(sessionId, args.path) ?? (exec.name === 'bash' ? touchesProtected(sessionId, args.command) : null)
|
|
5846
6374
|
if (suspect !== null) {
|
|
5847
|
-
return { kind: 'deny', reason: `clear/evidence、clear/knowledge/facts、clear/goals 由系统所有,做的人不能写:${suspect}
|
|
6375
|
+
return { kind: 'deny', reason: `clear/evidence、clear/knowledge/facts、clear/goals、clear/ontology 由系统所有,做的人不能写:${suspect}。事实、评估卡与词汇货架只能由系统落盘(词汇要改就调注册/修订/废止动词)。` }
|
|
5848
6376
|
}
|
|
5849
6377
|
// L4 人放行:ClearAI 的设计目标未实现,这里用宿主的审批瀑布实现。
|
|
5850
6378
|
// 门挂在**等级**上:主线看步骤等级,世界线看分支等级——两条路同一道门。
|
|
@@ -6017,6 +6545,15 @@ export function apply(ctx, config = {}) {
|
|
|
6017
6545
|
ontologyShelved.add(sessionId)
|
|
6018
6546
|
if (shelf !== null) ontologyNote = `\n- 本体已就位(${CONTRIB.ontology.objects.length} 个对象 · ${CONTRIB.ontology.levels.length} 级):${join('clear', 'ontology', `${CONTRIB.ontology.id}.md`)}——交付前对照它(哪些状态合法、每一级谁来判)。`
|
|
6019
6547
|
}
|
|
6548
|
+
/**
|
|
6549
|
+
* **领域词汇货架**每一拍都重铺一次(幂等:内容没变就不重写)。
|
|
6550
|
+
*
|
|
6551
|
+
* 为什么不像过程本体那样只铺一次:那一份随**发布版本**,这一份随**会话**——
|
|
6552
|
+
* 一个项目今天没用词汇、明天开始用,货架必须自己长出来,而不是等人记得去建。
|
|
6553
|
+
* 它也不进卡:货架的位置在提示词里说一次就够,每拍重复就是往上下文里灌水。
|
|
6554
|
+
* 所有权判据(子会话不写)在写入口——见 `ensureDomainShelf`。
|
|
6555
|
+
*/
|
|
6556
|
+
ensureDomainShelf(host(), sessionId)
|
|
6020
6557
|
let brainNote = ''
|
|
6021
6558
|
/** 事实货架那句话说一次就够(事实很少变)。 */
|
|
6022
6559
|
let factsNote = ''
|
|
@@ -6139,6 +6676,7 @@ export function apply(ctx, config = {}) {
|
|
|
6139
6676
|
/**
|
|
6140
6677
|
* **事实货架**:事实变了才重写、才在卡里提一句——**变了才发**,与目录同一条纪律。
|
|
6141
6678
|
* 事实很少变(升格一次),所以这句话在大多数回合里都不出现。
|
|
6679
|
+
* 所有权判据(子会话不写)在写入口——见 `ensureFactsShelf`。
|
|
6142
6680
|
*/
|
|
6143
6681
|
const shelf = ensureFactsShelf(sessionId, state)
|
|
6144
6682
|
if (shelf !== null) factsNote = `\n- 事实库多了一条(或边界改了):${shelf}——引用前先看它的边界(推翻条件)。`
|