@stardustlc/dsh-dream 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/web.d.ts CHANGED
@@ -5,6 +5,8 @@ import type { Evidence, Lesson } from './knowledge.js';
5
5
  export declare const DREAM_ROUTE = "/_dsh/dsh-dream/journal";
6
6
  /** M1 经验面板的只读路由。 */
7
7
  export declare const DREAM_KNOWLEDGE_ROUTE = "/_dsh/dsh-dream/knowledge";
8
+ /** M2 写入记录面板的只读路由。 */
9
+ export declare const DREAM_BRIDGE_ROUTE = "/_dsh/dsh-dream/bridge";
8
10
  /** ?limit 的默认值与上下界。 */
9
11
  export declare const DREAM_LIMIT_DEFAULT = 50;
10
12
  export declare const DREAM_LIMIT_MIN = 1;
@@ -24,13 +26,16 @@ export interface DreamWebPayload {
24
26
  /** 本次生效的条数上限。 */
25
27
  limit: number;
26
28
  }
27
- /** 知识路由的统计形状(与 FREEZE §2 的 stats() 对齐)。 */
29
+ /** 知识路由的统计形状(与 FREEZE §2 的 stats() 对齐;v1.1 R8 透传 badLines)。 */
28
30
  export interface KnowledgeWebStats {
29
31
  lessons: number;
30
32
  evidence: number;
31
33
  events: number;
32
34
  byState: Record<string, number>;
35
+ /** 有界回放被截断时为 true(面板据此显示提示行)。 */
33
36
  truncated?: boolean;
37
+ /** 事件 / 证据流中被跳过的坏行数(面板据此显示提示行)。 */
38
+ badLines: number;
34
39
  }
35
40
  /** 知识路由的 JSON 形状:脱敏后的经验 / 证据 / 统计。 */
