@stardustlc/dsh-dream 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,156 @@
1
+ /** 经验类别:偏好 / 流程 / 坑 / 事实。 */
2
+ export type LessonKind = 'preference' | 'procedure' | 'pitfall' | 'fact';
3
+ /** 经验状态。candidate=候选;usable=可用;disputed=有冲突;stale=待复核;rejected=已拒绝。 */
4
+ export type LessonState = 'candidate' | 'usable' | 'disputed' | 'stale' | 'rejected';
5
+ /** 审阅结论。 */
6
+ export type ReviewDecision = 'unreviewed' | 'accepted' | 'rejected';
7
+ /** 一条可复用经验。 */
8
+ export interface Lesson {
9
+ schemaVersion: 1;
10
+ id: string;
11
+ revision: number;
12
+ kind: LessonKind;
13
+ /** 一句话结论(不含条件时不得单独使用)。 */
14
+ title: string;
15
+ /** 可执行动作。 */
16
+ action: string;
17
+ /** 适用条件(何时该用)。 */
18
+ when: string;
19
+ /** 例外,检索时不得被截断。 */
20
+ exceptions: string[];
21
+ scope: {
22
+ projectId?: string;
23
+ workspaceRoot?: string;
24
+ global: boolean;
25
+ };
26
+ applicability: {
27
+ package?: string;
28
+ versions?: string;
29
+ platform?: string;
30
+ }[];
31
+ state: LessonState;
32
+ evidenceIds: string[];
33
+ /** 独立证据数(同证据复述不增加)。 */
34
+ independentSupportCount: number;
35
+ review: {
36
+ decision: ReviewDecision;
37
+ actor?: string;
38
+ at?: string;
39
+ };
40
+ createdAt: string;
41
+ updatedAt: string;
42
+ lastValidatedAt?: string;
43
+ reviewAfter?: string;
44
+ supersedes?: string[];
45
+ conflictIds: string[];
46
+ }
47
+ /** 一条证据:只存脱敏摘要 + 定位信息。 */
48
+ export interface Evidence {
49
+ schemaVersion: 1;
50
+ id: string;
51
+ kind: 'session' | 'user-correction' | 'local-artifact';
52
+ sessionId?: string;
53
+ recordSeq?: number | string;
54
+ projectId?: string;
55
+ observedAt: string;
56
+ /** 已脱敏的一句话。 */
57
+ summary: string;
58
+ /** read=插件读到原记录;claimed=仅模型声称。 */
59
+ verification: 'read' | 'claimed';
60
+ sourceHash?: string;
61
+ }
62
+ /** dream_learn / 迁移的输入。 */
63
+ export interface LessonInput {
64
+ kind: LessonKind;
65
+ title: string;
66
+ action: string;
67
+ when: string;
68
+ exceptions?: string[];
69
+ projectId?: string;
70
+ workspaceRoot?: string;
71
+ global?: boolean;
72
+ applicability?: Lesson['applicability'];
73
+ evidence: EvidenceInput[];
74
+ conflictIds?: string[];
75
+ }
76
+ /** 证据输入:id / schemaVersion / observedAt 由存储层补齐。 */
77
+ export interface EvidenceInput extends Omit<Evidence, 'schemaVersion' | 'id' | 'observedAt'> {
78
+ observedAt?: string;
79
+ }
80
+ /** 知识事件(events.jsonl 的每一行)。 */
81
+ export type KnowledgeEvent = {
82
+ schemaVersion: 1;
83
+ id: string;
84
+ at: string;
85
+ kind: 'lesson.create';
86
+ lessonId: string;
87
+ idempotencyKey: string;
88
+ payload: Lesson;
89
+ } | {
90
+ schemaVersion: 1;
91
+ id: string;
92
+ at: string;
93
+ kind: 'lesson.update';
94
+ lessonId: string;
95
+ revision: number;
96
+ idempotencyKey: string;
97
+ payload: Partial<Lesson>;
98
+ } | {
99
+ schemaVersion: 1;
100
+ id: string;
101
+ at: string;
102
+ kind: 'lesson.review';
103
+ lessonId: string;
104
+ revision: number;
105
+ idempotencyKey: string;
106
+ payload: {
107
+ decision: ReviewDecision;
108
+ actor?: string;
109
+ state: LessonState;
110
+ };
111
+ } | {
112
+ schemaVersion: 1;
113
+ id: string;
114
+ at: string;
115
+ kind: 'evidence.add';
116
+ idempotencyKey: string;
117
+ payload: Evidence;
118
+ };
119
+ /** 知识层错误码:invalid=数据不合法;revision=乐观锁不符;duplicate=重复;io=读写/锁失败。 */
120
+ export declare class KnowledgeError extends Error {
121
+ code: 'invalid' | 'revision' | 'duplicate' | 'io';
122
+ constructor(code: 'invalid' | 'revision' | 'duplicate' | 'io', message: string);
123
+ }
124
+ /** 校验并归一化一条经验;失败抛 KnowledgeError('invalid')。未知字段会被丢弃。 */
125
+ export declare function validateLesson(value: unknown): Lesson;
126
+ /** 校验并归一化一条证据;失败抛 KnowledgeError('invalid')。未知字段会被丢弃。 */
127
+ export declare function validateEvidence(value: unknown): Evidence;
128
+ /**
129
+ * 规范化用于比较的文本:Unicode NFC + 折叠连续空白 + trim。
130
+ * 展示仍用原样;指纹比较时再统一 lowercase。
131
+ */
132
+ export declare function normalizeLessonText(text: string): string;
133
+ /** lessonFingerprint 的最小输入;title 可选,但建议传入,让“应开启/不应开启”这类结论差异参与指纹。 */
134
+ export type LessonFingerprintInput = Pick<Lesson, 'kind' | 'action' | 'when' | 'scope'> & {
135
+ title?: string;
136
+ };
137
+ /**
138
+ * 经验指纹:kind + title + action + when + scope 的规范化小写序列表,
139
+ * sha256 取前 16 位十六进制。否定词、数值、路径、版本条件原样参与。
140
+ */
141
+ export declare function lessonFingerprint(input: LessonFingerprintInput): string;
142
+ /** 状态机:from → to 是否允许。同状态与未知状态一律不允许。 */
143
+ export declare function canTransition(from: LessonState, to: LessonState): boolean;
144
+ /** sessionId + recordSeq 组成的会话定位键;两者缺一不可。 */
145
+ export declare function evidenceSessionKey(evidence: Pick<Evidence, 'sessionId' | 'recordSeq'>): string | undefined;
146
+ /** sourceHash 键;空串视为缺失。 */
147
+ export declare function evidenceHashKey(evidence: Pick<Evidence, 'sourceHash'>): string | undefined;
148
+ /** 证据的天然身份键:同 sessionId + recordSeq 优先,其次同 sourceHash。 */
149
+ export declare function evidenceNaturalKey(evidence: Pick<Evidence, 'sessionId' | 'recordSeq' | 'sourceHash'>): string | undefined;
150
+ /** 去重键:优先天然身份键,否则退回证据 id。 */
151
+ export declare function evidenceDedupKey(evidence: Pick<Evidence, 'id' | 'sessionId' | 'recordSeq' | 'sourceHash'>): string;
152
+ /**
153
+ * 去重后的独立证据数:只看 lesson.evidenceIds 引用到的证据,
154
+ * 同 sessionId+recordSeq 或同 sourceHash 只算一次;verification=claimed 不计入。
155
+ */
156
+ export declare function mergeEvidenceSupport(lesson: Pick<Lesson, 'evidenceIds'>, evidence: Evidence[]): number;
@@ -0,0 +1,347 @@
1
+ /**
2
+ * dsh-dream 知识层:经验(Lesson)、证据(Evidence)与知识事件的类型定义、
3
+ * schema 校验与确定性纯函数。
4
+ *
5
+ * 本模块只做纯计算,不落盘;落盘见 knowledge-store.ts。
6
+ *
7
+ * @module dsh-dream/knowledge
8
+ */
9
+ import { createHash } from 'node:crypto';
10
+ /** 知识层错误码:invalid=数据不合法;revision=乐观锁不符;duplicate=重复;io=读写/锁失败。 */
11
+ export class KnowledgeError extends Error {
12
+ code;
13
+ constructor(code, message) {
14
+ super(message);
15
+ this.name = 'KnowledgeError';
16
+ this.code = code;
17
+ }
18
+ }
19
+ const LESSON_KINDS = ['preference', 'procedure', 'pitfall', 'fact'];
20
+ const LESSON_STATES = ['candidate', 'usable', 'disputed', 'stale', 'rejected'];
21
+ const REVIEW_DECISIONS = ['unreviewed', 'accepted', 'rejected'];
22
+ const EVIDENCE_KINDS = ['session', 'user-correction', 'local-artifact'];
23
+ const EVIDENCE_VERIFICATIONS = ['read', 'claimed'];
24
+ /** 允许的状态迁移。rejected 只能显式回到 candidate 重新审阅,不能直接变 usable。 */
25
+ const TRANSITIONS = {
26
+ candidate: ['usable', 'rejected', 'disputed', 'stale'],
27
+ usable: ['disputed', 'stale', 'rejected'],
28
+ disputed: ['usable', 'rejected', 'stale'],
29
+ stale: ['usable', 'rejected', 'disputed'],
30
+ rejected: ['candidate'],
31
+ };
32
+ function fail(field, detail) {
33
+ throw new KnowledgeError('invalid', '知识数据无效(' + field + '):' + detail);
34
+ }
35
+ function isRecord(value) {
36
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
37
+ }
38
+ function isEnum(value, allowed) {
39
+ return typeof value === 'string' && allowed.includes(value);
40
+ }
41
+ function readString(value, field, opts = {}) {
42
+ if (value === undefined || value === null) {
43
+ if (opts.required === true)
44
+ fail(field, '缺失');
45
+ return undefined;
46
+ }
47
+ if (typeof value !== 'string')
48
+ fail(field, '必须是字符串');
49
+ if (opts.nonEmpty === true && value.trim() === '')
50
+ fail(field, '不能为空');
51
+ return value;
52
+ }
53
+ function readStringArray(value, field) {
54
+ if (value === undefined || value === null)
55
+ return undefined;
56
+ if (!Array.isArray(value))
57
+ fail(field, '必须是字符串数组');
58
+ const out = [];
59
+ for (let i = 0; i < value.length; i++) {
60
+ const item = value[i];
61
+ if (typeof item !== 'string')
62
+ fail(field + '[' + i + ']', '必须是字符串');
63
+ if (item.trim() === '')
64
+ fail(field + '[' + i + ']', '不能为空');
65
+ if (!out.includes(item))
66
+ out.push(item);
67
+ }
68
+ return out;
69
+ }
70
+ function readIsoDate(value, field, required = false) {
71
+ const text = readString(value, field, { required, nonEmpty: true });
72
+ if (text === undefined)
73
+ return undefined;
74
+ if (Number.isNaN(Date.parse(text)))
75
+ fail(field, '必须是可解析的 ISO 时间');
76
+ return text;
77
+ }
78
+ function readScope(value) {
79
+ if (!isRecord(value))
80
+ fail('scope', '必须是对象');
81
+ const global = value.global;
82
+ if (typeof global !== 'boolean')
83
+ fail('scope.global', '必须是布尔值');
84
+ const projectId = readString(value.projectId, 'scope.projectId', { nonEmpty: true });
85
+ const workspaceRoot = readString(value.workspaceRoot, 'scope.workspaceRoot', { nonEmpty: true });
86
+ const scope = { global };
87
+ if (projectId !== undefined)
88
+ scope.projectId = projectId;
89
+ if (workspaceRoot !== undefined)
90
+ scope.workspaceRoot = workspaceRoot;
91
+ return scope;
92
+ }
93
+ function readApplicability(value) {
94
+ if (value === undefined || value === null)
95
+ return [];
96
+ if (!Array.isArray(value))
97
+ fail('applicability', '必须是对象数组');
98
+ const out = [];
99
+ for (let i = 0; i < value.length; i++) {
100
+ const entry = value[i];
101
+ if (!isRecord(entry))
102
+ fail('applicability[' + i + ']', '必须是对象');
103
+ const item = {};
104
+ const pkg = readString(entry.package, 'applicability[' + i + '].package', { nonEmpty: true });
105
+ const versions = readString(entry.versions, 'applicability[' + i + '].versions', { nonEmpty: true });
106
+ const platform = readString(entry.platform, 'applicability[' + i + '].platform', { nonEmpty: true });
107
+ if (pkg !== undefined)
108
+ item.package = pkg;
109
+ if (versions !== undefined)
110
+ item.versions = versions;
111
+ if (platform !== undefined)
112
+ item.platform = platform;
113
+ out.push(item);
114
+ }
115
+ return out;
116
+ }
117
+ function readReview(value) {
118
+ if (value === undefined || value === null)
119
+ return { decision: 'unreviewed' };
120
+ if (!isRecord(value))
121
+ fail('review', '必须是对象');
122
+ const decision = value.decision;
123
+ if (!isEnum(decision, REVIEW_DECISIONS))
124
+ fail('review.decision', '取值不合法');
125
+ const review = { decision };
126
+ const actor = readString(value.actor, 'review.actor', { nonEmpty: true });
127
+ const at = readIsoDate(value.at, 'review.at');
128
+ if (actor !== undefined)
129
+ review.actor = actor;
130
+ if (at !== undefined)
131
+ review.at = at;
132
+ return review;
133
+ }
134
+ /** 校验并归一化一条经验;失败抛 KnowledgeError('invalid')。未知字段会被丢弃。 */
135
+ export function validateLesson(value) {
136
+ if (!isRecord(value))
137
+ fail('lesson', '必须是对象');
138
+ if (value.schemaVersion !== 1)
139
+ fail('schemaVersion', '只支持 1');
140
+ const id = readString(value.id, 'id', { required: true, nonEmpty: true });
141
+ const revision = value.revision;
142
+ if (typeof revision !== 'number' || !Number.isInteger(revision) || revision < 1)
143
+ fail('revision', '必须是 >= 1 的整数');
144
+ const kind = value.kind;
145
+ if (!isEnum(kind, LESSON_KINDS))
146
+ fail('kind', '取值不合法');
147
+ const title = readString(value.title, 'title', { required: true, nonEmpty: true });
148
+ const action = readString(value.action, 'action', { required: true, nonEmpty: true });
149
+ const when = readString(value.when, 'when', { required: true, nonEmpty: true });
150
+ const exceptions = readStringArray(value.exceptions, 'exceptions') ?? [];
151
+ const scope = readScope(value.scope);
152
+ const applicability = readApplicability(value.applicability);
153
+ const state = value.state;
154
+ if (!isEnum(state, LESSON_STATES))
155
+ fail('state', '取值不合法');
156
+ const evidenceIds = readStringArray(value.evidenceIds, 'evidenceIds') ?? [];
157
+ const independentSupportCount = value.independentSupportCount;
158
+ if (typeof independentSupportCount !== 'number' || !Number.isInteger(independentSupportCount) || independentSupportCount < 0) {
159
+ fail('independentSupportCount', '必须是 >= 0 的整数');
160
+ }
161
+ const review = readReview(value.review);
162
+ const createdAt = readIsoDate(value.createdAt, 'createdAt', true);
163
+ const updatedAt = readIsoDate(value.updatedAt, 'updatedAt', true);
164
+ const lastValidatedAt = readIsoDate(value.lastValidatedAt, 'lastValidatedAt');
165
+ const reviewAfter = readIsoDate(value.reviewAfter, 'reviewAfter');
166
+ const supersedes = readStringArray(value.supersedes, 'supersedes');
167
+ const conflictIds = readStringArray(value.conflictIds, 'conflictIds') ?? [];
168
+ const lesson = {
169
+ schemaVersion: 1,
170
+ id,
171
+ revision,
172
+ kind,
173
+ title,
174
+ action,
175
+ when,
176
+ exceptions,
177
+ scope,
178
+ applicability,
179
+ state,
180
+ evidenceIds,
181
+ independentSupportCount,
182
+ review,
183
+ createdAt,
184
+ updatedAt,
185
+ conflictIds,
186
+ };
187
+ if (lastValidatedAt !== undefined)
188
+ lesson.lastValidatedAt = lastValidatedAt;
189
+ if (reviewAfter !== undefined)
190
+ lesson.reviewAfter = reviewAfter;
191
+ if (supersedes !== undefined)
192
+ lesson.supersedes = supersedes;
193
+ return lesson;
194
+ }
195
+ /** 校验并归一化一条证据;失败抛 KnowledgeError('invalid')。未知字段会被丢弃。 */
196
+ export function validateEvidence(value) {
197
+ if (!isRecord(value))
198
+ fail('evidence', '必须是对象');
199
+ if (value.schemaVersion !== 1)
200
+ fail('evidence.schemaVersion', '只支持 1');
201
+ const id = readString(value.id, 'evidence.id', { required: true, nonEmpty: true });
202
+ const kind = value.kind;
203
+ if (!isEnum(kind, EVIDENCE_KINDS))
204
+ fail('evidence.kind', '取值不合法');
205
+ const verification = value.verification;
206
+ if (!isEnum(verification, EVIDENCE_VERIFICATIONS))
207
+ fail('evidence.verification', '取值不合法');
208
+ const observedAt = readIsoDate(value.observedAt, 'evidence.observedAt', true);
209
+ const summary = readString(value.summary, 'evidence.summary', { required: true, nonEmpty: true });
210
+ const sessionId = readString(value.sessionId, 'evidence.sessionId', { nonEmpty: true });
211
+ const projectId = readString(value.projectId, 'evidence.projectId', { nonEmpty: true });
212
+ const sourceHash = readString(value.sourceHash, 'evidence.sourceHash', { nonEmpty: true });
213
+ const recordSeqRaw = value.recordSeq;
214
+ let recordSeq;
215
+ if (recordSeqRaw !== undefined && recordSeqRaw !== null) {
216
+ if (typeof recordSeqRaw === 'number') {
217
+ if (!Number.isInteger(recordSeqRaw) || recordSeqRaw < 0)
218
+ fail('evidence.recordSeq', '数字必须是 >= 0 的整数');
219
+ recordSeq = recordSeqRaw;
220
+ }
221
+ else if (typeof recordSeqRaw === 'string') {
222
+ if (recordSeqRaw.trim() === '')
223
+ fail('evidence.recordSeq', '字符串不能为空');
224
+ recordSeq = recordSeqRaw;
225
+ }
226
+ else {
227
+ fail('evidence.recordSeq', '必须是数字或字符串');
228
+ }
229
+ }
230
+ const evidence = { schemaVersion: 1, id, kind, observedAt, summary, verification };
231
+ if (sessionId !== undefined)
232
+ evidence.sessionId = sessionId;
233
+ if (recordSeq !== undefined)
234
+ evidence.recordSeq = recordSeq;
235
+ if (projectId !== undefined)
236
+ evidence.projectId = projectId;
237
+ if (sourceHash !== undefined)
238
+ evidence.sourceHash = sourceHash;
239
+ return evidence;
240
+ }
241
+ /**
242
+ * 规范化用于比较的文本:Unicode NFC + 折叠连续空白 + trim。
243
+ * 展示仍用原样;指纹比较时再统一 lowercase。
244
+ */
245
+ export function normalizeLessonText(text) {
246
+ if (typeof text !== 'string')
247
+ throw new KnowledgeError('invalid', 'normalizeLessonText 需要字符串');
248
+ return text.normalize('NFC').replace(/\s+/g, ' ').trim();
249
+ }
250
+ /**
251
+ * 经验指纹:kind + title + action + when + scope 的规范化小写序列表,
252
+ * sha256 取前 16 位十六进制。否定词、数值、路径、版本条件原样参与。
253
+ */
254
+ export function lessonFingerprint(input) {
255
+ const part = (text) => normalizeLessonText(text === undefined ? '' : text).toLowerCase();
256
+ const scope = input.scope;
257
+ const canonical = JSON.stringify([
258
+ input.kind,
259
+ part(input.title),
260
+ part(input.action),
261
+ part(input.when),
262
+ {
263
+ global: scope !== undefined && scope.global === true,
264
+ projectId: part(scope === undefined ? undefined : scope.projectId) || null,
265
+ workspaceRoot: part(scope === undefined ? undefined : scope.workspaceRoot) || null,
266
+ },
267
+ ]);
268
+ return createHash('sha256').update(canonical, 'utf8').digest('hex').slice(0, 16);
269
+ }
270
+ /** 状态机:from → to 是否允许。同状态与未知状态一律不允许。 */
271
+ export function canTransition(from, to) {
272
+ if (!isEnum(from, LESSON_STATES) || !isEnum(to, LESSON_STATES))
273
+ return false;
274
+ return TRANSITIONS[from].includes(to);
275
+ }
276
+ /** sessionId + recordSeq 组成的会话定位键;两者缺一不可。 */
277
+ export function evidenceSessionKey(evidence) {
278
+ if (typeof evidence.sessionId === 'string' &&
279
+ evidence.sessionId !== '' &&
280
+ evidence.recordSeq !== undefined &&
281
+ evidence.recordSeq !== null) {
282
+ return 'session:' + evidence.sessionId + '#' + String(evidence.recordSeq);
283
+ }
284
+ return undefined;
285
+ }
286
+ /** sourceHash 键;空串视为缺失。 */
287
+ export function evidenceHashKey(evidence) {
288
+ if (typeof evidence.sourceHash === 'string' && evidence.sourceHash !== '') {
289
+ return 'hash:' + evidence.sourceHash;
290
+ }
291
+ return undefined;
292
+ }
293
+ /** 证据的天然身份键:同 sessionId + recordSeq 优先,其次同 sourceHash。 */
294
+ export function evidenceNaturalKey(evidence) {
295
+ return evidenceSessionKey(evidence) ?? evidenceHashKey(evidence);
296
+ }
297
+ /** 去重键:优先天然身份键,否则退回证据 id。 */
298
+ export function evidenceDedupKey(evidence) {
299
+ return evidenceNaturalKey(evidence) ?? 'id:' + String(evidence.id);
300
+ }
301
+ /**
302
+ * 去重后的独立证据数:只看 lesson.evidenceIds 引用到的证据,
303
+ * 同 sessionId+recordSeq 或同 sourceHash 只算一次;verification=claimed 不计入。
304
+ */
305
+ export function mergeEvidenceSupport(lesson, evidence) {
306
+ const wanted = new Set(lesson.evidenceIds);
307
+ const items = evidence.filter((item) => wanted.has(item.id) && item.verification === 'read');
308
+ const parent = items.map((_, index) => index);
309
+ const find = (index) => {
310
+ let current = index;
311
+ while (parent[current] !== current) {
312
+ parent[current] = parent[parent[current]];
313
+ current = parent[current];
314
+ }
315
+ return current;
316
+ };
317
+ const union = (a, b) => {
318
+ const rootA = find(a);
319
+ const rootB = find(b);
320
+ if (rootA !== rootB)
321
+ parent[rootB] = rootA;
322
+ };
323
+ const bySession = new Map();
324
+ const byHash = new Map();
325
+ for (let i = 0; i < items.length; i++) {
326
+ const sessionKey = evidenceSessionKey(items[i]);
327
+ if (sessionKey !== undefined) {
328
+ const seen = bySession.get(sessionKey);
329
+ if (seen === undefined)
330
+ bySession.set(sessionKey, i);
331
+ else
332
+ union(i, seen);
333
+ }
334
+ const hashKey = evidenceHashKey(items[i]);
335
+ if (hashKey !== undefined) {
336
+ const seen = byHash.get(hashKey);
337
+ if (seen === undefined)
338
+ byHash.set(hashKey, i);
339
+ else
340
+ union(i, seen);
341
+ }
342
+ }
343
+ const roots = new Set();
344
+ for (let i = 0; i < items.length; i++)
345
+ roots.add(find(i));
346
+ return roots.size;
347
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * 任务相关经验检索(FREEZE §3)。
3
+ *
4
+ * 顺序:范围过滤 → 排除 rejected/stale/disputed → 确定性排序 → 字符预算。
5
+ * 条件(when)与例外(exceptions)不可截断:整条放不下时跳过并记 skipped.budget,
6
+ * 绝不返回半条经验。
7
+ *
8
+ * @module dsh-dream/retrieval
9
+ */
10
+ import type { Evidence, Lesson, LessonState } from './knowledge.js';
11
+ /** 检索入参;limit/maxChars 在内部按硬上限裁剪。 */
12
+ export interface RetrievalQuery {
13
+ query: string;
14
+ projectId?: string;
15
+ workspaceRoot?: string;
16
+ /** 默认 5,硬上限 20。 */
17
+ limit?: number;
18
+ /** 默认 3000,硬上限 20000。 */
19
+ maxChars?: number;
20
+ /** 当前任务相关依赖的版本,用于匹配 applicability.versions。 */
21
+ packageVersion?: string;
22
+ }
23
+ /** 检索结果中的单条经验(已完整保留 when/exceptions)。 */
24
+ export interface RetrievedLesson {
25
+ lessonId: string;
26
+ title: string;
27
+ when: string;
28
+ action: string;
29
+ exceptions: string[];
30
+ state: LessonState;
31
+ scopeLabel: string;
32
+ whyRelevant: string;
33
+ evidenceSummary: string;
34
+ lastValidatedAt?: string;
35
+ truncated: boolean;
36
+ }
37
+ /** 默认条数。 */
38
+ export declare const DEFAULT_RETRIEVAL_LIMIT = 5;
39
+ /** 条数硬上限。 */
40
+ export declare const MAX_RETRIEVAL_LIMIT = 20;
41
+ /** 默认字符预算。 */
42
+ export declare const DEFAULT_RETRIEVAL_MAX_CHARS = 3000;
43
+ /** 字符预算硬上限。 */
44
+ export declare const MAX_RETRIEVAL_MAX_CHARS = 20000;
45
+ /** 查询侧范围文案(面板与 dream_context 共用)。 */
46
+ export declare function queryScopeLabel(q: Pick<RetrievalQuery, 'projectId' | 'workspaceRoot'>): string;
47
+ /** 经验范围文案(面板与 dream_context 共用)。 */
48
+ export declare function lessonScopeLabel(scope: Lesson['scope']): string;
49
+ /** 证据摘要(面板与 dream_context 共用):独立支持数 + 已读/仅声明 + 脱敏摘要片段。 */
50
+ export declare function buildEvidenceSummary(lesson: Lesson, evidence: Evidence[]): string;
51
+ /** 单条结果的字符成本(与 dream_context.budget.usedChars 同口径)。 */
52
+ export declare function retrievedLessonChars(item: RetrievedLesson): number;
53
+ /** 解析 limit/maxChars,套用默认值与硬上限。 */
54
+ export declare function resolveRetrievalBudget(q: Pick<RetrievalQuery, 'limit' | 'maxChars'>): {
55
+ limit: number;
56
+ maxChars: number;
57
+ };
58
+ /** 简单 semver 条件匹配:支持 = / > / >= / < / <= / ^ / ~、逗号或空格 AND、|| OR、x/* 通配。 */
59
+ export declare function versionSatisfies(versionText: string, rangeText: string): boolean;
60
+ /**
61
+ * 任务相关经验检索。只返回 global 或与查询项目/工作区精确匹配的记录;
62
+ * 未知项目(未给 projectId/workspaceRoot)时只返回 global,绝不跨项目扫描。
63
+ */
64
+ export declare function retrieveLessons(lessons: Lesson[], evidence: Evidence[], q: RetrievalQuery): {
65
+ items: RetrievedLesson[];
66
+ skipped: {
67
+ lessonId: string;
68
+ reason: string;
69
+ }[];
70
+ };