@stardustlc/dsh-dream 0.5.0 → 0.5.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 +9 -0
- package/README.en.md +12 -7
- package/README.md +12 -7
- package/lib/client.js +54 -6
- package/lib/journal.d.ts +15 -0
- package/lib/journal.js +32 -11
- package/lib/knowledge-store.d.ts +92 -7
- package/lib/knowledge-store.js +815 -187
- package/lib/knowledge.d.ts +26 -3
- package/lib/knowledge.js +36 -5
- package/lib/retrieval.d.ts +35 -7
- package/lib/retrieval.js +130 -18
- package/lib/tools.js +49 -7
- package/lib/web.d.ts +4 -1
- package/lib/web.js +2 -1
- package/package.json +1 -1
- package/skills/dream-protocol/SKILL.md +8 -4
package/lib/knowledge.d.ts
CHANGED
|
@@ -85,6 +85,7 @@ export type KnowledgeEvent = {
|
|
|
85
85
|
kind: 'lesson.create';
|
|
86
86
|
lessonId: string;
|
|
87
87
|
idempotencyKey: string;
|
|
88
|
+
requestHash?: string;
|
|
88
89
|
payload: Lesson;
|
|
89
90
|
} | {
|
|
90
91
|
schemaVersion: 1;
|
|
@@ -94,6 +95,7 @@ export type KnowledgeEvent = {
|
|
|
94
95
|
lessonId: string;
|
|
95
96
|
revision: number;
|
|
96
97
|
idempotencyKey: string;
|
|
98
|
+
requestHash?: string;
|
|
97
99
|
payload: Partial<Lesson>;
|
|
98
100
|
} | {
|
|
99
101
|
schemaVersion: 1;
|
|
@@ -103,6 +105,7 @@ export type KnowledgeEvent = {
|
|
|
103
105
|
lessonId: string;
|
|
104
106
|
revision: number;
|
|
105
107
|
idempotencyKey: string;
|
|
108
|
+
requestHash?: string;
|
|
106
109
|
payload: {
|
|
107
110
|
decision: ReviewDecision;
|
|
108
111
|
actor?: string;
|
|
@@ -114,15 +117,35 @@ export type KnowledgeEvent = {
|
|
|
114
117
|
at: string;
|
|
115
118
|
kind: 'evidence.add';
|
|
116
119
|
idempotencyKey: string;
|
|
120
|
+
requestHash?: string;
|
|
117
121
|
payload: Evidence;
|
|
118
122
|
};
|
|
119
123
|
/** 知识层错误码:invalid=数据不合法;revision=乐观锁不符;duplicate=重复;io=读写/锁失败。 */
|
|
120
124
|
export declare class KnowledgeError extends Error {
|
|
121
125
|
code: 'invalid' | 'revision' | 'duplicate' | 'io';
|
|
122
|
-
|
|
126
|
+
/** 可选结构化上下文(脱敏后给工具/面板用;字段不承诺长期稳定)。 */
|
|
127
|
+
details?: Record<string, unknown>;
|
|
128
|
+
constructor(code: 'invalid' | 'revision' | 'duplicate' | 'io', message: string, details?: Record<string, unknown>);
|
|
123
129
|
}
|
|
124
|
-
/**
|
|
125
|
-
export declare
|
|
130
|
+
/** 当前内存模型 / 写盘的 lesson schemaVersion。 */
|
|
131
|
+
export declare const LESSON_SCHEMA_VERSION = 1;
|
|
132
|
+
/** 迁移函数:把 from 版本的原始记录纯函数地转换成 to 版本的原始记录;必须幂等、不得写盘。 */
|
|
133
|
+
export type LessonSchemaMigration = (from: number, raw: unknown, to: number) => unknown;
|
|
134
|
+
/** 已知迁移链占位:v2 落地时在此登记;存储层默认只识别不自动迁移。 */
|
|
135
|
+
export declare const LESSON_MIGRATIONS: Record<number, LessonSchemaMigration>;
|
|
136
|
+
/** validateLesson 的可选前向兼容参数;旧调用签名(单参)保持不变。 */
|
|
137
|
+
export interface ValidateLessonOptions {
|
|
138
|
+
/** 允许直接按当前形状读取的 schemaVersion 列表;默认 [1]。 */
|
|
139
|
+
accept?: number[];
|
|
140
|
+
/** 显式迁移函数:对非当前版本优先生效,返回值必须能通过 v1 校验。 */
|
|
141
|
+
migrate?: LessonSchemaMigration;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* 校验并归一化一条经验;失败抛 KnowledgeError('invalid')。未知字段会被丢弃。
|
|
145
|
+
* 前向兼容:schemaVersion 非当前版本时,先尝试 options.migrate / LESSON_MIGRATIONS;
|
|
146
|
+
* 否则仅当版本在 options.accept 内才按当前形状读取,返回内存模型 v1。
|
|
147
|
+
*/
|
|
148
|
+
export declare function validateLesson(value: unknown, options?: ValidateLessonOptions): Lesson;
|
|
126
149
|
/** 校验并归一化一条证据;失败抛 KnowledgeError('invalid')。未知字段会被丢弃。 */
|
|
127
150
|
export declare function validateEvidence(value: unknown): Evidence;
|
|
128
151
|
/**
|
package/lib/knowledge.js
CHANGED
|
@@ -10,10 +10,14 @@ import { createHash } from 'node:crypto';
|
|
|
10
10
|
/** 知识层错误码:invalid=数据不合法;revision=乐观锁不符;duplicate=重复;io=读写/锁失败。 */
|
|
11
11
|
export class KnowledgeError extends Error {
|
|
12
12
|
code;
|
|
13
|
-
|
|
13
|
+
/** 可选结构化上下文(脱敏后给工具/面板用;字段不承诺长期稳定)。 */
|
|
14
|
+
details;
|
|
15
|
+
constructor(code, message, details) {
|
|
14
16
|
super(message);
|
|
15
17
|
this.name = 'KnowledgeError';
|
|
16
18
|
this.code = code;
|
|
19
|
+
if (details !== undefined)
|
|
20
|
+
this.details = details;
|
|
17
21
|
}
|
|
18
22
|
}
|
|
19
23
|
const LESSON_KINDS = ['preference', 'procedure', 'pitfall', 'fact'];
|
|
@@ -131,12 +135,39 @@ function readReview(value) {
|
|
|
131
135
|
review.at = at;
|
|
132
136
|
return review;
|
|
133
137
|
}
|
|
134
|
-
/**
|
|
135
|
-
export
|
|
138
|
+
/** 当前内存模型 / 写盘的 lesson schemaVersion。 */
|
|
139
|
+
export const LESSON_SCHEMA_VERSION = 1;
|
|
140
|
+
/** 已知迁移链占位:v2 落地时在此登记;存储层默认只识别不自动迁移。 */
|
|
141
|
+
export const LESSON_MIGRATIONS = {};
|
|
142
|
+
/**
|
|
143
|
+
* 校验并归一化一条经验;失败抛 KnowledgeError('invalid')。未知字段会被丢弃。
|
|
144
|
+
* 前向兼容:schemaVersion 非当前版本时,先尝试 options.migrate / LESSON_MIGRATIONS;
|
|
145
|
+
* 否则仅当版本在 options.accept 内才按当前形状读取,返回内存模型 v1。
|
|
146
|
+
*/
|
|
147
|
+
export function validateLesson(value, options = {}) {
|
|
136
148
|
if (!isRecord(value))
|
|
137
149
|
fail('lesson', '必须是对象');
|
|
138
|
-
|
|
139
|
-
|
|
150
|
+
const version = value.schemaVersion;
|
|
151
|
+
if (version !== LESSON_SCHEMA_VERSION) {
|
|
152
|
+
const accept = options.accept === undefined ? [LESSON_SCHEMA_VERSION] : options.accept;
|
|
153
|
+
const migration = options.migrate !== undefined
|
|
154
|
+
? options.migrate
|
|
155
|
+
: (typeof version === 'number' ? LESSON_MIGRATIONS[version] : undefined);
|
|
156
|
+
if (migration !== undefined) {
|
|
157
|
+
let migrated;
|
|
158
|
+
try {
|
|
159
|
+
migrated = migration(version, value, LESSON_SCHEMA_VERSION);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
fail('schemaVersion', '迁移失败:' + (error instanceof Error ? error.message : String(error)));
|
|
163
|
+
}
|
|
164
|
+
return validateLesson(migrated, { accept: [LESSON_SCHEMA_VERSION] });
|
|
165
|
+
}
|
|
166
|
+
if (typeof version !== 'number' || !accept.includes(version)) {
|
|
167
|
+
fail('schemaVersion', '不支持 ' + String(version) + '(只识别 ' + accept.join(', ') + ')');
|
|
168
|
+
}
|
|
169
|
+
return validateLesson({ ...value, schemaVersion: LESSON_SCHEMA_VERSION }, { accept: [LESSON_SCHEMA_VERSION] });
|
|
170
|
+
}
|
|
140
171
|
const id = readString(value.id, 'id', { required: true, nonEmpty: true });
|
|
141
172
|
const revision = value.revision;
|
|
142
173
|
if (typeof revision !== 'number' || !Number.isInteger(revision) || revision < 1)
|
package/lib/retrieval.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 任务相关经验检索(FREEZE
|
|
2
|
+
* 任务相关经验检索(FREEZE v1 + v1.1 增补 R1/R3/R4)。
|
|
3
3
|
*
|
|
4
|
-
* 顺序:范围过滤 → 排除 rejected/stale/disputed →
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* 顺序:范围过滤 → 排除 rejected/stale/disputed → 候选/无证据策略过滤 → 版本包名上下文 →
|
|
5
|
+
* 确定性排序 → 按排名整条装入的字符预算(遇第一条放不下即停,rankInversionCount===0)。
|
|
6
|
+
* essential(title/when/action/exceptions,含 scopeLabel)永不截断;metadata(whyRelevant/
|
|
7
|
+
* evidenceSummary)放不下时可截断并置 truncated:true。
|
|
7
8
|
*
|
|
8
9
|
* @module dsh-dream/retrieval
|
|
9
10
|
*/
|
|
@@ -17,12 +18,19 @@ export interface RetrievalQuery {
|
|
|
17
18
|
limit?: number;
|
|
18
19
|
/** 默认 3000,硬上限 20000。 */
|
|
19
20
|
maxChars?: number;
|
|
20
|
-
/** 当前任务相关依赖的版本,用于匹配 applicability.versions
|
|
21
|
+
/** 当前任务相关依赖的版本,用于匹配 applicability.versions(R3:必须先识别包名)。 */
|
|
21
22
|
packageVersion?: string;
|
|
23
|
+
/** 显式包名上下文;与 query 文本一起用于识别 applicability[].package(R3)。 */
|
|
24
|
+
packageName?: string;
|
|
25
|
+
/** 是否返回 candidate;默认 true(面板/审计需要)。dream_context 默认传 false(R1)。 */
|
|
26
|
+
includeCandidates?: boolean;
|
|
27
|
+
/** 是否保留 independentSupportCount===0 且非 usable 的条目;默认 true。dream_context 传 false(R1)。 */
|
|
28
|
+
includeNoEvidence?: boolean;
|
|
22
29
|
}
|
|
23
|
-
/**
|
|
30
|
+
/** 检索结果中的单条经验(when/exceptions 永不截断)。 */
|
|
24
31
|
export interface RetrievedLesson {
|
|
25
32
|
lessonId: string;
|
|
33
|
+
revision: number;
|
|
26
34
|
title: string;
|
|
27
35
|
when: string;
|
|
28
36
|
action: string;
|
|
@@ -48,8 +56,23 @@ export declare function queryScopeLabel(q: Pick<RetrievalQuery, 'projectId' | 'w
|
|
|
48
56
|
export declare function lessonScopeLabel(scope: Lesson['scope']): string;
|
|
49
57
|
/** 证据摘要(面板与 dream_context 共用):独立支持数 + 已读/仅声明 + 脱敏摘要片段。 */
|
|
50
58
|
export declare function buildEvidenceSummary(lesson: Lesson, evidence: Evidence[]): string;
|
|
51
|
-
/**
|
|
59
|
+
/** essential 成本:title/when/action/exceptions/scopeLabel,永不截断(R4)。 */
|
|
60
|
+
export declare function essentialLessonChars(item: Pick<RetrievedLesson, 'title' | 'when' | 'action' | 'exceptions' | 'scopeLabel'>): number;
|
|
61
|
+
/** metadata 成本:whyRelevant/evidenceSummary,预算紧时可截断(R4)。 */
|
|
62
|
+
export declare function metadataLessonChars(item: Pick<RetrievedLesson, 'whyRelevant' | 'evidenceSummary'>): number;
|
|
63
|
+
/** 单条结果的完整字符成本(与 dream_context.budget.usedChars 同口径)。 */
|
|
52
64
|
export declare function retrievedLessonChars(item: RetrievedLesson): number;
|
|
65
|
+
/**
|
|
66
|
+
* rank inversion 指标(R4):被 budget-stop 跳过、且排在某个已返回条目之前的条目数。
|
|
67
|
+
* 按排名整条装入的预算策略必须让它恒为 0。
|
|
68
|
+
*/
|
|
69
|
+
export declare function rankInversionCount(rankedLessonIds: string[], result: {
|
|
70
|
+
items: Array<Pick<RetrievedLesson, 'lessonId'>>;
|
|
71
|
+
skipped: Array<{
|
|
72
|
+
lessonId: string;
|
|
73
|
+
reason: string;
|
|
74
|
+
}>;
|
|
75
|
+
}): number;
|
|
53
76
|
/** 解析 limit/maxChars,套用默认值与硬上限。 */
|
|
54
77
|
export declare function resolveRetrievalBudget(q: Pick<RetrievalQuery, 'limit' | 'maxChars'>): {
|
|
55
78
|
limit: number;
|
|
@@ -60,6 +83,11 @@ export declare function versionSatisfies(versionText: string, rangeText: string)
|
|
|
60
83
|
/**
|
|
61
84
|
* 任务相关经验检索。只返回 global 或与查询项目/工作区精确匹配的记录;
|
|
62
85
|
* 未知项目(未给 projectId/workspaceRoot)时只返回 global,绝不跨项目扫描。
|
|
86
|
+
*
|
|
87
|
+
* R1:includeCandidates=false 时 candidate 记 skipped:candidate-hold;includeNoEvidence=false 时
|
|
88
|
+
* independentSupportCount===0 且非 usable 记 skipped:no-evidence(优先于 candidate-hold)。
|
|
89
|
+
* R3:applicability.versions 仅在包名被识别时参与;包名识别但版本不满足 → skipped:version-mismatch。
|
|
90
|
+
* R4:预算按排名整条装入,第一条 essential 放不下即停(budget-stop);metadata 可截断。
|
|
63
91
|
*/
|
|
64
92
|
export declare function retrieveLessons(lessons: Lesson[], evidence: Evidence[], q: RetrievalQuery): {
|
|
65
93
|
items: RetrievedLesson[];
|
package/lib/retrieval.js
CHANGED
|
@@ -24,6 +24,16 @@ function oneLine(value) {
|
|
|
24
24
|
function clip(value, max) {
|
|
25
25
|
return value.length > max ? value.slice(0, max) + '…' : value;
|
|
26
26
|
}
|
|
27
|
+
/** 在预算内截断(含省略号占用);conditions/exceptions 不走这里。 */
|
|
28
|
+
function clipTo(text, max) {
|
|
29
|
+
if (max <= 0)
|
|
30
|
+
return '';
|
|
31
|
+
if (text.length <= max)
|
|
32
|
+
return text;
|
|
33
|
+
if (max === 1)
|
|
34
|
+
return '…';
|
|
35
|
+
return text.slice(0, max - 1) + '…';
|
|
36
|
+
}
|
|
27
37
|
function pathKey(value) {
|
|
28
38
|
return value.trim().replace(/\\/g, '/').replace(/\/+$/, '');
|
|
29
39
|
}
|
|
@@ -90,14 +100,48 @@ export function buildEvidenceSummary(lesson, evidence) {
|
|
|
90
100
|
return head;
|
|
91
101
|
return head + ':' + snippets.join(';') + (linked.length > 2 ? ' 等' : '');
|
|
92
102
|
}
|
|
93
|
-
/**
|
|
94
|
-
export function
|
|
95
|
-
let total = item.title.length + item.when.length + item.action.length
|
|
96
|
-
+ item.scopeLabel.length + item.whyRelevant.length + item.evidenceSummary.length;
|
|
103
|
+
/** essential 成本:title/when/action/exceptions/scopeLabel,永不截断(R4)。 */
|
|
104
|
+
export function essentialLessonChars(item) {
|
|
105
|
+
let total = item.title.length + item.when.length + item.action.length + item.scopeLabel.length;
|
|
97
106
|
for (const exception of item.exceptions)
|
|
98
107
|
total += exception.length;
|
|
99
108
|
return total;
|
|
100
109
|
}
|
|
110
|
+
/** metadata 成本:whyRelevant/evidenceSummary,预算紧时可截断(R4)。 */
|
|
111
|
+
export function metadataLessonChars(item) {
|
|
112
|
+
return item.whyRelevant.length + item.evidenceSummary.length;
|
|
113
|
+
}
|
|
114
|
+
/** 单条结果的完整字符成本(与 dream_context.budget.usedChars 同口径)。 */
|
|
115
|
+
export function retrievedLessonChars(item) {
|
|
116
|
+
return essentialLessonChars(item) + metadataLessonChars(item);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* rank inversion 指标(R4):被 budget-stop 跳过、且排在某个已返回条目之前的条目数。
|
|
120
|
+
* 按排名整条装入的预算策略必须让它恒为 0。
|
|
121
|
+
*/
|
|
122
|
+
export function rankInversionCount(rankedLessonIds, result) {
|
|
123
|
+
const rank = new Map();
|
|
124
|
+
rankedLessonIds.forEach((id, index) => { if (!rank.has(id))
|
|
125
|
+
rank.set(id, index); });
|
|
126
|
+
const returnedRanks = [];
|
|
127
|
+
for (const item of result.items) {
|
|
128
|
+
const index = rank.get(item.lessonId);
|
|
129
|
+
if (index !== undefined)
|
|
130
|
+
returnedRanks.push(index);
|
|
131
|
+
}
|
|
132
|
+
if (returnedRanks.length === 0)
|
|
133
|
+
return 0;
|
|
134
|
+
const lastReturned = Math.max(...returnedRanks);
|
|
135
|
+
let inversions = 0;
|
|
136
|
+
for (const entry of result.skipped) {
|
|
137
|
+
if (entry.reason !== 'budget-stop' && entry.reason !== 'budget')
|
|
138
|
+
continue;
|
|
139
|
+
const index = rank.get(entry.lessonId);
|
|
140
|
+
if (index !== undefined && index < lastReturned)
|
|
141
|
+
inversions += 1;
|
|
142
|
+
}
|
|
143
|
+
return inversions;
|
|
144
|
+
}
|
|
101
145
|
/** 解析 limit/maxChars,套用默认值与硬上限。 */
|
|
102
146
|
export function resolveRetrievalBudget(q) {
|
|
103
147
|
const rawLimit = q.limit;
|
|
@@ -203,21 +247,38 @@ export function versionSatisfies(versionText, rangeText) {
|
|
|
203
247
|
return clauses.every((clause) => satisfiesClause(version, clause));
|
|
204
248
|
});
|
|
205
249
|
}
|
|
250
|
+
/** R3:只有显式 packageName 一致,或 query 文本出现该包名,才认为包名被识别。 */
|
|
251
|
+
function packageIdentified(pkg, q, queryText) {
|
|
252
|
+
const normalizedPkg = normalizeText(pkg);
|
|
253
|
+
if (normalizedPkg === '')
|
|
254
|
+
return false;
|
|
255
|
+
const explicit = trimmed(q.packageName);
|
|
256
|
+
if (explicit !== undefined && normalizeText(explicit) === normalizedPkg)
|
|
257
|
+
return true;
|
|
258
|
+
return queryText.includes(normalizedPkg);
|
|
259
|
+
}
|
|
206
260
|
function matchApplicability(lesson, q, queryText) {
|
|
207
|
-
const out = { packages: [], versions: [], platforms: [] };
|
|
261
|
+
const out = { packages: [], versions: [], platforms: [], versionMismatch: false };
|
|
208
262
|
const packageVersion = trimmed(q.packageVersion);
|
|
263
|
+
const identifiedVersionChecks = [];
|
|
209
264
|
for (const entry of lesson.applicability) {
|
|
210
265
|
const pkg = trimmed(entry.package);
|
|
211
|
-
|
|
266
|
+
const identified = pkg !== undefined && packageIdentified(pkg, q, queryText);
|
|
267
|
+
if (identified && pkg !== undefined && !out.packages.includes(pkg))
|
|
212
268
|
out.packages.push(pkg);
|
|
213
269
|
const versions = trimmed(entry.versions);
|
|
214
|
-
if (
|
|
215
|
-
|
|
270
|
+
if (identified && versions !== undefined && packageVersion !== undefined) {
|
|
271
|
+
const ok = versionSatisfies(packageVersion, versions);
|
|
272
|
+
identifiedVersionChecks.push(ok);
|
|
273
|
+
if (ok && !out.versions.includes(versions))
|
|
274
|
+
out.versions.push(versions);
|
|
216
275
|
}
|
|
217
276
|
const platform = trimmed(entry.platform);
|
|
218
|
-
if (platform !== undefined && queryText.includes(normalizeText(platform)) && !out.platforms.includes(platform))
|
|
277
|
+
if (platform !== undefined && queryText.includes(normalizeText(platform)) && !out.platforms.includes(platform)) {
|
|
219
278
|
out.platforms.push(platform);
|
|
279
|
+
}
|
|
220
280
|
}
|
|
281
|
+
out.versionMismatch = identifiedVersionChecks.length > 0 && identifiedVersionChecks.every((ok) => !ok);
|
|
221
282
|
return out;
|
|
222
283
|
}
|
|
223
284
|
function queryTerms(query) {
|
|
@@ -308,6 +369,7 @@ function toRetrievedLesson(entry, evidence, q) {
|
|
|
308
369
|
const lesson = entry.lesson;
|
|
309
370
|
const item = {
|
|
310
371
|
lessonId: lesson.id,
|
|
372
|
+
revision: lesson.revision,
|
|
311
373
|
title: lesson.title,
|
|
312
374
|
when: lesson.when,
|
|
313
375
|
action: lesson.action,
|
|
@@ -338,11 +400,18 @@ function stateRank(state) {
|
|
|
338
400
|
/**
|
|
339
401
|
* 任务相关经验检索。只返回 global 或与查询项目/工作区精确匹配的记录;
|
|
340
402
|
* 未知项目(未给 projectId/workspaceRoot)时只返回 global,绝不跨项目扫描。
|
|
403
|
+
*
|
|
404
|
+
* R1:includeCandidates=false 时 candidate 记 skipped:candidate-hold;includeNoEvidence=false 时
|
|
405
|
+
* independentSupportCount===0 且非 usable 记 skipped:no-evidence(优先于 candidate-hold)。
|
|
406
|
+
* R3:applicability.versions 仅在包名被识别时参与;包名识别但版本不满足 → skipped:version-mismatch。
|
|
407
|
+
* R4:预算按排名整条装入,第一条 essential 放不下即停(budget-stop);metadata 可截断。
|
|
341
408
|
*/
|
|
342
409
|
export function retrieveLessons(lessons, evidence, q) {
|
|
343
410
|
const { limit, maxChars } = resolveRetrievalBudget(q);
|
|
344
411
|
const terms = queryTerms(q.query ?? '');
|
|
345
412
|
const queryText = normalizeText(q.query ?? '');
|
|
413
|
+
const includeCandidates = q.includeCandidates !== false;
|
|
414
|
+
const includeNoEvidence = q.includeNoEvidence !== false;
|
|
346
415
|
const skipped = [];
|
|
347
416
|
const candidates = [];
|
|
348
417
|
for (const lesson of lessons) {
|
|
@@ -353,8 +422,21 @@ export function retrieveLessons(lessons, evidence, q) {
|
|
|
353
422
|
skipped.push({ lessonId: lesson.id, reason: excludedReason });
|
|
354
423
|
continue;
|
|
355
424
|
}
|
|
356
|
-
|
|
425
|
+
// R3 优先于 R1 的候选扣留:包名已识别但版本不满足时,reason 必须是 version-mismatch。
|
|
357
426
|
const applicability = matchApplicability(lesson, q, queryText);
|
|
427
|
+
if (applicability.versionMismatch) {
|
|
428
|
+
skipped.push({ lessonId: lesson.id, reason: 'version-mismatch' });
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (!includeNoEvidence && lesson.state !== 'usable' && lesson.independentSupportCount === 0) {
|
|
432
|
+
skipped.push({ lessonId: lesson.id, reason: 'no-evidence' });
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (!includeCandidates && lesson.state === 'candidate') {
|
|
436
|
+
skipped.push({ lessonId: lesson.id, reason: 'candidate-hold' });
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const hits = keywordHits(lesson, terms);
|
|
358
440
|
const applicabilityCount = applicability.packages.length + applicability.versions.length + applicability.platforms.length;
|
|
359
441
|
if (hits.length === 0 && applicabilityCount === 0)
|
|
360
442
|
continue;
|
|
@@ -385,19 +467,49 @@ export function retrieveLessons(lessons, evidence, q) {
|
|
|
385
467
|
});
|
|
386
468
|
const items = [];
|
|
387
469
|
let usedChars = 0;
|
|
388
|
-
for (
|
|
470
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
389
471
|
if (items.length >= limit) {
|
|
390
|
-
|
|
391
|
-
|
|
472
|
+
for (let rest = index; rest < candidates.length; rest += 1) {
|
|
473
|
+
skipped.push({ lessonId: candidates[rest].lesson.id, reason: 'limit' });
|
|
474
|
+
}
|
|
475
|
+
break;
|
|
392
476
|
}
|
|
477
|
+
const candidate = candidates[index];
|
|
393
478
|
const item = toRetrievedLesson(candidate, evidence, q);
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
479
|
+
const remaining = maxChars - usedChars;
|
|
480
|
+
const essential = essentialLessonChars(item);
|
|
481
|
+
if (essential > remaining) {
|
|
482
|
+
for (let rest = index; rest < candidates.length; rest += 1) {
|
|
483
|
+
skipped.push({ lessonId: candidates[rest].lesson.id, reason: 'budget-stop' });
|
|
484
|
+
}
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
const fullCost = retrievedLessonChars(item);
|
|
488
|
+
if (fullCost <= remaining) {
|
|
489
|
+
items.push(item);
|
|
490
|
+
usedChars += fullCost;
|
|
397
491
|
continue;
|
|
398
492
|
}
|
|
399
|
-
|
|
400
|
-
|
|
493
|
+
const trimmed = truncateMetadata(item, remaining - essential);
|
|
494
|
+
const trimmedCost = retrievedLessonChars(trimmed);
|
|
495
|
+
if (trimmedCost > remaining) {
|
|
496
|
+
for (let rest = index; rest < candidates.length; rest += 1) {
|
|
497
|
+
skipped.push({ lessonId: candidates[rest].lesson.id, reason: 'budget-stop' });
|
|
498
|
+
}
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
items.push(trimmed);
|
|
502
|
+
usedChars += trimmedCost;
|
|
401
503
|
}
|
|
402
504
|
return { items, skipped };
|
|
403
505
|
}
|
|
506
|
+
/** metadata(whyRelevant/evidenceSummary)按预算截断,essential 保持不变。 */
|
|
507
|
+
function truncateMetadata(item, metadataBudget) {
|
|
508
|
+
const budget = Math.max(0, metadataBudget);
|
|
509
|
+
const whyBudget = Math.min(item.whyRelevant.length, Math.ceil(budget / 2));
|
|
510
|
+
const whyRelevant = clipTo(item.whyRelevant, whyBudget);
|
|
511
|
+
const evidenceSummary = clipTo(item.evidenceSummary, Math.max(0, budget - whyRelevant.length));
|
|
512
|
+
if (whyRelevant === item.whyRelevant && evidenceSummary === item.evidenceSummary)
|
|
513
|
+
return item;
|
|
514
|
+
return { ...item, whyRelevant, evidenceSummary, truncated: true };
|
|
515
|
+
}
|
package/lib/tools.js
CHANGED
|
@@ -456,7 +456,7 @@ export function buildDreamTools(config) {
|
|
|
456
456
|
},
|
|
457
457
|
description: '适用范围(可选)。',
|
|
458
458
|
},
|
|
459
|
-
evidence: { type: 'array',
|
|
459
|
+
evidence: { type: 'array', items: evidenceItemsSpec(), description: '证据数组(可选,缺省为空数组;无证据时只能保存为 candidate)。' },
|
|
460
460
|
}),
|
|
461
461
|
output: {
|
|
462
462
|
schema: baseSchema,
|
|
@@ -521,7 +521,17 @@ export function buildDreamTools(config) {
|
|
|
521
521
|
})),
|
|
522
522
|
}));
|
|
523
523
|
const store = knowledgeStoreOf(cfg);
|
|
524
|
-
|
|
524
|
+
let { lesson, created } = store.createLesson(lessonInput, idempotencyKey);
|
|
525
|
+
// R2:user-correction + read 证据视为用户已采纳;初始 candidate 经验直接进入 usable,
|
|
526
|
+
// 并在 review 记 actor=user。若存储层未来也实现 R2,这里会因 state 已 usable 而跳过(兼容)。
|
|
527
|
+
const hasUserCorrectionRead = evidenceInputs.some((entry) => entry.kind === 'user-correction' && entry.verification === 'read');
|
|
528
|
+
const reviewIsUserAccepted = lesson.review.decision === 'accepted' && lesson.review.actor === 'user';
|
|
529
|
+
const userReviewAllowed = lesson.state === 'candidate' || lesson.state === 'usable';
|
|
530
|
+
// 兼容两种时序:R2′ 未落盘时 createLesson 返回 candidate;已落盘时返回 usable(review 未必带 actor)。
|
|
531
|
+
// user-correction + read 一律记为用户已采纳;rejected/disputed/stale 不覆盖既有审阅结论。
|
|
532
|
+
if (hasUserCorrectionRead && !reviewIsUserAccepted && userReviewAllowed) {
|
|
533
|
+
lesson = store.reviewLesson(lesson.id, 'accepted', lesson.revision, 'dream_learn:user-correction:' + lesson.id + ':' + lesson.revision, 'user');
|
|
534
|
+
}
|
|
525
535
|
const byId = new Map(store.listEvidence().map((item) => [item.id, item]));
|
|
526
536
|
const evidence = lesson.evidenceIds
|
|
527
537
|
.map((id) => byId.get(id))
|
|
@@ -536,6 +546,14 @@ export function buildDreamTools(config) {
|
|
|
536
546
|
const claimedCount = evidence.filter((item) => item.verification === 'claimed').length;
|
|
537
547
|
notes.push('证据:' + readCount + ' 条已读(计入独立支持),' + claimedCount + ' 条仅声明(不计入独立支持)。');
|
|
538
548
|
}
|
|
549
|
+
if (hasUserCorrectionRead) {
|
|
550
|
+
if (lesson.state === 'usable' && lesson.review.decision === 'accepted' && lesson.review.actor === 'user') {
|
|
551
|
+
notes.push('检测到 user-correction + read 证据:按 R2 视为用户已采纳(state=usable,review.actor=user)。');
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
notes.push('检测到 user-correction + read 证据,但经验当前状态为 ' + lesson.state + ':不覆盖既有审阅结论。');
|
|
555
|
+
}
|
|
556
|
+
}
|
|
539
557
|
if (!created)
|
|
540
558
|
notes.push('已存在同指纹经验(' + lesson.id + '):复用既有记录并合并新证据,未新增经验。');
|
|
541
559
|
if (!isGlobal && projectId === undefined && workspaceRoot === undefined) {
|
|
@@ -559,14 +577,16 @@ export function buildDreamTools(config) {
|
|
|
559
577
|
};
|
|
560
578
|
const dreamContext = {
|
|
561
579
|
name: 'dream_context',
|
|
562
|
-
description: '取梦:按当前任务返回少量真正适用的经验(只读,默认最多 5 条 / 3000 字符)。只返回 global 或与 projectId/workspaceRoot 精确匹配的经验;未知项目只返回 global,绝不跨项目扫描。rejected/stale/disputed
|
|
580
|
+
description: '取梦:按当前任务返回少量真正适用的经验(只读,默认最多 5 条 / 3000 字符)。只返回 global 或与 projectId/workspaceRoot 精确匹配的经验;未知项目只返回 global,绝不跨项目扫描。rejected/stale/disputed 默认排除;候选默认不注入(candidate-hold),无独立证据且非 usable 不注入(no-evidence),版本条件与包名不符不注入(version-mismatch),skipped 均给原因。未审阅候选不得当作已证实结论;只有明确审阅候选时才传 includeCandidates=true。when/exceptions 完整保留,元数据可按预算截断,预算放不下即停。',
|
|
563
581
|
parameters: compileParameters({
|
|
564
582
|
query: { type: 'string', required: true, description: '当前任务描述或检索关键词。' },
|
|
565
583
|
projectId: { type: 'string', description: '当前项目 ID(可选;未知时只返回 global)。' },
|
|
566
584
|
workspaceRoot: { type: 'string', description: '当前工作区根目录(可选)。' },
|
|
567
585
|
limit: { type: 'integer', description: '条数上限 1-20(默认 5)。' },
|
|
568
586
|
maxChars: { type: 'integer', description: '字符预算 1-20000(默认 3000)。' },
|
|
569
|
-
packageVersion: { type: 'string', description: '
|
|
587
|
+
packageVersion: { type: 'string', description: '当前相关依赖版本(可选;需配合包名被识别才参与版本判定)。' },
|
|
588
|
+
packageName: { type: 'string', description: '当前相关依赖包名(可选;与 query 文本一起识别 applicability.package)。' },
|
|
589
|
+
includeCandidates: { type: 'boolean', description: '是否把候选经验纳入任务建议(默认 false;仅明确审阅候选时传 true,无证据候选仍不注入)。' },
|
|
570
590
|
}),
|
|
571
591
|
output: {
|
|
572
592
|
schema: baseSchema,
|
|
@@ -575,12 +595,26 @@ export function buildDreamTools(config) {
|
|
|
575
595
|
const items = Array.isArray(rec.items) ? rec.items : [];
|
|
576
596
|
const budget = asRecord(rec.budget);
|
|
577
597
|
if (items.length === 0) {
|
|
578
|
-
|
|
598
|
+
const skipped = Array.isArray(rec.skipped) ? rec.skipped : [];
|
|
599
|
+
const countReason = (reason) => skipped.filter((entry) => asRecord(entry).reason === reason).length;
|
|
600
|
+
const hints = [];
|
|
601
|
+
const held = countReason('candidate-hold');
|
|
602
|
+
const noEvidence = countReason('no-evidence');
|
|
603
|
+
const mismatched = countReason('version-mismatch');
|
|
604
|
+
if (held > 0)
|
|
605
|
+
hints.push(held + ' 条候选待审阅(要审阅候选时显式传 includeCandidates=true)');
|
|
606
|
+
if (noEvidence > 0)
|
|
607
|
+
hints.push(noEvidence + ' 条无独立证据不注入');
|
|
608
|
+
if (mismatched > 0)
|
|
609
|
+
hints.push(mismatched + ' 条版本条件不匹配不注入');
|
|
610
|
+
const suffix = hints.length > 0 ? ';' + hints.join(';') : '';
|
|
611
|
+
return [{ type: 'text', text: '没有可注入的适用经验(范围:' + rec.scopeLabel + suffix + ')。' }];
|
|
579
612
|
}
|
|
580
613
|
const lines = ['适用经验 ' + items.length + ' 条(范围:' + rec.scopeLabel + ',字符 ' + budget.usedChars + '/' + budget.maxChars + '):'];
|
|
581
614
|
for (const item of items) {
|
|
582
615
|
const lesson = asRecord(item);
|
|
583
|
-
|
|
616
|
+
const truncation = lesson.truncated === true ? '|元数据截断' : '';
|
|
617
|
+
lines.push('- [' + lesson.state + '|' + lesson.scopeLabel + '|rev ' + lesson.revision + truncation + '] ' + lesson.title);
|
|
584
618
|
lines.push(' 何时:' + lesson.when);
|
|
585
619
|
lines.push(' 行动:' + lesson.action);
|
|
586
620
|
const exceptions = Array.isArray(lesson.exceptions) ? lesson.exceptions : [];
|
|
@@ -598,6 +632,7 @@ export function buildDreamTools(config) {
|
|
|
598
632
|
const projectId = optionalString(args, 'projectId');
|
|
599
633
|
const workspaceRoot = optionalString(args, 'workspaceRoot');
|
|
600
634
|
const packageVersion = optionalString(args, 'packageVersion');
|
|
635
|
+
const packageName = optionalString(args, 'packageName');
|
|
601
636
|
const { limit, maxChars } = resolveRetrievalBudget({
|
|
602
637
|
limit: typeof args.limit === 'number' ? args.limit : undefined,
|
|
603
638
|
maxChars: typeof args.maxChars === 'number' ? args.maxChars : undefined,
|
|
@@ -606,9 +641,13 @@ export function buildDreamTools(config) {
|
|
|
606
641
|
query,
|
|
607
642
|
limit,
|
|
608
643
|
maxChars,
|
|
644
|
+
// R1:默认不注入候选;claimed-only / 无证据且非 usable 的条目也不注入(R1 附带规则)。
|
|
645
|
+
includeCandidates: args.includeCandidates === true,
|
|
646
|
+
includeNoEvidence: false,
|
|
609
647
|
...(projectId !== undefined ? { projectId } : {}),
|
|
610
648
|
...(workspaceRoot !== undefined ? { workspaceRoot } : {}),
|
|
611
649
|
...(packageVersion !== undefined ? { packageVersion } : {}),
|
|
650
|
+
...(packageName !== undefined ? { packageName } : {}),
|
|
612
651
|
};
|
|
613
652
|
const scopeLabel = queryScopeLabel(retrievalQuery);
|
|
614
653
|
const dir = knowledgeDirOf(cfg);
|
|
@@ -661,7 +700,10 @@ export function buildDreamTools(config) {
|
|
|
661
700
|
if (before === undefined)
|
|
662
701
|
throw new KnowledgeError('invalid', '经验不存在:' + lessonId);
|
|
663
702
|
if (before.revision !== expectedRevision) {
|
|
664
|
-
|
|
703
|
+
const error = new KnowledgeError('revision', '经验 ' + lessonId + ' 期望 revision ' + expectedRevision + ',实际 ' + before.revision);
|
|
704
|
+
error.currentRevision = before.revision;
|
|
705
|
+
error.details = { currentRevision: before.revision };
|
|
706
|
+
throw error;
|
|
665
707
|
}
|
|
666
708
|
const conflictWith = stringArray(args, 'conflictWith');
|
|
667
709
|
const note = optionalString(args, 'note');
|
package/lib/web.d.ts
CHANGED
|
@@ -24,13 +24,16 @@ export interface DreamWebPayload {
|
|
|
24
24
|
/** 本次生效的条数上限。 */
|
|
25
25
|
limit: number;
|
|
26
26
|
}
|
|
27
|
-
/** 知识路由的统计形状(与 FREEZE §2 的 stats()
|
|
27
|
+
/** 知识路由的统计形状(与 FREEZE §2 的 stats() 对齐;v1.1 R8 透传 badLines)。 */
|
|
28
28
|
export interface KnowledgeWebStats {
|
|
29
29
|
lessons: number;
|
|
30
30
|
evidence: number;
|
|
31
31
|
events: number;
|
|
32
32
|
byState: Record<string, number>;
|
|
33
|
+
/** 有界回放被截断时为 true(面板据此显示提示行)。 */
|
|
33
34
|
truncated?: boolean;
|
|
35
|
+
/** 事件 / 证据流中被跳过的坏行数(面板据此显示提示行)。 */
|
|
36
|
+
badLines: number;
|
|
34
37
|
}
|
|
35
38
|
/** 知识路由的 JSON 形状:脱敏后的经验 / 证据 / 统计。 */
|
|
36
39
|
export interface KnowledgeWebPayload {
|
package/lib/web.js
CHANGED
|
@@ -139,7 +139,7 @@ function maskDeep(value) {
|
|
|
139
139
|
}
|
|
140
140
|
return value;
|
|
141
141
|
}
|
|
142
|
-
/** 统计归一化:数字字段兜底为 0,byState 五个键齐全,truncated 只在为 true
|
|
142
|
+
/** 统计归一化:数字字段兜底为 0,byState 五个键齐全,truncated 只在为 true 时保留,badLines 始终透传。 */
|
|
143
143
|
function normalizeKnowledgeStats(value) {
|
|
144
144
|
const source = value !== null && typeof value === 'object' ? value : {};
|
|
145
145
|
const rawByState = source.byState !== null && typeof source.byState === 'object'
|
|
@@ -159,6 +159,7 @@ function normalizeKnowledgeStats(value) {
|
|
|
159
159
|
evidence: numberAt('evidence'),
|
|
160
160
|
events: numberAt('events'),
|
|
161
161
|
byState,
|
|
162
|
+
badLines: Math.floor(numberAt('badLines')),
|
|
162
163
|
};
|
|
163
164
|
if (source.truncated === true)
|
|
164
165
|
stats.truncated = true;
|
package/package.json
CHANGED
|
@@ -20,10 +20,10 @@ description: 做梦协议:任务开始取回适用经验,长任务收尾沉
|
|
|
20
20
|
|
|
21
21
|
## 一、任务前:取回上下文(dream_context)
|
|
22
22
|
|
|
23
|
-
- 任务涉及过往代码、用户偏好或踩过的坑时,先调 `dream_context`:给 `query`,能给就给 `projectId` / `workspaceRoot`
|
|
23
|
+
- 任务涉及过往代码、用户偏好或踩过的坑时,先调 `dream_context`:给 `query`,能给就给 `projectId` / `workspaceRoot`,以及 `packageName` + `packageVersion`(`applicability.versions` 只在该包名被识别时参与判定)。审阅候选时显式传 `includeCandidates: true`。
|
|
24
24
|
- **预算:默认 5 条 / 3000 字符**(工具硬上限 20 条 / 20000 字符)。取回少量真正适用的即可;没有相关经验就接受空结果,不要为了「显得有记忆」硬塞。
|
|
25
25
|
- `dream_context` 是只读的:不写盘,只返回标题、适用条件、行动建议、证据摘要与「为什么与当前任务相关」。
|
|
26
|
-
- 未知当前项目时只会返回全局经验;不会跨项目扫描。候选(candidate
|
|
26
|
+
- 未知当前项目时只会返回全局经验;不会跨项目扫描。候选(candidate)**可被检索到**(显式 `includeCandidates: true`,用于审阅候选),但**默认不注入任务建议**(`includeCandidates: false` 时记 `skipped: candidate-hold`;`independentSupportCount === 0` 且非 `usable` 的记 `no-evidence`),且排在可用经验之后——未经审阅不得当作已证实结论。
|
|
27
27
|
|
|
28
28
|
## 二、任务中:先把真实任务做完
|
|
29
29
|
|
|
@@ -38,7 +38,9 @@ description: 做梦协议:任务开始取回适用经验,长任务收尾沉
|
|
|
38
38
|
- 能定位就填 `sessionId` / `recordSeq`;
|
|
39
39
|
- `summary` 只写脱敏后的一句话,不搬原文;
|
|
40
40
|
- `verification`:`read` = 你或插件读到了原记录;`claimed` = 仅模型声称。**claimed 只计入证据列表,不计入独立支持数。**
|
|
41
|
+
- **只有真的读到来源(会话记录 / 本地验证产物 / 用户明确纠正)才能标 read,自我判断一律 claimed**("我记得""应该是"都算 claimed);标错 read 等于自我提权,禁止。
|
|
41
42
|
- 没有证据的观察只能是 candidate,不能写成确定事实,也不能说成「已验证」。
|
|
43
|
+
- 例外:证据为 `kind: "user-correction"` 且 `verification: "read"`(读到用户纠正原文)时,该经验初始状态直接是 `usable`,`review` 记 `actor: "user"`、`decision: "accepted"`——用户决定优先于模型推测;其余证据一律从 `candidate` 起,claimed 不计独立支持、也不能覆盖用户纠正。
|
|
42
44
|
- 用户明确采纳/驳回、发现经验互相冲突、依赖升级后旧经验需要复核 → 用 `dream_review`(必须带 `expectedRevision`)。
|
|
43
45
|
- 用户采纳必须来自真实用户指令;模型自己不能冒充 `actor: user`。
|
|
44
46
|
- 同一证据重复提交是幂等的,不会增加独立支持数。**禁止靠反复调用 `dream_save` 或 `dream_learn` 刷次数**——次数不等于正确,也不等于重要。
|
|
@@ -81,10 +83,10 @@ Dreams and **lessons** are two separate pipelines: a **first-person reflection**
|
|
|
81
83
|
|
|
82
84
|
## 1. Before the task: retrieve context (dream_context)
|
|
83
85
|
|
|
84
|
-
- When a task touches past code, user preferences or known pitfalls, call `dream_context` first with `query`, plus `projectId` / `workspaceRoot`
|
|
86
|
+
- When a task touches past code, user preferences or known pitfalls, call `dream_context` first with `query`, plus `projectId` / `workspaceRoot` and `packageName` + `packageVersion` when available (`applicability.versions` only counts when that package is identified). Pass `includeCandidates: true` explicitly when reviewing candidates.
|
|
85
87
|
- **Budget: 5 items / 3000 characters by default** (hard caps 20 items / 20000 characters). Take only what is relevant; an empty result is acceptable — never stuff memories in to look useful.
|
|
86
88
|
- `dream_context` is read-only: it writes nothing and returns title, applicability, action, evidence summary and why it is relevant.
|
|
87
|
-
- With an unknown project it returns global lessons only
|
|
89
|
+
- With an unknown project it returns global lessons only and never scans across projects. Candidates **can be retrieved** (`includeCandidates: true`, for candidate review) but are **held out of default task advice** (`includeCandidates: false` reports `skipped: candidate-hold`; evidence-free non-usable items report `no-evidence`), and they rank after usable lessons — never treat one as a verified conclusion before review.
|
|
88
90
|
|
|
89
91
|
## 2. During the task: do the real work first
|
|
90
92
|
|
|
@@ -99,7 +101,9 @@ Dreams and **lessons** are two separate pipelines: a **first-person reflection**
|
|
|
99
101
|
- fill `sessionId` / `recordSeq` when the source can be located;
|
|
100
102
|
- keep `summary` to one masked sentence — never copy the raw record;
|
|
101
103
|
- `verification`: `read` = the record was actually read; `claimed` = the model merely asserts it. **Claimed evidence does not count toward independent support.**
|
|
104
|
+
- **Only mark read when the source was actually read (session record / local artifact / explicit user correction); anything self-asserted is claimed.** Marking read without a source is self-promotion and is forbidden.
|
|
102
105
|
- An observation without evidence stays a candidate: do not present it as a fact, and never call it "verified".
|
|
106
|
+
- Exception: when the evidence is `kind: "user-correction"` with `verification: "read"` (the user's correction was actually read), the lesson starts as `usable` and `review` records `actor: "user"` / `decision: "accepted"` — a user decision outranks model inference. All other evidence starts as `candidate`; claimed evidence does not count toward independent support and cannot override a user correction.
|
|
103
107
|
- For explicit acceptance/rejection, conflicts between lessons, or re-validation after dependency upgrades → call `dream_review` with an `expectedRevision`.
|
|
104
108
|
- User acceptance must come from a real user instruction; the model must not impersonate `actor: user`.
|
|
105
109
|
- Submitting the same evidence twice is idempotent and does not increase independent support. **Never grind counts by calling `dream_save` or `dream_learn` repeatedly** — frequency is neither correctness nor importance.
|