36
41
  export interface KnowledgeWebPayload {
@@ -50,30 +55,75 @@ export interface KnowledgeReader {
50
55
  listEvidence(): Evidence[];
51
56
  stats(): KnowledgeWebStats;
52
57
  }
58
+ /** 写入记录的单条视图(展示用;字段缺失时按空字符串 / false 归一化)。 */
59
+ export interface BridgeWebLessonRef {
60
+ lessonId: string;
61
+ revision: number;
62
+ }
63
+ export interface BridgeWebRecord {
64
+ backupId: string;
65
+ at: string;
66
+ target: string;
67
+ action: string;
68
+ lessons: BridgeWebLessonRef[];
69
+ beforeSha256: string;
70
+ afterSha256: string;
71
+ rollbackable: boolean;
72
+ rollbackReason?: string;
73
+ }
74
+ /** 写入记录路由的 JSON 形状:脱敏后的应用记录 + 汇总。 */
75
+ export interface BridgeWebPayload {
76
+ records: BridgeWebRecord[];
77
+ stats: {
78
+ total: number;
79
+ rollbackable: number;
80
+ };
81
+ }
82
+ /** 桥接存储层返回的原始记录形状(字段由 src/bridge.ts 负责)。 */
83
+ export type BridgeApplicationView = Record<string, unknown>;
84
+ /** 只读桥接记录源的最小接口:方便测试注入假数据。 */
85
+ export interface BridgeReader {
86
+ listApplications(): BridgeApplicationView[];
87
+ }
53
88
  /** 安装参数;journalDir 缺省时从 config 解析。 */
54
89
  export interface DreamWebOptions {
55
90
  /** 梦境日记目录(优先)。 */
56
91
  journalDir?: string;
57
92
  /** 知识目录(优先;缺省 = <journalDir>/knowledge)。 */
58
93
  knowledgeDir?: string;
94
+ /** 桥接记录目录(优先;缺省 = <journalDir>/bridge)。 */
95
+ bridgeDir?: string;
59
96
  /** 插件配置,用来兜底 journalDir(resolveConfig 的入参)。 */
60
97
  config?: Record<string, unknown> | ResolvedDreamConfig | null;
61
98
  /** 测试注入:返回只读知识源;提供时不做目录存在性检查。 */
62
99
  knowledgeStoreFactory?: (knowledgeDir: string) => KnowledgeReader;
100
+ /** 测试注入:返回只读桥接记录源;提供时不做目录存在性检查。 */
101
+ bridgeStoreFactory?: (bridgeDir: string) => BridgeReader;
63
102
  }
64
103
  /** ?limit 解析:缺省/非数字 → fallback;越界钳制到 [1, 500]。 */
65
104
  export declare function limitFromQuery(raw: unknown, fallback?: number): number;
66
105
  /** 解析后的 knowledgeDir:显式路径优先,否则 = <journalDir>/knowledge。 */
67
106
  export declare function knowledgeDirOf(options: DreamWebOptions, journalDir?: string): string;
107
+ /** 解析后的 bridgeDir:显式路径优先,否则 = <journalDir>/bridge。 */
108
+ export declare function bridgeDirOf(options: DreamWebOptions, journalDir?: string): string;
68
109
  /** 知识目录不存在(或尚未建立)时的空载荷:不写盘、不报 404。 */
69
110
  export declare function emptyKnowledgePayload(): KnowledgeWebPayload;
111
+ /**
112
+ * 归一化桥接应用记录:只保留展示需要的字段,坏形状跳过。
113
+ * 数据源由 src/bridge.ts 负责(本层不做 IO、不重算可否回滚)。
114
+ */
115
+ export declare function normalizeBridgeApplications(value: unknown): BridgeWebRecord[];
116
+ /** 写入记录目录不存在时的空载荷。 */
117
+ export declare function emptyBridgePayload(): BridgeWebPayload;
70
118
  /** 组装梦境日记路由处理器;journalDir 在这一刻定下来(与插件的配置生命周期一致)。 */
71
119
  export declare function createDreamWebHandler(options?: DreamWebOptions): (req: any, res: any) => Promise<void>;
120
+ /** 组装只读写入记录路由处理器;零写盘:目录不存在就直接返回空结果。 */
121
+ export declare function createBridgeWebHandler(options?: DreamWebOptions): (req: any, res: any) => Promise<void>;
72
122
  /** 组装只读经验路由处理器;零写盘:目录不存在就直接返回空结果。 */
73
123
  export declare function createKnowledgeWebHandler(options?: DreamWebOptions): (req: any, res: any) => Promise<void>;
74
124
  /**
75
125
  * 把只读路由挂到宿主 webserver 上。
76
126
  * 与 dsh-calendar 相同:ctx.inject(['webServer']) + effect 注册,插件卸载即摘掉路由;
77
- * 同一个 effect 里注册日记与知识两条 exact 路由,卸载时一起释放。
127
+ * 同一个 effect 里注册日记 / 知识 / 写入记录三条 exact 路由,卸载时一起释放。
78
128
  */
79
129
  export declare function installDreamWeb(ctx: any, options?: DreamWebOptions): void;
package/lib/web.js CHANGED
@@ -6,25 +6,28 @@
6
6
  * 三道门缺一不可 ——
7
7
  * 1. remoteAddress 若是明确的非回环地址则拒绝(局域网不可达);
8
8
  * 2. Host 头必须是 localhost 名(挡 DNS rebinding:恶意域名解析到 127.0.0.1);
9
- * 3. 只接受 GET:两条路由都没有任何写路径,所以不需要 CSRF 那一套。
9
+ * 3. 只接受 GET:三条路由都没有任何写路径,所以不需要 CSRF 那一套。
10
10
  *
11
- * 零写盘纪律(M1):知识路由只读 <journalDir>/knowledge/。目录不存在时直接返回
12
- * 空 lessons/evidence + 全零 stats,绝不为了读去 mkdir 或 rebuildIndex;重建索引
13
- * 只发生在写路径或显式维护操作里。
11
+ * 零写盘纪律(M1/M2):知识路由只读 <journalDir>/knowledge/,写入记录路由只读
12
+ * <journalDir>/bridge/。目录不存在时直接返回空结果,绝不为了读去 mkdir 或 rebuildIndex;
13
+ * 重建索引只发生在写路径或显式维护操作里。
14
14
  *
15
15
  * @module dsh-dream/web
16
16
  */
17
17
  import { existsSync } from 'node:fs';
18
- import { join } from 'node:path';
18
+ import { isAbsolute, join, win32 } from 'node:path';
19
+ import * as bridgeModule from './bridge.js';
19
20
  import { resolveConfig } from './config.js';
20
21
  import { dreamStats, readDreams, searchDreams } from './journal.js';
21
22
  import { KnowledgeStore } from './knowledge-store.js';
22
- import { maskSecrets } from './mask.js';
23
+ import { isDreamReference, maskSecrets } from './mask.js';
23
24
  import { buildEvidenceSummary, lessonScopeLabel } from './retrieval.js';
24
25
  /** 面板与浏览器说话的同源路由。 */
25
26
  export const DREAM_ROUTE = '/_dsh/dsh-dream/journal';
26
27
  /** M1 经验面板的只读路由。 */
27
28
  export const DREAM_KNOWLEDGE_ROUTE = '/_dsh/dsh-dream/knowledge';
29
+ /** M2 写入记录面板的只读路由。 */
30
+ export const DREAM_BRIDGE_ROUTE = '/_dsh/dsh-dream/bridge';
28
31
  /** ?limit 的默认值与上下界。 */
29
32
  export const DREAM_LIMIT_DEFAULT = 50;
30
33
  export const DREAM_LIMIT_MIN = 1;
@@ -71,6 +74,12 @@ export function knowledgeDirOf(options, journalDir = journalDirOf(options)) {
71
74
  return options.knowledgeDir;
72
75
  return join(journalDir, 'knowledge');
73
76
  }
77
+ /** 解析后的 bridgeDir:显式路径优先,否则 = <journalDir>/bridge。 */
78
+ export function bridgeDirOf(options, journalDir = journalDirOf(options)) {
79
+ if (typeof options.bridgeDir === 'string' && options.bridgeDir.trim() !== '')
80
+ return options.bridgeDir;
81
+ return join(journalDir, 'bridge');
82
+ }
74
83
  /** 解析请求 URL;非法或缺失时退回路由本身(等价于没有查询参数)。 */
75
84
  function requestUrl(raw, fallback = DREAM_ROUTE) {
76
85
  const text = typeof raw === 'string' && raw !== '' ? raw : fallback;
@@ -125,21 +134,33 @@ function requestGate(req, res, readOnlyMessage) {
125
134
  }
126
135
  return false;
127
136
  }
128
- /** 递归脱敏所有字符串叶子;数字 / 布尔 / null 原样保留。 */
129
- function maskDeep(value) {
130
- if (typeof value === 'string')
137
+ /** 哈希字段不做二次脱敏:它们是校验值,不是密钥(面板要展示前 12 位)。 */
138
+ const HASH_KEY_RE = /^(?:sourceHash|beforeSha256|afterSha256|afterBlockSha256|afterFileSha256|sha256)$/;
139
+ const REFERENCE_KEYS = new Set(['id', 'lessonId', 'lessonIds', 'evidenceIds', 'conflictIds', 'supersedes', 'targetId', 'affectedIds', 'backupId', 'previewId']);
140
+ const PATH_KEYS = new Set(['target', 'path', 'resolvedPath', 'workspaceRoot']);
141
+ /** 递归脱敏;结构化绝对路径按目录分段,避免 Unix 分隔符把正常路径连成长令牌。 */
142
+ function maskDeep(value, key) {
143
+ if (typeof value === 'string') {
144
+ if (key !== undefined && HASH_KEY_RE.test(key) && /^[0-9a-f]{64}$/i.test(value))
145
+ return value;
146
+ if (key !== undefined && REFERENCE_KEYS.has(key) && isDreamReference(value))
147
+ return value;
148
+ if (key !== undefined && PATH_KEYS.has(key) && (isAbsolute(value) || win32.isAbsolute(value)) && !/[\x00-\x1f]/.test(value)) {
149
+ return value.split(/([\\/]+)/).map((part) => maskSecrets(part)).join('');
150
+ }
131
151
  return maskSecrets(value);
152
+ }
132
153
  if (Array.isArray(value))
133
- return value.map((item) => maskDeep(item));
154
+ return value.map((item) => maskDeep(item, key));
134
155
  if (value !== null && typeof value === 'object') {
135
156
  const out = {};
136
- for (const [key, item] of Object.entries(value))
137
- out[key] = maskDeep(item);
157
+ for (const [entryKey, item] of Object.entries(value))
158
+ out[entryKey] = maskDeep(item, entryKey);
138
159
  return out;
139
160
  }
140
161
  return value;
141
162
  }
142
- /** 统计归一化:数字字段兜底为 0,byState 五个键齐全,truncated 只在为 true 时保留。 */
163
+ /** 统计归一化:数字字段兜底为 0,byState 五个键齐全,truncated 只在为 true 时保留,badLines 始终透传。 */
143
164
  function normalizeKnowledgeStats(value) {
144
165
  const source = value !== null && typeof value === 'object' ? value : {};
145
166
  const rawByState = source.byState !== null && typeof source.byState === 'object'
@@ -159,6 +180,7 @@ function normalizeKnowledgeStats(value) {
159
180
  evidence: numberAt('evidence'),
160
181
  events: numberAt('events'),
161
182
  byState,
183
+ badLines: Math.floor(numberAt('badLines')),
162
184
  };
163
185
  if (source.truncated === true)
164
186
  stats.truncated = true;
@@ -172,6 +194,86 @@ export function emptyKnowledgePayload() {
172
194
  stats: normalizeKnowledgeStats(null),
173
195
  };
174
196
  }
197
+ function asDisplayString(value) {
198
+ return typeof value === 'string' ? value.trim() : '';
199
+ }
200
+ function asDisplayRevision(value) {
201
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? Math.floor(value) : 0;
202
+ }
203
+ /**
204
+ * 归一化桥接应用记录:只保留展示需要的字段,坏形状跳过。
205
+ * 数据源由 src/bridge.ts 负责(本层不做 IO、不重算可否回滚)。
206
+ */
207
+ export function normalizeBridgeApplications(value) {
208
+ if (!Array.isArray(value))
209
+ return [];
210
+ const records = [];
211
+ for (const raw of value) {
212
+ if (raw === null || typeof raw !== 'object')
213
+ continue;
214
+ const item = raw;
215
+ const backupId = asDisplayString(item.backupId);
216
+ const target = asDisplayString(item.target);
217
+ if (backupId === '' && target === '')
218
+ continue;
219
+ const lessons = [];
220
+ if (Array.isArray(item.lessons)) {
221
+ for (const entry of item.lessons) {
222
+ if (entry !== null && typeof entry === 'object') {
223
+ const ref = entry;
224
+ const lessonId = asDisplayString(ref.lessonId);
225
+ if (lessonId === '')
226
+ continue;
227
+ lessons.push({ lessonId, revision: asDisplayRevision(ref.revision) });
228
+ }
229
+ else {
230
+ const text = asDisplayString(entry);
231
+ if (text === '')
232
+ continue;
233
+ const match = /^(.*)@(\d+)$/.exec(text);
234
+ if (match !== null)
235
+ lessons.push({ lessonId: match[1], revision: Number(match[2]) });
236
+ else
237
+ lessons.push({ lessonId: text, revision: 0 });
238
+ }
239
+ }
240
+ }
241
+ const record = {
242
+ backupId,
243
+ at: asDisplayString(item.at),
244
+ target,
245
+ action: asDisplayString(item.action),
246
+ lessons,
247
+ beforeSha256: asDisplayString(item.beforeSha256),
248
+ afterSha256: asDisplayString(item.afterSha256),
249
+ rollbackable: item.rollbackable === true,
250
+ };
251
+ const reason = asDisplayString(item.rollbackReason);
252
+ if (reason !== '')
253
+ record.rollbackReason = reason;
254
+ records.push(record);
255
+ }
256
+ return records;
257
+ }
258
+ /** 写入记录目录不存在时的空载荷。 */
259
+ export function emptyBridgePayload() {
260
+ return {
261
+ records: [],
262
+ stats: { total: 0, rollbackable: 0 },
263
+ };
264
+ }
265
+ /**
266
+ * 从桥接存储层取原始记录(按 at 降序,只读零写盘)。
267
+ * 契约来源:M2-A,`listBridgeApplications(journalDir): BridgeApplicationView[]`;
268
+ * 未落地 / 未导出时安全返回空,路由不受影响。
269
+ */
270
+ function readBridgeApplicationsFromStore(journalDir) {
271
+ const candidate = bridgeModule.listBridgeApplications;
272
+ if (typeof candidate !== 'function')
273
+ return [];
274
+ const result = candidate(journalDir);
275
+ return Array.isArray(result) ? result : [];
276
+ }
175
277
  /** 组装梦境日记路由处理器;journalDir 在这一刻定下来(与插件的配置生命周期一致)。 */
176
278
  export function createDreamWebHandler(options = {}) {
177
279
  const journalDir = journalDirOf(options);
@@ -191,6 +293,39 @@ export function createDreamWebHandler(options = {}) {
191
293
  }
192
294
  };
193
295
  }
296
+ /** 组装只读写入记录路由处理器;零写盘:目录不存在就直接返回空结果。 */
297
+ export function createBridgeWebHandler(options = {}) {
298
+ const journalDir = journalDirOf(options);
299
+ const bridgeDir = bridgeDirOf(options, journalDir);
300
+ return async (req, res) => {
301
+ if (requestGate(req, res, 'dsh-dream bridge route is read-only; use GET'))
302
+ return;
303
+ try {
304
+ let raw;
305
+ if (options.bridgeStoreFactory !== undefined) {
306
+ raw = options.bridgeStoreFactory(bridgeDir).listApplications();
307
+ }
308
+ else if (existsSync(bridgeDir)) {
309
+ raw = readBridgeApplicationsFromStore(journalDir);
310
+ }
311
+ else {
312
+ raw = [];
313
+ }
314
+ const records = normalizeBridgeApplications(raw).map((record) => maskDeep(record));
315
+ const payload = {
316
+ records,
317
+ stats: {
318
+ total: records.length,
319
+ rollbackable: records.filter((record) => record.rollbackable).length,
320
+ },
321
+ };
322
+ responseJson(res, 200, payload);
323
+ }
324
+ catch (error) {
325
+ internalError(res, error);
326
+ }
327
+ };
328
+ }
194
329
  /** 组装只读经验路由处理器;零写盘:目录不存在就直接返回空结果。 */
195
330
  export function createKnowledgeWebHandler(options = {}) {
196
331
  const knowledgeDir = knowledgeDirOf(options);
@@ -227,7 +362,7 @@ export function createKnowledgeWebHandler(options = {}) {
227
362
  /**
228
363
  * 把只读路由挂到宿主 webserver 上。
229
364
  * 与 dsh-calendar 相同:ctx.inject(['webServer']) + effect 注册,插件卸载即摘掉路由;
230
- * 同一个 effect 里注册日记与知识两条 exact 路由,卸载时一起释放。
365
+ * 同一个 effect 里注册日记 / 知识 / 写入记录三条 exact 路由,卸载时一起释放。
231
366
  */
232
367
  export function installDreamWeb(ctx, options = {}) {
233
368
  if (ctx === null || ctx === undefined || typeof ctx.inject !== 'function') {
@@ -235,6 +370,7 @@ export function installDreamWeb(ctx, options = {}) {
235
370
  }
236
371
  const journalHandler = createDreamWebHandler(options);
237
372
  const knowledgeHandler = createKnowledgeWebHandler(options);
373
+ const bridgeHandler = createBridgeWebHandler(options);
238
374
  ctx.inject(['webServer'], (webCtx) => {
239
375
  webCtx.effect(() => {
240
376
  const disposers = [
@@ -248,6 +384,11 @@ export function installDreamWeb(ctx, options = {}) {
248
384
  path: DREAM_KNOWLEDGE_ROUTE,
249
385
  handler: (req, res) => knowledgeHandler(req, res),
250
386
  }, 'dsh-dream: knowledge route'),
387
+ webCtx.webServer.register({
388
+ kind: 'exact',
389
+ path: DREAM_BRIDGE_ROUTE,
390
+ handler: (req, res) => bridgeHandler(req, res),
391
+ }, 'dsh-dream: bridge log route'),
251
392
  ];
252
393
  return () => {
253
394
  for (const dispose of disposers) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stardustlc/dsh-dream",
3
- "version": "0.5.0",
4
- "description": "DSH 做梦插件:会话间隙回放近期对话、反思写入梦境日记,并把高频教训幂等桥接进 AGENTS.md;默认隐私脱敏,零运行时依赖。",
3
+ "version": "0.6.0",
4
+ "description": "DSH 做梦与经验记忆:核验来源、审阅经验,预览写入 AGENTS.md 并支持回滚;附梦境日记面板,默认隐私脱敏,零运行时依赖。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
@@ -20,10 +20,10 @@ description: 做梦协议:任务开始取回适用经验,长任务收尾沉
20
20
 
21
21
  ## 一、任务前:取回上下文(dream_context)
22
22
 
23
- - 任务涉及过往代码、用户偏好或踩过的坑时,先调 `dream_context`:给 `query`,能给就给 `projectId` / `workspaceRoot` / `packageVersion`。
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
+ - 所有新经验都先进入 candidate,包括已读到的用户纠正。read 只表示插件成功核对来源,不表示用户采纳;工具审阅记录 actor:model,不能冒充真人。会话证据须给 sessionId、recordSeq、可匹配 quote(缺省使用 summary);本地产物须给工作目录内的 artifactPath。插件独立核对项目、角色和原文,计算 sourceHash;失败降为 claimed 并给 verificationReason。
42
44
  - 用户明确采纳/驳回、发现经验互相冲突、依赖升级后旧经验需要复核 → 用 `dream_review`(必须带 `expectedRevision`)。
43
45
  - 用户采纳必须来自真实用户指令;模型自己不能冒充 `actor: user`。
44
46
  - 同一证据重复提交是幂等的,不会增加独立支持数。**禁止靠反复调用 `dream_save` 或 `dream_learn` 刷次数**——次数不等于正确,也不等于重要。
@@ -47,7 +49,8 @@ description: 做梦协议:任务开始取回适用经验,长任务收尾沉
47
49
 
48
50
  - 只沉淀规律与偏好,不复述流水账;`reflection` 用第一人称写你的观察与感受;
49
51
  - `lessons` 是 1–5 条可执行教训,每条以动词开头;拿不准就标注「存疑」;
50
- - **不得把主观反思升级成 AGENTS.md 指令**:不要用 `dream_bridge` 把未经审阅的感悟写成项目规则。桥接只能用于已采纳、当前项目范围、附证据的经验。
52
+ - **不得把主观反思升级成 AGENTS.md 指令**:不要用 `dream_bridge` 把未经审阅的感悟写成项目规则。桥接只能用于 `usable`、当前项目范围(或显式 global 且 review.decision 为 accepted)、附证据的经验;频次不再作为准入条件。
53
+ - **桥接三步(M2)**:`mode:"preview"` 只读返回 diff / 参与经验 / skipped 原因(零写入)→ 人工复核 → `mode:"apply"` 必须带预览的 `expectedSha256`(新文件为 null),建议同时带 `previewId`(文件被并发修改则 `conflict` 且零写入);`mode:"rollback"` 只在当前管理块仍等于该次 apply 结果时回滚。缺省不传 `mode` 仍是 0.5.x 直写并返回弃用提示;新流程一律显式 preview → apply。写入内容统一过 `mask.ts`。
51
54
 
52
55
  ## 何时做梦
53
56
 
@@ -60,6 +63,8 @@ description: 做梦协议:任务开始取回适用经验,长任务收尾沉
60
63
  - 想在日记里找原话 → `dream_recall`;
61
64
  - 想取少量可复用经验 → `dream_context`(带范围与预算,只读);
62
65
  - 想查看/审阅候选、处理冲突 → `dream_review`;
66
+ - attach-evidence 只补充核验后的证据,不自动采纳;resolve-conflict 必须给 resolution、affectedIds、note,整组原子更新;reopen 将驳回经验重新送审。
67
+
63
68
  - 面板查看:「设置 → 梦境日记」里有只读的梦境时间线与「经验」区块(状态、适用条件、证据摘要、最近核验时间),面板不提供任何写操作。
64
69
 
65
70
  ---
@@ -81,10 +86,10 @@ Dreams and **lessons** are two separate pipelines: a **first-person reflection**
81
86
 
82
87
  ## 1. Before the task: retrieve context (dream_context)
83
88
 
84
- - When a task touches past code, user preferences or known pitfalls, call `dream_context` first with `query`, plus `projectId` / `workspaceRoot` / `packageVersion` when available.
89
+ - 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
90
  - **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
91
  - `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; it never scans across projects. Candidates are not injected as task advice by default.
92
+ - 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
93
 
89
94
  ## 2. During the task: do the real work first
90
95
 
@@ -99,7 +104,9 @@ Dreams and **lessons** are two separate pipelines: a **first-person reflection**
99
104
  - fill `sessionId` / `recordSeq` when the source can be located;
100
105
  - keep `summary` to one masked sentence — never copy the raw record;
101
106
  - `verification`: `read` = the record was actually read; `claimed` = the model merely asserts it. **Claimed evidence does not count toward independent support.**
107
+ - **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
108
  - An observation without evidence stays a candidate: do not present it as a fact, and never call it "verified".
109
+ - Every new lesson starts as a candidate, including verified user corrections. Read means the plugin checked the source, not that a user accepted the lesson; tool review records actor:model. Session evidence needs sessionId, recordSeq and a matching quote (summary if omitted). Artifacts need artifactPath inside the working directory. The plugin checks project, role and text and computes sourceHash; failures become claimed with verificationReason.
103
110
  - For explicit acceptance/rejection, conflicts between lessons, or re-validation after dependency upgrades → call `dream_review` with an `expectedRevision`.
104
111
  - User acceptance must come from a real user instruction; the model must not impersonate `actor: user`.
105
112
  - 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.
@@ -108,7 +115,8 @@ Dreams and **lessons** are two separate pipelines: a **first-person reflection**
108
115
 
109
116
  - Consolidate patterns and preferences only — never a running transcript. Write `reflection` in the first person.
110
117
  - `lessons` are 1–5 actionable items; mark uncertain ones as "tentative".
111
- - **Never promote a subjective reflection into an AGENTS.md instruction**: do not use `dream_bridge` to write unreviewed reflections into project rules. Bridging is for accepted, project-scoped, evidence-backed lessons only.
118
+ - **Never promote a subjective reflection into an AGENTS.md instruction**: do not use `dream_bridge` to write unreviewed reflections into project rules. Bridging is for `usable`, project-scoped (or explicit global with review.decision accepted), evidence-backed lessons only; frequency is no longer an admission criterion.
119
+ - **Bridge in three steps (M2)**: `mode:"preview"` returns the diff / selected lessons / skipped reasons read-only (zero writes) → human review → `mode:"apply"` with the preview’s required `expectedSha256` (null for a new file); include `previewId` to bind the preview (a concurrent edit yields `conflict` and zero writes); `mode:"rollback"` only rolls back while the current managed block still equals that apply result. Omitting `mode` keeps the 0.5.x direct write and returns a deprecation notice; new flows should always use preview → apply. Everything written is masked via `mask.ts`.
112
120
 
113
121
  ## When to dream
114
122
 
@@ -121,4 +129,5 @@ Dreams and **lessons** are two separate pipelines: a **first-person reflection**
121
129
  - Looking for exact words in the journal → `dream_recall`;
122
130
  - Retrieving a few reusable lessons → `dream_context` (scoped, budgeted, read-only);
123
131
  - Browsing or reviewing candidates and conflicts → `dream_review`;
132
+ - attach-evidence adds verified sources without promoting state. resolve-conflict requires resolution, affectedIds and note and updates the group atomically. reopen sends a rejected lesson back for review.
124
133
  - To browse: **Settings → Dream journal** shows the read-only dream timeline and a **Lessons** block (state, applicability, evidence summary, last validated time). The panel has no write actions.